/* ===== THE TYPE SCALE =========================================================
   Matt, 2026-08-31: "font sizes feel all over the place (this is actually an
   issue for the whole game) ... On PC most of the text feels small, with a lot
   of deadspace. on mobile everything but the smallest text feels too big, so
   much so that the UI is hard to navigate."

   He is right, and it was measurable: 156 font-size declarations, 24 distinct
   values, 143 of them HARD-CODED PIXELS. A fixed 11px is cramped on a 1900px
   monitor and a fixed 24px is enormous on a 412px phone, so the same numbers
   were failing in opposite directions on the two devices.

   The fix is one scale, and the important half is that the RATIO moves too:

     BASE grows with the viewport  — 13.2px on a phone, 16px on a desktop, so
                                     the small text stops being cramped on PC.
     RATIO widens on desktop only  — 1.09 on a phone, 1.15 on a desktop, so the
                                     large text stops shouting on mobile.

   Both together are what actually fixes it. A base that grows alone would make
   the phone worse; a ratio that widens alone would leave PC cramped. Every step
   is derived, so there is exactly one number to turn if a whole screen feels
   off. Any size still written in px is a deliberate exception, not an oversight.
   ========================================================================== */
:root {
  /* 2026-09-01: pairs with the <meta name="color-scheme"> in index.html --
     the CSS-level declaration is what MDN documents as primary (it also
     themes native form controls like scrollbars), the meta tag is the
     fallback for anything that reads the HTML before CSS loads. Belt and
     suspenders against a mobile browser's auto-dark heuristic recolouring
     an already-dark-themed page it was never told about. */
  color-scheme: dark;
  /* Each step has its OWN curve rather than a shared base times a shared
     ratio. The obvious version — one base, one ratio, ratio widened by a
     media query — has a discontinuity: at 899px the base has already hit its
     ceiling while the ratio has not switched, so crossing 900px makes the
     SMALL text jump DOWN as the window gets wider. Backwards, and visible.

     Per-step clamps are monotonic everywhere. The spread still widens with the
     viewport, because the bigger steps have steeper slopes — which is the
     whole point: tight range on a phone so nothing shouts, wide range on a
     desktop so the small text is not cramped.

     phone 412px:  11.0  12.0  13.0  14.0  15.0  16.5  18.0
     desktop 1900: 13.0  14.5  16.0  18.5  21.5  25.0  29.0                   */
  --fs-2xs: clamp(11px,   0.28vw + 9.85px, 13px);
  --fs-xs:  clamp(12px,   0.38vw + 10.4px, 14.5px);
  --fs-sm:  clamp(13px,   0.50vw + 10.9px, 16px);
  --fs-md:  clamp(14px,   0.72vw + 11.0px, 18.5px);
  --fs-lg:  clamp(15px,   1.00vw + 10.9px, 21.5px);
  --fs-xl:  clamp(16.5px, 1.35vw + 10.9px, 25px);
  --fs-2xl: clamp(18px,   1.70vw + 11.0px, 29px);
}

/* STARVORE — 80s neon pixel style */

/* ===== THE GLOBAL RESET HAS NEVER RUN, AND TURNING IT ON IS ITS OWN JOB =====
   2026-09-06, found while rebuilding the map. There was a STRAY `}` sitting
   right here -- a second closing brace after :root's -- and it was not
   harmless punctuation. Per the CSS Syntax spec a `}` at the top level is
   consumed as part of the NEXT qualified rule's prelude, so the parser read
   the selector as `} *`, found it invalid, and threw the whole rule away.

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

   That line has been dead for the entire life of the stylesheet. Deleting the
   stray brace revives it -- and revives it for every element in the game at
   once. test_stick caught the first casualty immediately: #joyRing is
   `width: 120px` plus a 2px border, and the comment above it does the
   content-box arithmetic out loud ("120px + 2px border = the element", giving
   a 61px centreline). Under border-box the border moves inside and the
   centreline lands at 59, so the knob no longer parks on the ring it is drawn
   to park on.

   Every hard-coded pixel size in this file was tuned against the box model
   that was actually running. Flipping all of them in the middle of a map
   rebuild is how you spend a day finding out which. So the reset stays off,
   written out plainly instead of hidden behind a typo, and switching it on is
   its own change with its own pass over the sizes:

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

   `html, body` below now sets `margin: 0` explicitly -- the one piece of the
   reset anything actually relied on, and the only piece that changes nothing
   else. `overflow: hidden` on those two is why the missing margin never
   showed: the document was 8px taller than the viewport the whole time and
   nobody could scroll to see it. */

html, body {
  /* The one part of the dead reset above that is unambiguously safe to
     revive, and the only part anything depended on: without it <body> keeps
     the UA's 8px margin, which makes the document 8px taller than the
     viewport. `overflow: hidden` hides that in play, so it went unnoticed --
     until a fullscreen map had to assert that nothing scrolls. Two elements,
     no box-model change, no pixel sizes disturbed. */
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: #0d0221;
  touch-action: none;
  overscroll-behavior: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  font-family: "Courier New", Courier, monospace;
}

#wrap {
  position: fixed;
  inset: 0;
  background: #0d0221;
}

#screen {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* The backing store is now sized in DEVICE pixels, so on most displays this
     maps 1:1 and the hint is moot. It still matters where the render budget
     capped the scale below native, and there `pixelated` is what keeps a
     pixel-art downscale from turning to soup. */
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  touch-action: none;
}

/* ---- OPTIONS panel -------------------------------------------------------
   Built from SETTING_GROUPS, so this styles SHAPES rather than named rows:
   whatever gets declared next inherits the layout without a new rule. */
/* The overlay carries its own padding, so the body has to be narrower than the
   usable box or the right-hand column of a three-way choice runs off a phone. */
#optBody {
  width: min(520px, calc(var(--menuw, 94vw) - 34px));
  max-width: 100%;
  margin: 0 auto;
  box-sizing: border-box;
}
.optgroup {
  color: #ff6ac1;
  font-size: var(--fs-xs);
  letter-spacing: 3px;
  text-align: left;
  margin: 14px 0 4px;
  padding-bottom: 3px;
  border-bottom: 1px solid rgba(255, 106, 193, 0.35);
}
.optgroup:first-child { margin-top: 2px; }
.optrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 8px 0 2px;
}
.optlabel {
  flex: 1 1 130px;
  text-align: left;
  color: #e8e3ff;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
}
.optctl { display: flex; gap: 5px; flex: 0 0 auto; }
.optbtn {
  min-width: 0;              /* flex children must be allowed to shrink */
  background: rgba(45, 226, 230, 0.07);
  border: 1px solid rgba(45, 226, 230, 0.45);
  color: #9fd6de;
  font-family: inherit;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
  padding: 6px 9px;
  cursor: pointer;
  border-radius: 2px;
}
.optbtn.wide { padding: 7px 14px; }
.optbtn.on {
  background: rgba(45, 226, 230, 0.25);
  color: #f6f7f6;
  border-color: #2de2e6;
  box-shadow: 0 0 8px rgba(45, 226, 230, 0.45);
}
.opthint {
  flex: 1 1 100%;
  text-align: left;
  margin: 2px 0 0;
  font-size: var(--fs-2xs);
  line-height: 1.45;
  letter-spacing: 0;
  color: #8d86a8;
  opacity: 1;
}
/* a phone puts the control under the label rather than squeezing both */
@media (max-width: 520px) {
  .optlabel { flex: 1 1 100%; }
  .optctl { flex: 1 1 100%; }
  /* a thumb needs a target, so the padding grows as the type shrinks */
  .optbtn { flex: 1 1 0; padding: 10px 4px; letter-spacing: 0; font-size: var(--fs-2xs); }
  .optbtn.wide { padding: 11px 6px; }
}

/* CRT scanlines + vignette */
#wrap::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0)    0px,
      rgba(0, 0, 0, 0)    2px,
      rgba(0, 0, 0, 0.16) 3px
    ),
    radial-gradient(ellipse at center,
      rgba(0, 0, 0, 0) 55%,
      rgba(13, 2, 33, 0.55) 100%);
  z-index: 5;
}

.hidden { display: none !important; }

/* ---------- HUD ---------- */

/* The top-left stack used to be nine hardcoded `top:` values that had to be
   edited in lockstep — and again for the tutorial layout. Two variables now
   anchor the whole thing: --barY is the top of the bar ladder, --labelY the
   top of the text under it. Adding a bar means one rung and one number. */
#hud {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 10;
  padding: calc(10px + env(safe-area-inset-top, 0px)) 12px 0 12px;
  /* The rungs below sit at +8 / +19 / +27 / +35. Those are not arbitrary: the
     hull rung is 7px tall inside 2px borders — 11px of outer box — so a rung
     spacing of 10 overlapped the next by exactly one pixel. test_power
     measures the ladder for precisely this. */
  --barY: calc(12px + env(safe-area-inset-top, 0px));
  /* §HUDICONS: five 12px rungs (+2 gap) now, each with its icon.
     §HUDBARS: the ladder is drawn at 75% (see #hudBars), so the column under
     it starts where the smaller ladder ends: 80 x .75 = 60, +2 */
  --labelY: calc(62px + env(safe-area-inset-top, 0px));
  --barX: 30px;
}
/* §HUDBARS, 2026-10-05. Matt: "Make the stats hud that has energy and health
   75% its current size." The icons and bars live in one box drawn with CSS
   zoom, so every rung, gap, icon and border shrinks together and the rules
   below keep their numbers. The safe-area inset is added OUTSIDE the zoom
   (divided back out), so a notch is still cleared by the full amount. */
#hudBars {
  position: absolute;
  left: 4px;           /* x .75 = 3: the icons' edge stays on the column's 12px line */
  top: calc(env(safe-area-inset-top, 0px) / 0.75);
  width: 75%;        /* the rungs are 34% of this box wide: 75% of their old width */
  height: 0;
  zoom: 0.75;
  pointer-events: none;
  --barY: 12px;
}
.tutting #hudBars { --barY: 155px; }   /* 116 rendered: clear of the training banner */

/* §HUDICONS, 2026-09-28. Matt: "can you make icons for ship health, energy,
   system heat and heatscope? (heatscope can be a star)". One 12px glyph per
   rung, in the bar's own colour, glowing like the bar does. The bars move
   right by the icon's width; the rungs are 14px apart so an icon never
   touches the next one. */
.hico {
  position: absolute;
  left: 12px;
  width: 12px; height: 12px;
  display: block;
  filter: drop-shadow(0 0 3px currentColor);
}
.hico svg { width: 100%; height: 100%; display: block; overflow: visible; }
.hico path { fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linejoin: round; stroke-linecap: round; }
.hico path.f { fill: currentColor; stroke: none; }
#icoShield { top: var(--barY); color: #00f0ff; }
#icoHull   { top: calc(var(--barY) + 14px); color: #ff4d6d; }
#icoHeat   { top: calc(var(--barY) + 28px); color: #ffb319; }
#icoEnergy { top: calc(var(--barY) + 42px); color: #a9f6ff; }
#icoScope  { top: calc(var(--barY) + 56px); color: #ff6ac1; }
/* an inline icon inside a text readout (the cargo crate) */
.hico.inl { position: static; display: inline-block; vertical-align: -1px; margin-right: 6px; width: 11px; height: 11px; }

/* §HUDSTACK: credits, cargo, speed, the status chips, the mission and the pins
   flow down one column from --labelY, so nothing is pinned to a hand-set top
   that a longer status would print through */
#hudStack {
  position: absolute;
  top: var(--labelY);
  left: 12px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  max-width: min(440px, 62vw);
}
#hudStack #creditsLabel, #hudStack #cargoLabel, #hudStack #speedLabel,
#hudStack > #statusLabel, #hudStack > #missionLabel, #hudStack > #pinPanel {
  position: static; top: auto; left: auto;
}
/* §MSG, 2026-10-04: one line for credits / cargo / speed, and the status
   rows run on as one line too (wrapping only when they must) -- the left
   column was five lines tall and left the comms window three */
#hudLine { display: flex; align-items: baseline; flex-wrap: wrap; column-gap: 16px; row-gap: 2px; }
#statusLabel { flex-direction: row !important; flex-wrap: wrap; align-items: baseline; row-gap: 1px; }
#statusLabel .st + .st::before { content: '\00b7'; margin: 0 7px; color: #b8c2d6; opacity: .55; text-shadow: none; }
#hudStack > #pinPanel { margin-top: 3px; }
#hudStack > #missionLabel:empty { display: none; }

.bar {
  position: absolute;
  height: 12px;
  border: 2px solid #241734;
  background: rgba(13, 2, 33, 0.8);
  box-shadow: 0 0 8px rgba(0, 240, 255, 0.25);
}

#swrap {
  top: calc(var(--barY) + 3px);
  left: var(--barX);
  width: 34%;
  max-width: 220px;
  height: 6px;
  border-width: 1px;
}

#sbar {
  height: 100%;
  width: 100%;
  background: linear-gradient(to right, #00f0ff, #2de2e6);
  transition: width 0.15s steps(6);
}

#hpwrap {
  top: calc(var(--barY) + 14px);
  left: var(--barX);
  width: 34%;
  max-width: 220px;
  height: 7px;
}

#hpbar {
  height: 100%;
  width: 100%;
  background: linear-gradient(to right, #ff2975, #ff6c11);
  transition: width 0.15s steps(6);
}

#evowrap {
  top: calc(var(--barY) + 19px);
  left: 12px;
  width: 34%;
  max-width: 220px;
}

#evobar {
  height: 100%;
  width: 0%;
  background: linear-gradient(to right, #00f0ff, #39ff14);
  transition: width 0.15s steps(6);
}

#boostwrap {
  top: calc(var(--barY) + 30px);
  left: var(--barX);
  width: 34%;
  max-width: 220px;
  height: 5px;
  border-width: 1px;
}

#boostbar {
  height: 100%;
  width: 0%;
  background: linear-gradient(to right, #ffd319, #ff6c11, #ff3864);
}

#boostbar.hot { background: #ff3864; animation: warnblink 0.4s steps(2) infinite; }

/* The capacitor sits directly under the boost bar — the two things you spend
   and watch refill. It reads full at rest, so a glance mid-fight tells you
   whether the next shot is coming at full rate or at whatever your core
   can spare. Dry is amber, not red: a flat capacitor is a throttle, not a
   failure, and it must not read like damage. */
#capwrap {
  top: calc(var(--barY) + 44px);
  left: var(--barX);
  width: 34%;
  max-width: 220px;
  height: 5px;
  border-width: 1px;
}

#capbar {
  height: 100%;
  width: 100%;
  background: linear-gradient(to right, #2de2e6, #00f0ff, #a9f6ff);
  box-shadow: 0 0 6px rgba(0, 240, 255, .5);
  transition: width 0.08s linear;
}

/* running on spare output alone — every shot is now paced by the core */
#capbar.dry {
  background: linear-gradient(to right, #ffd319, #ff9d1b);
  box-shadow: 0 0 6px rgba(255, 211, 25, .55);
}

#speedLabel {
  position: absolute;
  top: calc(var(--labelY) + 34px);
  left: 12px;
  color: #f6f7f6;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 2px;
  text-shadow: 0 0 5px #8b2fc9;
}

#speedLabel.cruise { color: #00f0ff; text-shadow: 0 0 6px #00f0ff; }
#speedLabel.boost  { color: #ffd319; text-shadow: 0 0 6px #ff6c11; }

#boostBtn {
  position: absolute;
  right: 30px;
  bottom: calc(118px + env(safe-area-inset-bottom, 0px));
  pointer-events: auto;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  font-family: inherit;
  font-size: var(--fs-2xl);
  color: #ffd319;
  background: rgba(36, 23, 52, 0.75);
  border: 3px solid #ffd319;
  box-shadow: 0 0 12px rgba(255, 211, 25, 0.5);
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

#boostBtn.firing { background: rgba(255, 211, 25, 0.4); color: #f6f7f6; }

#creditsLabel {
  position: absolute;
  top: var(--labelY);
  left: 12px;
  color: #ffd319;
  font-weight: bold;
  font-size: var(--fs-sm);
  letter-spacing: 2px;
  text-shadow: 0 0 6px #ff6c11;
}

#cargoLabel {
  position: absolute;
  top: calc(var(--labelY) + 18px);
  left: 12px;
  color: #2de2e6;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 2px;
  text-shadow: 0 0 5px #00f0ff;
}

#statusLabel {
  position: absolute;
  top: calc(var(--labelY) + 50px);
  left: 12px;
  color: #ff3864;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 2px;
  text-shadow: 0 0 6px #ff3864;
}
/* §HUDSTACK, 2026-09-28. Matt: "I also do not like the red info display
   line, it's doing too many jobs so its really long." One job per line now:
   where you are (dim, the zone's colour), the hunt, then each live condition
   as its own short row, coloured by what kind of thing it is. The stable
   rows sit on top so the ones that come and go never shove them about. */
#statusLabel { display: flex; flex-direction: column; gap: 2px; }
#statusLabel:empty { display: none; }
#statusLabel .st { letter-spacing: 1.5px; line-height: 1.25; }   /* wraps inside the stack's width rather than running under the radar */
#statusLabel .st.zone  { font-size: var(--fs-2xs); letter-spacing: 3px; opacity: .85; text-shadow: none; }
#statusLabel .st.hunt  { color: #ff6ac1; text-shadow: 0 0 6px rgba(255, 106, 193, .6); font-size: var(--fs-2xs); }
#statusLabel .st.warn  { color: #ff3864; text-shadow: 0 0 6px #ff3864; }
#statusLabel .st.env   { color: #ff9a3c; text-shadow: 0 0 6px rgba(255, 154, 60, .6); }
#statusLabel .st.good  { color: #00f0ff; text-shadow: 0 0 6px #00f0ff; }
#statusLabel .st.note  { color: #ffd319; text-shadow: 0 0 6px rgba(255, 211, 25, .5); }

/* The danger banner: centred at the top, above the flight view but under the
   overlays, so it cannot be missed and cannot be clicked through. Two grades —
   amber means bring a better ship, red means turn around. */
#dangerBanner {
  position: absolute;
  /* Pinned to the very top of the view, not floated in the middle of it. A
     warning that covers the space you are trying to read is a warning that
     gets in the way of acting on it. */
  top: calc(6px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  max-width: min(420px, 86vw);
  padding: 6px 14px 7px;
  text-align: center;
  border: 1px solid #ff8c42;
  border-radius: 3px;
  background: rgba(30, 12, 4, 0.72);
  box-shadow: 0 0 14px rgba(255, 140, 66, 0.35);
  color: #ffb26b;
  pointer-events: none;
  animation: dangerIn .28s ease-out;
}
#dangerBanner b {
  display: block;
  font-size: var(--fs-sm);
  font-weight: bold;
  letter-spacing: 2px;
  text-shadow: 0 0 8px currentColor;
}
#dangerBanner small {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-2xs);
  letter-spacing: 1px;
  opacity: .82;
}
#dangerBanner.grave {
  border-color: #ff2975;
  background: rgba(38, 4, 18, 0.78);
  box-shadow: 0 0 18px rgba(255, 41, 117, 0.45);
  color: #ff6f9c;
  animation: dangerIn .28s ease-out;
}
@keyframes dangerIn {
  from { opacity: 0; transform: translate(-50%, -8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes dangerPulse {
  0%, 100% { box-shadow: 0 0 18px rgba(255, 41, 117, 0.45); }
  50%      { box-shadow: 0 0 26px rgba(255, 41, 117, 0.75); }
}
@media (max-width: 700px) {
  /* A phone's top edge is already spoken for — bars and readouts down the
     left, the three buttons down the right — so the banner drops just below
     both clusters rather than printing over them. Still the top of the
     screen; still nowhere near the middle of the view. */
  #dangerBanner {
    top: calc(var(--labelY) + 76px);
    max-width: min(360px, 94vw);
    padding: 4px 10px 5px;
  }
  #dangerBanner b { font-size: var(--fs-xs); letter-spacing: 1px; }
  #dangerBanner small { font-size: var(--fs-2xs); }
}

#hailBtn {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(168px + env(safe-area-inset-bottom, 0px));
  pointer-events: auto;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-sm);
  letter-spacing: 2px;
  color: #0d0221;
  background: #2de2e6;
  border: none;
  padding: 10px 18px;
  box-shadow: 0 0 10px rgba(45, 226, 230, 0.7), 3px 3px 0 #8b2fc9;
  cursor: pointer;
}

/* §STANDDOWN, 2026-09-13: the same button, in the other mood. It lights red
   and reads STAND DOWN when the ship on the other end is currently shooting
   at you, so the out is visible at the moment you need it rather than being
   a key you had to already know about. */
#hailBtn.danger {
  color: #ff3864;
  border-color: rgba(255, 56, 100, 0.65);
  box-shadow: 0 0 10px rgba(255, 56, 100, 0.35);
}

/* THE WEAPON CHIPS WRAP.
   Measured across the catalogue: the most a hull carries is TEN hardpoints
   (ETERNAL VANGUARD, BLACK LEVIATHAN, IRON SOVEREIGN, CRIMSON JUDGMENT — all
   four buyable), and six is the single most common count, on 42 of the 164
   hulls. Ten chips need 594px. A 390px phone in portrait therefore lost the
   first two and the last two off the edges of the screen entirely — not
   clipped, GONE, and each of them is a button you tap to safe a gun.

     390 portrait   6 chips fit ·  8 overflow by 44px · 10 by 102px
     320 portrait   6 overflow by 23px      · 10 by 137px
     844 landscape  all ten fit with room

   So the row wraps and centres, and grows upward away from the thumb
   controls. Two rows of five is 304px on a 390 phone: every chip on screen
   and still a comfortable target. */
#wpnBar {
  position: absolute;
  bottom: calc(10px + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  /* `width: max-content` for the same reason #mapFrameRow needs it: with
     `left: 50%` and `right: auto`, shrink-to-fit measures from `left` to the
     container's right edge — half the screen — and would wrap a row that fits
     perfectly well. */
  width: max-content;
  max-width: calc(100% - 16px);
  z-index: 5;
}

/* When it wraps it lifts clear of the thumb controls — see
   Game.placeWeaponBar(), which measures the height off those controls and
   writes it to style.bottom. This class only marks the state. */
#wpnBar.stacked { bottom: auto; }

/* ---------- how far, and to where ----------
   The autopilot's destination lived in the small AUTOPILOT button, which on a
   phone is about 90px wide and already carrying the word AUTOPILOT and a
   cancel glyph. Matt: "while autopilot is active we can no longer see the
   distance because its on a small button now."

   Pink, because that is the colour the course line and the next-waypoint
   arrow already wear — one autopilot, one colour. Its `bottom` is set in JS
   from the chip row's real height, so it rides above one row or two without
   knowing which. */
#apDist {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  max-width: calc(100% - 24px);
  text-align: center;
  font-family: inherit;
  font-size: var(--fs-2xs);
  letter-spacing: 1px;
  line-height: 1.3;
  color: #ff6ac1;
  text-shadow: 0 0 6px rgba(255, 106, 193, .45), 0 0 2px #0d0221;
  pointer-events: none;
  /* It wraps rather than truncating. "VESPER DOCK 321.8k u · gate 10.4k u ·
     2 jumps left" does not fit one line on a phone, and an ellipsis hides the
     jump count — which is the part that explains why the ship is flying at a
     gate instead of at the destination. Two short centred lines cost nothing;
     a cut-off sentence costs the reason. */
  white-space: normal;
  z-index: 5;
}
#apDist b { color: #ffd319; font-weight: bold; }
#apDist i { font-style: normal; color: #9a92b5; }

#wpnBar .wchip {
  pointer-events: auto;
  touch-action: none;
  background: rgba(13, 2, 33, .8);
  border: 1px solid #05d9e8;
  color: #05d9e8;
  font-family: inherit;
  font-size: var(--fs-2xs);
  font-weight: bold;
  letter-spacing: 1px;
  padding: 5px 8px;
  cursor: pointer;
  text-shadow: 0 0 4px rgba(5, 217, 232, .5);
}

#wpnBar .wchip span {
  display: block;
  font-size: var(--fs-2xs);
  color: #7a6fa8;
  letter-spacing: 0;
}

/* nova still charging: its ammo is your shield */
#wpnBar .wchip.charging {
  border-color: #8b2fc9;
  color: #b46cff;
  text-shadow: none;
}

#wpnBar .wchip.charging span { color: #ff8c42; }

#wpnBar .wchip.off {
  border-color: #4a3f70;
  color: #6b5a85;
  text-shadow: none;
  opacity: .75;
}

/* §FIRE rule 11: every weapon has its own colour, shown as a dot on its chip
   and worn by every HUD object that belongs to it (ring, cone, facing line,
   tick on its mark). The chip's --wc is set inline from the weapon def. */
#wpnBar .wchip { position: relative; }
#wpnBar .wchip .dot {
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--wc, #9ae6ff);
  box-shadow: 0 0 5px var(--wc, #9ae6ff);
  margin-right: 5px;
  vertical-align: 1px;
}
#wpnBar .wchip.off .dot { opacity: .35; box-shadow: none; }
/* a gun that is firing right now: the chip lights in its own colour */
#wpnBar .wchip.hot {
  border-color: var(--wc, #05d9e8);
  box-shadow: 0 0 8px var(--wc, #05d9e8), inset 0 0 6px rgba(255, 255, 255, .08);
}
/* a gun that has a mark inside its reach but is still turning onto it */
#wpnBar .wchip.aiming { border-style: dashed; }

#wpLabel {
  position: absolute;
  display: none;
  text-align: right;
  color: #ffd319;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
  text-shadow: 0 0 5px rgba(255, 211, 25, .45);
  line-height: 1.35;
}

#wpLabel span {
  display: block;
  color: #e8e3ff;
  opacity: .85;
  font-size: var(--fs-xs);
}

#missionLabel {
  position: absolute;
  top: calc(var(--labelY) + 66px);
  left: 12px;
  color: #ffd319;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
  text-shadow: 0 0 6px rgba(255, 211, 25, .5);
  max-width: 70vw;
}

#targetLabel {
  position: absolute;
  top: calc(10px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  color: #ff6ac1;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 2px;
  text-shadow: 0 0 6px #ff2975;
  white-space: nowrap;
  /* §14 made this line long enough to reach the shield bars in the top-left
     corner. A hard ceiling with an ellipsis means it can never do that again,
     whatever gets added to the readout later. */
  max-width: 64vw;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: top .18s ease-out;
  text-align: center;
}

/* A fleet readout is three times the length of a lone target's, and centred at
   full size it reached back into the shield bars in the top-left corner. It
   sheds letter-spacing and a couple of points of size rather than being
   truncated: what a formation IS is exactly the thing you need to read. */
#targetLabel.long {
  font-size: var(--fs-2xs);
  letter-spacing: 0.5px;
}

/* A phone has no room along the top edge for a fleet readout: the shield bars
   own the left of that row and the radar owns the right, and centring it
   between them leaves about 120px. So on a narrow screen it stops being a
   centred banner and becomes a left-aligned block under the bars, where there
   is width to spend and it can take a second line. */
@media (max-width: 600px) {
  #targetLabel.long {
    /* below the whole left stack — credits, cargo, speed, status, mission and
       the danger banner all live between labelY and labelY+110 — and stopping
       short of the radar column on the right */
    top: calc(var(--labelY) + 130px);
    left: 12px;
    transform: none;
    text-align: left;
    max-width: 252px;
    white-space: normal;
    line-height: 1.25;
  }
}

#fireBtn {
  position: absolute;
  right: 18px;
  bottom: calc(22px + env(safe-area-inset-bottom, 0px));
  pointer-events: auto;
  width: 86px;
  height: 86px;
  border-radius: 50%;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-md);
  letter-spacing: 2px;
  color: #ff2975;
  background: rgba(36, 23, 52, 0.75);
  border: 3px solid #ff2975;
  box-shadow: 0 0 14px rgba(255, 41, 117, 0.6);
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

#fireBtn.firing {
  background: rgba(255, 41, 117, 0.45);
  color: #f6f7f6;
}
/* the button reads OPEN FIRE on two lines — it is the deliberate act of
   §FIRE rule 1, not a trigger you hold for every shot */
#fireBtn small { display: block; font-size: var(--fs-2xs); letter-spacing: 1px; font-weight: normal; }

/* §FIRE rule 6: "Mobile needs a TARGET NEAREST button. PC already has the
   hotkey." Sits above BOOST in the right-hand thumb column (left of OPEN
   FIRE collided with the centred DOCK button on a 412px portrait). */
#tgtBtn {
  position: absolute;
  right: 30px;
  bottom: calc(190px + env(safe-area-inset-bottom, 0px));
  pointer-events: auto;
  width: 62px;
  height: 40px;
  border-radius: 20px;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-2xs);
  letter-spacing: 1px;
  line-height: 1.1;
  color: #ff2975;
  background: rgba(36, 23, 52, 0.75);
  border: 2px solid #ff2975;
  box-shadow: 0 0 10px rgba(255, 41, 117, 0.45);
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}
#tgtBtn.tapping { transform: scale(0.94); }
#tgtBtn small { display: block; font-size: var(--fs-2xs); font-weight: normal; letter-spacing: 0; }

#dockBtn {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(56px + env(safe-area-inset-bottom, 0px));
  pointer-events: auto;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-md);
  letter-spacing: 2px;
  color: #0d0221;
  background: #00f0ff;
  border: none;
  padding: 12px 22px;
  box-shadow: 0 0 12px rgba(0, 240, 255, 0.8), 3px 3px 0 #8b2fc9;
}

/* ---------- the right-hand instrument cluster ----------
   AUTOPILOT above the radar, MENU / MAP / INV below it, both rows exactly as
   wide as the radar and aligned to its edges. The radar is painted on the
   canvas, so CSS only knows where it is because layoutUi() publishes
   --radar-left/right/top/bottom/w every time the viewport changes. One
   layout, every screen size — the old arrangement had a stacked column on
   tall screens and a separate horizontal one on short screens, which meant
   two sets of numbers to keep in step and two chances to get it wrong. */
#hudApRow, #hudBtnRow {
  position: absolute;
  left: var(--radar-left, calc(100% - 122px));
  width: var(--radar-w, 110px);
  display: flex;
  gap: 4px;
  pointer-events: none;
}
/* An explicit height: #apBtn is absolutely positioned against this row and
   anchors to its bottom edge, so the row must not collapse when #apOpenBtn is
   hidden — otherwise the banner's baseline moves the moment it appears. */
#hudApRow  { top: calc(var(--radar-top, 90px) - 34px); height: 30px; }
#hudBtnRow { top: calc(var(--radar-bottom, 200px) + 6px); }
#hudApRow > button, #hudBtnRow > button {
  position: static;
  flex: 1 1 0;
  min-width: 0;
  pointer-events: auto;
  height: 30px;
  padding: 0;
  font-size: var(--fs-xs);
  font-weight: bold;
  letter-spacing: 1px;
  font-family: inherit;
  background: rgba(13, 2, 33, 0.7);
}

#mapBtn {
  color: #ffd319;
  border: 2px solid #ffd319;
  box-shadow: 0 0 8px rgba(255, 211, 25, 0.4);
}

#invBtn {
  color: #2de2e6;
  border: 2px solid #2de2e6;
  box-shadow: 0 0 8px rgba(45, 226, 230, 0.4);
}

/* §15: the carrier's bay. It lives with the instrument cluster rather than in
   the centre-bottom prompt stack (DOCK / JUMP / HAIL) because it is not a
   prompt — it is a persistent readout of a thing that is always running, and
   it is the only place the production line is visible. Only a carrier hull
   has one, so on everything else it is simply not there. */
#bayBtn {
  position: absolute;
  left: var(--radar-left, calc(100% - 122px));
  width: var(--radar-w, 110px);
  top: calc(var(--radar-bottom, 200px) + 40px);
  pointer-events: auto;
  height: 30px;
  padding: 0;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
  color: #b967ff;
  background: rgba(13, 2, 33, 0.7);
  border: 2px solid #b967ff;
  box-shadow: 0 0 8px rgba(185, 103, 255, 0.4);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
}
/* nothing to launch: still readable, but it stops asking to be pressed */
#bayBtn.empty { opacity: 0.62; }
#bayBtn small {
  display: block;
  font-size: var(--fs-2xs);
  font-weight: normal;
  letter-spacing: 0;
  opacity: 0.85;
}
#hud.landed #bayBtn { display: none !important; }

#jumpBtn {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(112px + env(safe-area-inset-bottom, 0px));
  pointer-events: auto;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-sm);
  letter-spacing: 2px;
  color: #0d0221;
  background: #ff6ac1;
  border: none;
  padding: 12px 20px;
  box-shadow: 0 0 12px rgba(255, 106, 193, 0.8), 3px 3px 0 #8b2fc9;
  white-space: nowrap;
}

/* §MAP2. The chart WAS a framed rectangle inside a scrolling column, with the
   frame's border and glow doing the work of saying "this is the map". It is
   the whole screen now, so the frame would be a box drawn around the edge of
   the display — every control sits on top of it instead, and the corners are
   what say where the map ends. */
#gmapWrap {
  position: relative;   /* the panels anchor to the CHART, not the overlay */
  width: 100%;
  height: 100%;
  overflow: hidden;
}

#gmapCanvas {
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: auto;
  touch-action: manipulation;
}

#interactBtn {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(112px + env(safe-area-inset-bottom, 0px));
  pointer-events: auto;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-md);
  letter-spacing: 2px;
  color: #0d0221;
  background: #ffd319;
  border: none;
  padding: 12px 22px;
  box-shadow: 0 0 12px rgba(255, 211, 25, 0.8), 3px 3px 0 #8b2fc9;
}

/* PART ICONS. The art is a white silhouette on transparency, so the tint is
   the stylesheet's job: cyan for a part that is doing its job, gold for a
   relic, grey for one you cannot use. Keeping the colour out of the PNG means
   39 files cover every state instead of 39 x every state. */
.picon {
  width: 26px;
  height: 26px;
  vertical-align: -7px;
  margin-right: 8px;
  image-rendering: auto;
  opacity: 0.92;
  /* the source is white, so a drop-shadow recolour is the cheap tint that
     works without a second copy of the file */
  filter: brightness(0) saturate(100%) invert(72%) sepia(65%)
          saturate(1200%) hue-rotate(140deg) brightness(103%) contrast(101%);
}
/* the CATEGORY tints, one hue-rotate each off the same white art: weapons
   salmon, equipment teal, reactors violet, mods yellow -- the colours the
   hold tiles, the merchant tabs and the ship diagram use (Matt, 2026-09-06) */
.picon.c-wpn  { filter: brightness(0) saturate(100%) invert(72%) sepia(65%) saturate(1200%) hue-rotate(337deg) brightness(105%) contrast(101%); }
.picon.c-gear { filter: brightness(0) saturate(100%) invert(72%) sepia(65%) saturate(1200%) hue-rotate(141deg) brightness(103%) contrast(101%); }
.picon.c-core { filter: brightness(0) saturate(100%) invert(72%) sepia(65%) saturate(1200%) hue-rotate(222deg) brightness(108%) contrast(101%); }
.picon.c-mod  { filter: brightness(0) saturate(100%) invert(72%) sepia(65%) saturate(1200%) hue-rotate(11deg) brightness(106%) contrast(101%); }
.picon.relic {
  filter: brightness(0) saturate(100%) invert(78%) sepia(58%)
          saturate(900%) hue-rotate(357deg) brightness(103%) contrast(103%);
}
.picon.dim { opacity: 0.4; filter: brightness(0) invert(52%); }
.picon.lg { width: 34px; height: 34px; vertical-align: -9px; }

/* Sits on the salvage button's right shoulder and only exists when there is
   more than one wreck to choose between. Same height as the button it steps,
   so the pair reads as one control rather than as two buttons that happen to
   be next to each other. */
#wreckNextBtn {
  position: absolute;
  left: 50%;
  bottom: calc(112px + env(safe-area-inset-bottom, 0px));
  pointer-events: auto;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-md);
  line-height: 1;
  color: #0d0221;
  background: #ffd319;
  border: none;
  border-left: 2px solid #8b2fc9;
  padding: 12px 13px;
  box-shadow: 0 0 12px rgba(255, 211, 25, 0.8), 3px 3px 0 #8b2fc9;
}

#hud.landed #wreckNextBtn { display: none !important; }

.hailTitle {
  color: #ff6ac1 !important;
  text-shadow: 0 0 10px #ff2975, 2px 2px 0 #8b2fc9 !important;
}

#hailOpts {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  max-width: 460px;   /* the contract board carries a payout line per posting */
}

.hopt {
  font-family: inherit;
  text-align: left;
  background: rgba(36, 23, 52, 0.95);
  border: 2px solid #00f0ff;
  box-shadow: 0 0 10px rgba(0, 240, 255, 0.4), 3px 3px 0 #8b2fc9;
  color: #f6f7f6;
  padding: 11px 14px;
  cursor: pointer;
}

.hopt:disabled { border-color: #3d1a6e; color: #6b5a85; box-shadow: none; }
.hopt:active:not(:disabled) { transform: translate(2px, 2px); }

.hopt .hname {
  color: #ffd319;
  font-weight: bold;
  font-size: var(--fs-md);
  letter-spacing: 2px;
  text-shadow: 0 0 5px #ff6c11;
}

.hopt .hdesc { color: #f6f7f6; font-size: var(--fs-xs); margin-top: 3px; }

/* Two-ring stick (2026-09-04). #joy is the hit area and the geometry base and
   is itself INVISIBLE; #joyRing is the throttle ring, #joyBurn the afterburner
   ring outside it. Ring RADII live in CFG.STICK as fractions of #joy, so these
   pixel sizes and those fractions have to stay in step:
     ring1 1.000 * 61 = 61px  ->  #joyRing  120px + 2px border = the element
     ring2 1.246 * 61 = 76px  ->  #joyBurn  150px + 2px border, knob stops here

   126px was the first try and it still overlapped DOCK by 2px on a 390px
   phone (joy right 144, dockBtn left 142). 122px leaves a 2px gap.

   #joy is EXACTLY the throttle ring (122px, r=61), NOT the burn ring, so
   #joyBurn deliberately overflows it. A 168px element that enclosed everything
   overlaps DOCK and the weapon readout on a phone — test_hudcluster fails it —
   and it never needed to: a thumb doesn't START a drag at the outer edge, and
   joyMove runs off setPointerCapture, so travel past the element still tracks.
   The burn ring is also drawn ONLY while a thumb is down (.live), so at rest
   the HUD looks exactly like it always did. */
/* §JOYEDGE, 2026-09-30. Matt: "On mobile the joystick needs a small buffer to
   the edge of the page because sometimes it does not register the joystick
   when boosting near the page edge." At 18px in, the burn ring (r=76 round a
   61px centre) ran to 3px off the glass -- boosting put the thumb in the
   phone's edge-gesture strip. 30px in puts the ring 15px clear. DOCK / JUMP /
   the gun chips now centre in the lane between the stick and FIRE
   (placeMidLane) so the wider stick footprint does not land on them. */
#joy {
  position: absolute;
  left: 30px;
  bottom: calc(30px + env(safe-area-inset-bottom, 0px));
  width: 122px;
  height: 122px;
  border-radius: 50%;
  pointer-events: auto;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  display: none;               /* shown only on touch devices */
}

#joy.touchmode { display: block; }
/* §JOYEDGE: a thumb that comes down already out on the burn ring still
   catches the stick. The pad reaches the ring on the left, top and bottom;
   its right side is clipped at the element's own edge so it never takes a
   tap meant for DOCK or the gun chips. */
#joy::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 172px;
  height: 172px;
  margin: -86px 0 0 -86px;
  border-radius: 50%;
  clip-path: inset(0 25px 0 0);
}
#dockBtn, #jumpBtn { left: var(--midX, 50%); }
/* the lane on a 360px phone is 104px; DOCK at full padding is 106 */
@media (max-width: 370px) { #dockBtn { padding-left: 16px; padding-right: 16px; } }

/* the afterburner ring — hidden until a thumb is down */
#joyBurn {
  position: absolute;
  left: 50%;
  top: 50%;
  /* sized so the CENTRELINE of the 2px stroke sits on ring2 (76px), which is
     exactly where the knob's centre stops — the knob parks ON the line, not
     inside it. Hence 2*76 - 2 = 150. Wider than #joy on purpose; see above. */
  width: 150px;
  height: 150px;
  margin: -75px 0 0 -75px;
  border-radius: 50%;
  border: 2px dashed rgba(255, 108, 17, 0.32);
  opacity: 0;
  pointer-events: none;
  transition: opacity .14s linear, border-color .12s linear, box-shadow .12s linear;
}
#joy.live #joyBurn { opacity: 1; }

/* the throttle ring: everything inside this is ordinary flying */
#joyRing {
  position: absolute;
  left: 50%;
  top: 50%;
  /* likewise: 2*61 - 2 = 120, so the stroke's centreline is ring1 */
  width: 120px;
  height: 120px;
  margin: -60px 0 0 -60px;
  border-radius: 50%;
  border: 2px solid rgba(0, 240, 255, 0.5);
  background: rgba(13, 2, 33, 0.4);
  box-shadow: 0 0 12px rgba(0, 240, 255, 0.25) inset;
  pointer-events: none;
  transition: border-color .12s linear;
}

/* past the throttle ring but not yet burning — the ring warms up so crossing
   the line is something you see before you feel it */
#joy.arm #joyBurn { border-color: rgba(255, 108, 17, 0.75); }
#joy.arm #joyRing { border-color: rgba(255, 108, 17, 0.55); }

#joy.boost #joyBurn {
  border-style: solid;
  border-color: #ff6c11;
  box-shadow: 0 0 18px rgba(255, 108, 17, 0.55);
}
#joy.boost #joyRing { border-color: rgba(255, 108, 17, 0.4); }

#joyKnob {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 46px;
  height: 46px;
  margin: -23px 0 0 -23px;
  border-radius: 50%;
  background: rgba(0, 240, 255, 0.35);
  border: 2px solid #00f0ff;
  box-shadow: 0 0 10px rgba(0, 240, 255, 0.7);
  pointer-events: none;
  transition: background .1s linear, border-color .1s linear, box-shadow .1s linear;
}

#joy.arm #joyKnob {
  background: rgba(255, 108, 17, 0.3);
  border-color: #ff9d4d;
  box-shadow: 0 0 12px rgba(255, 108, 17, 0.7);
}
#joy.boost #joyKnob {
  background: rgba(255, 108, 17, 0.55);
  border-color: #ffd319;
  box-shadow: 0 0 20px rgba(255, 108, 17, 0.95);
}

@keyframes dockpulse {
  0%   { box-shadow: 0 0 12px rgba(0, 240, 255, 0.8), 3px 3px 0 #8b2fc9; }
  50%  { box-shadow: 0 0 22px rgba(0, 240, 255, 1),   3px 3px 0 #8b2fc9; }
  100% { box-shadow: 0 0 12px rgba(0, 240, 255, 0.8), 3px 3px 0 #8b2fc9; }
}

/* was "II", which is a symbol you have to already know. It opens a menu, so
   it says MENU. */
#pauseBtn {
  color: #00f0ff;
  border: 2px solid #00f0ff;
  box-shadow: 0 0 10px rgba(0, 240, 255, 0.5);
}

/* ---------- Overlays ---------- */

.overlay {
  position: absolute;
  inset: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  text-align: center;
  background: rgba(13, 2, 33, 0.82);
  padding: 20px;
  /* a landscape phone is shorter than most of these menus — every one of
     them has to be reachable, so they all scroll rather than clip */
  overflow-y: auto;
  overscroll-behavior: contain;
}

.overlay h1 {
  font-size: clamp(40px, 12vw, 84px);
  letter-spacing: 6px;
  color: #ff2975;
  text-shadow:
    0 0 8px #ff2975,
    0 0 30px #ff2975,
    3px 3px 0 #8b2fc9;
}

.overlay h1 span {
  color: #00f0ff;
  text-shadow:
    0 0 8px #00f0ff,
    0 0 30px #00f0ff,
    3px 3px 0 #8b2fc9;
}

/* ---------- the logo ---------------------------------------------------- */
/* Matt's art, 2026-09-05. The GLOW and the REFLECTION live here rather than in
   the file, because he was right to worry about placement: baked in, they fix
   the logo's margins forever and have to be re-cut for every size it appears
   at. As CSS they follow the image wherever it goes, cost no extra download,
   and the reflection reuses the SAME file the browser has already fetched. */
.gamelogo {
  position: relative;
  width: min(560px, 78vw);
  margin: 0 auto 2px;
  line-height: 0;                      /* no descender gap under the image */
}
.gamelogo img {
  display: block;
  width: 100%;
  height: auto;
  /* Two lamps, not one: the art's own bloom is warm pink, so the outer halo is
     the cyan the wordmark's top half already is, and the inner is the ember
     red. Layered drop-shadows follow the ALPHA, which is why this reads as
     neon on the letterforms instead of a rectangle behind them. */
  filter: drop-shadow(0 0 10px rgba(0, 240, 255, 0.42))
          drop-shadow(0 0 26px rgba(255, 56, 100, 0.40))
          drop-shadow(0 0 60px rgba(139, 47, 201, 0.34));
}
/* The reflection. Same image, flipped, masked to fade out -- so it is a
   reflection of whatever the logo actually is, and stays in register if the
   art is ever replaced.
   IN FLOW, not absolutely positioned. As an absolute ::after it reserved no
   space and laid itself straight over FLY - TRADE - SURVIVE. `padding-bottom`
   as a PERCENTAGE resolves against the containing block's WIDTH, which is the
   one trick that lets a box be a fixed fraction of the logo's width without
   anyone hard-coding a pixel height: the logo is 1200x432, so 0.36 of its
   width is its full height and 14% is the bottom ~40% of it.
   `center bottom` on the background is the other load-bearing part -- the box
   shows the BOTTOM of the wordmark, and the flip then puts that edge nearest
   the real logo, which is what makes it read as a reflection rather than as a
   second logo upside down. */
.gamelogo .reflect {
  height: 0;
  padding-bottom: 14%;
  overflow: hidden;
  background: url(../assets/logo.webp) no-repeat center bottom / 100% auto;
  transform: scaleY(-1);
  -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0) 8%, rgba(0,0,0,0.5));
  mask-image: linear-gradient(to bottom, rgba(0,0,0,0) 8%, rgba(0,0,0,0.5));
  opacity: 0.38;
  filter: blur(1.5px) saturate(1.15);
  pointer-events: none;
}
/* The text title is the fallback and the accessible name. It is only hidden
   once the image has actually decoded, so a failed load still says the game's
   name instead of showing an empty gap. */
/* NOT an adjacent-sibling selector: the reflection div sits between the image
   and the heading, so `img + h1` silently stopped matching the moment the
   reflection moved into the flow -- and the old text title reappeared UNDER
   the logo. Scope it to the box instead, which cannot be broken by reordering
   what is inside it. */
.gamelogo h1.titleTwoLine { display: none; }
.gamelogo.broken img,
.gamelogo.broken .reflect { display: none; }
.gamelogo.broken h1.titleTwoLine { display: block; }

/* In PAUSE it is a wordmark, not a splash: smaller, no reflection (there is a
   live game behind it and a mirrored logo over the ship reads as a glitch). */
.gamelogo.small {
  width: min(300px, 56vw);
  margin-bottom: 6px;
}
.gamelogo.small .reflect { display: none; }
.gamelogo.small img {
  filter: drop-shadow(0 0 8px rgba(0, 240, 255, 0.36))
          drop-shadow(0 0 20px rgba(255, 56, 100, 0.32));
}

/* Short screens have no room for a splash AND a stack of buttons: the logo
   gives way first, and the reflection goes before the logo does.
   THE CAP HAS TO INCLUDE HEIGHT. Width alone was not enough -- on an 820x412
   landscape phone `min(420px, 62vw)` is 420px wide and therefore 151px TALL,
   which pushed the whole title screen up until the logo's own top sat at -86px
   and the button stack slid under the tap `test_rotatefs` makes. The wordmark
   is 1200x432, so 2.78 is its aspect: capping at 26vh of height means capping
   at 26vh x 2.78 of width, and the logo can never again be the thing that
   overflows a short screen. */
@media (max-height: 620px) {
  /* Capped by HEIGHT as well as width, because width alone let the logo be
     151px tall on an 820x412 landscape phone and push the button stack up
     under the tap `test_rotatefs` makes.
     24vh, measured against the RIGHT baseline: the title screen at that size
     already overflowed by 77px carrying the two-line TEXT title, and with the
     logo at 24vh it overflows by less than that. (A first pass compared
     against no title at all, which flattered the old layout and made the logo
     look like the regression it is not.) */
  .gamelogo { width: min(420px, 62vw, calc(24vh * 2.78)); }
  .gamelogo .reflect { display: none; }
  .gamelogo.small { width: min(220px, 44vw, calc(16vh * 2.78)); }
}

.overlay h1.titleTwoLine {
  font-size: clamp(32px, 10vw, 68px);
  line-height: 1.05;
}

.overlay h1.titleTwoLine span {
  color: #ff6c11;
  text-shadow:
    0 0 8px #ff6c11,
    0 0 30px #ff3864,
    3px 3px 0 #8b2fc9;
}

.overlay h2 {
  font-size: clamp(26px, 8vw, 48px);
  letter-spacing: 4px;
  color: #00f0ff;
  text-shadow: 0 0 10px #00f0ff, 2px 2px 0 #8b2fc9;
}

.overlay h2.red    { color: #ff3864; text-shadow: 0 0 10px #ff3864, 2px 2px 0 #8b2fc9; }
.overlay h2.yellow { color: #ffd319; text-shadow: 0 0 10px #ff6c11, 2px 2px 0 #8b2fc9; }

.overlay .sub {
  color: #ffd319;
  letter-spacing: 3px;
  font-size: var(--fs-sm);
  text-shadow: 0 0 6px #ff6c11;
}

.overlay .how {
  color: #f6f7f6;
  font-size: var(--fs-md);
  line-height: 1.7;
  text-shadow: 0 0 4px #8b2fc9;
}

.overlay .how em { color: #ff2975; font-style: normal; }

button.big {
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-lg);
  letter-spacing: 2px;
  color: #0d0221;
  background: #00f0ff;
  border: none;
  padding: 14px 28px;
  min-width: 220px;
  box-shadow:
    0 0 12px rgba(0, 240, 255, 0.8),
    4px 4px 0 #8b2fc9;
  cursor: pointer;
}

button.big:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 12px rgba(0, 240, 255, 0.8), 2px 2px 0 #8b2fc9;
}

/* ---------- Dock / station menu ---------- */

.dockoverlay {
  justify-content: flex-start;
  /* Matt, 2026-09-06: "use more of the available space on the sides and the
     new real estate on top" -- the header is a footer now, so the content
     starts under the safe area with a hand's width of margin, no more */
  padding: calc(10px + env(safe-area-inset-top, 0px)) 12px 0;
  overflow-y: auto;
  gap: 8px;
}
/* the bar along the bottom: purse | UNDOCK | who and how they feel about you */
#dockFoot {
  position: sticky;
  bottom: 0;
  z-index: 3;
  width: 100%;
  box-sizing: border-box;   /* the global reset is off (§CSS-RESET): 100% + padding overflowed the overlay sideways */
  max-width: 1800px;
  margin-top: auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 10px 18px;
  padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
  background: rgba(13, 2, 33, 0.92);
  border-top: 1px solid rgba(255, 106, 193, 0.35);
  box-shadow: 0 -10px 24px rgba(13, 2, 33, 0.85);
}
#dockFoot #dockCredits { margin: 0; text-align: left; justify-self: start; }
#dockFoot #dockWho { justify-self: end; text-align: right; min-width: 0; }
#dockFoot #dockName {
  font-size: clamp(15px, 2.2vw, 22px);
  letter-spacing: 3px;
  margin: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#dockFoot #dockFaction { margin: 2px 0 0; font-size: var(--fs-xs); }
#dockFoot #undockBtn { min-width: 170px; padding: 11px 24px; }
@media (max-width: 700px) {
  /* a phone: name and standing on their own line above, purse and UNDOCK
     side by side below it */
  #dockFoot { grid-template-columns: minmax(0, 1fr) auto; gap: 6px 12px; padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px)); }
  #dockFoot #dockWho { grid-row: 1; grid-column: 1 / -1; justify-self: stretch; text-align: center; }
  #dockFoot #dockCredits, #dockFoot #undockBtn { grid-row: 2; }
  #dockFoot #dockName { font-size: var(--fs-sm); letter-spacing: 2px; }
  #dockFoot #dockFaction { font-size: var(--fs-2xs); margin-top: 0; }
  #dockFoot #dockCredits { font-size: var(--fs-xs); letter-spacing: 1px; }
  #dockFoot #undockBtn { min-width: 0; padding: 10px 16px; font-size: var(--fs-md); }
}

#dockBody, #invBody {
  /* fill the overlay's CONTENT box: sizing against the viewport instead
     meant width + the overlay's own padding overflowed it sideways */
  width: 100%;
  max-width: 560px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 8px;
  flex: 0 0 auto;
}

.dsec {
  color: #00f0ff;
  font-weight: bold;
  font-size: var(--fs-lg);
  letter-spacing: 3px;
  text-align: left;
  margin-top: 10px;
  text-shadow: 0 0 6px #00f0ff;
  border-bottom: 1px solid #3d1a6e;
  padding-bottom: 3px;
}

.drow {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;          /* a flex item defaults to its content width, which
                            is how long rows shoved their buttons off-screen */
  flex-wrap: wrap;
  background: rgba(36, 23, 52, 0.9);
  border: 1px solid #3d1a6e;
  padding: 8px 10px;
  text-align: left;
}

.drow .dname {
  flex: 1 1 40%;
  min-width: 0;
  overflow-wrap: anywhere;
  color: #f6f7f6;
  font-size: var(--fs-lg);
  font-weight: bold;
  letter-spacing: 1px;
}

.drow .dname small {
  display: block;
  color: #ff6ac1;
  font-weight: normal;
  font-size: var(--fs-md);
  letter-spacing: 0;
}

.drow .dprice { color: #ffd319; font-size: var(--fs-lg); font-weight: bold; white-space: nowrap; }
.drow .dprice.good { color: #39ff14; }
.drow .dprice.bad  { color: #ff3864; }

.dbtn {
  flex: 0 0 auto;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-md);
  letter-spacing: 1px;
  color: #0d0221;
  background: #00f0ff;
  border: none;
  padding: 8px 12px;
  min-width: 54px;
  box-shadow: 2px 2px 0 #8b2fc9;
  cursor: pointer;
}

.dbtn.sell { background: #ffd319; }
/* material requirement lines under yard rows */
#dockBody small { font-size: var(--fs-md); }
.dbtn.warn { background: #ff2975; color: #f6f7f6; }
/* REQUIREMENTS: the thing is not buildable right now -- the button opens the
   bill to show what is missing rather than a purchase. Amber, so it never
   reads as the pink BUILD it is standing in for. */
.dbtn.req { background: #241734; color: #ffb000; border: 1px solid #ffb000; box-shadow: 2px 2px 0 #5a3a00; }
.dbtn:disabled { background: #241734; color: #6b5a85; box-shadow: none; }
.dbtn:active:not(:disabled) { transform: translate(1px, 1px); box-shadow: 1px 1px 0 #8b2fc9; }

/* ---------- Boss warning ---------- */

#bossWarn {
  position: absolute;
  top: 22%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 15;
  color: #ff3864;
  font-weight: bold;
  font-size: clamp(14px, 4.5vw, 24px);
  letter-spacing: 3px;
  text-shadow: 0 0 10px #ff3864;
  animation: warnblink 0.5s steps(2) infinite;
  pointer-events: none;
  white-space: nowrap;
}

/* §TOPLANE: moved into the lane between the HUD columns (game.js layoutTopLane) */
#bossWarn.inLane { text-align: center; font-size: clamp(13px, 2.6vw, 20px); letter-spacing: 2px; line-height: 1.2; }
#targetLabel.inLane { text-align: center; }

@keyframes warnblink {
  0%   { opacity: 1; }
  50%  { opacity: 0.15; }
  100% { opacity: 1; }
}


/* ---------- HEATSCOPE: the abyss keeps score ---------- */

#scopewrap {
  position: absolute;
  top: calc(var(--barY) + 58px);
  left: var(--barX);
  width: 34%;          /* §HUDICONS: the same width as the rungs above it */
  max-width: 220px;
  height: 5px;
  background: #241734;
  border: 1px solid #3a2a52;
}

#scopebar {
  height: 100%;
  width: 0;
  background: linear-gradient(to right, #8b2fc9, #ff2975, #ff3864);
  box-shadow: 0 0 6px rgba(255, 41, 117, .6);
}

#scopewrap .tick {
  position: absolute;
  top: -2px;
  width: 1px;
  height: 9px;
  background: #ffd319;
  opacity: .75;
}

/* §HEATLINES: placed by Game.placeScopeTicks from CFG.HEATSCOPE.stages (the
   old 35/65/85% here never matched the 25/50/75% the dispatcher fires at).
   A neon line, taller than the bar, brighter once the meter has crossed it. */
#scopewrap .tick {
  top: -4px;
  width: 2px;
  height: 13px;
  margin-left: -1px;
  background: #00f0ff;
  opacity: .8;
  box-shadow: 0 0 4px #00f0ff, 0 0 8px rgba(0, 240, 255, .6);
}
#scopewrap .tick.lit {
  background: #ffd319;
  opacity: 1;
  box-shadow: 0 0 5px #ffd319, 0 0 10px rgba(255, 211, 25, .7);
}

/* ---------- manual zoom controls ---------- */

#zoomInBtn, #zoomOutBtn {
  position: absolute;
  right: 108px;
  pointer-events: auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-xl);
  color: #2de2e6;
  background: rgba(36, 23, 52, 0.75);
  border: 2px solid #2de2e6;
  box-shadow: 0 0 8px rgba(45, 226, 230, 0.4);
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  cursor: pointer;
}

/* §THUMBS-2 (2026-10-05): BOOST and TARGET NEAREST are gone (the stick's
   boost ring and auto-targeting do their jobs), so the zoom pair comes down
   WEAPONS takes the column above OPEN FIRE with its gear beside it, and the
   zoom pair stands one place further left, clear of the centred DOCK prompt
   on a narrow phone */
#zoomInBtn, #zoomOutBtn { right: 156px; }
#zoomInBtn  { bottom: calc(160px + env(safe-area-inset-bottom, 0px)); }
#zoomOutBtn { bottom: calc(118px + env(safe-area-inset-bottom, 0px)); }

/* ---------- §MAP2: the map screen ----------
   Item 13: "With every edge clear, the map goes fullscreen." `.mapfull` is
   the overlay stripped of everything that made it a page — no padding, no
   gap, no scroll, no title. It sizes off --menuw/--menuh rather than vw/vh
   because game.js puts a CSS `zoom` on this element for touch, and vw inside
   a zoomed box still measures the unzoomed viewport. */
.overlay.mapfull {
  display: block;
  padding: 0;
  gap: 0;
  overflow: hidden;
  width: var(--menuw, 100vw);
  height: var(--menuh, 100vh);
  max-width: none;
  max-height: none;
}

/* Every control on the chart shares one look, so a corner reads as a corner
   rather than as four unrelated buttons that happen to be near each other. */
#gmap .mapctl {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 13px;
  font-family: inherit;
  font-size: var(--fs-lg);
  letter-spacing: 1px;
  color: #2de2e6;
  background: rgba(13, 2, 33, 0.88);
  border: 1px solid rgba(45, 226, 230, 0.55);
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.55);
  cursor: pointer;
  white-space: nowrap;
}
#gmap .mapctl:active { transform: translate(1px, 1px); }
#gmap .mapctl[aria-pressed="true"] {
  color: #0d0221;
  background: #2de2e6;
  border-color: #2de2e6;
}

/* Item 5: the overlay button carries its state in its COLOUR. A label is
   something you read; a colour is something you see, and this button sits
   beside a chart whose entire language is colour. */
#gmap #mapZoneBtn[data-overlay="danger"] {
  color: #ff3b30;
  border-color: rgba(255, 59, 48, 0.7);
  box-shadow: 0 0 10px rgba(255, 59, 48, 0.28), 0 0 10px rgba(0, 0, 0, 0.55);
}
#gmap #mapZoneBtn[data-overlay="flag"] {
  color: #ffd319;                      /* the + MARKER yellow */
  border-color: rgba(255, 211, 25, 0.7);
  box-shadow: 0 0 10px rgba(255, 211, 25, 0.28), 0 0 10px rgba(0, 0, 0, 0.55);
}
/* "off" keeps the plain .mapctl cyan — item 5 says PLAIN wears its current
   colour, and the current colour of a map control is cyan. */

/* Item 4: MY LOCATION, with a crosshair, in the same orange the markers use —
   because the thing it centres on is marked in that orange on the chart.
   Scoped under #gmap to beat `#gmap .mapctl`'s cyan, same as the panels above. */
#gmap #mapCenterBtn { color: #ff6c11; border-color: rgba(255, 108, 17, 0.6); }
#gmap #mapCourseBtn { color: #39ff14; border-color: rgba(57, 255, 20, 0.65); white-space: nowrap; }   /* §MAPCOURSE: the waypoint's green */
#gmap #mapCourseBtn { position: absolute; left: 50%; transform: translateX(-50%); z-index: 4;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px) + 54px); }
@media (max-width: 700px) { #gmap #mapCourseBtn .nm { display: none; } }
#mapCenterBtn .xhair {
  position: relative;
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
}
#mapCenterBtn .xhair::before,
#mapCenterBtn .xhair::after {
  content: '';
  position: absolute;
  background: currentColor;
}
#mapCenterBtn .xhair::before { left: 50%; top: -5px; bottom: -5px; width: 1.5px; margin-left: -0.75px; }
#mapCenterBtn .xhair::after  { top: 50%; left: -5px; right: -5px; height: 1.5px; margin-top: -0.75px; }

/* Item 3: ONE control, not two buttons that happen to sit together. The glass
   between the halves is what makes it read as a rocker you press a side of. */
#mapZoomRocker {
  display: inline-flex;
  align-items: stretch;
  background: rgba(13, 2, 33, 0.88);
  border: 1px solid rgba(45, 226, 230, 0.55);
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.55);
}
#mapZoomRocker button {
  min-width: 46px;
  padding: 6px 10px;
  font-family: inherit;
  font-size: var(--fs-2xl);
  line-height: 1.1;
  color: #2de2e6;
  background: none;
  border: none;
  cursor: pointer;
}
#mapZoomRocker button:active { background: rgba(45, 226, 230, 0.18); }
/* Drawn, not typed. The magnifier codepoints are a lottery — U+2315 and
   U+29A6 render as anything from a stray phi to a missing glyph box
   depending on the font stack, and U+1F50D drags a colour emoji into a
   monochrome control. Two divs make the same picture everywhere. */
#mapZoomRocker .rockglass {
  position: relative;
  display: flex;
  align-items: center;
  width: 30px;
  color: #8b7fa8;
  border-left: 1px solid rgba(45, 226, 230, 0.35);
  border-right: 1px solid rgba(45, 226, 230, 0.35);
}
#mapZoomRocker .rockglass::before {          /* the lens */
  content: '';
  position: absolute;
  left: 7px;
  top: 50%;
  width: 12px;
  height: 12px;
  margin-top: -8px;
  border: 1.6px solid currentColor;
  border-radius: 50%;
}
#mapZoomRocker .rockglass::after {           /* the handle */
  content: '';
  position: absolute;
  left: 17px;
  top: 50%;
  width: 1.6px;
  height: 7px;
  margin-top: 3px;
  background: currentColor;
  transform: rotate(-45deg);
  transform-origin: top center;
}

/* ---- the corners ---- */
#mapFrameRow {                       /* item 12: bottom centre */
  position: absolute;
  left: 50%;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* Wrapping is the backstop, not the plan. The short labels below are what
     make the row fit on a phone; if a future control makes it too wide
     anyway, it grows UPWARD into the chart rather than sliding under the
     corner clusters, which is the failure you can see and fix. */
  flex-wrap: wrap;
  /* `width: max-content` is load-bearing, not tidiness. With `left: 50%` and
     `right: auto` an absolutely positioned box's shrink-to-fit width is
     measured from `left` to the container's RIGHT EDGE -- half the screen --
     so a 440px row on an 844px map saw 422px of "available" space and wrapped
     for no reason. Sizing to content sidesteps the whole calculation. */
  width: max-content;
  max-width: calc(100% - 24px);
}
/* one label or the other, never both */
#gmap .mapctl .lblshort { display: none; }
#mapKeyCluster {                     /* items 6 + 7: bottom left */
  position: absolute;
  left: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  max-width: calc(100% - 24px);
}
#mapKeyRow { display: flex; gap: 8px; align-items: center; }
#mapMarkCluster {                    /* item 8: bottom right */
  position: absolute;
  right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  max-width: calc(100% - 24px);
}

/* Item 15: INFO on top, MARKERS in the middle, MAP KEY at the bottom — and
   the topmost of whatever is open raises its opacity so it stays readable
   against the one behind it. The mockup showed these only ever touching at
   844x390 and below, so this is a rule for phones that costs desktops
   nothing. */
/* All three scoped the SAME WAY on purpose. `#gmap .mappanel` is one id plus
   one class; a bare `#markerPanel` is one id, so it LOSES the cascade and all
   three panels sat at z-index 6 with the stacking rule doing nothing. A
   specificity mismatch inside one small block of rules is the quietest bug in
   CSS: nothing errors, the rule is simply never the one that applies. */
#gmap .mappanel   { z-index: 6; }
#gmap #markerPanel { z-index: 7; }
#gmap #mapTips     { z-index: 8; }
#gmap .mappanel.ontop {
  background: rgba(13, 2, 33, 0.985);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 6px 22px rgba(0, 0, 0, 0.7);
}

/* Item 10: the close X owns the TOP RIGHT, alone. It used to share that
   corner with the ⓘ, which meant the title had to be margined away from both
   of them and neither could grow. */
#closeMapBtn {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top, 0px));
  right: 12px;
  z-index: 9;
  width: 48px;
  height: 48px;
  padding: 0;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-2xl);
  line-height: 1;
  color: #0d0221;
  background: #00f0ff;
  border: none;
  box-shadow: 0 0 12px rgba(0, 240, 255, 0.8), 3px 3px 0 #8b2fc9;
  cursor: pointer;
}
#closeMapBtn:active { transform: translate(2px, 2px); }

/* Item 9: the ⓘ moves to the TOP LEFT and its popup opens below it, so help
   never covers the way out. */
#mapInfoBtn {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top, 0px));
  left: 12px;
  z-index: 9;
  width: 48px;
  height: 48px;
  padding: 0;
  font-family: Georgia, "Times New Roman", serif;
  font-style: italic;
  font-weight: bold;
  font-size: var(--fs-2xl);
  line-height: 1;
  color: #00f0ff;
  background: rgba(36, 23, 52, 0.85);
  border: 2px solid #00f0ff;
  border-radius: 50%;
  box-shadow: 0 0 10px rgba(0, 240, 255, 0.5);
  cursor: pointer;
}
#mapInfoBtn:active { transform: translate(2px, 2px); }
/* ---------- map key ----------
   Bottom-left, two columns, symbol and name only. An earlier version spelled
   each entry out in prose and covered a third of the chart — a bad trade for
   something a player reads twice and then knows. Left corner because the tips
   panel owns the right one. */
/* Item 6: the key opens ABOVE its own button, in the bottom-left cluster —
   so the thing that opens it and the thing that opens are one object, and
   clicking the button again closes it. In flow inside the cluster rather than
   absolutely placed, which is what keeps the button pinned to the corner
   while the panel grows upward out of it. */
#mapKey {
  order: -1;                     /* above the button row in the column */
  width: max-content;
  max-width: min(340px, calc(var(--menuw, 100vw) - 40px));
  max-height: calc(var(--menuh, 100vh) - 120px);
  overflow-y: auto;
  overscroll-behavior: contain;
  text-align: left;
  font-size: var(--fs-xs);
  line-height: 1.25;
  letter-spacing: 1px;
  color: #f6f7f6;
  background: rgba(13, 2, 33, 0.93);
  border: 1px solid #8b2fc9;
  box-shadow: 0 0 12px rgba(139, 47, 201, 0.4);
  padding: 8px 11px 10px;
}
#mapKey .kgrp {
  color: #ffd319;
  font-size: var(--fs-2xs);
  letter-spacing: 2.5px;
  margin: 9px 0 4px;
  padding-bottom: 2px;
  border-bottom: 1px solid #3d1a6e;
}
#mapKey .kgrp:first-child { margin-top: 0; }
#mapKey .kcols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px 14px;
}
#mapKey .krow {
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
/* the one row with an explanation on it wraps rather than being clipped */
#mapKey .krow.wide { grid-column: 1 / -1; white-space: normal; align-items: flex-start; }
#mapKey .krow.wide i { margin-top: 4px; }
#mapKey i { flex: 0 0 auto; display: block; }
/* the danger ramp: ten segments in order, so the key reads as a scale */
#mapKey .kcols.ramp { display: block; }
#mapKey .kramp { display: flex; height: 12px; border: 1px solid rgba(255,255,255,.18); }
#mapKey .kramp i { flex: 1 1 0; display: block; }
#mapKey .krampends {
  display: flex; justify-content: space-between;
  font-size: var(--fs-2xs); color: #b9a7e8; letter-spacing: 1px; margin-top: 3px;
}
#mapKey .ksw   { width: 14px; height: 8px; }
#mapKey .kln   { width: 14px; height: 3px;
                 background: linear-gradient(90deg, #ffd319, #ff6c11); }
#mapKey .kdash { width: 14px; height: 0; border-top: 2px dashed #ff3864; }
#mapKey .kap   { width: 14px; height: 0; border-top: 2px dashed #ff6ac1; }
/* Glyph shapes MATCH the chart: trade posts and signals are drawn as small
   squares, the player as a white disc inside a green ring. A key whose shapes
   disagree with the map is worse than none. */
#mapKey .kdot  { width: 7px; height: 7px; margin: 0 3.5px; }
#mapKey .kyou  { width: 7px; height: 7px; margin: 0 3.5px;
                 background: #f6f7f6; border-radius: 50%;
                 box-shadow: 0 0 0 2px #39ff14; }
#mapKey .kring { width: 9px; height: 9px; border: 1px solid; border-radius: 50%;
                 margin: 0 2.5px; }
#mapKey .kdia  { width: 8px; height: 8px; margin: 0 3px; transform: rotate(45deg); }
/* The legend always describes what the chart is actually drawing. Whichever
   territory map is off is dimmed rather than removed, so the key does not
   jump around under the reader's eye when the overlay is cycled. */
#mapKey [data-key] { transition: opacity .12s linear; }
#mapKey[data-overlay="flag"] [data-key="danger"],
#mapKey[data-overlay="danger"] [data-key="flag"],
#mapKey[data-overlay="off"] [data-key] { opacity: .34; }
#mapKey[data-overlay] .kgrp[data-key]::after { content: ''; }
#mapKey[data-overlay="danger"] .kgrp[data-key="danger"]::after,
#mapKey[data-overlay="flag"] .kgrp[data-key="flag"]::after {
  content: ' \2022 SHOWN'; color: #9a92b5; font-size: var(--fs-2xs);
}
#mapKey .knote { margin-top: 7px; color: #9a92b5; font-size: var(--fs-2xs);
                 letter-spacing: .4px; line-height: 1.35; }
#mapKey sup { color: #9a92b5; font-size: var(--fs-2xs); }

/* on a phone the chart is already small — one column, tighter still */
@media (max-width: 700px) {
  #mapKey { font-size: var(--fs-2xs); padding: 5px 7px 7px; max-width: min(200px, 52vw); }
  #mapKey .kcols { grid-template-columns: 1fr; }
  #mapKey .knote { display: none; }
}

#mapTips {
  position: absolute;
  top: calc(68px + env(safe-area-inset-top, 0px));
  left: 12px;
  width: max-content;
  max-width: min(420px, calc(var(--menuw, 100vw) - 24px));
  max-height: calc(var(--menuh, 100vh) - 140px);
  overflow-y: auto;
  overscroll-behavior: contain;
  text-align: left;
  font-size: var(--fs-xs);
  line-height: 1.5;
  letter-spacing: 1px;
  color: #c9c2e0;
  background: rgba(13, 2, 33, 0.93);
  border: 1px solid #8b2fc9;
  box-shadow: 0 0 12px rgba(139, 47, 201, 0.4);
  padding: 10px 13px 12px;
}
#mapTips b {
  display: block;
  color: #ffd319;
  letter-spacing: 2px;
  margin-bottom: 6px;
  text-shadow: 0 0 6px #ff6c11;
}

/* §MAP2: #mapZoomRow is gone, split into the rocker (#mapZoomRocker) and the
   corner clusters. Its button styling lives on .mapctl now, which every
   control on the chart shares. */

#gmapCanvas { touch-action: none; cursor: grab; }

/* ---------- shipyard portraits & stat deltas ---------- */

.shipthumb {
  width: 42px;
  height: 42px;
  image-rendering: pixelated;
  flex: none;
  margin-right: 10px;
  background: rgba(13, 2, 33, .6);
  border: 1px solid #3a2a52;
}

.drow .dname i.up { color: #39ff14; font-style: normal; font-size: var(--fs-sm); margin-left: 2px; }
.drow .dname i.dn { color: #ff3864; font-style: normal; font-size: var(--fs-sm); margin-left: 2px; }

/* ---------- tutorial ---------- */

#tut { z-index: 40; }

#tut #tutBody {
  max-width: 460px;
  min-height: 180px;
  text-align: left;
  line-height: 1.55;
}







#tutNav { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; }

#tutNav button {
  pointer-events: auto;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-sm);
  letter-spacing: 2px;
  color: #05d9e8;
  background: rgba(36, 23, 52, 0.8);
  border: 1px solid #05d9e8;
  padding: 10px 18px;
  cursor: pointer;
}

#tutNav button:disabled { color: #4a3f70; border-color: #3a2a52; }

#tutHideRow {
  display: block;
  color: #7a6fa8;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
  margin-bottom: 12px;
  cursor: pointer;
}

#tutHideRow input { accent-color: #05d9e8; margin-right: 6px; }

#tutHideRow small { display: block; font-size: var(--fs-xs); opacity: .8; }

.big.alt { background: #241734; color: #05d9e8; border: 1px solid #05d9e8; }


/* ---------- flight school banner ---------- */

#tutBanner {
  position: absolute;
  top: calc(6px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  width: max-content;              /* don't shrink to the 50%-offset column */
  max-width: min(560px, 92vw);
  display: flex;
  align-items: center;
  gap: 12px;
  background: rgba(13, 2, 33, .68);
  border: 1px solid #05d9e8;
  box-shadow: 0 0 14px rgba(5, 217, 232, .35);
  padding: 10px 14px;
  z-index: 45;            /* above the dock / map overlays — lessons stay visible while docked */
  pointer-events: none;   /* taps pass through to the game — only SKIP is a button */
}

/* while training, docked/map screens start below the lesson banner */
.tutting .dockoverlay {
  padding-top: calc(104px + env(safe-area-inset-top, 0px));
}

/* the banner owns the top edge during training — the HUD top cluster ducks
   down 104px so bars, buttons and readouts all stay in clear view */
.tutting #dangerBanner { top: calc(118px + env(safe-area-inset-top, 0px)); }
.tutting #targetLabel  { top: calc(114px + env(safe-area-inset-top, 0px)); }
.tutting #hudApRow     { top: calc(114px + env(safe-area-inset-top, 0px)); }
.tutting #hudBtnRow    { top: calc(var(--radar-bottom, 200px) + 110px); }
/* the training banner owns the top edge — move the two anchors, not nine rules */
.tutting #hud {
  --barY: calc(116px + env(safe-area-inset-top, 0px));
  /* §HUDBARS: the ladder still starts at 116 rendered, and is 68 x .75 = 51 tall */
  --labelY: calc(169px + env(safe-area-inset-top, 0px));
}

#tutBannerText {
  color: #e8e3ff;
  font-size: var(--fs-sm);
  letter-spacing: 1px;
  line-height: 1.5;
}

#tutBannerText b { color: #05d9e8; margin-right: 6px; }
#tutBannerText em { color: #ffd319; font-style: normal; }

#tutSkip {
  pointer-events: auto;
  flex: none;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
  color: #7a6fa8;
  background: transparent;
  border: 1px solid #4a3f70;
  padding: 6px 10px;
  cursor: pointer;
}

/* pointer-tap feedback for in-flight HUD buttons (see bindTap in main.js) */
.tapping {
  filter: brightness(1.45);
  transform: scale(0.96);
}
#wpnBar .wchip.tapping { transform: scale(0.94); }

/* parts requirements: what you hold vs what you still need */
#dockBody small b.have { color: #39ff14; font-weight: bold; }
#dockBody small b.need { color: #ff3864; font-weight: bold; }
#dockBody small b.none { color: #7a6fa8; font-weight: bold; }

/* faction standing bars in the inventory */
.stbar {
  position: relative;
  margin-top: 5px;
  height: 5px;
  width: 100%;
  background: rgba(13, 2, 33, .9);
  border: 1px solid #241734;
}

.stbar i { display: block; height: 100%; }

/* the NEUTRAL mark, so you can see which side of it you're on */
.stbar u {
  position: absolute;
  left: 50%;
  top: -2px;
  width: 1px;
  height: 9px;
  background: #7a6fa8;
}
/* §HEATLINES: the heat bar's hunter-wave lines -- one per stage, neon, and
   lit once the meter has crossed it */
.stbar.heat { overflow: visible; }
.stbar.heat u.hz {
  left: auto;
  top: -4px;
  width: 2px;
  height: 13px;
  margin-left: -1px;
  background: #00f0ff;
  box-shadow: 0 0 4px #00f0ff, 0 0 9px rgba(0, 240, 255, .65);
}
.stbar.heat u.hz.lit {
  background: #ffd319;
  box-shadow: 0 0 5px #ffd319, 0 0 10px rgba(255, 211, 25, .75);
}

#dockFaction { letter-spacing: 1px; }
#dockFaction small { color: #7a6fa8; }

/* ---------- planet surface ---------- */
/* The surface is a PLAYABLE screen: the canvas underneath is the walkable
   world, so nothing here may be a full-screen .overlay. */
#surfaceBar {
  position: absolute;
  top: calc(8px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 15;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 14px;
  box-sizing: border-box;
  /* shrink to content — on a wide monitor a full-width strip reads as a
     mystery bar and hides its own text under the HUD meters */
  width: auto;
  max-width: calc(100vw - 16px);
  white-space: nowrap;
  background: rgba(13, 2, 33, 0.72);
  border: 1px solid rgba(0, 240, 255, 0.45);
  box-shadow: 0 0 14px rgba(0, 240, 255, 0.18);
  pointer-events: auto;
}
#surfaceName {
  font-size: var(--fs-sm);
  letter-spacing: 2px;
  color: #00f0ff;
  text-shadow: 0 0 8px #00f0ff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;      /* let a long planet name give way, not the TAKEOFF button */
}
#surfaceHint {
  flex: 1 1 auto;
  font-size: var(--fs-2xs);
  letter-spacing: 1px;
  color: #ff6ac1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}


/* the interact button sits where DOCK does, so the thumb already knows it */
#siteBtn {
  position: absolute;
  left: 50%;
  bottom: calc(120px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 15;
  font: inherit;
  font-size: var(--fs-md);
  letter-spacing: 3px;
  padding: 12px 30px;
  color: #0d0221;
  background: #ffd319;
  border: 0;
  box-shadow: 4px 4px 0 #8b2fc9;
  cursor: pointer;
  pointer-events: auto;
}
#siteBtn:active { transform: translateX(-50%) translate(2px, 2px); box-shadow: 2px 2px 0 #8b2fc9; }

/* while landed you're still flying HER — full flight controls (fire, boost,
   weapons bar, speed, target) stay live; only the space-traffic controls
   (dock/hail/jump/salvage/zoom) have nothing to act on down here */
#hud.landed #dockBtn,
#hud.landed #hailBtn,
#hud.landed #jumpBtn,
#hud.landed #interactBtn,
#hud.landed #zoomInBtn,
#hud.landed #zoomOutBtn { display: none !important; }

/* the surface bar owns top-centre down here — the target readout slides
   underneath it instead of showing through its glass */
#hud.landed #targetLabel {
  top: calc(46px + env(safe-area-inset-top, 0px));
}

/* build stamp — so you can tell at a glance which build is actually live */
.buildstamp {
  font-size: var(--fs-2xs) !important;
  letter-spacing: 1px;
  color: rgba(45, 226, 230, 0.5) !important;
  margin-top: 2px;
}

/* a world you can't set down on — the button states the fact, greyed out */
#dockBtn.nolanding {
  background: rgba(90, 85, 104, 0.55);
  border-color: rgba(200, 200, 200, 0.35);
  color: #c8c8c8;
  box-shadow: none;
  font-size: var(--fs-sm);
  letter-spacing: 2px;
}


/* ---------- market menu board ---------- */
/* trade posts widen so the market reads like a menu, not a receipt */
#dockBody.wide { width: 100%; max-width: 1800px; }
/* the dock's own gutters are the overlay padding (12px); the split bench
   takes everything between them */

.mktmenu {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px 24px;
  align-items: start;
  margin-bottom: 8px;
}
.mktcol {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
/* narrow market columns: the buttons keep their size, the label gives way */
.mktcol .drow { gap: 6px; padding: 7px 8px; }
.mktcol .dbtn { min-width: 46px; padding: 7px 9px; }
.mktcol .dprice { flex: 0 0 auto; }
.mkthead {
  color: #ff6ac1;
  font-size: var(--fs-md);
  font-weight: bold;
  letter-spacing: 2px;
  text-align: left;
  padding: 4px 2px 3px;
  border-bottom: 1px dashed #3d1a6e;
}
.mkthead b { color: #ffd319; }

/* phones: the three columns become swipe-pannable pages with snap */
@media (max-width: 700px) {
  .mktmenu {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 88%;
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 4px;
  }
  .mktcol { scroll-snap-align: center; }
  /* swipe affordance */
  .mktmenu::after {
    content: '';
    display: block;
    width: 8px;
    flex: 0 0 8px;
  }
}

/* TAKE OFF: same look, same spot as LAND — your thumb already lives there */
#takeoffBtn {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(56px + env(safe-area-inset-bottom, 0px));
  z-index: 16;
  pointer-events: auto;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-md);
  letter-spacing: 2px;
  color: #0d0221;
  background: #00f0ff;
  border: none;
  padding: 12px 22px;
  box-shadow: 0 0 12px rgba(0, 240, 255, 0.8), 3px 3px 0 #8b2fc9;
}

/* ---- marker side panel on the galaxy map ---- */
/* §MAP2 deleted #gmapCols and #mapSplit. The chart no longer shares a row
   with anything, so there is no row. */
/* Item 8: the marker panel becomes a panel LIKE THE MAP KEY — same language,
   same behaviour, opened by a button in its own corner. Matt: "I *still* do
   not like the map markers panel." It was a column permanently eating a third
   of the chart to show a list that is usually empty. */
#markerPanel {
  order: -1;                     /* above its button in the column */
  width: min(320px, calc(var(--menuw, 100vw) - 40px));
  max-height: calc(var(--menuh, 100vh) - 120px);
  overflow-y: auto;
  overscroll-behavior: contain;
  text-align: left;
  border: 1px solid #8b2fc9;
  background: rgba(13, 2, 33, 0.93);
  box-shadow: 0 0 12px rgba(139, 47, 201, 0.4);
  padding: 8px 10px 10px;
  box-sizing: border-box;
}
#markerPanelTitle {
  color: #ff6c11;
  font-size: var(--fs-md);
  letter-spacing: 2px;
  margin-bottom: 6px;
}
#markerList .mkempty {
  color: rgba(232, 227, 255, 0.55);
  font-size: var(--fs-md);
  line-height: 1.5;
}
.mkrow {
  border: 1px solid rgba(61, 26, 110, 0.9);
  padding: 6px 8px;
  margin-bottom: 6px;
  cursor: pointer;
}
.mkrow.sel { border-color: #ff6c11; background: rgba(255, 108, 17, 0.08); }
.mktop { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
.mkname { color: #ffd319; font-size: var(--fs-md); letter-spacing: 1px; word-break: break-word; }
.mkdist { color: #2de2e6; font-size: var(--fs-md); white-space: nowrap; }
.mkbtns { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.mkbtns button {
  font: inherit;
  font-size: var(--fs-sm);
  letter-spacing: 1px;
  padding: 4px 8px;
  background: #1c0640;
  color: #00f0ff;
  border: 1px solid rgba(0, 240, 255, 0.5);
  cursor: pointer;
}
.mkbtns button:active { transform: translate(1px, 1px); }
.mkbtns button.danger { color: #ff3864; border-color: rgba(255, 56, 100, 0.5); }
.mkname-edit {
  font: inherit;
  font-size: var(--fs-md);
  width: 100%;
  background: #0d0221;
  color: #ffd319;
  border: 1px solid #ffd319;
  padding: 2px 4px;
  box-sizing: border-box;
}
.mknote {
  display: block;
  width: 100%;
  min-height: 52px;
  margin-top: 6px;
  font: inherit;
  font-size: var(--fs-md);
  line-height: 1.4;
  background: #0d0221;
  color: #e8e3ff;
  border: 1px solid rgba(45, 226, 230, 0.4);
  padding: 4px 6px;
  box-sizing: border-box;
  resize: vertical;
}
/* Small screens generally: the controls tuck in, and the panels stop being
   able to cover the whole chart. */
@media (max-width: 700px), (max-height: 620px) {
  #gmap #mapInfoBtn, #gmap #closeMapBtn {
    width: 38px; height: 38px; font-size: var(--fs-xl);
    top: calc(6px + env(safe-area-inset-top, 0px));
  }
  #gmap #closeMapBtn { right: 8px; }
  #gmap #mapInfoBtn { left: 8px; }
  #gmap #mapTips {
    top: calc(50px + env(safe-area-inset-top, 0px));
    left: 8px; right: 8px; width: auto; max-width: none;
    font-size: var(--fs-sm); line-height: 1.6;
  }
  #gmap .mapctl { padding: 8px 10px; font-size: var(--fs-md); gap: 5px; }
  #gmap #mapZoomRocker button { min-width: 40px; padding: 5px 8px; font-size: var(--fs-xl); }
  #gmap #mapFrameRow, #gmap #mapKeyCluster, #gmap #mapMarkCluster {
    bottom: calc(8px + env(safe-area-inset-bottom, 0px));
  }
  #gmap #mapKeyCluster { left: 8px; }
  #gmap #mapMarkCluster { right: 8px; }
  #gmap #mapKey { max-width: min(280px, calc(var(--menuw, 100vw) - 32px)); }
  #gmap #markerPanel {
    width: min(300px, calc(var(--menuw, 100vw) - 32px));
    max-height: calc(var(--menuh, 100vh) - 108px);
    padding: 6px;
  }
  #gmap #markerPanelTitle { font-size: var(--fs-sm); margin-bottom: 4px; }
  #gmap #markerActions { gap: 4px; margin-bottom: 5px; }
  #gmap #markerActions button { font-size: var(--fs-sm); padding: 6px 8px; }
  #gmap #apMenu { max-height: 30vh; }
  #gmap #apMenu .aprow { font-size: var(--fs-sm); padding: 6px; }
  #gmap .mkname { font-size: var(--fs-sm); }
  #gmap .mkdist { font-size: var(--fs-sm); }
  #gmap .mkbtns button { font-size: var(--fs-xs); padding: 4px 6px; }
}

/* ---------- §MAP2: THE NARROW ADAPTATION (item 14) ----------
   Mocked at four sizes before building. 1920x1080, 1280x800 and 844x390 all
   held; 390x844 did not — the bottom edge wanted ~404px of controls in 390px
   of screen. Matt called it in the spec: "four corners of buttons plus a
   bottom-centre cluster is a lot to ask of a portrait phone, and that is the
   size most likely to force a change." It was.

   Matt's decision: "definitely need to use the adapted layout when needed."
   So, below a width breakpoint and NOWHERE ELSE:
     - MY LOCATION becomes its crosshair alone. The same crosshair marks the
       ship on the chart, so the icon is already doing that job.
     - The bottom-left pair stacks, MAP KEY above DANGER.
   No control is removed and nothing changes corner.

   This is a breakpoint on the MAP, not a device test — a narrow desktop
   window behaves the same way, and the layout cannot drift out of sync with
   the width it is actually laying out in. */
@media (max-width: 560px) {
  #gmap #mapCenterBtn { padding: 9px 11px; gap: 0; }
  #gmap #mapCenterBtn .lbl { display: none; }
  /* stacked, so the corner keeps its footprint and the row keeps its labels */
  #gmap #mapKeyRow { flex-direction: column; align-items: flex-start; }
  /* and the mode button drops the world's name, which the chart is already
     showing — "▼ SURFACE: VORN-29 MINOR" is most of a phone's width on its own */
  #gmap .mapctl .lbl { display: none; }
  #gmap .mapctl .lblshort { display: inline; }

  /* THE CENTRE CLUSTER LIFTS ABOVE THE CORNERS.
     Measured rather than assumed: at 390 the bottom edge has 107px of corner
     on the left and 107px on the right, leaving 160px in the middle for a
     112px rocker, a crosshair and — in surface mode — a mode button. It does
     not fit, and at 320 it does not fit even without the mode button. The
     mockup found this at 390x844 and Matt's call was "definitely need to use
     the adapted layout when needed."
     So it takes its own line. Every control keeps its corner; the middle one
     simply sits a row higher, which is the smallest change that makes the
     edge hold. Wrapping was the accident that did this badly: an absolutely
     positioned flex container that wraps takes its MAX-WIDTH, so the row's
     box spread across the whole screen and overlapped both corners even
     though the buttons inside it did not. */
  #gmap #mapFrameRow {
    bottom: calc(96px + env(safe-area-inset-bottom, 0px));
    flex-wrap: nowrap;
  }
}

/* marker panel action row — the position tools live WITH the markers now */
#markerActions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}
#markerActions button {
  font: inherit;
  font-size: var(--fs-sm);
  letter-spacing: 1px;
  padding: 5px 8px;
  background: #1c0640;
  color: #ffd319;
  border: 1px solid rgba(255, 211, 25, 0.5);
  cursor: pointer;
}
#markerActions button:active { transform: translate(1px, 1px); }
#markerActions button.danger { color: #ff3864; border-color: rgba(255, 56, 100, 0.5); }

/* ▶ AUTOPILOT destination menu — waypoint, markers, nearest yard/tradepost */
/* ---------- the destination list, and the fact that it scrolls ----------
   Matt: "I knew there were more options so for me it was obvious but for a new
   player they might not realize the list can be scrolled."

   Three cues, because one is not enough. A styled scrollbar says a scrollbar
   exists (the OS one is invisible on a phone and near-invisible on a dark
   panel); a fade at the cut edge says the content continues past it; and a
   ▼ MORE tab says it in words for anyone who reads neither. All three are
   driven by measurement in updateApScrollCue() and all three disappear the
   moment you reach the bottom, so none of them ever promises a row that is
   not there. */
/* §COURSE-3: the wrap is the menu's own width, centred -- the course row above
   it made the column's content box wider than the menu, and the menu sat at
   its left edge */
#apMenuWrap { position: relative; margin-bottom: 8px; width: 100%; max-width: min(560px, 100%); align-self: center; }

#apMenu {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 40vh;
  overflow-y: auto;
  border: 1px solid rgba(255, 106, 193, 0.5);
  background: rgba(28, 6, 64, 0.9);
  padding: 6px;
  /* always reserve the gutter, so rows do not reflow the instant a marker
     tips the list over the cap */
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: #ff6ac1 rgba(13, 2, 33, 0.6);
}
/* WebKit/Blink wants its own, and the phone is the case that matters most --
   a touch device draws no scrollbar at all unless one is asked for. */
#apMenu::-webkit-scrollbar { width: 10px; }
#apMenu::-webkit-scrollbar-track {
  background: rgba(13, 2, 33, 0.6);
  border-left: 1px solid rgba(255, 106, 193, 0.25);
}
#apMenu::-webkit-scrollbar-thumb {
  background: #ff6ac1;
  border: 2px solid rgba(28, 6, 64, 0.9);
  min-height: 28px;
}
#apMenu::-webkit-scrollbar-thumb:active { background: #ffd319; }

/* the fades: only on the edge that actually has more past it */
#apMenuWrap::before,
#apMenuWrap::after {
  content: '';
  position: absolute;
  left: 1px; right: 11px;          /* clear of the border and the gutter */
  height: 22px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .12s linear;
}
#apMenuWrap::before {
  top: 1px;
  background: linear-gradient(rgba(28, 6, 64, 0.95), rgba(28, 6, 64, 0));
}
#apMenuWrap::after {
  bottom: 1px;
  background: linear-gradient(rgba(28, 6, 64, 0), rgba(28, 6, 64, 0.95));
}
#apMenuWrap:has(#apMenu.scrolls:not(.at-top))::before { opacity: 1; }
#apMenuWrap:has(#apMenu.scrolls:not(.at-end))::after  { opacity: 1; }

/* and the same thing in words, for anyone who reads neither */
#apMore {
  position: absolute;
  left: 50%;
  bottom: -1px;
  transform: translateX(-50%);
  padding: 1px 10px 2px;
  font-family: inherit;
  font-size: var(--fs-2xs);
  font-weight: bold;
  letter-spacing: 2px;
  color: #0d0221;
  background: #ff6ac1;
  pointer-events: none;
}
#apMenu .aprow {
  font: inherit;
  font-size: var(--fs-sm);
  letter-spacing: 1px;
  text-align: left;
  padding: 6px 8px;
  /* The type scale made every size responsive, and this row -- a long station
     name plus a right-floated distance, with letter-spacing on top -- was the
     one place that then pushed the PAGE sideways on a narrow phone. It has to
     be allowed to shrink and to break, rather than the type being shrunk back
     to fit it. */
  min-width: 0;
  overflow-wrap: anywhere;
  background: #1c0640;
  color: #ff6ac1;
  border: 1px solid rgba(255, 106, 193, 0.45);
  cursor: pointer;
}
#apMenu .aprow span { color: #2de2e6; float: right; margin-left: 6px; }
#apMenu .aprow em { font-style: normal; color: #ffd319; }
#apMenu .aptoggle {
  color: #7a6fa8;
  border-color: rgba(122, 111, 168, 0.5);
  margin-bottom: 2px;
}
#apMenu .aptoggle.on { color: #ffd319; border-color: rgba(255, 211, 25, 0.6); }
#apMenu .aptoggle span { color: #7a6fa8; }
#apMenu .aprow:active { transform: translate(1px, 1px); }
/* work-order destinations: cooking in blue, ready-to-collect in green */
#apMenu .aprow.job { color: #8fa8ff; border-color: rgba(143, 168, 255, 0.45); }
#apMenu .aprow.job.ready { color: #4fdc6b; border-color: rgba(79, 220, 107, 0.55); }
#apMenu .aprow.job span { color: inherit; opacity: .8; }
/* section headers inside the destination menu */
#apMenu .aphead {
  font-size: var(--fs-xs);
  letter-spacing: .18em;
  color: #7a6fa8;
  padding: 7px 2px 3px;
  border-bottom: 1px solid rgba(122, 111, 168, 0.25);
  margin-top: 3px;
}
#apMenu .aphead:first-child { margin-top: 0; }
#apMenu .aprow.mrow { color: #ffd319; border-color: rgba(255, 211, 25, 0.5); }
/* Every destination wears the colour its thing wears ON THE CHART. A menu
   that recolours everything to match itself makes you learn the same object
   twice: once as an orange diamond on the map, once as a pink line in a
   list. These are the map's own numbers, not near-misses. */
#apMenu .aprow.yard { color: #ff6c11; border-color: rgba(255, 108, 17, 0.55); }
#apMenu .aprow.post { color: #2de2e6; border-color: rgba(45, 226, 230, 0.55); }
#apMenu .aprow.wp   { color: #39ff14; border-color: rgba(57, 255, 20, 0.55); }
#apMenu .aprow.wp span { color: #39ff14; opacity: .75; }

/* autopilot disengage — bottom centre while the computer flies */
/* THE ENGAGED STATE SITS WHERE THE BUTTON THAT ENGAGED IT SITS.
   Matt, 2026-09-05: "can the pink autopilot banner be moved to where the
   autopilot button is when active so its out of the way? then when someone
   wants to change course its still intuitive because it is in the same
   place." It was a wide pink slab across the middle of the screen, directly
   over the ship — the one part of the frame the player is actually looking at.

   So #apBtn now lives inside #hudApRow and REPLACES #apOpenBtn while
   autopilot is running: one slot, two states, engage and cancel in the same
   spot. It is pinned to the row's BOTTOM edge and grows upward, so a
   two-line destination never pushes down into the radar. */
/* `#hudApRow > button` sets a flat height:30px for the row's controls, and it
   outranks a bare #apBtn — so the destination line was being clipped by
   overflow:hidden with no sign anything was wrong. Selector matches that
   specificity and then some. */
#hudApRow > #apBtn {
  position: absolute;
  left: calc(50% + 2px);   /* §COURSE: DISENGAGE takes ENGAGE's half; COURSE stays */
  right: 0;
  bottom: 0;
  height: auto;
  min-height: 30px;
  z-index: 15;
  pointer-events: auto;
  font: inherit;
  font-weight: bold;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
  line-height: 1.15;
  padding: 3px 6px;
  color: #0d0221;
  background: #ff6ac1;
  border: none;
  box-shadow: 0 0 12px rgba(255, 106, 193, 0.7), 3px 3px 0 #8b2fc9;
  cursor: pointer;
  overflow: hidden;
}
/* the destination line: smaller, and it truncates rather than growing the
   slab a third line tall on a long yard name */
#hudApRow > #apBtn small {
  display: block;
  font-size: 0.82em;
  letter-spacing: 0;
  opacity: 0.85;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@keyframes appulse {
  0%, 100% { box-shadow: 0 0 8px rgba(255, 106, 193, 0.5), 3px 3px 0 #8b2fc9; }
  50% { box-shadow: 0 0 18px rgba(255, 106, 193, 0.95), 3px 3px 0 #8b2fc9; }
}
#hudApRow > #apBtn:active { transform: translate(1px, 1px); }


/* ---------- short screens: landscape phones ----------
   Under ~480px of height nothing can afford its full-size layout. Menus
   compact so they still fit without scrolling where possible, and the HUD
   rearranges around the radar instead of underneath it. */
@media (max-height: 480px) {
  .overlay { justify-content: flex-start; gap: 7px; padding: 10px 12px; }
  .overlay h1 { font-size: clamp(24px, 7vw, 40px); }
  .overlay h1.titleTwoLine { font-size: clamp(20px, 6vw, 34px); line-height: 1; }
  .overlay h2 { font-size: clamp(17px, 4vw, 26px); letter-spacing: 2px; }
  .overlay .sub { font-size: var(--fs-xs); letter-spacing: 2px; }
  .overlay .how { font-size: var(--fs-sm); line-height: 1.45; }
  button.big {
    font-size: var(--fs-sm); padding: 9px 18px; min-width: 172px; letter-spacing: 1px;
  }
  /* the title screen has the most buttons — lay them out in two columns */
  #title { justify-content: center; }
  #title button.big { min-width: 150px; }
  .hopt { padding: 7px 10px; }
  .hopt .hname { font-size: var(--fs-sm); }
  .hopt .hdesc { font-size: var(--fs-xs); line-height: 1.35; }
  #hail .how { margin-bottom: 2px; }
  #tutBody { font-size: var(--fs-sm); }
  #tutHideRow { font-size: var(--fs-xs); }
}

/* ---------- the HUD lays out around the radar ----------
   The radar is painted on the canvas, so it is invisible to CSS unless the
   game publishes its box. On a short screen the pause/map/inv column turns
   into a row beside the radar, and the zoom pair drops to the bottom band
   with the other flight controls, so nothing sits on top of the scope. */
/* The cluster is anchored to the radar at every size now, so the short-screen
   layout has nothing left to rearrange — it just gets a smaller radar, and
   the rows follow it. Deleted rather than kept as a no-op: two layouts meant
   two sets of numbers to keep in step. */

body.compacthud #zoomInBtn,
body.compacthud #zoomOutBtn {
  right: 142px;
  width: 32px; height: 32px; font-size: var(--fs-lg);
}
body.compacthud #zoomInBtn  { bottom: calc(144px + env(safe-area-inset-bottom, 0px)); }
body.compacthud #zoomOutBtn { bottom: calc(106px + env(safe-area-inset-bottom, 0px)); }
body.compacthud #boostBtn {
  right: 116px;
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  width: 44px; height: 44px; font-size: var(--fs-lg);
}
body.compacthud #fireBtn { width: 68px; height: 68px; font-size: var(--fs-sm); }
body.compacthud #tgtBtn {
  right: 18px;
  bottom: calc(96px + env(safe-area-inset-bottom, 0px));
  width: 68px; height: 34px;
}
/* the waypoint readout tucks under the scope rather than through it */
body.compacthud #wpLabel { font-size: var(--fs-2xs); }


/* ---------- landscape phones: short, so the panels have to be shorter ----
   844x390 is the first size the mockup showed the panels touching, which is
   where item 15's stacking rule starts mattering. Height is the scarce thing
   here, not width, so the panels give up height and the corners stay put. */
@media (max-height: 620px) and (min-width: 701px) {
  #gmap #mapKey, #gmap #markerPanel, #gmap #mapTips {
    max-height: calc(var(--menuh, 100vh) - 96px);
  }
  #gmap #apMenu { max-height: none; gap: 3px; padding: 5px; }
  #gmap #apMenu .aprow { padding: 5px 7px; font-size: var(--fs-sm); }
  #gmap #apMenu .aphead { padding: 4px 2px 2px; font-size: var(--fs-xs); }
  #gmap #markerPanelTitle { font-size: var(--fs-xs); margin-bottom: 3px; }
  #gmap #markerActions { margin-bottom: 4px; }
  #gmap #markerActions button { font-size: var(--fs-xs); padding: 5px 7px; }
  #gmap .mkrow { padding: 4px 6px; margin-bottom: 4px; }
  #gmap .mapctl { padding: 7px 10px; font-size: var(--fs-md); }
  #gmap #mapZoomRocker button { padding: 5px 9px; font-size: var(--fs-xl); }
}


/* §MAP2 deleted the seam's stylesheet along with the seam. It was a real
   control -- drag toward the markers to grow the chart, toward the chart to
   grow the markers -- solving a problem that no longer exists, because the
   chart does not share the screen with anything.


*/

/* ---------- AUTOPILOT: its own control, and its own screen ----------
   Light orange so it reads as the one action button among view controls. */
#apOpenBtn {
  color: #ffb35c !important;
  border-color: #ffb35c !important;
  background: rgba(64, 34, 8, 0.75) !important;
  box-shadow: 0 0 10px rgba(255, 179, 92, 0.35);
  letter-spacing: 2px;
}
#apOpenBtn:active { transform: translate(1px, 1px); }

#apPopup { z-index: 30; gap: 8px; }
#apPopup h2 {
  color: #ffb35c;
  text-shadow: 0 0 10px rgba(255, 179, 92, .8), 2px 2px 0 #8b2fc9;
  font-size: clamp(20px, 5vw, 32px);
}
#apPopup .sub { color: #c9a06a; }
#apPopup #apMenu {
  width: 100%;
  max-width: min(560px, 100%);
  min-width: 0;
  /* content-box + max-width:100% is a lie: the border and padding are added
     ON TOP, so the menu came out 334px inside a 320px content box and the
     popup scrolled sideways by exactly that overhang. The type scale did not
     cause this -- it made it wide enough to notice. */
  box-sizing: border-box;
  max-height: 62vh;
  margin-bottom: 4px;
  border-color: rgba(255, 179, 92, 0.45);
}
#apPopup #apMenu .aprow { font-size: var(--fs-md); padding: 8px 10px; }
@media (max-height: 480px) {
  #apPopup h2 { font-size: var(--fs-xl); }
  #apPopup .sub { display: none; }
  #apPopup #apMenu { max-height: 58vh; }
  #apPopup #apMenu .aprow { font-size: var(--fs-sm); padding: 6px 8px; }
}

/* ---------- toast ----------------------------------------------------- */
/* Brief confirmations — "WAYPOINT SET TO …". It sits above every overlay
   (z 44, just under the tutorial banner) because the messages that matter
   most are fired from inside the yard, shop and map screens, where the HUD
   status line is buried. Never interactive: taps fall straight through. */
#toast {
  position: fixed;
  left: 50%;
  top: calc(58px + env(safe-area-inset-top, 0px));
  transform: translateX(-50%);
  width: max-content;
  max-width: min(520px, 88vw);
  padding: 11px 16px;
  background: rgba(13, 2, 33, .94);
  border: 1px solid #05d9e8;
  box-shadow: 0 0 18px rgba(5, 217, 232, .45), inset 0 0 22px rgba(5, 217, 232, .10);
  color: #05d9e8;
  font-size: var(--fs-sm);
  font-weight: bold;
  letter-spacing: 1.6px;
  line-height: 1.5;
  text-align: center;
  text-shadow: 0 0 7px rgba(5, 217, 232, .8);
  z-index: 44;
  pointer-events: none;
  animation: toastIn .18s ease-out;
}
#toast.out { animation: toastOut .3s ease-in forwards; }

/* STICKY toasts wait for the reader. A message long enough to need one — a
   refit report, a migration notice — is one you cannot re-open, so it holds
   the screen until dismissed. Only then does the toast take pointer events;
   an ordinary toast still lets taps fall straight through to the game. */
#toastClose { display: none; }
#toast.sticky { pointer-events: auto; max-width: min(560px, 92vw); cursor: pointer; }
#toast.sticky #toastClose {
  display: block;
  margin: 12px auto 0;
  padding: 7px 22px;
  background: rgba(5, 217, 232, .10);
  border: 1px solid currentColor;
  color: inherit;
  font: inherit;
  font-size: var(--fs-xs);
  letter-spacing: 2px;
  cursor: pointer;
  text-shadow: inherit;
}
#toast.sticky #toastClose:hover { background: rgba(5, 217, 232, .24); color: #fff; }
#toast.sticky.deny #toastClose { background: rgba(255, 56, 100, .12); }
#toast.sticky.deny #toastClose:hover { background: rgba(255, 56, 100, .26); color: #fff; }
#toast.deny {
  border-color: #ff3864;
  color: #ff3864;
  box-shadow: 0 0 18px rgba(255, 56, 100, .45), inset 0 0 22px rgba(255, 56, 100, .10);
  text-shadow: 0 0 7px rgba(255, 56, 100, .8);
}
/* the value being confirmed — the place, the thing — reads brighter */
#toast b { color: #fff; text-shadow: 0 0 8px currentColor; }
#toast.deny b { color: #ffd0da; }

@keyframes toastIn  { from { opacity: 0; transform: translate(-50%, -14px); }
                      to   { opacity: 1; transform: translate(-50%, 0); } }
@keyframes toastOut { from { opacity: 1; transform: translate(-50%, 0); }
                      to   { opacity: 0; transform: translate(-50%, -10px); } }

/* during training the banner owns the top edge — the toast steps below it */
.tutting #toast { top: calc(150px + env(safe-area-inset-top, 0px)); }

/* short landscape: the top edge is precious, so ride higher and tighter */
@media (max-height: 460px) {
  #toast { top: calc(8px + env(safe-area-inset-top, 0px)); padding: 8px 12px; font-size: var(--fs-xs); }
  .tutting #toast { top: calc(96px + env(safe-area-inset-top, 0px)); }
}

/* ---------- inventory board ------------------------------------------- */
/* The ship's sheet used to be one 560px column down the middle of a 1600px
   screen. It is a reference screen — everything on it wants to be visible at
   once — so the sections spread across the full width instead. */
#invBody.wide { width: 100%; max-width: 1500px; }

/* §INVRIGHT, 2026-09-27. Matt: "reduce the text size of the right column
   content in inventory. the left side looks good but the text on the right
   side is all much larger and the space is much less compact." The left is
   the yard's tile shelf (fs-2xs); the right was wearing the dock's full row
   sizes. One step down across the column, tighter rows, so the two halves
   read as one sheet. */
#invBody .mcol .invpanel { gap: 3px; }
#invBody .mcol .dsec { font-size: var(--fs-md); letter-spacing: 2px; margin-top: 6px; padding-bottom: 2px; }
#invBody .mcol .drow { gap: 6px; padding: 4px 8px; }
#invBody .mcol .drow .dname { font-size: var(--fs-sm); letter-spacing: .5px; line-height: 1.25; }
#invBody .mcol .drow .dname small { font-size: var(--fs-xs); line-height: 1.25; }
#invBody .mcol .drow .dprice { font-size: var(--fs-sm); }
#invBody .mcol .drow .dprice small { font-size: var(--fs-xs); letter-spacing: .5px; }
#invBody .mcol .drow.note .dname small { font-size: var(--fs-xs); }
#invBody .mcol .stbar { margin-top: 3px; height: 4px; }
#invBody .mcol .dbtn { padding: 4px 9px; font-size: var(--fs-xs); min-width: 0; }

.invgrid {
  display: grid;
  /* auto-FIT, not auto-fill: three panels divide the whole width rather than
     huddling at 300px each and leaving a dead track on the right */
  /* min() keeps a narrow phone to one column instead of forcing a 300px
     track wider than the screen and scrolling the page sideways */
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: 4px 22px;
  align-items: start;
  width: 100%;
}
.invpanel {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.invpanel .dsec { margin-top: 4px; }
.invpanel .drow { gap: 8px; padding: 7px 9px; }
.invpanel .dprice { flex: 0 0 auto; }

/* a standing row's tier and its number were running together (EXCELLENT937) */
.drow .dprice small {
  display: block;
  font-weight: normal;
  font-size: var(--fs-sm);
  color: #b9a7e8;
  letter-spacing: 1px;
}

/* The hold is the long list, so it takes the full width underneath and packs
   itself into as many columns as fit. Rows never wrap their buttons onto a
   second line — a DROP control that moves is a control you misfire. */
.invpanel.cargo {
  grid-column: 1 / -1;
  display: grid;
  /* wide enough that "FERRITE × 179  ~107 CR  DROP 1  ALL" never truncates,
     but never wider than the screen it is on */
  grid-template-columns: repeat(auto-fill, minmax(min(400px, 100%), 1fr));
  gap: 6px 22px;
  align-content: start;
}
.invpanel.cargo .dsec { grid-column: 1 / -1; }
.invpanel.cargo .drow {
  flex-wrap: nowrap;
  gap: 7px;
}
.invpanel.cargo .dname {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.invpanel.cargo .dbtn { min-width: 0; padding: 7px 9px; }
/* the closing note about scooping cargo back up spans the lot */
.invpanel.cargo .drow.note { grid-column: 1 / -1; }
.invpanel.cargo .drow.note .dname { white-space: normal; }

/* ---------- quantity picker ------------------------------------------- */
/* − [n] + MAX. Every counter that sells or makes something uses it, so it
   has to survive a three-column market on a phone as well as a desk. */
.qtyctl {
  display: flex;
  align-items: stretch;
  flex: 0 0 auto;
  gap: 0;
  border: 1px solid #3d1a6e;
  background: rgba(13, 2, 33, .6);
}
.qbtn {
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-md);
  line-height: 1;
  color: #05d9e8;
  background: #241734;
  border: none;
  padding: 7px 9px;
  min-width: 28px;
  cursor: pointer;
}
.qbtn.max {
  font-size: var(--fs-sm);
  letter-spacing: 1px;
  color: #ffd319;
  min-width: 38px;
}
.qbtn:active { background: #3d1a6e; }
.qnum {
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-md);
  color: #f6f7f6;
  background: transparent;
  border: none;
  border-left: 1px solid #3d1a6e;
  border-right: 1px solid #3d1a6e;
  width: 46px;
  min-width: 0;
  padding: 6px 2px;
  text-align: center;
  -moz-appearance: textfield;
}
.qnum:focus { outline: none; background: rgba(5, 217, 232, .12); color: #05d9e8; }

/* the running total sits above its own per-unit note */
.dprice .costline small,
.dprice .feeline small,
.drow .dprice > span > small {
  display: block;
  font-weight: normal;
  font-size: var(--fs-sm);
  color: #b9a7e8;
  letter-spacing: 0;
  white-space: nowrap;
}

/* narrow market / factory columns: shrink the picker, never the buttons */
.mktcol .qbtn { padding: 6px 7px; min-width: 24px; font-size: var(--fs-md); }
.mktcol .qbtn.max { min-width: 34px; font-size: var(--fs-xs); }
.mktcol .qnum { width: 38px; font-size: var(--fs-md); }

/* a refused purchase says which limit bit, in place of the per-unit note */
/* It WRAPS, unlike the per-unit notes above it. Those are short and fixed
   ("142 CR each"); this one is a sentence naming a hull and a number, and it
   inherited `white-space: nowrap` from the rule above -- 470px of unbreakable
   text in a 350px column, which made the whole claim screen scroll sideways
   on every phone. Found by the layout audit once PROGRESSION step 7 put a
   2-cargo fighter in the test berth, so the refusal actually fired; the CSS
   was wrong the whole time and nothing had been long enough to show it. */
.dprice small.why {
  color: #ff3864;
  font-weight: bold;
  letter-spacing: 1px;
  text-shadow: 0 0 5px rgba(255, 56, 100, .6);
  white-space: normal;
  overflow-wrap: anywhere;
}

/* ---------- claims: ground of your own -------------------------------- */
/* Sits above SITE/TAKE OFF so it never covers the way home. Violet, because
   it is neither a trade action (cyan) nor a fight (pink) — it is yours. */
#claimBtn {
  position: absolute;
  left: 50%;
  bottom: calc(176px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 15;
  font: inherit;
  font-weight: bold;
  font-size: var(--fs-sm);
  letter-spacing: 2px;
  padding: 11px 24px;
  color: #0d0221;
  background: #b06cff;
  border: 0;
  box-shadow: 0 0 12px rgba(176, 108, 255, .7), 3px 3px 0 #4a1d7a;
  cursor: pointer;
  pointer-events: auto;
  white-space: nowrap;
  max-width: 88vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
#claimBtn:active { transform: translateX(-50%) translate(2px, 2px); box-shadow: 1px 1px 0 #4a1d7a; }
@keyframes claimping {
  0%, 100% { box-shadow: 0 0 12px rgba(176, 108, 255, .7), 3px 3px 0 #4a1d7a; }
  50%      { box-shadow: 0 0 22px rgba(255, 56, 100, .95), 3px 3px 0 #4a1d7a; }
}
/* short landscape has no room for a third stacked button — tuck it up top */
@media (max-height: 460px) {
  #claimBtn {
    bottom: auto;
    top: calc(8px + env(safe-area-inset-top, 0px));
    left: auto; right: 50%;
    transform: translateX(50%);
    font-size: var(--fs-xs); padding: 8px 14px;
  }
  #claimBtn:active { transform: translateX(50%) translate(2px, 2px); }
}

#claimConfirm h2 { color: #b06cff; text-shadow: 0 0 10px #b06cff, 2px 2px 0 #4a1d7a; }
#ccBody {
  width: 100%;
  max-width: 620px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 52vh;
  overflow-y: auto;
  text-align: left;
}
#ccBody .ccrule {
  background: rgba(36, 23, 52, .9);
  border: 1px solid #3d1a6e;
  padding: 8px 12px;
  font-size: var(--fs-md);
  color: #e8e3ff;
  letter-spacing: 1px;
}
#ccBody .ccrule.bad { border-color: #ff3864; color: #ff3864; font-weight: bold; }
#ccBody .cctype {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  background: rgba(36, 23, 52, .7);
  border: 1px solid #3d1a6e;
  padding: 6px 10px;
}
#ccBody .cctype b { color: #b06cff; font-size: var(--fs-md); letter-spacing: 2px; }
#ccBody .cctype small { color: #ff6ac1; font-size: var(--fs-sm); }

/* §E1: the cart -- one receipt for every purchase */
#cart { z-index: 30; }
#cart h2 { color: #2de2e6; text-shadow: 0 0 10px #2de2e6, 2px 2px 0 #0d5d61; }
#cartBody {
  width: 100%;
  max-width: 620px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 46vh;
  overflow-y: auto;
  text-align: left;
}
#cartBody .crline {
  background: rgba(10, 40, 42, .9);
  border: 1px solid #0f6b70;
  padding: 8px 12px;
  font-size: var(--fs-md);
  color: #dffcff;
  letter-spacing: 1px;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
#cartBody .crline small { color: #7fe8ee; font-size: var(--fs-sm); display: block; }
/* the REFURB bill: what the bench buys in for you reads warmer than what
   comes out of your own hold, so the sourcing lines stand out at a glance */
#cartBody .crline.buy { border-color: #b8651a; background: rgba(48, 26, 8, .9); }
#cartBody .crline.buy b { color: #ffb000; }
#cartBody .crline.own b { color: #7fe8ee; }
#cartBody .crline.short { border-color: #7a1f33; background: rgba(48, 8, 18, .9); }
#cartBody .crline.short small { color: #ff3864; }
#cartTotal { font-size: var(--fs-md); color: #2de2e6; font-weight: bold; }
#cartExtra { min-width: 0; }

/* the merchant's recipe block: every material, have / need */
.mdrecipe { margin-top: 8px; padding-top: 6px; border-top: 1px dashed #3d1a6e; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; font-size: var(--fs-xs); letter-spacing: 1px; }
.mdrecipe .mdrl { color: #7a6fa8; font-weight: bold; }
.mdrecipe .mdr { color: #dffcff; }
.mdrecipe .mdr a { color: inherit; text-decoration: underline dotted; text-underline-offset: 2px; }
.mdrecipe .mdr b { color: #39ff14; }
.mdrecipe .mdr.short b { color: #ff3864; }
.mdrecipe .mdr.pinned { color: #ffd319; }

/* ---------- pinned recipes on the HUD ---------- */
#pinPanel {
  position: absolute;
  left: 12px;
  top: calc(var(--labelY) + 74px);
  width: 188px;
  max-height: 46vh;
  overflow-y: auto;
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: var(--fs-2xs);
  letter-spacing: 1px;
  z-index: 4;
}
#pinPanel .pin {
  background: rgba(13, 2, 33, .72);
  border: 1px solid #3d1a6e;
  border-left: 3px solid #ffd319;
  padding: 4px 6px 5px;
}
#pinPanel .pin.ready { border-left-color: #39ff14; }
#pinPanel .pinhead { display: flex; justify-content: space-between; align-items: center; gap: 6px; color: #ffd319; font-weight: bold; }
#pinPanel .pin.ready .pinhead { color: #39ff14; }
#pinPanel .pinhead i { font-style: normal; color: #7a6fa8; font-weight: normal; }
#pinPanel .pinx { background: none; border: 0; color: #7a6fa8; font-family: inherit; font-size: var(--fs-md); line-height: 1; padding: 0 2px; cursor: pointer; }
#pinPanel .pinx:hover { color: #ff3864; }
#pinPanel .pinrow { display: flex; justify-content: space-between; color: #c9bde6; padding-top: 2px; }
#pinPanel .pinrow b { color: #ff3864; font-variant-numeric: tabular-nums; }
#pinPanel .pinrow.ok b { color: #39ff14; }
#pinPanel .pinfly { margin-top: 5px; width: 100%; background: rgba(57, 255, 20, .14); border: 1px solid #39ff14; color: #39ff14; font-family: inherit; font-size: var(--fs-2xs); font-weight: bold; letter-spacing: 1px; padding: 4px 6px; cursor: pointer; }
body.compacthud #pinPanel { top: calc(var(--labelY) + 62px); width: 150px; }
#hud.landed #pinPanel { top: calc(var(--labelY) + 74px); }
#cartTotal .why { color: #ff3864; display: block; }

/* the build-progress bar on a claim's stage row */
.clbar {
  position: relative;
  height: 7px;
  background: #241734;
  border: 1px solid #3d1a6e;
  margin-top: 5px;
}
.clbar i {
  display: block; height: 100%;
  background: #b06cff;
  box-shadow: 0 0 8px rgba(176, 108, 255, .8);
}
.clstage { color: #b06cff; font-weight: bold; letter-spacing: 2px; }
.cllog { color: #ff3864; font-size: var(--fs-sm); letter-spacing: 1px; }

/* ---------- paired title-screen toggles -------------------------------- */
/* SOUND and MUSIC are one decision in two halves, so they share a row and
   together occupy exactly the width of a single stacked button above them. */
.btnrow {
  display: flex;
  gap: 10px;
  width: 100%;
  max-width: 278px;      /* .big is 220px + its 28px side padding, x2 */
  justify-content: center;
}
button.big.half {
  flex: 1 1 0;
  min-width: 0;
  padding: 14px 8px;
  font-size: var(--fs-sm);
  letter-spacing: 1.5px;
  white-space: nowrap;
}
/* an OFF toggle reads as off, without shouting about it */
button.big.half.off {
  color: #7a6fa8;
  border-color: #3d1a6e;
  background: #1a1028;
}
@media (max-height: 460px) {
  button.big.half { padding: 10px 8px; font-size: var(--fs-sm); }
}

/* §14: a target that is dead in the water is not a threat any more, and the
   readout should stop shouting at you about it. */
#targetLabel.hulk { color: #9a92b5; text-shadow: 0 0 6px rgba(154, 146, 181, 0.6); }
/* §VARIANTS: what beats the build, under the target line */
#targetLabel .tgtBeat { display: block; font-size: var(--fs-2xs); letter-spacing: 1px; color: #ffd27a; text-shadow: 0 0 5px rgba(255, 170, 60, 0.5); overflow: hidden; text-overflow: ellipsis; }
#targetLabel .tgtWhy { color: #ff7a9c; text-shadow: 0 0 5px rgba(255, 41, 117, 0.5); white-space: normal; }
/* §SCAN: the fleet score, red when it outguns you; and what the scan turned up */
#targetLabel .tgtScan { color: #8fe8ff; text-shadow: 0 0 5px rgba(0, 240, 255, 0.45); }
#targetLabel .tgtScan.tgtOver { color: #ff3b5c; text-shadow: 0 0 6px rgba(255, 30, 70, 0.6); font-weight: bold; }
#targetLabel .tgtWeak { color: #b9f5c8; text-shadow: 0 0 5px rgba(80, 255, 140, 0.4); white-space: normal; }
.claimSiege .dsec { color: #ff3b5c; }

/* ---------- the sound panel ----------
   Opened from the title screen and from PAUSE, so it is styled once and
   looks the same from both. A row is: the switch, the dial, the number —
   in that order, because the switch decides whether the dial means
   anything and a disabled dial should read as disabled. */
/* Nearly opaque, unlike the other overlays. This one opens over the TITLE
   screen, which is the brightest thing in the game — at the usual 0.82 the
   menu behind it read straight through the sliders. */
#soundOpts { z-index: 32; gap: 6px; background: #0d0221; }
#soundOpts h2 { color: #2de2e6; text-shadow: 0 0 10px rgba(45, 226, 230, .8), 2px 2px 0 #8b2fc9; }
#soundBody {
  width: 100%;
  max-width: 460px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 6px;
}
#soundOpts .sorow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 4px 2px;
}
#soundOpts .sotog {
  font: inherit;
  font-size: var(--fs-sm);
  letter-spacing: 1px;
  flex: 0 0 108px;
  padding: 7px 4px;
  color: #39ff14;
  background: rgba(13, 2, 33, 0.75);
  border: 2px solid #39ff14;
  box-shadow: 0 0 8px rgba(57, 255, 20, 0.3);
  cursor: pointer;
}
#soundOpts .sotog.off {
  color: #7a6fa8;
  border-color: rgba(122, 111, 168, 0.6);
  box-shadow: none;
}
#soundOpts .sopct {
  flex: 0 0 46px;
  text-align: right;
  font-size: var(--fs-sm);
  color: #2de2e6;
  letter-spacing: 1px;
}
#soundOpts .sohint {
  margin: 0 0 2px 4px;
  font-size: var(--fs-xs);
  color: #7a6fa8;
  letter-spacing: 1px;
  text-align: left;
}
/* The track and the handle are drawn by hand: a browser's default range
   control is a grey pill from another operating system, and it is the one
   thing on this screen that would look borrowed. */
#soundOpts .soslider {
  flex: 1 1 auto;
  min-width: 0;
  -webkit-appearance: none;
  appearance: none;
  height: 20px;
  background: transparent;
  cursor: pointer;
}
#soundOpts .soslider:disabled { opacity: .35; cursor: default; }
#soundOpts .soslider::-webkit-slider-runnable-track {
  height: 6px;
  background: linear-gradient(to right, #8b2fc9, #2de2e6);
  border: 1px solid #3d1a6e;
}
#soundOpts .soslider::-moz-range-track {
  height: 6px;
  background: linear-gradient(to right, #8b2fc9, #2de2e6);
  border: 1px solid #3d1a6e;
}
#soundOpts .soslider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 18px;
  margin-top: -7px;
  background: #0d0221;
  border: 2px solid #2de2e6;
  box-shadow: 0 0 8px rgba(45, 226, 230, 0.6);
}
#soundOpts .soslider::-moz-range-thumb {
  width: 14px;
  height: 18px;
  border-radius: 0;
  background: #0d0221;
  border: 2px solid #2de2e6;
  box-shadow: 0 0 8px rgba(45, 226, 230, 0.6);
}
@media (max-width: 560px) {
  #soundOpts .sotog { flex: 0 0 92px; font-size: var(--fs-xs); }
  #soundOpts .sopct { flex: 0 0 40px; font-size: var(--fs-xs); }
}
#soundOpts .sonow { justify-content: center; gap: 14px; padding-top: 2px; }
#soundOpts #musNow {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  font-size: var(--fs-sm);
  letter-spacing: 2px;
  color: #ffd319;
  text-shadow: 0 0 8px rgba(255, 211, 25, 0.5);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#soundOpts #musNow.dim { color: #7a6fa8; text-shadow: none; letter-spacing: 1px; }
#soundOpts .soskip {
  font: inherit;
  font-size: var(--fs-sm);
  flex: 0 0 42px;
  padding: 6px 0;
  color: #ffd319;
  background: rgba(13, 2, 33, 0.75);
  border: 2px solid rgba(255, 211, 25, 0.6);
  cursor: pointer;
}
#soundOpts .soskip:disabled { color: #7a6fa8; border-color: rgba(122,111,168,.4); cursor: default; }
#soundOpts .soskip:active:not(:disabled) { transform: translate(1px, 1px); }
/* §NPPAUSE: play / pause, in the same dress as the skips, wide enough to say which */
#soundOpts .soplay {
  font: inherit;
  font-size: var(--fs-sm);
  flex: 0 0 132px;
  padding: 6px 0;
  letter-spacing: 2px;
  color: #ffd319;
  background: rgba(13, 2, 33, 0.75);
  border: 2px solid rgba(255, 211, 25, 0.6);
  cursor: pointer;
}
#soundOpts .soplay:disabled { color: #7a6fa8; border-color: rgba(122,111,168,.4); cursor: default; }
#soundOpts .soplay:active:not(:disabled) { transform: translate(1px, 1px); }

/* ===== §17 THE VISUAL OUTFITTER =================================== */
/* The ship diagram. Centred and given room to breathe — it is the thing
   you look at first and every other region is downstream of what is
   selected on it. */
canvas.ofdiag { display: block; margin: 2px auto 6px; max-width: 100%; }
/* The plate spans its column so the stat gutters sit at the panel edges
   rather than floating in the middle with dead space outside them. */
canvas.ofdiag.wide { width: 100%; height: auto; }
/* Everything in this panel lives inside a column that is narrower than the
   window. Without these, one fixed width anywhere makes the whole page scroll
   sideways — which is exactly what test_layout_audit catches. */
.drow.ofrow { min-width: 0; overflow: hidden; }
.drow.ofrow .dname { min-width: 0; max-width: 100%; }

/* The bench. One column when only one side is chosen, two when both are —
   which is the comparison Matt asked for, and it needs no separate mode. */
.bsplit { display: flex; gap: 14px; width: 100%; min-width: 0; }
.bcol { flex: 1 1 0; min-width: 0; }
.bcol.b { border-left: 1px solid rgba(150,200,255,0.18); padding-left: 12px; }
.blab { font-size: var(--fs-xs); letter-spacing: 0.08em; opacity: 0.55; }
.bnm  { font-weight: bold; margin: 1px 0 3px; }
.btab { width: 100%; border-collapse: collapse; font-size: var(--fs-xs); }
.btab td { padding: 1px 0; }
.btab td:first-child { opacity: 0.55; letter-spacing: 0.05em; }
.btab td:last-child { text-align: right; }
.btab td:first-child { overflow: hidden; text-overflow: ellipsis; }

/* The inventory. auto-fill at 78px, so a phone gets four columns and a
   desktop gets as many as fit — the tile size is fixed and the count is
   what varies, because a tile that stretches stops reading as an icon. */
.ofgrid { display: flex; gap: 14px; width: 100%; min-width: 0; }
.ofside {
  display: grid;
  /* min() so a column narrower than one tile gets a single squeezed track
     rather than a 78px track that pushes the page wider than the screen */
  /* em, not px: the tile has to grow with the type inside it, or a bigger
     font on a desktop starts clipping names that fitted a moment ago */
  /* §TILES: a row tile wants room for the name beside the icon. Sized off
     the TILE'S type (--fs-2xs), not the inherited em -- the grid sits under
     a 21.5px section font on a desktop, so 10em there was 215px and two
     tiles across where four fit. 12 x the small size is ~155px:
     "REINFORCED HOUSING" on two lines beside a 2em icon. A 640px merchant
     column gets four across, a 340px phone column two, at half the height
     the column tile had */
  grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--fs-2xs) * 12), 100%), 1fr));
  gap: 4px;
  flex: 1 1 0;
  min-width: 0;
  align-content: start;
}
/* At a bench that builds, the grid splits: yours left, theirs right. The
   divider is what makes the two sides read as two inventories rather than
   as one long shelf. */
.ofgrid.split .ofside.them {
  border-left: 1px solid rgba(150,200,255,0.22);
  padding-left: 12px;
}
/* §TILES, 2026-09-24. Matt: "how can we condense the item card sizes. They
   are large on PC but huge on mobile." The tile was a column -- icon, name,
   price stacked -- 76px tall for three short lines. It is a ROW now: icon
   on the left, name and price stacked beside it, so a tile is as tall as
   two lines of small type (~36px) and the shelf reads as a dense list. Same
   three spans, same classes, same builders (outfit.js invTile / holdTile,
   game.js merchStock) -- only the arrangement changed. */
.oftile {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto auto;   /* name / price / (condition bar, when a hold part has one) */
  column-gap: 6px; row-gap: 0;
  align-items: center;
  text-align: left;
  padding: 3px 6px 3px 4px;
  background: rgba(120,170,230,0.06);
  border: 1px solid rgba(150,200,255,0.20);
  border-radius: 3px;
  color: inherit; font: inherit; cursor: pointer;
  min-width: 0; overflow: hidden;
}
.oftile .ofico { grid-row: 1 / span 3; grid-column: 1; }
.oftile .ofnm  { grid-row: 1; grid-column: 2; text-align: left; }
.oftile .ofsub { grid-row: 2; grid-column: 2; }
.oftile .cbar.tile { grid-column: 2; grid-row: 3; width: 100%; margin-top: 1px; }
.oftile:hover { background: rgba(120,170,230,0.14); }
.oftile.sel  { border-color: #ff6ac1; background: rgba(255,106,193,0.14); }
.oftile.relic { border-color: rgba(255,211,25,0.55); }
.oftile.hurt .ofsub { color: #ff8a7a; }
.oftile.them { border-style: dashed; }
/* off the tile's own type, not the inherited em (21.5px under a desktop
   section header made a 2em icon 43px tall and the tile 67px) */
.ofico img, .ofico svg { width: calc(var(--fs-2xs) * 2.4); height: calc(var(--fs-2xs) * 2.4); display: block; }
.ofnm  { font-size: var(--fs-2xs); line-height: 1.15;
         max-height: 2.4em; overflow: hidden; opacity: 0.9;
         overflow-wrap: anywhere; }
.ofsub { font-size: var(--fs-2xs); opacity: 0.6; }
.ofnone { grid-column: 1 / -1; font-size: var(--fs-xs); opacity: 0.55; padding: 6px 2px; }

/* One grid per side is five columns at 412px, which is where Matt expected
   this to bite. Held as the option he asked for: stack the two sides rather
   than shrink the tiles, so a tile is always legible. */
@media (max-width: 620px) {
  .ofgrid.split { flex-direction: column; gap: 8px; }
  .ofgrid.split .ofside.them {
    border-left: 0; padding-left: 0;
    border-top: 1px solid rgba(150,200,255,0.22); padding-top: 8px;
  }
  .bsplit { gap: 10px; }
}

/* ===== §17R THE SPLIT: player left, merchant right ================== */
/* Two equal columns rather than three unequal ones. The divider is what
   makes the rule visible: everything left of it is yours. */
/* minmax(0, 1fr), not 1fr: a bare 1fr track is min-content-floored, so a
   merchant shelf full of long mod names pushed the right column wider and
   squeezed the left one. The plate sizes itself to the column it lands in, so
   the whole diagram — type included — changed size when you changed TABS.
   That is the bug; this is the fix. */
.mktmenu.split2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.mktmenu.split2 > .mktcol { min-width: 0; }
/* Matt, 2026-09-06: "I want the line between the personal side and the
   merchant side to be more prominent. a glowing pink line." */
.mktmenu.split2 .mcol {
  border-left: 2px solid #ff6ac1;
  box-shadow: -1px 0 6px rgba(255, 106, 193, 0.75), -3px 0 18px rgba(255, 41, 117, 0.35);
  padding-left: 18px;
}
.mktmenu.split2 { gap: 4px 20px; }
@media (max-width: 760px) {
  /* The market board turns into a swipe pager at <=700px, and that rule sets
     grid-auto-flow: column. Setting grid-template-columns alone did NOT undo
     it: the first column took the 1fr track and the second landed in an
     IMPLICIT 88%-wide one beside it, which is why the player column was
     crushed to a single character per line on a phone. The split is two
     stacked panels here, not pages you swipe between, so every part of the
     pager has to be switched off explicitly. */
  .mktmenu.split2 {
    display: grid;
    grid-auto-flow: row;
    grid-auto-columns: auto;
    grid-template-columns: minmax(0, 1fr);
    overflow-x: visible;
    scroll-snap-type: none;
  }
  .mktmenu.split2 > .mktcol { scroll-snap-align: none; }
  .mktmenu.split2::after { content: none; display: none; }
  .mktmenu.split2 .mcol {
    border-left: 0; padding-left: 0; box-shadow: none;
    border-top: 2px solid #ff6ac1; padding-top: 12px; margin-top: 6px;
    box-shadow: 0 -1px 6px rgba(255, 106, 193, 0.75), 0 -3px 18px rgba(255, 41, 117, 0.35);
  }
}

/* ---------- the merchant's tabs (Matt, 2026-09-06) ----------
   Real tabs, not buttons: each part type's colour (its tile tint) on the tab,
   and the selected tab joins the PAGE under it, which wears the same colour as
   an inner border -- the tab and the page are one shape. Left-justified. */
.mtabwrap { width: 100%; min-width: 0; }
.mtabs {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 3px;
  padding: 6px 0 0 6px;
  position: relative;
  z-index: 2;
}
.mtab {
  font-family: inherit;
  font-weight: bold;
  font-size: var(--fs-sm);
  letter-spacing: 2px;
  padding: 7px 14px 6px;
  color: var(--tc, #2de2e6);
  opacity: 0.6;
  background: rgba(13, 2, 33, 0.55);
  border: 2px solid transparent;
  border-bottom: 0;
  border-radius: 4px 4px 0 0;
  cursor: pointer;
  position: relative;
  top: 2px;                       /* sits ON the page's top border */
}
.mtab:hover { opacity: 0.9; }
.mtab.on {
  opacity: 1;
  border-color: var(--tc, #2de2e6);
  background: #120428;            /* the page's own ground, so the border line breaks under it */
  text-shadow: 0 0 6px var(--tc, #2de2e6);
  padding-bottom: 8px;            /* covers the page border by the 2px it sits over */
}
.mpage {
  border: 2px solid var(--tc, #2de2e6);
  border-radius: 0 4px 4px 4px;
  background: #120428;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
  box-sizing: border-box;
  box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.35);
}
.mpage > .drow { margin: 0; }
@media (max-width: 700px) {
  /* four tabs on one line, always -- a wrapped tab row leaves the selected
     tab floating above a page it no longer touches */
  .mtabs { flex-wrap: nowrap; gap: 2px; padding-left: 4px; }
  .mtab { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
          font-size: var(--fs-2xs); letter-spacing: 1px; padding: 6px 8px 5px; }
  .mtab.on { padding-bottom: 7px; }
}
/* §OFMFIT-2, 2026-09-23. Matt, from a phone in LANDSCAPE: "The outfitter fit
   is still not perfect on phones." The rule above keys on the VIEWPORT being
   narrow, but at 844x390 the viewport is wide and the split gives the
   merchant a 380px column -- narrower than any portrait phone -- so the four
   tabs wrapped, REACTORS dropped onto the page's top border, and the
   selected tab no longer touched its page. The strip's own width is the
   thing that decides this, so it is a container query on the wrapper now;
   the media query stays for browsers without them. */
.mtabwrap { container-type: inline-size; }
@container (max-width: 520px) {
  .mtabs { flex-wrap: nowrap; gap: 2px; padding-left: 4px; }
  .mtab { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
          font-size: var(--fs-2xs); letter-spacing: 1px; padding: 6px 8px 5px; }
  .mtab.on { padding-bottom: 7px; }
}
/* ...and at a 300px column (667x375, two columns) even that ellipsised every
   tab -- "HUL…", "WEAPO…". Spacing and padding to the bone; the names are
   short enough to fit whole at 4px of padding. */
@container (max-width: 360px) {
  .mtabs { gap: 1px; padding-left: 2px; }
  .mtab { letter-spacing: 0; padding: 6px 4px 5px; }
}
/* §OFMFIT-2, the third piece: a SHORT screen that is still wide enough for
   two columns keeps them. 667x375 (a small phone on its side) fell under the
   760px stacking rule, so the plate took the full width and 600px of height
   on a 375px screen, and the stacked three-row footer took half of what was
   left. Two 320px columns are what a portrait phone already gets, and the
   footer goes back to its one-row desktop shape, compact. */
@media (max-height: 500px) and (min-width: 600px) {
  .mktmenu.split2 {
    display: grid; grid-auto-flow: row;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 4px 14px;
  }
  .mktmenu.split2 .mcol {
    border-top: 0; padding-top: 0; margin-top: 0;
    border-left: 2px solid #ff6ac1; padding-left: 12px;
    box-shadow: -1px 0 6px rgba(255, 106, 193, 0.75), -3px 0 18px rgba(255, 41, 117, 0.35);
  }
  #dockFoot { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 6px 12px;
              padding: 5px 10px calc(5px + env(safe-area-inset-bottom, 0px)); }
  #dockFoot #dockWho { grid-row: auto; grid-column: auto; justify-self: end; text-align: right; }
  #dockFoot #dockCredits, #dockFoot #undockBtn { grid-row: auto; }
  #dockFoot #dockName { font-size: var(--fs-sm); letter-spacing: 2px; }
  #dockFoot #dockFaction { font-size: var(--fs-2xs); margin-top: 0; }
  #dockFoot #dockCredits { font-size: var(--fs-xs); letter-spacing: 1px; }
  #dockFoot #undockBtn { min-width: 0; padding: 8px 18px; font-size: var(--fs-md); }
}
/* Same report, the other half: on a short screen the dock's content starts at
   the very top, so the HUD bars, the credit line and the AUTOPILOT button
   showed through the 0.82 overlay right under the section headers -- garbage
   behind "SHIP - tap a mount". Near-opaque there; the desktop keeps its
   translucency, where the panel is inset and nothing sits behind the text.
   And the two section headers are a size down, so "SHIP - tap a mount or a
   chip" is one line instead of two at a 400px column. */
@media (max-width: 760px), (max-height: 500px) {
  /* fully opaque: at 0.97 the AUTOPILOT and DOCK buttons still ghosted
     through. Portrait phones too -- scrolled to the merchant column the HUD
     bars and the credit line showed through under the MERCHANT header */
  .dockoverlay { background: #0d0221; }
}
@media (max-height: 500px) {
  .dockoverlay .dsec { font-size: var(--fs-sm); letter-spacing: 2px; margin-top: 6px; }
}

/* The merchant's details area. Fixed at the top of the column so it never
   jumps when a different item is picked — a details pane that moves is a
   details pane you have to re-find every time. */
.mdet {
  border: 1px solid rgba(150,200,255,0.20);
  border-radius: 3px;
  padding: 8px 10px;
  min-height: 96px;
  width: 100%;
  /* the global box-sizing reset is off (§CSS-RESET), so 100% + 20px of
     padding + 2px of border ran 22px past the column and the row clipped
     it: the FOR SALE column lost its last two characters at every width
     (Matt's screenshot, 2026-09-06) */
  box-sizing: border-box;
}
.mdnone { opacity: 0.5; font-size: var(--fs-xs); }
.mdhead { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
.mdnm { font-weight: bold; }
.mddesc { font-size: var(--fs-xs); opacity: 0.75; margin-bottom: 5px; }
.mdet .picon { width: 34px; height: 34px; }

/* §MDPOP, 2026-09-24: the merchant's pick, as a panel over the page. Same
   look as the outfitter's context menu (#ofMenu): pink line, near-opaque
   ground, a sheet from the bottom on a phone. One column -- header, the
   compare or the lines, the recipe -- and a sticky action row. */
#merchPop {
  position: fixed;
  z-index: 26;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto auto;   /* body (scrolls) · quantity · action */
  background: rgba(13, 2, 33, 0.97);
  border: 1px solid #ff6ac1;
  box-shadow: 0 0 14px rgba(255, 106, 193, 0.55), 0 12px 40px rgba(0, 0, 0, 0.7);
  border-radius: 4px;
  color: #dffcff;
  font-size: var(--fs-sm);
  box-sizing: border-box;
  overflow: hidden;
}
#merchPop.pop {
  /* over the SHIP side, not the shelf: every tile stays visible and
     tappable while the details are up, which is how you compare. These
     are the fallback; placeMerchPop() (§MDPOP-3) pins it to the YOU
     column's right edge, centred on that column, whenever the yard's
     split is side by side -- desktop and landscape phone alike. */
  left: 24px; top: 50%; transform: translateY(-50%);
  width: min(460px, calc(100vw - 32px));
  max-height: min(80vh, 640px);
}
/* Matt, 2026-09-24, on the phone: "the pop up covers the options as you are
   browsing. it would be better for it to be more compact, not spanning the
   width of the page unnecessarily. Also for it to be at the top instead of
   the bottom." So: a card pinned to the top, inset from the edges, capped
   at ~45% of the screen so the shelf keeps scrolling underneath, small type
   inside. Not a sheet any more; the class name stays because the JS keys
   the phone layout on it. */
#merchPop.sheet {
  top: calc(8px + env(safe-area-inset-top, 0px)); left: 12px; right: 12px;
  width: auto;
  max-height: 46vh;
  border-radius: 6px;
  font-size: var(--fs-xs);
}
#merchPop.sheet .mdbody, #merchPop.tight .mdbody { padding: 6px 8px 2px; }
#merchPop.sheet .btab td, #merchPop.tight .btab td { padding: 1px 4px; font-size: var(--fs-2xs); }
#merchPop.sheet .picon, #merchPop.tight .picon { width: 24px; height: 24px; }
#merchPop.sheet .mdacts, #merchPop.tight .mdacts { padding: 5px 8px; }
#merchPop.sheet .mdacts .dbtn, #merchPop.tight .mdacts .dbtn { padding: 6px 12px; font-size: var(--fs-xs); }
#merchPop.sheet .mddesc, #merchPop.tight .mddesc { margin-bottom: 3px; }
#merchPop.tight { font-size: var(--fs-xs); }
#merchPop .mdbody { overflow-y: auto; min-height: 0; padding: 10px 12px 4px; border: 0; border-radius: 0; }
/* §BIGX, 2026-09-30. Matt: "id like the close button on the details screen
   when something is selected to be a bit larger so its easier to see and
   use." Both detail closers (this and .ofm-x) are a 34px boxed target now. */
#merchPop .mdhead { position: relative; padding-right: 44px; min-height: 34px; }
#merchPop .mdclose {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: 34px; height: 34px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,106,193,0.10); border: 1px solid rgba(255,106,193,0.7); border-radius: 4px;
  color: #ff6ac1; font-family: inherit; font-size: 22px; font-weight: bold; line-height: 1; cursor: pointer;
  box-shadow: 0 0 6px rgba(255,106,193,0.35);
}
#merchPop .mdclose:hover, #merchPop .mdclose:focus-visible { background: rgba(255,106,193,0.25); outline: none; }
#merchPop .picon { width: 34px; height: 34px; }
#merchPop .mdacts { padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px)); align-items: center; justify-content: space-between;
                    background: rgba(13,2,33,0.99); border-top: 1px solid rgba(255,106,193,0.3); margin: 0; }
#merchPop .mdacts small { color: #ffd319; letter-spacing: 1px; }
#merchPop .mdacts small .need { color: #ff3864; }
#merchPop .mdacts small .avg { color: #9fd8ff; letter-spacing: 0; opacity: 0.85; }
/* §TRADER: the trade post's popup carries a quantity under the numbers and
   the lead line over the shelf */
/* Matt, 2026-09-24: "its a little weird that the quantity is hidden in the
   scrollable details". So it is not in them: the picker is a band of its
   own between the scrolling body and the action row, always on screen. */
#merchPop .mdqty { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
                   padding: 6px 12px; border-top: 1px solid rgba(255,106,193,0.3);
                   background: rgba(13,2,33,0.99);
                   font-size: var(--fs-xs); letter-spacing: 1px; }
#merchPop.sheet .mdqty, #merchPop.tight .mdqty { padding: 4px 8px; }
#merchPop .mdqty .mdrl { color: #7a6fa8; font-weight: bold; }
#merchPop .mddesc .ledg { color: #9fd8ff; }
#merchPop .mkdn { color: #ff8a7a; font-weight: bold; letter-spacing: 1px; }
#merchPop .mkup { color: #7CE7A6; font-weight: bold; letter-spacing: 1px; }
#merchPop .btab td.good { color: #7CE7A6; }
#merchPop .btab td.bad { color: #ff8a7a; }
.ofsplit.tlead { border-top: 0; margin-top: 0; padding-top: 0; margin-bottom: 4px; }
.tgrid .oftile.k-raw .ofsub, .tgrid .oftile.k-ref .ofsub { opacity: 0.85; }

/* Kind tints. One glance separates a gun from an ore, which is the whole
   reason the two stores share a grid. */
.oftile.k-wpn  { border-color: rgba(255,140,110,0.45); background: rgba(255,140,110,0.07); }
.oftile.k-gear { border-color: rgba(45,226,230,0.42);  background: rgba(45,226,230,0.06); }
.oftile.k-mod  { border-color: rgba(255,211,25,0.45);  background: rgba(255,211,25,0.07); }
.oftile.k-raw  { border-color: rgba(160,175,200,0.40); background: rgba(160,175,200,0.06); }
.oftile.k-ref  { border-color: rgba(124,231,166,0.42); background: rgba(124,231,166,0.06); }
.oftile.k-hull { border-color: rgba(190,150,255,0.45); background: rgba(190,150,255,0.07); }
/* §TRADER 2026-09-24: goods (meds, tech, contraband, relics) get the
   counter's gold, so the three tabs and their tiles share a colour each */
.oftile.k-goods { border-color: rgba(255,211,25,0.40); background: rgba(255,211,25,0.06); }

/* Materials have no icon of their own yet — see the icon request in
   ROADMAP §17R. A tinted two-letter chip is legible and honest; borrowing a
   picture of a gun for an ore would not be. */
.ofchip {
  display: flex; align-items: center; justify-content: center;
  width: 2.6em; height: 2.6em; border-radius: 3px;
  font-size: var(--fs-sm); font-weight: bold; letter-spacing: 0.02em;
}
.ofchip.raw { background: rgba(160,175,200,0.18); color: #c3cede; }
.ofchip.ref { background: rgba(124,231,166,0.18); color: #9de7bb; }
.ofchip.goods { background: rgba(255,211,25,0.18); color: #ffe27a; }
/* §DESIGN 2026-09-08: a commodity's own icon, where Matt has drawn one. Same
   26px box as a part icon, tinted on the raw/refined axis the chips already
   used rather than the part CATEGORY hues -- an ore is not a weapon. */
.picon.comm { filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(420%) hue-rotate(178deg) brightness(102%) contrast(96%); }

/* A purchase has to land somewhere visible, or it is just a number going
   down. The new tile flashes for a moment in the hold it landed in. */
@keyframes offresh {
  0%   { box-shadow: 0 0 0 2px #ffd319, 0 0 14px 2px rgba(255,211,25,0.7); }
  70%  { box-shadow: 0 0 0 2px #ffd319, 0 0 14px 2px rgba(255,211,25,0.5); }
  100% { box-shadow: 0 0 0 0 rgba(255,211,25,0); }
}
.oftile.fresh { animation: offresh 1.8s ease-out 1; }
@media (prefers-reduced-motion: reduce) { .oftile.fresh { animation: none; outline: 2px solid #ffd319; } }
/* Over-tier stock is SHOWN, not hidden — the point is to tell you where to
   fly for it, which an absent row cannot do. */
.oftile.over { opacity: 0.5; border-style: dashed; }
.mddesc.need { color: #ff9b8a; }

/* ===== §17R-2 the one comparison ==================================== */
/* Three columns: what the stat is, what you have, what you are considering.
   The headers are the point — Matt's complaint was not knowing which side
   was which, and a coloured number cannot answer that on its own. */
.cmp { width: 100%; font-size: var(--fs-sm); }
.cmprow { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; padding: 1px 0; }
.cmprow .cmpk { opacity: 0.55; letter-spacing: 0.04em; font-size: var(--fs-xs); }
.cmprow .cmpa, .cmprow .cmpb { text-align: right; }
.cmphead { border-bottom: 1px solid rgba(150,200,255,0.22); padding-bottom: 3px; margin-bottom: 3px; }
.cmphead .cmpa, .cmphead .cmpb {
  font-size: var(--fs-xs); letter-spacing: 0.06em; opacity: 0.8; font-weight: bold;
}
.cmphead .cmpa { color: #9fb6d4; }
.cmphead .cmpb { color: #2de2e6; }
.cmpname { font-weight: bold; margin-bottom: 4px; }
.cmpname .cmpa { opacity: 0.75; }
.cmprow .cmpb.up   { color: #7CE7A6; }
.cmprow .cmpb.down { color: #ff8a7a; }
.cmpd { font-size: var(--fs-xs); opacity: 0.85; }

/* a tile you cannot use, in the hold as well as on the shelf. Matt,
   2026-09-06: "instead of crossed out, make currently unobtainable equipment
   and mods greyed out somehow but keep the category colors" -- so the border
   keeps its category colour (dashed, to say "not now") and only what is
   INSIDE the tile goes grey and dim. No strike-through. */
.oftile.cant { opacity: 1; border-style: dashed; background: rgba(13, 2, 33, 0.35); }
.oftile.cant .ofico, .oftile.cant .ofnm, .oftile.cant .ofsub { filter: grayscale(1); opacity: 0.42; }
.oftile.cant .ofnm { text-decoration: none; }

/* (§Q%, 2026-10-04: the grade glows are gone -- quality is the .qtag) */

/* the diagram is a drag surface: stop the browser treating a drag as a
   text selection. It is NOT allowed to eat the page pan any more -- Matt,
   2026-09-06: "on mobile drag to scroll does not work everywhere you drag."
   pan-y hands a vertical finger to the scroller (the browser fires
   pointercancel and the bench drag simply does not start); a sideways drag
   and every mouse drag still work. Tap-to-select + FIT / SWAP IN is the
   primary path on a phone; drag-and-drop is the extra. */
canvas.ofdiag { touch-action: pan-y; user-select: none; -webkit-user-select: none; }
.ofside .oftile { touch-action: pan-y; }

/* ===== §17R-2b mods share a shelf with the hardware they bolt into ===== */
/* Colour is what keeps them apart: a mod is a modifier, not a thing you
   mount, and on a merged shelf that difference has to be visible at a
   glance or the shelf reads as one undifferentiated pile. */
.oftile.ismod {
  border-style: dashed;
  background: rgba(255,211,25,0.10);
}
.oftile.ismod .ofnm { color: #ffd97a; }
.oftile.k-core { border-color: rgba(178,140,255,0.5); background: rgba(178,140,255,0.08); }
.ofsplit {
  grid-column: 1 / -1;
  font-size: var(--fs-xs); letter-spacing: 0.08em;
  color: rgba(255,211,25,0.75);
  border-top: 1px solid rgba(255,211,25,0.3);
  margin-top: 6px; padding-top: 5px;
}


/* An embedded panel inside OPTIONS (the sound controls). It is the real
   #soundBody moved in, not a copy, so it keeps its own internal styling —
   all this does is give it the full row instead of the right-hand control
   column, which is far too narrow for two sliders and a track skipper. */
.optrow.optembed {
  display: block;
  padding: 0;
}
.optrow.optembed #soundBody { margin: 0; }
/* The sound panel's own hint style is scoped to #soundOpts, so once the panel
   moves in here it fell back to the generic .sub — centred and coloured — and
   read as a different kind of text from every other explanation in the list.
   Match .opthint instead, so the embedded rows look like they belong. */
.optrow.optembed .sohint {
  margin: 2px 0 6px;
  text-align: left;
  font-size: var(--fs-2xs);
  line-height: 1.45;
  letter-spacing: 0;
  color: #8d86a8;
  opacity: 1;
}


/* ==========================================================================
   COMPOSITED PULSES  (2026-09-04)
   --------------------------------------------------------------------------
   Every glow pulse in this file used to animate `box-shadow`. box-shadow is
   not a compositable property, so each keyframe step forces the browser to
   REPAINT the element on the main thread. #dockBtn / #jumpBtn / #takeoffBtn
   ran `dockpulse 1s steps(2) infinite` — a hard repaint exactly once a
   second, on a DPR-3 phone, whether or not the game was drawing anything.
   Matt: "on mobile the game still stutters regularly about every second",
   and his title-screen reading showed 125ms spikes with the game idle and
   GC/s 0 — browser work, not game work.

   The glow now lives on an ::after that carries a STATIC bright shadow and
   animates only its OPACITY, which the compositor can do on the GPU without
   repainting. The element keeps its own dim shadow underneath, so the two
   states look exactly as they did.
   ========================================================================== */
/* No position override here on purpose: all five are already
   position:absolute, which is itself a containing block for the ::after.
   An earlier draft set position:relative and would have dropped every one
   of them out of its absolute placement. */

#dockBtn::after, #jumpBtn::after, #takeoffBtn::after,
#apBtn::after, #claimBtn.unread::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
}

/* cyan buttons — the bright half of the old dockpulse */
#dockBtn::after, #takeoffBtn::after {
  box-shadow: 0 0 22px rgba(0, 240, 255, 1);
  animation: pulseFade 1s steps(2) infinite;
}
/* pink jump button */
#jumpBtn::after {
  box-shadow: 0 0 22px rgba(255, 106, 193, 1);
  animation: pulseFade 1s steps(2) infinite;
}
/* autopilot: the old appulse ramped smoothly */
#apBtn::after {
  box-shadow: 0 0 18px rgba(255, 106, 193, 0.95);
  animation: pulseFadeSmooth 1.6s ease-in-out infinite;
}
#claimBtn.unread::after {
  box-shadow: 0 0 22px rgba(255, 56, 100, .95);
  animation: pulseFadeSmooth 1.4s ease-in-out infinite;
}

@keyframes pulseFade       { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0; } }
@keyframes pulseFadeSmooth { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }


/* ---- title-screen art loading bar (2026-09-04) ----
   A cold start fetches ~415 files. This exists so a slow load reads as a slow
   load and not as a broken game. main.js only reveals it when the load is
   genuinely still running after 250 ms. */
#artLoad { margin: 10px auto 0; max-width: 260px; width: 80%; }
#artLoad.hidden { display: none; }
#artLoadTrack {
  height: 4px;
  background: rgba(45, 226, 230, 0.12);
  border-radius: 2px;
  overflow: hidden;
}
#artLoadFill {
  height: 100%;
  width: 0%;
  background: linear-gradient(to right, #00f0ff, #2de2e6);
  transition: width 0.18s linear;
}
#artLoad .sub { margin: 4px 0 0; font-size: 9px; opacity: 0.55; letter-spacing: 1px; }
#artLoad.done #artLoadFill { background: linear-gradient(to right, #00f0ff, #39ff14); }


/* ---------- THE STRIP: the blade bar and cut buttons (§Q3/LOOT) ----------
   Matt, 2026-09-06: one cut per point. Tap CUT and the blade runs along the
   bar for the line; tap STOP as close to the line as you dare. Past the line
   is always a fail, so the line is drawn hard and the blade turns red the
   moment it crosses. */
.stripgauge { margin: 6px 0 10px; }

/* §CUTUI, 2026-09-13. Matt: "i dont like that the cutting interface shows the
   result over the game area for the next cut." The outcome now belongs to the
   panel, in its own line under the blade bar, instead of riding the global
   toast that is centred over the viewport and landed on the bar itself.
   Reserves its height whether or not it is showing, so the rows below never
   jump when a cut resolves. */
.stripresult {
  min-height: 1.35em;
  margin: 6px 2px 0;
  font-size: 0.78rem;
  font-weight: bold;
  letter-spacing: 0.04em;
  opacity: 0;
  transition: opacity 140ms linear;
}
.stripresult.show { opacity: 1; }
.stripresult.good { color: #39ff14; }
.stripresult.bad  { color: #ff3864; }
.striplabel {
  color: #ff6ac1;
  font-size: var(--fs-xs);
  letter-spacing: 2px;
  margin-bottom: 4px;
}
.striplabel small { color: #9a8fb5; letter-spacing: 0; margin-left: 6px; }
.stripbar {
  position: relative;
  height: 12px;
  background: rgba(13, 2, 33, 0.85);
  border: 1px solid #3a2a56;
  overflow: hidden;
}
.stripbar i {
  display: block;
  height: 100%;
  width: 0;
  background: #2de2e6;
  box-shadow: 0 0 8px rgba(45, 226, 230, 0.7);
  /* no transition: this repaints every frame off the cut ticker, and an
     eased width would lag the blade the player is actually watching */
}
/* stopped short of the line: held, gold */
.stripbar i.held {
  background: #ffd319;
  box-shadow: 0 0 10px rgba(255, 211, 25, 0.8);
}
/* past the line: lost */
.stripbar i.past {
  background: #ff3864;
  box-shadow: 0 0 10px rgba(255, 56, 100, 0.9);
}
/* THE LINE */
.stripbar u {
  position: absolute;
  top: -1px; bottom: -1px;
  width: 3px;
  background: #ff3864;
  box-shadow: 0 0 6px rgba(255, 56, 100, 0.9);
  text-decoration: none;
}
.dbtn.cutbtn { min-width: 66px; touch-action: none; user-select: none; }
.dbtn.cutbtn.cutting {
  background: #ff6ac1;
  color: #0d0221;
  border-color: #ff6ac1;
  box-shadow: 0 0 12px rgba(255, 106, 193, 0.8);
}

/* ===== §MENU: the outfitter's context menu (Matt, 2026-09-06) ============
   "when you click a ship slot you get a menu of equipment that can fit in
   that slot and when you select one a fly out pops up with the details".
   One floating panel: the LIST on the left (what could go here / where this
   could go), the FLYOUT on the right (compare, sockets, EQUIP). A popover
   hung off what was tapped on a desktop; a bottom sheet on a phone, where
   the list turns into a strip of pills above the flyout. Sits under the
   cart (30) so a bill opened from it reads on top. */
#ofMenu {
  position: fixed;
  z-index: 26;
  display: grid;
  grid-template-columns: 15.5em minmax(0, 1fr);
  gap: 0;
  background: rgba(13, 2, 33, 0.97);
  border: 1px solid #ff6ac1;
  box-shadow: 0 0 14px rgba(255, 106, 193, 0.55), 0 12px 40px rgba(0, 0, 0, 0.7);
  border-radius: 4px;
  color: #dffcff;
  font-size: var(--fs-sm);
  box-sizing: border-box;
  overflow: hidden;
}
#ofMenu.pop {
  width: min(680px, calc(100vw - 16px));
  max-height: min(78vh, 640px);
  /* §OFMFIT, 2026-09-13. Matt: "this interface is cut off."
     MEASURED at 1280x694 with a hold part selected: the panel box was 541px
     tall and inside its max-height, but `.ofm-acts` laid out at bottom 703 --
     past the panel AND past the window -- while `.ofm-fly` reported
     scrollHeight 650 === clientHeight 650, i.e. it was never scrolling at all.
     The row was the whole problem: this rule set grid-template-COLUMNS and no
     rows, so the implicit row is `auto` and sizes to its content. max-height
     then only CLIPS the box; the children keep their natural height, the
     buttons walk out of the bottom, and `overflow-y: auto` on the columns
     never engages because they have no definite height to scroll within.
     `.sheet` already declared its rows, which is exactly why the phone layout
     never showed this. One bounded row gives both columns a height, so they
     scroll and the action buttons stay inside the panel. */
  grid-template-rows: minmax(0, 1fr);
}
#ofMenu.sheet {
  left: 0; right: 0; bottom: 0;
  width: 100%;
  max-height: 74vh;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  border-radius: 8px 8px 0 0;
  border-bottom: 0;
}
.ofm-list {
  overflow-y: auto;
  border-right: 1px solid rgba(255, 106, 193, 0.35);
  background: rgba(30, 10, 60, 0.45);
  min-height: 0;
  padding-bottom: 6px;
}
.ofm-fly {
  overflow-y: auto;
  min-height: 0;
  /* bottom padding lives on `.ofm-acts` — see the sticky note there */
  padding: 10px 12px 0;
  display: flex; flex-direction: column; gap: 8px;
}
.ofm-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 8px 6px 10px;
  color: #ff6ac1; font-weight: bold; letter-spacing: 2px; font-size: var(--fs-xs);
  border-bottom: 1px solid rgba(255, 106, 193, 0.35);
  position: sticky; top: 0; background: rgba(13, 2, 33, 0.97); z-index: 1;
}
.ofm-x {
  flex: 0 0 auto; width: 34px; height: 34px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,106,193,0.10); border: 1px solid rgba(255,106,193,0.7); border-radius: 4px;
  color: #ff6ac1; font-family: inherit; font-size: 20px; font-weight: bold; line-height: 1;
  cursor: pointer; box-shadow: 0 0 6px rgba(255,106,193,0.35);
}
.ofm-x:hover, .ofm-x:focus-visible { background: rgba(255,106,193,0.25); outline: none; }
.ofm-group {
  padding: 8px 10px 3px;
  font-size: var(--fs-2xs); letter-spacing: 2px; color: #9a8fb5; font-weight: bold;
}
.ofm-note { padding: 6px 10px; font-size: var(--fs-xs); opacity: 0.7; }
.ofm-item {
  display: flex; align-items: center; gap: 8px;
  width: 100%; box-sizing: border-box;
  padding: 6px 10px;
  background: none; border: 0; border-left: 3px solid transparent;
  color: inherit; font: inherit; text-align: left; cursor: pointer;
  min-width: 0;
}
.ofm-item:hover { background: rgba(120, 170, 230, 0.10); }
.ofm-item.on { background: rgba(255, 106, 193, 0.14); border-left-color: #ff6ac1; }
.ofm-item.cant .ofm-nm, .ofm-item.cant .ofm-ico { opacity: 0.42; }
.ofm-item.cant .ofm-sub { color: #ffb000; opacity: 0.85; }
.ofm-ico { flex: 0 0 auto; display: flex; }
.ofm-ico .picon { width: 26px; height: 26px; vertical-align: middle; }
.ofm-txt { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.ofm-nm { font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ofm-sub { font-size: var(--fs-2xs); opacity: 0.65; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ofm-r { flex: 0 0 auto; font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
.ofm-item.ok .ofm-r { color: #7CE7A6; }
.ofm-item.worn .ofm-r { color: #FFC46B; }
.ofm-item.bad .ofm-r { color: #ff8a7a; }
.ofm-item.shop .ofm-r { color: #ffd319; }
.ofm-title { display: flex; flex-direction: column; gap: 2px; }
.ofm-tn { font-weight: bold; font-size: var(--fs-md); display: flex; align-items: center; gap: 6px; }
.ofm-tn .picon { width: 28px; height: 28px; }
.ofm-ts { font-size: var(--fs-2xs); letter-spacing: 2px; color: #9a8fb5; }
.cmp.one .cmprow { grid-template-columns: 1fr 1fr; }
.ofm-budget {
  display: flex; flex-direction: column; gap: 2px;
  font-size: var(--fs-xs); letter-spacing: 1px;
  padding: 6px 8px; border: 1px dashed rgba(150, 200, 255, 0.25); border-radius: 3px;
  color: #bfe3ff;
}
.ofm-budget .bad { color: #ff8a7a; }
.ofm-socks { display: flex; flex-direction: column; gap: 5px; }
.ofm-sock { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: var(--fs-xs); }
.ofm-sockl { color: #ffd319; letter-spacing: 1px; flex: 0 0 auto; }
.ofm-sel {
  flex: 1 1 10em; min-width: 0;
  background: #1a0b33; color: #dffcff; border: 1px solid #ffd319;
  font: inherit; font-size: var(--fs-xs); padding: 4px 6px; border-radius: 3px;
}
.ofm-sel:disabled { opacity: 0.5; }
/* §OFMFIT, 2026-09-13: the action row is PINNED to the bottom of the column
   it lives in. Bounding the grid row above made the column scroll instead of
   overflowing, which stopped the buttons escaping the window -- but it left
   them below the fold on a short screen, so the fix for "cut off" would have
   been "scroll to find the button you came here to press". Sticky keeps them
   on screen at every height while the stats scroll underneath. The background
   is opaque because the comparison table passes behind it. */
.ofm-acts {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-top: 4px;
  position: sticky;
  bottom: 0;
  z-index: 2;
  /* Solid, not a gradient: the comparison table scrolls behind this row and a
     soft top edge let it read through the buttons.
     A sticky `bottom: 0` anchors to the scrollport's CONTENT box, not its
     padding box -- MEASURED, the column's 12px bottom padding left exactly a
     12px strip of scrolled content peeking out under the buttons, and a
     negative margin did not move the painted edge. So the column gives up its
     bottom padding and this row carries it instead, which puts the pinned
     edge flush with the bottom of what the column shows. */
  padding: 8px 0 12px;
  background: rgba(13, 2, 33, 0.99);
  border-top: 1px solid rgba(255, 106, 193, 0.30);
}
/* §OFMFIT: a fly column with NO action row still needs a foot. */
.ofm-fly > :last-child:not(.ofm-acts) { margin-bottom: 12px; }
.ofm-acts .dbtn { margin: 0; }
.dbtn.sm { padding: 3px 8px; font-size: var(--fs-2xs); }

/* ---------- §INV: the reworked inventory sheet ----------
   The sheet is the outfitter's plate plus a read-only version of its context
   menu, so almost all of the styling above is reused as-is. These are the
   three things it needs that the yard does not. */

/* a row that opens the menu on whatever it names — the whole row, not a
   button inside it, because on a phone the row IS the target */
.invpanel .drow.invrow { cursor: pointer; }
.invpanel .drow.invrow:hover { background: rgba(120, 170, 230, 0.10); }
.invpanel .drow.invrow:active { background: rgba(255, 106, 193, 0.12); }

/* §INV2, 2026-09-09: the sheet is now the YARD'S split — `.mktmenu.split2`,
   the same two columns and the same glowing divider — so it needs none of its
   own layout. Matt, on seeing the first version at desk width: "on PC the ship
   diagram is huge ... put it on the left side the same way it is at the
   shipyard but on the right side in inv thats where we show GAME statuses".
   The full-width plate rule that used to live here is gone with it; the plate
   sizes itself to the column, which at half a desktop is the size the yard
   already draws it. */

/* the flight-deck chip: amber like the marker, and its bar is the production
   line rather than a condition */
.ofpc.deck .ofcpc { color: #ffd319; }

/* the production line, as a countdown rather than a percentage */
.baybar {
  position: relative;
  height: 20px;
  border: 1px solid rgba(255, 211, 25, 0.45);
  background: rgba(255, 211, 25, 0.08);
  overflow: hidden;
}
.baybar i {
  position: absolute; inset: 0 auto 0 0;
  background: rgba(255, 211, 25, 0.32);
  transition: width 0.25s linear;
}
.baybar b {
  position: relative;
  display: block;
  text-align: center;
  line-height: 20px;
  font-size: var(--fs-2xs);
  letter-spacing: 1.5px;
  color: #ffd319;
}
.baybar.paused { border-color: rgba(255, 138, 122, 0.5); }
.baybar.paused b { color: #ff8a7a; }
.baybar.paused i { background: rgba(255, 138, 122, 0.22); }

/* the sheet: the list sits above the flyout. Matt, 2026-09-26: "its
   difficult to choose a item to slot in on the ship once the slot is
   selected because its all in a scrolling tab list, so almost all options
   are hidden until you cycle through them all." It WAS one line of pills
   scrolling sideways -- on a 390px phone with a hold of a dozen parts, zero
   of them were on screen. Now it is the shelf's grid: small tiles that
   wrap, every candidate in view at once, and only past ~38% of the screen
   does it scroll -- vertically, like everything else. */
#ofMenu.sheet .ofm-list {
  border-right: 0; border-bottom: 1px solid rgba(255, 106, 193, 0.35);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--fs-2xs) * 12), 100%), 1fr));
  gap: 4px; align-content: start; padding: 0 8px 6px;
  max-height: 38vh; overflow-y: auto; overflow-x: hidden;
}
#ofMenu.sheet .ofm-head { grid-column: 1 / -1; position: sticky; top: 0; border: 0; padding: 8px 4px 4px 0; margin: 0 -8px; padding-left: 8px; }
#ofMenu.sheet .ofm-group { grid-column: 1 / -1; padding: 4px 2px 0; }
#ofMenu.sheet .ofm-note { grid-column: 1 / -1; padding: 2px 2px 4px; }
#ofMenu.sheet .ofm-item {
  width: auto; max-width: none; min-width: 0;
  border: 1px solid rgba(150, 200, 255, 0.25); border-left-width: 3px; border-radius: 4px;
  padding: 4px 6px; gap: 6px;
}
#ofMenu.sheet .ofm-item .ofm-nm { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere; font-size: var(--fs-2xs); line-height: 1.15; }
#ofMenu.sheet .ofm-item .ofm-sub { white-space: normal; overflow: visible; text-overflow: clip; }
#ofMenu.sheet .ofm-item .ofm-ico .picon { width: 22px; height: 22px; }
#ofMenu.sheet .ofm-item.on { border-color: #ff6ac1; }
#ofMenu.sheet .ofm-fly { padding: 8px 12px 14px; }

/* ---------- the chips under the plate ----------
   one per slot in the ship's own order, the category colour on the edge,
   the condition as a number coloured by band; HULL beside them; REPAIR ALL
   last, only when there is anything to repair */
.ofpcs { display: flex; flex-wrap: wrap; gap: 5px; width: 100%; min-width: 0;
  /* the row's .dname sets overflow-wrap: anywhere and a bold display size;
     neither is wanted on a chip, and the first one shrinks a chip to one
     letter wide because it makes the min-content width a single glyph */
  overflow-wrap: normal; font-weight: normal; letter-spacing: 0; }
.ofpc {
  display: flex; align-items: center; gap: 5px; flex: 0 0 auto; white-space: nowrap;
  padding: 3px 8px 3px 5px;
  background: rgba(120, 170, 230, 0.06);
  border: 1px solid rgba(150, 200, 255, 0.22);
  border-left: 3px solid var(--cc, #2de2e6);
  border-radius: 3px;
  color: inherit; font: inherit; font-size: var(--fs-xs); cursor: pointer;
  min-width: 0; max-width: 100%;
}
.ofpc:hover { background: rgba(120, 170, 230, 0.14); }
.ofpc.on { border-color: #ff6ac1; border-left-color: var(--cc, #2de2e6); background: rgba(255, 106, 193, 0.14); }
.ofpc.empty { opacity: 0.55; border-style: dashed; border-left-style: solid; }
.ofcico .picon { width: 18px; height: 18px; vertical-align: middle; }
.ofcdot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--cc, #2de2e6); opacity: 0.8; }
.ofcnm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 9em; }
.ofcpc { font-variant-numeric: tabular-nums; }
.ofcpc.dim { opacity: 0.5; font-weight: normal; }
.ofpc.ok .ofcpc { color: #7CE7A6; }
.ofpc.worn .ofcpc { color: #FFC46B; }
.ofpc.bad .ofcpc { color: #ff8a7a; }
.ofpc.all { border-color: #ffb000; border-left-color: #ffb000; color: #ffb000; font-weight: bold; letter-spacing: 1px; }
.ofpc.all .ofcpc { color: #ffb000; }

/* ---------- §24 item 4: the condition bar ----------
   Matt: "show condition as a bar rather than a bare percentage, with the
   permanently-lost portion — the gap between 100% and the part's current
   repairCap ceiling — rendered in red at the end of the bar, so a glance
   shows how much of a part's life is gone for good versus how much is just
   currently worn and repairable." cf = what is left, cg = repairable, cx =
   gone for good. */
.cbar {
  position: relative; display: block; height: 4px; width: 100%;
  background: rgba(150, 200, 255, 0.14); border-radius: 2px; overflow: hidden;
}
.cbar i { position: absolute; top: 0; bottom: 0; left: 0; display: block; }
.cbar .cf { background: #7CE7A6; }
.cbar.worn .cf { background: #FFC46B; }
.cbar.bad .cf { background: #ff8a7a; }
.cbar .cg { background: repeating-linear-gradient(90deg, rgba(255, 196, 107, 0.55) 0 2px, transparent 2px 4px); }
.cbar .cx { background: #ff3864; box-shadow: 0 0 4px rgba(255, 56, 100, 0.8); }
.cbar.chip { width: 38px; height: 4px; flex: 0 0 auto; }
.cbar.tile { width: 84%; height: 3px; margin-top: 2px; }
.cbar.row { margin-top: 3px; height: 3px; width: 90%; }
.cbar.title { margin-top: 3px; height: 5px; max-width: 22em; }

/* §22: the wave on a market row -- what your own trading did to the price */
.drow .dname small .mkdn { color: #ff8a7a; font-weight: bold; letter-spacing: 1px; }
.drow .dname small .mkup { color: #7CE7A6; font-weight: bold; letter-spacing: 1px; }
/* §ECON: a known price elsewhere -- the ledger's one hint on a market row */
.drow .dname small .ledg { color: #9fd8ff; }

/* ---- §SAVES: the account on the title screen, the sign-in box ---------- */
.acct { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 4px; }
.acct .acctline { margin: 0; letter-spacing: 1px; line-height: 1.5; }
.acct .acctline b { color: #00f0ff; }
.acct .acctnote { font-size: var(--fs-xs); opacity: 0.8; letter-spacing: 1px; text-transform: none; }
.acct .acctnote .bad { color: #ff3864; }
#acctBtn { min-width: 160px; padding: 10px 22px; font-size: var(--fs-sm); }

/* ---- §SLOTS: the save slot picker ---------------------------------------- */
#title .contline { margin: -6px 0 0; font-size: var(--fs-xs); letter-spacing: 1px; text-transform: none; opacity: .85; }
#title .contline b { color: #ffd319; font-weight: normal; }
#slots { background: rgba(13, 2, 33, 0.94); }
#newConfirm { z-index: 30; }   /* above the slot picker it is asked from */
#slotsBtn { min-width: 160px; padding: 10px 22px; font-size: var(--fs-sm); }
.slotlist { display: flex; flex-direction: column; gap: 8px; width: min(560px, 96vw); text-align: left; }
.slotrow { display: grid; grid-template-columns: 34px 1fr auto; gap: 8px 12px; align-items: center;
  background: #241734; border: 1px solid #8b2fc9; padding: 10px 12px; box-shadow: 3px 3px 0 #8b2fc9; }
.slotrow.cur { border-color: #00f0ff; box-shadow: 0 0 10px rgba(0,240,255,.5), 3px 3px 0 #8b2fc9; }
.slotrow.empty { opacity: .75; }
.slotrow .sn { font-size: var(--fs-lg); font-weight: bold; color: #ff6c11; text-align: center; letter-spacing: 0; }
.slotrow.cur .sn { color: #00f0ff; }
.slotrow .sname { font-size: var(--fs-md); font-weight: bold; color: #fff; letter-spacing: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.slotrow .sname i { font-style: normal; color: #05d9e8; opacity: .8; font-weight: normal; }
.slotrow .ssum { font-size: var(--fs-xs); color: #05d9e8; letter-spacing: 1px; text-transform: none; line-height: 1.4; }
.slotrow .ssum b { color: #ffd319; font-weight: normal; }
.slotrow .sacts { display: flex; flex-direction: column; gap: 4px; }
.slotrow .sacts button, .slotrow .sacts .slotplay { font-family: inherit; font-weight: bold; font-size: var(--fs-xs); letter-spacing: 1px;
  padding: 7px 12px; min-width: 88px; cursor: pointer; border: 1px solid #05d9e8; background: #241734; color: #05d9e8; }
.slotrow .sacts .slotplay { background: #00f0ff; color: #0d0221; border-color: #00f0ff; }
.slotrow .sacts .slotdel { border-color: #ff3864; color: #ff3864; }
.slotrow .sacts button:active { transform: translate(1px, 1px); }
.slotrow input.srename { font-family: inherit; font-size: var(--fs-sm); letter-spacing: 1px; color: #fff; background: #0d0221;
  border: 1px solid #00f0ff; padding: 6px 8px; width: 100%; box-sizing: border-box; }
@media (max-width: 640px) {
  .slotrow { grid-template-columns: 26px 1fr; }
  .slotrow .sacts { grid-column: 1 / -1; flex-direction: row; flex-wrap: wrap; }
  .slotrow .sacts button, .slotrow .sacts .slotplay { flex: 1; min-width: 70px; }
}
.stale { z-index: 60; background: rgba(13, 2, 33, 0.96); }

/* every subdomain links back to the root (2026-09-08) */
#title .mfhome { margin: 2px 0 0; letter-spacing: 2px; opacity: .6; text-decoration: none; }
#title .mfhome:hover { opacity: 1; }

/* §SURFACE 2026-09-24: a read-only hold (the mine, the factory) -- tiles
   you look at, not tap */
.ofgrid.quiet .oftile { cursor: default; }
#merchPop .mkbill { display: contents; }
.ofgrid.quiet .oftile:hover { filter: none; transform: none; }

/* §MENU-GRID (same day): a landscape phone gets the popover, not the sheet,
   and its list column showed four of sixteen candidates. Give the popover
   the whole width there and let the list wrap into the same tile grid. */
@media (max-height: 500px) {
  #ofMenu.pop { width: calc(100vw - 16px); grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); }
  #ofMenu.pop .ofm-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--fs-2xs) * 13), 100%), 1fr));
    gap: 4px; align-content: start; padding: 0 6px 6px;
  }
  #ofMenu.pop .ofm-head { grid-column: 1 / -1; margin: 0 -6px; padding-left: 8px; }
  #ofMenu.pop .ofm-group, #ofMenu.pop .ofm-note { grid-column: 1 / -1; padding: 4px 2px 0; }
  #ofMenu.pop .ofm-item {
    width: auto; min-width: 0; border: 1px solid rgba(150, 200, 255, 0.25); border-left-width: 3px; border-radius: 4px;
    padding: 3px 6px; gap: 6px;
  }
  #ofMenu.pop .ofm-item .ofm-nm, #ofMenu.pop .ofm-item .ofm-sub { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere; font-size: var(--fs-2xs); line-height: 1.15; }
  #ofMenu.pop .ofm-item .ofm-ico .picon { width: 20px; height: 20px; }
  #ofMenu.pop .ofm-item.on { border-color: #ff6ac1; }
}

/* §HUNT 2026-09-27: TRACK on a bill's sigil line, and the roster's buttons */
.mdrecipe .mdtrack {
  font: inherit; font-size: var(--fs-2xs); letter-spacing: 1px; font-weight: bold;
  color: #ffd479; background: rgba(255,212,121,0.10); border: 1px solid rgba(255,212,121,0.5);
  border-radius: 3px; padding: 1px 6px; cursor: pointer;
}
.mdrecipe .mdtrack:hover { background: rgba(255,212,121,0.22); }

/* ===== §CUTGAMES (2026-09-27): the three wreck minigames =================
   Pipes for a rich HOLD, rings for every MOUNT, the maze for the CORE.
   Sized to sit inside the salvage panel on a phone without scrolling. */
.drow .dname small .cgtag {
  display: inline-block; font-style: normal; font-weight: bold;
  color: #00f0ff; letter-spacing: 1px; font-size: var(--fs-xs);
}
.dbtn.gamebtn {
  min-width: 66px; background: rgba(0, 240, 255, .12);
  border-color: #00f0ff; color: #00f0ff;
  box-shadow: 0 0 8px rgba(0, 240, 255, .35);
}
.cutgame { display: flex; flex-direction: column; gap: 6px; margin: 6px 0 10px; }
.cghead { color: #ff6ac1; font-size: var(--fs-sm); letter-spacing: 2px; }
.cghead b { color: #00f0ff; text-shadow: 0 0 6px #00f0ff; }
.cghead span { color: #b9a7e8; letter-spacing: 1px; }
.cghow { color: #9a8fb5; font-size: var(--fs-xs); }
.cgtime { height: 6px; background: rgba(13, 2, 33, .85); border: 1px solid #3a2a56; }
.cgtime i { display: block; height: 100%; width: 100%; background: #ff6ac1; box-shadow: 0 0 8px rgba(255, 106, 193, .7); }
.cgtime i.low { background: #ff3864; box-shadow: 0 0 8px rgba(255, 56, 100, .9); }
.cgread { font-size: var(--fs-xs); font-weight: bold; letter-spacing: 1px; color: #ffd319; min-height: 1.3em; }
.cgread.good { color: #39ff14; }
.cgread.bad { color: #ff3864; }
.cgboard { display: flex; flex-direction: column; align-items: center; gap: 8px; }

/* pipes */
.pgrid { display: grid; gap: 4px; width: min(300px, 100%); align-items: center; }
.pport { color: #00f0ff; text-align: center; font-size: 12px; text-shadow: 0 0 6px #00f0ff; }
.pport.out { color: #ff6ac1; text-shadow: 0 0 6px #ff6ac1; }
.ptile {
  aspect-ratio: 1; padding: 0; cursor: pointer; touch-action: none;
  background: rgba(36, 23, 52, .9); border: 1px solid #3d1a6e; border-radius: 3px;
}
.ptile svg { width: 100%; height: 100%; display: block; transition: transform 120ms ease-out; }
.ptile path { fill: none; stroke: #5b4a86; stroke-width: 7; stroke-linecap: square; }
.ptile.lit path { stroke: #ff6ac1; filter: drop-shadow(0 0 3px #ff6ac1); }
.ptile.done path { stroke: #39ff14; filter: drop-shadow(0 0 4px #39ff14); }
.ptile:active { background: rgba(255, 106, 193, .14); }
@media (prefers-reduced-motion: reduce) { .ptile svg { transition: none; } }

/* §CUTGAMES-2: the core circuit (maze; replaced welds) */
.mgrid { display: grid; gap: 3px; width: min(330px, 100%); align-items: center; touch-action: none; user-select: none; -webkit-user-select: none; }
.mport { color: #00f0ff; text-align: center; font-size: 12px; text-shadow: 0 0 6px #00f0ff; }
.mport.out { color: #ff6ac1; text-shadow: 0 0 6px #ff6ac1; }
.mcell { aspect-ratio: 1; border-radius: 3px; }
.mcell.block {
  background: repeating-linear-gradient(45deg, #151021 0 4px, #1d1630 4px 8px);
  border: 1px solid #120c1c;
}
.mcell.node {
  background: rgba(36, 23, 52, .92); border: 1px solid #5b4a86; cursor: pointer;
  box-shadow: inset 0 0 0 3px rgba(13, 2, 33, .9), inset 0 0 0 5px rgba(122, 111, 168, .35);
}
.mcell.node.next { border-color: #00f0ff; box-shadow: inset 0 0 0 3px rgba(13, 2, 33, .9), inset 0 0 0 5px rgba(0, 240, 255, .45); }
.mcell.lit { background: #ff6ac1; border-color: #ffb3e0; box-shadow: 0 0 8px rgba(255, 106, 193, .75); }
.mcell.head { background: #fff0fa; box-shadow: 0 0 12px #ff6ac1; }
.mcell.done { background: #39ff14; border-color: #b6ff9e; box-shadow: 0 0 8px rgba(57, 255, 20, .75); }
.mcell.dead { background: #ff3864; border-color: #ff9ab0; box-shadow: 0 0 12px #ff3864; }

/* rings */
.rcanvas { width: min(240px, 70vw); aspect-ratio: 1; touch-action: none; cursor: pointer; }
.dbtn.cglock { min-width: 140px; touch-action: none; user-select: none; }
.dbtn.cglock small { opacity: .6; font-size: .8em; }


/* §LVLTAG, 2026-09-30: a part's level, on every name that carries one */
.lvtag {
  display: inline-block;
  margin-right: 4px;
  padding: 0 3px;
  font-size: 0.82em;
  font-weight: bold;
  letter-spacing: 0.5px;
  line-height: 1.25;
  color: #0d0221;
  background: #2de2e6;
  border-radius: 2px;
  vertical-align: 1px;
  font-variant-numeric: tabular-nums;
}
/* §Q%, 2026-10-04: a part's quality, Q50%..Q200%, beside its level tag --
   purple, its text running red -> white -> green -> amber with the number */
.qtag {
  display: inline-block;
  margin-right: 4px;
  padding: 0 3px;
  font-size: 0.82em;
  font-weight: bold;
  letter-spacing: 0.3px;
  line-height: 1.25;
  color: var(--qc, #fff);
  background: #6f4a85;
  border-radius: 2px;
  vertical-align: 1px;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 1px rgba(13, 2, 33, 0.6);
}


/* §HOLDBAR (2026-09-30): what is taking up the hold, as one bar */
.holdbarrow { display: block; padding: 2px 0 6px; border: none; background: none; }
.holdbar { display: flex; flex-direction: column; gap: 3px; }
.hbtrack {
  display: flex; height: 18px; width: 100%; overflow: hidden;
  background: rgba(13, 2, 33, .9); border: 1px solid #3a2a56; border-radius: 3px;
}
.holdbar.over .hbtrack { border-color: #ff3864; box-shadow: 0 0 6px rgba(255, 56, 100, .6); }
.hbseg {
  min-width: 0; height: 100%; padding: 0 3px; margin: 0; border: none; cursor: pointer;
  background: var(--c, #8a7ab0);
  background: color-mix(in srgb, var(--c, #8a7ab0) 72%, #0d0221);
  border-right: 1px solid rgba(13, 2, 33, .95);
  color: #0d0221; font: bold 9px/18px "Courier New", monospace; letter-spacing: .5px;
  text-align: left; overflow: hidden; white-space: nowrap;
}
.hbseg span { display: block; overflow: hidden; text-overflow: ellipsis; }
.hbseg:hover, .hbseg:focus-visible { background: var(--c, #8a7ab0); outline: none; }
.hbseg.more { background: repeating-linear-gradient(90deg, #5b4a86 0 3px, #4a3d6e 3px 6px); color: #d8ccf5; }
.hbfree { display: block; height: 100%; background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(122, 111, 168, .16) 4px 7px); }
.hbread { font-size: var(--fs-2xs, 10px); color: #b9a7e8; letter-spacing: .5px; min-height: 1.2em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* §HOLDSORT (2026-09-30): sort chips over the hold */
.hsort { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: 5px; }
.hsort span { font-size: var(--fs-2xs, 10px); color: #7a6fa8; letter-spacing: 1px; margin-right: 2px; }
.hsbtn {
  font: bold 10px/1 "Courier New", monospace; letter-spacing: 1px; padding: 4px 7px; cursor: pointer;
  color: #b9a7e8; background: rgba(36, 23, 52, .9); border: 1px solid #4a3d6e; border-radius: 3px;
}
.hsbtn:hover { border-color: #8a7ab0; color: #e6dcff; }
.hsbtn.on { color: #0d0221; background: #00f0ff; border-color: #00f0ff; box-shadow: 0 0 6px rgba(0, 240, 255, .5); }
.hsbtn:focus-visible { outline: 1px solid #ff6ac1; outline-offset: 1px; }
.ofsub .ofsort { color: #00f0ff; font-weight: bold; }

/* §MODFIND (2026-09-30): the slot type, everywhere a mod or a socket shows */
.slottag {
  display: inline-block; padding: 0 4px; border-radius: 2px; vertical-align: 1px;
  font: bold 9px/13px "Courier New", monospace; letter-spacing: .5px;
  color: #0d0221; background: var(--sc, #b8c2d6);
}
.mdfits { margin: 4px 0 6px; padding: 5px 7px; border-left: 2px solid rgba(255, 211, 25, .5); background: rgba(255, 211, 25, .05); }
.mdfits .mdfk { font-size: var(--fs-xs); color: #ffd97a; letter-spacing: 1px; }
.mdfits .mdfk small { color: #9a8fb5; letter-spacing: 0; }
.mdfits .mdfn { font-size: var(--fs-xs); color: #d8ccf5; margin-top: 2px; }
.mdfits .mdfn.none { color: #ff8a7a; }
.mdfits .mdfn small { color: #9a8fb5; }
.modshop { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 4px 2px 6px; font-size: var(--fs-xs); color: #d8ccf5; }
.modshop .dbtn { margin-left: auto; }
.ofm-sock .sockshop { margin-left: 4px; }

/* §COURSE (2026-10-01): COURSE | ENGAGE, and the course row in the menu */
#hudApRow > #apOpenBtn, #hudApRow > #apGoBtn { letter-spacing: 1px; flex: 0 0 calc(50% - 2px); white-space: nowrap; overflow: hidden; padding: 0 2px; }
#hudApRow .apl { white-space: nowrap; }
#hudApRow .apl i { font-style: normal; }
#hudApRow .apl .ln { display: none; }
/* §COURSE-2: the row's width varies with the radar, so the labels are FIT by
   measurement (fitApRow), one step at a time, only as far as each needs:
   fit1 drops the glyph and the letter-spacing, fit2 a smaller face,
   fit3 says STOP for DISENGAGE. */
#hudApRow > button.fit1 { letter-spacing: 0 !important; }
#hudApRow > button.fit1 .apl .apg { display: none; }
#hudApRow > button.fit2 { font-size: var(--fs-2xs, 10px) !important; }
#hudApRow > button.fit3 .apl .lw { display: none; }
#hudApRow > button.fit3 .apl .ln { display: inline; }
#hudApRow > #apGoBtn {
  color: #5dff9d; border: 2px solid #5dff9d; background: rgba(6, 40, 22, 0.75);
  box-shadow: 0 0 10px rgba(93, 255, 157, 0.35);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  line-height: 1.05; overflow: hidden; cursor: pointer;
}
#hudApRow > #apGoBtn small {
  display: block; max-width: 100%; font-size: 0.78em; letter-spacing: 0; opacity: .85;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 3px;
}
#hudApRow > #apGoBtn:disabled {
  color: rgba(93, 255, 157, 0.35); border-color: rgba(93, 255, 157, 0.25);
  box-shadow: none; cursor: default;
}
#hudApRow > #apGoBtn:not(:disabled):active { transform: translate(1px, 1px); }
#apCourseRow {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px;
  width: 100%; max-width: min(560px, 100%); padding: 6px 8px; box-sizing: border-box; align-self: center;
  border: 1px solid rgba(93, 255, 157, 0.45); background: rgba(6, 40, 22, 0.5);
}
#apCourseRow.hidden { display: none; }
#apCourseRow .apcname { color: #c9ffe0; font-size: var(--fs-sm); letter-spacing: 1px; margin-right: auto; }
#apCourseRow .apcname b { color: #5dff9d; }
#apCourseRow .apcname span { color: #ff6ac1; font-size: var(--fs-xs); margin-left: 6px; }
#apCourseRow button {
  font: inherit; font-weight: bold; font-size: var(--fs-xs); letter-spacing: 1px; padding: 7px 12px; cursor: pointer;
}
#apCourseRow .apcgo { color: #0d0221; background: #5dff9d; border: none; }
#apCourseRow .apcclr { color: #ff6ac1; background: rgba(255, 106, 193, 0.1); border: 1px solid #ff6ac1; }
#hudApRow > #apBtn { letter-spacing: 0; }
#hudApRow > #apBtn .apl .apg { display: none; }

/* §MODFX (2026-10-01): behaviour-mod meters on the weapon chips, and the
   CYCLE GOVERNOR dial at the bench */
#wpnBar .wchip[data-meter]::after {
  content: ''; position: absolute; left: 3px; bottom: 2px; height: 2px;
  width: calc((100% - 6px) * var(--wm, 0)); border-radius: 1px;
  background: #2de2e6; box-shadow: 0 0 4px #2de2e6;
}
#wpnBar .wchip[data-meter="heat"]::after { background: #ff9a3c; box-shadow: 0 0 4px #ff9a3c; }
#wpnBar .wchip[data-meter="vent"]::after { background: #ff3864; box-shadow: 0 0 6px #ff3864; }
#wpnBar .wchip[data-meter="vent"] { border-color: #ff3864; }
#wpnBar .wchip[data-meter="lock"]::after { background: #b8c2d6; box-shadow: none; }
#wpnBar .wchip[data-meter="locked"]::after { background: #5dff9d; box-shadow: 0 0 4px #5dff9d; }
.ofm-sock .moddial { display: inline-flex; align-items: center; gap: 4px; margin-left: 4px; flex-wrap: wrap; }
.ofm-sock .moddial .dialv { min-width: 64px; text-align: center; color: #ffd97a; font-size: var(--fs-xs); letter-spacing: 1px; }

/* ---------- §SELLFILTER: KEEP / USED / SELL tags and CLEAR HOLD ---------- */
.needtag {
  display: inline-block; padding: 0 4px; border-radius: 2px; vertical-align: 1px;
  font: bold 9px/13px "Courier New", monospace; letter-spacing: .5px;
  border: 1px solid currentColor;
}
.needtag.nt-keep { color: #0d0221; background: #ffd479; border-color: #ffd479; }
.needtag.nt-used { color: #2de2e6; }
.needtag.nt-sell { color: #8d86a6; }
.clrcats { display: flex; flex-direction: column; gap: 4px; margin: 6px 0; }
.clrcat {
  display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: center;
  padding: 6px 8px; border: 1px solid rgba(255, 255, 255, .12); border-radius: 3px; cursor: pointer;
  font-size: var(--fs-xs); letter-spacing: 1px;
}
.clrcat input { width: 18px; height: 18px; accent-color: #ffd319; margin: 0; }
.clrcat .clrnm small { display: block; opacity: .65; letter-spacing: .5px; font-size: var(--fs-2xs); }
.clrcat .clrv { color: #ffd319; white-space: nowrap; font-variant-numeric: tabular-nums; }
.clrcat.none { opacity: .45; cursor: default; }
.clrcat.sub { margin-left: 22px; }
.clrflt { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin: 6px 0 2px; font-size: var(--fs-xs); letter-spacing: 1px; }
.clrflt > span { color: #ffd479; }
.clrflt label { display: flex; align-items: center; gap: 6px; }
.clrflt select {
  font: inherit; letter-spacing: 1px; color: #e8e4ff; background: #160a2e;
  border: 1px solid rgba(255, 255, 255, .25); border-radius: 3px; padding: 3px 4px; min-height: 26px;
}
.clrkept { font-size: var(--fs-2xs); line-height: 1.5; }
.clrkept b { color: #ffd479; letter-spacing: 1px; }
.clrkept i { opacity: .6; font-style: normal; }
.dbtn.keepon { color: #0d0221; background: #ffd479; border-color: #ffd479; }

/* ---------- §PAIRS / §LOADOUT ---------- */
.pairs { margin: 6px 0 2px; padding: 5px 7px; border-left: 2px solid rgba(178, 140, 255, .5); background: rgba(178, 140, 255, .06); }
.pairs .mdrl { display: block; color: #c9b3ff; letter-spacing: 1px; font-size: var(--fs-2xs); margin-bottom: 2px; }
.pairrow { font-size: var(--fs-2xs); line-height: 1.45; opacity: .6; }
.pairrow b { color: #c9b3ff; letter-spacing: .5px; }
.pairrow.on { opacity: 1; }
.pairrow.on b { color: #ffd479; }
.fieldrefit { margin-top: 8px; border-top: 1px solid rgba(255, 255, 255, .1); padding-top: 6px; }
.fieldrefit > .mdrl { display: block; color: #2de2e6; letter-spacing: 1px; font-size: var(--fs-xs); margin-bottom: 4px; }
.fieldrefit .refitno { color: #ff6a6a; }
.fieldrefit small { opacity: .7; letter-spacing: .5px; }
.invpanel.loadouts .drow { align-items: center; gap: 6px; }
.invpanel.loadouts .worn { color: #7ce7a6; letter-spacing: 1px; }
.invpanel.loadouts .loname {
  width: 100%; box-sizing: border-box; background: rgba(0, 0, 0, .35); color: #e8e4ff;
  border: 1px solid rgba(255, 255, 255, .2); border-radius: 3px; padding: 6px 8px;
  font: inherit; font-size: var(--fs-xs); letter-spacing: 1px; text-transform: uppercase;
}

/* ---------- §COMMS: the comms log, left side, vertically centred ---------- */
#comms {
  position: absolute; left: calc(10px + env(safe-area-inset-left, 0px)); top: 50%;
  transform: translateY(-50%);
  width: min(320px, 34vw); max-height: 32vh;
  display: flex; flex-direction: column;
  /* §COMMS-4, Matt 2026-10-05: "reduce the opacity to 30%" (was 50%) */
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.10); border-radius: 4px;
  font: 11px/1.4 "Courier New", monospace; color: #d8d4ea;
  pointer-events: auto; z-index: 6;
}
#comms .commsHead {
  display: flex; align-items: center; gap: 8px; padding: 3px 4px 3px 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08); letter-spacing: 2px; font-size: 10px;
  color: #2de2e6; cursor: default; user-select: none;
}
#comms .commsNew { color: #ff6ac1; letter-spacing: 1px; flex: 1; }
/* §COMMS-4: a window's title-bar buttons -- FOLLOW, minimise, maximise */
#comms .commsBtn {
  width: 26px; height: 22px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 3px;
  background: rgba(0, 0, 0, 0.35); color: #e8e4ff; cursor: pointer; flex: none;
}
#comms .commsBtn svg { width: 14px; height: 14px; }
#comms .commsBtn:hover, #comms .commsBtn:focus-visible { border-color: rgba(45, 226, 230, 0.6); color: #2de2e6; outline: none; }
#comms .commsFollow { color: #9a92b5; }
#comms .commsFollow.on { color: #0d0221; background: #2de2e6; border-color: #2de2e6; }
#comms.min .commsFollow { display: none; }
#comms .commsLog {
  overflow-y: auto; padding: 4px 8px 6px; min-height: 0;
  scrollbar-width: thin; scrollbar-color: rgba(45, 226, 230, 0.5) transparent;
  overscroll-behavior: contain;
}
#comms .commsLog::-webkit-scrollbar { width: 6px; }
#comms .commsLog::-webkit-scrollbar-thumb { background: rgba(45, 226, 230, 0.45); border-radius: 3px; }
#comms .commsLog:empty::before { content: 'comms quiet'; opacity: 0.45; letter-spacing: 1px; }
#comms .cl { padding: 2px 0; border-left: 2px solid transparent; padding-left: 5px; margin-left: -7px; transition: background .6s; word-wrap: break-word; }
#comms .cl.fresh { background: rgba(255, 255, 255, 0.08); border-left-color: var(--kc); }
#comms .clt { opacity: 0.5; }
#comms .clk { color: var(--kc); letter-spacing: 1px; }
#comms .cco {
  color: #ffd319; text-decoration: none; border-bottom: 1px dotted rgba(255, 211, 25, 0.7);
  white-space: nowrap; cursor: pointer; padding: 2px 0;
}
#comms .cco:hover, #comms .cco:focus-visible { color: #fff27a; outline: none; border-bottom-style: solid; }
/* §MSG, 2026-10-04: the expand button, and the tall view over the stick */
#comms .commsHead { cursor: default; gap: 4px; }
#comms.min .commsHead { cursor: pointer; }
#comms.open {
  z-index: 40; background: rgba(4, 0, 14, 0.86);
  border-color: rgba(45, 226, 230, 0.35);
  box-shadow: 0 0 18px rgba(0, 0, 0, 0.6);
}
#comms.min { max-height: none; width: auto; }
#comms.min .commsLog { display: none; }
#comms.min .commsHead { border-bottom: none; }
/* §COMMS-2: on a phone the box is placed and sized by Game.commsFit (it
   measures the gap between the left column and the stick); this is just the
   smaller type that suits it */
#comms.phone { font-size: 9px; line-height: 1.3; }
#comms.phone .commsHead { padding: 1px 3px 1px 6px; font-size: 9px; }
#comms.phone .commsBtn { width: 24px; height: 20px; }
#comms.phone .commsBtn svg { width: 12px; height: 12px; }
#comms.phone .commsLog { padding: 2px 6px 4px; }
/* §COMMS-3: a live link follows something that moves */
#comms .cco.live { color: #7ce7a6; border-bottom-color: rgba(124, 231, 166, 0.7); }
#comms .cgone { opacity: 0.45; }

/* §HEAT (2026-10-02): the lock ring that replaced the blade bar. Eight pads
   round a ring; the lit one burns amber and drains as its window closes. */
.hwrap { position: relative; width: min(300px, 82vw); aspect-ratio: 1; touch-action: none; user-select: none; -webkit-user-select: none; }
.hcanvas { position: absolute; left: 19%; top: 19%; width: 62%; height: 62%; pointer-events: none; }
.hpad {
  position: absolute; width: 19%; aspect-ratio: 1; transform: translate(-50%, -50%);
  padding: 0; cursor: pointer; touch-action: none; overflow: hidden;
  background: rgba(27, 30, 42, .92); border: 1px solid #3d3a5c; border-radius: 50%;
  box-shadow: inset 0 0 0 3px rgba(13, 2, 33, .9), inset 0 0 0 5px rgba(122, 111, 168, .25);
  transition: background 80ms, border-color 80ms, box-shadow 80ms;
}
.hpad i {
  position: absolute; inset: 14%; border-radius: 50%; transform: scale(0);
  background: radial-gradient(circle, #fff3c4 0%, #ffb040 55%, rgba(255, 110, 30, .2) 100%);
}
.hpad small { position: relative; color: #6c6890; font-size: 10px; font-weight: bold; letter-spacing: 1px; }
.hpad.lit { border-color: #ffb040; background: rgba(80, 40, 10, .95); box-shadow: 0 0 14px rgba(255, 150, 40, .85), inset 0 0 0 3px rgba(13, 2, 33, .6); }
.hpad.lit small { color: #10131c; }
.hpad.hit { border-color: #39ff14; box-shadow: 0 0 12px rgba(57, 255, 20, .8); }
.hpad.miss { border-color: #ff3864; background: rgba(70, 10, 24, .95); box-shadow: 0 0 12px rgba(255, 56, 100, .85); }
@media (prefers-reduced-motion: reduce) { .hpad { transition: none; } }
/* short screens (a phone on its side): the ring sits beside the readout so
   all eight pads are on screen at once -- this one is played against the clock */
@media (max-height: 520px) {
  .cutgame.cg-heat { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 14px; align-content: start; }
  .cutgame.cg-heat > :not(.cgboard) { grid-column: 1; }
  .cutgame.cg-heat > .cgboard { grid-column: 2; grid-row: 1 / span 6; }
  .cg-heat .hwrap { width: min(300px, calc(100dvh - 135px)); min-width: 190px; }
}
/* §PARLEY: an answerable comms line */
#comms .cco.talk { color: #ffb347; border-bottom-color: rgba(255, 179, 71, 0.7); }
/* §PARLEY: a phone on its side cannot stack four or five long options --
   two columns, so OPEN FIRE is not hidden under the fold */
@media (max-height: 480px) and (min-width: 600px) {
  #hailOpts { display: grid; grid-template-columns: 1fr 1fr; max-width: 860px; gap: 8px; }
}

/* §TALK (2026-10-03): who you are talking to, and how they feel about it */
#hailMood { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 6px 14px;
  font-size: var(--fs-xs); letter-spacing: 1px; color: #b9a7e8; max-width: 640px; }
#hailMood.hidden { display: none; }
#hailMood .hmCap { color: #f6f7f6; font-weight: bold; letter-spacing: 2px; }
#hailMood .hmCap i { font-style: normal; font-weight: normal; color: #2de2e6; border: 1px solid rgba(45, 226, 230, .45);
  padding: 0 5px; margin-left: 4px; border-radius: 2px; }
#hailMood .hmCap i.hmKnown { color: #ffd319; border-color: rgba(255, 211, 25, .45); }
#hailMood .hmMood { display: inline-flex; align-items: center; gap: 6px; color: var(--mc); }
#hailMood .hmMood u { display: inline-block; width: 90px; height: 6px; background: rgba(13, 2, 33, .85); border: 1px solid #3a2a56; text-decoration: none; }
#hailMood .hmMood s { display: block; height: 100%; background: var(--mc); box-shadow: 0 0 6px var(--mc); text-decoration: none; }
#hailMood .hmWhy { color: #9a8fb5; font-style: italic; }

/* §HULLTIER (2026-10-03): ship tiers at the yard -- a tag on every hull, a
   heading per tier, and the next tier up from yours lit */
.lvtag.tiertag { background: #be96ff; }
.ofsplit.nextup { color: #39ff14; border-top-color: rgba(57, 255, 20, .5); font-weight: bold; }
.oftile.k-hull.nextup { border-color: rgba(57, 255, 20, .75); box-shadow: 0 0 8px rgba(57, 255, 20, .35); }
.oftile.k-hull.below { opacity: .6; }
.nexttier { color: #39ff14; letter-spacing: 1px; }
.nexttier b { color: #f6f7f6; }

/* §TITLE2 (2026-10-03): the title buttons in pairs. One column, each pair a
   row of two equal buttons; a pair whose buttons are all hidden takes no
   room, and a lone visible button fills its row. */
#title .tpair { display: flex; gap: 10px; width: min(480px, 100%); }
#title .tpair > button.big { flex: 1 1 0; min-width: 0; margin: 0; }
#title .tpair:not(:has(> :not(.hidden))) { display: none; }
#title > button.big.tfull { width: min(480px, 100%); min-width: 0; box-sizing: border-box; }
#title .contline { margin: 0; }
#title .tpair > button.big { box-sizing: border-box; font-size: var(--fs-md); padding-left: 10px; padding-right: 10px; }
@media (min-width: 600px) { #title .tpair > button.big { white-space: nowrap; } }
/* §TITLE2b: with a save to continue, CONTINUE is the one bright button --
   NEW GAME steps back to the secondary style beside SAVE SLOTS */
#title #continueBtn:not(.hidden) + .tpair #startBtn { background: #241734; color: #05d9e8; border: 1px solid #05d9e8; }

/* §HULLART, 2026-10-04: the hull itself, big, at the top of its details */
#merchPop .mdart {
  display: flex; align-items: center; justify-content: center;
  margin: 6px 0 8px; padding: 10px;
  height: clamp(150px, 30vh, 260px);
  background: radial-gradient(ellipse at center, rgba(45, 226, 230, 0.10), rgba(13, 2, 33, 0.85) 70%),
              repeating-linear-gradient(0deg, rgba(45, 226, 230, 0.05) 0 1px, transparent 1px 24px),
              repeating-linear-gradient(90deg, rgba(45, 226, 230, 0.05) 0 1px, transparent 1px 24px);
  border: 1px solid rgba(45, 226, 230, 0.25);
  border-radius: 3px;
}
#merchPop .mdart img {
  max-width: 100%; max-height: 100%; object-fit: contain;
  image-rendering: pixelated;
  filter: drop-shadow(0 0 10px rgba(45, 226, 230, 0.25));
}

/* §PINBUY (2026-10-04): your pins across the top of every counter, with a BUY
   for whatever this counter sells that a pin is short of. Same card as the
   HUD's pin panel, laid out in a row. */
#pinStrip { margin: 0 0 10px; font-size: var(--fs-2xs); letter-spacing: 1px; text-align: left; }
#pinStrip .pshead { color: #ffd319; font-weight: bold; margin-bottom: 4px; }
#pinStrip .pshead i { font-style: normal; font-weight: normal; color: #9a92b5; }
#pinStrip .psrow { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px; }
#pinStrip .pin { flex: 0 0 210px; background: rgba(13, 2, 33, .72); border: 1px solid #3d1a6e; border-left: 3px solid #ffd319; padding: 4px 6px 6px; }
#pinStrip .pin.ready { border-left-color: #39ff14; }
#pinStrip .pinhead { display: flex; justify-content: space-between; align-items: center; gap: 6px; color: #ffd319; font-weight: bold; }
#pinStrip .pin.ready .pinhead { color: #39ff14; }
#pinStrip .pinhead i { font-style: normal; color: #7a6fa8; font-weight: normal; }
#pinStrip .pinx { background: none; border: 0; color: #7a6fa8; font-family: inherit; font-size: var(--fs-md); line-height: 1; padding: 0 2px; cursor: pointer; }
#pinStrip .pinx:hover { color: #ff3864; }
#pinStrip .pinrow { display: flex; justify-content: space-between; color: #c9bde6; padding-top: 2px; }
#pinStrip .pinrow b { color: #ff3864; font-variant-numeric: tabular-nums; }
#pinStrip .pinrow.ok b { color: #39ff14; }
#pinStrip .pinrow b em { font-style: normal; color: #2de2e6; }
#pinStrip .psbuy { margin-top: 6px; width: 100%; background: rgba(45, 226, 230, .14); border: 1px solid #2de2e6; color: #2de2e6; font-family: inherit; font-size: var(--fs-2xs); font-weight: bold; letter-spacing: 1px; padding: 5px 6px; cursor: pointer; }
#pinStrip .psbuy:disabled { opacity: .45; cursor: default; }
#pinStrip .psbuy:not(:disabled):hover { background: rgba(45, 226, 230, .28); }
#pinStrip .psready { margin-top: 5px; color: #39ff14; font-weight: bold; }
#pinStrip .psmiss { margin-top: 4px; color: #7a6fa8; }

/* §SAVESYNC: which game to keep */
#syncConflict .syncopts { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; }
#syncConflict .syncopt { display: flex; flex-direction: column; gap: 4px; min-width: 240px; max-width: 340px; padding: 12px 16px; text-align: left; }
#syncConflict .syncopt b { font-size: var(--fs-md); }
#syncConflict .syncopt small { font-size: var(--fs-2xs); letter-spacing: 1px; opacity: .85; font-weight: normal; }
#syncConflict .how { max-width: 620px; }

/* §HUDICONS (2026-10-05): the right-hand cluster as icons. One 24-unit grid,
   2-unit round strokes in each button's own colour; the words live in the
   tooltip and aria-label. The bay is a fourth button in the MENU/MAP/INV row. */
.icobtn { display: flex; align-items: center; justify-content: center; }
.icobtn .bico { width: 20px; height: 20px; display: block; flex: 0 0 auto; filter: drop-shadow(0 0 3px currentColor); }
#hudApRow > .icobtn .bico { width: 19px; height: 19px; }
#hudBtnRow > #bayBtn {
  position: relative; left: auto; top: auto; width: auto; height: 30px;
  flex: 1 1 0; min-width: 0; padding: 0; overflow: visible;
}
#hudBtnRow > #bayBtn.hidden { display: none; }
#bayBtn .hbadge {
  position: absolute; top: -6px; right: -4px; min-width: 15px; height: 15px; padding: 0 3px;
  border-radius: 8px; background: #b967ff; color: #0d0221; font-size: 10px; line-height: 15px;
  font-weight: bold; letter-spacing: 0; text-align: center; box-shadow: 0 0 6px rgba(185, 103, 255, .7);
}
/* §BAYSHORT: the line is waiting on materials -- small and still, on the corner */
#bayBtn { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; touch-action: none; }   /* §DECKBTN: long press */
#bayBtn .hshort {
  position: absolute; top: -8px; right: -1px; color: #ff3b5c; font-size: 14px; line-height: 14px;
  font-weight: bold; letter-spacing: 0; pointer-events: none; text-shadow: 0 0 2px #0d0221, 0 0 2px #0d0221, 0 0 2px #0d0221;
}
#bayBtn .hbadge ~ .hshort { right: auto; left: -1px; }   /* the count badge owns top-right */
#hudApRow > #apGoBtn:disabled .bico { filter: none; opacity: .55; }

/* ---------- §WPNCTL: one WEAPONS button, a gear, a radial, a panel ----------
   Matt, 2026-10-05: "a single red circle button that says WEAPONS in white
   text and then either Armed or Disarmed ... along with the background of the
   button to clearly show the state ... Next to it, make a smaller circle
   button that is a gear icon". Red, not the OPEN FIRE pink beside it: this
   one is a switch you leave set, that one is the act. The chip row is kept
   for the per-frame fire pass and the developer's equip mode, undrawn. */
#wpnBar:not(.devequip) { display: none; }

#wpnCtl {
  position: absolute;
  right: 18px;
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: none;
  z-index: 6;
}
#wpnCtl button {
  pointer-events: auto;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  font-family: inherit;
  cursor: pointer;
  padding: 0;
}
.wcMain {
  width: 74px;
  height: 74px;
  border-radius: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  color: #fff;
  background: rgba(40, 8, 18, 0.82);
  border: 3px solid #ff2a3d;
  box-shadow: 0 0 8px rgba(255, 42, 61, 0.35);
  transition: background .15s, box-shadow .15s;
}
.wcMain b { font-size: 11px; letter-spacing: 1px; font-weight: bold; }
.wcMain small { font-size: 9.5px; letter-spacing: .5px; color: #ff9aa5; }
.wcMain.armed {
  background: #e3142c;
  border-color: #ff5a68;
  box-shadow: 0 0 16px rgba(255, 42, 61, 0.75), inset 0 0 10px rgba(255, 255, 255, .18);
}
.wcMain.armed small { color: #fff; font-weight: bold; }
.wcMain.none { opacity: .55; }
.wcMain.tapping, .wcGear.tapping { transform: scale(0.94); }
.wcGear {
  position: relative;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #ff6b78;
  background: rgba(36, 23, 52, 0.8);
  border: 2px solid #ff2a3d;
  box-shadow: 0 0 8px rgba(255, 42, 61, 0.35);
}
.wcGear .bico { width: 24px; height: 24px; }
.wcWarn {
  position: absolute;
  top: -2px;
  right: -2px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: #ff8c42;
  box-shadow: 0 0 6px #ff8c42;
  border: 2px solid #0d0221;
}
body.compacthud .wcMain { width: 64px; height: 64px; }
body.compacthud .wcMain b { font-size: 10px; letter-spacing: .5px; }
body.compacthud .wcMain small { font-size: 8.5px; }
body.compacthud .wcGear { width: 38px; height: 38px; }

/* the radial: a fan of configurations off the button, held or tapped */
#wpnRadial {
  position: absolute;
  inset: 0;
  z-index: 30;
  pointer-events: auto;
  background: radial-gradient(circle at 80% 70%, rgba(13, 2, 33, .55), rgba(13, 2, 33, .25));
  touch-action: none;
}
#wpnRadial .wrHub {
  position: absolute;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: #ff2a3d;
  box-shadow: 0 0 12px #ff2a3d;
}
#wpnRadial .wrOpt {
  position: absolute;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  font-family: inherit;
  color: #fff;
  background: rgba(40, 8, 18, 0.92);
  border: 2px solid #ff2a3d;
  box-shadow: 0 0 10px rgba(255, 42, 61, .4);
  pointer-events: auto;
  animation: wrIn .14s ease-out;
}
#wpnRadial .wrOpt b {
  font-size: 9.5px;
  letter-spacing: .5px;
  line-height: 1.15;
  overflow-wrap: anywhere;
}
#wpnRadial .wrOpt.wr-safe { border-color: #9a92b5; }
#wpnRadial .wrOpt.cur { background: #e3142c; border-color: #ff5a68; }
#wpnRadial .wrOpt.wr-safe.cur { background: #3a3352; border-color: #c8c2dd; }
#wpnRadial .wrOpt.hit { transform: scale(1.14); box-shadow: 0 0 18px #ffd319; border-color: #ffd319; }
@keyframes wrIn { from { transform: scale(.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { #wpnRadial .wrOpt { animation: none; } }

/* the panel (pauses the game) */
#wpnMenu { justify-content: flex-start; padding-top: calc(14px + env(safe-area-inset-top, 0px)); }
#wpnMenu .wmPanel {
  width: min(560px, calc(var(--menuw, 100vw) - 24px));
  display: flex;
  flex-direction: column;
  gap: 10px;
  text-align: left;
}
#wpnMenu h2 { margin: 0; text-align: center; color: #ff4d5e; letter-spacing: 4px; }
#wpnMenu h3 { margin: 8px 0 0; font-size: var(--fs-sm); letter-spacing: 2px; color: #ff9aa5; }
#wpnMenu .wmHint { margin: 0; font-size: var(--fs-2xs); color: #9a92b5; line-height: 1.4; text-align: center; }
#wpnMenu .wmHint b { color: #f6f7f6; }
#wpnMenu .wmAll { display: flex; gap: 8px; justify-content: center; }
#wpnMenu .wmList { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 6px; }
#wpnMenu .wmB {
  font-family: inherit;
  font-size: var(--fs-2xs);
  letter-spacing: 1px;
  padding: 8px 12px;
  color: #f6f7f6;
  background: rgba(36, 23, 52, .85);
  border: 1px solid #9a92b5;
  border-radius: 4px;
  cursor: pointer;
}
#wpnMenu .wmB.go { border-color: #ff2a3d; color: #ffd6da; }
#wpnMenu .wmB.bad { border-color: #ff8c42; color: #ffb27a; }
#wpnMenu .wmGun {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 46px;
  padding: 6px 10px;
  font-family: inherit;
  text-align: left;
  color: #9a92b5;
  background: rgba(20, 10, 34, .9);
  border: 1px solid #3a3352;
  border-radius: 4px;
  cursor: pointer;
}
#wpnMenu .wmGun .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--wc, #ff2a3d); flex: none; opacity: .35; }
#wpnMenu .wmGun.on { color: #f6f7f6; border-color: #ff2a3d; }
#wpnMenu .wmGun.on .dot { opacity: 1; box-shadow: 0 0 6px var(--wc, #ff2a3d); }
#wpnMenu .wmN { font-size: var(--fs-2xs); color: #9a92b5; width: 1.4em; text-align: right; flex: none; font-variant-numeric: tabular-nums; }
#wpnMenu .wmName { flex: 1; font-size: var(--fs-xs); letter-spacing: .5px; min-width: 0; }
#wpnMenu .wmName small { display: block; font-size: 10.5px; color: #9a92b5; letter-spacing: .5px; margin-top: 2px; }
#wpnMenu .wmGun em { font-style: normal; font-size: 10.5px; color: #5dff9d; letter-spacing: .5px; }
#wpnMenu .wmGun em.bad { color: #ff8c42; }
#wpnMenu .wmSw {
  flex: none;
  min-width: 64px;
  text-align: center;
  font-size: 10.5px;
  letter-spacing: 1px;
  padding: 5px 6px;
  border-radius: 12px;
  color: #c8c2dd;
  background: #2a2340;
}
#wpnMenu .wmGun.on .wmSw { color: #fff; background: #e3142c; font-weight: bold; }
#wpnMenu .wmSets { display: flex; flex-direction: column; gap: 6px; }
#wpnMenu .wmSet {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 6px 10px;
  background: rgba(20, 10, 34, .9);
  border: 1px solid #3a3352;
  border-radius: 4px;
}
#wpnMenu .wmSet.on { border-color: #ff2a3d; }
#wpnMenu .wmSetName { flex: 1; min-width: 120px; font-size: var(--fs-xs); letter-spacing: 1px; color: #f6f7f6; }
#wpnMenu .wmSetName small { display: block; font-size: 10.5px; color: #ff9aa5; margin-top: 2px; }
#wpnMenu .wmBtns { display: flex; gap: 6px; flex-wrap: wrap; }
#wpnMenu .wmBtns .wmB { padding: 6px 10px; }
#wpnMenu .wmNew { display: flex; gap: 8px; }
#wpnMenu .wmIn {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: var(--fs-xs);
  letter-spacing: 1px;
  text-transform: uppercase;
  padding: 8px 10px;
  color: #f6f7f6;
  background: #120a22;
  border: 1px solid #9a92b5;
  border-radius: 4px;
}
#wpnMenu .wmIn:focus, #wpnMenu .wmB:focus-visible, #wpnMenu .wmGun:focus-visible { outline: 2px solid #ffd319; outline-offset: 1px; }
#wpnMenu #wmCloseBtn { align-self: center; margin: 4px 0 16px; }

/* ---------- §SIGILKEYS: the ten sigils as one row of keys ----------
   Matt, 2026-10-05: "left to right increasing we will show the icon for each
   greyed out and then filled in once its earned. clicking the icon starts
   the hunt. the name is below the icon." Ten across, five across when the
   column is narrow. Grey line work until earned; gold, filled and lit after.
   The one being hunted wears the hunt trail's pink, breathing. */
.invpanel.sigils { container-type: inline-size; }
.sgGrid {
  display: grid;
  grid-template-columns: repeat(10, minmax(0, 1fr));
  gap: 8px 4px;
  padding: 8px 4px 4px;
}
@container (max-width: 560px) { .sgGrid { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.sgl {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 2px 0;
  min-width: 0;
  font-family: inherit;
  background: none;
  border: 0;
  color: #6b6484;
  cursor: pointer;
}
.sgl .sgIco {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid #3a3352;
  background: rgba(20, 10, 34, .7);
  transition: border-color .15s, box-shadow .15s, transform .1s;
}
.sgl .sgsvg { width: 28px; height: 28px; }
.sgl .sgNm {
  font-size: 9px;
  letter-spacing: 0;
  line-height: 1.15;
  text-align: center;
  overflow-wrap: normal;
  max-width: 100%;
  color: #6b6484;
}
.sgl:hover .sgIco, .sgl:focus-visible .sgIco { border-color: #9a92b5; }
.sgl:focus-visible { outline: none; }
.sgl:focus-visible .sgIco { outline: 2px solid #ffd319; outline-offset: 2px; }
.sgl:active .sgIco { transform: scale(.94); }
.sgl.earned { color: #ffd479; }
.sgl.earned .sgIco {
  border-color: #ffd479;
  background: rgba(255, 212, 121, .1);
  box-shadow: 0 0 10px rgba(255, 212, 121, .45);
}
.sgl.earned .f { fill: rgba(255, 212, 121, .35); }
.sgl.earned .sgNm { color: #ffd479; }
.sgl.away { opacity: .45; }
.sgl.tracking { color: #ff6ac1; }
.sgl.tracking .sgNm { color: #ff6ac1; }
.sgl.tracking .sgIco { border-color: #ff6ac1; animation: sgHunt 1.6s ease-in-out infinite; }
@keyframes sgHunt { 0%, 100% { box-shadow: 0 0 4px rgba(255, 106, 193, .35); } 50% { box-shadow: 0 0 14px rgba(255, 106, 193, .8); } }
@media (prefers-reduced-motion: reduce) { .sgl.tracking .sgIco { animation: none; box-shadow: 0 0 10px rgba(255, 106, 193, .6); } }

/* §CLAIMFIND-2: a station's local chart, under the pins */
#chartStrip { margin: 0 0 10px; font-size: var(--fs-2xs); letter-spacing: 1px; text-align: left; }
#chartStrip .cshead { color: #2de2e6; font-weight: bold; margin-bottom: 4px; }
#chartStrip .cshead i { font-style: normal; font-weight: normal; color: #9a92b5; }
#chartStrip .csrow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 6px 8px;
  background: rgba(13, 2, 33, .72); border: 1px solid #3d1a6e; border-left: 3px solid #2de2e6; color: #d8d4ea; }
#chartStrip .csrow span { flex: 1 1 220px; }
#chartStrip .csbuy { font-family: inherit; font-size: var(--fs-2xs); letter-spacing: 1px; padding: 6px 10px; color: #0d0221;
  background: #2de2e6; border: 0; border-radius: 3px; cursor: pointer; }
#chartStrip .csbuy:disabled { opacity: .45; cursor: default; }

/* ---------- §GEARROWS: one row per mount, spares that fit it, ▲ badges ---------- */
.ofsub .ofup { color: #0d0221; background: #7CE7A6; padding: 0 4px; border-radius: 2px; font-weight: bold; letter-spacing: 0; }
.gshost { width: 100%; min-width: 0; }
.gsrows { display: flex; flex-direction: column; gap: 4px; width: 100%; min-width: 0; }
.gsgrp { margin: 8px 0 2px; font-size: var(--fs-2xs); letter-spacing: 2px; color: #8d86a6; }
.gsgrp:first-child { margin-top: 2px; }
.gsrow { border: 1px solid rgba(120, 170, 230, .22); border-left: 3px solid var(--cc, #2de2e6); border-radius: 3px; background: rgba(10, 6, 30, .55); }
.gsrow.open { border-color: rgba(255, 106, 193, .55); border-left-color: var(--cc, #2de2e6); background: rgba(255, 106, 193, .07); }
.gsrow.empty .gsnm i { opacity: .55; }
.gshead, .gscand { display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0; text-align: left; font: inherit; font-size: var(--fs-xs); color: #e8e4ff; }
.gshead { background: none; border: 0; padding: 6px 8px; cursor: pointer; letter-spacing: .5px; }
.gshead:hover { background: rgba(120, 170, 230, .10); }
.gslab { flex: 0 0 auto; min-width: 54px; font-size: var(--fs-2xs); letter-spacing: 1px; color: var(--cc, #2de2e6); }
.gsico { flex: 0 0 auto; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; }
.gsico img, .gsico canvas, .gsico svg { max-width: 20px; max-height: 20px; }
.gsnm { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; line-height: 1.25; }
.gsnm small { opacity: .6; }
.gsval { flex: 0 0 auto; color: #2de2e6; font-variant-numeric: tabular-nums; white-space: nowrap; }
.gscond { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
.gscond.ok { color: #7CE7A6; } .gscond.worn { color: #FFC46B; } .gscond.bad { color: #ff8a7a; }
.gsup { flex: 0 0 auto; color: #0d0221; background: #7CE7A6; border-radius: 2px; padding: 0 5px; font-size: var(--fs-2xs); }
.gscaret { flex: 0 0 auto; opacity: .6; }
.gsbody { display: flex; flex-direction: column; gap: 3px; padding: 2px 8px 8px; }
.gsmore { align-self: flex-start; margin: 2px 0 4px; padding: 3px 10px !important; font-size: var(--fs-2xs) !important; letter-spacing: 1px; }
.gscand { padding: 4px 6px; border-radius: 3px; background: rgba(255, 255, 255, .03); }
.gscand.up { background: rgba(124, 231, 166, .09); }
.gscand.other .gsnm, .gscand.other .gsval { opacity: .7; }
.gsdelta { flex: 0 0 auto; min-width: 0; font-variant-numeric: tabular-nums; white-space: nowrap; }
.gsdelta.pos { color: #7CE7A6; } .gsdelta.neg { color: #ff8a7a; }
.gsswap { flex: 0 0 auto; max-width: 46%; white-space: normal; padding: 3px 12px !important; font-size: var(--fs-xs) !important; min-height: 0 !important; }
.gsswap:disabled { font-size: var(--fs-2xs); opacity: .7; }
.gsnone { font-size: var(--fs-2xs); opacity: .7; padding: 4px 0; }
.gsall { align-self: flex-start; background: none; border: 0; color: #2de2e6; font: inherit; font-size: var(--fs-2xs); text-decoration: underline; cursor: pointer; padding: 2px 0; }
@media (max-width: 600px) {
  .gshead, .gscand { flex-wrap: wrap; row-gap: 3px; }
  .gsnm { flex-basis: 50%; }
  .gscand .gsswap { margin-left: auto; }
}
/* §GEARROWS-2: details in place, and the difference against what is fitted */
.gsdet { padding: 2px 4px 8px 26px; }
.gsdetin { display: flex; flex-direction: column; gap: 6px; font-size: var(--fs-xs); }
.gsdetin .mddesc { margin: 0; }
.gscand { cursor: pointer; }
.gscand.det { background: rgba(45, 226, 230, .08); }
/* §GSFITTED, 2026-10-13. Matt: "lets highlight the row of the fitted item. I
   think it blends in too much so it can be confusing when you select a row to
   know what is actually fitted vs. the items that *could* be fitted." The
   fitted row is now plainly the one that is IN the slot: a solid cyan frame
   with a bright left bar and a tinted fill, and an outlined FITTED chip. */
.gscand.fitted { border: 1px solid rgba(45, 226, 230, .6); border-left: 4px solid #2de2e6;
  background: rgba(45, 226, 230, .13); box-shadow: inset 0 0 12px rgba(45, 226, 230, .12); padding-left: 4px; }
.gscand.fitted.det { background: rgba(45, 226, 230, .19); }
.gscand.fitted .gsnm { color: #fff; }
.gscand.fitted .gsfit { color: #2de2e6; border: 1px solid #2de2e6; border-radius: 2px; padding: 1px 6px; background: rgba(45, 226, 230, .12); }
.gsfit { flex: 0 0 auto; font-size: var(--fs-2xs); letter-spacing: 1px; color: #8d86a6; margin-left: auto; }
.gssub { margin: 6px 0 1px; font-size: var(--fs-2xs); letter-spacing: 2px; color: #8d86a6; background: none; border: 0; padding: 0; text-align: left; font-family: inherit; }
button.gssub.gsshop { cursor: pointer; color: #ffd319; }
.gscand.shop.cant .gsnm { opacity: .6; }
.gsdiff { display: flex; flex-direction: column; gap: 1px; font-variant-numeric: tabular-nums; }
.gsdl { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); gap: 6px; padding: 1px 4px; align-items: baseline; }
.gsdl .gsdk { opacity: .6; letter-spacing: .04em; font-size: var(--fs-2xs); }
.gsdl .gsdv { text-align: right; }
.gsdl .gsdd { font-weight: bold; white-space: nowrap; }
.gsdl .gsdw { opacity: .5; font-size: var(--fs-2xs); text-align: right; }
.gsdl.good .gsdd, .gsdl.good .gsdv { color: #7CE7A6; }
.gsdl.bad .gsdd, .gsdl.bad .gsdv { color: #ff8a7a; }
.gsdl.chg .gsdd { color: #FFC46B; }
.gsdl.same { opacity: .55; }
.gsacts { flex-wrap: wrap; }
@media (max-width: 600px) { .gsdet { padding-left: 4px; } .gsdl { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, 1fr); } .gsdl .gsdw { display: none; } }

/* the jump button: a harder zone's name in its own dark tag, in its colour */
#jumpBtn .jzone { background: #0d0221; padding: 1px 6px; border-radius: 2px; text-shadow: none; }

/* §TITLEFIT, 2026-10-11. Matt, launching from his root site in app mode: "the
   logo is cut off." A flex column centred with justify-content:center pushes an
   over-tall menu up past the top of its scroll box, where no scrolling can reach
   it -- so on a short window the top of the logo was simply gone. `safe center`
   centres while it fits and starts at the top when it does not (browsers that
   do not know the keyword keep plain center). The logo also never takes more
   than ~29% of the window's height now, and the top padding clears a notch or
   a translucent status bar in app mode. */
.overlay { justify-content: safe center; padding-top: max(20px, env(safe-area-inset-top, 0px)); }
@media (max-height: 480px) { #title { justify-content: safe center; } }
.gamelogo { width: min(560px, 78vw, 80vh); }

/* §MODAUDIT: what a mod does in the part it sits in */
.mfx { display: inline; font-size: var(--fs-xs); white-space: normal; }
.mfx + .mfx::before { content: ' · '; color: #6f6690; }
.mfx.good { color: #9fe6a8; }
.mfx.bad { color: #ff9b8a; }
.mfx.free { color: #ffd319; }
.ofm-modfx { font-size: var(--fs-xs); line-height: 1.35; margin: -2px 0 6px 2px; }
.ofm-modfx small { color: #9a8fb5; }
.mdfits .mdfn.mdfx { margin-left: 8px; line-height: 1.35; }

/* ---------- §FACTORY: the gear floor's picker ---------- */
.facpick {
  font: inherit; letter-spacing: 1px; color: #e8e4ff; background: #160a2e; max-width: 100%;
  border: 1px solid rgba(255, 255, 255, .25); border-radius: 3px; padding: 4px 6px; min-height: 30px; min-width: 240px;
}
.facpick optgroup { color: #ffd479; }

/* §MKTAVG: price against the galaxy average */
.vsavg { font-size: 0.92em; letter-spacing: 0.02em; white-space: nowrap; }
.vsavg.good { color: #7CE7A6; }
.vsavg.bad  { color: #ff8a7a; }
.vsavg.even { color: #9fb6d4; opacity: 0.8; }

/* §HOLDGROUP: the hold in sections, mods in stacks */
.ofside .hgrp { grid-column: 1 / -1; display: flex; align-items: baseline; gap: 10px; cursor: pointer; user-select: none;
  margin: 10px 0 2px; padding: 3px 4px 4px; border-bottom: 1px solid rgba(150, 200, 255, 0.22);
  font-size: var(--fs-xs); letter-spacing: 0.14em; color: #cfe3ff; }
.ofside .hgrp:first-child { margin-top: 0; }
.ofside .hgrp b { font-weight: bold; }
.ofside .hgrp small { opacity: 0.55; letter-spacing: 0.06em; }
.ofside .hgrp:hover, .ofside .hgrp:focus-visible { color: #fff; border-bottom-color: rgba(150, 200, 255, 0.5); outline: none; }
.ofside .hgrp.folded { opacity: 0.7; }
.oftile.hstack { border-style: solid; border-width: 1px; box-shadow: 3px 3px 0 -1px rgba(255, 211, 25, 0.28), 6px 6px 0 -2px rgba(255, 211, 25, 0.14); }
.oftile.hstack.open { background: rgba(255, 211, 25, 0.16); box-shadow: none; }
.oftile.hstack .ofico { position: relative; }
.oftile.hstack .hscount { position: absolute; right: -6px; bottom: -4px; font-size: var(--fs-2xs); color: #ffd319;
  background: rgba(13, 2, 33, 0.9); padding: 0 3px; border-radius: 3px; line-height: 1.3; }
.oftile.hstack .hsopen { font-style: normal; opacity: 0.55; }
.oftile.instack { margin-left: 0; border-left: 3px solid rgba(255, 211, 25, 0.55); }
.ofside .hstackbox { grid-column: 1 / -1; display: grid; gap: 4px; padding: 6px;
  grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--fs-2xs) * 12), 100%), 1fr));
  border: 1px dashed rgba(255, 211, 25, 0.45); border-radius: 4px; background: rgba(255, 211, 25, 0.04); }

/* §CLAIMCLEAR: STAKE CLAIM where the rules refuse it -- still tappable, it says why */
#claimBtn.blocked { filter: grayscale(0.85); opacity: 0.55; }

/* §KINDLE (E5, 2026-10-27): kindled gear burns ember-orange */
.ktag {
  display: inline-block;
  margin-right: 4px;
  padding: 0 3px;
  font-size: 0.82em;
  font-weight: bold;
  letter-spacing: 0.3px;
  line-height: 1.25;
  color: #2a0b00;
  background: linear-gradient(180deg, #ffc46b, #ff7a3d);
  border-radius: 2px;
  vertical-align: 1px;
  box-shadow: 0 0 5px rgba(255, 122, 61, 0.75);
}
.picon.c-wpn.kin { filter: brightness(0) saturate(100%) invert(72%) sepia(65%) saturate(1200%) hue-rotate(337deg) brightness(105%) contrast(101%) drop-shadow(0 0 2px #ff8a3d) drop-shadow(0 0 4px rgba(255, 110, 40, 0.75)); }
.picon.c-gear.kin { filter: brightness(0) saturate(100%) invert(72%) sepia(65%) saturate(1200%) hue-rotate(141deg) brightness(103%) contrast(101%) drop-shadow(0 0 2px #ff8a3d) drop-shadow(0 0 4px rgba(255, 110, 40, 0.75)); }
.picon.c-core.kin { filter: brightness(0) saturate(100%) invert(72%) sepia(65%) saturate(1200%) hue-rotate(222deg) brightness(108%) contrast(101%) drop-shadow(0 0 2px #ff8a3d) drop-shadow(0 0 4px rgba(255, 110, 40, 0.75)); }
.picon.c-mod.kin { filter: brightness(0) saturate(100%) invert(72%) sepia(65%) saturate(1200%) hue-rotate(11deg) brightness(106%) contrast(101%) drop-shadow(0 0 2px #ff8a3d) drop-shadow(0 0 4px rgba(255, 110, 40, 0.75)); }
.picon.kin:not(.c-wpn):not(.c-gear):not(.c-core):not(.c-mod) { filter: drop-shadow(0 0 2px #ff8a3d) drop-shadow(0 0 4px rgba(255, 110, 40, 0.75)); }
.dbtn.kindle { background: linear-gradient(180deg, #ffc46b, #ff7a3d); border-color: #ff8a3d; color: #2a0b00; box-shadow: 0 0 8px rgba(255, 122, 61, 0.55); }
.dbtn.kindle:disabled { background: #3a2018; color: #c98a62; box-shadow: none; }
.dbtn.kindle small { opacity: .8; font-size: .8em; margin-left: 4px; }

/* ---------- §HUB: the console's tabs (2026-10-28) ----------
   Matt: "a place to look at quests, goals (sigil hunt) look at and interact
   with cargo, look at and interact with the ship, a place for logs". One tab a
   job; the split's two columns stack into one page under the tab bar. */
.invTabs { width: 100%; box-sizing: border-box; border-bottom: 2px solid rgba(45, 226, 230, 0.35); padding-left: 0; margin-bottom: 8px; }
.invTabs .mtab { top: 2px; }
.invTabs .mtab .itk { opacity: 0.45; font-size: var(--fs-2xs); margin-right: 6px; letter-spacing: 0; }
.invTabs .mtab small { opacity: 0.75; font-size: var(--fs-2xs); letter-spacing: 1px; margin-left: 4px; }
@media (max-width: 700px) { .invTabs .mtab .itk { display: none; } }
.mktmenu.split2.tabbed { grid-template-columns: minmax(0, 1fr); }
.mktmenu.split2.tabbed .mcol { border: 0; box-shadow: none; padding: 0; margin: 0; }
.mktmenu.split2.tabbed > .mktcol:empty { display: none; }
#invBody .drow.road .rdmk { display: inline-block; width: 1.3em; color: #ff9a4a; }
#invBody .drow.road.done { opacity: 0.7; }
#invBody .drow.road.done .rdmk { color: #7ce7a6; }
#invBody .drow.road.next { border-left: 2px solid #ff9a4a; background: rgba(255, 154, 74, 0.07); }
#invBody .drow.road.later { opacity: 0.5; }
#invBody .drow.road small { display: block; margin-top: 2px; white-space: normal; }
.logKinds { display: flex; flex-wrap: wrap; gap: 4px; margin: 2px 0 6px; }
.logKinds .logKind { color: var(--kc); border-color: var(--kc); opacity: 0.55; }
.logKinds .logKind.on { opacity: 1; box-shadow: 0 0 8px var(--kc); }
.logArchive { max-height: min(62vh, 640px); overflow-y: auto; font-size: var(--fs-sm); line-height: 1.35;
              padding: 4px 8px; background: rgba(13, 2, 33, 0.55); border: 1px solid rgba(124, 231, 166, 0.25); border-radius: 4px; }
.logArchive .cl { padding: 3px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.04); word-wrap: break-word; }
.logArchive .clt { opacity: 0.5; }
.logArchive .clk { color: var(--kc); letter-spacing: 1px; }
.logArchive .cco { color: #fff27a; border-bottom: 1px dashed rgba(255, 242, 122, 0.6); text-decoration: none; cursor: pointer; }
.logArchive .cco.live { color: #7ce7a6; }
.logArchive .cco.talk { color: #ffb347; }
.logArchive .cgone { opacity: 0.45; }
/* the console holds still when you change tabs: it hangs from the top */
#inv.overlay { justify-content: flex-start; }
.logArchive { text-align: left; }
.logKinds .dbtn.logKind { background: transparent; border: 1px solid var(--kc); color: var(--kc); opacity: 0.7; padding: 3px 9px; font-size: var(--fs-xs); min-width: 0; box-shadow: none; }
.logKinds .dbtn.logKind.on { background: var(--kc); color: #0d0221; opacity: 1; box-shadow: 0 0 8px var(--kc); }
/* §CODEX (E7): the log's entries, by thread */
#invBody .drow.cxThread { padding-top: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); background: transparent; }
#invBody .drow.cxEntry { border-left: 2px solid var(--cxc, #2de2e6); }
#invBody .drow.cxEntry .dname b { color: var(--cxc, #2de2e6); letter-spacing: 1px; }
#invBody .drow.cxEntry .cxText { display: block; margin-top: 3px; white-space: normal; line-height: 1.4; color: #d8d0f0; }
#invBody .cxNew { display: inline-block; background: #7ce7a6; color: #0d0221; font-size: var(--fs-2xs); padding: 0 5px; border-radius: 2px; letter-spacing: 1px; margin-right: 4px; }

/* ---------- §DESCENT: the run's screen (2026-10-28) ---------- */
#descent { position: fixed; inset: 0; z-index: 60; background: #000; display: none; align-items: center; justify-content: center; }
#descent .dscScreen { position: relative; width: 100%; height: 100%; touch-action: none; user-select: none; -webkit-user-select: none; }   /* §DESCENT-5: fills the screen, either way up */
#descent canvas { display: block; width: 100%; height: 100%; }
#descent .dscPause { position: absolute; top: 34px; left: 10px; width: 36px; height: 30px; font: bold 12px "Courier New", monospace; color: #e8e4f4;
  background: rgba(13, 2, 33, 0.6); border: 1px solid #3d1a6e; cursor: pointer; }
#descent .dscCard { position: absolute; inset: 0; display: none; flex-direction: column; align-items: center; justify-content: center; gap: 14px; padding: 24px;
  text-align: center; background: radial-gradient(ellipse at center, rgba(13, 2, 33, 0.55), rgba(13, 2, 33, 0.9)); font-family: "Courier New", monospace; }
#descent .dscCard .k { font-size: 11px; letter-spacing: .3em; color: #9b8fc2; }
#descent .dscCard .t { font-size: clamp(20px, 6vw, 30px); letter-spacing: .12em; color: #00f0ff; text-shadow: 0 0 12px rgba(0, 240, 255, .5); }
#descent .dscCard .t.bad { color: #ff2975; text-shadow: 0 0 12px rgba(255, 41, 117, .5); }
#descent .dscCard .s { font-size: 13px; color: #e8e4f4; line-height: 1.6; max-width: 34ch; }
#descent .dscCard .b { display: flex; flex-direction: column; gap: 10px; align-items: center; }
#descent .dscCard .go { font: inherit; font-size: 15px; letter-spacing: .2em; color: #0d0221; background: #00f0ff; border: 0; padding: 12px 28px; cursor: pointer; box-shadow: 4px 4px 0 #ff2975; }
#descent .dscCard .go.alt { background: transparent; color: #9b8fc2; box-shadow: none; border: 1px solid #3d1a6e; font-size: 12px; padding: 8px 18px; }

/* §GEARPOP (20261101j): a mount's row is what is fitted; WHAT FITS opens the choices in a popup over the dock */
.gsline { display: flex; flex-wrap: wrap; align-items: stretch; min-width: 0; }
/* §GEARROWS-3 (20261103a): in a narrow dock column the name was squeezed to one letter wide (the buttons never shrink,
   and the old wrap rule only looked at the screen's width). Now the row itself wraps: below ~300px of name room the
   DETAILS / WHAT FITS buttons drop to their own line, and the name never goes narrower than a word. */
.gsline .gshead { flex: 1 1 300px; min-width: 0; flex-wrap: wrap; row-gap: 3px; }
.gsline .gshead .gsnm { flex: 1 1 8em; min-width: 8em; overflow-wrap: break-word; }
.gsbtns { display: flex; flex: 0 0 auto; align-items: center; margin-left: auto; padding-left: 8px; }   /* (DETAILS and WHAT FITS stay together) */
.gsfits { flex: 0 0 auto; align-self: center; margin: 4px 6px 4px 0; font: inherit; font-size: var(--fs-2xs); letter-spacing: 1px;
  color: #2de2e6; background: rgba(45, 226, 230, .08); border: 1px solid rgba(45, 226, 230, .55); border-radius: 2px; padding: 4px 8px; cursor: pointer; white-space: nowrap; }
.gsfits:hover { background: rgba(45, 226, 230, .2); color: #fff; }
.gsfits.up { color: #0d0221; background: #7CE7A6; border-color: #7CE7A6; font-weight: bold; }
.gsfits.up b { color: #0d0221; }
.gsfits.none { opacity: .45; }
/* §GEARPOP-3 (20261102a): DETAILS and WHAT FITS as floating windows you can drag, that make room for each other */
.gswin { position: fixed; z-index: 401; display: flex; flex-direction: column; min-height: 0; }
.gswin.gsslide { transition: left .25s ease, top .25s ease; }
.gswin .gspanel { position: relative; width: 100%; max-height: inherit; display: flex; flex-direction: column; min-height: 0;
  background: #120a24; border: 1px solid #3a2a52; box-shadow: 0 10px 40px rgba(0, 0, 0, .65), 0 0 0 1px rgba(45, 226, 230, .12); }
.gswin .gsphead { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 10px; border-bottom: 1px solid #241734;
  cursor: move; touch-action: none; user-select: none; background: linear-gradient(#1c1236, #150c2a); }
.gswin .gsplab { flex: 1 1 auto; font-weight: bold; letter-spacing: 2px; font-size: var(--fs-sm); }
.gswin .gspx { font: inherit; font-size: var(--fs-2xs); letter-spacing: 1px; color: #e8e4ff; background: #241734; border: 1px solid #3a2a52; padding: 5px 10px; cursor: pointer; }
.gswin .gspbody { overflow-y: auto; overflow-x: hidden; padding: 6px 10px 10px; display: flex; flex-direction: column; gap: 3px; min-height: 0; }
.gswin .gsdet { min-width: 0; max-width: 100%; overflow-x: auto; }
.gswin .gsdet .dbtn { white-space: normal; max-width: 100%; }
.gswin .gsphint { color: #8d86a6; font-size: var(--fs-2xs); margin: 10px 0 0; }
.gswin .gscand.empty { opacity: .6; }
.gswin .gscand.static { cursor: default; }
.gsfits.gsinfo { color: #e8e4ff; border-color: #6a5a8a; }
.gswin { overflow: hidden; }
/* §GEARPOP-3: the part being compared is boxed and named, so SELL / SCRAP are clearly its own */
.gswin .gsdim { opacity: .62; }
.gswin .gspick { margin-top: 8px; border: 2px solid #ffd319; background: rgba(255, 211, 25, .05); padding: 6px; box-shadow: 0 0 12px rgba(255, 211, 25, .18); }
.gswin .gspickh { font-size: var(--fs-2xs); letter-spacing: 1.5px; color: #ffd319; font-weight: bold; margin: 0 0 4px; }
