/* ============================================================
   SIMULATION 7749 — interface
   Everything is dark, monospaced and deliberately instrument-like.
   ============================================================ */

:root {
  --ink: #e8f4ff;
  --dim: #6d829a;
  --line: rgba(120, 190, 255, 0.18);
  --panel: rgba(6, 12, 22, 0.72);
  --accent: #59e0ff;
  --accent-2: #ff5d8f;
  --warn: #ff4d6d;
  --safe-t: env(safe-area-inset-top, 0px);
  /* Non-notch phones report 0 but still sit under a translucent status bar,
     so the top furniture uses a floored value. */
  --hud-t: max(env(safe-area-inset-top, 0px), 20px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --bar-h: 64px;
  /*
   * The ladder's band. `--rail-t` is the top bar's own top plus its 44 of
   * height and 4 of air -- the bar is as tall as the menu button in it, and
   * the rail was written against the 32 the chips used to be, which put it
   * 12px inside the button. `--rail-h` is 38 of row and 6 of air under it.
   * Everything that used to be measured from the top bar is measured from
   * the bottom of this.
   *
   * `--boss-h` is the space kept for the boss bar. It used to be kept always
   * -- the alerts column sat 64px below a slot that is empty for most of
   * every run -- and with the rail above it as well there was no room left
   * on a 568 screen for even one pill. Both are conditional now: the rail is
   * not up during a fight and the reservation is not made outside one, so
   * the column below has the same room it always had, either way.
   *
   * ---- build 292: every one of these is smaller ------------------------
   *
   * Measured at 320x568 before the change, which is the size the furniture
   * actually competes for room at: the field -- the band of canvas between
   * the bottom of the rail and the top of the tallest column in the strip --
   * was 167px of 568, TWENTY-NINE PER CENT of the screen. The rest was
   * furniture over a game whose whole subject is what is on the field.
   *
   * `--bar-h` 74 -> 64 gives ten of that back twice over: the ability bar is
   * shorter AND `world.floorY` is derived from the same three numbers, so
   * the floor line drops with it.
   *
   * Build 292 took the rail's own band from 52 reserved over 44 measured down
   * to 48, which was the slack; the ROW was not, and shaving it to 38 broke a
   * tap target the suite has asserted since the rail went in.
   *
   * BUILD 295 REMOVED THE BAND. The rail's readout is in the top bar and its
   * controls are in the wave sheet, so there is no second row: `--rail-t` is
   * the top bar's own bottom -- its 8px inset plus 44 of row plus 4 of air --
   * and `--rail-h` is zero. Both names are kept because `--under-rail` is
   * derived from them and five things below read it; renaming would be churn
   * in six files for no change of value.
   */
  --rail-t: calc(var(--hud-t) + 56px);
  --rail-h: 0px;
  --boss-h: 0px;
  --under-rail: calc(var(--rail-t) + var(--rail-h));
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%;
  width: 100%;
  overflow: hidden;
  background: #04050a;
  color: var(--ink);
  font-family: var(--mono);
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior: none;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

#app {
  position: fixed;
  inset: 0;
  overflow: hidden;
}

#safeProbe {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  /* ...and where the chrome ENDS, resolved the same way: `Game.resize` reads
     this margin to stand the portal clear of the top bar. A calc() of var()s
     is not readable as a variable, and #topbar's own box is all zeros while
     the title screen is up, which is when the constructor first asks. */
  margin-top: var(--under-rail);
  visibility: hidden;
  pointer-events: none;
}

#stage {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
}

#ui {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 10;
}

#ui button { pointer-events: auto; font-family: var(--mono); }

/*
 * Before the interface exists.
 *
 * The title screen is in the markup, but the controls list under it and the
 * build stamp under that are written by Hud — so a phone painted a complete,
 * wrong title screen for however long the modules took to load, then filled in
 * two more blocks under it. This holds the whole overlay back until Hud takes
 * the class off. The canvas underneath is already the right black, so what is
 * on screen in the meantime is the game's own background and nothing else.
 *
 * The animation is the failsafe. If the modules never arrive the class is
 * never removed, and a title screen nobody can see is worse than an incomplete
 * one — so it reveals itself after four seconds on its own. When Hud does its
 * job the class goes long before that and the animation never runs.
 */
:root.preboot #ui { animation: preboot 4s step-end both; }
@keyframes preboot { 0% { opacity: 0; } 100% { opacity: 1; } }
#ui { transition: opacity 0.28s ease-out; }

/* ---------------- top bar ---------------- */

#topbar {
  position: absolute;
  top: calc(var(--hud-t) + 8px);
  left: calc(var(--safe-l) + 8px);
  right: calc(var(--safe-r) + 8px);
  display: flex;
  align-items: center;
  gap: 5px;
}

/*
 * The readouts give, and the door does not. Anything that will not fit is
 * clipped off the right of this box -- which is the phase tag, the last and
 * least of the three -- instead of being pushed onto the menu button.
 */
#barChips {
  /* Content-sized from build 295: the RAIL is what grows to fill this row now,
     and a purse taking the slack would squeeze the nodes as it climbed. It may
     still shrink -- `tight`/`tighter` drop labels from the inside -- but it no
     longer claims room the ladder needs. */
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 5px;
  overflow: hidden;
}
#menuBtn { flex: 0 0 auto; margin-left: auto; }

.chip {
  position: relative;
  font-size: 12px;
  letter-spacing: 0.07em;
  padding: 8px 9px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  line-height: 1;
  white-space: nowrap;
}

.chip em {
  font-style: normal;
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 0.18em;
  margin-left: 6px;
}

/*
 * Banked salvage. It sits beside the count because for the whole first run
 * they are two halves of the same statement: what the simulation is taking,
 * and what is left over.
 */
#bytesChip {
  display: flex;
  align-items: baseline;
  gap: 5px;
  transition: opacity 0.4s, border-color 0.4s;
}
#bytesChip em {
  font-style: normal;
  font-size: 11px;
  letter-spacing: 0.18em;
  color: var(--dim);
  margin: 0;
}
#bytesNum {
  font-size: 13px;
  font-weight: 700;
  color: #7cffb2;
  font-variant-numeric: tabular-nums;
}
/* Something is sitting on the intake. The only place corruption costs a number. */
#bytesChip.choked {
  border-color: rgba(255, 77, 109, 0.4);
  opacity: 0.72;
}
#bytesChip.choked #bytesNum { color: #9fb3c8; }
/*
 * ...and the other direction: the depth dividend is live, so the label slot is
 * carrying a multiplier instead of the word ENERGY. Deliberately quieter than
 * `choked` -- corruption on the intake is a thing to look at, and this is a
 * standing fact about how deep the run is, which should not blink for hours.
 */
#bytesChip.rich em {
  color: var(--accent);
  opacity: 0.95;
  font-variant-numeric: tabular-nums;
}

#counter #killNum {
  font-size: 14px;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

/*
 * The counter after ORDINAL takes it over. Same chip, same place, wrong
 * colour — the number the player watched climb for the whole run is now
 * somebody else's and it is going down.
 */
#counter.ledger { border-color: rgba(255, 45, 111, 0.4); }
#counter.ledger #killNum { color: var(--warn); }
#counter.ledger em { color: rgba(255, 45, 111, 0.75); }
#counter.spent #killNum,
#counter.spent em { animation: spentPulse 1.4s infinite ease-in-out; }
@keyframes spentPulse { 50% { opacity: 0.35; } }

.chip.ghost { color: var(--dim); border-color: rgba(120, 190, 255, 0.1); }
.chip.btn { -webkit-appearance: none; appearance: none; cursor: pointer; }
.chip.btn:active { background: rgba(89, 224, 255, 0.2); }
.chip.btn.off { color: #45566a; text-decoration: line-through; }

/*
 * The other purse. Sits in the tree's header beside the energy, and is only
 * there at all once ORDINAL has left one.
 */
.treeSouls {
  font-size: 9px;
  letter-spacing: 0.18em;
  color: #ffb8ee;
  border: 1px solid rgba(255, 94, 200, 0.5);
  border-radius: 3px;
  padding: 2px 7px;
  margin-left: auto;
  margin-right: 8px;
  text-shadow: 0 0 10px rgba(255, 47, 158, 0.6);
}
.treeSouls[hidden] { display: none; }

/*
 * The energy chip is a button now — the way into the tree. The badge is how
 * many rows are within reach, which is the one figure that decides whether
 * opening it is worth the tap.
 */
#bytesChip { cursor: pointer; -webkit-tap-highlight-color: transparent; }
#bytesChip:active { transform: translateY(1px); }
#bytesChip.canBuy {
  border-color: rgba(124, 255, 178, 0.5);
  box-shadow: 0 0 18px -6px rgba(124, 255, 178, 0.8);
}
#bytesBuys {
  display: none;
  margin-left: 6px;
  min-width: 16px;
  padding: 1px 4px;
  /* A number the player reads before every trip to the shop. It was 8px. */
  font-size: 11px;
  font-style: normal;
  letter-spacing: 0.04em;
  text-align: center;
  color: #04140c;
  background: #7cffb2;
  border-radius: 2px;
  box-shadow: 0 0 10px -2px rgba(124, 255, 178, 0.9);
}
#bytesBuys.on { display: inline-block; }

/*
 * The record and the resume detail are styled with the rest of the title
 * screen, further down this file -- see `.bootStatus` and what follows it.
 * They had a block of their own up here, beside the energy chip, from when
 * the record was a row of inline chips rather than three tiles.
 *
 * Both are worth having and neither is worth the fold. On a 320x568 screen
 * the wordmark plus the control rows already ends near the bottom, and the
 * one button on this screen has to be reachable without a scroll -- so the
 * tiles go on a short screen and the detail line, which is one line and is
 * the whole basis of pressing CONTINUE, stays.
 */
@media (max-height: 660px) {
  #bootRecord { display: none !important; }
}

/* ---------------- options ---------------- */

/*
 * One row per preference, tapped to step through its values. A pip strip
 * rather than a switch, because three of these have three states and a switch
 * cannot say "medium" — the same reason the volume control is a bar of steps
 * and not a checkbox.
 */
.optRows { display: flex; flex-direction: column; gap: 4px; margin: 8px 0 10px; }
.optRow {
  -webkit-appearance: none;
  appearance: none;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  font: inherit;
  color: var(--ink);
  border: 1px solid var(--line);
  background: rgba(6, 12, 22, 0.66);
  clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 6px 100%, 0 calc(100% - 6px));
  transition: border-color 0.14s, opacity 0.14s;
}
.optRow:active { transform: translateY(1px); }
.optName { flex: 1 1 auto; font-size: 9px; letter-spacing: 0.18em; color: #8fb6d8; text-align: left; }
.optPips { flex: 0 0 auto; display: flex; gap: 3px; }
.optPips i {
  display: block;
  width: 12px;
  height: 4px;
  background: rgba(120, 190, 255, 0.16);
  transition: background 0.14s, box-shadow 0.14s;
}
.optPips i.on { background: var(--accent); box-shadow: 0 0 8px -1px var(--accent); }
.optWord {
  flex: 0 0 auto;
  min-width: 56px;
  text-align: right;
  font-size: 9px;
  letter-spacing: 0.14em;
  color: var(--accent);
}
/* Off is a state worth reading as off, not as "the first of three". */
.optRow.off .optName, .optRow.off .optWord { color: #4d5f74; }
.optRow.off .optPips i.on { background: rgba(120, 190, 255, 0.22); box-shadow: none; }

/* ---------------- the way in ---------------- */

/*
 * A control, not a status line. It sits where the boss bar will be, so the
 * banner is replaced in place by the thing it summoned rather than moving
 * something else around when the fight starts.
 */
/*
 * The container is a list -- one row per boss whose way in is held -- and
 * every colour in a row comes off `--tone` and `--glow`, which Hud
 * .syncApertures() sets per row from the anomaly table. It was one button in
 * one magenta because there was one boss.
 */
#apertureBar {
  position: absolute;
  /* Below the rail -- see --rail-t for what the offset is made of. */
  top: var(--under-rail);
  left: calc(var(--safe-l) + 10px);
  right: calc(var(--safe-r) + 10px);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
#apertureBar[hidden] { display: none; }
.apRow {
  --tone: #ff5ec8;
  --glow: #ff2f9e;
  -webkit-appearance: none;
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 8px 12px;
  font: inherit;
  color: color-mix(in srgb, var(--tone) 26%, #ffffff);
  /* Both stops off the boss's own glow. The right one was a fixed dark
     magenta, which left every other boss's row fading into ORDINAL's colour;
     the mixes below land within a few units of the authored magenta when the
     glow *is* ORDINAL's. */
  background:
    linear-gradient(90deg,
      color-mix(in srgb, var(--glow) 22%, rgba(2, 2, 6, 0.95)),
      color-mix(in srgb, var(--glow) 9%, rgba(2, 2, 6, 0.9)));
  border: 1px solid color-mix(in srgb, var(--tone) 55%, transparent);
  border-radius: 4px;
  box-shadow: 0 0 22px color-mix(in srgb, var(--glow) 28%, transparent),
    inset 0 0 18px color-mix(in srgb, var(--glow) 12%, transparent);
  animation: apPulse 2.4s ease-in-out infinite;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.apRow:active { transform: translateY(1px); }
.apName { font-size: 10px; letter-spacing: 0.24em; }
.apGo {
  font-size: 9px;
  letter-spacing: 0.2em;
  color: color-mix(in srgb, var(--tone) 62%, #ffffff);
  border: 1px solid color-mix(in srgb, var(--tone) 60%, transparent);
  border-radius: 3px;
  padding: 3px 8px;
}
@keyframes apPulse {
  0%, 100% {
    box-shadow: 0 0 18px color-mix(in srgb, var(--glow) 22%, transparent),
      inset 0 0 14px color-mix(in srgb, var(--glow) 10%, transparent);
  }
  50% {
    box-shadow: 0 0 34px color-mix(in srgb, var(--glow) 50%, transparent),
      inset 0 0 22px color-mix(in srgb, var(--glow) 20%, transparent);
  }
}

/* ---------------- boss bar ---------------- */

/*
 * A gauge on a machine, not a progress bar.
 *
 * It was a 6px track with a gradient in it and two hairlines underneath, which
 * says "something has health" and nothing else. Three things are worth reading
 * off it during a fight and none of them were legible: how hard the last hit
 * landed, how much of each frame is still standing, and how far it is to the
 * stage that changes the problem.
 */

#bossBar {
  position: absolute;
  /* Below the rail, alongside #apertureBar -- the two never show at once. */
  top: var(--under-rail);
  left: calc(var(--safe-l) + 10px);
  right: calc(var(--safe-r) + 10px);
  --boss: #ff5ec8;
  --bossLit: #ffb8ee;
}
#bossBar[hidden] { display: none; }

.bossName {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 10px;
  letter-spacing: 0.22em;
  margin-bottom: 4px;
  text-shadow: 0 0 12px rgba(0, 0, 0, 0.9);
}
.bossName #bossTitle { color: var(--bossLit); }
.bossName span:last-child {
  color: var(--boss);
  font-size: 8px;
  letter-spacing: 0.18em;
  padding: 2px 6px;
  border: 1px solid color-mix(in srgb, var(--boss) 45%, transparent);
  background: rgba(10, 2, 10, 0.7);
}

/* ---- the core ---- */

.bossCore {
  position: relative;
  height: 11px;
  border: 1px solid color-mix(in srgb, var(--boss) 40%, transparent);
  background: rgba(2, 6, 12, 0.75);
  box-shadow: inset 0 0 12px -4px var(--boss);
  clip-path: polygon(0 0, calc(100% - 4px) 0, 100% 4px, 100% 100%, 4px 100%, 0 calc(100% - 4px));
  overflow: hidden;
}
.bossCore i {
  position: absolute;
  inset: 0;
  display: block;
  transform-origin: left center;
}
/*
 * The ghost. It holds where the health *was* and catches up a beat later, so
 * a big hit is visible as a hit — a bar that only ever gets shorter says
 * nothing about how it got there.
 */
.bossGhost {
  background: rgba(255, 255, 255, 0.55);
  transition: transform 0.5s cubic-bezier(0.4, 0, 1, 1) 0.18s;
}
.bossFill {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--boss) 55%, #000), var(--boss) 55%, var(--bossLit));
  box-shadow: 0 0 14px -1px var(--boss);
  transition: transform 0.12s linear;
}
/* Notches, so it reads as an instrument and a tenth is a countable thing. */
.bossNotch {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(90deg,
    rgba(0, 0, 0, 0) 0 calc(10% - 1px), rgba(2, 6, 12, 0.85) calc(10% - 1px) 10%);
}
/*
 * Where the next stage begins, on the track it begins at. The fight's shape
 * is three thresholds on one number and this is the only place that says so.
 */
.bossMark {
  position: absolute;
  top: -2px;
  bottom: -2px;
  width: 2px;
  margin-left: -1px;
  background: rgba(255, 255, 255, 0.75);
  box-shadow: 0 0 6px rgba(255, 255, 255, 0.7);
}
.bossMark.past { background: rgba(255, 255, 255, 0.2); box-shadow: none; }

/* While it is still coming through there is no health to report. */
.bossCore.arriving .bossFill,
.bossCore.arriving .bossGhost { display: none; }
.bossCore.arriving {
  background:
    repeating-linear-gradient(115deg,
      color-mix(in srgb, var(--boss) 34%, transparent) 0 10px,
      rgba(2, 6, 12, 0.75) 10px 20px);
  background-size: 200% 100%;
  animation: bossArrive 1.1s linear infinite;
}
@keyframes bossArrive { to { background-position: -40px 0; } }

/* ---- the frames ---- */

.bossShell { display: flex; gap: 8px; margin-top: 4px; }
.bossShellRow { flex: 1; display: flex; align-items: center; gap: 5px; }
.bossShellRow em {
  font-style: normal;
  font-size: 6.5px;
  letter-spacing: 0.16em;
  color: color-mix(in srgb, var(--boss) 70%, #6d829a);
}
/*
 * Notched at the real segment count — twenty-four outer, sixteen inner — so
 * the meter is not an abstraction of the frame, it is a picture of it, and
 * one tick going out is one panel going out.
 */
.bossShellTrack {
  position: relative;
  flex: 1;
  height: 4px;
  background: rgba(255, 255, 255, 0.07);
  overflow: hidden;
}
.bossShellTrack::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(90deg,
    rgba(0, 0, 0, 0) 0 calc((100% / var(--seg)) - 1px),
    rgba(2, 6, 12, 0.9) calc((100% / var(--seg)) - 1px) calc(100% / var(--seg)));
}
.bossShellTrack i {
  display: block;
  height: 100%;
  width: 100%;
  transform-origin: left center;
  background: var(--bossLit);
  box-shadow: 0 0 8px var(--boss);
  transition: transform 0.14s linear;
}

/* ---------------- alerts (breach / boss powers) ---------------- */

/*
 * Transient messages only. There used to be a second row of sticky status
 * pills below this one describing everything the boss was doing; between the
 * two of them they covered the boss completely, and every state they named was
 * already visible on screen. They are gone: nothing here explains a mechanic.
 */
#alerts {
  position: absolute;
  left: 0;
  right: 0;
  /* ...and below the boss bar, when there is one. */
  top: calc(var(--under-rail) + var(--boss-h));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.alert {
  /*
   * Never wider than the screen. A long line used to run off both edges with
   * its padding outside the viewport — REMAINDER RECOVERED did exactly that —
   * and an alert that cannot be read is not an alert.
   */
  max-width: calc(100vw - var(--safe-l) - var(--safe-r) - 28px);
  text-align: center;
  font-size: 12px;
  letter-spacing: 0.2em;
  padding: 6px 13px;
  border-radius: 3px;
  border: 1px solid currentColor;
  background: rgba(6, 10, 18, 0.66);
  animation: alertIn 0.18s ease-out;
}
.alert.breach { color: var(--warn); }
/*
 * The simulation halted. The one thing in the game that is an END rather than
 * an event, so it is allowed the weight of a REMAINDER and the colour of a
 * warning -- and it is the same `--warn` the breach pill already wears rather
 * than a new hue, because that colour is measured and a fresh one would be a
 * fresh contrast claim. Composited, `#ff4d6d` on this plate over the field is
 * about 5.4:1, which the suite's own sweep re-measures.
 */
.alert.halt {
  color: var(--warn);
  font-size: 11px;
  letter-spacing: 0.22em;
  padding: 8px 17px;
  background: rgba(22, 5, 10, 0.92);
  box-shadow: 0 0 26px -4px currentColor, inset 0 0 18px -8px currentColor;
  animation: alertIn 0.18s ease-out, rigDone 1.2s ease-out;
}
.alert.power { color: #ffcf5c; }
.alert.info { color: var(--accent); }
/*
 * A fitting going on to the turret, and the moment the last one does. The
 * second is the only thing in a run that is ever finished, so it is allowed to
 * be louder than anything else in the stack.
 */
.alert.rig { color: #9fe8ff; letter-spacing: 0.26em; }
.alert.rigDone {
  color: #7cffb2;
  font-size: 11px;
  letter-spacing: 0.2em;
  padding: 7px 16px;
  background: rgba(6, 14, 12, 0.9);
  box-shadow: 0 0 22px -4px currentColor, inset 0 0 16px -8px currentColor;
  animation: alertIn 0.18s ease-out, rigDone 1.1s ease-out;
}
@keyframes rigDone {
  0% { transform: scale(1.08); filter: brightness(2.2); }
  100% { transform: none; filter: none; }
}

/*
 * A REMAINDER coming in. The rarest line the interface ever shows -- one per
 * ORDINAL and the only currency there is a second of -- so it gets the widest
 * letters, the longest hold and a colour nothing else uses.
 */
.alert.remainder {
  color: #ffb8ee;
  font-size: 11px;
  letter-spacing: 0.24em;
  padding: 8px 18px;
  background: rgba(24, 4, 20, 0.92);
  border-color: rgba(255, 94, 200, 0.6);
  box-shadow: 0 0 30px -4px currentColor, inset 0 0 20px -8px currentColor;
  animation: alertIn 0.18s ease-out, rigDone 1.4s ease-out;
}

/*
 * An object recorded for the first time. Wider letters and a lit edge, and the
 * colour comes inline from the object itself — the one alert that is about a
 * specific thing on the field rather than about the run.
 */
.alert.found {
  font-size: 10.5px;
  letter-spacing: 0.28em;
  padding: 5px 14px;
  background: rgba(6, 10, 18, 0.82);
  box-shadow: 0 0 14px -4px currentColor, inset 0 0 12px -8px currentColor;
}
/*
 * ORDINAL's arrival captions. Centred over the field, one line at a time,
 * away from the pill stack — the entrance is the one moment the interface is
 * supposed to go quiet.
 */
#bossCaption {
  /*
   * In the alert column, not floating over it — see the note in index.html.
   * Nothing here positions it; the column does that, and the column cannot
   * overlap itself.
   */
  align-self: stretch;
  padding: 0 22px;
  text-align: center;
  font-size: 13px;
  line-height: 1.7;
  letter-spacing: 0.22em;
  color: #f3e6ff;
  text-shadow: 0 0 22px rgba(0, 0, 0, 0.95), 0 0 40px rgba(230, 179, 255, 0.5);
  opacity: 0;
  pointer-events: none;
  /* Fast enough that a short line is readable almost as soon as it is up: at
     0.9s a 3.4-second caption spent a quarter of its life arriving. */
  transition: opacity 0.4s ease;
}
/* Out of the flow entirely when it is not talking, or an empty caption holds
   a gap in the column and the pills sit lower than they should. */
#bossCaption:not(.show) {
  height: 0;
  overflow: hidden;
  margin: 0;
}
#bossCaption.show {
  opacity: 1;
  /* Clear of the pill under it even while captionIn is still running: at
     6px the two boxes touched by a pixel mid-animation. */
  margin-bottom: 12px;
  animation: captionIn 1s ease-out;
}
@keyframes captionIn {
  from { opacity: 0; transform: translateY(10px); filter: blur(7px); letter-spacing: 0.5em; }
  to { opacity: 1; transform: none; filter: none; }
}

/*
 * Not bought yet. Visible on purpose — the shape of what a turret can become
 * is part of what the offers are for — but it has to read as scenery rather
 * than as a dim button. Fully grey against neighbours that now carry colour,
 * and hatched, which says "not a control" without depending on the border:
 * the cells are clip-pathed, so a dashed outline breaks at the cut corner.
 */
.ab.sealed,
.qc.sealed {
  opacity: 0.4;
  filter: grayscale(1) brightness(0.62);
  border-color: rgba(120, 160, 200, 0.13);
  cursor: default;
}
.ab.sealed::after,
.qc.sealed::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    -45deg,
    rgba(150, 180, 210, 0.07) 0 2px,
    transparent 2px 6px);
}
.ab.sealed .fill { display: none; }
.qc.sealed .qLbl { opacity: 0.75; }

/*
 * The moment one opens. The offer sheet closes over the whole screen, so
 * without this the only sign of which of fourteen things just became yours was
 * a cell somewhere going from grey to not-grey while you were looking at the
 * card. It blooms once and then keeps a glow for a few seconds, long enough to
 * be found after the sheet is gone.
 */
.qc.justOpened, .ab.justOpened {
  animation: justOpened 5.5s ease-out;
  z-index: 2;
}
@keyframes justOpened {
  0% { box-shadow: 0 0 0 0 currentColor; filter: brightness(2.6); }
  8% { box-shadow: 0 0 22px 6px currentColor; filter: brightness(2.2); }
  22% { box-shadow: 0 0 14px 1px currentColor; filter: none; }
  36% { box-shadow: 0 0 22px 4px currentColor; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* A press that was refused. Small, and over quickly: it says "not this",
   not "you did something wrong". */
.ab.refused, .qc.refused { animation: refused 0.26s ease-out; }
@keyframes refused {
  0%, 100% { transform: none; }
  30% { transform: translateX(-2px); }
  70% { transform: translateX(2px); }
}

/* A row in the ARSENAL sheet for something not bought yet. */
.armRow.sealed { opacity: 0.4; filter: grayscale(1) brightness(0.7); }
.armRow.sealed .codexName::after {
  content: ' — locked';
  font-size: 0.82em;
  letter-spacing: 0.1em;
  color: var(--dim);
}

.alert.fade { opacity: 0.55; }

@keyframes alertIn {
  from { transform: translateY(-6px) scaleY(0.6); opacity: 0; }
  to { transform: none; opacity: 1; }
}


/* ---------------- menu ---------------- */

/*
 * A sheet over the bottom of the screen, where the thumb already is. The
 * simulation holds while it is open, so a loadout change mid-wave costs
 * nothing. Everything inside is built from data in src/menu.js.
 */


/* The main way into the menu was a 31x25 target on a screen full of 44s. */
.chip.menuBtn { padding: 11px 12px; }
.chip.menuBtn svg { width: 20px; height: 20px; display: block; }
.chip.menuBtn.on { border-color: var(--accent); color: var(--accent); }

/* A new object went into the record. Said without a word. */
.chip.menuBtn.recorded { animation: recorded 1.5s ease-out; }

@keyframes recorded {
  0% { color: var(--ink); border-color: var(--accent); box-shadow: 0 0 0 0 rgba(89, 224, 255, 0.85); }
  18% { color: var(--accent); box-shadow: 0 0 0 7px rgba(89, 224, 255, 0); }
  40% { color: var(--accent); box-shadow: 0 0 0 0 rgba(89, 224, 255, 0.6); }
  60% { box-shadow: 0 0 0 7px rgba(89, 224, 255, 0); }
  100% { color: var(--ink); }
}

#menuScrim {
  position: absolute;
  inset: 0;
  background: rgba(2, 4, 9, 0.55);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease;
  z-index: 24;
}
#menuScrim.on { opacity: 1; pointer-events: auto; }

#menu {
  position: absolute;
  left: calc(var(--safe-l) + 8px);
  right: calc(var(--safe-r) + 8px);
  bottom: calc(var(--safe-b) + 8px);
  /*
   * The whole screen, not three quarters of it. It was `max-height: 74%`, so
   * a quarter of the phone sat above the sheet showing a dimmed field that
   * nothing could be done with -- and the first row you could buy started 305
   * pixels down a 664 pixel screen, with six rows under it. The field behind
   * is paused and scrimmed; there is nothing up there to look at.
   */
  top: calc(var(--safe-t) + 8px);
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, rgba(8, 15, 26, 0.97), rgba(3, 6, 12, 0.98));
  border: 1px solid rgba(120, 190, 255, 0.22);
  /* Cut corners rather than rounded: the whole interface is polygonal. */
  clip-path: polygon(0 12px, 12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%);
  transform: translateY(112%);
  opacity: 0;
  pointer-events: none;
  transition: transform 0.26s cubic-bezier(0.2, 0.9, 0.2, 1), opacity 0.2s ease;
  z-index: 25;
  overflow: hidden;
}
#menu.open { transform: none; opacity: 1; pointer-events: auto; }

/* faint scanlines, the same substrate as the field */
#menu::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(180deg, rgba(120, 190, 255, 0.045) 0 1px, transparent 1px 3px);
}

.menuBar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px 0;
}
/*
 * The two menus, as a switch. Big on purpose: this is the one control in the
 * sheet that changes what everything under it means, so it is the largest
 * text in the sheet and sits where the title used to. The title said the name
 * of the game to somebody who had already opened it.
 */
#menuGroups { display: flex; gap: 2px; flex: 1 1 auto; min-width: 0; }
.menuGroup {
  -webkit-appearance: none;
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--dim);
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.22em;
  padding: 6px 10px 8px;
  -webkit-tap-highlight-color: transparent;
}
.menuGroup.on { color: var(--ink); border-bottom-color: var(--accent); }
.menuGroup:active { color: var(--accent); }
.menuX {
  /* The push to the right edge lived on the count that used to sit between
     the title and this button; when the count went, the button followed the
     title. The margin belongs to the thing being pushed. */
  margin-left: auto;
  -webkit-appearance: none;
  appearance: none;
  background: none;
  border: 1px solid var(--line);
  color: var(--dim);
  font-size: 11px;
  line-height: 1;
  padding: 4px 7px;
  border-radius: 3px;
}
.menuX:active { color: var(--ink); border-color: var(--accent); }

#menuTabs {
  display: flex;
  gap: 0;
  border-bottom: 1px solid rgba(120, 190, 255, 0.14);
}
.menuTab {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 0;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 0.22em;
  padding: 10px 4px 8px;
}
.menuTab.on { color: var(--accent); border-bottom-color: var(--accent); background: rgba(89, 224, 255, 0.06); }
.menuTab[hidden] { display: none; }
/* 0.42 measured 2.48:1, which is a navigation label a player cannot read.
   The state is carried by the padlock and the border anyway, so the text can
   afford to be legible. */
.menuTab.sealed { color: rgba(150, 180, 210, 0.68); }
.menuTab.sealed.on { color: var(--dim); border-bottom-color: rgba(150, 180, 210, 0.5); background: rgba(150, 180, 210, 0.04); }
.tabLock { display: inline-block; width: 10px; height: 10px; margin-right: 4px; vertical-align: -1px; }
.tabLock svg { width: 10px; height: 10px; display: block; }
/* The panel peeks sideways while it is being pulled; off while the thumb is
   on it, for the same reason the dismiss is. */
#menuPanels.swiping { transition: none; }

/* ---------------- the sealed room ---------------- */
.sealedRoom {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 48px 24px;
  text-align: center;
  color: var(--dim);
}
.sealedMark { width: 34px; height: 34px; color: rgba(150, 180, 210, 0.5); }
.sealedMark svg { width: 34px; height: 34px; display: block; }
.sealedName { font-size: 13px; letter-spacing: 0.32em; color: var(--ink); }
.sealedLine { font-size: 11px; line-height: 1.5; max-width: 260px; text-wrap: balance; }

#menuPanels {
  /* The sheet has a definite height now, so the scroller has to claim what
     is left of it rather than growing to fit its content. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
.menuPanel { padding: 10px 12px 16px; }
.menuPanel[hidden] { display: none; }

.menuHead {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 6px 0 7px;
  font-size: 9px;
  letter-spacing: 0.26em;
  color: var(--ink);
}
.menuHead em {
  font-style: normal;
  margin-left: auto;
  font-size: 8px;
  letter-spacing: 0.12em;
  color: #45566a;
}

.menuGrid {
  display: grid;
  /* Two, fixed, at any width — an auto-fill count would break the last-row
     rule below and gives no benefit on a phone. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  margin-bottom: 12px;
}
/* An odd number of cells leaves a hole at the end of the last row, which reads
   as something missing. The odd one out takes the whole row instead. */
.menuGrid > .menuCell:last-child:nth-child(odd) { grid-column: 1 / -1; }

.menuCell {
  -webkit-appearance: none;
  appearance: none;
  text-align: left;
  padding: 8px 9px;
  border: 1px solid var(--line);
  background: rgba(6, 12, 22, 0.7);
  color: var(--dim);
  display: flex;
  flex-direction: column;
  gap: 3px;
  clip-path: polygon(0 0, calc(100% - 7px) 0, 100% 7px, 100% 100%, 7px 100%, 0 calc(100% - 7px));
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
/* Changed at the source rather than overridden below: these two selectors
   are more specific than a bare class, so a later `.cellSub { font-size }`
   loses to them silently -- which is how the sub-line stayed at 8px and
   2.64:1 through a pass that was meant to raise it. */
.menuCell .cellName { font-size: 14px; letter-spacing: 0.12em; color: var(--ink); }
.menuCell .cellSub {
  font-size: 12px;
  line-height: 1.4;
  letter-spacing: 0.06em;
  color: #8fa6bd;
}
.menuCell:active { transform: translateY(1px); }
.menuCell.on {
  border-color: var(--accent);
  background: rgba(89, 224, 255, 0.12);
  box-shadow: inset 0 0 0 1px rgba(89, 224, 255, 0.18);
}
.menuCell.on .cellName { color: var(--accent); }
.menuCell.on .cellSub { color: rgba(89, 224, 255, 0.7); }

/*
 * Volume, as a level rather than a slider. Six segments rising left to right:
 * the first is off, so mute is a position on the scale instead of a second
 * control that can disagree with it. Sized for a thumb — 44px of height in the
 * row, which is the whole reason this is not an <input type=range>.
 */
.volRow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 9px;
  margin-bottom: 6px;
  border: 1px solid var(--line);
  background: rgba(6, 12, 22, 0.7);
  clip-path: polygon(0 0, calc(100% - 7px) 0, 100% 7px, 100% 100%, 7px 100%, 0 calc(100% - 7px));
}
.volName {
  font-size: 10px;
  letter-spacing: 0.16em;
  color: var(--ink);
  flex: 0 0 auto;
}
.volBar {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  flex: 1 1 auto;
  height: 30px;
}
.volStep {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
  display: flex;
  align-items: flex-end;
  cursor: pointer;
}
/* The bar inside the target: the tap area is the full 30px, the mark is not. */
.volStep i {
  display: block;
  width: 100%;
  height: var(--h, 50%);
  border: 1px solid var(--line);
  background: rgba(89, 224, 255, 0.05);
  transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
}
.volStep.on i {
  border-color: var(--accent);
  background: rgba(89, 224, 255, 0.34);
  box-shadow: 0 0 6px rgba(89, 224, 255, 0.35);
}
/* Off is its own state, not an unlit segment: the leftmost mark goes red so a
   muted game says so rather than merely failing to say anything. */
.volStep.muted i {
  border-color: #ff5d8f;
  background: rgba(255, 93, 143, 0.28);
  box-shadow: 0 0 6px rgba(255, 93, 143, 0.3);
}
.volStep:active i { transform: translateY(1px); }
.volStep:focus-visible i { outline: 1px solid var(--accent); outline-offset: 1px; }

.menuKeys { display: flex; flex-direction: column; gap: 7px; margin-bottom: 10px; }
.menuKey {
  border-left: 2px solid rgba(120, 190, 255, 0.2);
  padding-left: 9px;
}
.menuKey span {
  display: block;
  font-size: 9px;
  letter-spacing: 0.22em;
  color: var(--accent);
  margin-bottom: 2px;
}
.menuKey p { font-size: 8.5px; line-height: 1.55; color: var(--dim); }

/* ---------------- object glossary ---------------- */

.codexGrid,
.armGrid { display: flex; flex-direction: column; gap: 6px; }

.codexCell,
.armRow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 9px;
  border: 1px solid var(--line);
  background: rgba(6, 12, 22, 0.6);
  clip-path: polygon(0 0, calc(100% - 7px) 0, 100% 7px, 100% 100%, 7px 100%, 0 calc(100% - 7px));
}
.codexCell.boss { border-color: rgba(255, 207, 92, 0.4); background: rgba(24, 15, 6, 0.6); }

.codexArt {
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 1px solid rgba(120, 190, 255, 0.12);
  background: rgba(2, 6, 12, 0.75);
}
.codexArt canvas { width: 36px; height: 36px; display: block; }

.codexBody { min-width: 0; }
.codexName { font-size: 10px; letter-spacing: 0.2em; color: var(--ink); }
.codexCell.boss .codexName { color: #ffcf5c; }
.codexLine {
  font-size: 8.5px;
  line-height: 1.5;
  letter-spacing: 0.03em;
  color: var(--dim);
  margin-top: 2px;
}

/* ARSENAL rows. Same shape as an object record, because that is what they
   are: the reference for a thing, not the control for it. The control is the
   strip on the play screen, and these light to match it. */
.codexArt.arm { color: var(--dim); }
.codexArt.arm svg { width: 20px; height: 20px; display: block; }
.armRow.on { border-color: color-mix(in srgb, var(--tone, var(--accent)) 45%, transparent); }
.armRow.on .codexArt {
  color: var(--tone, var(--accent));
  border-color: color-mix(in srgb, var(--tone, var(--accent)) 40%, transparent);
}
.armRow.on .codexArt svg { filter: drop-shadow(0 0 5px currentColor); }
.armRow.on .codexName { color: var(--tone, var(--accent)); }
/* Loaded right now, said without a word: a bar down the left edge. */
.armRow.on::before {
  content: '';
  width: 2px;
  align-self: stretch;
  margin: -7px 0 -7px -9px;
  background: var(--tone, var(--accent));
}

/* Not yet destroyed: a name in blocks, and no portrait. */
.codexCell.locked { border-color: rgba(120, 190, 255, 0.07); background: rgba(4, 8, 14, 0.5); }
.codexCell.locked .codexName { color: #33414f; letter-spacing: 0.1em; }
.codexCell.locked .codexLine { color: #2f3d4c; }
.codexCell.locked .codexArt { border-style: dashed; }

/* Nothing on the field is interactive while the sheet is up. */
body.menuOpen #abilities,
body.menuOpen #quickBar,
body.menuOpen #alerts,
body.menuOpen #bossCaption { opacity: 0.25; transition: opacity 0.2s ease; }
body.menuOpen #abilities,
body.menuOpen #quickBar { pointer-events: none; }


/*
 * The frame. Drawn at the edge of the screen, so it is impossible to miss and
 * impossible to collide with anything — it works over the field, over the boss
 * bar, and on any size of phone. Twice, then gone.
 */
@keyframes amendFrame {
  0% { opacity: 0; border-width: 14px; }
  10% { opacity: 1; border-width: 3px; }
  32% { opacity: 0.15; border-width: 2px; }
  46% { opacity: 0.9; border-width: 3px; }
  100% { opacity: 0; border-width: 2px; }
}

/* ---------------- loadout ---------------- */
/*
 * The AMMO and MINES tabs of the menu, from build 226. This was a sheet of its
 * own (`#loadout`, with `#loadScrim` under it and `body.loadoutOpen` holding
 * the strip out of play) that the strip's two config buttons opened over the
 * field; it is two panels in the menu now, built by Menu.buildLoadout and
 * filled by Hud.syncLoadoutSheet. Everything below that used to be an id is a
 * class, because there are two of each.
 */
.menuPanel.loadout { padding: 0 0 8px; }
.loadNote {
  font-size: 9px;
  letter-spacing: 0.16em;
  color: var(--dim);
  padding: 10px 14px 0;
}

/* The slots, in the order they sit on the strip. */
.loadSlots {
  display: flex;
  gap: 5px;
  padding: 10px 14px;
  border-bottom: 1px solid rgba(89, 224, 255, 0.1);
}
.loadSlot {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 6px 2px;
  border: 1px solid color-mix(in srgb, var(--tone, var(--accent)) 40%, transparent);
  color: var(--tone, var(--accent));
  background: rgba(11, 26, 42, 0.7);
}
.loadSlot svg { width: 15px; height: 15px; }
.loadSlot span {
  font-size: 6px;
  letter-spacing: 0.06em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.loadSlot.empty {
  border-style: dashed;
  border-color: rgba(120, 160, 200, 0.22);
  color: rgba(150, 180, 210, 0.45);
  background: transparent;
}
.loadEmpty { font-size: 6px; letter-spacing: 0.1em; }
/* A filled slot is pressable: it removes what is in it. The caption only
   appears on the ones that have something to remove. */
.loadSlot { -webkit-appearance: none; appearance: none; font: inherit; cursor: pointer; }
button.loadSlot:active { background: rgba(20, 44, 68, 0.9); }
.slotOff {
  font-size: 5.5px !important;
  letter-spacing: 0.12em;
  color: rgba(150, 180, 210, 0.42);
}
button.loadSlot:active .slotOff { color: var(--tone, var(--accent)); }


/* Inside the menu's own scroller now, so it does not scroll itself: two
   nested scrollers is a list that traps the thumb. */
.loadList {
  padding: 8px 14px 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.loadRow {
  -webkit-appearance: none;
  appearance: none;
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
  padding: 8px 10px;
  border: 1px solid var(--line);
  background: rgba(9, 17, 30, 0.7);
  color: var(--ink);
  clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px));
}
.loadRow:active { transform: translateY(1px); }
.loadArt {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--tone, var(--accent)) 30%, transparent);
  color: var(--tone, var(--accent));
}
.loadArt svg { width: 17px; height: 17px; }
.loadBody { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.loadName { font-size: 10px; letter-spacing: 0.18em; }
.loadLine { font-size: 8.5px; line-height: 1.45; color: var(--dim); }
/* Damage, then effect. Two labelled rows in a fixed order rather than a
   paragraph, so the eye can run down a column of them and compare. */
.spec {
  display: flex;
  gap: 6px;
  align-items: baseline;
  font-size: 8.5px;
  line-height: 1.45;
}
.spec b {
  flex: 0 0 20px;
  font-weight: 400;
  font-size: 6.5px;
  letter-spacing: 0.12em;
  color: rgba(150, 180, 210, 0.5);
  padding-top: 1px;
}
.spec i { font-style: normal; color: var(--dim); }
.spec.dmg i {
  color: color-mix(in srgb, var(--tone, var(--accent)) 78%, #dfeaf4);
  letter-spacing: 0.04em;
}
.codexSpec { margin-top: 3px; display: flex; flex-direction: column; gap: 1px; }
.loadBody .spec { margin-top: 1px; }

.loadState {
  flex: 0 0 auto;
  font-size: 7px;
  letter-spacing: 0.14em;
  color: var(--dim);
  padding: 3px 6px;
  border: 1px solid var(--line);
}
.loadRow.on {
  border-color: color-mix(in srgb, var(--tone, var(--accent)) 55%, transparent);
  background: rgba(11, 26, 42, 0.9);
}
.loadRow.on .loadState {
  color: var(--tone, var(--accent));
  border-color: color-mix(in srgb, var(--tone, var(--accent)) 45%, transparent);
}
/* Owned, but there is nowhere to put it — or it is the last thing to shoot. */
.loadRow.stuck .loadState { opacity: 0.5; }
/* Not owned. The same language the strip uses: grey, and hatched. */
.loadRow.sealed {
  opacity: 0.4;
  filter: grayscale(1) brightness(0.62);
  position: relative;
}
.loadRow.sealed::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    -45deg,
    rgba(150, 180, 210, 0.07) 0 2px,
    transparent 2px 6px);
}

/*
 * The bar at the foot of the sheet: the door to the tree.
 *
 * Outside the scroller on purpose. It is the answer to the question the list
 * raises -- half of what is on it reads LOCKED, and locked things are bought
 * in one place -- so it has to be there at the moment the question is asked
 * rather than at the bottom of a scroll. Full width, because a bar reads as a
 * destination where a chip in a corner reads as a detail.
 */
.loadMore {
  -webkit-appearance: none;
  appearance: none;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 11px 14px;
  text-align: left;
  font: inherit;
  cursor: pointer;
  color: var(--ink);
  border: 0;
  border-top: 1px solid rgba(89, 224, 255, 0.16);
  background: linear-gradient(180deg, rgba(11, 26, 42, 0.7), rgba(6, 14, 24, 0.9));
  transition: background 0.14s, border-color 0.14s;
}
.loadMore:active { background: rgba(20, 44, 68, 0.92); }
.loadMoreIcon {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
  color: var(--accent);
}
.loadMoreIcon svg { width: 15px; height: 15px; display: block; }
.loadMoreBody { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.loadMoreName { font-size: 9.5px; letter-spacing: 0.2em; color: var(--accent); }
.loadMoreLine { font-size: 8px; letter-spacing: 0.1em; color: var(--dim); }
/* Something is affordable in this branch right now. Same green the tree and
   the energy chip use for the same fact, so it is one language. */
.loadMore.reach { border-top-color: rgba(124, 255, 178, 0.4); }
.loadMore.reach .loadMoreLine { color: #7cffb2; text-shadow: 0 0 8px rgba(124, 255, 178, 0.35); }
.loadMoreGo {
  flex: 0 0 auto;
  font-size: 15px;
  line-height: 1;
  color: color-mix(in srgb, var(--accent) 70%, transparent);
}
.loadMore.reach .loadMoreGo { color: #7cffb2; }

/*
 * The wave sheet, held open the way the menu and the loadout are.
 *
 * The rail goes out of play with the strip, which the other two do not need to
 * say: the sheet is opened FROM the rail and sits directly under it, so a
 * thumb coming off the node it just pressed lands on an arrow. The disables
 * that actually land are the id-carrying ones further down this file -- see
 * the note there; these three lines only dim.
 */
body.sheetOpen #abilities,
body.sheetOpen #quickBar,
/* The nodes only -- the rail is inside the top bar from build 295, so the
   purse and the door beside them stay lit. It is a readout being dimmed while
   the sheet that explains it is up, which is the same thing it always was. */
body.sheetOpen #waveRail { pointer-events: none; opacity: 0.25; }
body.sheetOpen #alerts { opacity: 0.25; transition: opacity 0.2s ease; }
/* ...and the sheet itself is the one thing that still takes input. */
body.sheetOpen #waveSheet,
body.sheetOpen #waveSheet button { pointer-events: auto; opacity: 1; }


/* Already stacked. The number is the point of the marks. */


/* ---------------- the strip ---------------- */

/*
 * Mines stacked at the left edge, the two that run on their own side by side
 * in the middle, ammunition stacked at the right edge. The stacks grow upward
 * from the floor line along the edges, where the field is emptiest and the
 * thumb is already resting; the turret and the whole of the lever's arc sit in
 * the centre, between them.
 */
#quickBar {
  position: absolute;
  left: calc(var(--safe-l) + 8px);
  right: calc(var(--safe-r) + 8px);
  /* The floor line: world.floorY is derived from these same three numbers.
     (game.js floors the viewport at 420px tall before doing that sum, so on
     anything shorter than that the two part company — no phone this targets
     is, in either orientation.) */
  bottom: calc(var(--safe-b) + var(--bar-h) + 22px);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 6px;
  /* Only the groups take input; the space between them is still field, because
     the middle of this band is where the lever is worked. */
  pointer-events: none;
}

.qGroup {
  display: flex;
  flex-direction: column;
  gap: 3px;
  /* Every gap and cut corner inside a group swallows input too — a near-miss
     between two cells used to reach the canvas and take a shot. */
  pointer-events: auto;
}
/* Wide enough that a five-letter round -- TITHE, SPORE, SNARE -- has real
   air on both sides. At 62px the label sat 3px off each edge, against a
   corner that is itself cut 6px, so the text touched the notch. The centre
   row gives up 2px per cell to pay for it; the whole band still clears a
   390 screen with slack. */
.q_mines, .q_ammo { width: 70px; }
/* The two that open the AMMO and MINES tabs, one beside each stack. */
.q_cfg { justify-content: flex-end; }

/*
 * These open a screen; the two beside them are toggles. Sitting in the same
 * row at the same size they read as two more toggles, so they are drawn as a
 * different kind of object: a square filled plate rather than a cut-cornered
 * outline, which is the one visual distinction this interface already makes
 * everywhere else.
 */
.qc.cfg {
  height: 40px;
  width: 44px;
  clip-path: none;
  border-radius: 3px;
  border-color: rgba(150, 190, 230, 0.26);
  background: linear-gradient(180deg, rgba(24, 38, 58, 0.92), rgba(14, 22, 36, 0.94));
  color: #a9c2da;
}
.qc.cfg svg { width: 15px; height: 15px; opacity: 0.85; }
.qc.cfg .qLbl { opacity: 1; }
.qc.cfg:active { transform: translateY(1px); }

/*
 * A cell with nothing in it. It is an outline rather than a button, so a stack
 * of four with two things in it still reads as four cells long — which is the
 * only way an empty slot can be an invitation instead of a gap.
 */
.qc.empty {
  border-style: dashed;
  border-color: rgba(120, 160, 200, 0.2);
  background: rgba(6, 11, 20, 0.5);
  color: rgba(150, 180, 210, 0.4);
  clip-path: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.qc.empty .qLbl { font-size: 11px; opacity: 0.7; letter-spacing: 0; }

.qc {
  -webkit-appearance: none;
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  /* A weapon switch is pressed mid-fight, with things falling. 30px was the
     shortest control on the screen. */
  height: 36px;
  padding: 0 8px;
  border: 1px solid var(--line);
  /*
   * Glass, not a panel. These were 86-92% opaque, which is a wall: TERMINUS's
   * boundary is a ring around the turret for the whole fight and half of it
   * was simply not on the screen. At 72-80% over a 6px backdrop blur the ring
   * arrives as a soft band through the control rather than as nothing, and
   * the labels still clear 4.5:1 over the brightest boss sky in the game --
   * which is asserted, not assumed, in scripts/regress.mjs.
   */
  background: linear-gradient(180deg, rgba(10, 18, 32, 0.72), rgba(4, 8, 15, 0.8));
  color: var(--dim);
  overflow: hidden;
  position: relative;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  /* Cut corners rather than rounded: the whole interface is polygonal. */
  clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 6px 100%, 0 calc(100% - 6px));
  transition: transform 0.07s ease, color 0.15s, border-color 0.15s, background 0.15s;
}

/* The two in the middle are the only ones stacked icon over label, so they
   read as their own thing rather than as two more of the seven. */
.qc.wide {
  flex-direction: column;
  gap: 2px;
  height: 40px;
  width: 50px;
  padding: 0 4px;
}
/*
 * Three states, and the middle one used to be missing. A cell you can press
 * but have not selected looked all but identical to one you have not unlocked
 * — both were a grey outline with a grey label — so an AMENDMENT that opened
 * SHOT changed almost nothing on screen. Anything unsealed now carries its own
 * tone in the border, the icon and the label, and `.on` is a further step up
 * from there rather than the only step.
 */
.qc:not(.sealed) {
  color: color-mix(in srgb, var(--tone, var(--accent)) 58%, var(--dim));
  border-color: color-mix(in srgb, var(--tone, var(--accent)) 34%, transparent);
  background: linear-gradient(180deg, rgba(13, 24, 42, 0.74), rgba(5, 10, 19, 0.8));
}
.qc:not(.sealed) .qLbl { opacity: 1; }

.qc.run { border-color: rgba(120, 190, 255, 0.32); }
.qc.run .qLbl { opacity: 1; }

.qc svg { width: 14px; height: 14px; display: block; flex: 0 0 auto; }
.qc .qLbl {
  font-size: 11px;
  letter-spacing: 0.08em;
  white-space: nowrap;
  color: inherit;
  opacity: 0.85;
}
.qc:active { transform: translateY(1px); }

.qc.on {
  color: var(--tone, var(--accent));
  border-color: color-mix(in srgb, var(--tone, var(--accent)) 58%, transparent);
  background: rgba(11, 26, 42, 0.92);
}
.qc.on svg { filter: drop-shadow(0 0 5px currentColor); }
.qc.on .qLbl { opacity: 1; }
/* Which round is loaded is the one thing here that must be readable at a
   glance, mid-fight, without looking directly at it. */
.qc.on::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: currentColor;
}
.qc.wide.on::before { left: 0; right: 0; bottom: auto; width: auto; height: 2px; }

/* A round is a choice; the middle two are a thing left running. Filled, not lit. */
.qc.run.on { box-shadow: inset 0 0 0 40px color-mix(in srgb, var(--accent) 13%, transparent); }

/*
 * The row AUTO AIM opens: one button per position the assist can be put in.
 *
 * It spans the whole bar, and it is out of the flow to do it.
 *
 * It used to be a column inside .q_auto, stacked over the two cells. That
 * group sits in the middle of the quick bar with a stack either side of it and
 * has no width to give: measured on a 414 screen the column came out 104px
 * wide and each button 33px tall, four of them adjacent, laid over the turret
 * and the whole arc of the lever. 33px is below every touch target on this
 * screen and well below the 44 a thumb needs -- so the control was three
 * near-misses stacked on the one part of the field the hand is already
 * covering, which is what "unusable" meant, three times.
 *
 * `position: absolute` against #quickBar takes it out of that group entirely.
 * It gets the bar's full width, one equal column per position, and a height a
 * thumb can land on. It also stops the group resizing when the row opens,
 * which used to shift the AIM cell sideways under the finger reaching for it.
 */
.q_auto { flex-direction: column; align-items: stretch; gap: 4px; }
.autoCells { display: flex; flex-direction: row; gap: 4px; }
#aimModes {
  position: absolute;
  left: 0;
  right: 0;
  /* Directly over the row it belongs to: one .qc.wide (40px) and the group's
     own 4px gap. */
  bottom: calc(40px + 4px);
  /* One column per position offered, all equal -- a hidden button is display:
     none and takes no track, so two, three and four all fill the bar. */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 4px;
  /* It spans the bar, so its end columns lie over the two stacks -- which come
     later in the DOM and would otherwise paint over it. Measured before this
     line: ALL was under the second ammunition cell at every width tested, and
     `elementFromPoint` on the middle of the button returned the cell. */
  z-index: 2;
}
/*
 * THE SLOTS, and they are the same band one level across.
 *
 * Build 294. The ammunition was a permanent vertical column on both edges of
 * the strip -- 121px at 320x568 over field the floor line puts at y 482, so
 * all of it on playable ground, to display a choice made about once a wave.
 * It is one cell now (`.roundCell`, the loaded round) and these are what it
 * opens: exactly `#aimModes` above, because the assist has opened a spanning
 * row from a cell in this bar since build 185 and a second pattern for the
 * same gesture would be a second thing to learn.
 *
 * Nine columns rather than four, so the cells are narrow -- but a round's
 * name is its whole label and `.qc` already centres one. At 320 that is 29px
 * a cell against the 44 a `.qc.wide` gets, which is a READOUT you are picking
 * from with the row open and under your thumb, not a control you hunt for
 * mid-fight; the thing you press mid-fight is the one cell that opened it.
 */
#ammoRow {
  position: absolute;
  left: 0;
  right: 0;
  /* Directly over the row it belongs to: one .qc.wide (40px) and the group's
     own 4px gap. Same seat as #aimModes, which opens from the band beside it. */
  bottom: calc(40px + 4px);
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 4px;
  /* It spans the bar, so its end columns lie over the round cell and the AMMO
     door -- both of which come later in the DOM and would paint over it. */
  z-index: 2;
}
/* The same guard #aimModes needs, for the same reason: the rule above is
   written on an id and beats the user agent's `[hidden]`. Without this the
   property flips, every test agrees the row is shut, and it goes on painting
   over the field and taking the taps aimed at it. */
#ammoRow[hidden] { display: none; }
#ammoRow .qc { width: auto; height: 40px; }

/*
 * The cell that opens them: what is in the barrel, in that round's own colour.
 *
 * Always `.on`, because it is not a toggle -- there is always a round loaded,
 * and the tone is how the bar says which. The label is the round's `short`,
 * which is what the slot cells have always shown.
 */
.roundCell { width: 50px; }
.roundCell[aria-expanded='true'] {
  border-color: color-mix(in srgb, var(--tone, var(--accent)) 80%, transparent);
}

/*
 * While the slots are up, the rest of the strip is not what you are doing --
 * and more to the point a miss must not land on AIM, FIRE or the door under
 * the band. The same pair of rules `body.aimOpen` needs, and the second one
 * has to name an id: `#ui button { pointer-events: auto }` carries one, so
 * nothing built out of classes can turn a control back off.
 */
body.ammoOpen .q_auto,
body.ammoOpen .q_cfg { opacity: 0.22; }
body.ammoOpen #quickBar .q_auto button,
body.ammoOpen #quickBar .q_cfg button { pointer-events: none; }
/* ...and the aim row's dim reaches the new band too. */
body.aimOpen .q_round { opacity: 0.22; }
body.aimOpen #quickBar .q_round button { pointer-events: none; }

/*
 * ...and what puts it away. This is the whole of the bug that was reported
 * three times as "the menu will not collapse".
 *
 * `openAimRow(false)` sets `row.hidden = true`, `aimRowOpen()` reads it back
 * as closed, and every case in the suite that asks whether the row is shut
 * passes. The band stays on the screen. `[hidden] { display: none }` lives in
 * the user-agent sheet at one class of specificity, and both rules above it
 * here are written on an id and a class -- so the display they set has beaten
 * the attribute since the row was built, and `hidden` has never once hidden
 * anything. A shut row went on painting over the field and went on taking the
 * taps aimed at the field behind it.
 *
 * Anything that sets `display` on an element it also hides by attribute owes
 * that element one of these. Asserted on the rendered box now, not on the
 * property -- see regress.mjs; the property was always right.
 */
#aimModes[hidden] { display: none; }
.aimMode[hidden] { display: none; }
.aimMode {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  /* The floor for a thumb. Everything else on this screen clears it; this is
     the one control that did not. */
  min-height: 46px;
  padding: 5px 4px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: rgba(8, 18, 30, 0.94);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--ink);
  font: inherit;
  line-height: 1.15;
  text-align: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.aimMode:active { transform: translateY(1px); }
.aimName { font-size: 11px; letter-spacing: 0.08em; }
/* Allowed to wrap. It used to be nowrap so that no one button came out taller
   than the rest -- in a grid they are all one row and all the same height by
   construction, so the only thing nowrap can still do here is push "grey +
   hostile" out of a 73px column on a 320 screen. */
.aimNote { font-size: 9px; letter-spacing: 0.04em; color: var(--dim); }

/*
 * While the row is up, the rest of the strip is not what you are doing.
 *
 * Two reasons, and the second is the one that matters: it says which control
 * the row belongs to, and it stops a miss landing on the mine or the round
 * directly under it. The band covers the second cell of both stacks, and a
 * thumb going for ALL that catches BLAST instead has changed the loadout
 * mid-fight to choose an aim mode.
 */
body.aimOpen .q_mines,
body.aimOpen .q_ammo,
body.aimOpen .q_cfg { opacity: 0.22; }

/*
 * ...and what actually turns them off.
 *
 * `#ui button { pointer-events: auto }` is declared at the top of this file so
 * that controls work inside an overlay that does not take input. It carries an
 * id, so nothing built out of classes can beat it -- and every rule in this
 * file that means "this part of the strip is out of play" is built out of
 * classes. `body.menuOpen #quickBar { pointer-events: none }` and its loadout
 * twin have therefore never disabled a single button: the strip went to 25%
 * and stayed fully pressable underneath the sheet covering it. Measured, not
 * assumed -- `elementFromPoint` over a strip cell with the sheet open returns
 * the cell.
 *
 * A deliberate disable has to name an id of its own to land. These do.
 */
body.menuOpen #quickBar button,
body.sheetOpen #quickBar button,
body.sheetOpen #abilities button,
/* ...and under the other two sheets, which had the group rule and not this
   one. `#ui button { pointer-events: auto }` carries an id, so nothing built
   out of classes turns a control back off -- measured with body.menuOpen,
   getComputedStyle(PULSE).pointerEvents was 'auto' and a pointerdown
   dispatched on the element spent the charge and started the cooldown. Only
   the scrim's geometry was stopping a real thumb. Same shape as the
   #quickBar note above it. */
body.menuOpen #abilities button,
/* `body.sheetOpen #waveRail button` was here and came out in build 295. The
   rail's arrows and switch ARE the sheet's controls now, so disabling them
   while the sheet is up would disable the thing the sheet is for -- and the
   readout that is left in the top bar has no buttons in it to disable. A rule
   matching nothing does not announce itself; this one would have read as a
   rule that holds. */
body.aimOpen #quickBar .q_mines button,
body.aimOpen #quickBar .q_ammo button,
body.aimOpen #quickBar .q_cfg button { pointer-events: none; }
/* Where it is now, so the row says the current position rather than only
   offering four. */
.aimMode.at {
  border-color: color-mix(in srgb, var(--accent) 62%, transparent);
  color: var(--accent);
}
.aimMode.m_drift.at { border-color: color-mix(in srgb, #b8f0a0 62%, transparent); color: #b8f0a0; }
.aimMode.m_all.at { border-color: color-mix(in srgb, #ffd166 62%, transparent); color: #ffd166; }

/*
 * AUTO AIM's third position: hunting DRIFT, and nothing else.
 *
 * DRIFT's own grey would be the honest tone and is the one colour the game
 * will not spend here -- grey means harmless, and the rule is that nothing
 * else may wear it (see CFG.debris.grey and check-build's colour test). So it
 * takes the pale green the SIEVE itself is drawn in, which is the part that
 * bought the position.
 *
 * Below .qc.on and .qc.run.on, both of which it has to beat: a later rule of
 * the same specificity wins, and this file has already lost a border to a
 * block written above the one it meant to override.
 *
 * These cells transition their colour, so a probe that reads getComputedStyle
 * on the frame the class lands gets the colour being LEFT and concludes the
 * rule never applied. It cost a specificity bump that fixed nothing before the
 * transition was remembered. Read it a frame later.
 */
.qc.on.drift {
  --tone: #b8f0a0;
  color: #b8f0a0;
  border-color: color-mix(in srgb, #b8f0a0 62%, transparent);
}
.qc.run.on.drift {
  box-shadow: inset 0 0 0 40px color-mix(in srgb, #b8f0a0 15%, transparent);
}

/*
 * ...and the fourth: everything at once.
 *
 * Amber rather than a third green. The two SIEVE positions are one part's
 * doing and could have shared a tone, but they are opposites in play -- one
 * stops defending you and one stops asking you anything -- so they are told
 * apart at a glance rather than by reading the word.
 */
.qc.on.everything {
  --tone: #ffd166;
  color: #ffd166;
  border-color: color-mix(in srgb, #ffd166 62%, transparent);
}
.qc.run.on.everything {
  box-shadow: inset 0 0 0 40px color-mix(in srgb, #ffd166 15%, transparent);
}

/* ---------------- abilities ---------------- */

#abilities {
  position: absolute;
  left: calc(var(--safe-l) + 8px);
  right: calc(var(--safe-r) + 8px);
  bottom: calc(var(--safe-b) + 8px);
  height: var(--bar-h);
  display: flex;
  gap: 4px;
  pointer-events: auto;
}

.ab {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  /*
   * Always tinted with the ability's own colour, even cold. Every ability is
   * owned from the first frame, so a plain grey border on a recharging one was
   * indistinguishable from a button you had not bought — and since the pips
   * only draw above one charge, a single-charge ability on cooldown had no
   * colour, no dots and no glow anywhere on it. Readiness is still the loud
   * step (.ab.ready below); this is only the difference between "mine, not
   * yet" and "not mine".
   */
  border: 1px solid color-mix(in srgb, currentColor 36%, var(--line));
  border-radius: 8px;
  background: linear-gradient(180deg, rgba(12, 22, 38, 0.72), rgba(5, 9, 16, 0.78));
  color: var(--ink);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  overflow: hidden;
  transition: transform 0.07s ease, border-color 0.2s;
}

/* 22 from build 292, with the bar at 64 rather than 74. The label under it
   is unchanged: the icon is what carries the button and the word is what
   names it, so the word is the last thing that should shrink. */
.ab svg { width: 22px; height: 22px; display: block; }
.ab .lbl { font-size: 11px; letter-spacing: 0.02em; color: #9fb8d2; white-space: nowrap; }

.ab .fill {
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0.14;
  transform-origin: bottom center;
  transform: scaleY(0);
  pointer-events: none;
}

/*
 * The one button ORDINAL can never take away. It is the thing you always
 * still have when everything else has been subtracted, so it reads as fixed
 * rather than merely present: a second ring set inside the border, a brighter
 * frame, and a label that stays lit whether or not the button is ready.
 */
.ab.essential {
  border-color: color-mix(in srgb, currentColor 52%, transparent);
  background:
    radial-gradient(120% 90% at 50% 108%, color-mix(in srgb, currentColor 12%, transparent), transparent 70%),
    linear-gradient(180deg, rgba(12, 22, 38, 0.86), rgba(5, 9, 16, 0.9));
}
.ab.essential::before {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 5px;
  border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
  pointer-events: none;
}
.ab.essential .lbl { color: color-mix(in srgb, currentColor 70%, var(--dim)); }

/*
 * Charges. Nothing is drawn until an ability has more than one use, so a
 * turret that has bought nothing shows no empty slots anywhere — an empty
 * slot for a thing you do not have is a nag, not a readout.
 */
.pips {
  position: absolute;
  top: 4px;
  right: 4px;
  display: flex;
  gap: 2.5px;
  pointer-events: none;
}
.pips i {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, currentColor 45%, transparent);
}
.pips i.on {
  background: currentColor;
  box-shadow: 0 0 4px currentColor;
}

.ab.ready { border-color: color-mix(in srgb, currentColor 80%, transparent); }
.ab.ready .lbl { color: var(--ink); }
.ab.ready svg { filter: drop-shadow(0 0 6px currentColor); }
.ab:active { transform: scale(0.94); }

/*
 * Placed AFTER .ab.ready deliberately. Both are two-class selectors, so the
 * later one wins -- and with this block above it, `.ab.ready` was quietly
 * putting the border and the label back to PULSE's cyan on the exact button
 * this is trying to make unmistakable. The declarations were in the source
 * and simply did not apply. Same trap the menu hit three times.
 */
/*
 * SOMETHING IS ON THE TURRET, AND THIS IS THE BUTTON.
 *
 * The barrel cannot point at a thing sitting on its own mount -- that is the
 * whole reason PULSE exists and the reason ORDINAL can never take it away --
 * and nothing on the screen ever said so. A player who has not worked it out
 * watches the feed break up, keeps shooting at the field, and has no idea
 * which of eight buttons is the answer.
 *
 * Two states rather than one, because a button that shouts "press me" while
 * it is on cooldown is a lie a player only has to be told once to stop
 * believing:
 *
 *   .urgent  something is attached. A slow breath on the frame, in the
 *            corruption's own red rather than PULSE's cyan, so it belongs to
 *            the thing that is happening and not to the button.
 *   .now     ...and there is a use in hand. The ring lands, faster and
 *            brighter, and the label comes up to full.
 *
 * The cooldown fill goes on saying how long, exactly as it always has.
 */
.ab.urgent {
  border-color: rgba(255, 93, 93, 0.55);
  animation: abHeld 1.5s ease-in-out infinite;
}

/*
 * A button something is HOLDING shut. Distinct from "on cooldown" and from
 * "not owned", which both leave it un-ready: three states that look the same
 * are one state as far as the player is concerned. Only AXIOM writes it.
 */
.ab.held {
  opacity: 0.5;
  border-color: rgba(217, 179, 16, 0.7);
  box-shadow: inset 0 0 0 1px rgba(217, 179, 16, 0.28);
}
.ab.held::after {
  content: '';
  position: absolute; left: 22%; right: 22%; top: 50%; height: 2px;
  background: rgba(242, 207, 58, 0.85);
  transform: rotate(-24deg);
  pointer-events: none;
}
.ab.urgent.now {
  animation: abNow 0.9s ease-in-out infinite;
}
.ab.urgent .lbl { color: #ffc9c9; }
.ab.urgent.now .lbl { color: #fff0f0; }
/* A second frame inside the border, so the pulse reads on the button's own
   shape rather than as a glow floating behind it. */
.ab.urgent::after {
  content: '';
  position: absolute;
  inset: 1px;
  border-radius: 7px;
  border: 1px solid rgba(255, 93, 93, 0.45);
  pointer-events: none;
  animation: abHeldRing 1.5s ease-in-out infinite;
}
.ab.urgent.now::after { animation: abHeldRing 0.9s ease-in-out infinite; }

@keyframes abHeld {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 93, 93, 0); }
  50% { box-shadow: 0 0 12px 1px rgba(255, 93, 93, 0.3); }
}
@keyframes abNow {
  0%, 100% { box-shadow: 0 0 4px 0 rgba(255, 93, 93, 0.35); }
  50% { box-shadow: 0 0 22px 4px rgba(255, 120, 120, 0.75); }
}
@keyframes abHeldRing {
  0%, 100% { opacity: 0.25; transform: scale(1); }
  50% { opacity: 0.9; transform: scale(1.03); }
}
/* Still legible, still obviously the one, without the movement. */
@media (prefers-reduced-motion: reduce) {
  .ab.urgent, .ab.urgent.now, .ab.urgent::after { animation: none; }
  .ab.urgent { box-shadow: 0 0 10px 1px rgba(255, 93, 93, 0.35); }
  .ab.urgent.now { box-shadow: 0 0 18px 3px rgba(255, 120, 120, 0.6); }
  .ab.urgent::after { opacity: 0.7; }
}

.ab.flash { animation: abFlash 0.34s ease-out; }

@keyframes abFlash {
  0% { box-shadow: inset 0 0 0 40px color-mix(in srgb, currentColor 45%, transparent); }
  100% { box-shadow: inset 0 0 0 0 transparent; }
}

/*
 * First-use captions, in the quiet band just above the lever — out of the way
 * of whatever is being shot at.
 */
#abilityHint {
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: calc(var(--safe-b) + var(--bar-h) + 214px);
  text-align: center;
  white-space: pre-line;
  font-size: 10px;
  letter-spacing: 0.12em;
  color: var(--dim);
  opacity: 0;
  transition: opacity 0.25s;
  text-shadow: 0 0 10px #000;
}

/*
 * The opening tutorial uses the same element and is not the same thing: it is
 * the first text a player ever sees, over an empty field, so it sits out in
 * the open above the turret and is sized to be read rather than glanced at.
 */
#abilityHint.tutorial {
  left: calc(var(--safe-l) + 18px);
  right: calc(var(--safe-r) + 18px);
  /* Measured off the same bottom as the hint band rather than as a percentage
     of the viewport: the turret's height above the ability bar is fixed in
     pixels, so a percentage put this on top of the turret on a short screen. */
  bottom: calc(var(--safe-b) + var(--bar-h) + 252px);
  font-size: 12px;
  line-height: 1.85;
  letter-spacing: 0.14em;
  color: #cfe2f2;
  transition: opacity 0.35s;
  text-shadow: 0 0 14px #000, 0 0 30px #000;
  /*
   * A plate, not just a shadow. These lines land over a field of lit objects
   * and two shadows were not enough to hold them apart from it — measured
   * against a full field, the grey-on-neon was close to unreadable. Sized to
   * the sentence and centred: left, right and width are all set, so the auto
   * margins split the slack evenly.
   */
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
  padding: 9px 15px;
  background: rgba(4, 9, 17, 0.82);
  border: 1px solid rgba(120, 170, 210, 0.22);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  clip-path: polygon(0 0, calc(100% - 9px) 0, 100% 9px, 100% 100%, 9px 100%, 0 calc(100% - 9px));
}
/*
 * The opening's band keeps the line before. The new one arrives at the bottom
 * and the old one is pushed up and dimmed rather than taken away — a sentence
 * you have just acted on should still be there while you look at what it gave
 * you.
 */
.hLine { display: block; }
.hLine.now { animation: hLineIn 0.32s ease-out; }
.hLine.past {
  opacity: 0.4;
  font-size: 0.86em;
  padding-bottom: 3px;
  margin-bottom: 4px;
  border-bottom: 1px solid rgba(120, 170, 210, 0.14);
}
@keyframes hLineIn {
  from { opacity: 0; transform: translateY(7px); }
  to { opacity: 1; transform: none; }
}

#abilityHint.show { opacity: 1; }

/* ---------------- debug ---------------- */

#debugPanel {
  position: absolute;
  top: calc(var(--hud-t) + 40px);
  right: calc(var(--safe-r) + 10px);
  width: min(280px, calc(100vw - 24px));
  background: rgba(4, 8, 14, 0.94);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px;
  pointer-events: auto;
  z-index: 20;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
#debugPanel[hidden] { display: none; }

.dbgHead {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 10px;
  letter-spacing: 0.24em;
  color: var(--dim);
  margin-bottom: 8px;
}
.dbgHead .x {
  background: none;
  border: 0;
  color: var(--dim);
  font-size: 14px;
  line-height: 1;
  padding: 2px 4px;
}

.dbgGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }

.dbgGrid button {
  -webkit-appearance: none;
  appearance: none;
  font-size: 9.5px;
  letter-spacing: 0.06em;
  padding: 8px 4px;
  border-radius: 5px;
  border: 1px solid var(--line);
  background: rgba(14, 26, 42, 0.8);
  color: var(--ink);
  text-align: center;
}
.dbgGrid button:active { background: rgba(89, 224, 255, 0.24); }
.dbgGrid button.on { border-color: var(--accent); color: var(--accent); }
/* Armed: the two that destroy something a new run does not put back. */
.dbgGrid button.ask { border-color: #ff5d5d; color: #ff5d5d; }
.dbgGrid button.wide { grid-column: span 2; }

/*
 * A group heading. A DIV and not a disabled button: a thing that looks like a
 * control and refuses every press is worse than a label, and the panel already
 * has enough real controls to keep track of.
 */
.dbgHeadRow {
  grid-column: span 2;
  font-size: 8px;
  letter-spacing: 0.22em;
  color: var(--dim);
  padding: 6px 2px 1px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 1px;
}
.dbgHeadRow:first-child { padding-top: 0; }

.dbgStats {
  margin-top: 8px;
  font-size: 9px;
  line-height: 1.5;
  color: var(--dim);
  white-space: pre;
}

/* ---------------- debug: spawn screen ---------------- */

/*
 * The picker wants three columns to be readable and the panel is 280px, so the
 * panel widens while the screen is up and narrows again on the way out. It can
 * also now outgrow the screen -- twenty buttons already did on a 568-tall
 * phone -- so it scrolls inside itself rather than off the bottom.
 */
#debugPanel {
  max-height: calc(100vh - var(--hud-t) - 56px - var(--safe-b));
  max-height: calc(100dvh - var(--hud-t) - 56px - var(--safe-b));
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
#debugPanel.wide { width: min(340px, calc(100vw - 16px)); }

/*
 * .dbgGrid is display:grid, which outranks the hidden attribute on its own --
 * and so does .dbgBoss. Anything here that is given a `display` and is also
 * hidden by attribute owes itself this line; see CLAUDE.md on `[hidden]`.
 */
.dbgGrid[hidden], .dbgSpawn[hidden], .dbgBoss[hidden] { display: none; }

/* ---------------- debug: the fights ---------------- */

/*
 * One row per anomaly, in ladder order, each wearing its own colour -- which
 * is the identity in this game: a player who has seen amber once knows what
 * is arriving. The rung and the era are on the row because both are facts
 * about getting there, and the era is the one the button actually changes.
 */
.dbgBoss { display: grid; grid-template-columns: 1fr; gap: 4px; }
.dbgBossRow {
  -webkit-appearance: none;
  appearance: none;
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: 7px;
  padding: 7px 8px;
  border-radius: 5px;
  border: 1px solid var(--line);
  background: rgba(14, 26, 42, 0.8);
  color: var(--ink);
  text-align: left;
}
.dbgBossRow:active { background: rgba(89, 224, 255, 0.24); }
/* `flex` was here and the row is a GRID, where it does nothing. */
.dbgBossDot { width: 8px; height: 8px; border-radius: 50%; }
.dbgBossName { font-size: 10px; letter-spacing: 0.12em; white-space: nowrap; }
.dbgBossAt, .dbgBossEra {
  font-size: 8px;
  letter-spacing: 0.08em;
  color: var(--dim);
  white-space: nowrap;
}
/* The one that is a state change rather than a number, so it reads as one. */
.dbgBossRow.away .dbgBossEra { color: var(--accent); }
/* Live: the fight that is up right now, so the list is a readout as well. */
.dbgBossRow.on { border-color: var(--accent); }

.spawnHead {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 6px;
  margin-bottom: 7px;
}
.spawnBack {
  -webkit-appearance: none;
  appearance: none;
  flex: 0 0 auto;
  white-space: nowrap;
  background: none;
  border: 0;
  padding: 2px 0;
  font-size: 9.5px;
  letter-spacing: 0.14em;
  color: var(--accent);
}
.spawnTally {
  flex: 1 1 auto;
  font-size: 8px;
  letter-spacing: 0.02em;
  color: var(--dim);
  text-align: right;
}
.spawnPeek {
  -webkit-appearance: none;
  appearance: none;
  flex: 0 0 auto;
  padding: 3px 6px;
  font-size: 8px;
  letter-spacing: 0.12em;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: rgba(14, 26, 42, 0.8);
  color: var(--dim);
}

/*
 * Folded. The picker and its option rows go; the head, the spawn button and
 * CLEAR FIELD stay, and the panel narrows so most of the arena is visible
 * beside it. This is the state a spawn leaves it in -- see Hud.miniSpawn().
 */
#debugPanel.mini .spawnPick,
#debugPanel.mini .spawnRow,
#debugPanel.mini .dbgStats { display: none; }
#debugPanel.wide.mini { width: min(244px, calc(100vw - 16px)); }
#debugPanel.mini .spawnGo { margin-top: 2px; }
#debugPanel.mini .spawnTally { white-space: nowrap; }

/* The picker's own divider: a `.dbgHeadRow` has to span all three columns
   here rather than the grid's two. */
.spawnPick .spawnSplit { grid-column: span 3; }

.spawnPick {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  margin-bottom: 9px;
}
.spawnChip {
  -webkit-appearance: none;
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 5px 2px 4px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: rgba(14, 26, 42, 0.8);
  color: var(--ink);
}
.spawnChip canvas { width: 30px; height: 30px; display: block; }
.spawnChip span { font-size: 7.5px; letter-spacing: 0.05em; color: var(--dim); }
.spawnChip.on { border-color: var(--accent); background: rgba(89, 224, 255, 0.14); }
.spawnChip.on span { color: var(--accent); }

.spawnRow { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.spawnLabel {
  flex: none;
  width: 50px;
  font-size: 8px;
  letter-spacing: 0.14em;
  color: var(--dim);
}
.spawnOpts { display: flex; flex-wrap: wrap; gap: 3px; flex: 1; }
.spawnOpts button {
  -webkit-appearance: none;
  appearance: none;
  min-width: 24px;
  padding: 5px 6px;
  font-size: 8.5px;
  letter-spacing: 0.04em;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: rgba(14, 26, 42, 0.8);
  color: var(--ink);
}
.spawnOpts button.on {
  border-color: var(--accent);
  color: var(--accent);
  background: rgba(89, 224, 255, 0.14);
}

.spawnGo {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  margin-top: 9px;
  padding: 11px 6px;
  font-size: 10.5px;
  letter-spacing: 0.14em;
  border: 1px solid var(--accent);
  border-radius: 6px;
  background: rgba(89, 224, 255, 0.16);
  color: var(--accent);
}
.spawnGo:active { background: rgba(89, 224, 255, 0.32); }
.spawnClear {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  margin-top: 5px;
  padding: 7px 6px;
  font-size: 9px;
  letter-spacing: 0.12em;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: rgba(14, 26, 42, 0.8);
  color: var(--dim);
}
.spawnClear:active { background: rgba(89, 224, 255, 0.2); }

/* ---------------- boot ---------------- */

#boot {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--hud-t) + 24px) 26px calc(var(--safe-b) + 24px);
  background: radial-gradient(120% 80% at 50% 30%, rgba(10, 20, 36, 0.86), rgba(2, 3, 7, 0.97) 70%);
  pointer-events: auto;
  z-index: 30;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/*
 * The copy is centred while it fits and scrollable when it does not. A flex
 * container that centres with `justify-content` clips the overflowing end
 * instead of letting it scroll, which silently ate the build footer the first
 * time the instruction rows grew. Auto margins collapse to zero once there is
 * no free space, so the block starts at the top and the whole thing scrolls.
 */
#boot {
  flex-direction: column;
  justify-content: flex-start;
  /* Taller content scrolls instead of running off the bottom edge: the
     footer used to end seven pixels below a 664px screen, half a line of
     "BUILD 160" simply missing. */
  overflow-y: auto;
  padding-bottom: calc(var(--safe-b) + 12px);
}
.bootInner { margin-block: auto; }
#boot[hidden] { display: none; }

/*
 * ...and it stops taking taps on the frame it starts leaving.
 *
 * This animated opacity, transform and blur and nothing else, while `#boot`
 * keeps `inset: 0` and `pointer-events: auto` and `hidden` arrives on a 500ms
 * timeout -- and an element at opacity 0 still hit-tests. So for half a second
 * after the way in, every tap landed on a dead full-screen panel instead of
 * the field, and a tap where the primary had been pressed it AGAIN: `start()`
 * twice is a second reset and a second SIMULATION ONLINE.
 */
#boot.out { pointer-events: none; }

#boot.out { animation: bootOut 0.5s ease-in forwards; }
@keyframes bootOut { to { opacity: 0; transform: scale(1.06); filter: blur(6px); } }

.bootInner { max-width: 420px; width: 100%; }

/*
 * ---- the status line ----
 *
 * The dot breathes and the figure on the right is the object count of the
 * arena actually running behind this panel -- `phase = 'boot'` keeps the field
 * live, so the one piece of motion up here is real telemetry rather than an
 * animation of nothing.
 */
/*
 * ---- one clean row, or two, and never a ragged three ------------------
 *
 * This is a flex row and its children had no `white-space`, so at every phone
 * width the TEXT inside them wrapped instead of the row doing it: measured,
 * three line boxes at 320, 390 and 414, and at 320 the readout hung 32px off
 * the right edge -- the first line of the first screen, clipped, on the width
 * that binds. Nothing could see it: the suite's sweep walks font sizes and
 * contrast, and a wrap flips no property and clips nothing a colour test can
 * read.
 *
 * Each part is `nowrap` and the ROW is what wraps, so it is one line where
 * there is room and two where there is not, with the readout keeping the
 * right-hand end of whichever line it lands on.
 */
.bootStatus {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  row-gap: 4px;
  /* 11px is the title screen's floor -- the same one the menu keeps, and the
     suite walks every text node on this panel against it. */
  font-size: 11px;
  letter-spacing: 0.18em;
  color: var(--dim);
  padding-bottom: 10px;
  margin-bottom: 16px;
  border-bottom: 1px solid rgba(120, 190, 255, 0.12);
}
.bootDot {
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: #7cffb2;
  box-shadow: 0 0 8px rgba(124, 255, 178, 0.8);
  animation: bootPulse 2.4s ease-in-out infinite;
}
@keyframes bootPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
.bootSess { color: #a3b8ce; white-space: nowrap; }
.bootLink { color: #7cffb2; white-space: nowrap; }
.bootTele {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  /* Solid rather than 0.6 alpha: composited over this ground that came out at
     3.88:1, under the 4.5 the panel is held to. */
  color: #8fa9c4;
  white-space: nowrap;
  letter-spacing: 0.1em;
  white-space: nowrap;
}

/*
 * ---- the wordmark ----
 *
 * Two lines that read as one object: the word in a thin wide cap and the
 * number heavy under it, with a sweep crossing it every few seconds. The
 * sweep is a clipped gradient rather than an opacity flicker on the number --
 * a flicker on the largest element on the screen reads as a fault in the
 * display, which is a good joke exactly once and a distraction after that.
 */
.bootMark {
  position: relative;
  display: flex;
  flex-direction: column;
  margin-bottom: 14px;
  font-weight: 400;
}
.bootMark span {
  font-size: 13px;
  letter-spacing: 0.62em;
  color: #a3b8ce;
  text-indent: 0.62em;
}
.bootMark b {
  font-size: 68px;
  line-height: 0.94;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--accent);
  text-shadow: 0 0 34px rgba(89, 224, 255, 0.45);
}
.bootMark::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(100deg, transparent 42%, rgba(191, 244, 255, 0.5) 50%, transparent 58%);
  background-size: 260% 100%;
  animation: bootSweep 7s ease-in-out infinite;
  mix-blend-mode: overlay;
}
@keyframes bootSweep {
  0%, 62% { background-position: 130% 0; }
  86%, 100% { background-position: -30% 0; }
}

.bootBody {
  font-size: 12px;
  line-height: 1.7;
  color: #a9c2da;
  margin-bottom: 18px;
  max-width: 34em;
}

/*
 * ---- what the device has to show for itself ----
 *
 * Three figures, as tiles rather than a sentence: they are compared with a
 * glance and a sentence has to be read. Only there once there is something in
 * it -- see Hud.showRecord() -- because a row of zeroes is a worse welcome
 * than no row at all.
 */
#bootRecord {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  margin-bottom: 18px;
}
#bootRecord[hidden] { display: none; }
#bootRecord span {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 9px 8px;
  border: 1px solid rgba(120, 190, 255, 0.16);
  background: rgba(9, 17, 30, 0.6);
  clip-path: polygon(0 0, calc(100% - 7px) 0, 100% 7px, 100% 100%, 7px 100%, 0 calc(100% - 7px));
}
#bootRecord b {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
/* The denominator is context, not the figure: same line, half the weight. */
#bootRecord b i {
  font-style: normal;
  font-size: 11px;
  color: #8fa9c4;
}
#bootRecord em {
  font-style: normal;
  font-size: 11px;
  letter-spacing: 0.1em;
  color: #8fa9c4;
}

.bootKeys { list-style: none; margin: 18px 0 0; }
.bootKeys li {
  font-size: 11px;
  line-height: 1.5;
  color: #a3b8ce;
  padding: 6px 0;
  border-top: 1px solid rgba(120, 190, 255, 0.09);
}
.bootKeys li span {
  color: var(--accent);
  letter-spacing: 0.16em;
  margin-right: 8px;
}

/*
 * ---- the way in ----
 *
 * ONE primary. It is CONTINUE when there is a run on disk and BEGIN
 * SIMULATION when there is not, and it is the same button either way -- NEW
 * RUN used to sit beside CONTINUE, doing the destructive thing with the
 * quieter label and a whole run behind it.
 */
#startBtn, #resumeBtn {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  padding: 17px;
  font-size: 12px;
  letter-spacing: 0.3em;
  border: 1px solid var(--accent);
  background: linear-gradient(180deg, rgba(89, 224, 255, 0.16), rgba(89, 224, 255, 0.07));
  color: var(--accent);
  text-shadow: 0 0 12px rgba(89, 224, 255, 0.6);
  clip-path: polygon(0 0, calc(100% - 11px) 0, 100% 11px, 100% 100%, 11px 100%, 0 calc(100% - 11px));
  box-shadow: 0 0 26px -8px rgba(89, 224, 255, 0.7);
}
#startBtn:active, #resumeBtn:active { background: rgba(89, 224, 255, 0.3); }
.bootGo { display: flex; }
.bootGo button { min-width: 0; white-space: nowrap; }
#resumeBtn {
  border-color: #ffcf5c;
  background: linear-gradient(180deg, rgba(255, 207, 92, 0.18), rgba(255, 207, 92, 0.07));
  color: #ffcf5c;
  text-shadow: 0 0 12px rgba(255, 207, 92, 0.55);
  box-shadow: 0 0 26px -8px rgba(255, 207, 92, 0.7);
  letter-spacing: 0.2em;
}
#resumeBtn:active { background: rgba(255, 207, 92, 0.3); }
/* When there is a run to continue, that is the button. */
#resumeBtn:not([hidden]) ~ #startBtn { display: none; }
/*
 * ...and CONTINUE's own guard, which it was the one element on this panel
 * without. `[hidden] { display: none }` is the user agent's, at one class of
 * specificity, and loses to any author rule written on an id -- so a button
 * this panel hides by attribute owes itself this line. See CLAUDE.md.
 */
#resumeBtn[hidden] { display: none; }

#resumeNote {
  margin-top: 8px;
  text-align: center;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: #8fa9c4;
}
#resumeNote[hidden] { display: none; }

/*
 * ---- the foot, and the one thing here that cannot be undone ----
 */
.bootFoot {
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid rgba(120, 190, 255, 0.09);
  text-align: center;
  font-size: 11px;
  letter-spacing: 0.1em;
  /* 5.8:1. It was #3d4d5e at 2.35:1 -- and the build number in it is the
     line two devices get compared by. */
  color: #7d90a6;
}
.bootWipe {
  -webkit-appearance: none;
  appearance: none;
  background: none;
  border: 0;
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.18em;
  color: #ff6b7a;
  padding: 8px 12px;
  cursor: pointer;
}
.bootWipe:active { color: #ff6b7a; }
.bootWipe[hidden] { display: none; }

/* The word, asked for rather than a second tap: a typed word cannot be hit by
   accident, and this throws a run away. */
.bootAsk {
  margin: 4px auto 10px;
  max-width: 320px;
  text-align: left;
  border: 1px solid rgba(255, 107, 122, 0.34);
  background: rgba(30, 8, 12, 0.6);
  padding: 10px 11px;
}
.bootAsk[hidden] { display: none; }
.bootAsk label {
  display: block;
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: 0.04em;
  color: #d8b6bc;
  margin-bottom: 8px;
}
.bootAsk label b { color: #ff6b7a; letter-spacing: 0.2em; }
.bootAskRow { display: flex; gap: 6px; }
#wipeWord {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 0;
  min-width: 0;
  background: rgba(4, 6, 12, 0.8);
  border: 1px solid rgba(255, 107, 122, 0.34);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.28em;
  text-align: center;
  padding: 8px 4px;
}
#wipeWord:focus { outline: none; border-color: #ff6b7a; }
/*
 * ...and ACCEPTED. `hud.js` has toggled `.ok` on this field on every keystroke
 * since the box was written and nothing in this stylesheet has ever styled it,
 * so the one moment the screen most needs to confirm the word painted nothing
 * on the field the thumb is looking at -- the only signal was an 11px label
 * beside it losing its disabled state.
 */
#wipeWord.ok { border-color: #ff6b7a; color: #ff6b7a; }
#wipeGo, .bootAskNo {
  -webkit-appearance: none;
  appearance: none;
  flex: 0 0 auto;
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.1em;
  padding: 8px 10px;
  border: 1px solid rgba(255, 107, 122, 0.4);
  background: rgba(255, 107, 122, 0.1);
  color: #ff6b7a;
}
/*
 * DISABLED, and in a real colour rather than behind an alpha.
 *
 * It was `opacity: 0.35` over `#8fa9c4`, which composites to 1.92:1 against
 * this panel's ground -- and the suite's own contrast sweep is blind to
 * opacity, so it read the UNCOMPOSITED 8.08 and passed. A readout the guard
 * cannot see is a readout with no guard. #707f94 is the same hue without the
 * alpha at 4.92:1 -- clear of the 4.5 this panel is held to rather than on it,
 * because the first attempt was computed against the wrong ground and the
 * sweep caught it at 3.63. It still reads as plainly off.
 */
#wipeGo[disabled] { color: #707f94; border-color: var(--line); background: transparent; }
.bootAskNo { border-color: var(--line); background: transparent; color: #8fa9c4; }

.bootBuild { font-variant-numeric: tabular-nums; }
.bootHome { margin-top: 4px; font-size: 11px; color: #7d90a6; }

/* ---------------- the ladder ---------------- */
/*
 * The rail. A window of five tiers with the run's own position lit, the two
 * arrows that move it, and one line saying whether it is climbing by itself.
 *
 * Sits under the top bar rather than in it. The chip this replaces was fixed
 * furniture in a bar whose whole job is to give ground when the purse grows,
 * and a rail is far too wide to be a passenger there -- so it takes its own
 * band, and #apertureBar, #bossBar and #alerts each start below it.
 */
/*
 * IN THE TOP BAR from build 295, and it takes whatever the purse and the door
 * leave. Measured at 320 with the widest figure the formatter can produce:
 * 304 available, MENU 46, the purse 79, two 5px gaps -- 169 for five nodes,
 * 30.6px each against the 33.3 they had in a band of their own.
 *
 * What made that band 300px wide was the two arrows and the switch, and those
 * are in the sheet this rail opens -- a tap anywhere on the standing rung is
 * what opens it, so the control surface is one tap from the thing it moves.
 * The nodes were always a readout; its own docstring has said so since the
 * rail went in.
 */
#waveRail {
  flex: 1 1 auto;
  min-width: 0;
  align-self: stretch;
  display: flex;
  align-items: stretch;
  gap: 4px;
  pointer-events: auto;
}
#waveRail[hidden] { display: none; }

/*
 * A fight owns the top of the screen: the boss bar takes this slot, the
 * ladder is not what anyone is reading, and the alerts column needs the room
 * back. See --rail-h for the measurement that made this necessary.
 */
body.bossUp {
  /*
   * `--under-rail` is set outright rather than by moving `--rail-h`. A custom
   * property's value is substituted where it is DEFINED, not where it is
   * used: `--under-rail` is computed on :root against :root's `--rail-h`, and
   * a fresh `--rail-h` down here changes nothing it already resolved. That
   * cost a measurement -- the boss bar moved and the alerts column did not.
   */
  --under-rail: var(--rail-t);
  --boss-h: 64px;
}
body.bossUp #waveRail { display: none; }

/*
 * The two that move it. Everything else in this band is a readout, so these
 * are the whole control surface and they are sized for a thumb: 44 tall, and
 * wide enough that the arrow is not the target -- the plate is. Build 292
 * took them to 38 while looking for field height and put them back: the seat
 * is asserted, the four pixels were worth less than the promise, and what the
 * rail actually had spare was the eight of `--rail-h` reserved over a band
 * that has always measured 44.
 */
.railStep {
  -webkit-appearance: none;
  appearance: none;
  pointer-events: auto;
  flex: 0 0 auto;
  width: 40px;
  min-height: 44px;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
  border-radius: 5px;
  background: linear-gradient(180deg, rgba(12, 22, 38, 0.86), rgba(5, 9, 16, 0.9));
  color: var(--ink);
  font: inherit;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.railStep:disabled { opacity: 0.3; }
#railSkip[hidden] { display: none; }
/* The skip is the same plate as the arrows and reads a shade quieter: it is
   the convenience, not the control. */
#railSkip { border-color: color-mix(in srgb, var(--accent) 20%, var(--line)); }
.railStep:active:not(:disabled) { transform: translateY(1px); }

/*
 * The window itself. Equal columns, so the rail is the same shape at 320 as
 * at 414 and only the nodes narrow -- five fixed-width nodes would either
 * overflow the small screen or leave a gap on the large one.
 */
#railNodes {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 4px;
  flex: 1 1 auto;
  min-width: 0;
}

/*
 * One node per tier in the window. Three states and they are meant to be
 * told apart at arm's length, so each one changes more than a shade: passed
 * is a tick over a filled plate, standing is lit and outlined in the accent,
 * ahead is an outline with nothing in it.
 */
.railNode {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  min-height: 44px;
  padding: 3px 2px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: rgba(6, 12, 22, 0.7);
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  overflow: hidden;
}
.railNode b { font-size: 13px; font-weight: 700; letter-spacing: 0.02em; }
/* The tick, and the space it keeps when there is nothing to say. Held at a
   fixed height so a node does not change size as the run passes it. */
.railNode i {
  font-style: normal;
  font-size: 10px;
  line-height: 9px;
  height: 9px;
  opacity: 0.95;
}

.railNode.done {
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 14%, rgba(6, 12, 22, 0.7));
  color: color-mix(in srgb, var(--accent) 62%, var(--dim));
}
/* Ahead, but already climbed -- see syncRail. Lighter than passed and heavier
   than never-reached, which is exactly where it sits. */
.railNode.seen {
  border-color: color-mix(in srgb, var(--accent) 34%, transparent);
  color: color-mix(in srgb, var(--accent) 40%, var(--dim));
}
/*
 * Never reached, and not reachable: the arrow stops at the rung below it.
 * Dashed, because that is already what this interface means by "a place for
 * something that is not there yet" -- see .qc.empty on the strip.
 */
.railNode.locked {
  border-style: dashed;
  border-color: color-mix(in srgb, var(--line) 70%, transparent);
  background: rgba(6, 12, 22, 0.4);
  color: color-mix(in srgb, var(--dim) 55%, transparent);
}
.railNode.at {
  border-color: color-mix(in srgb, var(--accent) 72%, transparent);
  background: color-mix(in srgb, var(--accent) 16%, rgba(6, 12, 22, 0.8));
  color: var(--accent);
  box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 18%, transparent);
}
.railNode.at b { font-size: 15px; }
/*
 * The rung under trial: stood on, and not yet earned. It takes `.at` as well,
 * so this only has to say the second half -- dashed like a locked rung,
 * because that is what this interface means by "not yours yet", but lit like
 * the one you are on, because you are on it. It comes after `.at` so the
 * dashed border wins on the element that carries both.
 */
/*
 * The rules the running wave is carrying, under its number. Monochrome and in
 * the rail's own palette: a trait never recolours anything, because the
 * roster's colours already mean something and two meanings on one channel is
 * one too many.
 */
.rTraits {
  display: block;
  font-style: normal;
  font-size: 9px;
  line-height: 9px;
  height: 9px;
  letter-spacing: 1px;
  color: color-mix(in srgb, var(--accent) 55%, var(--dim));
}
.railNode.at .rTraits { color: var(--accent); }
/* ...and the same row asking rather than reporting: a gate's two lanes. */
.railNode.offering .rTraits {
  color: var(--accent);
  font-size: 11px;
  line-height: 11px;
  animation: laneBreathe 1.8s ease-in-out infinite;
}
@keyframes laneBreathe { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }

/*
 * ============ A STEP BACK, ON THE RAIL ============
 *
 * Build 296. The glitch discharge is the only involuntary way down this game
 * has, and it arrived with a shake, a red flash, a narrator line, an alert and
 * a dissolving field -- while the ladder, the thing that actually changed,
 * snapped. Measured: ninety frames sampled through a discharge gave ONE
 * distinct rail state, the same count as ninety frames of nothing happening.
 *
 * Three marks rather than one colour, because a state that lives only in a hue
 * is a state a colourblind player never receives. The rung LOST pulses and
 * hands itself back; the rung LANDED takes the lit mark; and the band is
 * knocked and settles, which is the motion the words describe. Each is a
 * different channel: brightness, geometry, position.
 *
 * ~0.55s, against a 0.9s field dissolve and a 5s alert: quick enough to be
 * part of the same event rather than a thing that outlives it. `markStep`
 * clears the classes at 700ms so none can outlive its own animation.
 */

/*
 * The rung handed back. It ends on the AHEAD look -- an outline with nothing
 * in it, dashed like every other "not yours" in this interface -- because
 * that is what the rung now is, and the animation is the handing over rather
 * than a flourish laid on top of the old paint.
 */
.railNode.lostRung {
  animation: railLost 0.55s ease-out both;
}
@keyframes railLost {
  0% {
    border-color: color-mix(in srgb, var(--warn) 85%, transparent);
    background: color-mix(in srgb, var(--warn) 26%, rgba(6, 12, 22, 0.8));
    color: var(--warn);
    box-shadow: 0 0 14px -2px color-mix(in srgb, var(--warn) 60%, transparent);
    transform: translateY(0) scale(1);
  }
  35% {
    border-color: color-mix(in srgb, var(--warn) 70%, transparent);
    background: color-mix(in srgb, var(--warn) 14%, rgba(6, 12, 22, 0.8));
    color: var(--warn);
    /* Lifted and shrinking: the rung is being taken off, not merely recoloured. */
    transform: translateY(-2px) scale(0.94);
  }
  100% {
    border-color: var(--line);
    border-style: dashed;
    background: rgba(6, 12, 22, 0.4);
    color: color-mix(in srgb, var(--dim) 55%, transparent);
    box-shadow: 0 0 0 0 transparent;
    transform: translateY(0) scale(1);
  }
}

/*
 * ...and the one it fell onto. Deliberately quieter than the rung it lost:
 * arriving here is not an achievement, and `.at`'s own accent is already
 * painted underneath by the time this runs. What this adds is the ARRIVAL --
 * a ring closing inward -- so the eye is taken from the rung above to this
 * one rather than being left to notice a different cell is lit.
 */
.railNode.landedRung {
  animation: railLanded 0.55s ease-out both;
}
@keyframes railLanded {
  0% {
    box-shadow: 0 0 0 6px color-mix(in srgb, var(--warn) 34%, transparent);
    border-color: color-mix(in srgb, var(--warn) 70%, transparent);
  }
  60% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--warn) 18%, transparent);
  }
  100% {
    box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 72%, transparent);
  }
}

/*
 * The band, knocked back and settling. A `transform`, so nothing in the top
 * bar moves: the rail shares that row with the purse and the door from build
 * 295, and a change that reflowed the row would shove both of them.
 *
 * One node's width is the honest distance -- that is what a rung IS here --
 * and `--rail-knock` is set from it rather than guessed, because the node
 * width is `1fr` of whatever the purse leaves and runs 30 to 43px.
 */
#waveRail.knocked {
  animation: railKnock 0.5s cubic-bezier(0.2, 0.9, 0.25, 1) both;
}
@keyframes railKnock {
  0% { transform: translateX(var(--rail-knock, 14px)); }
  100% { transform: translateX(0); }
}

/*
 * Reduced motion keeps the STATEMENT and drops the travel. The two rungs still
 * change -- that is the information -- and the band does not slide.
 */
@media (prefers-reduced-motion: reduce) {
  #waveRail.knocked { animation: none; }
  .railNode.lostRung { animation-duration: 0.01s; }
  .railNode.landedRung { animation-duration: 0.01s; }
}
@media (prefers-reduced-motion: reduce) {
  .railNode.offering .rTraits { animation: none; opacity: 0.9; }
}

/*
 * The wave sheet: opened from the rung the run is standing on, and it holds
 * the world while it is up. Sits under the rail rather than over the field's
 * middle, so the thing it is describing is still visible behind it.
 */
#waveSheet {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: var(--under-rail);
  width: min(94vw, 420px);
  z-index: 6;
  display: grid;
  gap: 8px;
  padding: 10px 12px 12px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  backdrop-filter: blur(8px);
  pointer-events: auto;
}
#waveSheet[hidden] { display: none; }
.sheetHead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--accent);
}
.sheetX {
  background: none;
  border: 0;
  color: var(--dim);
  font-size: 20px;
  line-height: 1;
  padding: 4px 8px;
  min-height: 44px;
  min-width: 44px;
}
.sheetRow {
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--dim);
  line-height: 1.5;
}
#sheetMeters { display: grid; gap: 5px; }
.sheetMeter { display: grid; grid-template-columns: 92px 1fr 40px; gap: 8px; align-items: center; }
.sheetMeter span { font-size: 9px; letter-spacing: 1px; color: var(--dim); }
.sheetMeter u { display: block; height: 4px; border-radius: 2px; background: rgba(120, 190, 255, 0.14); }
.sheetMeter u i { display: block; height: 100%; border-radius: 2px; background: var(--accent); }
.sheetMeter b { font-size: 10px; color: var(--dim); text-align: right; font-variant-numeric: tabular-nums; }
.sheetMeter.hot u i { background: var(--warn); }
/*
 * The ladder's control surface, in the sheet from build 295.
 *
 * It is the same three buttons the rail band carried -- ids unchanged -- and
 * they keep the 44px seat the suite has asserted since the rail went in. In
 * here they can have it without costing the field a row: the sheet is opened
 * from the standing rung and holds the world while it is up.
 *
 * The arrows are fixed and the switch takes the rest, because the switch is
 * the one that carries a word ("AUTO PROGRESS ON" against "AUTO") and the
 * arrows are glyphs that do not grow.
 */
#sheetLadder {
  display: flex;
  align-items: stretch;
  gap: 6px;
}
#sheetLadder .railStep { flex: 0 0 auto; width: 44px; }
#sheetLadder #railAuto { flex: 1 1 auto; min-width: 0; }
/* The counter reads in here now rather than taking a third of the top bar,
   and it is a full row of its own: nothing else in this sheet is a chip, so
   it takes the sheet's own type instead of the bar's cramped one. */
#waveSheet #counter {
  width: 100%;
  justify-content: space-between;
  background: rgba(6, 12, 22, 0.6);
}
#waveSheet #counter em { display: inline; }

.sheetActs { display: grid; gap: 6px; }
.sheetAct {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: 'name clock' 'line clock';
  gap: 0 8px;
  text-align: left;
  min-height: 48px;
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: rgba(6, 12, 22, 0.6);
  color: var(--accent);
}
.sheetAct b { grid-area: name; font-size: 12px; letter-spacing: 2px; }
.sheetAct span { grid-area: line; font-size: 9px; letter-spacing: 1px; color: var(--dim); }
.sheetAct i {
  grid-area: clock;
  align-self: center;
  font-style: normal;
  font-size: 11px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}
/* Sealed, spent, or nothing to use it on. Dashed like every other place this
   interface means "not yours yet" -- see .railNode.locked and .qc.empty. */
#waveSheet .sheetAct[disabled] {
  border-style: dashed;
  color: color-mix(in srgb, var(--dim) 70%, transparent);
  opacity: 0.65;
  pointer-events: none;
}

/*
 * The three meters, under the rung the run is standing on: contact, time since
 * the last release, and how much of the wave is down. The same three AUDIT
 * shows -- the rail says how it is going, the sheet says why.
 */
.rBars {
  display: none;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 2px;
  text-decoration: none;
  margin-top: 2px;
}
.railNode.live .rBars { display: grid; }
.rBars em {
  display: block;
  height: 2px;
  border-radius: 1px;
  background: color-mix(in srgb, var(--accent) 70%, transparent);
  width: 0;
  /* Stepped in tenths by syncRailBars, so a transition reads as motion rather
     than as a stutter. */
  transition: width 0.18s linear;
}
.rBars em.hot { background: var(--warn); }
@media (prefers-reduced-motion: reduce) { .rBars em { transition: none; } }
/* A gate stands on a rung, so it is honest at any distance -- unlike a trait,
   which belongs to a wave that may not have been chosen yet. */
.railNode.gate { border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }
.railNode.gate.shut .rTraits { color: var(--accent-2); }

/*
 * A panel being pushed away. The transition is what makes a short swipe spring
 * back, so it has to be OFF while the finger is on it -- otherwise the panel
 * lags the thumb by the transition's own duration.
 */
#menu.swiping, #waveSheet.swiping,
/*
 * `#debugPanel` was named here too and has no `transition` of its own to
 * switch off -- a rule that has never done anything. `#aimModes` does.
 */
#aimModes.swiping { transition: none; }

.railNode.trial {
  border-style: dashed;
  border-color: color-mix(in srgb, var(--warn, var(--accent)) 78%, transparent);
  color: color-mix(in srgb, var(--warn, var(--accent)) 88%, var(--dim));
  box-shadow: 0 0 10px color-mix(in srgb, var(--warn, var(--accent)) 20%, transparent);
}

/*
 * The last rung there is, drawn as a closed end.
 *
 * STRUCTURE and not a shade: the right edge is trebled and solid where every
 * other edge on the rail is a hairline, so the ladder reads as walled off at
 * arm's length and to a player who receives no hue at all -- the same rule
 * the dummy's five bands follow. `border-box` is global and the nodes are
 * `1fr`, so a thicker edge moves nothing.
 *
 * It keeps whatever else the node is -- `.at`, `.gate`, `.done`, a trial --
 * because the ceiling is a property of the RUNG and those are properties of
 * the run, and two different facts about one cell must not overwrite each
 * other. Written with the rail's id so it wins the right edge even under
 * `#waveRail.held .railNode.at`, which sets `border-color` as a shorthand.
 */
#waveRail .railNode.end {
  border-right-width: 3px;
  border-right-style: solid;
  border-right-color: color-mix(in srgb, var(--dim) 80%, transparent);
  border-top-right-radius: 1px;
  border-bottom-right-radius: 1px;
}

/*
 * Pinned. The rail stops being a climb and becomes a place you are standing,
 * so it stops wearing the colour the climb is drawn in -- otherwise HELD and
 * AUTO differ by one word on one line and nothing else on the screen.
 */
#waveRail.held .railNode.at {
  border-color: color-mix(in srgb, var(--accent-2) 66%, transparent);
  background: color-mix(in srgb, var(--accent-2) 15%, rgba(6, 12, 22, 0.8));
  color: var(--accent-2);
  box-shadow: 0 0 10px color-mix(in srgb, var(--accent-2) 16%, transparent);
}

/*
 * ...and the line that says which it is, which is also the switch. One
 * control, one sentence, present tense: it reads as the state and it changes
 * the state, and there is nothing else it could be mistaken for.
 */
#railAuto {
  -webkit-appearance: none;
  appearance: none;
  pointer-events: auto;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 8px;
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: rgba(6, 12, 22, 0.72);
  color: var(--dim);
  font: inherit;
  font-size: 9px;
  letter-spacing: 0.16em;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
#railAuto.on {
  border-color: color-mix(in srgb, var(--accent) 34%, transparent);
  color: color-mix(in srgb, var(--accent) 78%, var(--ink));
}
#waveRail.held #railAuto {
  border-color: color-mix(in srgb, var(--accent-2) 40%, transparent);
  color: var(--accent-2);
}
#railAuto:active { transform: translateY(1px); }
/* The long form where it fits and the short one where it does not -- both are
   written every time, so neither can go stale behind the other. */
.rShort { display: none; }
/* Short on anything phone-shaped. It was 392 -- wide enough that a 414 screen
   still read "AUTO PROGRESS ON" -- and that was before the rail carried a
   fourth control. At 414 the long form left the five nodes 24px each. */
@media (max-width: 520px) {
  .rLong { display: none; }
  .rShort { display: inline; }
}

/* ---------------- the furniture gets out of the way ---------------- */
/*
 * See Hud.recede(). The strip and the ability bar own the bottom third of the
 * screen and everything the turret emits is a circle centred on the turret,
 * which sits at 60-70% of the way down it. On a beat where something big is
 * being drawn AND NOTHING IS BEING PRESSED -- an arrival, a stage turning
 * over -- both go to a whisper and come back over a third of a second. Using
 * an ability is not one of those beats and has not been since build 191.
 *
 * `pointer-events` is deliberately NOT dropped. A recede runs for under a
 * second and the player may want PULSE inside it; the button is faint, not
 * gone, and it is in the place their thumb already knows.
 */
#quickBar,
#abilities {
  transition: opacity 0.34s ease;
}
/* One class, both bands. There were two -- `recedeStrip` for the strip alone,
   which is what an ability press asked for, and `recede` for both -- and the
   press stopped asking in build 191, so every caller left wants both. */
body.recede #quickBar,
body.recede #abilities {
  opacity: 0.14;
  /* Out fast, back slowly: the effect it is clearing for has already
     started, and the way back should not pull the eye off it. */
  transition: opacity 0.11s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  body.recede #quickBar,
  body.recede #abilities { opacity: 0.45; transition: none; }
}

/*
 * The live HUD, while the title screen is up.
 *
 * This list used to end in a fourth selector, `#toggleRack`, and that element
 * was removed in the build that replaced it with the menu. The removal took
 * `{ opacity: 0; pointer-events: none }` with it and left the bare
 * `body.booting` dangling, so the parser ran it together with the list that
 * followed and fed the pair the NEXT rule's block. The whole live HUD then
 * painted at full strength behind the title screen: the strip, the ability
 * bar and the top chips all legible through the briefing copy, which reads as
 * a rendering fault on the first thing anybody sees. Asserted in regress.mjs.
 *
 * A matching `body.ending` list sat above this one, along with #endScreen,
 * #endText and #resetBtn. All of it went in build 186: there has been no
 * #endScreen element since the run stopped having an end, and nothing has set
 * the class since either.
 */
/*
 * ...and the rail with them, from build 227. It was not in this list, so the
 * five rung nodes and the AUTO switch sat visibly on top of the title screen
 * -- faint, but plainly interface, and directly behind the wordmark on a
 * 320-wide phone. The title screen is a screen, not an overlay with the game's
 * furniture showing through it; `#alerts` is here for the same reason.
 */
body.booting #topbar,
body.booting #abilities,
body.booting #waveRail,
body.booting #alerts,
body.booting #quickBar {
  opacity: 0;
  pointer-events: none;
}

/* ---------------- narrow phones ---------------- */

/*
 * One block, not two. Everything that has to give up a few pixels on a 320px
 * screen gives them up here: the strip's cells, and the ability labels, whose
 * six-letter names (STASIS, SIPHON) run past a 34px button at the default
 * tracking and get clipped by .ab's overflow.
 */
/*
 * The bar dropping labels because the numbers got long.
 *
 * Set by Hud.fitBar, which measures rather than guessing at a width -- what
 * overflows the group is a long purse and the affordable-rows badge, and no
 * media query can see either. OBJECTS goes first: it is ambient, and the
 * number beside it is still there. The purse's own label slot goes second and
 * only if that was not enough.
 *
 * That second rung does less than it did, and deliberately. It used to drop
 * the word ENERGY; from build 285 the figure carries its own unit and the
 * slot is empty unless a dividend is live, so `.plain` has already taken the
 * space and `tighter` only bites when there is a multiplier in there. The
 * group is better off either way: a purse that read `2400000000` reads
 * `2.40 GB`, which is seven characters against ten.
 */
#barChips.tight #counter em { display: none; }
/*
 * No dividend, so the label slot has nothing to say -- the figure beside it
 * ends in kB or MB and carries its own unit. Written with the id AND the
 * class because the `tighter` rule below also sets `display` on this element;
 * a rule built out of classes alone would lose to it and the slot would keep
 * its 5px flex gap for nothing.
 */
#bytesChip.plain em { display: none; }
#barChips.tighter #bytesChip em { display: none; }

@media (max-width: 372px) {
  /* The green number is unmistakable beside the count; the word is not
     carrying anything the colour does not. */
  #bytesChip em { display: none; }
  #bytesChip { padding: 5px 8px; }

  /* The phase tag steps aside rather than being clipped. A boss announces
     itself with a named bar across the top of the field, so this chip is
     only ever repeating FIELD while there is nothing else to say. */
  /*
   * `#phaseTag { display: none }` lived here: the FIELD readout was the first
   * thing sacrificed when the bar got tight. Its replacement is the ladder's
   * control -- the only way to hold or step down a tier -- and a control that
   * vanishes on a narrow phone is a feature that does not exist there. It
   * shrinks instead.
   */
  /* The arrows keep their 44 of height; the nodes are a readout and narrow. */
  .railStep { width: 32px; font-size: 15px; }
  .railNode { padding: 3px 1px; }
  .railNode b { font-size: 12px; }
  #railAuto { padding: 4px 6px; }

  /* Budgeted, not inherited: 2 stacks + 4 wide cells + the gaps have to
     close inside 320. At the widths above the band ran 14px off the right
     edge -- measured, not noticed, because nothing this narrow was being
     screenshotted. */
  .q_mines, .q_ammo { width: 54px; }
  .qc { height: 28px; gap: 3px; padding: 0 4px; }
  .qc.wide { height: 38px; width: 44px; }
  /* The round cell is a `.qc.wide` and takes that width; the slots in the
     row it opens are grid tracks and size themselves. */
  .roundCell { width: 44px; }
  #ammoRow { bottom: calc(38px + 4px); }
  #ammoRow .qc { height: 38px; }
  .qc svg { width: 13px; height: 13px; }
  .qc .qLbl { font-size: 9px; letter-spacing: 0; }

  .ab svg { width: 18px; height: 18px; }
  .ab .lbl { font-size: 9px; letter-spacing: 0.03em; }

  /* The title screen scrolls rather than clipping, but the one button on it
     below the fold on the smallest phone is a start button nobody finds. The
     wordmark and the copy are what give ground -- the record tiles and the
     button do not, because they are the two things worth coming here for. */
  .bootMark { margin-bottom: 10px; }
  .bootMark span { font-size: 11px; letter-spacing: 0.5em; }
  .bootMark b { font-size: 48px; }
  /* Tighter, never smaller: 11px is the floor everywhere on this panel. */
  .bootStatus { margin-bottom: 12px; padding-bottom: 8px; letter-spacing: 0.1em; }
  .bootBody { font-size: 11px; line-height: 1.55; margin-bottom: 14px; }
  #bootRecord { margin-bottom: 14px; }
  #bootRecord b { font-size: 15px; }
  .bootKeys { margin-top: 14px; }
  .bootKeys li { font-size: 11px; line-height: 1.5; padding: 4px 0; }
  #startBtn, #resumeBtn { padding: 14px; }
  #resumeBtn { font-size: 11px; letter-spacing: 0.16em; }
  .bootFoot { margin-top: 14px; padding-top: 10px; }
  /* A 10px rule for BEGIN SIMULATION sat here. The identical selector
     already sets `display: none`, so it could never paint -- and 10px is
     under the 11px floor this panel's own comment states. Left from build
     227, when NEW RUN stood beside CONTINUE and had to shrink. */

  /* The opening lines carry their own break, and at 320 the plate's padding
     was enough to wrap each half again — four rendered lines for a two-line
     sentence. Tighter tracking and less inset, so the break stays where it
     was written. */
  #abilityHint.tutorial {
    left: calc(var(--safe-l) + 10px);
    right: calc(var(--safe-r) + 10px);
    padding: 8px 11px;
    font-size: 11px;
    letter-spacing: 0.08em;
  }
}

/* Salvage banked. Deliberately brief and small: this fires as often as a
   fragment reaches the intake, and anything longer would be a chip that never
   stops moving on a floor being swept. */
@keyframes tookSalvage {
  0% { transform: scale(1); }
  30% { transform: scale(1.13); }
  100% { transform: scale(1); }
}
#bytesChip.took {
  animation: tookSalvage 0.26s ease-out;
  border-color: rgba(159, 232, 255, 0.55);
}
@media (prefers-reduced-motion: reduce) {
  #bytesChip.took { animation: none; }
}


/* ============================================================
   THE TREE
   Eighty nodes on a 390px screen. An indented outline rather than a drawn
   graph: a row reads at a glance, the rail down the left says what hangs off
   what, and a closed branch costs one line instead of fourteen.
   ============================================================ */

/*
 * :not([hidden]) matters. `.menuPanel[hidden] { display: none }` is declared
 * six hundred lines above this and carries the same specificity, so a bare
 * `.menuPanel.tree { display: flex }` won the cascade by being later and the
 * tree stayed on screen under every other tab -- pick SYSTEM, still see
 * UPGRADES. Any future per-panel display goes the same way.
 */
.menuPanel.tree:not([hidden]) { display: flex; flex-direction: column; gap: 2px; }

.treeHead {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 8px 10px;
  margin-bottom: 6px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  /* Opaque. At 0.96 the rows scrolled visibly through it, which does not read
     as translucency -- it reads as a rendering fault. */
  background: #040a12;
  clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px));
}
.treeHeadName { font-size: 11px; letter-spacing: 0.2em; color: var(--dim); }
.treeHead b {
  font-size: 17px;
  color: #7cffb2;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 10px rgba(124, 255, 178, 0.4);
}
@keyframes treeLanded {
  0%, 55% { border-color: var(--tone); box-shadow: 0 0 18px -4px var(--tone); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.shopCard.refuse { animation: treeRefuse 0.3s ease-out; }
@keyframes treeRefuse {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-3px); border-color: #ff5d8f; }
  75% { transform: translateX(3px); border-color: #ff5d8f; }
}

.menuCell.armed {
  border-color: #ffcf5c;
  background: rgba(255, 207, 92, 0.12);
}
.menuCell.armed .cellName { color: #ffcf5c; }
.menuCell.armed .cellSub { color: rgba(255, 207, 92, 0.8); }


/* ======================================================================
 * THE ROOM
 *
 * What opens above the tree: the machine you are building, how much of it
 * you have built, and the two cheapest things you can afford right now.
 *
 * The tree could say neither of the first two. Screenshot the panel owning
 * nothing, buy all of it, screenshot again, diff below the energy strip:
 * zero differing pixels. The only figure that moved was the wallet, which
 * measures what you have NOT spent. So the machine goes at the top and the
 * count goes under it, and both of them only ever grow.
 * ====================================================================== */

.rigHero {
  position: relative;
  margin: 0 0 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.rigHero canvas {
  display: block;
  width: 100%;
  height: 150px;
  /* The same substrate as the field, so the machine is standing somewhere. */
  background:
    radial-gradient(ellipse at 50% 64%, rgba(89, 224, 255, 0.09), transparent 62%),
    repeating-linear-gradient(180deg, rgba(120, 190, 255, 0.035) 0 1px, transparent 1px 14px);
}

/*
 * What you have built, as one line that never goes down.
 *
 * The total is every level of every finite node bought, and it has no
 * denominator on purpose: there are 136 of them and a run does not reach
 * that, so "17 OF 136" is a permanent failure state rather than a trophy.
 * The branch beside it does have one, because the turret can actually be
 * finished -- and it is the branch the drawing above is made of.
 */
.rigCount {
  font-size: 13px;
  letter-spacing: 0.14em;
  color: #7f93a8;
  padding: 3px 0 9px;
  text-align: center;
}
.rigCount b {
  color: #59e0ff;
  font-size: 15px;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
.rigCount i { font-style: normal; color: #6d8096; margin: 0 6px; }

/* The flare a new part lands with. Drawn over the canvas rather than into
   it: the simulation is held while the menu is open, so the machine's own
   clock is stopped and anything driven off it would never decay. */
.rigFlare {
  position: absolute;
  left: 50%;
  /* On the machine, which drawHero centres at 0.62 of the canvas height. */
  top: 93px;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  border: 2px solid var(--flare, #59e0ff);
  opacity: 0;
  pointer-events: none;
}
.rigFlare.go { animation: rigFlare 0.42s cubic-bezier(0.15, 0.8, 0.3, 1) 1; }
@keyframes rigFlare {
  0% { opacity: 0.95; transform: scale(0.4); }
  100% { opacity: 0; transform: scale(6.2); }
}

/* --------------------------------------------------------- section labels */

.shopLab {
  font-size: 11px;
  letter-spacing: 0.24em;
  /* 5.8:1. It was #56687e -- 3.57:1, under the floor. A section label is
     allowed to be quiet and is not allowed to be unreadable. */
  color: #7d90a6;
  padding: 8px 2px 7px;
  border-top: 1px solid rgba(120, 190, 255, 0.12);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}
/* "44 MORE BELOW" is the one line that says whether opening a branch is
   worth the tap, and it was #3f5165 at 2.5:1. */
.shopLab em { font-style: normal; color: #72859a; letter-spacing: 0.1em; }

/*
 * One object, one target.
 *
 * The tree row has three: the gutter opens it, the body arms it, the price
 * buys it. That is the thing menu.js already fixed once and it is not being
 * reintroduced at a larger size -- the whole card is the button, the first
 * press turns it over, the second spends.
 */
.shopCard {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 0;
  min-width: 0;
  min-height: 184px;
  padding: 12px 8px 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  color: var(--ink);
  border: 1px solid var(--tone, #59e0ff);
  background: rgba(6, 12, 22, 0.86);
  clip-path: polygon(0 0, calc(100% - 9px) 0, 100% 9px, 100% 100%, 9px 100%, 0 calc(100% - 9px));
  box-shadow: 0 0 16px -4px color-mix(in srgb, var(--tone, #59e0ff) 42%, transparent);
  -webkit-tap-highlight-color: transparent;
  transition: border-color 0.14s, box-shadow 0.14s, transform 0.1s;
}
.shopCard:active { transform: translateY(1px); }

/* The art at the size it was drawn for. It was rendering at 18. */
.shopIcon { flex: 0 0 auto; width: 40px; height: 40px; color: var(--tone, #59e0ff); }
.shopIcon svg { width: 40px; height: 40px; display: block; }

.shopBody { display: contents; }
.shopCard.arm .shopBody { display: block; }

.shopName {
  display: block;
  /* Fixed, not shrinkable. The card is a fixed height and these are flex
     items, so without this the name and the stat give up lines to fit --
     which is a clamp nobody wrote, silently cutting SPINES off at "40
     DAMAGE A". */
  flex: 0 0 auto;
  font-size: 16px;
  letter-spacing: 0.1em;
  color: #e6f1fb;
  margin-top: 9px;
  line-height: 1.15;
}
/*
 * No clamp. A card grows to hold what it says.
 *
 * It was `-webkit-line-clamp`, first at two lines and then at three, and both
 * times the answer to a card that overflowed was to shorten the sentence
 * until it fit. That is a guarantee held up by hand: the ellipsis comes back
 * the moment anyone writes a longer line, and it comes back on the half of
 * the sentence that says what the thing does. SPINES ended on "40 DAMAGE A…"
 * and SNARE on "It never…".
 *
 * The clamp was only ever there to keep two cards in a row the same height,
 * and grid does that on its own: the row is as tall as its tallest card and
 * both stretch to it. So the height is a floor now, not a ceiling.
 */
.shopStat {
  display: block;
  flex: 0 0 auto;
  font-size: 12px;
  line-height: 1.3;
  /* 8.4:1 on this ground. The row it replaces was 8px at 2.99:1. */
  color: #9fb8d2;
  margin-top: 5px;
}
.shopMeter {
  display: flex;
  gap: 2px;
  width: 100%;
  height: 4px;
  margin-top: auto;
  margin-bottom: 9px;
  background: rgba(255, 255, 255, 0.08);
}
.shopMeter i {
  flex: 1 1 0;
  background: transparent;
  transition: background 0.18s ease;
}
.shopMeter i.on { background: var(--tone, #59e0ff); }
/* Kept for layout, invisible: see makeCard. `display: none` would hand the
   flex column's spare space to a second auto margin and float the price. */
.shopMeter.none { visibility: hidden; }
.shopPrice {
  display: block;
  width: calc(100% + 16px);
  margin: 0 -8px;
  padding: 8px 0;
  font-size: 19px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: #05080f;
  background: var(--tone, #59e0ff);
}

/*
 * A ROUND, A MINE, AN ABILITY -- the thing itself.
 *
 * Full width, so it heads the mods that follow it, and laid out sideways so
 * the numbers and the effect both fit. What it replaces: a card identical to
 * its own mods, in the same colour at the same size, so HE and OVERPRESSURE
 * read as two upgrades to the same thing.
 */
.shopCard.arm {
  grid-column: 1 / -1;
  height: auto;
  min-height: 104px;
  display: grid;
  grid-template-columns: 56px 1fr auto;
  grid-template-areas: 'kind kind kind' 'icon body meter' 'price price price';
  align-items: center;
  column-gap: 12px;
  padding: 0 12px;
  text-align: left;
  background:
    linear-gradient(100deg, color-mix(in srgb, var(--tone) 13%, transparent), rgba(6, 12, 22, 0.9) 62%);
}
.shopCard.arm .shopKind {
  grid-area: kind;
  align-self: start;
  font-size: 11px;
  letter-spacing: 0.26em;
  color: var(--tone);
  padding: 9px 0 7px;
}
.shopCard.arm .shopIcon { grid-area: icon; width: 52px; height: 52px; }
.shopCard.arm .shopIcon svg { width: 52px; height: 52px; }
.shopCard.arm .shopBody { grid-area: body; min-width: 0; }
.shopCard.arm .shopName { font-size: 20px; margin-top: 0; }
.shopCard.arm .shopStat { margin-top: 4px; }
.shopCard.arm .shopMeter { grid-area: meter; width: 46px; margin: 0; align-self: center; }
.shopCard.arm .shopPrice { grid-area: price; width: calc(100% + 24px); margin: 10px -12px 0; text-align: center; }

/* The numbers, on a line of their own. An arm's own text leads with its
   damage -- "Damage 11, then 25 a jump" -- and putting that where the effect
   goes is how ARC came to say nothing about jumping to four more bodies. */
.shopSpec {
  display: block;
  font-family: inherit;
  font-size: 13px;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
  color: var(--tone);
  margin-top: 3px;
}
.shopSpec[hidden] { display: none; }

/* Issued, not bought: BOLT, PULSE and FAN. */
.shopCard.issued .shopPrice { letter-spacing: 0.26em; font-size: 13px; }

/* Too dear: the same size, dimmed. Legible is a state; gone is not. */
.shopCard.poor {
  border-color: color-mix(in srgb, var(--tone, #59e0ff) 30%, transparent);
  box-shadow: none;
}
.shopCard.poor .shopIcon { opacity: 0.45; }
.shopCard.poor .shopName { color: #7d8ea1; }
.shopCard.poor .shopPrice { background: transparent; color: #6a7e93; }

/*
 * Armed. The card turns over instead of a border changing hue: the price
 * fills the face and the whole thing says what the next press does.
 */
.shopCard.armed { border-color: #ffffff; }
/*
 * `display`, not `visibility`. Hidden-but-laid-out, the icon and the stat
 * still held their 60-odd pixels, the name and the big price were added on
 * top, and the BUY bar was pushed past the bottom edge -- where the card's
 * own clip-path cut it off. The button said nothing at all.
 */
.shopCard.armed .shopIcon,
.shopCard.armed .shopStat,
.shopCard.armed .shopMeter { display: none; }
.shopCard.armed .shopName {
  /* Two auto margins: the free space splits above the name and above the
     bar, so the price centres and BUY stays welded to the bottom edge. */
  margin-top: auto;
  font-size: 15px;
  color: var(--tone, #59e0ff);
}
.shopCard.armed .shopName::after {
  content: attr(data-price);
  display: block;
  margin-top: 8px;
  font-size: 30px;
  letter-spacing: 0;
  color: #e6f1fb;
  font-variant-numeric: tabular-nums;
}
.shopCard.armed .shopPrice { margin-top: auto; background: #e6f1fb; color: #05080f; }


@media (prefers-reduced-motion: reduce) {
  .rigFlare.go { animation: none; }
}


/* ======================================================================
 * YOUR MACHINE
 *
 * What the tree was. Five rows, one per category, each with a meter in its
 * own colour and its own fraction: the shape of eighty-three purchases in
 * five lines. Tap one and it opens in place, and opening one shuts the last,
 * so the top of the panel is never more than a screen away.
 * ====================================================================== */

.branch { --tone: #8fb6d8; margin-bottom: 2px; }
.branch.shut > .branchGrid { display: none; }

.branchRow {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 52px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 11px;
  text-align: left;
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--tone) 34%, transparent);
  background: rgba(10, 20, 34, 0.86);
  clip-path: polygon(0 0, calc(100% - 7px) 0, 100% 7px, 100% 100%, 7px 100%, 0 calc(100% - 7px));
  -webkit-tap-highlight-color: transparent;
  transition: border-color 0.14s, background 0.14s;
}
.branchRow:active { transform: translateY(1px); }

.branchIcon { flex: 0 0 auto; width: 22px; height: 22px; color: var(--tone); }
.branchIcon svg { width: 22px; height: 22px; display: block; }

.branchName {
  flex: 0 0 auto;
  /* Wide enough for AMMUNITION. At 92px the name overran its box and the
     meter's filled end sat against the last letter, which read as a glitch. */
  width: 112px;
  font-size: 14px;
  letter-spacing: 0.15em;
  color: var(--tone);
}

/* The meter. Three 6px hollow squares is what this replaces, which is a
   progress bar drawn at the size of a full stop. */
.branchBar {
  flex: 1 1 auto;
  height: 5px;
  min-width: 20px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.branchBar > i {
  display: block;
  height: 5px;
  width: 0;
  background: var(--tone);
  transition: width 0.3s cubic-bezier(0.2, 0.8, 0.3, 1);
}
.branchFrac {
  flex: 0 0 auto;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: #8fa6bd;
  min-width: 44px;
  text-align: right;
}
/* Points right when the branch is shut and down when it is open: the one
   convention nobody has to be taught. */
.branchCaret {
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-right: 1.6px solid var(--tone);
  border-bottom: 1.6px solid var(--tone);
  transform: rotate(-45deg);
  opacity: 0.8;
  transition: transform 0.16s ease;
}
.branch:not(.shut) > .branchRow .branchCaret { transform: rotate(45deg); }

/* Something in there is affordable right now. The reason to open it, without
   opening it. */
.branch.reach > .branchRow {
  border-color: color-mix(in srgb, var(--tone) 80%, transparent);
  box-shadow: 0 0 14px -5px var(--tone);
}
.branch.done > .branchRow { background: linear-gradient(90deg, color-mix(in srgb, var(--tone) 13%, transparent), rgba(10, 20, 34, 0.9)); }
.branch.done .branchFrac { color: var(--tone); }

/* ANOMALY wears every boss colour at once — the one row that says how many
   of these there are going to be. */
.branch.spectrum > .branchRow .branchIcon { color: #e6f1fb; }
.branch.spectrum .branchBar > i { background: linear-gradient(90deg, var(--tones)); }
.branch.spectrum > .branchRow .branchName {
  background: linear-gradient(90deg, var(--tones));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ------------------------------------------------------------- the grid */

.branchGrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Both cards in a row take the height of the taller one, which is the job
     the clamp used to do by cutting the taller one's text off. */
  align-items: stretch;
  gap: 8px;
  padding: 8px 0 14px;
}
/*
 * A heading, not a purchase: ALL ROUNDS, and the three arms the turret starts
 * with. It is what the indent used to say -- the cards under it belong to it
 * -- said by adjacency and a rule in its colour instead.
 */
.grpLab {
  grid-column: 1 / -1;
  font-size: 11px;
  letter-spacing: 0.22em;
  color: var(--tone);
  padding: 10px 2px 4px;
  border-bottom: 1px solid color-mix(in srgb, var(--tone) 30%, transparent);
}

/*
 * ---- the rows that apply to EVERYTHING, in a second channel ----
 *
 * `--tone` already separates them: the three ALL-X groups wear a warm bone
 * (GROUP_TONE in tree.js) where every arm wears its own saturated hue, which
 * is the fix for ALL MINES having been BLAST's colour to within dE 0.6.
 *
 * This is the same statement made structurally, because a distinction carried
 * only in a hue is one a colourblind player does not receive -- and because
 * these rows sit in the same grid as the arm rows they are meant to be told
 * apart from. A universal card carries a second inset edge, so it reads as a
 * card with something around it rather than as a card of a different colour;
 * the heading over them repeats the same doubling as a rule under a rule.
 *
 * `inset` rather than a real border so the box model is untouched -- these
 * cards sit in a grid with the arm cards and must not be a pixel wider.
 */
.shopCard.univ {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--tone, #d7c49a) 26%, transparent),
    inset 0 0 0 4px rgba(6, 12, 22, 0.86),
    inset 0 0 0 5px color-mix(in srgb, var(--tone, #d7c49a) 42%, transparent),
    0 0 16px -4px color-mix(in srgb, var(--tone, #d7c49a) 42%, transparent);
}

.grpLab.univ {
  border-bottom-style: double;
  border-bottom-width: 4px;
}

/* ------------------------------------------------- the card's other states */

/*
 * Yours, and finished. The brightest thing on the screen.
 *
 * A bought row used to grey out and get a tick, so colour meant FOR SALE and
 * grey meant YOURS -- and finishing the tree made the tree go dim. A tick is
 * a receipt; a lit part is a trophy.
 */
.shopCard.own {
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--tone) 14%, transparent), rgba(6, 12, 22, 0.9));
  box-shadow: inset 0 0 22px -10px var(--tone);
}
.shopCard.own .shopName { color: var(--tone); }
.shopCard.own .shopPrice { font-size: 16px; letter-spacing: 0.3em; }

/* Behind something unbought. Dim, still readable, still priced — the reason
   to buy the thing in front of it is what is behind it. */
.shopCard.locked {
  border-style: dashed;
  border-color: rgba(120, 190, 255, 0.16);
  box-shadow: none;
}
.shopCard.locked .shopIcon { opacity: 0.3; }
/* Dim is a state the border and the icon carry; the words stay readable.
   These were #5d6d80 / #4d5f74 / #45566a -- 3.7, 3.0 and 2.6 to one against
   a 4.5 floor, on the cards whose text is the reason to buy the arm above
   them. The contrast sweep only caught it once the branch order changed and
   it finally measured a branch that had locked cards in it. */
.shopCard.locked .shopName { color: #8a99ab; }
.shopCard.locked .shopStat { color: #7b8b9e; }
.shopCard.locked .shopPrice { background: transparent; color: #7b8b9e; }

/* A row you were sent to, so arriving reads as arriving. */
.branchRow.landed { animation: treeLanded 1.1s ease-out; }

@media (prefers-reduced-motion: reduce) {
  .branchBar > i { transition: none; }
  .branchRow.landed { animation: none; }
}


/* ======================================================================
 * THE REST OF THE MENU
 *
 * OBJECTS and SYSTEM never had the pass UPGRADES had. Measured on the same
 * 390x664 phone: every one of OBJECTS' 78 pieces of text was under 11px, 72
 * of them failed 4.5:1, and the worst was 1.82. SYSTEM's 23 were all under
 * 11px too. The shop had a floor and the other two tabs did not, which is
 * one interface with two standards in it.
 * ====================================================================== */

/* -------------------------------------------------------- shared headings */

.menuHead { font-size: 12px; letter-spacing: 0.2em; }
.menuHead em { font-size: 11px; color: #7d90a6; }
.menuHead[hidden] { display: none; }

/* ------------------------------------------------------- OBJECTS: the head */

/*
 * The count, first and as a meter. It was "0/34" at 8.5px in the title bar --
 * the one number this whole tab is about, set smaller than anything else on
 * the screen and parked beside the close button.
 */
.codexHead {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 11px;
  margin-bottom: 4px;
  border: 1px solid rgba(120, 190, 255, 0.22);
  background: rgba(10, 20, 34, 0.86);
  clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px));
}
.codexCount {
  flex: 0 0 auto;
  font-size: 13px;
  letter-spacing: 0.14em;
  color: #8fa6bd;
  font-variant-numeric: tabular-nums;
}
.codexCount b { color: #59e0ff; font-size: 17px; letter-spacing: 0.04em; }
.codexBar {
  flex: 1 1 auto;
  height: 5px;
  min-width: 24px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.codexBar > i {
  display: block;
  height: 5px;
  width: 0;
  background: #59e0ff;
  transition: width 0.35s cubic-bezier(0.2, 0.8, 0.3, 1);
}

.codexNone {
  font-size: 12px;
  line-height: 1.5;
  color: #8fa6bd;
  padding: 14px 12px 18px;
}
.codexNone[hidden] { display: none; }

/* --------------------------------------------------- OBJECTS: what is known */

.codexName { font-size: 14px; letter-spacing: 0.1em; }
.codexLine { font-size: 12px; line-height: 1.4; color: #9fb8d2; }
.codexSpec { font-size: 11px; }
.codexSpec b { font-size: 11px; color: #9fb8d2; }
.codexSpec i { font-size: 11px; color: #9fb8d2; }

/* ------------------------------------------------- OBJECTS: what is not yet */

/*
 * A block of blanks, not a list of them. Thirty-four full rows each saying
 * "No record. Destroy one." was 2166px -- 3.8 screens of the same sentence.
 * The sentence is on the heading now and these are tiles: the block shrinks
 * as the list above it grows, which is the shape of a collection.
 */
.codexUnseen[hidden] { display: none; }
.codexUnseen {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
  gap: 7px;
  padding: 4px 2px 16px;
}
.codexUnseen .codexCell {
  display: block;
  padding: 0;
  border: 1px dashed rgba(120, 190, 255, 0.2);
  background: rgba(6, 12, 22, 0.5);
  aspect-ratio: 1;
  min-height: 0;
}
.codexUnseen .codexCell .codexArt {
  width: 100%;
  height: 100%;
  border: 0;
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.codexUnseen .codexCell .codexArt::after {
  content: '?';
  font-size: 17px;
  color: rgba(120, 190, 255, 0.26);
}
.codexUnseen .codexCell canvas,
.codexUnseen .codexCell .codexBody { display: none; }

/* ------------------------------------------------------- SYSTEM: the volume */

.volWord {
  flex: 0 0 auto;
  min-width: 52px;
  text-align: right;
  font-size: 13px;
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums;
  color: #7cffb2;
}
.volWord.off { color: #8fa6bd; }
.volName { font-size: 13px; letter-spacing: 0.16em; }

/* --------------------------------------------------------- SYSTEM: the rows */

.optName { font-size: 13px; letter-spacing: 0.16em; }
.optWord { font-size: 13px; letter-spacing: 0.1em; }
.menuKey > span { font-size: 13px; letter-spacing: 0.14em; }
.menuKey p { font-size: 12px; line-height: 1.5; color: #9fb8d2; }

/* The one that cannot be undone: on its own, full width, and red enough that
   nothing about it reads as a convenience. */
.menuCell.wipe {
  --tone: #ff5f6d;
  border-color: color-mix(in srgb, var(--tone) 45%, transparent);
  background: linear-gradient(100deg, color-mix(in srgb, var(--tone) 10%, transparent), rgba(6, 12, 22, 0.9) 60%);
}
.menuCell.wipe .cellName { color: var(--tone); }
.menuCell.wipe.armed {
  border-color: var(--tone);
  box-shadow: 0 0 18px -5px var(--tone);
}
.menuCell.wipe.armed .cellSub { color: var(--tone); }

/* ------------------------------------------------------- SYSTEM: the stamp */

/*
 * Readable. It was 8.5px at 1.84 to one, and its whole job is being compared
 * against the same line on another device.
 */
.menuStamp {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  padding: 16px 12px 6px;
  font-size: 11px;
  letter-spacing: 0.14em;
  color: #7d90a6;
}
.menuStamp b {
  color: #9fb8d2;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
}


/* ---------------------------------------------------------- the stack fold */

/*
 * Show/hide at the FOOT of each stack. Four mine slots and four ammunition
 * slots is a lot of column on a phone, and a run that lays no mines is
 * looking straight past that side of the screen. The button never folds
 * away with the stack -- a control that hides itself is one nobody finds.
 *
 * 28px, not the 22 it was at the head of the column. At the foot it sits on
 * the floor line with `#abilities` 14px below it, and a 22px target with
 * PULSE that close is a mis-hit waiting to happen -- the cell that used to
 * hold this seat was a full 40. It is still the shortest thing in the bar,
 * because it is the only control here that does nothing to the field.
 */
.qc.fold {
  height: 28px;
  min-height: 28px;
  gap: 6px;
  padding: 0 8px;
  border-color: rgba(120, 190, 255, 0.16);
  background: rgba(6, 12, 22, 0.7);
}
.qc.fold .qLbl { color: #8fa6bd; }
.foldArrow {
  /* The arrow is a bare span and `.qc svg`'s `flex: 0 0 auto` does not reach
     it, so it was the flex item that gave ground: measured, 8px down to 3px
     at 320 the moment the label beside it had anything in it. `.qc` clips
     rather than scrolls, so it failed with nothing to read back. */
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-left: 1.6px solid #8fa6bd;
  border-bottom: 1.6px solid #8fa6bd;
  transform: rotate(-45deg) translate(1px, -1px);
  transition: transform 0.16s ease;
}
.qc.fold.off .foldArrow { transform: rotate(135deg) translate(1px, -1px); }
.qc.fold:active { border-color: rgba(120, 190, 255, 0.4); }

/*
 * Folded: the slots go, the fold stays -- and so does the door.
 *
 * `.cfg` is the button that opens the group's tab in the sheet, and from
 * build 292 it lives at the FOOT of the ammunition column rather than in a
 * band of its own (see buildStrip: two equal columns and two empty bands is
 * what puts AIM and FIRE in the middle of a `space-between` strip). A door
 * folded away with the slots is a tab reachable only by unfolding first.
 */
.qGroup.folded > .qc:not(.fold):not(.cfg) { display: none; }

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

/* ============================== SANDBOX ==============================
 *
 * The bench. One bar across the top and one sheet under it.
 *
 * Everything here is a player's, so it is held to the 11px floor the title
 * screen is already held to -- the debug panel next door runs at 7.5-10.5px
 * because it is a developer's, and this borrowed those sizes for one build
 * while it still had that panel's spawn picker in it.
 */
#sandbox {
  position: absolute;
  inset: 0;
  z-index: 46;
  pointer-events: none;
}
#sandbox[hidden] { display: none !important; }

/*
 * The bar takes the top bar's place rather than sitting under it.
 *
 * In the range the OBJECTS chip counts nothing and the energy chip buys
 * nothing, so those 58px were being spent on two readouts that do not apply
 * -- and on a 568-tall screen they were the difference between the panel
 * clearing the rig and landing on it. The hamburger stays: the tree is worth
 * reaching without leaving, and it is the only other door up there.
 */
#sbBar {
  position: absolute;
  top: var(--hud-t);
  left: max(10px, var(--safe-l));
  right: calc(max(10px, var(--safe-r)) + 56px);
  height: 44px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: rgba(10, 18, 28, 0.86);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  pointer-events: auto;
}
.sbTag {
  font-size: 11px;
  letter-spacing: 0.16em;
  color: #8fb8d8;
  /* Takes the slack, so the two doors sit hard right whatever the name is. */
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 36px in a 44px bar, against the 31 they were: the bar is already the full
   height and the buttons were floating 6.5px of dead glass above and below
   themselves. A thumb covers roughly a 44px disc (config.js). */
.sbBtn {
  -webkit-appearance: none;
  appearance: none;
  flex: none;
  min-height: 36px;
  padding: 8px 10px;
  font-size: 11px;
  letter-spacing: 0.1em;
  font-family: inherit;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(16, 30, 46, 0.9);
  color: var(--ink);
}

/*
 * The panel, and it is ALWAYS up.
 *
 * It was a 56vh sheet behind a STATS toggle, and it landed exactly on the
 * dummy -- so you could read the rates or watch the thing they were about,
 * and not both. The four rates and the clock are about 116px together, which
 * clears the rig on every screen the game is played at; the only thing that
 * can cover the field is the per-source table, which is a list of unbounded
 * length and folds away by default.
 */
#sbPanel {
  position: absolute;
  /* 48 for the era row's top + 32 for its height + 6 of air. */
  top: calc(var(--hud-t) + 86px);
  left: max(10px, var(--safe-l));
  right: max(10px, var(--safe-r));
  /* `dvh`, like the rest of the file: 62vh on iOS is 62% of the LARGE
     viewport, i.e. of a screen the URL bar is not on. */
  max-height: 62dvh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 9px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: rgba(8, 15, 24, 0.9);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
  pointer-events: auto;
}
.sbTable[hidden] { display: none !important; }

/*
 * ---- the lead row: the one thing that is always up ----
 *
 * The ten-second rate, how far round the record has got, and the chevron.
 * This is what `Sandbox.standoff` measures, so it is the ONE part of the
 * readout that must not change size -- the numbers are tabular and the
 * lifetime cell is a fixed shape for exactly that reason.
 */
.sbLine {
  display: flex;
  align-items: baseline;
  gap: 7px;
  font-variant-numeric: tabular-nums;
}
.sbLine b { font-size: 20px; font-weight: 600; color: var(--ink); line-height: 1.15; }
.sbLine em {
  font-size: 11px;
  font-style: normal;
  letter-spacing: 0.1em;
  color: var(--dim);
}
.sbLineRec {
  margin-left: auto;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: #8fb8d8;
}
/* The chevron. A bare caret drawn in CSS rather than an svg, because it is
   two lines and the file already has three of these. */
.sbMoreBtn {
  -webkit-appearance: none;
  appearance: none;
  flex: none;
  width: 38px;
  min-height: 34px;
  align-self: center;
  position: relative;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(16, 30, 46, 0.9);
}
.sbMoreBtn::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 8px;
  height: 8px;
  margin: -6px 0 0 -4px;
  border-right: 1.5px solid #a9c0d6;
  border-bottom: 1.5px solid #a9c0d6;
  transform: rotate(45deg);
  transition: transform 0.16s ease, margin 0.16s ease;
}
.sbMoreBtn.on { border-color: #8fb8d8; }
.sbMoreBtn.on::before { transform: rotate(225deg); margin-top: -2px; }

/* Everything else. `[hidden]` needs the guard because this carries a display. */
.sbMore { margin-top: 8px; }
.sbMore[hidden] { display: none !important; }

/* RESET and the fold, side by side: two full-width buttons stacked would be
   28px of height spent on nothing, and that 28px is the margin the panel
   clears the rig by on a 568-tall screen. */
.sbActs { display: grid; grid-template-columns: 1fr auto; gap: 6px; margin: 7px 0 0; }
.sbFold {
  -webkit-appearance: none;
  appearance: none;
  min-height: 36px;
  padding: 9px 11px;
  font-size: 11px;
  letter-spacing: 0.1em;
  font-family: inherit;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(16, 30, 46, 0.9);
  /* Was `--dim`, which measures 4.28:1 on this plate over every sky the room
     uses -- under the 4.5 the rest of the player's interface is held to. */
  color: #a9c0d6;
  white-space: nowrap;
}
.sbFold.on { border-color: #8fb8d8; color: #cfe6f6; }

/* ---- the stats table ---- */
/* The unit, said once over the row instead of four times inside it. */
.sbCap {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin: 0 1px 5px;
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--dim);
}
.sbCapWin { color: rgba(150, 180, 210, 0.55); }

/*
 * The record: the one row here that is not the session.
 *
 * A hairline above it and a tag of its own, because RESET COUNTER clears
 * everything else on the panel and does not touch this -- a player who could
 * not tell them apart would read the reset as broken.
 */
.sbRec {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 7px;
  margin-top: 7px;
  padding-top: 7px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.sbRecTag { color: #8fb8d8; }
.sbRecBar {
  min-width: 46px;
  height: 5px;
  border-radius: 3px;
  background: rgba(120, 190, 255, 0.1);
  overflow: hidden;
}
.sbRecBar b {
  display: block;
  height: 100%;
  min-width: 2px;
  background: rgba(125, 148, 176, 0.85);
  transition: width 0.3s linear;
}
.sbRecNum { font-size: 12px; color: var(--ink); }
/* Third column, on its own line under the number: "47/100 · 40k TO NEXT" is
   too long to sit beside it at 320 and too useful to drop. */
.sbRecOf { grid-column: 3; font-size: 11px; text-align: right; }

.sbStatHead { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.sbStatHead div {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 5px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(14, 26, 42, 0.7);
}
.sbStatHead b { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
.sbStatHead em { font-size: 11px; font-style: normal; letter-spacing: 0.08em; color: var(--dim); }
.sbStatSub {
  display: flex;
  justify-content: space-between;
  gap: 6px;
  margin-top: 6px;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.sbReset {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  min-height: 36px;
  padding: 9px 6px;
  font-size: 11px;
  letter-spacing: 0.12em;
  font-family: inherit;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(16, 30, 46, 0.9);
  color: var(--ink);
}
.sbTable h4 {
  margin: 10px 0 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  color: var(--dim);
}
.sbRow, .sbHeadRow {
  display: grid;
  grid-template-columns: 8px 1fr 46px 42px 46px;
  align-items: center;
  gap: 6px;
  padding: 4px 0;
}
.sbHeadRow { font-size: 11px; letter-spacing: 0.08em; color: var(--dim); }
.sbRow i { width: 8px; height: 8px; border-radius: 2px; display: block; }
.sbName { font-size: 11px; letter-spacing: 0.05em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sbBar { height: 6px; border-radius: 3px; background: rgba(120, 190, 255, 0.1); overflow: hidden; }
.sbBar b { display: block; height: 100%; }
.sbNum, .sbTot { font-size: 11px; text-align: right; font-variant-numeric: tabular-nums; }
.sbTot { color: var(--dim); }
.sbEmpty { margin-top: 12px; font-size: 11px; line-height: 1.5; color: var(--dim); }

/*
 * ...and the run's own chrome, taken down.
 *
 * One class on the body, with a rule per element, because `hidden` loses to
 * any id rule that sets `display` -- the trap CLAUDE.md keeps a note about.
 * The object count, the quick strip and the ability bar all stay: they are
 * the kit, and the kit is what the bench is for.
 */
body.sandbox #barChips,
body.sandbox #waveRail { display: none !important; }

/*
 * ...and the two things left up there, put right.
 *
 * The bar sits at `--hud-t` and `#topbar` sits at `--hud-t + 8px`, so the
 * hamburger was 8px lower than the bar beside it -- two staggered 44px boxes
 * across the top of the screen. And `#sbPanel` starts at `--hud-t + 50` while
 * the button's box ends at `--hud-t + 52`, so the panel (z 46, the highest in
 * this file, `pointer-events: auto` across its whole rectangle) sat on the
 * door's bottom 2px and ate those taps.
 *
 * Both are fixed by moving the BUTTON rather than the panel: a pixel spent
 * pushing the panel down is a pixel the rig does not get, and on a 568-tall
 * screen the rig clears the panel by six of them.
 */
body.sandbox #topbar { top: var(--hud-t); }
body.sandbox #menuBtn { z-index: 47; }

/*
 * The alerts column is behind the panel in this room, and it is reachable:
 * the hamburger is still live, the tree is two taps away, and buying anything
 * fires `${tier} FITTED`. `#alerts` lives at `--under-rail`, which is inside
 * the panel's box on every screen -- so it is lifted over it rather than
 * moved, because a 2.6s pill over a readout is better than a 2.6s pill
 * nobody sees, and moving it would put it on the rig instead.
 */
body.sandbox #alerts { z-index: 47; }

/* The SANDBOX room's open half. The shut half wears `.sealedRoom`, which
   ULTIMATE already defines. */
.sbRoom {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 26px 18px;
  text-align: center;
}
.sbRoom[hidden] { display: none !important; }
.sealedRoom[hidden] { display: none !important; }
.sbCost { color: #8fb8d8; }
.sbEnter, .ufEnter {
  -webkit-appearance: none;
  appearance: none;
  margin-top: 6px;
  padding: 13px 22px;
  font-size: 12px;
  letter-spacing: 0.16em;
  font-family: inherit;
  border: 1px solid #8fb8d8;
  border-radius: 7px;
  background: rgba(143, 184, 216, 0.14);
  color: #cfe6f6;
}
/* 0.4 put the label at 3.23:1 and said nothing about why. */
.sbEnter:disabled, .ufEnter:disabled { opacity: 0.62; }

/* The padlock comes off when the run buys the node. A class, because
   `[hidden]` loses to the rule that gives `.tabLock` its display. */
.menuTab.unlocked .tabLock { display: none !important; }

/* The bench's own furniture is play-screen furniture: a sheet over the whole
   screen is not the place to leave it standing. Same rule the aim row has. */
body.menuOpen #sandbox { display: none !important; }



/* The last session, in the SANDBOX room. The tab is worth opening outside the
   bench because of this; before it, the room was one button and two thirds of
   a screen of nothing. */
.sbLast { width: 100%; margin-top: 18px; text-align: left; }
.sbLast[hidden] { display: none !important; }
.sbLast h5 {
  margin: 16px 0 7px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  color: var(--dim);
}
/* The first heading in the block, whichever it is: with no session on record
   LIFETIME is the only one there and must not be pushed down. */
.sbLast h5.first { margin-top: 0; }
.sbLastHead { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin-bottom: 8px; }
.sbLastHead div {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 7px 5px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(14, 26, 42, 0.7);
}
.sbLastHead b { font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.sbLastHead em { font-size: 11px; font-style: normal; letter-spacing: 0.04em; color: var(--dim); }
.sbLastRow {
  display: grid;
  grid-template-columns: 8px 1fr auto;
  align-items: center;
  gap: 7px;
  padding: 4px 0;
  font-size: 11px;
}
.sbLastRow i { width: 8px; height: 8px; border-radius: 2px; }
.sbLastRow b { font-weight: 600; font-variant-numeric: tabular-nums; }

/* The record, under the session. Same shape as the panel's own LIFETIME row
   so the two readouts of one number look like one number. */
.sbLife {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 8px;
  margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.sbLifeBar {
  height: 6px;
  border-radius: 3px;
  background: rgba(120, 190, 255, 0.1);
  overflow: hidden;
}
.sbLifeBar b { display: block; height: 100%; min-width: 2px; background: rgba(125, 148, 176, 0.85); }
.sbLifeNum { font-size: 13px; font-weight: 600; color: var(--ink); }
.sbLifeOf {
  grid-column: 1 / -1;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--dim);
}

/*
 * A short screen. 320x568 shows 916 world units of depth against a 390x844's
 * 1361, so the readout and the rig are competing for a band that is 169 units
 * tall -- and the rig is 136 of it. Ten pixels off the panel is the
 * difference between the two clearing each other and not.
 */
/*
 * ---- 568 tall, and every pixel of the panel is one the rig does not get ----
 *
 * `Sandbox.standoff` stands the dummy clear of the panel's measured bottom
 * edge, but it clamps at `DUMMY.nearest` -- and on a 320x568 screen the turret
 * sits at world y 551, so the clamp is what governs and the panel simply has
 * to fit above it. Measured: 127px is the whole budget, against 180 on a
 * 390x844. So the caption goes (the room is the unit), the record's second
 * line folds up beside its first, and everything else gives a pixel or two.
 * `regress.mjs` asserts the clearance at 320x568 rather than trusting this.
 */
@media (max-height: 660px) {
  #sbPanel { padding: 6px; }
  .sbLine b { font-size: 18px; }
  .sbCap { display: none; }
  .sbStatHead { gap: 5px; }
  /* The tiles lie DOWN on a short screen: "4.2k 10s" on one line instead of
     stacked, which is 14px of the ~16 the 11px floor cost. Measured at 320:
     68px of column against 56px of content, so nothing wraps. */
  .sbStatHead div {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: 3px;
    padding: 5px 5px;
  }
  .sbStatHead b { font-size: 13px; }
  /* 11px stands. Everything in this room is a player's, and the 11px floor is
     the one the title screen, the menu and the strip are all held to -- so the
     pixels come out of the padding and the buttons, not out of the type. */
  .sbStatSub { margin-top: 4px; }
  .sbBtn, .sbReset, .sbFold { min-height: 34px; }
  /* The record keeps its number and gives up its rule and its second line: it
     is the row that must not be dropped, because it is the only one on the
     panel that is still true tomorrow. */
  /* The record keeps its own two-row shape here. It was folded onto one line
     while the panel was fighting for height; now that everything below the
     lead row is behind a chevron and allowed to cover the field, the height
     is not worth the collapsed bar it cost -- `1fr` between two long cells
     went to zero and the shell's own progress bar simply was not drawn. */
  .sbRec { margin-top: 5px; padding-top: 5px; }
  .sbActs { margin-top: 5px; }
  .sbReset, .sbFold { padding: 7px 6px; }
}

/* ---- the evolution -------------------------------------------------------
 * The chrome stands down for the approach and the unmaking. One rule rather
 * than eight inline styles, so it is legible and so reduced motion inherits
 * it: the class is applied on the same act boundaries whatever the span.
 * `pointer-events` goes with it — a control that has faded out must not still
 * take the tap that was meant to skip.
 */
body.evolving #quickBar,
body.evolving #abilities,
body.evolving #abilityHint,
body.evolving #barChips,
body.evolving #waveRail,
body.evolving #menuBtn {
  opacity: 0;
  pointer-events: none;
  transform: translateY(14px);
  transition: opacity 1.4s ease, transform 1.4s ease;
}
body.evolving #abilities { transform: translateY(28px); }
body.evolving #barChips,
body.evolving #waveRail { transform: translateY(-18px); }
@media (prefers-reduced-motion: reduce) {
  body.evolving #quickBar,
  body.evolving #abilities,
  body.evolving #abilityHint,
  body.evolving #barChips,
  body.evolving #waveRail,
  body.evolving #menuBtn { transition: opacity 0.3s linear; transform: none; }
}

/* The two counters in the NEW FORM room. A met half is stated as met rather
 * than only coloured: a rule that lives in a hue is one a colourblind player
 * never receives. */
.sealedRoom .ufNeed,
.sealedRoom .ufNeed2 {
  font: 600 11px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
  letter-spacing: 0.08em;
  color: #7fa8c8;
  opacity: 0.75;
}
.sealedRoom .ufMet { color: #7ef0b0; opacity: 1; }
.sealedRoom .ufMet::after { content: " \2713"; }

/* ---- the NEW FORM banner -------------------------------------------------
 * Over the middle of the field, not in the top furniture: `Hud.pillCap`
 * measures the gap up there and can legitimately return 0 on a 568-tall
 * screen, so anything added to that column costs a teaching line. This is up
 * once in a run and the field behind it is about to be taken anyway.
 *
 * The `[hidden]` guard is not optional. `[hidden] { display: none }` is the
 * user agent's, at one class of specificity, and loses to any author rule
 * written on an id — so without this the property flips, every test that reads
 * it back agrees, and the element stays on the screen taking taps.
 */
#newFormBar {
  position: absolute;
  left: 50%;
  top: 44%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
  max-width: 84vw;
  padding: 14px 22px;
  border: 1px solid #ffd9f6;
  border-radius: 10px;
  background: rgba(10, 6, 16, 0.88);
  box-shadow: 0 0 24px rgba(255, 217, 246, 0.18);
  color: #ffd9f6;
  pointer-events: auto;
  animation: nfBreathe 2.6s ease-in-out infinite;
}
#newFormBar[hidden] { display: none; }
#newFormBar .nfTitle {
  font: 700 15px/1.1 ui-monospace, "SF Mono", Menlo, monospace;
  letter-spacing: 0.16em;
}
#newFormBar .nfLine {
  font: 500 11px/1.4 ui-monospace, "SF Mono", Menlo, monospace;
  letter-spacing: 0.04em;
  color: #cfd8e6;
  text-align: center;
}
@keyframes nfBreathe {
  0%, 100% { box-shadow: 0 0 20px rgba(255, 217, 246, 0.14); }
  50% { box-shadow: 0 0 34px rgba(255, 217, 246, 0.30); }
}
@media (prefers-reduced-motion: reduce) { #newFormBar { animation: none; } }

/* ---- the emplacements ---------------------------------------------------
 *
 * The TURRETS tab. The shut state reuses `.sealedRoom` wholesale -- it is the
 * same shape of door as the testbed's and there is no reason for a second
 * vocabulary. Open, it is a header, one switch and the tree's own card grid,
 * so almost everything here is spacing.
 */
.gunRoom { display: flex; flex-direction: column; gap: 10px; }
/* `.gunRoom` gives itself a `display`, so it owes `[hidden]` a guard: the user
 * agent's `[hidden] { display: none }` is one class of specificity and loses
 * to any author rule written on a class. Without this the property flips, the
 * test reads it back happily, and the room stays on the screen. */
.gunRoom[hidden] { display: none !important; }
.gunHead { display: flex; flex-direction: column; gap: 4px; }
.gunState {
  font-size: 11px; letter-spacing: .16em; color: #8fb8e8; opacity: .85;
  font-variant-numeric: tabular-nums;
}
.gunSwitch {
  align-self: stretch; padding: 11px 12px; border-radius: 8px;
  border: 1px solid rgba(143, 184, 232, .55); background: rgba(143, 184, 232, .10);
  color: #cfe0f2; font-size: 12px; letter-spacing: .18em; cursor: pointer;
}
.gunSwitch.off { border-color: rgba(159, 179, 200, .4); background: rgba(159, 179, 200, .06); color: #9fb3c8; }
.gunSwitch:active { background: rgba(143, 184, 232, .2); }
/* The grid is `.branchGrid`, which is laid out for a branch that is open
 * inside a wrapper. Here it is the whole panel, so it needs no inset. */
.gunGrid { margin: 0; }

/* ---- the ASSAY's three rooms -------------------------------------------
 *
 * A segmented control under the bar: which era's room you are in. Its own row
 * rather than three more buttons in the bar, which at 320 has no width left --
 * and ABOVE the panel, so the readout's bottom edge (what `standoff` measures
 * the rig against) is not moved by it.
 */
/*
 * ---- POSITIONED, and it can be tapped ---------------------------------
 *
 * It was a static block child of `#sandbox`, which is `position: absolute;
 * inset: 0` -- and both its siblings, `#sbBar` and `#sbPanel`, are absolute
 * too. So the only in-flow child flowed from the top of the VIEWPORT and
 * rendered underneath the bar; and `#sandbox` is `pointer-events: none` with
 * each child opting in, which this never did. The room's own era row has
 * therefore never been visible where it was drawn and has never taken a
 * press, since the day it went in at build 262. Found by review.
 *
 * It sits between the bar (44 tall at `--hud-t`) and the panel, and the panel
 * moved down by exactly this row's height to make the room for it.
 */
#sbEras {
  position: absolute;
  top: calc(var(--hud-t) + 48px);
  left: max(10px, var(--safe-l));
  right: max(10px, var(--safe-r));
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px;
  pointer-events: auto;
}
/*
 * ...and the same control in the tab that opens the room, so which era you
 * are measuring in is a decision taken at the door rather than one tap after
 * walking in. The buttons are the same class and therefore the same thing to
 * learn; only the layout and the size differ, and the size differs because
 * this one is IN THE MENU, where the swept floor is 11px.
 */
#sbDoorEras {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
  margin: 12px 0 4px;
}
/*
 * 11px because the swept menu floor is 11px, and the tracking comes DOWN to
 * pay for it: "ERA III" plus its padlock is 82px at .18em against a cell of
 * 76 at 320, which wrapped. At .1em it is 71.
 */
#sbDoorEras .sbEra {
  font-size: 11px; padding: 9px 2px; gap: 4px; letter-spacing: .1em;
}
/*
 * `nowrap` is not decoration. A segmented control is three cells of one
 * height, and an over-long label does not clip -- it WRAPS, makes its own
 * cell taller than its siblings and drags the row's height with it, with no
 * property flipped for a test to read back. That is the menu tab strip's
 * documented failure mode and "ERA III" hit it at 320 the first time this row
 * was drawn there.
 */
.sbEra {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  padding: 7px 4px; border-radius: 6px; white-space: nowrap;
  border: 1px solid rgba(143, 184, 216, .28); background: rgba(10, 16, 24, .72);
  color: #8fa9c4; font-size: 10px; letter-spacing: .18em; cursor: pointer;
}
.sbEra.on {
  border-color: rgba(143, 184, 216, .8); background: rgba(143, 184, 216, .14);
  color: #dbe6f2;
}
/*
 * The padlock is always in the markup and shown by a class, because era 2's
 * room opens MID-RUN the moment the NEW FORM is bought -- a mark that is only
 * ever added is a mark that can never come off. `.sbEra:not(.shut)` is three
 * classes against the one on `.sbEraLock`, so the hide wins; writing it the
 * other way round (a `.shut` rule alone) would lose to the base rule and the
 * lock would sit on every cell.
 */
.sbEraLock { display: inline-block; width: 9px; height: 9px; opacity: .75; }
.sbEra:not(.shut) .sbEraLock { display: none; }
.sbEraLock svg { width: 100%; height: 100%; display: block; }
/* Shut reads as shut: dimmer, and it keeps the padlock beside the name. */
.sbEra.shut { opacity: .62; }
/* A door that does not open says so where it was pressed, once. */
.sbEra.refuse { animation: sbRefuse .34s ease-out; }
@keyframes sbRefuse {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-3px); }
  60% { transform: translateX(3px); }
}
