/* SURVEYOR — field instrument HUD.
   Type is a single monospace stack at three weights; the character comes from
   spacing and rule-work rather than a display face, which keeps the folder
   asset-free and CSP-clean. */

:root {
  --ink: #070c0f;
  --panel: rgba(9, 19, 23, 0.70);
  --edge: rgba(43, 224, 200, 0.26);
  --phos: #2be0c8;
  --bone: #f2f6e6;
  /* --amber is gone: every warn state now shares one hue, declared with the
     type scale below as --warn. Two warm colours that neither ranked against
     each other nor differed from the world palette was the old problem. */
  --beacon: #ff5fa2;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--ink);
  font-family: var(--mono);
  color: var(--bone);
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

#stage {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  outline: none;
  touch-action: none;
}

/* ---- THE POINTER ----
   The system arrow is a dark glyph with a thin white keyline, and on Vault's
   ice and Tarn's pale shallows that keyline is the only thing separating it
   from the ground — it vanishes outright over a bright shoreline.

   So it is drawn here, in the game's own line-work: an OUTLINED arrow, cyan,
   with the tip as the hotspot. It replaces a centred circle-and-ticks reticle,
   which was built on the assumption that the mouse aims something. It does not:
   the craft is driven on WASD, and the pointer's whole job is the HUD, the warp
   row and the pause menu. A reticle over a game with no crosshair is an
   instrument reading nothing, and it read as one.

   VISIBLE ON BOTH GROUNDS BY CONSTRUCTION, not by picking a lucky colour, and
   this part is kept exactly as it was. The outline is drawn twice: a 3.4px
   casing in --ink first, then the 1.4px cyan line on top of it. That leaves
   about a pixel of near-black either side of every cyan stroke, so the shape
   carries its own contrast onto whatever it is over. A single flat colour
   cannot do that, and a white one fails on Vault the same way the arrow does.

   Inline SVG in a data: URI — no file, no request, and the games CSP allows it
   (`img-src 'self' data: blob:`, which is what a CSS cursor image is checked
   against). Each declaration ends in a keyword, so a browser that refuses the
   data URI falls back to `default`/`pointer` rather than to nothing.

   28px against a ~20px system arrow, hotspot at the tip, 4 4. */
:root {
  --cur-reticle: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='28'%20height='28'%3E%3Cg%20fill='none'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cg%20stroke='%23070c0f'%20stroke-opacity='.88'%20stroke-width='3.4'%3E%3Cpath%20d='M3.6%203.6V19.4L8.3%2015.1%2011.6%2022.4%2014.8%2020.9%2011.4%2013.8%2017.6%2013.4Z'/%3E%3C/g%3E%3Cg%20stroke='%232be0c8'%20stroke-width='1.4'%3E%3Cpath%20d='M3.6%203.6V19.4L8.3%2015.1%2011.6%2022.4%2014.8%2020.9%2011.4%2013.8%2017.6%2013.4Z'/%3E%3C/g%3E%3C/g%3E%3Ccircle%20cx='3.6'%20cy='3.6'%20r='1.15'%20fill='%232be0c8'/%3E%3C/svg%3E") 4 4, default;
  --cur-target: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='28'%20height='28'%3E%3Cpath%20d='M3.6%203.6V19.4L8.3%2015.1%2011.6%2022.4%2014.8%2020.9%2011.4%2013.8%2017.6%2013.4Z'%20fill='%232be0c8'%20fill-opacity='.24'/%3E%3Cg%20fill='none'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cg%20stroke='%23070c0f'%20stroke-opacity='.88'%20stroke-width='3.4'%3E%3Cpath%20d='M3.6%203.6V19.4L8.3%2015.1%2011.6%2022.4%2014.8%2020.9%2011.4%2013.8%2017.6%2013.4Z'/%3E%3C/g%3E%3Cg%20stroke='%232be0c8'%20stroke-width='1.8'%3E%3Cpath%20d='M3.6%203.6V19.4L8.3%2015.1%2011.6%2022.4%2014.8%2020.9%2011.4%2013.8%2017.6%2013.4Z'/%3E%3C/g%3E%3C/g%3E%3Ccircle%20cx='3.6'%20cy='3.6'%20r='1.15'%20fill='%232be0c8'/%3E%3C/svg%3E") 4 4, pointer;
}

html, body, #stage { cursor: var(--cur-reticle); }

/* Over something you can press, the ring closes and fills. Same instrument,
   one state on — a system hand here would be the only object on screen not
   drawn in this language. */
.chip { cursor: var(--cur-target); }
/* .wbtn and #begin declare it at their own rules further down: those sit
   later in the file and would win on equal specificity from here. */

/* The pause menu ships its own stylesheet from js/pausemenu.js and is injected
   AFTER this file, so its `cursor:pointer` would otherwise win on equal
   specificity. Doubling the class is the whole reason these are written this
   way; nothing else about that component is touched from here. */
.cmenu .cmenu-btn,
.cmenu .cmenu-swatch,
.cmenu .aud-toggle { cursor: var(--cur-target); }

/* ---- the type scale ----
   One place, so "bigger" is one decision rather than forty. The old HUD
   assumed a focused player on a large monitor; these assume neither. The
   FLOORS are what make it legible at the site's embed size rather than only
   fullscreen, and the ceilings stop a 4K display turning it into signage.
   vw rather than vmin: the panel is laid out across the width, so that is the
   axis its type should track. */
:root {
  --t-label: clamp(10px, 0.80vw, 13px);
  --t-row:   clamp(12px, 1.00vw, 16px);
  --t-num:   clamp(15px, 1.30vw, 21px);
  --t-speed: clamp(40px, 4.10vw, 70px);
  --t-chip:  clamp(11px, 0.95vw, 15px);
  /* WARNING — one hue, two intensities: caution holds, critical pulses.
     Deliberately a colour the WORLD never uses. The palette's warm slots are
     the emissive orange on a vent and the pink of a beacon, and both of those
     are things out there to fly TOWARD. A warning that borrowed either would
     be saying "go here" with the colour and "stop" with the words, so this
     sits yellow — between them, and outside both. */
  --warn: #ffc93c;
}

/* ---- shared panel treatment: hairline rules like a chart margin ---- */
.panel {
  background: var(--panel);
  border: 1px solid var(--edge);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  padding: clamp(9px, 0.85vw, 13px) clamp(12px, 1.05vw, 17px);
  position: relative;
}
.panel::before {
  content: "";
  position: absolute;
  inset: 3px;
  border: 1px solid rgba(43, 224, 200, 0.10);
  pointer-events: none;
}

/* ---- CONTEXTUAL elements ----
   A trigger, a HOLD, then a slow fade. The hold is hud.js's — see Cue there —
   and this is only the fade. The asymmetry is the whole point: 170ms in, so
   the cue arrives with the event that caused it, and 900ms out, so nothing on
   this screen can flicker. A panel that leaves the instant its trigger clears
   is more distracting than one that never moved, which is why the trigger and
   the disappearance are deliberately not the same moment.
   --cue-x carries any horizontal centring the element already needed, so the
   two transforms do not fight. */
.cue {
  --cue-x: 0px;
  opacity: 0;
  transform: translate(var(--cue-x), -5px);
  transition: opacity 900ms ease, transform 900ms ease;
}
.cue.on {
  opacity: 1;
  transform: translate(var(--cue-x), 0);
  transition-duration: 170ms;
}
@media (prefers-reduced-motion: reduce) {
  .cue, .cue.on { transform: translate(var(--cue-x), 0); transition: opacity 200ms linear; }
}

/* ---- STATE CHANGES YOU DO NOT HAVE TO READ ----
   A ring leaving the thing that changed. The toast still says what happened in
   words, but the words are for when you look; this is for when you do not, and
   while driving you do not. Expanding-and-fading rather than a flash, because
   a flash competes with the impact flash and this is not an impact.
   Both are box-shadow only, so nothing reflows and nothing moves the panel a
   pixel - a gauge that jumped when charge arrived would be worse than silence.
   hud.js restarts these by removing the class, reading offsetWidth and adding
   it back; it also skips them entirely under prefers-reduced-motion. */
@keyframes ringOut {
  from { box-shadow: 0 0 0 0 rgba(43, 224, 200, 0.50); }
  to   { box-shadow: 0 0 0 14px rgba(43, 224, 200, 0); }
}
.gauge.gain { animation: ringOut 720ms ease-out; }
.chip.swap { animation: ringOut 520ms ease-out; }

#hud {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 5;
  opacity: 0;
  transition: opacity 700ms ease 250ms;
}
body.ready #hud { opacity: 1; }

.label {
  font-size: var(--t-label);
  letter-spacing: 0.20em;
  text-transform: uppercase;
  color: rgba(242, 246, 230, 0.52);
  margin-bottom: 3px;
}

/* ---- top left: the survey log — CONTEXTUAL ----
   Three figures and a coordinate. What left: vents claimed, hyper per minute,
   threat and the six planet pips, all four of which the survey overlay already
   draws as a table with a row per world. Keeping a second copy on screen
   permanently was the single biggest thing making this read as a spreadsheet.
   What is left is only the progress of the world you are standing on, and it
   is up when one of the numbers moves. */
#log {
  position: absolute;
  top: 16px; left: 16px;
  min-width: clamp(178px, 15vw, 236px);
}
#log .row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  font-size: var(--t-row);
  line-height: 1.75;
}
#log .row span:last-child {
  color: var(--phos);
  font-weight: 700;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
}
#log .row.wide {
  margin-top: 5px;
  padding-top: 5px;
  border-top: 1px solid var(--edge);
}
#coord {
  font-size: var(--t-label) !important;
  color: rgba(242, 246, 230, 0.62) !important;
  letter-spacing: 0.10em;
  font-variant-numeric: tabular-nums;
}

/* ---- top centre: bearing tape — PERMANENT ---- */
/* ON A PANEL, and that is the legibility constraint rather than a decoration.
   The compass is phosphor line work and the speed is bone: against Home's
   slate both read, and against Tarn's pale sky and bright water both vanished
   outright — measured, not guessed, from the six-sky sheet. Every other
   readout on this HUD already sat on the panel treatment; these two were the
   only ones trusting the sky behind them to stay dark. */
#tape {
  position: absolute;
  top: 14px; left: 50%;
  transform: translateX(-50%);
  padding: 7px 12px 4px;
  text-align: center;
}
#compass { display: block; }
#range {
  --cue-x: 0px;
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.18em;
  color: var(--beacon);
  margin-top: 3px;
}

/* ---- the stack under the tape — CONTEXTUAL ----
   Scan and flooding are the same shape and opposite meanings, which is on
   purpose: one fills toward something you want, the other toward something you
   do not. #hud.flying is set while the tape carries the artificial horizon,
   which makes it 30px taller and moves everything under it down. */
#scan, #flood {
  --cue-x: -50%;
  position: absolute;
  left: 50%;
  width: clamp(260px, 24vw, 360px);
  transition: opacity 900ms ease, transform 900ms ease, top 220ms ease;
}
#scan.on, #flood.on { transition-duration: 170ms, 170ms, 220ms; }
#scan { top: 100px; }
#flood { top: 164px; }
#hud.flying #scan { top: 130px; }
#hud.flying #flood { top: 194px; }
#scanTrack, #floodTrack {
  height: 7px;
  overflow: hidden;
}
#scanTrack { background: rgba(255, 95, 162, 0.16); }
#scanFill {
  height: 100%;
  width: 0%;
  background: var(--beacon);
  transition: width 90ms linear;
}
#flood { border-color: rgba(255, 201, 60, 0.42); }
#flood .label { color: var(--warn); }
#floodTrack { background: rgba(255, 201, 60, 0.16); }
#floodFill {
  height: 100%;
  width: 0%;
  background: var(--warn);
  transition: width 120ms linear;
}
/* Critical is the same hue harder, not a second colour: full-strength fill,
   a lit border and a pulse. Two intensities of one warning beats two hues
   nobody can rank against each other. */
#flood.crit {
  border-color: var(--warn);
  box-shadow: 0 0 0 1px rgba(255, 201, 60, 0.35);
  animation: pulse 700ms ease-in-out infinite;
}

/* ---- the survey overlay: the system view, and what is selected ----
   Held, not toggled, so it never becomes a mode you can leave switched on. The
   x-ray tint itself is #xray below; this is the half made of words. */
#survey {
  position: absolute;
  /* The sound toggle used to own this corner and this panel started below it.
     It does not exist any more, so the overlay takes the corner itself. */
  top: 16px; right: 16px;
  width: 330px;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 160ms ease, transform 160ms ease;
}
#survey.on { opacity: 1; transform: translateY(0); }
.srow {
  display: grid;
  grid-template-columns: 1fr 44px 52px 44px 40px;
  gap: 6px;
  align-items: baseline;
  font-size: 11.5px;
  line-height: 1.85;
  color: rgba(242, 246, 230, 0.78);
  border-left: 2px solid transparent;
  padding-left: 5px;
}
.srow.head {
  font-size: 8.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(242, 246, 230, 0.38);
  border-bottom: 1px solid var(--edge);
  padding-bottom: 2px;
  margin-bottom: 3px;
}
.srow .sprog, .srow .srate, .srow .sthreat, .srow .strip { text-align: right; }
.srow .srate { color: var(--phos); }
.srow .sthreat { color: var(--beacon); letter-spacing: -0.08em; }
.srow.here { border-left-color: var(--phos); color: var(--bone); }
.srow.far .strip { color: rgba(255, 95, 162, 0.75); }
.srow.far { opacity: 0.55; }
.srow.full .sprog { color: var(--phos); font-weight: 700; }
.srow.hot { background: rgba(255, 95, 162, 0.07); }
#surveySel {
  margin-top: 7px;
  padding-top: 6px;
  border-top: 1px solid var(--edge);
  display: none;
}
#surveySel.on { display: block; }
#selName {
  display: block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  color: var(--bone);
}
#selRange {
  display: block;
  font-size: 13px;
  color: var(--phos);
  letter-spacing: 0.10em;
}
#selNote {
  display: block;
  font-size: 9.5px;
  letter-spacing: 0.10em;
  color: rgba(242, 246, 230, 0.5);
}

/* ---- the beam: a state, not a gauge - CONTEXTUAL ----
   It used to sit here permanently at 32% opacity reading HOLD E, which is an
   instruction rather than an instrument: a control you are not using does not
   need a panel of its own. It now comes up when the beam fires and holds a
   moment after it stops. Where the key is written down is the pause menu. */
#beamRow {
  --cue-x: 0px;
  position: absolute;
  /* Directly above the form chips: the beam is available from all three, and
     sitting it over them is the shortest way to say so. */
  left: 16px; bottom: clamp(96px, 8.4vw, 126px);
  border-color: var(--phos);
}
#beamState {
  font-size: var(--t-row);
  font-weight: 700;
  letter-spacing: 0.18em;
  color: var(--phos);
}
#beamRow.hit { border-color: var(--beacon); }
#beamRow.hit #beamState { color: var(--beacon); }

/* ---- dev warp: TEMPORARY TESTING SCAFFOLDING ----
   PERMANENT AND ON THE SCREEN, centre-right, as a stacked list. It spent a
   while inside #survey, which cost it nothing in screen space and cost every
   test two extra steps — hold a key, find the panel, then click. It is the way
   the game is checked, so it is reachable.
   Still the chip language: hairline edge, phos-lit "you are here", no fills.
   It is deliberately quiet — lower contrast than any permanent readout, so it
   sits behind the instruments rather than competing with them — and it goes
   away entirely with DEBUG.warp.
   Click-only. 1, 2 and 3 are the transforms. */
#warp {
  position: fixed;
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  /* ...and then lifted clear of the speed block. Centred on its own, the sixth
     button lands underneath the bottom-right readout and ANVIL is half a word.
     The nudge is a constant rather than a percentage because what it has to
     clear is a panel of fixed height, not a fraction of the viewport. */
  margin-top: -60px;
  display: flex;
  flex-direction: column;
  gap: 5px;
  pointer-events: auto;
  z-index: 6;
}
#warp[hidden] { display: none; }
#warp > .label { text-align: right; opacity: 0.55; }
#warpRow {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.wbtn {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  /* Big enough to read and click without aiming, which is the whole reason it
     came back out, and no bigger: six of these plus a label have to fit between
     the compass and the speed block. */
  min-width: 152px;
  padding: 8px 13px;
  text-align: right;
  color: rgba(242, 246, 230, 0.62);
  background: rgba(6, 12, 14, 0.34);
  border: 1px solid rgba(242, 246, 230, 0.18);
  cursor: var(--cur-target);
  transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
}
.wbtn:hover { color: var(--bone); border-color: var(--edge); }
/* Press state. A hover alone tells a mouse it is over the button and tells a
   touch device nothing at all, which is most of why the warp row felt dead on
   a phone. */
.wbtn:active { transform: translateY(1px); background: rgba(43, 224, 200, 0.10); }
.wbtn.on {
  color: var(--phos);
  border-color: var(--phos);
  background: rgba(43, 224, 200, 0.13);
  /* The world you are already on is not a button. Still the reticle, not
     the system arrow — see the cursor block at the top of this file. */
  cursor: var(--cur-reticle);
}
.wbtn:focus-visible { outline: 2px solid var(--bone); outline-offset: 2px; }

/* ---- the icon family ----
   Line work at one weight, no fills, drawn on the same 24 grid as each other
   so a rover and a jet read as members of one set rather than three separate
   drawings. Inline SVG rather than a font: nothing to request, nothing for the
   CSP to refuse, and it inherits currentColor so a lit chip lights its icon. */
.ico {
  width: clamp(20px, 1.7vw, 27px);
  height: clamp(20px, 1.7vw, 27px);
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---- bottom left: form selector - PERMANENT ----
   The largest permanent thing on the left, because "which of the three am I,
   and will the jet take me" is the question asked most often. */
#forms {
  position: absolute;
  bottom: 18px; left: 16px;
  display: flex;
  gap: 8px;
}
.chip {
  display: grid;
  justify-items: center;
  gap: 3px;
  background: var(--panel);
  border: 1px solid var(--edge);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  padding: clamp(9px, 0.85vw, 13px) clamp(11px, 1vw, 16px) clamp(8px, 0.75vw, 11px);
  min-width: clamp(76px, 6.6vw, 106px);
  color: rgba(242, 246, 230, 0.55);
  transition: border-color 180ms ease, background 180ms ease, transform 180ms ease, color 180ms ease;
}
.chip .key {
  font-size: var(--t-label);
  letter-spacing: 0.2em;
  color: rgba(242, 246, 230, 0.38);
}
.chip .name {
  font-size: var(--t-chip);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.chip.on {
  border-color: var(--phos);
  background: rgba(43, 224, 200, 0.13);
  transform: translateY(-3px);
  color: var(--phos);
}
.chip.on .name { color: var(--phos); }
.chip.on .key { color: rgba(43, 224, 200, 0.7); }
/* Locked is a WARNING, not a dimming. The jet refusing to launch is the most
   common thing this panel has to tell you, and 35% opacity said it far too
   quietly to catch mid-drive. */
.chip.locked {
  opacity: 0.8;
  border-color: rgba(255, 201, 60, 0.45);
  color: var(--warn);
}
.chip.locked .name { color: var(--warn); }

/* ---- bottom right: velocity, and the two things that run out - PERMANENT ---- */
#gauges {
  position: absolute;
  bottom: 18px; right: 16px;
  text-align: right;
  min-width: clamp(212px, 19vw, 300px);
  padding: clamp(10px, 0.95vw, 15px) clamp(12px, 1.1vw, 18px) clamp(11px, 1vw, 16px);
}
#speedRow {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 7px;
}
#speed {
  font-size: var(--t-speed);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
/* Hyper. The readout keeps every digit and gives up size instead, so the
   number stays one number rather than becoming a unit change. */
#speed.huge { font-size: calc(var(--t-speed) * 0.80); letter-spacing: 0; }
#speed.vast { font-size: calc(var(--t-speed) * 0.62); letter-spacing: -0.01em; }
#speedUnit {
  font-size: var(--t-label);
  letter-spacing: 0.2em;
  color: rgba(242, 246, 230, 0.5);
}
#altRow {
  --cue-x: 0px;
  font-size: var(--t-row);
  font-weight: 700;
  letter-spacing: 0.16em;
  color: var(--phos);
  margin-top: 4px;
}
#altRow::after {
  content: "";
  margin-left: 8px;
  letter-spacing: 0.16em;
}
#altRow.auto::after { content: "\00B7 AUTOPILOT"; color: rgba(242, 246, 230, 0.6); }
#altRow.pull::after { content: "\00B7 PULL UP"; color: var(--warn); }

/* The two tanks. Each gets a head, so one glance lands on an icon, a label, a
   number and a bar together rather than on four loose rows. */
.gauge { margin-top: clamp(9px, 0.85vw, 14px); }
.gaugeHead {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}
.gaugeHead .label { margin-bottom: 0; }
.gaugeHead .ico { color: rgba(242, 246, 230, 0.42); }
#fuelNum, #hyperNum {
  font-size: var(--t-num);
  font-weight: 700;
  color: var(--phos);
  font-variant-numeric: tabular-nums;
  min-width: 3.2ch;
  text-align: right;
}
#fuelBar {
  display: flex;
  gap: 2px;
  margin-top: 5px;
  justify-content: flex-end;
}
#fuelBar i {
  display: block;
  flex: 1 1 0;
  max-width: 13px;
  height: clamp(14px, 1.2vw, 20px);
  /* Up from 0.10: an unlit segment has to read as an empty socket, and at a
     tenth it simply was not there against a bright sky. */
  background: rgba(242, 246, 230, 0.20);
  transition: background 140ms ease;
}
#fuelBar i.lit { background: var(--phos); }
#fuelBar.low i.lit { background: var(--warn); }
#fuelBar.crit i.lit { background: var(--warn); animation: pulse 900ms ease-in-out infinite; }
#cellGauge.low #fuelNum { color: var(--warn); }
#cellGauge.crit #fuelNum { color: var(--warn); animation: pulse 900ms ease-in-out infinite; }
#cellGauge.low .gaugeHead .ico, #cellGauge.crit .gaugeHead .ico { color: var(--warn); }
@keyframes pulse { 50% { opacity: 0.35; } }

/* Hyper is a continuous bar against the cell gauge's segments, so the two
   tanks never read as more of the same one. */
#hyperBar {
  height: clamp(6px, 0.55vw, 9px);
  margin-top: 5px;
  background: rgba(242, 246, 230, 0.20);
  position: relative;
  overflow: hidden;
}
#hyperBar::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--fill, 0%);
  background: var(--phos);
  transition: width 200ms ease;
}
#hyperBar.empty::after { background: var(--warn); }
#hyperGauge.empty #hyperNum { color: var(--warn); }

/* ---- toast ---- */
#toast {
  position: absolute;
  bottom: clamp(150px, 13vw, 205px); left: 50%;
  transform: translateX(-50%) translateY(8px);
  font-size: var(--t-row);
  letter-spacing: 0.20em;
  text-transform: uppercase;
  padding: clamp(9px, 0.85vw, 13px) clamp(16px, 1.5vw, 24px);
  background: var(--panel);
  border-left: 3px solid var(--phos);
  opacity: 0;
  transition: opacity 220ms ease, transform 220ms ease;
  white-space: nowrap;
}
#toast.on { opacity: 1; transform: translateX(-50%) translateY(0); }
#toast.warn { border-left-color: var(--warn); color: var(--warn); }
#toast.good { border-left-color: var(--phos); color: var(--phos); }
#toast.bad { border-left-color: var(--warn); color: var(--warn); }

/* ---- boost streaks + impact flash ---- */
/* ANCHORED TO THE VIEWPORT, not sized relative to it, and that is the fix.
   This element SPINS, and a spinning rectangle only 20% larger than the frame
   pulls its own corners inward as it turns — so its straight outer edge swept
   through the picture. It showed up in the jet first because the jet is where
   the streaks are strongest, but the edge was always there and the rover and
   the boat were merely hiding it.
   A square whose side is at least the viewport DIAGONAL can never expose an
   edge at any angle. The diagonal is sqrt(W^2+H^2), which is at most
   sqrt(2)*vmax and only reaches that on a perfectly square window, so 142vmax
   covers every case with a hair to spare. Centred with margins rather than a
   translate, because `transform` belongs to the spin — putting the centring
   there too would have the keyframe overwrite it on the first frame.
   The mask stops moved from percentages to vmax for the same reason: a
   percentage is read against the ELEMENT, so growing the element would have
   pushed the clear centre out past the corners of the screen and quietly
   deleted the effect. In vmax they are measured against the frame instead, and
   18/54 reproduces the old geometry to within a pixel or two from 16:9 through
   to a portrait phone. */
#streaks {
  position: fixed;
  top: 50%;
  left: 50%;
  width: 142vmax;
  height: 142vmax;
  margin-top: -71vmax;
  margin-left: -71vmax;
  pointer-events: none;
  z-index: 4;
  opacity: 0;
  background: repeating-conic-gradient(from 0deg at 50% 50%,
    transparent 0deg 1.6deg,
    rgba(242, 246, 230, 0.16) 1.6deg 2.1deg);
  -webkit-mask-image: radial-gradient(circle at 50% 50%, transparent 18vmax, #000 54vmax);
  mask-image: radial-gradient(circle at 50% 50%, transparent 18vmax, #000 54vmax);
  animation: spin 40s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---- going under ---- */
#deep {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 4;
  opacity: 0;
  background:
    radial-gradient(circle at 50% 42%, rgba(11, 66, 74, 0.25) 0%, rgba(4, 20, 28, 0.92) 78%),
    linear-gradient(rgba(9, 46, 55, 0.55), rgba(4, 18, 26, 0.85));
  transition: opacity 180ms linear;
}
#deep.on {
  backdrop-filter: blur(3px) saturate(0.6);
  -webkit-backdrop-filter: blur(3px) saturate(0.6);
}

/* ---- the x-ray wash ----
   The terrain itself goes to wireframe in overlay.js; this is the tint over the
   top of it, and the grid is a chart's grid rather than a sci-fi scanline. It
   carries no information, so it stays weak — the markers are the instrument. */
#xray {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 3;
  opacity: 0;
  /* Dark, and darker than it looks like it needs to be. The markers are ADDITIVE
     — they add light to whatever is behind them — so their contrast is set by
     how dark the ground under them is, not by how bright they are authored.
     Measured: only about four of twenty-four markers fall inside a 90-degree
     cone at any moment, and each is 11-38px across at a kilometre out. Those
     are the marks that have to read, and dropping the ground is what makes them. */
  background:
    radial-gradient(circle at 50% 50%, rgba(6, 26, 34, 0.58) 0%, rgba(3, 11, 17, 0.86) 84%),
    repeating-linear-gradient(0deg, rgba(43, 224, 200, 0.07) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(90deg, rgba(43, 224, 200, 0.07) 0 1px, transparent 1px 34px);
}
/* No backdrop-filter here, deliberately. A full-viewport filter is one of the
   most expensive things a page can ask for — it is a readback of the whole
   frame every frame — and this one bought a saturation wash that a flat tint
   already gives. #deep keeps its filter because going under is a moment; this
   is held for as long as you are reading a chart. */

#flash {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 6;
  background: rgba(255, 177, 61, 0.5);
  opacity: 0;
}
#flash.on { animation: hit 320ms ease-out; }
@keyframes hit { 0% { opacity: 0.75; } 100% { opacity: 0; } }

/* ---- start card ----
   Arena 1's controls modal is the house pattern and this is built to it: one
   centred panel, a row per binding, keycaps rather than bold text, and type
   sized to be read rather than studied. Chart-like and bold are not in
   tension — an altimeter is both, and the old card was small and dense for no
   reason the art direction ever asked for.
   What is NOT here is as deliberate as what is: no settings, no lore, no
   feature list, and four bindings where there were eleven. */
#start {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 20px;
  /* The scrim and the chart grid. The PLACE is behind them — see ::before. */
  background:
    linear-gradient(rgba(7, 12, 15, 0.72), rgba(7, 12, 15, 0.90)),
    repeating-linear-gradient(0deg, rgba(43, 224, 200, 0.07) 0 1px, transparent 1px 26px),
    repeating-linear-gradient(90deg, rgba(43, 224, 200, 0.07) 0 1px, transparent 1px 26px);
  transition: opacity 480ms ease;
}

/* THE WORLD, WHILE THERE IS NOT ONE YET.
   The canvas behind this card is black until the engine has compiled and a
   planet has been built, and a black screen behind a title card reads as a
   game that has failed to start rather than one that is starting. So the card
   sits on a photograph of Home instead: 1440px, blurred, 11KB of WebP, which
   is cheaper than a third of a second of anything.

   Deliberately soft. It is a backdrop under a scrim behind a card, so detail is
   noise — and blurring it is most of why it fits in 11KB.

   It STAYS UP until Begin. Fading it out when the world was ready was tried
   and is worse: what is behind the card at that point is the spawn, at ground
   level, in whatever light the world opens in — a dull view of a hillside, not
   a composed one. The photograph is a title card's backdrop and it is chosen;
   the live frame is neither. Behind the scrim rather than in it, so it can be
   dimmed without dimming the grid over it. */
#start::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: #070c0f url("../assets/ui/start.webp") center / cover no-repeat;
  transition: opacity 900ms ease;
}

#start.gone { opacity: 0; pointer-events: none; }

#card {
  position: relative;
  width: min(620px, 100%);
  padding: clamp(26px, 4.4vw, 46px) clamp(22px, 4vw, 44px) clamp(20px, 3.2vw, 34px);
  background: rgba(9, 19, 23, 0.90);
  border: 1px solid var(--edge);
  border-radius: 18px;
  /* Now that it is a rounded panel floating over a photograph rather than a
     hard-edged plate over black, it needs to sit off the backdrop rather than
     be cut into it. A shadow, and NOT a backdrop-filter: a 3px blur here was
     tried and cost 1.3 SECONDS on the WebGL context creation, because the
     compositor has to keep a readback of a full-screen backdrop layer and that
     contends with the engine coming up behind it. Measured — Begin went live at
     2.4s with the blur and 1.0s without, and the card itself painted at 210ms
     against 70ms. The prettiest thing on the loading screen was the loading. */
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
}
/* Corner ticks — the chart device, in CSS so the folder stays asset-free.
   Two pseudo-elements on opposite corners is enough to read as a plotted
   frame; four would be drawing a box around a box.
   They carry the card's radius on their own outer corner, so the device
   survives the rounding instead of fighting it. */
#card::before,
#card::after {
  content: "";
  position: absolute;
  width: 26px;
  height: 26px;
  border: 2px solid var(--phos);
}
#card::before {
  top: -1px; left: -1px;
  border-right: 0; border-bottom: 0;
  border-top-left-radius: 18px;
}
#card::after {
  right: -1px; bottom: -1px;
  border-left: 0; border-top: 0;
  border-bottom-right-radius: 18px;
}

#card h1 {
  margin: 0;
  font-size: clamp(34px, 8vw, 60px);
  font-weight: 700;
  /* Tracking is added to the RIGHT of every glyph including the last, so
     without the matching indent the word sits visibly left of centre in its
     own box. Same trick on the button below. */
  letter-spacing: 0.30em;
  text-indent: 0.30em;
  line-height: 1.04;
  color: var(--bone);
}
#card .brief {
  margin: clamp(12px, 1.8vw, 18px) 0 0;
  max-width: 42ch;
  font-size: clamp(12px, 1.5vw, 16px);
  line-height: 1.65;
  color: rgba(242, 246, 230, 0.76);
}

#keys {
  display: grid;
  gap: clamp(9px, 1.2vw, 14px);
  margin-top: clamp(20px, 3vw, 28px);
  padding-top: clamp(18px, 2.6vw, 26px);
  border-top: 1px solid var(--edge);
}
.krow {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 16px;
}
.kact {
  font-size: clamp(11px, 1.3vw, 15px);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bone);
}
.kkeys {
  display: flex;
  align-items: center;
  gap: 6px;
  justify-self: end;
}
.kkeys kbd {
  display: inline-grid;
  place-content: center;
  min-width: 2.2em;
  padding: 0.40em 0.55em;
  font-family: inherit;
  font-size: clamp(11px, 1.15vw, 14px);
  font-weight: 700;
  letter-spacing: 0.10em;
  color: var(--phos);
  background: rgba(43, 224, 200, 0.07);
  border: 1px solid var(--edge);
  border-bottom-width: 3px;      /* reads as a keycap, not a chip */
}
.kkeys kbd.wide { min-width: 0; letter-spacing: 0.14em; }
.kkeys em {
  font-style: normal;
  font-size: clamp(9px, 0.95vw, 12px);
  letter-spacing: 0.10em;
  color: rgba(242, 246, 230, 0.50);
}

#begin {
  display: block;
  margin-top: clamp(22px, 3.2vw, 30px);
  width: 100%;
  padding: clamp(15px, 2vw, 20px);
  font-family: var(--mono);
  font-size: clamp(12px, 1.5vw, 16px);
  font-weight: 700;
  letter-spacing: 0.30em;
  text-indent: 0.30em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--phos);
  border: 0;
  cursor: var(--cur-target);
  transition: background 160ms ease, transform 160ms ease;
}
#begin { border-radius: 10px; }
#begin:hover { background: var(--bone); transform: translateY(-1px); }
#begin:focus-visible { outline: 2px solid var(--bone); outline-offset: 3px; }

/* WHILE THE ENGINE IS STILL COMPILING. js/boot.js puts the button here before
   main.js exists and main.js takes it out again the moment Begin actually
   works — see the note there. A control that looks pressable and is not is the
   one thing worse than a slow load, because the player presses it and concludes
   the game is broken. The bar underneath is indeterminate on purpose: the boot
   has no honest progress figure, and inventing one is a lie that gets caught. */
#begin[disabled] {
  color: rgba(242, 246, 230, 0.62);
  background: rgba(43, 224, 200, 0.14);
  cursor: var(--cur-reticle);
  position: relative;
  overflow: hidden;
}
#begin[disabled]:hover { background: rgba(43, 224, 200, 0.14); transform: none; }
#begin[disabled]::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 34%;
  height: 2px;
  background: var(--phos);
  animation: beginwait 1150ms ease-in-out infinite;
}
#begin[data-state="failed"] { background: rgba(224, 86, 74, 0.22); }
#begin[data-state="failed"]::after { animation: none; width: 100%; opacity: 0.35; }

@keyframes beginwait {
  0%   { transform: translateX(-110%); }
  100% { transform: translateX(330%); }
}
@media (prefers-reduced-motion: reduce) {
  #begin[disabled]::after { animation: none; width: 100%; opacity: 0.45; }
  #start::before { transition: none; }
}

/* Where the other seven bindings went. Quiet on purpose: it is a signpost,
   not an instruction, and the button above is the only thing being asked for. */
#card .more {
  margin: clamp(12px, 1.6vw, 16px) 0 0;
  font-size: clamp(9px, 1vw, 12px);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(242, 246, 230, 0.46);
  text-align: center;
}
#card .more kbd {
  font-family: inherit;
  font-size: inherit;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--phos);
  background: rgba(43, 224, 200, 0.07);
  border: 1px solid var(--edge);
  border-bottom-width: 2px;
  padding: 0.18em 0.45em;
  margin-right: 0.7em;
}

/* Arena 1's controls modal collapses its rows to one column at this width, for
   the reason this one has to as well: below it a label and its keycaps cannot
   share a line without whichever is longest setting the card's width for
   everything else. Stacked, the label leads and the caps sit under it. */
@media (max-width: 560px) {
  .krow { grid-template-columns: 1fr; gap: 5px; }
  .kkeys { justify-self: start; }
  #card .more { text-align: left; }
}

@media (prefers-reduced-motion: reduce) {
  #start, #begin { transition: none; }
}

/* Narrow screens get LAYOUT changes only. This block used to shrink the type
   as well - speed to 30px, rows to 11, the sound toggle to 8 - which is the
   exact opposite of what a small screen needs and was undoing the floors in
   the scale above before they could do anything. The clamps already size the
   type; all that is left here is making things fit side by side. */
@media (max-width: 620px) {
  #log { min-width: 0; }
  #forms { gap: 6px; }
  .chip { min-width: 0; padding: 8px 9px 7px; }
  #scan, #flood { width: min(300px, calc(100vw - 32px)); }
  #gauges { min-width: 0; }
  #survey { width: min(300px, calc(100vw - 32px)); }
  /* The compass is drawn to the canvas, and hud.js already sizes that to the
     viewport - see resize(). Nothing to do here but let it. */
}

@media (prefers-reduced-motion: reduce) {
  #streaks, #flash, #fuelBar.crit i.lit, #flood.crit { animation: none; }
  * { transition-duration: 1ms !important; }
}
