/* Stellar Foundry — a dark, calm control panel.
   Design tokens live at the top; prefer adding a token over a magic value. */

:root {
  color-scheme: dark;
  --bg: #080b16;
  --bg-soft: #0f1426;
  --panel: #141a2f;
  --panel-2: #1a2140;
  --line: #26304f;
  --text: #e7ecff;
  --muted: #94a0c4;
  --accent: #8ab4ff;
  --accent-2: #c9a6ff;
  --good: #6ee7a8;
  --warn: #ffcf7a;
  --danger: #ff8a8a;
  --shard: #d9b8ff;
  --neutronium: #e05cff;
  --duty: #ffb547;
  --aether: #6fe0ff;
  --expedition: #5fe0cf;
  /* The Descent (L2) — a colder, deeper cousin of the bay's mint, because it
     is the bay's sibling: the same crew, a different question. */
  --descent: #46b8a4;
  /* The Mill (L2) — annealed brass, warm against the shaft's jade. */
  --mill: #ffc247;
  /* The Mill's three goods. Three different hues rather than three shades of
     one, because they sit side by side in the stock row: violet-slate for the
     rock out of the compressed body, olive for fused ash, cold steel for the
     hardest thing in the game. */
  /* Nudged from #8f7ad6, which read 4.86:1 on `--panel` but only **4.43:1** on
     `--panel-2` — and the Mill's stock row, the one place the value is printed
     at 16px in this colour, sits on `--panel-2`. Same violet, one step up. */
  --basalt: #9a87da;
  --sinter: #9aa88c;
  /* Was #e6edf8 — indistinguishable from `--text: #e7ecff`, so Carbide was the
     one currency whose "colour" reinforced nothing. Cold steel with enough blue
     in it to read as its own mark; 8.8:1 on the panel, 8.8:1 on the disabled
     well, and clearly not Basalt's violet or Sinter's olive. */
  --carbide: #8fb6d9;
  --salvage: #d09a55;
  --crucible: #ff6b35;
  /* Embers (docs/CURRENCIES.md, "The Sky"). Three warm currencies now sit in
     this list and they have to be told apart at 14px: Salvage is a muted
     brown-gold, the Crucible a red-orange, and this is the brightest and
     yellowest of the three — a coal still glowing rather than metal still hot.
     12.6:1 on `--panel`, so it survives the disabled well as well as the row. */
  --ember: #ffc65c;
  /* The chart's own ground. Darker than every panel in the game, because the
     Sky is the one screen that is meant to read as a night rather than as a
     surface. */
  --sky-ground: #080b1c;
  --record: #e0b34d;
  --laurel: #a9d977;
  --workshop: #ffa552;
  --temper: #ff7a45;
  --faction-compression: #ff4fa3;
  --faction-expansion: #5b8cff;
  --rarity-common: #8ea2d6;
  --rarity-rare: #6fb3ff;
  --rarity-legendary: #ffb454;
  /* --- Corner radii -------------------------------------------------------
     Three steps and a pill, because `CLAUDE.md` §5 asks for consistent radii
     and the sheet had drifted to eleven raw values (2, 3, 4, 6, 7, 8, 9, 10,
     12, 50%, 999px) plus three one-off tokens. New rules use these; the
     existing raw values are being converged on them as each area is touched
     rather than in one sweep, so a diff stays reviewable.

     This block also fixes a real bug: `--r-sm` and `--r-md` were *used* by the
     Creep card in 0.41.0 and never defined, so an undefined `var()` made the
     whole declaration invalid and that card rendered with square corners while
     every other card on the screen was rounded. */
  /* --- Disabled controls -------------------------------------------------
     One treatment for the whole game, defined here and applied once at the
     bottom of this sheet (see "The disabled convention").

     A disabled control in an idle game is almost never an inert control. It is
     a **price the player is saving toward**, and it is the state most rows are
     in most of the time — at ten days all six Foundry rows are disabled at
     once. Opacity used to carry it, and opacity fades the number:

       .buy label / cost      3.94 / 3.96 : 1     (opacity 0.45)
       .btn--engage           2.38 : 1
       .duty-step             2.91 : 1            (opacity 0.35)
       .sym--neutronium       2.16 : 1
       .sym--aether           3.31 : 1

     The coloured currency marks came off worst, because they start closer to
     the ground than white text does — so the tint that exists to separate
     `◆` from `◈` was the first thing to disappear.

     So the **chrome** recedes and the **content** stays: a recessed well, no
     hover, `not-allowed`, label at `--muted` (7.2:1) and the cost at `--text`
     (15.9:1). `.btn--engage` already did exactly this and was still dim,
     because the base `.btn:disabled { opacity }` stacked on top of it. */
  --off-surface: color-mix(in srgb, var(--panel) 45%, var(--bg));
  --off-line: color-mix(in srgb, var(--line) 65%, transparent);
  --r-sm: 8px;   /* controls, chips, small tiles */
  --r-md: 12px;  /* cards and panels — the same 12px --radius has always been */
  --r-lg: 16px;  /* the few large surfaces that want more */
  --r-pill: 999px;
  --radius: var(--r-md);
  --shadow: 0 8px 30px rgb(0 0 0 / 45%);
  --font: 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font);
  color: var(--text);
  background: var(--bg);
}

/* The ambient wash, on a **fixed** layer rather than painted onto the document.
   It used to sit on `body` anchored at the page top (`at 15% -10%`), which
   meant it existed only for the first screenful: Achievements is 4,975px tall
   and Settings 5,872px, so 80% of those pages was flat near-black, and the
   space under a short panel like Upgrades or Stats was flat too. Fixing it to
   the viewport costs one element and gives every screen the same atmosphere
   the first one always had. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(1000px 600px at 15% -6%, #1b2450 0%, transparent 60%),
    radial-gradient(800px 500px at 110% 8%, #2a1b45 0%, transparent 55%),
    radial-gradient(900px 700px at -10% 108%, #1a1c3f 0%, transparent 60%),
    var(--bg);
}

/* Focus must always be visible: the game is fully keyboard-playable. */
:focus-visible {
  outline: 2px solid var(--accent-2);
  outline-offset: 2px;
}

/* Motion is decoration; never make it a requirement. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* --- Top bar ------------------------------------------------------------ */

/* One bar carries the whole always-on layer: who you are, what you have, the
 * one button you press, and the three drawers that are never the thing you
 * came to do. It is a grid rather than a wrapping flex row so the pieces can
 * re-order into two rows on a phone without any of them changing size. */
.topbar {
  display: grid;
  grid-template-areas: "brand readout action tools";
  /* `minmax(0, …)` on the two flexible columns rather than the default
     `auto`/`1fr`, both of which floor at min-content. The readout's banners
     are `nowrap` by nature (a countdown must not break mid-number), so a
     comet boost and a running Trial at the same time used to widen the
     readout past the bar and push the collapsed Collect button off the right
     edge of the page — 95px of horizontal overflow at 820px, and at 390px the
     button was entirely off screen. Nothing in a sticky header may be
     unreachable; the readout gives way instead. */
  grid-template-columns: auto minmax(0, auto) minmax(min-content, 1fr) auto;
  gap: 10px 18px;
  align-items: center;
  padding: 10px 20px;
  border-bottom: 1px solid var(--line);
  /* Opaque enough that a card scrolling under it never shows through the
     brand or the stardust total — contrast beats the blur effect. */
  background: rgb(8 11 22 / 94%);
  backdrop-filter: blur(10px);
  position: sticky;
  top: 0;
  z-index: 5;
}
/* Settings → Display: let it scroll away instead. */
.topbar--static { position: static; }

.brand { grid-area: brand; display: flex; align-items: center; gap: 10px; }
.brand__mark { color: var(--accent-2); font-size: 22px; }
.brand__name { margin: 0; font-size: 17px; letter-spacing: 0.02em; white-space: nowrap; }

.readout { grid-area: readout; min-width: 0; }
.readout__amount { font-size: 24px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.15; }
.readout__amount .unit { font-size: 12px; color: var(--muted); font-weight: 400; }
/* Rate and both banners want one line, so a comet or a challenge starting does
 * not change the height of a bar the whole page is sitting under — but they
 * *wrap* rather than overflow when both are running at once and the column is
 * too narrow to hold them. On a phone the two together are wider than the
 * whole screen, and one extra line is the cheapest of the three possible
 * losses: the alternatives were pushing the Collect button off the page
 * (what it used to do) or truncating a live countdown to "12m…". */
.readout__meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 10px;
  min-height: 17px;
  min-width: 0;
}
.readout__rate { color: var(--accent); font-variant-numeric: tabular-nums; font-size: 13px; }
.readout__boost {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--good);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* The last line of defence: one banner longer than the whole column (a very
     long challenge name on a 320px phone) clips itself rather than widening
     the grid. Wrapping above handles every case the game can actually reach. */
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.readout__boost[hidden] { display: none; }

/* The collapsed Collect button. It keeps its grid cell whether or not it is
 * shown — the hole it leaves at the top of the page is calmer than the brand
 * and readout sliding sideways every time the player scrolls past the hero. */
.topbar__action {
  grid-area: action;
  justify-self: stretch;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 300px;
  /* The floor lives on the *track* (`minmax(min-content, 1fr)` above), not
     here. A `min-width` on the item does not reserve track space: the button
     simply overflowed its own column and slid under the Achievements icon.
     An `auto` track takes its max-content before an `fr` track gets a share,
     so without that track minimum the readout squeezed the primary action of
     the whole game down to 35px — the readout gives way first, because it can
     wrap and the button cannot. */
  margin-left: auto;
  opacity: 0;
  transform: translateY(-8px) scale(0.96);
  pointer-events: none;
  transition: opacity 0.22s ease, transform 0.22s ease;
}
.topbar__action--shown {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* --- Comet (timed boost) -------------------------------------------------- */

.comet {
  position: fixed;
  top: var(--comet-y, 22%);
  left: var(--comet-x, 22%);
  transform: translate(-50%, -50%);
  width: 64px;
  height: 64px;
  padding: 0;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 15;
  background: radial-gradient(circle at 35% 30%, #fff, var(--accent-2) 40%, var(--accent) 78%, transparent 80%);
  box-shadow:
    0 0 22px 6px color-mix(in srgb, var(--accent-2) 65%, transparent),
    0 0 56px 14px color-mix(in srgb, var(--accent) 35%, transparent);
  animation: cometDrift 3.4s ease-in-out infinite, cometPulse 1.1s ease-in-out infinite;
}
.comet--visible { display: flex; }
.comet:hover { filter: brightness(1.12); }
.comet:active { transform: translate(-50%, -50%) scale(0.94); }
.comet__icon { font-size: 26px; pointer-events: none; filter: drop-shadow(0 0 4px rgb(8 16 32 / 60%)); }
.comet__ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); pointer-events: none; }
.comet__ring circle { fill: none; stroke-width: 3; stroke-linecap: round; stroke-dasharray: 100.5; }
.comet__ring-drain {
  stroke: color-mix(in srgb, var(--good) 85%, white);
  stroke-dashoffset: calc(100.5 * var(--comet-progress, 0));
  transition: stroke-dashoffset 0.2s linear;
}
/* The Beacon's slice: the part of the window it will claim if nobody moves.
   The drain retreats clockwise toward twelve o'clock, so this fixed arc marks
   where it will stop — "how long have I got before the machine takes it" as a
   shape rather than a number nobody is told. Painted over the drain so it
   reads for the whole window, and only drawn once the Beacon is owned. */
/* Scoped under `.comet__ring` on purpose: the shared `.comet__ring circle`
   rule above sets `stroke-dasharray: 100.5` and would otherwise out-specify
   this one, drawing the Beacon's "slice" as the entire ring. */
.comet__ring .comet__ring-beacon {
  stroke: color-mix(in srgb, var(--warn) 85%, transparent);
  stroke-dasharray: calc(100.5 * var(--comet-beacon-share, 0)) 100.5;
  stroke-dashoffset: 0;
  /* Butt, not round: a rounded cap on a short arc overstates how much of the
     window the Beacon actually takes. */
  stroke-linecap: butt;
  opacity: 0;
}
.comet--beacon .comet__ring .comet__ring-beacon { opacity: 1; }

/* "x3" on the orb: what taking it yourself is worth over leaving it. The
   game's one presence-gated reward, so it says so on the thing itself
   rather than in a doc nobody opens. */
.comet__bonus {
  position: absolute;
  right: -2px;
  bottom: -2px;
  min-width: 22px;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--bg-soft);
  border: 1px solid color-mix(in srgb, var(--good) 60%, transparent);
  color: var(--good);
  font-size: 11px;
  font-weight: 700;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.comet__bonus:empty { display: none; }
@keyframes cometDrift {
  0%, 100% { margin: 0; }
  50% { margin-top: -10px; margin-left: 6px; }
}
@keyframes cometPulse {
  0%, 100% { filter: brightness(1); }
  50% { filter: brightness(1.2); }
}
/* Comet Beacon: a comet caught before the player could ever see it appear
 * still needs a moment (CLAUDE.md §5) — a quick burst-and-fade at a fresh
 * spot, distinct from the idle drift/pulse above and never clickable. */
.comet--auto {
  animation: cometAutoCatch 0.6s ease-out forwards;
  pointer-events: none;
}
.comet--auto .comet__ring,
.comet--auto .comet__bonus { display: none; }
@keyframes cometAutoCatch {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }
  35% { opacity: 1; transform: translate(-50%, -50%) scale(1.3); filter: brightness(1.5); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.55); filter: brightness(1); }
}

/* --- Layout ------------------------------------------------------------- */

.layout {
  max-width: 900px;
  margin: 0 auto;
  padding: 24px 20px 80px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  box-shadow: var(--shadow);
}
/* The hide utility, and the one rule in the sheet that gets `!important`.
   `setClass(node, 'panel--hidden', …)` is applied to elements of every kind —
   panels, buttons, switches, notes — and any of those with a `display` of its
   own beats a bare class on source order. That had already been patched four
   separate times (`.buy--claim`, `.card--fitting .buy`, `.casting-boost`,
   `#upgrade-shop-empty`), each after something failed to hide, each with a
   comment explaining the same trap; the master automation switch was about to
   be the fifth. A utility whose whole job is "this is not on screen" should
   win once, here, instead of being re-argued per element. */
.panel--hidden { display: none !important; }
.panel__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.panel__head h2 { margin: 0; font-size: 16px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.panel__head-title { display: flex; align-items: center; gap: 10px; }
.subhead { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin: 18px 0 8px; }
.hint { color: var(--muted); margin-top: 0; }

/* The offline cap, disclosed. Warn-coloured because it is telling the player
   they lost something — not an error, but not a neutral aside either. */
.hint--capped { color: var(--warn); }

/* --- Collect button ----------------------------------------------------- */

/* --- Hero -------------------------------------------------------------- */

/* The one thing on the page that is unambiguously the primary action, so it
 * gets the full width of the column and the biggest type on screen after the
 * stardust total itself. */
.hero { display: flex; flex-direction: column; gap: 8px; }
.collect--hero { padding: 22px; font-size: 19px; min-height: 68px; letter-spacing: 0.01em; }
.collect--hero .collect__value { font-size: 17px; }

/* The same button, collapsed into the top bar. */
.collect--mini {
  position: relative;
  overflow: hidden;
  padding: 10px 16px;
  min-height: 44px;
  font-size: 15px;
  gap: 8px;
}
/* The click-charge meter, with no room for a label: a hairline along the foot
   of the button, the same fraction the hero's full bar shows. */
.collect__charge {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 3px;
  background: rgb(8 16 32 / 55%);
  transition: width 0.25s ease, background 0.25s ease;
}
.collect__charge[hidden] { display: none; }
.collect__charge--empty { background: var(--danger); }

.collect {
  width: 100%;
  padding: 12px 18px;
  min-height: 52px;
  font: inherit;
  font-size: 16px;
  font-weight: 600;
  color: #08101f;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  box-shadow: var(--shadow);
  transition: transform 0.08s ease, filter 0.15s ease;
}
.collect:hover:not(:disabled):not(.collect--disabled) { filter: brightness(1.06); }
.collect:active { transform: translateY(2px); }
.collect__value { font-variant-numeric: tabular-nums; opacity: 0.75; }


/* Compact enough to ride under the collect button in the top bar: the icon
 * names it, the title attribute explains it, the bar is the whole readout. */
.click-charge { display: flex; align-items: center; gap: 8px; }
.click-charge__label {
  flex: none;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.click-charge[hidden] { display: none; }
.click-charge__icon { display: flex; flex: none; color: var(--accent-2); }
.click-charge .progress { flex: 1; margin: 0; }
.click-charge .progress__bar { background: linear-gradient(90deg, var(--accent-2), var(--accent)); transition: width 0.25s ease, background 0.25s ease; }
.click-charge--empty .progress__bar { background: var(--danger); animation: charge-empty-pulse 1.4s ease-in-out infinite; }
@keyframes charge-empty-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .click-charge--empty .progress__bar { animation: none; }
}
.pulse { animation: pulse 0.22s ease; }
@keyframes pulse {
  0% { transform: scale(1); }
  45% { transform: scale(0.985); }
  100% { transform: scale(1); }
}

/* --- Tabs --------------------------------------------------------------- */

.tab {
  position: relative;
  font: inherit;
  color: var(--muted);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 11px 18px;
  min-height: 44px;
  cursor: pointer;
}
.tab:hover { color: var(--text); }
.tab--active { color: #08101f; background: var(--accent); border-color: transparent; font-weight: 600; }
.badge {
  display: none;
  margin-left: 8px;
  min-width: 18px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--good);
  color: #06301c;
  font-size: 11px;
  font-weight: 700;
}
.badge--visible { display: inline-block; }
/* A badge with no text is a rendering bug, not a state: `min-width` and the
   solid fill above turn it into a bare coloured sliver. The Challenges badge
   shipped like that. Belt and braces — the caller prints `!`, and this makes an
   empty one impossible. */
.badge:empty { display: none; }
.badge--count { background: var(--panel); color: var(--accent-2); border: 1px solid var(--line); }
.badge--legendary {
  background: var(--rarity-legendary);
  color: #3a2200;
  border-color: transparent;
}

/* --- Cards -------------------------------------------------------------- */

.cards { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.card {
  display: grid;
  gap: 6px;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  transition: border-color 0.2s ease;
}
.card--affordable { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
.card--managed { background: color-mix(in srgb, var(--shard) 6%, var(--panel-2)); }
.card__head { display: flex; align-items: center; gap: 8px; }

/* The tier's own glyph. Sized off the name so it scales with the type. */
.card__mark {
  display: inline-flex;
  flex: none;
  color: var(--accent-2);
  opacity: 0.85;
}
.card__name { margin: 0; font-size: 16px; }
.card__auto {
  /* `display` has to be set *here*, above `.card__auto--hidden`, and not in
     the automation block further down: same specificity, so source order
     decides, and a later `display: inline-flex` silently beat the hide. The
     unowned Fitter badge rendered as an empty pill on the Vise header. */
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font: inherit;
  margin-left: 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 2px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--shard) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--shard) 50%, var(--line));
  color: var(--shard);
  cursor: pointer;
}
/* A real <button> so the switch is keyboard-operable and announces its own
   state — the "⚙ AUTO" tag toggles the same automation the Settings panel
   does, right where you're already looking. */
.card__auto:hover:not(:disabled) { background: color-mix(in srgb, var(--shard) 38%, transparent); }
.card__auto--hidden { display: none; }
/* An automation badge with nothing in it is 18px of coloured pill and no
   meaning — the same defect `.badge:empty` guards, and the note above records
   it having already shipped once on the Vise header. */
.card__auto:empty { display: none; }
/* The pill is 20px tall — under WCAG 2.5.8's 24px floor and less than half the
   44px `CLAUDE.md` §5 asks for. It measured 121x20, 96x20, 83x20 … across nine
   panels, which is every automation switch in the game.

   Its *size* is doing real layout work: it sits inline in a crowded card title
   next to the tier glyph, the temper badge and the owned count, and growing the
   pill would wrap the name. So grow the **hit area** and leave the pill alone —
   the same trade `.chip` and `.stepper__btn` already make one breakpoint up,
   and for the same reason (full size on fingers, unchanged on a mouse).

   `::after` rather than padding: padding would move the border with it. */
@media (pointer: coarse) {
  .card__auto,
  /* The Mill's converter cards put the level and the run switch in one corner,
   and this div had no rule in the sheet at all — so `level 4` and the RUNNING
   pill sat flush against each other and the pill's border cut through the
   digit whenever the level reached two characters. */
.card__reading {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  flex: none;
}

.card__switch { position: relative; }
  .card__auto::after,
  .card__switch::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 44px;
    transform: translateY(-50%);
  }
  /* Two badges can sit side by side in a panel head (Vise: RATCHET + PACKER),
     and a 44px band around a 20px pill would otherwise overlap its neighbour's
     band and steal the tap. The row gets the height back instead. */
  .card__head,
  .panel__head { row-gap: 12px; }
}
.card__count {
  margin-left: auto;
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent-2);
}
.card__flavor { margin: 0; color: var(--muted); font-size: 13px; }
.card__stats { display: flex; gap: 12px; font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
.card__output { color: var(--good); }
.progress__label.card__milestone-label { margin: 0; font-size: 11px; }
.card__synergy {
  margin: 0;
  font-size: 12px;
  color: var(--accent-2);
}
.card__synergy--hidden { display: none; }
.progress.progress--sm { height: 6px; margin: 2px 0; }

.buy {
  margin-top: 4px;
  font: inherit;
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 11px 14px;
  min-height: 44px;
  align-items: center;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.buy:hover:not(:disabled) { background: var(--accent); color: #08101f; border-color: transparent; }
/* The disabled treatment lives in one block at the bottom of this sheet. */
.buy__cost { font-weight: 600; }

.card__condense {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 2px;
}
.card__condense[hidden] { display: none; }
.condense {
  font: inherit;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  min-height: 44px;
  border-radius: 8px;
  border: 1px dashed color-mix(in srgb, var(--accent-2) 50%, var(--line));
  background: transparent;
  color: var(--accent-2);
  cursor: pointer;
  font-size: 12px;
  white-space: nowrap;
}
.condense:hover:not(:disabled) { background: color-mix(in srgb, var(--accent-2) 16%, transparent); border-style: solid; }

.condense svg { flex: none; }
.condense__hint {
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* --- Temper (docs/IDEAS.md #46: sacrifice a tier for a permanent multiplier) --- */

.card--temperable { box-shadow: 0 0 0 1px color-mix(in srgb, var(--temper) 35%, transparent) inset; }

.card__temper-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 11px;
  font-weight: 700;
  color: var(--temper);
  font-variant-numeric: tabular-nums;
}
.card__temper-badge--hidden { display: none; }
.card__temper-badge svg { flex: none; }

.card__temper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 2px;
}
.card__temper--hidden { display: none; }

.temper {
  font: inherit;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  min-height: 44px;
  border-radius: 8px;
  border: 1px dashed color-mix(in srgb, var(--temper) 45%, var(--line));
  background: transparent;
  color: color-mix(in srgb, var(--temper) 70%, var(--muted));
  cursor: pointer;
  font-size: 12px;
  white-space: nowrap;
}
.temper svg { flex: none; }
.temper__mark { display: inline-flex; flex: none; }
.temper--ready { color: var(--temper); border-style: solid; }
.temper--ready:hover:not(:disabled) { background: color-mix(in srgb, var(--temper) 18%, transparent); }
/* Sealed: a Trial is running, and the engine refuses a Temper outright rather
   than "not yet" (engine: temperGenerator — the sacrifice has to be real).
   Two waits that mean different things must not look alike, so this one is
   dotted and carries a padlock where the flame goes. The shared disabled rule
   near the end of the sheet already flattens the fill and keeps the text
   legible; this only changes the mark. */
.temper--sealed { border-style: dotted; }

.temper__hint {
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* ==========================================================================
   ROW STATE + PURCHASE FEEDBACK  (added, not edited — nothing above changed)

   Two ideas, one theme: the row should show its own state.

   1. `--row-fill` (0..1, set per row from the engine — milestone progress on a
      generator, progress toward affording on an upgrade) drives a fill behind
      the row and the brightness of its rim. A card at 24/25 can no longer look
      identical to one at 1/25.
   2. `.fx--tap` / `.fx--snap` / `.fx--crest` are the three steps of the
      success-dependent purchase scale in `src/ui/feedback.js`. Nothing here
      shakes the page, nothing fires on every purchase, nothing runs past
      500ms — see that file for why.
   ========================================================================== */

.card--generator,
#upgrade-list .card {
  position: relative;
  isolation: isolate;
}

/* The fill: a wash that advances across the row, feathered at the leading edge
   so it reads as a level rather than a border. Painted behind the row's
   content but over its background (negative z-index inside the card's own
   stacking context), so nothing here can cover a number. */
.card--generator::before,
#upgrade-list .card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  opacity: calc(0.3 + var(--row-fill, 0) * 0.7);
  transition: opacity 0.3s ease;
}

/* 12% is a measured ceiling, not a taste call: the muted flavour text sits on
   whatever this wash makes of --panel-2, and at 22% it fell to 3.8:1. At 12%
   it holds 4.76:1, clear of the 4.5 floor, and the fill is still plainly
   readable across a column of rows. */
.card--generator::before {
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--accent-2) 12%, transparent) 0%,
    color-mix(in srgb, var(--accent-2) 6%, transparent) calc(var(--row-fill, 0) * 100% - 44px),
    transparent calc(var(--row-fill, 0) * 100%)
  );
}

/* The rim. Brightens with the fill, so "nearly there" is legible from across
   the screen without reading a single digit. */
.card--generator::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent-2) 70%, transparent),
    0 0 18px -8px color-mix(in srgb, var(--accent-2) 60%, transparent);
  opacity: calc(var(--row-fill, 0) * 0.7);
  transition: opacity 0.3s ease;
}

.card--generator.card--milestone-near::after { opacity: 0.85; }
.card--generator.card--milestone-imminent::after {
  opacity: 1;
  box-shadow:
    inset 0 0 0 1px var(--accent-2),
    0 0 22px -5px color-mix(in srgb, var(--accent-2) 80%, transparent);
  animation: milestone-imminent 2.4s ease-in-out infinite;
}
@keyframes milestone-imminent {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 1; }
}

/* --- The countdown, stated at the sharp end ------------------------------ */
/* "24/25" never says "one press from ×256". Both halves are on the row now:
   where you are on the left, what the next press buys on the right. */
.card--generator .card__milestone-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 0;
  font-size: 11px;
}
.card--generator .card__milestone-count {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.card--generator .card__milestone-next {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--accent-2) 65%, var(--muted));
  transition: color 0.2s ease;
}
.card--generator .card__milestone-next svg { flex: none; opacity: 0.65; }
.card--milestone-near .card__milestone-next { color: var(--accent-2); }
.card--milestone-imminent .card__milestone-next { color: var(--text); font-weight: 700; }
.card--milestone-imminent .card__milestone-next svg { opacity: 1; color: var(--accent-2); }

/* The bar gains weight as it fills, so the last quarter looks like the last
   quarter. Same accent throughout — the meaning of the colour does not move. */
.card--milestone-near .progress--milestone .progress__bar {
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
}
.card--milestone-imminent .progress--milestone .progress__bar {
  background: linear-gradient(90deg, var(--accent-2), #fff);
  box-shadow: 0 0 10px -2px var(--accent-2);
}

/* --- Endowed progress: the Temper bar debuts half full ------------------- */
/* The control appears at half its target (TEMPER_PREVIEW_AT) and the bar is
   credited with every unit the tier already holds, so the first Temper bar a
   player ever sees starts at 50% rather than 0%. The tick marks where it
   started. Kivetz, Urminsky & Zheng (948 loyalty-card members): a 10-stamp card
   with 2 pre-filled beat an 8-stamp empty card at identical real effort,
   34% vs 19% completion. */
/* Sits directly under the Temper row it belongs to, and thinner than the
   milestone bar above it: this is the secondary goal on the card, and it should
   not compete with the one the Buy button is working toward. */
.progress.card__temper-track {
  position: relative;
  height: 4px;
  margin: 2px 0 0;
}
.card__temper-track--hidden { display: none; }
.progress--temper .progress__bar--temper {
  background: linear-gradient(90deg, color-mix(in srgb, var(--temper) 35%, transparent), color-mix(in srgb, var(--temper) 80%, transparent));
}
.progress--temper.progress--temper-ready .progress__bar--temper {
  background: var(--temper);
  box-shadow: 0 0 8px -2px var(--temper);
}
.progress__endow {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--record);
  opacity: 0;
  transition: opacity 0.3s ease;
}
.progress--endowed .progress__endow { opacity: 0.9; }

/* --- Purchase feedback: three steps, and no more ------------------------- */
/* ×1 settles, ×10 snaps, a milestone crossing crests. Transform only: none of
   these move a neighbour, and none of them touch the page. */

.card__flare {
  position: absolute;
  inset: -1px;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    130% 150% at 50% 100%,
    color-mix(in srgb, var(--good) 45%, transparent) 0%,
    color-mix(in srgb, var(--accent-2) 22%, transparent) 40%,
    transparent 70%
  );
}

.fx--tap { animation: fx-tap 140ms ease-out; }
@keyframes fx-tap {
  0% { transform: translateY(1.5px) scale(0.996); }
  100% { transform: none; }
}

.fx--snap { animation: fx-snap 280ms cubic-bezier(0.2, 0.9, 0.3, 1); }
@keyframes fx-snap {
  0% { transform: translateY(2px) scale(0.993); }
  45% { transform: translateY(-1px) scale(1.004); }
  100% { transform: none; }
}
/* A batch buy should look like a step, not a slide: the bar jumps to its new
   level instead of easing there over 300ms. */
.fx--snap .progress--milestone .progress__bar { transition-duration: 70ms; }

.fx--crest { animation: fx-crest 470ms cubic-bezier(0.2, 0.9, 0.3, 1); }
@keyframes fx-crest {
  0% { transform: translateY(3px) scale(0.99); }
  18% { transform: translateY(-2px) scale(1.007); }
  100% { transform: none; }
}
.fx--crest .card__flare { animation: fx-flare 470ms ease-out; }
@keyframes fx-flare {
  0% { opacity: 0; }
  12% { opacity: 1; }
  100% { opacity: 0; }
}
.fx--crest .card__count { animation: fx-count 470ms ease-out; }
@keyframes fx-count {
  0%, 18% {
    color: var(--good);
    text-shadow: 0 0 14px color-mix(in srgb, var(--good) 70%, transparent);
  }
  100% { color: var(--accent-2); text-shadow: none; }
}
.fx--crest .card__mark { animation: fx-mark 470ms ease-out; }
@keyframes fx-mark {
  0% { transform: scale(1); }
  22% { transform: scale(1.3); }
  100% { transform: scale(1); }
}

/* Every press answers inside 100ms, before any frame has landed: an instant,
   un-animated settle on the control itself. Deliberately not animated, so it
   is still there for a player who has asked for reduced motion. */
.buy:active:not(:disabled) { transform: translateY(1px) scale(0.995); }
.temper:active:not(:disabled) { transform: translateY(1px); }
.condense:active:not(:disabled) { transform: translateY(1px); }

/* Reduced motion keeps every state — the fills, the rim, the countdown — and
   drops only the movement. The global rule at the top of this file already
   collapses durations; this stops the imminent rim from parking dim. */
@media (prefers-reduced-motion: reduce) {
  .card--generator.card--milestone-imminent::after { animation: none; opacity: 1; }
  .fx--tap, .fx--snap, .fx--crest,
  .fx--crest .card__flare,
  .fx--crest .card__count,
  .fx--crest .card__mark { animation: none; }
}

/* --- Upgrade rows fill toward affordability ------------------------------ */
/* Same device, same reading: how close is this row to its next event. On an
   upgrade that is "can I pay for it yet", which is the only state an upgrade
   row has. Accent, matching the affordable border it is heading toward.
   Scoped to #upgrade-list rather than `.card--upgrade`, because the Collapse,
   Vault and fleet-equipment shops wear that class too and have devices of
   their own — the fleet rack already has a fill bar behind its price. */
#upgrade-list .card::before {
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--accent) 10%, transparent) 0%,
    color-mix(in srgb, var(--accent) 5%, transparent) calc(var(--row-fill, 0) * 100% - 44px),
    transparent calc(var(--row-fill, 0) * 100%)
  );
}
#upgrade-list .card--affordable::before {
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--accent) 12%, transparent) 0%,
    color-mix(in srgb, var(--accent) 7%, transparent) 70%,
    transparent 100%
  );
  opacity: 1;
}

.btn--temper {
  background: linear-gradient(135deg, var(--temper), var(--danger));
  color: #2a0a00;
  border-color: transparent;
  font-weight: 600;
}
.btn--temper:hover:not(:disabled) { filter: brightness(1.08); }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  font: inherit;
  font-size: 13px;
  /* On the spacing scale, and 36px rather than the old 34 so the pill is a
     round number of steps tall. */
  padding: 8px 12px;
  min-height: 36px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--muted);
  cursor: pointer;
}
.chip--active { background: var(--accent); color: #08101f; border-color: transparent; font-weight: 600; }

/* **Touch targets are 44px, which is a rule this sheet was breaking on its own
   most-pressed control.** `.chip` is the buy-amount selector (x1/x10/x100/Max),
   the Duty policy picker, the achievement category filter and the expedition
   crew buttons — all touch-facing, all sitting at 34px against `CLAUDE.md` §5's
   "touch targets >= 44px". Raised on coarse pointers only: the rule is about
   fingers, and a mouse does not need a 44px pill on a dense desktop row.
   `.nav` chips already override this with their own larger sizing. */
@media (pointer: coarse) {
  .chip { min-height: 44px; padding: 12px 16px; }
}

.pills { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.pill {
  font-size: 12px;
  padding: 5px 10px;
  border-radius: 999px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--muted);
}

/* --- Harmonics (the Vault's repeatable line) ------------------------------- */
/* Reads as one block rather than a card in the shop list above it: the shop is
   finished by the time this appears, and the two are different kinds of thing
   — nine purchases that end, and one that does not. */

.harmonics {
  margin: 18px 0 4px;
  padding: 16px;
  border: 1px solid color-mix(in srgb, var(--aether) 40%, var(--line));
  border-radius: 12px;
  background:
    radial-gradient(120% 100% at 50% 0%, color-mix(in srgb, var(--aether) 12%, transparent), transparent 70%),
    var(--panel-2);
}
.harmonics__head { display: flex; align-items: flex-start; gap: 12px; }
.harmonics__mark { display: inline-flex; color: var(--aether); flex: none; }
.harmonics__title { margin: 0 0 2px; font-size: 16px; }
.harmonics__flavor { margin: 0; font-size: 13px; color: var(--muted); }
/* The multiplier is the biggest thing in the block: it is what the player is
   actually buying, and the level is only how they got there. */
.harmonics__level { margin-left: auto; text-align: right; flex: none; }
.harmonics__mult {
  display: block;
  font-size: 22px;
  font-weight: 700;
  color: var(--aether);
  font-variant-numeric: tabular-nums;
}
.harmonics__count { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.harmonics .progress { margin-top: 12px; }
.harmonics .progress__bar { background: var(--aether); }
.harmonics .buy { margin-top: 10px; }

.ladder { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.ladder__row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 12px;
  align-items: baseline;
  padding: 7px 10px;
  border-radius: 6px;
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.ladder__row:nth-child(odd) { background: color-mix(in srgb, var(--aether) 4%, transparent); }
.ladder__row--next { color: var(--text); background: color-mix(in srgb, var(--aether) 12%, transparent); }
.ladder__cost { color: var(--aether); }
.ladder__gain { min-width: 52px; text-align: right; font-weight: 600; }

/* --- Fleet equipment (Salvage) -------------------------------------------- */
/* Salvage is the bay's own currency, so it gets its own bronze register —
   distinct from the tab's teal, which stays the colour of the missions. */

.group--equipment .group__mark { color: var(--salvage); }
.salvage-readout { color: var(--salvage); font-variant-numeric: tabular-nums; }
.card--salvage .card__mark { color: var(--salvage); }
.card--salvage .progress__bar { background: var(--salvage); }
/* Affordability reads the same everywhere in the game: the card's rim takes
   the system's colour and the button stops being disabled. No second idiom
   here — a filled button would mean "affordable" only in this one shop. */
.card--salvage.card--affordable { border-color: color-mix(in srgb, var(--salvage) 55%, transparent); }

/* Fitted gear keeps its glyph instead of turning into a grey text pill —
   the reward state should not be the dullest thing on the screen. */
.pills--fitted { gap: 10px; }
.pill--marked,
.pill--salvage {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px 6px 9px;
  border-color: color-mix(in srgb, var(--salvage) 45%, var(--line));
  color: var(--salvage);
  background: color-mix(in srgb, var(--salvage) 10%, var(--panel-2));
}
.pill__mark { display: inline-flex; }
.pill__label { color: var(--text); }
.pill__effect {
  color: var(--salvage);
  font-variant-numeric: tabular-nums;
  padding-left: 7px;
  border-left: 1px solid color-mix(in srgb, var(--salvage) 35%, transparent);
}

.subhead--sm { margin: 14px 0 8px; font-size: 12px; }
.empty-note { color: var(--muted); font-size: 13px; margin: 4px 0 0; }
.empty-note--done { color: var(--salvage); }

/* --- Currency symbols ---------------------------------------------------- */
/* One class per currency, coloured from the token already named for it. Two of
   the six symbols are diamonds — `◈` Aether and `◆` Shards — and at 13px they
   are near-identical, so colour is what separates them at a glance. It is
   never the only signal: the glyphs themselves differ, which is what a reader
   who cannot separate cyan from violet reads instead.
   Stardust deliberately has no rule: it inherits, because it is the currency
   on almost every row and tinting it would colour most of the screen. */
.sym { font-variant-numeric: normal; }
.sym--shard { color: var(--shard); }
/* ...except on a filled surface. The Collapse button is a light violet-to-pink
   gradient with dark text, and a `--shard` violet diamond on top of it is
   invisible — the currency tint assumes the dark panel ground it was picked
   against. On any filled control the symbol inherits the control's own text
   colour, which is what keeps it legible. */
.btn--collapse .sym,
.btn--compress .sym,
.tab--active .sym,
.badge .sym { color: inherit; }
.sym--aether { color: var(--aether); }
.sym--crucible { color: var(--crucible); }
.sym--laurel { color: var(--laurel); }
.sym--salvage { color: var(--salvage); }
/* `✹` (U+2739) draws noticeably smaller than the other five marks in the
   system font stack — at 13px it read as a dot rather than a star. A small
   optical bump puts it on the same visual weight as `◆` beside it; the mark is
   inline in a line box its neighbours already size, so nothing moves. */
.sym--neutronium { color: var(--neutronium); font-size: 1.2em; line-height: 1; }
.sym--basalt { color: var(--basalt); }
.sym--sinter { color: var(--sinter); }
.sym--carbide { color: var(--carbide); }
.sym--ember { color: var(--ember); }

/* --- Stats -------------------------------------------------------------- */

/* **160px, and the number is measured rather than chosen.** Both grouped
   sections here hold exactly four tiles, and the panel's content column is
   695px with a 10px gap — so four across needs `(695 - 3*10) / 4 = 166px` or
   less. A previous pass spotted the same 3+1 orphan and set 180px, which is
   14px too wide to fit four and therefore changed nothing; that rule was also a
   *second* `.stats` declaration a hundred lines below this one, quietly winning
   on source order. One rule now, and a value with headroom: 160px fits four at
   desktop, two on a phone, and never five. */
.stats { margin: 0; display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
/* Same shape as `.record` above, and for the same reason — the two grids are
   stacked on one panel and their numbers should share a line. */
.stat {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 12px;
  display: flex;
  flex-direction: column;
}
.stat .stat__value { margin-top: auto; }
.stat__label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.stat__label .icon { flex: none; opacity: 0.75; }
.stat__value { margin: 4px 0 0; font-size: 18px; font-variant-numeric: tabular-nums; }

/* --- Stats hierarchy: hero / totals / trivia ------------------------------ */
/* Added this session. The Stats tab used to be twelve tiles of identical
   weight, so nothing on it answered "how am I doing?". Three registers now:
   one hero number at display size with the three figures that give it context,
   the same .stat tiles for the totals worth glancing at, and the trivia folded
   into a <details> instead of competing. Every number here is tabular and every
   box is fixed-width, so the 20Hz repaint moves nothing. */

.stats-hero {
  margin: 0 0 16px;
  padding: 16px;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
  border-radius: var(--radius);
  background:
    radial-gradient(130% 150% at 0% 0%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 62%),
    var(--panel-2);
}
.stats-hero__label {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.stats-hero__label .icon { flex: none; color: var(--accent); }
.stats-hero__value {
  margin: 4px 0 0;
  font-size: clamp(30px, 7vw, 44px);
  line-height: 1.15;
  font-weight: 700;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 24px color-mix(in srgb, var(--accent) 30%, transparent);
  overflow-wrap: anywhere;
}
.stats-hero__facts {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid color-mix(in srgb, var(--accent) 18%, var(--line));
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}
.stats-hero__fact { min-width: 0; }
.stats-hero__fact-label {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.stats-hero__fact-label .icon { flex: none; opacity: 0.7; }
.stats-hero__fact-value {
  display: block;
  margin-top: 3px;
  font-size: clamp(14px, 3.6vw, 17px);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* On a phone the three supporting figures stop being columns and become rows,
   so their labels never wrap into ragged two-line stacks. */
@media (max-width: 600px) {
  .stats-hero__facts { grid-template-columns: 1fr; gap: 0; }
  .stats-hero__fact {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 8px 0;
    border-top: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
  }
  .stats-hero__fact:first-child { border-top: 0; padding-top: 0; }
  .stats-hero__fact-value { margin-top: 0; text-align: right; }
}

/* Trivia, folded. True, occasionally nice, never the answer to anything. */
.stats-more {
  margin: 16px 0 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: color-mix(in srgb, var(--panel-2) 55%, transparent);
}
.stats-more__summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 14px;
  cursor: pointer;
  list-style: none;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.stats-more__summary::-webkit-details-marker { display: none; }
.stats-more__summary:hover { color: var(--text); }
.stats-more__summary .icon { flex: none; opacity: 0.75; }
.stats-more__summary::after {
  content: '';
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.2s ease;
}
.stats-more[open] .stats-more__summary::after { transform: translateY(2px) rotate(225deg); }
.trivia { margin: 0; padding: 0 14px 6px; }
.trivia__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 0;
  border-top: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
}
.trivia__row:first-child { border-top: 0; }
.trivia__label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--muted);
}
.trivia__label .icon { flex: none; opacity: 0.65; }
.trivia__value { margin: 0; font-size: 13px; font-variant-numeric: tabular-nums; }

/* --- Records -------------------------------------------------------------- */
/* A gold identity of their own — the one section of the Stats tab that isn't
   navy/violet, so the permanent bests read as a shelf, not another tile. */

/* Four records, and the same measured 160px `.stats` uses — the two grids sit
   directly above and below each other on this panel, so a different column
   count between them reads as a mistake even when each is defensible alone. At
   200px this one rendered three across with a lone orphan underneath. */
.records {
  margin: 0 0 16px;
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
/* Column, with the value pushed to the bottom: narrower tiles mean some labels
   wrap to two lines and some do not, and without this the numbers sit at four
   different heights across one row. The label grows upward, the value stays on
   the baseline it shares with its neighbours. */
.record {
  background: color-mix(in srgb, var(--record) 8%, var(--panel-2));
  border: 1px solid color-mix(in srgb, var(--record) 35%, var(--line));
  border-radius: var(--r-sm);
  padding: 12px;
  display: flex;
  flex-direction: column;
}
.record .record__value { margin-top: auto; }
.record__label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--record) 70%, var(--muted));
}
.record__label .icon { flex: none; opacity: 0.9; color: var(--record); }
.record__value { margin: 4px 0 0; font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
/* Colour is the only thing that changes: same size, same weight, so the shelf
   holds its shape and an arriving record does not reflow the row. */
.record__value--empty { color: color-mix(in srgb, var(--muted) 75%, var(--panel-2)); }

/* --- Production history --------------------------------------------------- */

.history-card {
  margin: 0 0 16px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px;
}
.history-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin: 0 0 6px;
}
.history-card__title {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.history-card__title .icon { flex: none; opacity: 0.75; }
.history-card__now { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
.history-card__empty { margin: 4px 0; font-size: 13px; color: var(--muted); }
.sparkline { display: block; width: 100%; height: 64px; overflow: visible; }
.sparkline__fill { fill: color-mix(in srgb, var(--accent) 22%, transparent); stroke: none; }
.sparkline__line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.sparkline__dot {
  fill: var(--accent);
  animation: sparklinePulse 1.8s ease-in-out infinite;
}
.sparkline__record {
  stroke: var(--record);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.7;
}
.history-card__caption { margin: 4px 0 0; font-size: 11px; color: var(--muted); }
@keyframes sparklinePulse {
  0%, 100% { r: 2.5; opacity: 1; }
  50% { r: 4; opacity: 0.6; }
}

/* --- Production breakdown -------------------------------------------------- */

.breakdown {
  margin: 0 0 16px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px;
}
.breakdown__title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 8px;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.breakdown__title .icon { flex: none; opacity: 0.75; }
.breakdown__empty { margin: 0; font-size: 13px; color: var(--muted); }
.breakdown__bar {
  display: flex;
  height: 14px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--bg-soft);
  border: 1px solid var(--line);
}
.breakdown__segment {
  height: 100%;
  min-width: 2px;
  transition: flex-basis 0.3s ease;
}
.breakdown__legend {
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 6px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.breakdown__legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--muted);
}
.breakdown__swatch { flex: none; width: 10px; height: 10px; border-radius: 3px; }
.breakdown__legend-value { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--text); }

/* --- Credits ------------------------------------------------------------- */

/* Required by the game-icons.net licence (CC BY 3.0) — see docs/CREDITS.md. */
.credits { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--line); }
.credits__title {
  margin: 0 0 6px;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.credits__line { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.6; }
.credits__line a { color: var(--accent); }

/* --- Tab gating and the settings gear ------------------------------------ */

/* A tab only appears once it has something in it (engine: visibleTabs). */
.tab--hidden { display: none; }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  color: var(--muted);
  cursor: pointer;
}
.icon-btn:hover:not(:disabled) { color: var(--text); border-color: var(--accent); }
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.icon-btn--active { color: var(--accent); border-color: var(--accent); }
.icon-btn--hidden { display: none; }

/* The topbar's consult-only cluster: Achievements, Stats, Settings. Grouped so
   they read as one control group rather than three loose buttons, and so the
   gap between them stays fixed as icons appear. */
.topbar__tools {
  grid-area: tools;
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

/* "Something is waiting" — deliberately a dot and not a count. A number in the
   topbar restates a score; the dot says only "there is something to press".
   Absolutely positioned so showing it never moves the icon. */
.icon-btn { position: relative; }

.icon-btn__dot {
  position: absolute;
  top: 7px;
  right: 7px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--laurel);
  border: 2px solid var(--panel);
}

.icon-btn__dot--full { background: var(--rarity-legendary); }

@media (prefers-reduced-motion: no-preference) {
  .icon-btn__dot { animation: dotPulse 2.4s ease-in-out infinite; }
}

@keyframes dotPulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.25); opacity: 0.75; }
}

/* --- Claiming ------------------------------------------------------------- */

.buy--claim { margin-top: 10px; }
.card--claimable { border-color: var(--good); }
.badge--claim { background: var(--good); color: #062b18; }
.actions--claim { margin: 10px 0 0; }
.btn--claim { border-color: var(--good); color: var(--good); }

/* --- Icons --------------------------------------------------------------- */

/* Vendored Lucide glyphs (docs/CREDITS.md). Drawn on currentColor, so an icon
   always takes the colour of whatever it sits in — including a re-palette. */
.icon {
  display: inline-block;
  vertical-align: -0.125em;
  flex: none;
}

/* --- Settings ----------------------------------------------------------- */

.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.btn {
  font: inherit;
  padding: 11px 18px;
  min-height: 44px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer;
}
/* `:not(:disabled)` on the base rule, and it was missing for a long time. A
   disabled `.btn` took the full accent fill on hover and read as the most
   pressable thing on the panel — caught on the chock's "Seat all", which is
   disabled whenever the Mill is empty. Four variants below had already worked
   around it one at a time (`.btn--temper`, `.btn--collapse`, `.btn--compress`,
   `.btn--claim`); this fixes it once, for every button in the game. */
.btn:hover:not(:disabled) { background: var(--accent); color: #08101f; border-color: transparent; }

.btn--danger { border-color: color-mix(in srgb, var(--danger) 50%, transparent); color: var(--danger); }
.btn--danger:hover:not(:disabled) { background: var(--danger); color: #2a0a0a; }
.save-code {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  color: var(--text);
  background: var(--bg-soft);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px;
  resize: vertical;
}

/* --- Collapse / prestige -------------------------------------------------- */

.panel--collapse {
  border-color: color-mix(in srgb, var(--shard) 35%, var(--line));
  box-shadow: var(--shadow), 0 0 40px -20px color-mix(in srgb, var(--shard) 70%, transparent);
}
.shard-readout { font-variant-numeric: tabular-nums; color: var(--shard); font-weight: 600; }
.shard-readout .unit { color: var(--muted); font-weight: 400; font-size: 12px; }

.progress {
  height: 10px;
  border-radius: 999px;
  background: var(--bg-soft);
  border: 1px solid var(--line);
  overflow: hidden;
  margin: 12px 0 6px;
}
.progress__bar {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--accent-2), var(--shard));
  transition: width 0.3s ease;
}
.progress--ready .progress__bar { background: linear-gradient(90deg, var(--good), var(--shard)); }
.progress__label { margin: 0 0 16px; font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }

.btn--collapse {
  width: 100%;
  font-weight: 600;
  background: linear-gradient(135deg, var(--accent-2), var(--shard));
  color: #1a0f2e;
  border-color: transparent;
  margin-bottom: 12px;
}
.btn--collapse:hover:not(:disabled) { filter: brightness(1.08); }


.card--shard.card--affordable { border-color: color-mix(in srgb, var(--shard) 55%, transparent); }
.card--shard .card__mark { color: var(--shard); }
.pill--shard { border-color: color-mix(in srgb, var(--shard) 45%, var(--line)); color: var(--shard); }
.pill--shard.pill--marked { background: color-mix(in srgb, var(--shard) 10%, var(--panel-2)); }
.pill--shard .pill__mark { color: var(--shard); }
.pill--hostile { border-color: color-mix(in srgb, var(--danger) 45%, var(--line)); color: var(--danger); }

.collapse-milestone {
  padding: 10px 12px;
  margin: 4px 0 16px;
  border: 1px solid color-mix(in srgb, var(--good) 30%, var(--line));
  border-radius: 10px;
  background: color-mix(in srgb, var(--good) 6%, var(--panel-2));
}
.collapse-milestone .hint { margin: 0 0 8px; }
.collapse-milestone .progress { margin: 0 0 4px; }
.collapse-milestone .progress__bar { background: linear-gradient(90deg, var(--good), var(--shard)); }
.collapse-milestone .progress__label { margin: 0; font-size: 12px; }

/* --- The Vise (Compression, L2) ---------------------------------------------
   The second prestige layer gets its own register rather than the Collapse
   tab's violet with a different label on it: a hotter magenta rim, a magenta
   press button, and a ledger that reads like a weighbridge ticket. Arriving
   here should feel like arriving somewhere, which is what a layer is for.

   The two bars on this panel are painted by id, not by `.panel--vise .progress
   __bar`: Ram's card is inside this panel and paints its own bar from
   `--line-accent`, and a descendant rule of equal specificity landing later in
   the file would quietly take it over. */

.panel--vise {
  border-color: color-mix(in srgb, var(--neutronium) 40%, var(--line));
  box-shadow: var(--shadow), 0 0 46px -18px color-mix(in srgb, var(--neutronium) 75%, transparent);
}
.neutronium-readout { font-variant-numeric: tabular-nums; color: var(--neutronium); font-weight: 600; }
.neutronium-readout .unit { color: var(--muted); font-weight: 400; font-size: 12px; }

#compress-progress-bar { background: linear-gradient(90deg, var(--shard), var(--neutronium)); }
.progress--ready #compress-progress-bar { background: linear-gradient(90deg, var(--good), var(--neutronium)); }

.btn--compress {
  width: 100%;
  font-weight: 600;
  background: linear-gradient(135deg, var(--neutronium), #ff9de0);
  color: #24052c;
  border-color: transparent;
  margin-bottom: 12px;
}
.btn--compress:hover:not(:disabled) { filter: brightness(1.08); }


/* The jaws. One row per thing a Compress would take, so the decision is a list
   of numbers rather than a paragraph of prose. */
.vise-ledger {
  padding: 12px 14px;
  margin: 6px 0 18px;
  border: 1px solid color-mix(in srgb, var(--neutronium) 28%, var(--line));
  border-radius: 10px;
  background: color-mix(in srgb, var(--neutronium) 6%, var(--panel-2));
}
.vise-ledger .subhead--sm { margin-top: 0; }
.vise-ledger__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.vise-row {
  display: grid;
  grid-template-columns: 18px 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 6px;
  border-radius: 7px;
  font-size: 13px;
}
.vise-row:nth-child(odd) { background: color-mix(in srgb, var(--neutronium) 5%, transparent); }
.vise-row__mark { display: inline-flex; color: color-mix(in srgb, var(--neutronium) 70%, var(--muted)); }
.vise-row__label { color: var(--muted); }
.vise-row__value { color: var(--text); font-variant-numeric: tabular-nums; font-weight: 600; }
.vise-ledger__list--modal { margin: 12px 0 4px; grid-template-columns: 1fr; max-height: 220px; overflow-y: auto; }
.hint--kept { margin: 10px 0 0; color: var(--good); font-size: 12px; }
.hint--shelf { margin: 8px 0 0; color: color-mix(in srgb, var(--duty) 75%, var(--muted)); font-size: 12px; }

.line-card--ram { --line-accent: var(--neutronium); }

/* --- Duty (the allocation pool, L2) ------------------------------------------
   The pool gets its own register inside Compression's magenta: an amber
   `--duty` for load, because "how hot is this station running" is a different
   idea from "how much Neutronium do I hold" and the two sit on the same tab.

   Everything here is a row. The one rule the panel is built around is that a
   row must read *without being read*: the load bar behind it says how much duty
   is on it, the pip strip says running-versus-drafted, and the whole panel dims
   toward grey while the machine is spinning up. */

.panel--duty {
  border-color: color-mix(in srgb, var(--duty) 34%, var(--line));
  box-shadow: var(--shadow), 0 0 44px -20px color-mix(in srgb, var(--duty) 60%, transparent);
}
.duty-readout { text-align: right; font-variant-numeric: tabular-nums; color: var(--duty); font-weight: 600; }
.duty-readout__spare {
  display: block;
  font-size: 12px;
  font-weight: 400;
  color: var(--muted);
}
.duty-readout__spare--idle { color: var(--warn); }

/* The spin-up meter: the friction curve, made visible. Taller than the other
   bars in the game on purpose — it is the state of the whole machine, not a
   progress bar toward one purchase. */
.duty-spinup { margin: 14px 0 18px; }
.progress--lg { height: 16px; margin: 0 0 6px; }
#duty-spinup-bar { background: linear-gradient(90deg, var(--warn), var(--duty)); }
.progress--ready #duty-spinup-bar { background: linear-gradient(90deg, var(--good), var(--duty)); }
.duty-spinup--running #duty-spinup-bar {
  background-image: linear-gradient(90deg, var(--warn), var(--duty)),
    repeating-linear-gradient(115deg, rgb(255 255 255 / 16%) 0 8px, transparent 8px 18px);
  background-blend-mode: screen;
  animation: duty-sweep 1.1s linear infinite;
}
@keyframes duty-sweep {
  from { background-position: 0 0, 0 0; }
  to { background-position: 0 0, 36px 0; }
}

.duty-stations { list-style: none; margin: 0 0 16px; padding: 0; display: grid; gap: 10px; }

.duty-row {
  position: relative;
  overflow: hidden;
  display: grid;
  gap: 8px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  transition: border-color 0.2s ease;
}
/* The load behind the row. Sits under everything, so the row gets visibly
   heavier as duty goes on it — the state reads before the numbers do. */
.duty-row__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  background: linear-gradient(90deg, color-mix(in srgb, var(--duty) 22%, transparent), transparent);
  transition: width 0.35s ease;
  pointer-events: none;
}
.duty-row > *:not(.duty-row__fill) { position: relative; }
.duty-row--dirty { border-color: color-mix(in srgb, var(--duty) 60%, transparent); }
.duty-row--idle { color: var(--muted); }
.duty-row--idle .duty-row__mark { color: var(--muted); }

.duty-row__head { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; }
.duty-row__mark { display: inline-flex; flex: none; color: var(--duty); opacity: 0.9; }
.duty-row__name { margin: 0; font-size: 16px; }
.duty-row__summary { margin: 0; font-size: 12.5px; color: var(--muted); }
.duty-row__reading { text-align: right; }
.duty-row__mult {
  display: block;
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--duty);
}
.duty-row__rated { display: block; font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* The pip strip: filled = running, outlined = drafted, dim = headroom. One
   picture for "what the machine is doing" and "what I am about to ask it to
   do", which is the whole reason the draft is separate from the commit. */
.duty-row__meter { display: flex; align-items: center; gap: 10px; }
.duty-row__pips { display: flex; flex-wrap: wrap; gap: 4px; flex: 1; }
.duty-pip {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  border: 1px solid color-mix(in srgb, var(--duty) 30%, var(--line));
  background: var(--bg-soft);
  transition: background 0.2s ease, border-color 0.2s ease;
}
.duty-pip--on { background: var(--duty); border-color: var(--duty); }
.duty-pip--planned {
  background: color-mix(in srgb, var(--duty) 22%, transparent);
  border-color: var(--duty);
  border-style: dashed;
}
/* Past the station's own soft cap, so "this is where it stops paying" is a
   thing you can see rather than a curve you have to infer. */
.duty-pip--over { border-radius: 999px; opacity: 0.75; }
.duty-row__units { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

.duty-row__controls { display: flex; align-items: center; gap: 8px; }
.duty-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
}
.duty-step:hover:not(:disabled) { border-color: var(--duty); color: var(--duty); }

.duty-step--plus { border-color: color-mix(in srgb, var(--duty) 45%, var(--line)); }
.duty-row__next { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

.duty-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.btn--engage {
  flex: 1 1 220px;
  min-height: 44px;
  font-weight: 600;
  background: linear-gradient(135deg, var(--duty), var(--neutronium));
  color: #24120a;
  border-color: transparent;
}
.btn--engage:hover:not(:disabled) { filter: brightness(1.08); }
/* Disabled means inert, not dimmed: 0.4 opacity over a two-colour gradient
   still reads as a live button you are allowed to press. Flatten it instead. */

.btn--ghost { min-height: 44px; }
/* "Hopper opens at Compression #2." It was a bare amber sentence between the
   action row and the Governor card with nothing around it — the one line on the
   panel that had no container at all. Same left-rail treatment the bought-out
   note on the Upgrades tab already uses, in Duty's own colour. */
.hint--next {
  color: color-mix(in srgb, var(--duty) 70%, var(--muted));
  margin: 12px 0 0;
  padding: 9px 14px;
  border-left: 2px solid color-mix(in srgb, var(--duty) 45%, transparent);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: color-mix(in srgb, var(--duty) 6%, transparent);
}
.hint--next:empty { display: none; }

.duty-policy {
  margin: 4px 0 18px;
  padding: 12px 14px;
  border: 1px solid color-mix(in srgb, var(--duty) 26%, var(--line));
  border-radius: 10px;
  background: color-mix(in srgb, var(--duty) 5%, var(--panel-2));
}
.duty-policy .subhead--sm { margin-top: 0; }
.duty-policy .chip--active { background: var(--duty); color: #24120a; }

.card--fitting { border-color: color-mix(in srgb, var(--neutronium) 26%, var(--line)); }
.card--fitting .card__mark { color: var(--neutronium); }
.card--fitting.card--owned { color: var(--muted); border-color: color-mix(in srgb, var(--good) 30%, var(--line)); }
.card--fitting.card--owned .card__mark { color: var(--muted); }
.card__desc { margin: 0; font-size: 13.5px; }


/* --- Doctrines --------------------------------------------------------------
   Two wings, both open. Until 0.39.0 this was a one-way choice with a card
   picker and a confirm modal; both are gone, and each wing now keeps only its
   colour so the two read apart at a glance. */

.doctrine { margin-bottom: 20px; }
.doctrine:last-of-type { margin-bottom: 8px; }
#doctrine-compression { --faction-active: var(--faction-compression); }
#doctrine-expansion { --faction-active: var(--faction-expansion); }
.faction-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  margin: 4px 0 16px;
  border: 1px solid color-mix(in srgb, var(--faction-active, var(--shard)) 35%, var(--line));
  border-radius: 10px;
  background: color-mix(in srgb, var(--faction-active, var(--shard)) 8%, var(--panel-2));
}
.faction-banner__icon { display: inline-flex; flex-shrink: 0; color: var(--faction-active, var(--shard)); }
.faction-banner__name { margin: 0; font-size: 15px; color: var(--faction-active, var(--shard)); }
.faction-banner__tagline { margin: 2px 0 0; font-size: 12px; color: var(--muted); font-style: italic; }
.faction-panel .card--shard.card--affordable {
  border-color: color-mix(in srgb, var(--faction-active, var(--shard)) 55%, transparent);
}
.faction-panel .card--shard .card__mark { color: var(--faction-active, var(--shard)); }
.faction-panel .pill--shard {
  border-color: color-mix(in srgb, var(--faction-active, var(--shard)) 45%, var(--line));
  color: var(--faction-active, var(--shard));
}
.faction-panel .pill--shard.pill--marked { background: color-mix(in srgb, var(--faction-active, var(--shard)) 10%, var(--panel-2)); }
.faction-panel .pill--shard .pill__mark { color: var(--faction-active, var(--shard)); }

/* --- Vault (Aether) --------------------------------------------------------- */

.panel--vault {
  border-color: color-mix(in srgb, var(--aether) 35%, var(--line));
  box-shadow: var(--shadow), 0 0 40px -20px color-mix(in srgb, var(--aether) 70%, transparent);
}
.aether-readout { color: var(--aether); display: inline-flex; align-items: center; gap: 6px; }
.card--aether.card--affordable { border-color: color-mix(in srgb, var(--aether) 55%, transparent); }
.card--aether .card__mark { color: var(--aether); }
.card--aether .progress__bar { background: var(--aether); }
.pill--aether { border-color: color-mix(in srgb, var(--aether) 45%, var(--line)); color: var(--aether); }
.pill--aether.pill--marked { background: color-mix(in srgb, var(--aether) 10%, var(--panel-2)); }
.pill--aether .pill__mark { color: var(--aether); }
#aether-rate { margin: 14px 0 4px; font-weight: 600; color: var(--aether); }
#vault-nexus-hint { margin: 0 0 16px; }

/* --- Achievements --------------------------------------------------------- */

.cards--achievements { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.card--achievement { color: var(--muted); border-left: 3px solid var(--line); }
.card--achievement.card--earned {
  opacity: 1;
  border-color: color-mix(in srgb, var(--good) 55%, transparent);
  background: color-mix(in srgb, var(--good) 8%, var(--panel-2));
}
/* Rarity only decorates an *earned* card — an unearned legendary should not
   spoil itself with a gold border before the player has cleared it. */
.card--achievement.card--rarity-rare.card--earned {
  border-left-color: var(--rarity-rare);
}
.card--achievement.card--rarity-legendary.card--earned {
  border-left-color: var(--rarity-legendary);
  background: color-mix(in srgb, var(--rarity-legendary) 10%, var(--panel-2));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--rarity-legendary) 30%, transparent);
}
.achievement__head { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
.achievement__glyph { display: inline-flex; flex: none; color: var(--muted); }
.card--earned .achievement__glyph { color: var(--good); }
.card--rarity-rare.card--earned .achievement__glyph { color: var(--rarity-rare); }
.card--rarity-legendary.card--earned .achievement__glyph { color: var(--rarity-legendary); }
.card--achievement .card__name { font-size: 14px; margin: 0; flex: 1 1 auto; }
.rarity-pill {
  flex: none;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 999px;
  color: var(--rarity-common);
  border: 1px solid color-mix(in srgb, var(--rarity-common) 45%, transparent);
}
.card--rarity-rare .rarity-pill {
  color: var(--rarity-rare);
  border-color: color-mix(in srgb, var(--rarity-rare) 45%, transparent);
}
.card--rarity-legendary .rarity-pill {
  color: var(--rarity-legendary);
  border-color: color-mix(in srgb, var(--rarity-legendary) 45%, transparent);
}
.achievement__status {
  margin: 0;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.card--earned .achievement__status { color: var(--good); font-weight: 600; }

.achievement-complete {
  margin: -2px 0 12px;
  padding: 8px 12px;
  border-radius: 10px;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  color: var(--rarity-legendary);
  border: 1px solid color-mix(in srgb, var(--rarity-legendary) 45%, transparent);
  background: color-mix(in srgb, var(--rarity-legendary) 10%, var(--panel-2));
  animation: constellationGlow 3s ease-in-out infinite;
}
@keyframes constellationGlow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--rarity-legendary) 35%, transparent); }
  50% { box-shadow: 0 0 12px 1px color-mix(in srgb, var(--rarity-legendary) 35%, transparent); }
}

/* --- Challenges ------------------------------------------------------------
   The hostile system. Every other panel gives you something; this is the one
   that takes things away, so CLAUDE.md §5 blesses it having a palette of its
   own instead of the Foundry's violet. Red rims, hazard tape, and a glyph per
   challenge so seven different fictions stop sharing one mark.

   The tokens are declared with the section they belong to rather than at the
   top of the file: the whole identity is then one block to read, move or
   re-theme. Contrast, measured against the ground each colour actually sits on
   (sRGB, the same mix color-mix does):

     --challenge #ff6b6b on --panel-2 #1a2140 ....................... 5.67:1
     --challenge on an open card (4% wash, #232442) ................. 5.40:1
     --challenge on a running card (10% wash, #312844) .............. 4.99:1
     --muted #94a0c4 on a running card ............................. 5.33:1
     --text #e7ecff on a running card .............................. 11.76:1
     --good #6ee7a8 on a cleared card (6% good, #1f2d46) ............ 8.97:1
     --muted on the panel's own wash (#242033) ...................... 6.08:1

   Body copy stays --text/--muted everywhere; the red only ever carries small
   labels, marks and rims, so nothing on the screen drops under 4.5:1. Locked
   cards are dimmed by desaturating their *mark*, never by fading the whole
   card — the old `opacity: 0.55` took the flavour text under the floor.

   Four states, four looks: locked (dashed rim, grey mark, a bar counting
   toward the collapse that opens it), open (red rim and the one button that
   matters), running (hazard edge, live meter, pulsing badge), cleared (green
   rim, green mark, and the banked reward). Only the two states with something
   to measure carry a meter at all. */

:root {
  --challenge: #ff6b6b; /* the system colour: rims, marks, the running state */
  --challenge-deep: #5e2130; /* its shadow: hazard tape, plates, bar ends */
}

.panel--challenges {
  border-color: color-mix(in srgb, var(--challenge) 34%, var(--line));
  background: linear-gradient(180deg, color-mix(in srgb, var(--challenge) 7%, var(--panel)) 0, var(--panel) 240px);
  box-shadow: var(--shadow), 0 0 44px -24px color-mix(in srgb, var(--challenge) 75%, transparent);
}
.challenge__head-mark { display: inline-flex; color: var(--challenge); }

/* The top-bar banner belongs to this system too, so it takes the system's
   colour rather than the generic warn amber it shared with comets. */
.readout__boost--challenge { color: var(--challenge); }

/* Hazard tape under the heading — the section announces itself before you have
   read a word of it. It crawls only while a challenge is actually running, and
   turns green once every challenge is cleared. */
.challenge-tape {
  height: 6px;
  margin: -4px 0 14px;
  border-radius: 999px;
  opacity: 0.5;
  background: repeating-linear-gradient(135deg, var(--challenge) 0 10px, var(--challenge-deep) 10px 20px);
  transition: opacity 0.3s ease;
}
.panel--challenges-running .challenge-tape {
  opacity: 0.95;
  animation: challenge-tape-crawl 1.6s linear infinite;
}
.panel--challenges-swept .challenge-tape {
  opacity: 0.8;
  background: repeating-linear-gradient(
    135deg,
    var(--good) 0 10px,
    color-mix(in srgb, var(--good) 40%, var(--bg)) 10px 20px
  );
}
@keyframes challenge-tape-crawl {
  to { background-position: 28.3px 0; }
}

/* --- Trials: the rank ladder over every challenge ---------------------------
   The one device on this panel that is *not* hostile, and it is painted that
   way on purpose. Every card below is red because every card below takes
   something away; this is the shelf they pay into, so it takes the gold the
   records rail on the Stats tab already uses for "a thing you have kept".

   It is a banner rather than a card: full width, above the list, so the first
   thing the panel says is what the whole set is worth rather than what the
   first challenge costs. */

.trials {
  display: grid;
  gap: 8px;
  padding: 12px 14px;
  margin: 0 0 16px;
  border: 1px solid color-mix(in srgb, var(--laurel) 34%, var(--line));
  border-radius: 12px;
  background:
    radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--laurel) 12%, transparent) 0, transparent 62%),
    var(--panel-2);
}
.trials__head { display: flex; align-items: center; gap: 10px; }
.trials__mark { display: inline-flex; color: var(--laurel); flex: none; }
/* A held rank lights its mark; an unranked save leaves it as furniture, so the
   first rank arriving is visible without reading a word. */
.trials--unranked .trials__mark { color: var(--muted); }
.trials__title { display: grid; gap: 2px; min-width: 0; flex: 1 1 auto; }
.trials__rank {
  font-size: 15px;
  font-weight: 650;
  letter-spacing: 0.02em;
  color: var(--laurel);
}
.trials--unranked .trials__rank { color: var(--text); }
.trials__rungs { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.trials__worth {
  display: grid;
  justify-items: end;
  gap: 1px;
  font-size: 18px;
  font-weight: 700;
  color: var(--laurel);
  font-variant-numeric: tabular-nums;
  flex: none;
  text-align: right;
}
/* The figure on its own said "x1.72" and nothing else. A number that large on
   a panel full of multipliers needs to say which one it is. */
.trials__worth .unit {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.trials--unranked .trials__worth { color: var(--muted); }
.trials .progress { margin: 0; }
/* Kept inside the ladder's own family. The first draft ran laurel → challenge
   red, and at a low fill all that showed was the red end — a progress bar
   toward a reward, drawn in the colour this game uses for danger. */
.trials .progress__bar {
  background: linear-gradient(90deg, color-mix(in srgb, var(--laurel) 55%, var(--good)), var(--laurel));
}
.trials .progress__label { margin: 0; font-size: 12px; }
.trials__flavor {
  margin: 0;
  font-size: 12px;
  font-style: italic;
  color: var(--muted);
}
/* Nothing to say yet: the flavour line only appears once a rank is held, so an
   unranked banner is three lines rather than four with a blank in it. */
.trials__flavor:empty { display: none; }
/* At phone width the unit label and the rungs count end up shoulder to
   shoulder with about eight pixels between them, and a longer rank name would
   close that. The line under the bar already says "all production", so the
   label is the half that goes. */
@media (max-width: 480px) {
  .trials__worth .unit { display: none; }
}

/* The tally: seven diamonds, one per challenge, in table order. The whole
   set's state at a glance, next to the count that spells it out. */
.challenge-tally { display: flex; align-items: center; gap: 12px; }
.challenge-pips { display: flex; align-items: center; gap: 7px; }
.challenge-pip {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  transform: rotate(45deg);
  border: 1px solid var(--line);
  background: var(--bg-soft);
  transition: background 0.3s ease, border-color 0.3s ease;
}
.challenge-pip--open { border-color: color-mix(in srgb, var(--challenge) 65%, transparent); }
.challenge-pip--running {
  border-color: var(--challenge);
  background: var(--challenge);
  animation: challenge-pip-pulse 1.6s ease-in-out infinite;
}
.challenge-pip--cleared { border-color: color-mix(in srgb, var(--good) 70%, transparent); background: var(--good); }
/* Climbed more than once: the pip keeps its green but gains a halo, so the
   strip reads depth as well as breadth without adding a second row. */
.challenge-pip--deep { box-shadow: 0 0 0 2px color-mix(in srgb, var(--good) 32%, transparent); }
@keyframes challenge-pip-pulse {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 1; }
}

/* --- The card ------------------------------------------------------------- */

.cards--challenges { gap: 14px; }
.card--challenge {
  position: relative;
  isolation: isolate;
  gap: 10px;
  padding: 14px 16px;
  border-color: color-mix(in srgb, var(--challenge) 20%, var(--line));
  background: color-mix(in srgb, var(--challenge) 4%, var(--panel-2));
  transition: border-color 0.2s ease, background 0.3s ease, box-shadow 0.3s ease;
}

.challenge__head { display: flex; align-items: flex-start; gap: 12px; }

/* The challenge's own mark, on a plate that takes the state's colour. */
.challenge__mark {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 12px;
  color: var(--challenge);
  background: color-mix(in srgb, var(--challenge) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--challenge) 38%, transparent);
  transition: color 0.3s ease, background 0.3s ease, border-color 0.3s ease;
}
.challenge__title { min-width: 0; display: grid; gap: 3px; }
.challenge__rule { margin: 0; font-size: 13px; color: var(--text); }
.card--challenge .card__flavor { font-size: 12.5px; font-style: italic; }

/* The state chip. Also the seam: when challenges become re-enterable this is
   where the rung goes, and nothing around it has to move. */
.challenge__badge {
  flex: none;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--challenge);
  border: 1px solid color-mix(in srgb, var(--challenge) 45%, var(--line));
  background: color-mix(in srgb, var(--challenge) 12%, transparent);
}
.challenge__badge-mark { display: inline-flex; }

/* One pill per hostile rule, each with its own mark, so a stacked challenge
   shows at a glance how many ways it is stacked. */
.card--challenge .pills { margin: 2px 0 0; }
.card--challenge .pill--hostile {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px 5px 9px;
  color: var(--challenge);
  border-color: color-mix(in srgb, var(--challenge) 45%, var(--line));
  background: color-mix(in srgb, var(--challenge) 8%, var(--panel-2));
}
.card--challenge .pill__label { color: var(--challenge); }

/* --- The rung ladder ------------------------------------------------------
   One bar per rung cleared, then a hollow one for the rung on offer. A fixed
   bar height and a fixed row height, so climbing a rung never moves a number
   below it. Capped at six bars in JS; past that the label counts instead. */
.challenge__rungs {
  display: flex;
  align-items: center;
  gap: 5px;
  min-height: 14px;
  margin: 2px 0 0;
}
.challenge__rungs[hidden] { display: none; }
.challenge__rung {
  width: 16px;
  height: 8px;
  border-radius: 2px;
  border: 1px solid color-mix(in srgb, var(--challenge) 40%, var(--line));
  background: transparent;
}
.challenge__rung--done {
  border-color: transparent;
  background: linear-gradient(90deg, var(--challenge-deep), var(--challenge));
}
.challenge__rung--next { border-style: dashed; }
.challenge__rung--climbing {
  border-style: solid;
  border-color: var(--challenge);
  background: color-mix(in srgb, var(--challenge) 30%, transparent);
  animation: challenge-edge-pulse 1.8s ease-in-out infinite;
}
.challenge__rungs-label {
  margin-left: 4px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.card--challenge-cleared .challenge__rung--done {
  background: linear-gradient(90deg, color-mix(in srgb, var(--good) 45%, var(--panel)), var(--good));
}
.card--challenge-cleared .challenge__rungs-label { color: color-mix(in srgb, var(--good) 70%, var(--muted)); }
.card--challenge-cleared .challenge__rung--next { border-color: color-mix(in srgb, var(--good) 45%, var(--line)); }

.challenge__meter { margin: 4px 0 0; height: 8px; }
.challenge__meter .progress__bar {
  background: linear-gradient(90deg, var(--challenge-deep), var(--challenge));
}
.challenge__status { margin: 0; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Target, reward, and whatever a later mechanic wants to add: a keyed list of
   icon / label / value rows, not a hand-built paragraph. */
.challenge__meta { margin: 2px 0 0; display: grid; gap: 7px; }
.challenge__meta-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  padding-top: 7px;
  border-top: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
}
.challenge__meta-row:first-child { padding-top: 0; border-top: 0; }
.challenge__meta-term {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.challenge__meta-mark { display: inline-flex; color: color-mix(in srgb, var(--challenge) 70%, var(--muted)); }
.challenge__meta-value {
  margin: 0;
  font-size: 13px;
  text-align: right;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.challenge__action { justify-content: center; font-weight: 600; }
.challenge__action[hidden] { display: none; }
.challenge__action--enter {
  border-color: color-mix(in srgb, var(--challenge) 55%, transparent);
  background: color-mix(in srgb, var(--challenge) 14%, var(--panel));
}
.challenge__action--enter:hover:not(:disabled) {
  background: var(--challenge);
  border-color: transparent;
  color: #2a0b0b;
}

/* --- Locked: dimmed, dashed, and counting toward the collapse that opens it */

.card--challenge-locked {
  border-style: dashed;
  border-color: var(--line);
  background: var(--panel-2);
}
.card--challenge-locked .challenge__mark {
  color: var(--muted);
  background: color-mix(in srgb, var(--muted) 7%, transparent);
  border-color: var(--line);
  opacity: 0.75;
}
.card--challenge-locked .challenge__badge {
  color: var(--muted);
  border-color: var(--line);
  background: transparent;
}
.card--challenge-locked .pill--hostile {
  color: var(--muted);
  border-color: var(--line);
  background: var(--panel-2);
}
.card--challenge-locked .pill__label { color: var(--muted); }
.card--challenge-locked .challenge__meta-mark { color: var(--muted); }
.card--challenge-locked .challenge__meter .progress__bar {
  background: linear-gradient(90deg, var(--muted), var(--accent));
}

/* --- Open: the rim brightens, and the row carries the one button that acts - */

.card--challenge-open { border-color: color-mix(in srgb, var(--challenge) 45%, transparent); }
.card--challenge-open:hover { border-color: color-mix(in srgb, var(--challenge) 70%, transparent); }

/* --- Running: the loud one ------------------------------------------------ */

.card--challenge-running {
  border-color: var(--challenge);
  background: color-mix(in srgb, var(--challenge) 10%, var(--panel-2));
  box-shadow: 0 0 28px -12px color-mix(in srgb, var(--challenge) 85%, transparent);
}
/* A strip of hazard tape down the sharp edge of the card. Absolutely
   positioned inside the card's own stacking context, so it can never move a
   number or cover one. */
.card--challenge-running::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 5px;
  z-index: -1;
  border-radius: var(--radius) 0 0 var(--radius);
  background: repeating-linear-gradient(180deg, var(--challenge) 0 9px, var(--challenge-deep) 9px 18px);
  animation: challenge-edge-pulse 1.8s ease-in-out infinite;
}
.card--challenge-running .challenge__status { color: var(--challenge); font-weight: 600; }
.card--challenge-running .challenge__badge {
  color: #2a0b0b;
  background: var(--challenge);
  border-color: transparent;
  animation: challenge-edge-pulse 1.8s ease-in-out infinite;
}
@keyframes challenge-edge-pulse {
  0%, 100% { opacity: 0.62; }
  50% { opacity: 1; }
}

/* --- Cleared: green, because green already means earned everywhere else --- */

.card--challenge-cleared {
  border-color: color-mix(in srgb, var(--good) 40%, transparent);
  background: color-mix(in srgb, var(--good) 6%, var(--panel-2));
}
.card--challenge-cleared .challenge__mark {
  color: var(--good);
  background: color-mix(in srgb, var(--good) 12%, transparent);
  border-color: color-mix(in srgb, var(--good) 40%, transparent);
}
.card--challenge-cleared .challenge__badge {
  color: var(--good);
  border-color: color-mix(in srgb, var(--good) 45%, var(--line));
  background: color-mix(in srgb, var(--good) 12%, transparent);
}
.card--challenge-cleared .challenge__status { color: var(--good); }
.card--challenge-cleared .challenge__meta-mark { color: color-mix(in srgb, var(--good) 70%, var(--muted)); }
.card--challenge-cleared .challenge__meter .progress__bar {
  background: linear-gradient(90deg, color-mix(in srgb, var(--good) 55%, var(--panel)), var(--good));
}
.card--challenge-cleared .pill--hostile {
  color: var(--muted);
  border-color: var(--line);
  background: var(--panel-2);
}
.card--challenge-cleared .pill__label { color: var(--muted); }

/* On a phone the head stays one row — mark, title, chip — and the title text
   wraps inside it rather than the chip dropping onto a line of its own. The
   meta rows do stack, because a label and a long reward sentence will not sit
   side by side at 390px without one of them being squeezed to nothing. */
@media (prefers-reduced-motion: reduce) {
  .challenge__rung--climbing { animation: none; }
}

@media (max-width: 560px) {
  /* Mark, name and chip on the first row; the rule drops to a full-width
     second row instead of being squeezed into a 120px column. */
  .challenge__head {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    column-gap: 10px;
    row-gap: 6px;
  }
  .challenge__title { display: contents; }
  .challenge__mark { grid-area: 1 / 1; }
  .challenge__head .card__name { grid-area: 1 / 2; }
  .challenge__badge { grid-area: 1 / 3; margin-left: 0; }
  .challenge__rule { grid-area: 2 / 1 / 3 / -1; }
  .challenge__mark { width: 42px; height: 42px; }
  .challenge__meta-row { flex-direction: column; align-items: flex-start; gap: 3px; }
  .challenge__meta-value { text-align: left; }
}

/* --- Expeditions ------------------------------------------------------------ */

.panel--expeditions {
  border-color: color-mix(in srgb, var(--expedition) 30%, var(--line));
}
.card--expedition.card--locked { color: var(--muted); }
.card--expedition.card--locked .card__mark { color: var(--muted); }
.card--expedition.card--active-expedition {
  border-color: color-mix(in srgb, var(--expedition) 55%, transparent);
  background: color-mix(in srgb, var(--expedition) 8%, var(--panel-2));
}
.card--expedition.card--ready {
  border-color: color-mix(in srgb, var(--expedition) 80%, transparent);
  background: color-mix(in srgb, var(--expedition) 16%, var(--panel-2));
  animation: expeditionReadyGlow 1.8s ease-in-out infinite;
}
.card--expedition .card__mark { color: var(--expedition); }
.card--expedition .card__stats[hidden] { display: none; }
.card--expedition .progress--sm[hidden] { display: none; }
.card--expedition .progress--sm .progress__bar { background: linear-gradient(90deg, var(--expedition), var(--accent)); }
.expedition__status { margin: 0; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.card--active-expedition .expedition__status { color: var(--expedition); font-weight: 600; }
.card--ready .expedition__status { color: var(--expedition); font-weight: 700; }
/* --- Stepper (assign crew to a destination) ------------------------------- */

.stepper {
  display: flex;
  align-items: center;
  gap: 2px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg-soft);
  padding: 2px;
  flex: none;
}
.stepper__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--expedition);
  cursor: pointer;
}
.stepper__btn:hover:not(:disabled) { background: color-mix(in srgb, var(--expedition) 18%, transparent); }
/* The crew stepper is a pair of 40px buttons, four short of the 44px touch
   target `CLAUDE.md` §5 states — and it is a control people press repeatedly to
   move surveyors one at a time, which is exactly where a missed tap costs. Same
   treatment as `.chip`: full size on fingers, unchanged on a mouse. */
@media (pointer: coarse) {
  .stepper__btn { width: 44px; height: 44px; }
}

.stepper__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Fixed width and tabular figures: the row must not shuffle as crew changes. */
.stepper__value {
  min-width: 78px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 14px;
}

.expedition__stake { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 4px; }
.expedition__stake[hidden] { display: none; }
/* Shown only with the standing order switched off, so the bay is still
   playable by hand. `.buy` sets display:flex, which beats the UA [hidden]. */
.expedition__collect { margin-top: 6px; }
.expedition__collect[hidden] { display: none; }
.expedition__input {
  flex: 1 1 140px;
  min-width: 0;
  min-height: 44px;
  padding: 0 12px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-variant-numeric: tabular-nums;
}
.expedition__stake .buy { flex: 1 1 110px; min-width: 0; }
.expedition__launch[hidden] { display: none; }
.card--expedition .buy[hidden],
.card--expedition .expedition__stake[hidden] { display: none; }
.expedition__preview {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--expedition);
  font-variant-numeric: tabular-nums;
}
.expedition__preview:empty { display: none; }

.card--surveyor-bay {
  margin-bottom: 12px;
  border-color: color-mix(in srgb, var(--expedition) 35%, var(--line));
  background: color-mix(in srgb, var(--expedition) 6%, var(--panel-2));
}
.card--surveyor-bay .card__mark { color: var(--expedition); }
.card--surveyor-bay .expedition__stake .buy { flex: 0 0 auto; }

@keyframes expeditionReadyGlow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--expedition) 45%, transparent); }
  50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--expedition) 0%, transparent); }
}

.collapse-flash {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--shard) 60%, transparent), transparent 70%);
}
.collapse-flash--play { animation: collapseFlash 0.7s ease; }
@keyframes collapseFlash {
  0% { opacity: 0; transform: scale(0.35); }
  35% { opacity: 1; transform: scale(1.05); }
  100% { opacity: 0; transform: scale(1.7); }
}
/* A collapse that also lands a collapse milestone (every 5th) gets a second,
   wider ring in the good colour on top of the ordinary shard flash. */
.collapse-flash--big {
  background:
    radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--good) 55%, transparent), transparent 55%),
    radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--shard) 60%, transparent), transparent 70%);
}
.collapse-flash--big.collapse-flash--play { animation: collapseFlashBig 1s ease; }
@keyframes collapseFlashBig {
  0% { opacity: 0; transform: scale(0.3); }
  30% { opacity: 1; transform: scale(1.1); }
  100% { opacity: 0; transform: scale(2.1); }
}
/* A legendary achievement (or clearing the whole set) reuses the same
   full-screen overlay in the rarity's own gold rather than the shard's
   violet, so the rarer moment reads as a different colour, not just a
   bigger version of the same flash. */
.collapse-flash--legendary {
  background: radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--rarity-legendary) 60%, transparent), transparent 65%);
}
.collapse-flash--legendary.collapse-flash--play { animation: collapseFlashLegendary 0.9s ease; }
@keyframes collapseFlashLegendary {
  0% { opacity: 0; transform: scale(0.3); }
  30% { opacity: 1; transform: scale(1.1); }
  100% { opacity: 0; transform: scale(1.9); }
}

/* --- Modal & toasts ----------------------------------------------------- */

.modal {
  position: fixed;
  inset: 0;
  display: none;
  place-items: center;
  background: rgb(4 6 14 / 70%);
  z-index: 20;
}
.modal--open { display: grid; }
.modal__box {
  max-width: 380px;
  margin: 20px;
  padding: 24px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--panel);
  box-shadow: var(--shadow);
}
.modal__box h2 { margin-top: 0; }

/* === TOAST STACK ========================================================= *
 *
 * Three tiers, one shape. See src/ui/toast.js for the rules; this is only how
 * they look.
 *
 *   .toast--notice    an icon, a title, a consequence line, a dismiss button
 *                     and a hairline that drains as the timer runs.
 *   .toast--event     the same row plus a lit rim and a one-off reveal sweep.
 *   .toast--overflow  the "+N more" summary row; its body is a press target.
 *
 * The stack is a fixed-width column so a row whose number grows a digit never
 * changes the width of anything — the whole stack is the same width always.
 * `kind` is carried on one custom property (--toast-accent) so the glyph, the
 * rim and the hairline all take the same colour without three more rules.
 * ========================================================================= */

.toasts {
  --toast-radius: 10px;
  --toast-pad: 10px;
  --toast-gap: 8px;
  --toast-accent: var(--accent);
  --toast-life: 3200ms;

  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 30;
  display: grid;
  gap: var(--toast-gap);
  width: min(360px, calc(100vw - 36px));
  justify-items: stretch;
  /* A tall stack must not swallow presses on the game behind it. */
  pointer-events: none;
}

.toast {
  position: relative;
  display: grid;
  grid-template-columns: 26px 1fr 32px;
  align-items: start;
  gap: 2px 10px;
  padding: var(--toast-pad) var(--toast-pad) calc(var(--toast-pad) + 2px);
  border-radius: var(--toast-radius);
  background: var(--panel-2);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  overflow: hidden;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast--in { opacity: 1; transform: none; }
.toast--out { opacity: 0; transform: translateY(4px); }

/* One property carries the whole colour identity of a row. */
.toast--info { --toast-accent: var(--accent); }
.toast--good { --toast-accent: var(--good); }
.toast--warn { --toast-accent: var(--warn); }
.toast--rare { --toast-accent: var(--rarity-rare); }
.toast--legendary { --toast-accent: var(--rarity-legendary); }
.toast { border-color: color-mix(in srgb, var(--toast-accent) 34%, var(--line)); }

.toast__glyph {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 8px;
  color: var(--toast-accent);
  background: color-mix(in srgb, var(--toast-accent) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--toast-accent) 26%, transparent);
}

.toast__body {
  display: grid;
  gap: 2px;
  min-width: 0;
  padding: 2px 0;
  align-self: center;
}
.toast__body--press {
  font: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
  /* The row is 44px tall already; the button fills it so the target is real. */
  min-height: 40px;
  align-content: center;
  padding: 0;
  border-radius: 6px;
}
.toast__body--press:hover .toast__title { color: var(--toast-accent); }

.toast__title {
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text);
  overflow-wrap: anywhere;
}
/* The consequence line: what it bought, not what happened. */
.toast__detail {
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.toast__detail:empty { display: none; }

/* 32px of column, 44px of target: the padding reaches out past the row. */
.toast__close {
  justify-self: end;
  align-self: start;
  position: relative;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  margin: -4px -2px 0 0;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.toast__close::after {
  content: '';
  position: absolute;
  inset: -6px;
  min-width: 44px;
  min-height: 44px;
}
.toast__close:hover,
.toast__close:focus-visible {
  color: var(--text);
  background: color-mix(in srgb, var(--toast-accent) 16%, transparent);
}

/* The hairline is the timer made visible — and hovering visibly stops it. */
.toast__life {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  transform-origin: left center;
  background: linear-gradient(90deg, var(--toast-accent), color-mix(in srgb, var(--toast-accent) 30%, transparent));
  animation: toastLife var(--toast-life) linear forwards;
}
.toast--paused .toast__life { animation-play-state: paused; }
@keyframes toastLife {
  from { transform: scaleX(1); }
  to { transform: scaleX(0); }
}

/* --- The event tier ------------------------------------------------------ */

.toast--event {
  padding-top: calc(var(--toast-pad) + 2px);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--toast-accent) 13%, transparent), transparent 60%),
    var(--panel-2);
  border-color: color-mix(in srgb, var(--toast-accent) 62%, transparent);
  box-shadow: var(--shadow), 0 0 0 1px color-mix(in srgb, var(--toast-accent) 22%, transparent);
}
.toast--event .toast__title { font-size: 14.5px; }
.toast--event .toast__glyph {
  background: color-mix(in srgb, var(--toast-accent) 22%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--toast-accent) 45%, transparent);
}
/* A single 420ms sweep across the rim, then it is gone. Reserve it. */
.toast--event::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 30%,
    color-mix(in srgb, var(--toast-accent) 38%, transparent) 50%,
    transparent 70%
  );
  transform: translateX(-100%);
  animation: toastReveal 420ms ease-out 60ms 1;
}
@keyframes toastReveal {
  to { transform: translateX(100%); }
}

/* --- The "+N more" row --------------------------------------------------- */

.toast--overflow {
  --toast-accent: var(--muted);
  background: color-mix(in srgb, var(--panel) 80%, var(--panel-2));
  border-style: dashed;
}
.toast--overflow .toast__title { color: var(--muted); font-weight: 600; }
.toast--overflow .toast__life { display: none; }

/* Motion is decoration. The hairline is a clock, so it goes rather than
   snapping to empty the instant it appears. */
@media (prefers-reduced-motion: reduce) {
  .toast__life { display: none; }
  .toast--event::before { display: none; }
}

@media (max-width: 760px) {
  .toasts {
    left: 12px;
    right: 12px;
    bottom: 12px;
    width: auto;
  }
}
/* === end toast stack ===================================================== */

/* Two rows on a phone: identity and drawers above, the numbers and the button
 * you press below. Nothing resizes — the grid areas just re-flow. */
@media (max-width: 760px) {
  .topbar {
    grid-template-areas:
      "brand tools"
      "readout action";
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .topbar__tools { justify-self: end; }
  /* Two rows: the collapsed button shares its row with the readout, so it
     takes what is left rather than everything. Still 44px+ to press. */
  .topbar__action { max-width: 190px; }
  .collect--mini { min-height: 44px; padding: 9px 12px; font-size: 14px; }
}

@media (max-width: 600px) {
  .topbar { padding: 12px 14px 10px; }
  .collect--hero { padding: 18px; font-size: 17px; min-height: 60px; }
  .readout__amount { font-size: 21px; }
  .brand__name { font-size: 15px; }
  .brand__mark { font-size: 19px; }
  .layout { padding: 18px 14px 60px; }
  .panel { padding: 14px; }
  .panel__head h2 { flex: 1 1 100%; }
  .panel__head-title { flex: 1 1 100%; }
  .panel__head-title h2 { flex: none; }
}

/* --- Launch sequence ----------------------------------------------------- 
   The boot screen (index.html #preload, driven by src/ui/preloader.js). It is
   the only full-bleed set piece in the game, so it is allowed to be the most
   decorated thing here: a drifting starfield, an orbiting mark, and the
   release notes when there are any.

   Gated on `html.js` so a browser that never runs the module — index.html
   opened straight off disk, most obviously — shows the game rather than a
   loading screen nothing can dismiss. */

.preload { display: none; }

.js .preload {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 24px;
  background:
    radial-gradient(900px 620px at 50% -5%, #1e2a5e 0%, transparent 65%),
    radial-gradient(700px 520px at 15% 110%, #37205e 0%, transparent 60%),
    var(--bg);
  opacity: 1;
  transition: opacity 0.42s ease;
}
.js .preload[hidden] { display: none; }
.preload--done { opacity: 0; pointer-events: none; }

/* Two parallax star layers, drawn entirely from repeating radial gradients —
   no image, no canvas, nothing to load on the screen whose whole job is to be
   up before anything has loaded. */
.preload__sky {
  position: absolute;
  inset: -10%;
  /* Two layers whose tile sizes share no small common multiple, so the
     repeat is long enough not to read as a grid. */
  background-image:
    radial-gradient(1.5px 1.5px at 12% 22%, rgb(231 236 255 / 90%) 50%, transparent 51%),
    radial-gradient(1.1px 1.1px at 37% 9%, rgb(201 166 255 / 70%) 50%, transparent 51%),
    radial-gradient(1.7px 1.7px at 63% 41%, rgb(231 236 255 / 75%) 50%, transparent 51%),
    radial-gradient(1px 1px at 81% 17%, rgb(138 180 255 / 85%) 50%, transparent 51%),
    radial-gradient(1.3px 1.3px at 26% 68%, rgb(231 236 255 / 65%) 50%, transparent 51%),
    radial-gradient(1.2px 1.2px at 91% 78%, rgb(201 166 255 / 60%) 50%, transparent 51%),
    radial-gradient(1.6px 1.6px at 54% 88%, rgb(231 236 255 / 55%) 50%, transparent 51%),
    radial-gradient(1px 1px at 7% 53%, rgb(138 180 255 / 70%) 50%, transparent 51%);
  background-size: 517px 431px, 517px 431px, 517px 431px, 517px 431px,
    293px 359px, 293px 359px, 293px 359px, 293px 359px;
  opacity: 0.9;
  animation: preload-drift 140s linear infinite;
  /* Darker at the edges so the panel sits in the middle of something. */
  mask-image: radial-gradient(120% 100% at 50% 45%, #000 35%, transparent 100%);
}
@keyframes preload-drift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-517px, -431px, 0); }
}

.preload__panel {
  position: relative;
  width: min(420px, 100%);
  text-align: center;
}
.preload__panel[hidden] { display: none; }

.preload__emblem {
  position: relative;
  width: 128px;
  height: 128px;
  margin: 0 auto 18px;
  display: grid;
  place-items: center;
  /* The forge itself: a bloom the rings orbit, so the emblem reads as a light
     source rather than as a spinner. */
  background: radial-gradient(circle at 50% 50%, rgb(201 166 255 / 26%) 0%, rgb(138 180 255 / 10%) 42%, transparent 68%);
}
.preload__orbit { position: absolute; inset: 0; width: 128px; height: 128px; }
.preload__orbit circle { fill: none; }
.preload__orbit-track { stroke: var(--line); stroke-width: 1.5; }
.preload__orbit-track--inner { stroke: color-mix(in srgb, var(--line) 60%, transparent); }
.preload__orbit-arc {
  stroke: var(--accent-2);
  stroke-width: 2.5;
  stroke-linecap: round;
  /* r=54 -> circumference ~339. A 70-unit dash is a fifth of the ring. */
  stroke-dasharray: 70 269;
  transform-origin: 64px 64px;
  animation: preload-spin 2.4s cubic-bezier(0.5, 0.05, 0.5, 0.95) infinite;
}
.preload__orbit-arc--inner {
  stroke: var(--accent);
  stroke-width: 2;
  /* r=40 -> circumference ~251. A 40-unit dash, counter-rotating. */
  stroke-dasharray: 40 211;
  animation-duration: 3.6s;
  animation-direction: reverse;
}
.preload__orbit-spin {
  transform-origin: 64px 64px;
  animation: preload-spin 6s linear infinite reverse;
}
.preload__orbit-dot { fill: var(--accent); }
@keyframes preload-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.preload__mark {
  font-size: 40px;
  color: var(--accent-2);
  text-shadow: 0 0 24px rgb(201 166 255 / 55%);
  animation: preload-pulse 3.2s ease-in-out infinite;
}
@keyframes preload-pulse {
  0%, 100% { opacity: 0.75; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.08); }
}

.preload__title { margin: 0; font-size: 26px; letter-spacing: 0.04em; }
.preload__version {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.preload__progress { margin: 22px 0 10px; }
.preload__step { margin: 0; color: var(--muted); font-size: 13px; min-height: 18px; }
.preload--failed .preload__step { color: var(--warn); }
.preload--failed .preload__progress .progress__bar { background: var(--warn); }

/* --- What's new ---------------------------------------------------------- */

.preload__news {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(560px, 100%);
  max-height: min(82vh, 760px);
  padding: 24px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  box-shadow: var(--shadow);
  backdrop-filter: blur(6px);
}
.preload__news[hidden] { display: none; }
.preload__news-head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.preload__news-icon { color: var(--accent-2); display: inline-flex; }
.preload__news-title { margin: 0; font-size: 19px; }
.preload__news-more { color: var(--muted); font-size: 12px; margin: 12px 0 0; }
.preload__continue {
  margin-top: 18px;
  width: 100%;
  flex: none;
  border-color: var(--accent);
  color: var(--accent);
}
.preload__continue:hover { background: var(--accent); color: #08101f; border-color: transparent; }

/* --- Release notes (boot screen and Settings share these) ---------------- */

.releases { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 12px; align-content: start; }
/* Only the list scrolls, so "Enter the foundry" is always on screen. */
.preload__news .releases { overflow-y: auto; flex: 1; min-height: 0; padding-right: 4px; }
.release {
  padding: 14px 16px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--panel-2);
  /* A quiet accent rail so a stack of notes reads as a timeline, not a wall. */
  border-left: 3px solid var(--accent-2);
}
.release__head { display: flex; align-items: center; gap: 9px; }
.release__icon { color: var(--accent-2); display: inline-flex; }
.release__title { margin: 0; font-size: 15px; flex: 1; }
.release__version {
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg-soft);
}
.release__notes { margin: 8px 0 0; padding-left: 18px; color: var(--muted); font-size: 13px; }
.release__notes ::marker, .release__notes li::marker { color: var(--accent-2); }
.release__notes li + li { margin-top: 6px; }

.version-line { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.version-line .hint { margin: 0; font-variant-numeric: tabular-nums; }

@media (max-width: 620px) {
  .preload__news { padding: 18px; }
  .preload__news-title { font-size: 17px; }
  .release__title { font-size: 14px; }
  .preload__emblem { width: 104px; height: 104px; }
  .preload__orbit { width: 104px; height: 104px; }
  .preload__mark { font-size: 38px; }
}

/* --- The Crucible -------------------------------------------------------
   The fifth system colour identity, and the only warm one: molten ember
   against everyone else's navy, violet, teal, cyan and gold. The panel keeps
   its own warm ground rather than only tinting the cards, because a foundry
   that melts things down should not look like the shop next door. */

.panel--crucible {
  border-color: color-mix(in srgb, var(--crucible) 34%, var(--line));
  background:
    radial-gradient(560px 260px at 50% -30%, color-mix(in srgb, var(--crucible) 16%, transparent) 0%, transparent 70%),
    var(--panel);
}
.panel--crucible .panel__head h2 { color: var(--crucible); }

.ingot-readout { color: var(--crucible); }
.ingot-readout span:first-child { display: inline-flex; vertical-align: -3px; }

/* The panel's headline: what a fresh run would open at, not the wallet. */
.crucible-opening {
  margin: 14px 0 4px;
  padding: 16px 18px;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--crucible) 40%, var(--line));
  background: linear-gradient(180deg, color-mix(in srgb, var(--crucible) 13%, var(--panel-2)), var(--panel-2));
  text-align: center;
}
.crucible-opening__label {
  margin: 0;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.crucible-opening__rate {
  margin: 4px 0 6px;
  font-size: 30px;
  font-weight: 700;
  color: var(--crucible);
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 22px color-mix(in srgb, var(--crucible) 45%, transparent);
}
.crucible-opening .progress__label { margin: 0; font-size: 12px; }

.crucible-melt {
  margin: 12px 0 4px;
  padding: 12px 16px;
  border-radius: var(--radius);
  border: 1px dashed color-mix(in srgb, var(--crucible) 34%, var(--line));
  background: var(--panel-2);
}
.crucible-melt__line {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 14px;
}
.crucible-melt__line span:first-child { color: var(--crucible); display: inline-flex; }
.crucible-melt__line strong { color: var(--crucible); font-variant-numeric: tabular-nums; }
.crucible-melt .progress__label { margin: 6px 0 0; font-size: 12px; }

/* Residue — what a collapse leaves behind. Stardust-coloured rather than warn
   or crucible: it is the one line in the Collapse panel that is a gain. */
.hint--residue { color: var(--accent); }
.hint--melt { color: var(--crucible); margin: -8px 0 12px; }

.card--pattern {
  border-left: 3px solid color-mix(in srgb, var(--crucible) 70%, transparent);
}
.card--pattern .card__mark { color: var(--crucible); }
.card--pattern .card__count { color: var(--crucible); font-variant-numeric: tabular-nums; }
.card--pattern.card--complete { color: var(--muted); border-left-color: var(--good); }
.card--pattern.card--complete .card__mark { color: var(--muted); }
.card--pattern.card--complete .card__count { color: var(--good); }

/* `.buy` sets its own display, so the plain `hidden` *attribute* cannot hide
   it on its own. The class utility is `!important` now; the attribute is not,
   so this one stays. */
.buy[hidden] { display: none; }

.buy--pattern { border-color: color-mix(in srgb, var(--crucible) 55%, transparent); color: var(--crucible); }
.buy--pattern:hover:not(:disabled) { background: var(--crucible); color: #180a04; border-color: transparent; }

/* Rung pips: a pattern's depth reads before the words do. */
.pips { display: flex; gap: 6px; margin: 10px 0 4px; }
.pip {
  flex: 1;
  height: 6px;
  border-radius: 999px;
  background: var(--bg-soft);
  border: 1px solid var(--line);
}
.pip--filled {
  background: linear-gradient(90deg, color-mix(in srgb, var(--crucible) 75%, transparent), var(--crucible));
  border-color: transparent;
  box-shadow: 0 0 10px color-mix(in srgb, var(--crucible) 45%, transparent);
}

/* Castings: queued before a collapse, spent on the run right after it. */
.casting-boost {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 0 4px;
  padding: 10px 14px;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--crucible) 45%, var(--line));
  background: color-mix(in srgb, var(--crucible) 12%, var(--panel-2));
}
.casting-boost svg { color: var(--crucible); flex-shrink: 0; }
.casting-boost__text { flex: 1; font-size: 13px; color: var(--text); }
.casting-boost__bar {
  width: 72px;
  height: 6px;
  border-radius: 999px;
  background: var(--bg-soft);
  overflow: hidden;
  flex-shrink: 0;
}
.casting-boost__fill {
  height: 100%;
  background: var(--crucible);
  transition: width 0.2s linear;
}

.casting-queue {
  margin: 4px 0 14px;
}
.casting-queue .pips { margin: 0 0 6px; max-width: 160px; }
.casting-queue .progress__label { margin: 0; font-size: 12px; color: var(--muted); }

/* The Condense control belongs to the Crucible, so it wears its colour now
   rather than the violet it borrowed from the prestige shop. */
.card__condense .condense { border-color: color-mix(in srgb, var(--crucible) 50%, transparent); color: var(--crucible); }
.card__condense .condense:hover:not(:disabled) { background: color-mix(in srgb, var(--crucible) 18%, transparent); }
.card__condense .condense__hint { color: var(--muted); }

/* --- Laurel readout ------------------------------------------------------
   The Achievements panel's headline: how much of the wreath is filled and
   what it is currently paying. The multiplier is the biggest thing in the
   block because it is the number the player actually cares about. */

.laurels {
  display: flex;
  gap: 12px;
  align-items: center;
  padding: 12px 14px;
  margin-bottom: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background:
    radial-gradient(120% 160% at 0% 50%, rgb(169 217 119 / 12%) 0%, transparent 60%),
    var(--panel-2);
}

.laurels__mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  color: var(--laurel);
  background: rgb(169 217 119 / 10%);
  border: 1px solid rgb(169 217 119 / 25%);
}

.laurels__body { flex: 1 1 auto; min-width: 0; }

.laurels__line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}

.laurels__held {
  color: var(--laurel);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.laurels__mult {
  font-size: 1.35rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.laurels .progress__bar { background: linear-gradient(90deg, var(--laurel), var(--good)); }

.laurels__hint {
  margin: 6px 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}

/* --- Achievement categories ----------------------------------------------
   A second row of navigation *inside* a panel, so it has to read as
   subordinate to the tab bar above it: smaller, quieter, no fill until
   active. Wraps rather than scrolls — on a phone these are ten short chips
   and a horizontal scroller would hide half of them. */

.achievement-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 12px 0;
}

.chips--categories {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  flex: 1 1 auto;
}

.chip--category {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  position: relative;
}

.chip--category .chip__mark { display: inline-flex; opacity: 0.75; }
.chip--category .chip__count { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 0.78rem; }
.chip--category.chip--active .chip__count { color: inherit; }
.chip--complete .chip__count { color: var(--good); }

/* Same meaning as the topbar dot: something in here is waiting to be claimed. */
.chip__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--laurel);
}
.chip__dot[hidden] { display: none; }

.chip--toggle { flex: 0 0 auto; }
.chip--toggle[aria-pressed='true'] { color: var(--accent); border-color: var(--accent); }

.empty-state {
  margin: 24px 0;
  padding: 24px;
  text-align: center;
  color: var(--muted);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
}
.empty-state[hidden] { display: none; }

/* --- Panel groups --------------------------------------------------------
   A titled section *inside* a panel, for pages that hold two different kinds
   of thing. Expeditions is the first: a crew you own and missions you send it
   on are different nouns with different lifetimes, and rendering the crew as
   another card in the mission list made it read as a fifth expedition. */

.group { margin-top: 18px; }
.group:first-of-type { margin-top: 12px; }

.group__title {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.group__title::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--line);
}

.group__mark { display: inline-flex; color: var(--expedition); }

/* Pushed past the rule, so the section header reads title — line — status. */
.group__meta {
  order: 3;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--muted);
}


.surveyor__actions {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 10px;
}
.surveyor__actions .buy { flex: 1 1 auto; }

/* --- Workshop ------------------------------------------------------------
   The seventh system colour: a working amber, warmer than the Crucible's
   molten ember and cooler than the record gold, for the tab where the crew's
   haul turns into something. */

.panel--workshop {
  background:
    radial-gradient(120% 90% at 100% 0%, rgb(255 165 82 / 8%) 0%, transparent 55%),
    var(--panel);
}

/* Four fixed tiles: values update in place so a ticking number never reflows. */
.materials {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 8px;
  margin: 12px 0 16px;
}

.material {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--workshop);
}

.material--empty { color: var(--muted); opacity: 0.6; }
.material__mark { display: inline-flex; }
.material__name { color: var(--text); font-size: 0.85rem; }
.material--empty .material__name { color: var(--muted); }

.material__value {
  margin-left: auto;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* One pip per level, filled as you build — a row that shows its own state.
   **Namespaced**, because these are not the Crucible's pips. Both features had
   claimed the bare `.pips`/`.pip`, and this block being second in the sheet
   meant the Crucible's rung pips silently rendered at this size and in this
   colour instead of their own. `tests/css.test.js` catches the shape now. */
.pips--building { gap: 4px; margin: 8px 0 4px; }

.pip--building {
  width: 100%;
  height: 4px;
  border-radius: 2px;
  background: var(--line);
  border: 0;
}

.pip--building.pip--on { background: var(--workshop); }

.card--building.card--maxed { border-color: color-mix(in srgb, var(--workshop) 45%, var(--line)); }

/* A finished building kept a dead Build button. This is a state class rather
   than the hide utility, so it stays as a rule of its own. */
.card--maxed .buy { display: none; }

/* A bundle, not a single price: each material can go short on its own, so
   "you are missing Isotope" reads without doing the arithmetic. */
.costs { display: inline-flex; gap: 10px; margin-left: auto; align-items: center; }

/* A cost is a **chip**, not two glued spans. Read as bare marks-and-numbers,
   two costs side by side ran together into one figure: the Mill's build button
   printed `607` and `273` as `607▮273`, because `.buy__cost` had no gap and the
   13px mark between them looked like a thousands separator. The Descent's
   strata had a gap and still read as a run, because nothing said where one
   amount ended.

   The ground is what separates them; the gap alone was not enough. Everything
   that renders a bundle goes through this class — the Mill, the Descent, the
   Workshop and the line cards — so it lands in four places at once. */
.cost {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--line) 42%, transparent);
  font-variant-numeric: tabular-nums;
}

/* Was 0.8: the mark is the only thing saying *which* resource this is, and it
   is already the smallest element in the row. */
.cost__mark { display: inline-flex; }
.cost--short { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, transparent); }

/* The one bundle container that had no gap at all. It is a modifier rather
   than a rule on `.buy__cost` itself, because the same class also carries a
   plain single cost written by `setAmount` — a text node and a `.sym` — and a
   flex gap there would push the symbol off its own number. */
.buy__cost--bundle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* --- Automation switches -------------------------------------------------
   A real <button> with aria-pressed rather than a checkbox: it has to be
   keyboard-operable and announce its own state, and the track/knob is easier
   to keep legible than a restyled native control. */

.switches { display: grid; gap: 8px; margin: 12px 0 4px; }

.switch {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 56px;
  padding: 10px 14px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.switch:hover { border-color: var(--accent); }
.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.switch__mark { display: inline-flex; flex: none; color: var(--muted); }
.switch[aria-pressed='true'] .switch__mark { color: var(--good); }
.switch__body { flex: 1 1 auto; min-width: 0; }
.switch__name { font-weight: 600; }
.switch__note { font-size: 0.82rem; color: var(--muted); }

/* The track. Fixed size so toggling never moves the row. */
.switch__track {
  flex: none;
  position: relative;
  width: 44px;
  height: 24px;
  border-radius: 999px;
  background: var(--line);
  transition: background 120ms ease;
}

.switch__knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--muted);
  transition: transform 120ms ease, background 120ms ease;
}

.switch[aria-pressed='true'] .switch__track { background: color-mix(in srgb, var(--good) 40%, var(--line)); }
.switch[aria-pressed='true'] .switch__knob { background: var(--good); transform: translateX(20px); }

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

/* --- Automation controls -------------------------------------------------
   The badge next to the thing it automates, in three positions. They have to
   be distinguishable without reading the word after the gear, because a
   header can carry two of them (Collapse runs the Broker *and* the Clock) and
   the difference between "off" and "held by the master switch" is the whole
   reason the master switch is safe to press.

     on      shard violet, gear
     off     muted, gear, dimmed — a choice about this one
     paused  warn amber, pause bars — a choice about all of them

   Amber rather than a third dim grey: paused is a *live* state the player
   turned on somewhere else and will want to find again. */
.card__auto { text-transform: uppercase; white-space: nowrap; }
.card__auto-mark { display: inline-flex; }
.card__auto-mark svg { display: block; }

.card__auto--off { color: var(--muted); border-color: var(--line); opacity: 0.7; }
.card__auto--off:hover:not(:disabled) { opacity: 1; background: color-mix(in srgb, var(--muted) 20%, transparent); }

.card__auto--paused {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 50%, var(--line));
  background: color-mix(in srgb, var(--warn) 16%, transparent);
}
.card__auto--paused:hover:not(:disabled) { background: color-mix(in srgb, var(--warn) 30%, transparent); }

/* The master switch, and the rows it is holding. */
.switch--master { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
.switch--master .switch__name { font-size: 1.02rem; }
.switch--held {
  border-color: color-mix(in srgb, var(--warn) 55%, var(--line));
  background: color-mix(in srgb, var(--warn) 8%, var(--panel-2));
}
.switch--held[aria-pressed='false'] .switch__mark { color: var(--warn); }
.switch--held[aria-pressed='false'] .switch__track { background: color-mix(in srgb, var(--warn) 30%, var(--line)); }

/* A switch the player left on that the master is currently holding. The knob
   stays where they put it — the state is "on, not running", not "off".
   `[aria-pressed]` is carried here on purpose: the green on/off rules above are
   attribute selectors, so a bare `.switch--paused` loses to them on
   specificity and the row goes on reading as running. */
.switch--paused[aria-pressed='true'] .switch__mark { color: var(--warn); }
.switch--paused[aria-pressed='true'] .switch__track { background: color-mix(in srgb, var(--warn) 35%, var(--line)); }
.switch--paused[aria-pressed='true'] .switch__knob { background: var(--warn); }

/* The group control on a fold header: the same track, no body, no label. */
.switch--group {
  width: auto;
  min-height: 44px;
  min-width: 44px;
  justify-content: center;
  padding: 8px 10px;
  border-color: transparent;
  background: transparent;
}
.switch--group:hover:not(:disabled) { border-color: var(--accent); background: var(--panel-2); }
/* Half-lit: some on, some off. The track shows it as a partial fill so the
   next press ("fill the bench") is the obvious one. Half-lit means the group
   switch reads `aria-pressed="false"`, and saying so keeps this above the
   attribute rules it is layered over rather than under them. */
.switch--partial[aria-pressed='false'] .switch__track {
  background: linear-gradient(to right, color-mix(in srgb, var(--good) 40%, var(--line)) 50%, var(--line) 50%);
}
/* Half-lit, so the knob sits at half travel: at rest it read as plain "off"
   next to a header saying "8 of 9 on". 20px is the full throw (see
   `[aria-pressed='true']` above), so this is exactly half of it. */
.switch--partial[aria-pressed='false'] .switch__knob {
  background: color-mix(in srgb, var(--good) 60%, var(--muted));
  transform: translateX(10px);
}

/* --- Activity feed -------------------------------------------------------
   What the automations did, under the switches that did it. A scanned list,
   so the shape is fixed: a mark, an actor in the text colour, what it did in
   the muted one, and a right-aligned age on tabular figures so twenty rows of
   times line up instead of ragging. */
.feed { margin-top: 18px; }
.feed__head { display: flex; align-items: baseline; gap: 8px; }
.feed__legend { font-size: 0.72rem; color: var(--muted); letter-spacing: 0.04em; text-transform: uppercase; }

.feed__list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--panel);
  overflow: hidden;
}

.feed__row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  font-size: 0.86rem;
  border-left: 2px solid transparent;
}
.feed__row + .feed__row { border-top: 1px solid color-mix(in srgb, var(--line) 60%, transparent); }
.feed__mark { display: inline-flex; flex: none; color: var(--muted); }
.feed__body { flex: 1 1 auto; min-width: 0; }
.feed__actor { font-weight: 600; color: var(--text); }
.feed__text { color: var(--muted); margin-left: 6px; }
.feed__count {
  margin-left: 6px;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--accent-2);
  font-variant-numeric: tabular-nums;
}
.feed__count--hidden { display: none; }
.feed__age {
  flex: none;
  font-size: 0.74rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Three rows read differently on purpose, and the rim is the whole signal —
   a player scanning for "when did it collapse" is scanning for these. */
.feed__row--event {
  border-left-color: var(--shard);
  background: color-mix(in srgb, var(--shard) 7%, transparent);
}
.feed__row--event .feed__mark { color: var(--shard); }
.feed__row--away {
  border-left-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.feed__row--away .feed__mark { color: var(--accent); }
.feed__row--bad .feed__mark { color: var(--danger); }

/* --- Fold ----------------------------------------------------------------
   A section collapsed to its header, for lists that are long because they are
   finished. The header keeps the count and the group control on screen, so a
   folded bench still says what it is doing. */
.folds { display: grid; gap: 10px; margin: 12px 0 4px; }

.fold {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--panel);
  overflow: hidden;
}
.fold--held { border-color: color-mix(in srgb, var(--warn) 35%, var(--line)); }
/* A held bench: the group control goes amber with the rows inside it, so the
   page reads one state rather than green headers over amber rows. */
.fold--held .switch--group[aria-pressed='true'] .switch__track { background: color-mix(in srgb, var(--warn) 35%, var(--line)); }
.fold--held .switch--group[aria-pressed='true'] .switch__knob { background: var(--warn); }

.fold__head {
  display: flex;
  align-items: center;
  gap: 4px;
  padding-right: 6px;
}

.fold__toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 52px;
  padding: 10px 14px;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.fold__toggle:hover { background: var(--panel-2); }
.fold__toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.fold__mark { display: inline-flex; flex: none; color: var(--accent-2); }
/* Each bench in the colour of the layer it belongs to, so the four groups are
   tellable apart before the label is read — the same colours the nav and the
   panels already use for those sections. */
.fold[data-group='foundry'] .fold__mark { color: var(--accent); }
.fold[data-group='expeditions'] .fold__mark { color: var(--expedition); }
.fold[data-group='collapse'] .fold__mark { color: var(--shard); }
.fold[data-group='vise'] .fold__mark { color: var(--neutronium); }
.fold__title { display: flex; flex-direction: column; min-width: 0; }
.fold__name { font-weight: 600; }
.fold__count { font-size: 0.78rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* The chevron points down when open, right when shut. Fixed box either way,
   so the header height never moves. */
.fold__marker {
  display: inline-flex;
  flex: none;
  margin-left: auto;
  color: var(--muted);
  transform: rotate(-90deg);
  transition: transform 140ms ease;
}
.fold--open .fold__marker { transform: rotate(0deg); }
.fold__aside { display: flex; flex: none; align-items: center; }

/* The expander form: a control added to a heading that already exists, for the
   four lists that are long because the player finished them (Installed,
   Owned, Doctrines held, Refined). The count stays on screen while the list is
   away — a folded list still has to say how much is in it. */
.fold-head { display: flex; align-items: center; gap: 8px; }
.fold__expander {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 44px;
  min-width: 44px;
  justify-content: center;
  margin-left: auto;
  padding: 4px 8px;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
}
.fold__expander:hover:not(:disabled) { border-color: var(--line); background: var(--panel-2); color: var(--text); }
.fold__expander:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.fold__expander-count {
  font-size: 0.78rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}
/* The marker rotates on the expander itself rather than on a `.fold--open`
   ancestor, because there is no wrapper element here to carry the state. */
.fold__expander .fold__marker { transform: rotate(-90deg); }
.fold__expander--open .fold__marker { transform: rotate(0deg); }

.fold__body { padding: 0 10px 10px; display: grid; gap: 8px; }
.fold__body[hidden] { display: none; }

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

/* --- Navigation: two levels, fixed slots ---------------------------------
   Replaces the flat `.tabs` pill row, which wrapped to two desktop rows and
   four phone rows and — worse — moved its wrap point every time a section
   unlocked, so a returning player found the Vault somewhere new.

   From 900px up the nav is a grouped vertical sidebar in the margin the page
   used to waste: every unlocked section on screen at once, one click to any
   of them. Below 900px it is group pills (level 1) over the chip row of the
   open group (level 2) — two rows, always, however many sections exist.

   Rules this stylesheet has to keep (src/ui/app.js carries the same list):
     * a section keeps its slot whether it is unlocked or not — a locked slot
       shows `.nav__lock` and holds the space, so nothing ever reflows;
     * a group is hidden outright until its first child unlocks;
     * no overflow control at either size — no "More", no hamburger, ever;
     * in the sidebar, level 2 reads quieter than level 1 — a solid block of
       accent in a 208px gutter shouts over the panel it points at. **Below
       900px it is the other way round**, because there the two levels are
       stacked and adjacent, and the loud one has to be the tab you are on.
       See the `max-width: 899px` block near the sidebar rules.

   The buttons keep `.tab`, `data-tab` and `.tab--hidden` — the screenshot and
   smoke scripts drive the game through those. Everything below is scoped
   under `.nav` so it beats the base `.tab` and `.chip` rules on specificity
   rather than by editing them. */

:root {
  --nav-width: 208px;
  --nav-gap: 20px;
  --nav-radius: 10px;
  --nav-row: 42px;
  /* Clears the sticky topbar so the sidebar parks under it, not behind it. */
  --nav-sticky-top: 84px;
}

.nav { display: flex; flex-direction: column; gap: 10px; }

/* Level 1 — the groups. Never wraps: the pills shrink and ellipsise instead,
   so a fourth group changes their widths but never their row. */
.nav__groups { display: flex; flex-wrap: nowrap; gap: 8px; }
.nav__group {
  flex: 0 1 auto;
  min-width: 0;
  font: inherit;
  font-size: 14px;
  color: var(--muted);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0 16px;
  min-height: 44px;
  cursor: pointer;
}
.nav__group-label { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav__group:hover { color: var(--text); }
.nav__group--active { color: #08101f; background: var(--accent); border-color: transparent; font-weight: 600; }
.nav__group--hidden { display: none; }

/* Level 2 — one group's sections. Only the open group is on screen below
   900px; above it, every group is. */
.nav__section { display: none; }
.nav__section--open { display: block; }
.nav__section--hidden { display: none; }
.nav__title { display: none; }

.nav__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
}
/* Equal shares of one row: three sections or five, the row is the same
   height and no chip ever jumps to a second line. */
.nav__slot { flex: 1 1 0; min-width: 0; display: flex; }

/* `.chip--category` is declared after `.tab--hidden`, so the gating rule has
   to be restated here or a locked section would show its name. */
.nav .tab--hidden { display: none; }

.nav .tab {
  flex: 1 1 auto;
  min-width: 0;
  justify-content: center;
  min-height: 44px;
  padding: 5px 10px;
  border-radius: 999px;
  /* Level 2, so: thinner than a group pill, and washed out rather than
     filled. The group row above stays the loud one. */
  background: color-mix(in srgb, var(--panel-2) 55%, transparent);
  border-color: color-mix(in srgb, var(--line) 70%, transparent);
}
.nav .tab__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav .tab:hover { color: var(--text); }
/* Quieter than a group pill on purpose: a tint and an accent label rather
   than the solid fill level 1 uses. Green still means affordable elsewhere;
   accent still means "you are here". */
.nav .tab--active {
  color: var(--text);
  background: color-mix(in srgb, var(--accent) 18%, var(--panel-2));
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  font-weight: 600;
}
.nav .tab--active .chip__mark { opacity: 1; color: var(--accent); }

/* Below the sidebar breakpoint a chip has room for its name or its count,
   never both — "Collapse 210.76K" crushed the label to "C…" and pushed the
   badge out through the pill's own edge. So the count becomes a dot, pinned
   to the corner where it costs the label nothing. The number is still there
   on the sidebar, which has the width for it. */
@media (max-width: 899px) {
  .nav__slot { position: relative; }
  .nav .badge {
    position: absolute;
    top: 3px;
    right: 5px;
    width: 9px;
    height: 9px;
    min-width: 0;
    padding: 0;
    border-radius: 50%;
    overflow: hidden;
    font-size: 0;
    line-height: 0;
    border: 0;
  }
}

/* The slot a locked section is holding open. No name and no target — just
   the fact that something arrives here later. */
.nav__lock { display: none; }
.nav__lock-label {
  font-size: 12px;
  letter-spacing: 0.04em;
  color: color-mix(in srgb, var(--muted) 70%, transparent);
}
.nav__slot--locked {
  /* The button inside is display:none (`.tab--hidden`); the padlock is what
     keeps the space, so the slot never collapses and nothing below moves. */
  pointer-events: none;
}
.nav__slot--locked .nav__lock {
  flex: 1 1 auto;
  display: inline-flex;
  gap: 8px;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  border: 1px dashed var(--line);
  border-radius: 999px;
  color: var(--muted);
  opacity: 0.4;
}

/* Below the sidebar breakpoint the two levels sit stacked and adjacent, and
   the emphasis was upside down: the group pill took a solid accent fill while
   the tab you were actually looking at got a tint and a border. The louder
   control was the less important one, at one minute and at ten days alike.

   The comment on `.nav .tab--active` above argues for the tint, and it is right
   — *at sidebar width*, where a solid block of accent in a 208px gutter shouts
   over the panel it points at. Here there is no gutter and no distance, so the
   trade goes the other way. Swapped only in this range; the sidebar keeps its
   reasoning intact. */
@media (max-width: 899px) {
  .nav__group--active {
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    border-color: color-mix(in srgb, var(--accent) 38%, transparent);
    font-weight: 600;
  }
  .nav .tab--active {
    color: #08101f;
    background: var(--accent);
    border-color: transparent;
    font-weight: 700;
  }
  .nav .tab--active .chip__mark { color: #08101f; opacity: 1; }
  /* The count badge sits on the filled pill now, so it needs a ground that is
     not the same accent it is printed on. */
  .nav .tab--active .badge { background: #08101f; color: var(--accent); }
}

/* --- The sidebar --------------------------------------------------------- */
@media (min-width: 900px) {
  /* Two columns: the nav in the gutter, everything else where it always was.
     Only one panel is ever un-hidden, so the content column is exactly two
     grid rows (hero + panel) and the nav can span both. */
  .layout {
    max-width: calc(900px + var(--nav-width) + var(--nav-gap));
    display: grid;
    grid-template-columns: var(--nav-width) minmax(0, 1fr);
    /* auto + 1fr, not auto + auto: a grid item spanning a *flexible* track
       does not grow the intrinsic ones, so a tall sidebar beside a short
       panel can no longer push the hero's row open. */
    grid-template-rows: auto 1fr;
    align-items: start;
    column-gap: var(--nav-gap);
  }
  .layout > * { grid-column: 2; }
  .layout > #tabs {
    grid-column: 1;
    grid-row: 1 / -1;
    align-self: start;
    position: sticky;
    top: var(--nav-sticky-top);
    gap: 18px;
  }

  /* Level 1 collapses into headings: every group is open at once, so the
     pills have nothing left to do. */
  .nav__groups { display: none; }
  .nav__section { display: block; }
  .nav__section--hidden { display: none; }
  .nav__section + .nav__section { margin-top: 18px; }
  .nav__title {
    display: block;
    margin: 0 0 8px 12px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
  }

  .nav__list { flex-direction: column; gap: 2px; }
  .nav__slot { flex: none; position: relative; }

  .nav .tab {
    width: 100%;
    justify-content: flex-start;
    gap: 10px;
    min-height: var(--nav-row);
    padding: 0 12px;
    border-radius: var(--nav-radius);
    border-color: transparent;
    background: transparent;
    text-align: left;
  }
  .nav .tab:hover { background: var(--panel-2); }
  .nav .tab .badge { margin-left: auto; }
  /* A rail on the left edge rather than a filled pill: at sidebar width a
     solid block of accent shouts louder than the panel it points at. */
  .nav .tab--active {
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    border-color: color-mix(in srgb, var(--accent) 32%, transparent);
    box-shadow: inset 3px 0 0 var(--accent);
  }
  .nav__slot--locked .nav__lock {
    justify-content: flex-start;
    padding: 0 12px;
    min-height: var(--nav-row);
    border-radius: var(--nav-radius);
  }
}

/* Four group names have to fit one phone row without ellipsis.
   Compression made the row four-up, and at 14px/16px-padding "Production",
   "Prestige" and "Fleet" all ellipsised to "Produ…", "Pre…", "Fl…" on a 390px
   phone — decision 12 says the answer is never an overflow control, so the
   type tightens instead. The 44px touch target is untouched. */
@media (max-width: 520px) {
  .nav__group { padding: 0 10px; font-size: 12px; letter-spacing: 0.01em; }
  .nav__groups { gap: 6px; }
}

/* On the narrowest phones the section labels need every pixel of their row
   more than they need a mark beside them. */
@media (max-width: 360px) {
  .nav .tab .chip__mark { display: none; }
  .nav .tab { padding: 5px 6px; }
  .nav__group { padding: 0 8px; font-size: 11px; }
}

/* === THE RETURN SCREEN ==================================================== *
 *
 * The "while you were away" modal, rebuilt from three sentences into a
 * receipt, a diff and a hook. Markup shell in index.html (#offline-modal),
 * filled by showOfflineReport() in src/ui/app.js.
 *
 * One idea holds the whole block together: `--return-accent`. The length of
 * the absence picks a colour on `.modal--return` and everything downstream —
 * the header tile, the time bar, the total rule, the produced figure — takes
 * it from there, so five minutes away and five days away are different
 * *colours* rather than the same screen with a bigger number on it.
 *
 *   short  accent blue    the ordinary tea-break return
 *   long   accent violet  hours; the automation actually did something
 *   epic   record gold    the cap bit, or it has been most of a day
 *
 * Gold is already "a record" everywhere else in the game, which is exactly
 * what a full offline cap is.
 * ========================================================================== */

.modal--return {
  --return-accent: var(--accent);
  /* The overlay sits over a game the player has not looked at for hours: a
     touch darker than the other modals, so the receipt is the only thing lit. */
  background: rgb(3 5 12 / 82%);
}
.modal--return.return--long { --return-accent: var(--accent-2); }
.modal--return.return--epic { --return-accent: var(--record); }

.modal__box--return {
  --return-gap: 16px;
  max-width: 460px;
  width: min(460px, calc(100vw - 32px));
  padding: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  max-height: min(88vh, 780px);
  overflow: hidden;
  border-color: color-mix(in srgb, var(--return-accent) 34%, var(--line));
  background:
    radial-gradient(520px 220px at 12% -12%, color-mix(in srgb, var(--return-accent) 16%, transparent), transparent 70%),
    var(--panel);
  animation: returnRise 260ms ease-out;
}
@keyframes returnRise {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to { opacity: 1; transform: none; }
}

/* --- Header ------------------------------------------------------------- */

.return__head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 20px var(--return-gap) 16px;
  border-bottom: 1px solid var(--line);
}
.return__mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  color: var(--return-accent);
  border: 1px solid color-mix(in srgb, var(--return-accent) 40%, transparent);
  background: color-mix(in srgb, var(--return-accent) 14%, transparent);
  box-shadow: inset 0 0 18px color-mix(in srgb, var(--return-accent) 18%, transparent);
}
.return__title {
  margin: 0;
  font-size: 19px;
  line-height: 1.2;
}
.return__sub {
  margin: 3px 0 0;
  color: var(--muted);
  font-size: 13px;
}

/* --- Body --------------------------------------------------------------- */

.return__body {
  overflow-y: auto;
  padding: var(--return-gap);
  display: grid;
  gap: 18px;
  align-content: start;
}
.return__body:focus { outline: none; }

/* Small-caps section rules — the same "quiet label over content" idiom the
   Expeditions panel's .group__title uses, narrowed for a modal. */
.return__legend {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}
.return__legend::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* --- The time bar ------------------------------------------------------- *
 * Elapsed time drawn to scale. The filled part is what the game paid for;
 * the hatched remainder is what the 24h cap threw away. When nothing was
 * capped the bar is simply full, which reads as "all of it counted" without
 * a word being spent on it. */

.timebar { margin-bottom: 12px; }
.timebar__track {
  height: 10px;
  border-radius: 999px;
  overflow: hidden;
  border: 1px solid var(--line);
  background: repeating-linear-gradient(
    -45deg,
    rgb(148 160 196 / 22%) 0 4px,
    transparent 4px 9px
  );
}
.timebar__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  /* Opaque, not translucent: the track's hatching must never show through the
     counted part, or a bar with nothing capped reads as entirely thrown away. */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--return-accent) 55%, var(--panel-2)),
    var(--return-accent));
  box-shadow: 0 0 12px color-mix(in srgb, var(--return-accent) 45%, transparent);
}
.timebar__legend {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--muted);
}

/* --- The ledger --------------------------------------------------------- *
 * Accounting layout, deliberately: label left, figure right, tabular so the
 * digits stack, and a doubled rule above the total the way a paper receipt
 * does it. The point is that the produced number is *derivable* from the
 * rows above it rather than asserted on its own. */

.receipt__rows { margin: 0; }
.receipt__row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 6px 0;
  font-size: 14px;
}
.receipt__row dt {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--muted);
}
.receipt__row dt .icon { color: var(--muted); flex: none; }
/* Leader dots: the cheapest possible way to tie a label to a figure across a
   gap, and the one piece of typographic furniture a ledger cannot do without. */
.receipt__row::before {
  content: '';
  order: 1;
  flex: 1;
  align-self: center;
  height: 0;
  border-bottom: 1px dotted color-mix(in srgb, var(--line) 90%, transparent);
}
.receipt__row dd {
  order: 2;
  margin: 0;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.receipt__row--lost dt,
/* Drift: credited, but at a falling rate — muted-accent, between counted and lost. */
.receipt__row--drift dt,
.receipt__row--lost dd { color: var(--warn); }
.receipt__row--total {
  margin-top: 10px;
  padding-top: 14px;
  border-top: 3px double color-mix(in srgb, var(--return-accent) 45%, var(--line));
}
.receipt__row--total dt { color: var(--text); font-weight: 600; }
.receipt__row--total dd {
  font-size: clamp(24px, 6.5vw, 32px);
  font-weight: 700;
  line-height: 1;
  color: var(--return-accent);
  text-shadow: 0 0 22px color-mix(in srgb, var(--return-accent) 35%, transparent);
}
.receipt__row--total::before { align-self: center; }

/* --- The diff ----------------------------------------------------------- *
 * What actually changed. One row per system that did something, each in its
 * own colour (`--diff-accent`), so a screen full of automation reads as a
 * spread of systems rather than one grey sentence with commas in it. */

.difflist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px;
}
.diff {
  --diff-accent: var(--accent);
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 11px;
  border-radius: 10px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--diff-accent);
  background: color-mix(in srgb, var(--diff-accent) 7%, var(--panel-2));
}
.diff__mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 8px;
  color: var(--diff-accent);
  background: color-mix(in srgb, var(--diff-accent) 16%, transparent);
}
.diff__text { min-width: 0; }
.diff__title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
}
.diff__detail {
  margin: 1px 0 0;
  font-size: 12.5px;
  color: var(--muted);
}
.diff__count {
  margin-left: auto;
  flex: none;
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--diff-accent);
}

/* The two silences are different facts and must not look the same: nothing
   was automated, versus nothing had time to happen. Dashed, like every other
   deliberate empty state in the game. */
.difflist__empty {
  margin: 0;
  padding: 14px;
  border: 1px dashed var(--line);
  border-radius: 10px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}

/* --- The hook ----------------------------------------------------------- *
 * One line naming the next press. Green because green means "you can act on
 * this" everywhere else in the game, and this is the only actionable thing
 * on the screen apart from the button that closes it. */

.return__hook {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px dashed color-mix(in srgb, var(--good) 55%, transparent);
  background: color-mix(in srgb, var(--good) 8%, transparent);
}
.return__hook[hidden] { display: none; }
.return__hook-mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 9px;
  color: var(--good);
  background: color-mix(in srgb, var(--good) 16%, transparent);
}
.return__hook-label {
  display: block;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--good);
}
.return__hook-text {
  margin: 2px 0 0;
  font-size: 14px;
  line-height: 1.4;
}

/* --- Footer ------------------------------------------------------------- */

.return__foot {
  display: grid;
  gap: 12px;
  padding: 14px var(--return-gap);
  border-top: 1px solid var(--line);
  background: rgb(8 11 22 / 55%);
  /* Cast upward over the scrolling diff. A busy absence runs past the fold,
     and a row sliced flat by a hard edge reads as a rendering fault rather
     than as "there is more" — this is the cheapest possible way to say it. */
  box-shadow: 0 -20px 24px -18px rgb(0 0 0 / 80%);
}
.btn--return {
  width: 100%;
  min-height: 46px;
  font-size: 15px;
  font-weight: 600;
  color: #08101f;
  border-color: transparent;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--return-accent) 82%, white),
    var(--return-accent));
}
.btn--return:hover:not(:disabled) { filter: brightness(1.08); }

@media (max-width: 420px) {
  .modal__box--return { --return-gap: 12px; max-height: 92vh; }
  .return__head { padding: 16px 12px 12px; }
}
/* ==========================================================================
   THE UPGRADE LEDGER — the reward state  (added, not edited)

   The Upgrades table is bought out a few hours into a run and stays bought
   out, so "everything owned" is the state the panel spends most of its life
   in. It used to be ten grey text pills under the word OWNED, floating at the
   top of a tall dark void (CLAUDE.md §5 names it). Three pieces answer that,
   all of them additive:

     .ledger    what the bought half is currently worth — a running total, and
                one full-width marked row per upgrade saying what it still
                does. Same glyph/label/effect idiom as the Vault's Refined
                list and the fleet's Fitted rack, laid out as a shelf rather
                than a wrapping cloud, because ten assets in a row of tags
                read as leftovers.
     .upcoming  what is still gated, closest first, with a bar. This is what
                keeps a next goal on screen while the shop stands empty.
     .empty-note--done (existing) says the shop is finished on purpose, in the
                same position the Vault's does, so a repeatable line can drop
                in underneath it later without moving anything.

   Colour: violet. Blue is affordability everywhere in this game and has to
   keep meaning only that, so "bought" takes the accent the card marks already
   wear. Nothing here animates — a reward state should read as calm and
   earned.
   ========================================================================== */

/* A name for the register, declared here rather than by editing the token
   block at the top of the file. Upgrades are the one system that never had a
   colour of its own. */
:root {
  --upgrade: var(--accent-2);
}

/* --- Bought out ---------------------------------------------------------- */
/* The shared done-note is bronze, which is the fleet's currency colour and
   means nothing in this panel — at full width it read as a warning strip over
   the thing it is congratulating you about. Same note, this panel's register:
   a violet tick and body text, said once and quietly. */

#upgrade-shop-empty {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 14px 0 0;
  padding: 10px 14px;
  border-left: 2px solid color-mix(in srgb, var(--upgrade) 45%, transparent);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--upgrade) 6%, transparent);
  color: var(--muted);
}
#upgrade-shop-empty .icon { flex: none; margin-top: 2px; color: var(--upgrade); }

/* Where the multipliers carry on. Only ever visible under the bought-out note,
   and only for benches the run has opened — see `upgradeList.js` CONTINUE_AT.
   Each link wears the colour of the currency it spends, so the row reads as
   three different shops rather than three buttons. */
.continue { margin: 16px 0 4px; }
.continue__label {
  margin: 0 0 8px;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}
.continue__row { display: flex; flex-wrap: wrap; gap: 10px; }
.continue__link {
  font: inherit;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 8px 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.continue__link:hover { border-color: color-mix(in srgb, var(--accent) 60%, transparent); background: var(--panel); }
.continue__name { font-weight: 600; }
.continue__currency { font-size: 12px; color: var(--muted); }
/* One per bench, in the order a run reaches them. */
#upgrade-continue-row .continue__link:nth-child(1) { color: var(--shard); }
#upgrade-continue-row .continue__link:nth-child(2) { color: var(--aether); }
#upgrade-continue-row .continue__link:nth-child(3) { color: var(--crucible); }

/* --- The ledger block ---------------------------------------------------- */

.ledger {
  margin: 18px 0 4px;
  padding: 16px;
  border: 1px solid color-mix(in srgb, var(--upgrade) 32%, var(--line));
  border-radius: var(--radius);
  background:
    radial-gradient(120% 100% at 50% 0%, color-mix(in srgb, var(--upgrade) 10%, transparent), transparent 70%),
    var(--panel-2);
}

.ledger__head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.ledger__mark { display: inline-flex; flex: none; color: var(--upgrade); }
.ledger__heading { min-width: 0; }
.ledger__title { margin: 0 0 2px; font-size: 16px; }
.ledger__count { margin: 0; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* The running total is the biggest thing in the block, because it is the
   answer to "what did all this buy me": the factor current output would lose
   if every bought upgrade were taken away (engine: upgradeLedger). It moves
   with the generator mix, so it is a live number. */
.ledger__stats { margin-left: auto; display: flex; align-items: baseline; gap: 18px; }
.ledger__stat { text-align: right; }
.ledger__stat-value {
  display: block;
  font-size: 22px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--upgrade);
  font-variant-numeric: tabular-nums;
}
/* Output is the headline; what a click is worth is a footnote to it. Equal
   type sizes made the row read as three of the same thing, which is the
   hierarchy rule in §5 broken in the smallest possible way. */
.ledger__stat ~ .ledger__stat .ledger__stat-value { font-size: 16px; color: var(--text); }
.ledger__stat-label {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.ledger__stat-label .icon { flex: none; opacity: 0.75; }

/* --- The rows ------------------------------------------------------------ */
/* Same marked pill as every other owned list in the game, given a column
   instead of a wrap. */

.pills--ledger { display: grid; gap: 6px; }
.pills--ledger .pill {
  width: 100%;
  border-radius: 10px;
  padding: 9px 14px 9px 11px;
}
/* The Crucible's own ledger. Same component, its own register: a system with a
   colour keeps it in its reward state, or the finished shelf reads as belonging
   to a different tab. */
.ledger--crucible {
  border-color: color-mix(in srgb, var(--crucible) 32%, var(--line));
  background:
    radial-gradient(120% 100% at 50% 0%, color-mix(in srgb, var(--crucible) 10%, transparent), transparent 70%),
    var(--panel-2);
}
.ledger--crucible .ledger__mark,
.ledger--crucible .ledger__stat-value { color: var(--crucible); }
.pill--pattern { border-color: color-mix(in srgb, var(--crucible) 40%, var(--line)); color: var(--crucible); }
.pill--pattern.pill--marked { background: color-mix(in srgb, var(--crucible) 10%, var(--panel-2)); }
.pill--pattern .pill__mark { color: var(--crucible); }
.pill--pattern .pill__effect { margin-left: auto; padding-left: 12px; border-left: none; color: var(--crucible); }

.pill--upgrade { border-color: color-mix(in srgb, var(--upgrade) 40%, var(--line)); color: var(--upgrade); }
.pill--upgrade.pill--marked { background: color-mix(in srgb, var(--upgrade) 10%, var(--panel-2)); }
.pill--upgrade .pill__mark { color: var(--upgrade); }
/* No divider rule here, unlike the wrapping chips: right-aligned down a
   column, ten little left-borders land at ten different x positions and read
   as noise. The gap does the separating. */
.pill--upgrade .pill__effect {
  margin-left: auto;
  padding-left: 12px;
  border-left: none;
  color: var(--upgrade);
}

/* The Vault's Refined chips gained the same "what it still does" tail, so the
   divider and the value have to take the Vault's colour rather than the
   bronze the rack's chips are declared in. */
.pill--aether .pill__effect {
  color: var(--aether);
  border-left-color: color-mix(in srgb, var(--aether) 35%, transparent);
}

/* --- Still on order ------------------------------------------------------ */
/* Not controls: goals. Dashed rim and a muted name say "locked" without a
   second lock glyph on every row, and the bar says how far off it is. */

.upcoming { margin: 16px 0 4px; }
.upcoming__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.upcoming__row {
  display: grid;
  gap: 6px;
  padding: 11px 14px;
  border: 1px dashed var(--line);
  border-radius: 10px;
  background: color-mix(in srgb, var(--panel-2) 55%, transparent);
}
.upcoming__head { display: flex; align-items: center; gap: 8px; }
.upcoming__mark { display: inline-flex; flex: none; color: var(--muted); opacity: 0.8; }
/* A notch below a card name on purpose: the one row you can actually press
   has to stay the loudest thing in the panel. */
.upcoming__name { font-size: 14px; font-weight: 600; color: var(--muted); }
/* The price never breaks: "50.00M ✦" split across two lines on a phone and
   took the name down with it. The name wraps instead. */
.upcoming__price {
  margin-left: auto;
  flex: none;
  white-space: nowrap;
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.upcoming__gate { margin: 0; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.upcoming .progress__bar { background: color-mix(in srgb, var(--upgrade) 55%, transparent); }

/* --- Narrow ------------------------------------------------------------- */

@media (max-width: 600px) {
  /* The running total drops under the heading rather than squeezing it. */
  .ledger__head { flex-wrap: wrap; }
  .ledger__stats { margin-left: 0; width: 100%; gap: 22px; }
  .ledger__stat { text-align: left; }
  .ledger__stat-label { justify-content: flex-start; }

  /* Two lines instead of one: the effect keeps the name's left edge rather
     than being pushed off the row. */
  .pills--ledger .pill {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2px 8px;
    align-items: center;
  }
  .pills--ledger .pill__effect {
    grid-column: 2;
    margin-left: 0;
    padding-left: 0;
    border-left: none;
  }
}

/* --- Repeatable lines (Refit, Scaffold, Creed) ---------------------------- */
/* One card, three lines (src/ui/lineCard.js). Deliberately the Vault's
   Harmonics block again rather than a fourth idiom: the mark and title left,
   the multiplier as the largest thing on the card, a bar to the next level,
   one primary button, then "the line ahead". A player who has tuned a Harmonic
   can read all three of these without being taught anything.

   What differs per line is one custom property. `--line-accent` is the colour
   of the currency it spends — bronze Salvage, orange Workshop, violet Shards —
   so the card reads as belonging to the shop it stands behind, and colour keeps
   meaning the same thing it means everywhere else. */

.line-card {
  --line-accent: var(--accent-2);
  margin: 18px 0 4px;
  padding: 16px;
  border: 1px solid color-mix(in srgb, var(--line-accent) 40%, var(--line));
  border-radius: var(--radius);
  background:
    radial-gradient(120% 100% at 50% 0%, color-mix(in srgb, var(--line-accent) 12%, transparent), transparent 70%),
    var(--panel-2);
}
.line-card--refit { --line-accent: var(--salvage); }
.line-card--scaffold { --line-accent: var(--workshop); }
.line-card--creed { --line-accent: var(--shard); }

/* Affordable is affordable everywhere: the same rim the shop cards use when a
   row can be bought, so "this is ready" never needs reading. */
.line-card--ready {
  border-color: color-mix(in srgb, var(--good) 55%, var(--line));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--good) 18%, transparent);
}

.line-card__head { display: flex; align-items: flex-start; gap: 12px; }
.line-card__mark { display: inline-flex; color: var(--line-accent); flex: none; }
.line-card__title { min-width: 0; }
.line-card__name {
  margin: 0 0 2px;
  font-size: 16px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.line-card__flavor { margin: 0; font-size: 13px; color: var(--muted); }
/* The multiplier is the biggest thing on the card: it is what the player is
   buying, and the level is only how they got there. */
.line-card__reading { margin-left: auto; text-align: right; flex: none; }
.line-card__mult {
  display: block;
  font-size: 22px;
  font-weight: 700;
  color: var(--line-accent);
  font-variant-numeric: tabular-nums;
}
.line-card__level { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* Not a button here — the switch itself lives in Settings and on the panel
   head. This is a read-out saying the automation is already buying these. */
.line-card__auto { cursor: default; font-size: 9px; }

.line-card .progress { margin-top: 12px; }
.line-card .progress__bar { background: var(--line-accent); }
.line-card__next { margin-bottom: 0; }
.line-card .buy { margin-top: 10px; }
/* A bundle of four is four chips, so the price wraps within the button rather
   than running off the end of it — the Workshop's own Build buttons never had
   to carry a bundle *and* a label this long. */
.line-card__costs { display: inline-flex; gap: 10px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
.line-card .buy__label { white-space: nowrap; }

/* The ladder is shared with the Vault, which paints it in Aether cyan; inside
   a line card it takes the line's own accent instead. */
.line-card .ladder__row:nth-child(odd) { background: color-mix(in srgb, var(--line-accent) 4%, transparent); }
.line-card .ladder__row--next { color: var(--text); background: color-mix(in srgb, var(--line-accent) 12%, transparent); }
.line-card .ladder__cost { color: var(--line-accent); display: inline-flex; gap: 10px; align-items: center; }

@media (max-width: 560px) {
  /* The reading drops under the title rather than squeezing the flavour into a
     column two words wide. */
  .line-card__head { flex-wrap: wrap; }
  .line-card__reading { margin-left: 0; text-align: left; width: 100%; display: flex; align-items: baseline; gap: 8px; }
  /* The label above its price, so a four-material bundle has the whole width
     to wrap into instead of two words of it. */
  .line-card .buy { flex-direction: column; align-items: stretch; gap: 6px; }
  .line-card__costs { justify-content: flex-start; }
  /* Level and gain on one line, the bundle beneath — every position explicit,
     because auto-placement puts an item that spans both columns *first* and
     the rung reads backwards. */
  .line-card .ladder__row { grid-template-columns: 1fr auto; row-gap: 2px; }
  .line-card .ladder__level { grid-column: 1; grid-row: 1; }
  .line-card .ladder__gain { grid-column: 2; grid-row: 1; }
  .line-card .ladder__cost { grid-column: 1 / -1; grid-row: 2; flex-wrap: wrap; }
}

/* --- Grain, Tolerances and Fracture -----------------------------------------
   Compression's two features and the measure between them (content.js "GRAIN",
   "TOLERANCES", "FRACTURE"). All three sit inside the Vise, so all three take
   the layer's magenta — `--neutronium` is what says "this is L2", the same way
   Salvage bronze says "this is the bay". Fracture takes a hotter register on
   top of it, because it is the only thing in the game meant to stop you. */

.grain {
  margin: 4px 0 18px;
  padding: 14px;
  border: 1px solid color-mix(in srgb, var(--neutronium) 28%, var(--line));
  border-radius: var(--radius);
  background:
    radial-gradient(90% 120% at 20% 0%, color-mix(in srgb, var(--neutronium) 10%, transparent), transparent 70%),
    var(--panel-2);
}
.grain__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.grain .subhead--sm { margin-top: 0; }
/* The most important number in the section is the biggest one in it. */
.grain__percent {
  font-size: 26px;
  font-weight: 700;
  color: var(--neutronium);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.grain__body { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.grain__rosette { flex: none; }
.grain__legend { flex: 1 1 220px; min-width: 0; max-width: 380px; }

/* The rosette itself. Twelve spokes, one per channel; the web is the shape of
   how this save has been played. */
.rosette { width: 132px; height: 132px; display: block; overflow: visible; }
.rosette__ring { fill: none; stroke: color-mix(in srgb, var(--neutronium) 16%, transparent); stroke-width: 1; }
.rosette__spoke { stroke: color-mix(in srgb, var(--neutronium) 12%, transparent); stroke-width: 1; }
.rosette__web {
  fill: color-mix(in srgb, var(--neutronium) 26%, transparent);
  stroke: var(--neutronium);
  stroke-width: 1.5;
  stroke-linejoin: round;
  transition: none;
}
.rosette__dot { fill: var(--neutronium); }
/* A channel never touched reads as a hollow point, so "the short one" is
   findable without hovering anything. */
.rosette__dot--empty { fill: var(--panel-2); stroke: color-mix(in srgb, var(--muted) 60%, transparent); stroke-width: 1; }

.grain-legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }
.grain-legend__row {
  display: grid;
  grid-template-columns: 1fr 72px 44px;
  align-items: center;
  gap: 8px;
  font-size: 12px;
}
.grain-legend__label { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.grain-legend__track {
  height: 5px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--neutronium) 12%, transparent);
  overflow: hidden;
}
.grain-legend__fill { display: block; height: 100%; background: var(--neutronium); border-radius: 3px; }
.grain-legend__count { text-align: right; font-variant-numeric: tabular-nums; color: var(--text); }

/* Tolerances: six cards, one shape. */
.tolerances { margin: 0 0 18px; }
.tolerance-grid {
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.tol-card {
  padding: 12px;
  border: 1px solid color-mix(in srgb, var(--neutronium) 26%, var(--line));
  border-radius: var(--radius);
  background: var(--panel-2);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.tol-card--ready {
  border-color: color-mix(in srgb, var(--good) 55%, var(--line));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--good) 18%, transparent);
}
.tol-card__head { display: flex; align-items: center; gap: 10px; }
.tol-card__mark { display: inline-flex; color: var(--neutronium); flex: none; }
.tol-card__title { min-width: 0; }
.tol-card__name { margin: 0; font-size: 15px; }
.tol-card__level { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* The ceiling is the number being bought, so it is the big one on the card. */
.tol-card__cap {
  font-size: 20px;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.tol-card__summary { margin: 0; font-size: 12px; color: var(--muted); }
.tol-card__cut { font-size: 11px; color: var(--muted); }
.tol-card__cut--on { color: var(--good); }
.tol-card__list { color: var(--muted); font-size: 12px; margin-right: 6px; text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--muted) 60%, transparent); }
.tol-card .progress { margin: 2px 0; }
.tol-card .progress__bar { background: var(--neutronium); }
.buy--tol { width: 100%; justify-content: space-between; margin-top: auto; }

/* Fracture. The one wall in the game, and it looks like one: a hotter rim, a
   heavier head, and the strain line that says why the price is what it is. */
.fracture {
  margin: 18px 0 4px;
  padding: 16px;
  border: 1px solid color-mix(in srgb, var(--warn) 40%, var(--line));
  border-radius: var(--radius);
  background:
    radial-gradient(130% 110% at 50% 0%, color-mix(in srgb, var(--warn) 12%, transparent), transparent 72%),
    var(--panel-2);
}
.fracture--ready {
  border-color: color-mix(in srgb, var(--good) 55%, var(--line));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--good) 18%, transparent);
}
.fracture__head { display: flex; align-items: flex-start; gap: 12px; }
.fracture__mark { display: inline-flex; color: var(--warn); flex: none; }
.fracture__title { min-width: 0; }
.fracture__name { margin: 0 0 2px; font-size: 18px; }
.fracture__flavor { margin: 0; font-size: 13px; color: var(--muted); }
.fracture__broken {
  margin-left: auto;
  flex: none;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.fracture__reward { margin: 12px 0 4px; font-size: 13px; color: var(--text); font-weight: 600; }
.fracture__strain { margin: 0 0 10px; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.fracture__lever { margin: 8px 0 0; font-size: 12px; color: var(--accent-2); }
.fracture .progress__bar { background: linear-gradient(90deg, var(--warn), var(--neutronium)); }
.btn--fracture {
  width: 100%;
  margin-top: 10px;
  font-weight: 600;
  background: linear-gradient(135deg, var(--warn), var(--neutronium));
  color: #2a0716;
  border-color: transparent;
}
.btn--fracture:hover:not(:disabled) { filter: brightness(1.08); }

.ladder--fracture .ladder__row { grid-template-columns: 20px minmax(70px, auto) auto 1fr; }
.ladder--fracture .ladder__mark { display: inline-flex; color: var(--warn); }
.ladder--fracture .ladder__gain { color: var(--muted); font-size: 12px; text-align: right; }

@media (max-width: 520px) {
  .grain__body { justify-content: center; }
  .fracture__broken { display: none; }
}
/* --- The Descent and the Mill (L2) ------------------------------------------
   Two panels that sit in the Compression group but must not read as two more
   copies of the Vise. Each gets its own register:

     Descent — a cold jade, cousin of the bay's mint, because it *is* the bay's
               sibling: same crew, different question. The strata rows darken as
               they go down, so scrolling the list feels like descending it.
     Mill    — annealed brass. Warm, industrial, and nothing like the magenta
               above it or the jade beside it.

   Both keep the game's own furniture — `.card`, `.buy`, `.cost`, `.progress`,
   `.line-card` — so a player who has used the Workshop already knows how to
   read them. What changes is the colour and the one control each panel is
   actually about: a stratum row on the left, a converter's flow on the right.
*/

.panel--descent {
  border-color: color-mix(in srgb, var(--descent) 34%, var(--line));
  box-shadow: var(--shadow), 0 0 46px -20px color-mix(in srgb, var(--descent) 65%, transparent);
}
.panel--mill {
  border-color: color-mix(in srgb, var(--mill) 30%, var(--line));
  box-shadow: var(--shadow), 0 0 46px -20px color-mix(in srgb, var(--mill) 55%, transparent);
}
.descent-readout { font-variant-numeric: tabular-nums; color: var(--descent); font-weight: 600; }
.mill-readout { font-variant-numeric: tabular-nums; color: var(--mill); font-weight: 600; }
.descent-readout .unit,
.mill-readout .unit { color: var(--muted); font-weight: 400; font-size: 12px; }

/* Rank: the one thing on the shaft that is earned rather than bought, so it
   sits above everything and carries the panel's colour at full strength. */
.rank {
  display: flex;
  gap: 12px;
  align-items: center;
  padding: 12px 14px;
  margin: 12px 0;
  border: 1px solid color-mix(in srgb, var(--descent) 30%, var(--line));
  border-radius: var(--radius);
  background:
    radial-gradient(120% 100% at 0% 0%, color-mix(in srgb, var(--descent) 12%, transparent), transparent 70%),
    var(--panel-2);
}
.rank__mark { display: inline-flex; color: var(--descent); flex: none; }
.rank__body { flex: 1; min-width: 0; }
.rank__line { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.rank__name { font-size: 18px; font-weight: 700; color: var(--descent); }
.rank__depth { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.rank .progress { margin: 6px 0 4px; }
.rank .progress__bar { background: var(--descent); }
.rank__note { margin: 0; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Crew on the cage, out of the bay's own pool. */
.crewbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
}
.crewbar__mark { display: inline-flex; color: var(--expedition); flex: none; }
.crewbar__label { color: var(--muted); font-size: 13px; }
.crewbar__value {
  margin-left: auto;
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--descent);
}
.crewbar__buttons { display: flex; gap: 6px; }
.btn--step { min-width: 44px; min-height: 44px; padding: 0 10px; font-weight: 700; }
.btn--wide { min-width: 56px; }
.hint--sm { font-size: 12px; margin: 6px 0 14px; }

/* The strata. One row per candidate depth: press it to set the dive's target.
   The gradient down the list is the panel's one piece of theatre — the rows
   get darker and colder the further down they are, so the shape of the shaft
   is legible before a single number is read. */
.strata { list-style: none; margin: 0 0 16px; padding: 0; display: grid; gap: 6px; }
.stratum { border-radius: var(--radius); }
.stratum__hit {
  font: inherit;
  color: inherit;
  text-align: left;
  width: 100%;
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 12px 14px;
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, transform 0.12s ease;
}
.stratum:nth-child(1) .stratum__hit { background: color-mix(in srgb, var(--descent) 7%, var(--panel-2)); }
.stratum:nth-child(2) .stratum__hit { background: color-mix(in srgb, var(--descent) 6%, var(--bg-soft)); }
.stratum:nth-child(3) .stratum__hit { background: color-mix(in srgb, var(--descent) 5%, var(--bg-soft)); }
.stratum:nth-child(4) .stratum__hit { background: color-mix(in srgb, var(--descent) 4%, var(--bg)); }
.stratum:nth-child(5) .stratum__hit { background: color-mix(in srgb, var(--descent) 3%, var(--bg)); }
.stratum:nth-child(6) .stratum__hit { background: color-mix(in srgb, #000 35%, var(--bg)); }
.stratum__hit:hover:not(:disabled) { border-color: color-mix(in srgb, var(--descent) 55%, var(--line)); }
.stratum__hit:active:not(:disabled) { transform: translateY(1px); }
.stratum--planned .stratum__hit { border-color: color-mix(in srgb, var(--descent) 45%, var(--line)); }
.stratum--target .stratum__hit {
  border-color: var(--descent);
  box-shadow: inset 3px 0 0 0 var(--descent);
}
.stratum--locked .stratum__hit { color: var(--muted); cursor: not-allowed; }
.stratum__mark { display: inline-flex; color: var(--descent); flex: none; margin-top: 2px; }
.stratum--locked .stratum__mark { color: var(--muted); }
.stratum__body { flex: 1; min-width: 0; display: grid; gap: 4px; }
.stratum__line { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.stratum__name { font-weight: 600; }
.stratum__badge {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 999px;
  color: var(--muted);
  border: 1px solid var(--line);
}
.stratum--best .stratum__badge {
  color: var(--good);
  border-color: color-mix(in srgb, var(--good) 45%, var(--line));
  background: color-mix(in srgb, var(--good) 12%, transparent);
}
/* "best rate" is a state, not the row's name — it sits beside the depth badge
   rather than replacing it, so every stratum still says how deep it is. */
.stratum__best-mark {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--good);
}
/* Risk runs 0% → 12% down the shaft, and printing all of it in one amber gave
   the scale no shape. Green / amber / red, in the meaning those colours already
   carry everywhere else in the game. */
.stratum__risk { margin-left: auto; font-size: 12px; font-variant-numeric: tabular-nums; }
.stratum__risk--safe { color: var(--good); }
.stratum__risk--warn { color: var(--warn); }
.stratum__risk--hot { color: var(--danger); }
.stratum__desc { margin: 0; font-size: 12px; color: var(--muted); }
.stratum__haul { display: flex; gap: 10px; flex-wrap: wrap; font-variant-numeric: tabular-nums; font-size: 13px; }
.stratum__stats {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.stratum--best .stratum__rate { color: var(--good); }

/* The cage itself: one bar and one primary button, the panel's only one. */
.dive { margin-bottom: 16px; }
.dive .progress__bar { background: linear-gradient(90deg, var(--descent), var(--basalt)); }
.dive .progress--ready .progress__bar { background: var(--good); }
.btn--descend {
  width: 100%;
  font-weight: 600;
  background: linear-gradient(135deg, var(--descent), color-mix(in srgb, var(--descent) 40%, var(--bg-soft)));
  color: #04231e;
  border-color: transparent;
}
.btn--descend:hover:not(:disabled) { filter: brightness(1.1); }


/* The Winch — one purchase, and it is the automation, so it is drawn as an
   offer rather than as a shop row. Disappears for good once fitted. */
.winch {
  display: flex;
  gap: 12px;
  padding: 14px;
  border: 1px solid color-mix(in srgb, var(--carbide) 30%, var(--line));
  border-radius: var(--radius);
  background:
    radial-gradient(120% 100% at 100% 0%, color-mix(in srgb, var(--carbide) 8%, transparent), transparent 70%),
    var(--panel-2);
}
.winch__mark { display: inline-flex; color: var(--carbide); flex: none; }
.winch__body { flex: 1; min-width: 0; }
.winch__name { margin: 0 0 2px; font-size: 15px; }
.winch__flavor { margin: 0 0 8px; font-size: 13px; color: var(--muted); }
.winch__cost { display: inline-flex; gap: 10px; font-weight: 600; }

/* --- The Mill --------------------------------------------------------------- */

/* `.materials--mill .material` is 0,2,0 and the three per-good rules under it
   were 0,1,0, so the container won and every tile in the Mill's stock row
   painted `--mill` gold — while the *inline* `.sym--basalt`/`--sinter`/
   `--carbide` marks for the same three goods did use their own tints. Two
   colour systems for one set of materials, and the documented one never
   rendered. Matching the specificity fixes it; `tests/css.test.js` now asserts
   that no container rule outranks these three. */
.materials--mill .material { color: var(--mill); }
.materials--mill .material--basalt { color: var(--basalt); }
.materials--mill .material--sinter { color: var(--sinter); }
.materials--mill .material--carbide { color: var(--carbide); }

.card--mill { border-color: color-mix(in srgb, var(--mill) 22%, var(--line)); }
.card--mill.card--affordable { border-color: color-mix(in srgb, var(--mill) 60%, transparent); }
.card--mill .card__mark { color: var(--mill); }
.card--idle { color: var(--muted); }
.card--idle .card__mark { color: var(--muted); }
.card__level { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.card__switch {
  font: inherit;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 3px 9px;
  min-height: 24px;
  border-radius: 999px;
  cursor: pointer;
  color: var(--good);
  border: 1px solid color-mix(in srgb, var(--good) 45%, var(--line));
  background: color-mix(in srgb, var(--good) 12%, transparent);
}
.card__switch:disabled { color: var(--muted); border-color: var(--line); background: transparent; cursor: default; }
.card--stopped .card__switch { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, var(--line)); background: color-mix(in srgb, var(--warn) 12%, transparent); }
.card--stopped { opacity: 0.88; }

/* The chain, drawn as a chain: what goes in, an arrow, what comes out. */
.flow {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 8px 10px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--mill) 6%, var(--bg-soft));
  font-variant-numeric: tabular-nums;
}
.flow__side { display: inline-flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.flow__side--in { color: var(--muted); }
.flow__side--out { color: var(--mill); font-weight: 600; }
.flow__arrow { display: inline-flex; color: color-mix(in srgb, var(--mill) 70%, var(--muted)); }
.flow__note { margin: 0; font-size: 12px; color: var(--muted); }
.card--stopped .flow__note { color: var(--warn); }
/* Two bars on a converter card mean two different things — how well fed it is,
   and how close the next level is — and drawn identically they read as one
   thing twice. The feed bar is half the height and sits tight under the flow
   it describes; the level bar keeps the panel's normal weight. */
.progress--starve { height: 3px; margin-top: 2px; }
/* Red at the empty end, green at the full one — deliberately *not* the panel's
   own gold, which is what the level bar under it uses. Two gold bars of nearly
   equal width on one card were indistinguishable in the screenshots. */
.progress--starve .progress__bar { background: linear-gradient(90deg, var(--danger), var(--good)); }
.card--mill .flow + .progress--starve { margin-top: 4px; }
.card--mill .progress:not(.progress--starve) .progress__bar { background: var(--mill); }

.line-card--anneal { --line-accent: var(--mill); }

/* --- Creep: what the jaws give up between presses -------------------------
   src/ui/creepGauge.js. The device is a working press rather than a bar with
   a label: the ram travels, the slab loses height, and its grain lines crowd
   together as the charge fills. The bar underneath is the same `.progress`
   every other meter in the game uses, so the reading is legible even when the
   drawing is the thing being looked at. */
.creep {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: linear-gradient(180deg, color-mix(in srgb, var(--neutronium) 6%, var(--panel-2)), var(--panel));
  padding: 12px 14px;
  margin: 12px 0;
}
.creep__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.creep__rate { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.creep__flavor { margin: 4px 0 10px; }
.creep__body { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.creep__meter { flex: 1 1 200px; min-width: 0; }
.creep__next {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  /* The countdown re-renders twenty times a second and its digit count moves.
     A fixed line box is what stops the factors below it stepping up and down
     — "no layout shift" is a rule, not a preference. */
  min-height: 16px;
}
.progress--creep .progress__bar { background: linear-gradient(90deg, var(--shard), var(--neutronium)); }

.creep-press { width: 148px; height: 76px; flex: none; display: block; }
.creep-press__pillar { fill: color-mix(in srgb, var(--line) 80%, transparent); }
.creep-press__plate { fill: color-mix(in srgb, var(--neutronium) 34%, var(--panel-2)); }
.creep-press__plate--bed { fill: color-mix(in srgb, var(--neutronium) 22%, var(--panel-2)); }
.creep-press__slab {
  fill: color-mix(in srgb, var(--neutronium) 20%, transparent);
  stroke: color-mix(in srgb, var(--neutronium) 70%, transparent);
  stroke-width: 1;
}
.creep-press__grain { stroke: color-mix(in srgb, var(--neutronium) 45%, transparent); stroke-width: 0.75; }
/* The last tenth. The same "a row shows its state" rule the milestone bars
   follow — nearly-there should be readable without the numbers. */
.creep-press--tight .creep-press__slab { stroke: var(--neutronium); fill: color-mix(in srgb, var(--neutronium) 34%, transparent); }
.creep-press--tight .creep-press__plate { fill: color-mix(in srgb, var(--neutronium) 58%, var(--panel-2)); }

/* The three factors, named so the player can see which lever is theirs to
   pull. `tabular-nums` because all three tick. */
.creep__factors { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.creep-factor {
  flex: 1 1 0;
  min-width: 72px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--panel);
  padding: 5px 8px;
}
.creep-factor__label {
  display: block;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.creep-factor__value {
  display: block;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* --- The chock --------------------------------------------------------------
   The jaws' own control, under the four factor tiles. Carbide-coloured, which
   is the one hue on the Vise that is not Neutronium magenta — the whole point
   of the panel is that what goes in the jaws comes from somewhere else. */
/* Brass, not Neutronium magenta: what goes in the jaws comes out of the Mill,
   and the panel should say where it came from. `--carbide` itself is very
   nearly white, so it works as the symbol's colour and not as the frame's. */
.chock {
  margin-top: 10px;
  padding: 10px 12px;
  border: 1px solid color-mix(in srgb, var(--mill) 30%, var(--line));
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--mill) 6%, var(--panel));
}
.chock__head { display: flex; align-items: baseline; gap: 8px; }
.chock__mark { display: inline-flex; flex: none; color: var(--mill); align-self: center; }
.chock__title {
  margin: 0;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.chock__held {
  margin-left: auto;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--carbide);
}
.chock__note { margin: 6px 0 0; font-size: 12px; color: var(--muted); }
.chock__actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }

.btn--chock {
  flex: 1 1 auto;
  min-height: 44px;
  border-color: color-mix(in srgb, var(--mill) 45%, var(--line));
  color: var(--mill);
  background: color-mix(in srgb, var(--mill) 10%, transparent);
}
.btn--chock:hover:not(:disabled) { background: color-mix(in srgb, var(--mill) 24%, transparent); color: #241605; }
/* Taking the block back out is the undo, so it reads as the quiet one. */
.btn--chock-out {
  flex: 0 0 auto;
  border-color: var(--line);
  color: var(--muted);
  background: transparent;
}
.btn--chock-out:hover:not(:disabled) { color: var(--text); background: var(--panel-2); }

/* A Bolster with no duty on it is a prompt rather than a statistic. */
.creep-factor--idle .creep-factor__value { color: var(--muted); font-weight: 500; }

@media (prefers-reduced-motion: no-preference) {
  .creep-press__slab,
  .creep-press__plate,
  .creep-press__grain { transition: y 180ms linear, height 180ms linear, fill 240ms ease; }
}

@media (max-width: 420px) {
  .creep__body { gap: 10px; }
  .creep-press { width: 100%; height: 64px; }
}

/* Settings' release history, folded. The toggle is a full-width control rather
   than a `.fold` header, because the list it opens is the tail of another list
   that is already on screen — there is nothing to put in a header. */
.releases-older { margin-top: 12px; }
.releases-older__toggle { width: 100%; justify-content: center; }
.releases-older #release-older { margin-top: 12px; }
.releases-older #release-older[hidden] { display: none; }

/* --- The inactive-card convention ----------------------------------------
   The same rule as the disabled one below, one level up. A card that is
   locked, owned-out, idle or already finished used to be dimmed with
   `opacity` — 0.45 on a locked stratum, 0.55 on a locked expedition, 0.72 on
   an unearned achievement — and an ancestor's opacity fades everything under
   it, including a button that had just been given a legible disabled state of
   its own. The Crucible's queue button measured **4.47:1** for exactly that
   reason: the button was fine and the card around it was at 0.72.

   The Challenges panel had already worked this out ("locked cards are dimmed
   by desaturating their *mark*, never by fading the whole card — the old
   `opacity: 0.55` took the flavour text under the floor") and the lesson never
   left that one panel. Seven card states now follow it: the text goes to
   `--muted` (6.1:1 on `--panel-2`, still comfortably readable) and the mark
   loses its colour, so "inactive" reads without anything becoming unreadable.

   `tests/css.test.js` "a disabled control never fades its own content" guards
   the control; this block is the ancestor half, and the re-measurement that
   found it lives in `docs/CHANGELOG.md` under 0.50.0. */

/* --- The disabled convention -------------------------------------------- */
/* Deliberately last in the sheet. Every variant above sets its own
   `background` — gradients on `.collect`, `.btn--collapse`, `.btn--compress`,
   `.btn--engage`, `.btn--fracture`, `.btn--descend` — and a variant's
   `:disabled` rule carries the same specificity as this one, so source order
   is what decides. Putting the convention at the end means a new gradient
   button gets the right disabled state for free instead of needing its own
   rule, which is how the sheet ended up with eleven of them (0.35 / 0.4 /
   0.45 / 0.5) that all failed contrast in the same way.

   The reasoning and the measurements are at the top of this file under
   "Disabled controls". The short version: dim the chrome, keep the number. */
.buy:disabled,
.btn:disabled,
.condense:disabled,
.temper:disabled,
.duty-step:disabled,
.stepper__btn:disabled,
.collect:disabled,
.collect--disabled {
  cursor: not-allowed;
  /* The three properties the old rules used to fade the content. */
  opacity: 1;
  filter: none;
  box-shadow: none;
  background: var(--off-surface);
  border-color: var(--off-line);
  color: var(--muted);
}
/* `.collect` and the gradient buttons paint dark text on a light fill, so they
   also have to give the border back — they set `border-color: transparent`. */
.collect:disabled,
.collect--disabled { border: 1px solid var(--off-line); }
/* The cost is the reason the row is disabled, so it is the one thing that
   keeps full weight. `.sym` keeps its currency tint for the same reason:
   fading it was what made `◈` and `◆` indistinguishable on an unaffordable
   row, which is the exact confusion the tint exists to prevent. */
.buy:disabled .buy__cost,
.btn:disabled .ladder__cost,
.buy:disabled .cost__amount,
.btn:disabled .cost__amount { color: var(--text); }
.buy:disabled .sym,
.btn:disabled .sym,
.condense:disabled .sym,
.temper:disabled .sym { opacity: 1; }
/* …except where the symbol was told to inherit a filled button's dark ink.
   Once that button is flat, `inherit` would hand it `--muted`; give it the
   currency's own colour back. */
.btn--compress:disabled .sym--neutronium { color: var(--neutronium); }

/* --- The Sky ------------------------------------------------------------
   content.js "THE SKY". The one panel in the game that is meant to read as a
   night rather than as a surface: the chart sits on `--sky-ground`, darker
   than any other panel, and everything on it is drawn as light. The figure
   cards under it are ordinary cards on ordinary panel stock — the chart is
   the picture, the cards are the interface (src/ui/skyPanel.js).

   Four hues, one per figure, all of them already in the palette so the board
   does not introduce a fifth vocabulary. They are identity, never state:
   affordability is carried by the star's ring and by the row button, the same
   way it is everywhere else in the game. */
.panel--sky {
  --sky-net: var(--accent);
  --sky-harpoon: var(--accent-2);
  --sky-anvil: var(--ember);
  --sky-wheel: var(--aether);
}
.sky__head-mark { display: inline-flex; color: var(--ember); }
.sky-readout { display: flex; align-items: baseline; gap: 12px; }
.sky-readout__held {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-size: 1.15rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.sky-readout__lit {
  color: var(--muted);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

/* The chart. A fixed aspect ratio so a figure opening never moves the cards
   below it — the panel updates 20 times a second and nothing here may shift. */
.sky-chart {
  position: relative;
  margin: 0 0 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--sky-ground);
}
.sky-chart__svg { display: block; width: 100%; aspect-ratio: 100 / 62; }
.sky-chart__dust circle { fill: #cfd8ff; }

.sky-figure { color: var(--muted); }
.sky-figure--net.is-open { color: var(--sky-net); }
.sky-figure--harpoon.is-open { color: var(--sky-harpoon); }
.sky-figure--anvil.is-open { color: var(--sky-anvil); }
.sky-figure--wheel.is-open { color: var(--sky-wheel); }
/* A figure whose Embers are not earned yet is barely there — visible enough to
   say "there is more sky than this", quiet enough not to compete. */
.sky-figure { opacity: 0.34; transition: opacity 0.6s ease; }
.sky-figure.is-open { opacity: 1; }

.sky-figure__label {
  font-size: 2.6px;
  letter-spacing: 0.35px;
  text-transform: uppercase;
  fill: currentColor;
  opacity: 0.55;
}
.sky-figure.is-complete .sky-figure__label { opacity: 0.95; }

.sky-line {
  stroke: currentColor;
  stroke-width: 0.18;
  stroke-linecap: round;
  opacity: 0.22;
  transition: opacity 0.4s ease, stroke-width 0.4s ease;
}
.sky-line.is-lit { opacity: 0.8; stroke-width: 0.3; }

.sky-star { cursor: default; }
.sky-star__hit { fill: transparent; }
.sky-star__glow { opacity: 0; transition: opacity 0.5s ease; }
.sky-star__ring {
  fill: none;
  stroke: currentColor;
  stroke-width: 0.16;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.sky-star__core {
  fill: currentColor;
  opacity: 0.3;
  transition: opacity 0.4s ease, r 0.4s ease;
}
.sky-star.is-lit .sky-star__core { opacity: 1; r: 1.35; }
.sky-star.is-lit .sky-star__glow { opacity: 1; }
/* Reachable but unpaid: a ring, no fill. Affordable: the ring breathes. The
   two are different marks and not two shades of one, because "I could buy
   this" and "I will be able to buy this" are different facts. */
.sky-star.is-available { cursor: pointer; }
.sky-star.is-available .sky-star__ring { opacity: 0.45; }
.sky-star.is-affordable .sky-star__ring { opacity: 0.95; animation: sky-pulse 2.4s ease-in-out infinite; }
.sky-star.is-affordable .sky-star__core { opacity: 0.7; }
@keyframes sky-pulse {
  0%, 100% { stroke-width: 0.16; opacity: 0.55; }
  50% { stroke-width: 0.34; opacity: 1; }
}

.sky-goal {
  margin: 0 0 14px;
  color: var(--muted);
  font-size: 0.9rem;
}
.sky-goal.is-ready { color: var(--good); }

.sky-figures { display: grid; gap: 12px; }
@media (min-width: 900px) {
  .sky-figures { grid-template-columns: 1fr 1fr; }
}

.sky-card {
  --tone: var(--muted);
  border: 1px solid var(--line);
  border-left: 3px solid var(--tone);
  border-radius: var(--r-md);
  background: var(--panel-2);
  padding: 14px;
}
.sky-card--net { --tone: var(--sky-net); }
.sky-card--harpoon { --tone: var(--sky-harpoon); }
.sky-card--anvil { --tone: var(--sky-anvil); }
.sky-card--wheel { --tone: var(--sky-wheel); }
.sky-card.is-locked { --tone: var(--line); opacity: 0.85; }
.sky-card.is-complete {
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--tone) 10%, transparent), transparent 60%),
    var(--panel-2);
}

.sky-card__head { display: flex; align-items: center; gap: 10px; }
.sky-card__glyph { display: inline-flex; color: var(--tone); }
.sky-card__name { margin: 0; font-size: 1rem; flex: 1; }
.sky-card__tally {
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
  color: var(--muted);
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
}
.sky-card.is-complete .sky-card__tally { color: var(--tone); border-color: var(--tone); }
.sky-card__flavor { margin: 6px 0 12px; color: var(--muted); font-size: 0.85rem; font-style: italic; }

.sky-card__locked { margin-bottom: 4px; }
.sky-card__lock { margin: 0 0 8px; color: var(--muted); font-size: 0.85rem; }

.sky-card__stars { display: grid; gap: 6px; }
/* A locked figure's rows are a preview, not a shelf: same layout, quieter, and
   every one of them disabled. */
.sky-card.is-locked .sky-row { opacity: 0.72; }

/* One star, as a row you press. The whole row is the target — 44px tall on a
   phone — and the price sits hard right so eight rows scan as a column of
   numbers rather than eight sentences of different lengths. */
.sky-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  padding: 6px 10px;
  text-align: left;
  font: inherit;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.1s ease;
}
.sky-row:hover:not(:disabled) { border-color: var(--tone); background: color-mix(in srgb, var(--tone) 10%, var(--panel)); }
.sky-row:active:not(:disabled) { transform: translateY(1px); }
.sky-row:disabled { cursor: not-allowed; background: var(--off-surface); border-color: var(--off-line); }
.sky-row.is-lit { border-color: color-mix(in srgb, var(--tone) 55%, var(--line)); background: color-mix(in srgb, var(--tone) 8%, var(--panel)); }
.sky-row__mark { display: inline-flex; color: var(--tone); flex: none; }
.sky-row.is-lit .sky-row__mark { color: var(--good); }
.sky-row__text { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.sky-row__name { font-weight: 600; font-size: 0.92rem; }
.sky-row__effect { color: var(--muted); font-size: 0.8rem; }
.sky-row__price {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 0.9rem;
}
.sky-row__price--lit { color: var(--good); font-weight: 500; }
.sky-row__price--waiting { color: var(--muted); font-weight: 500; font-size: 0.8rem; }

/* The capstone. Set apart from the stars above it by a rule and by its own
   mark, because it is not a row you can press — it arrives when the last star
   does. */
.sky-cap {
  display: flex;
  gap: 10px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px dashed var(--line);
}
.sky-cap__mark { display: inline-flex; color: var(--muted); flex: none; margin-top: 2px; }
.sky-cap.is-owned .sky-cap__mark { color: var(--tone); }
.sky-cap__text { display: flex; flex-direction: column; gap: 2px; }
.sky-cap__name { font-weight: 700; font-size: 0.9rem; }
.sky-cap.is-owned .sky-cap__name { color: var(--tone); }
.sky-cap__effect { color: var(--muted); font-size: 0.82rem; }
.sky-cap__grant { color: var(--warn); font-size: 0.82rem; }

@media (prefers-reduced-motion: reduce) {
  .sky-star.is-affordable .sky-star__ring { animation: none; }
}

/* ===========================================================================
   THE EXPANSION (L3)

   Three sections and a gallery. The layer's own register is documents and
   seals, so its furniture is vellum on the game's navy; a **world** re-grounds
   its palette entirely (src/ui/worldArt.js) and is the one place in this sheet
   where a panel is allowed to stop looking like Stellar Foundry.

   What does *not* re-ground, ever: contrast, focus rings, tabular figures, and
   green meaning affordable.
   =========================================================================== */

:root {
  /* Vellum. The warm near-neutral is the one genuinely empty region left —
     the oranges are the Crucible, the Workshop, Temper, Salvage, Duty, the
     Mill and Embers; the violets are two prestige currencies deep; the cyans
     are Aether's; and green is not available at all because green means
     affordable everywhere and always. 12.9:1 on `--panel`. */
  --charter: #e8dcb5;
  /* A world's two currencies. Both are display-only marks beside a number, and
     both have to be told apart from every warm token above at 13px: Applause is
     a clean brass, Ovation the paler gold of the crown it is named for. */
  --applause: #f0c46b;
  --ovation: #ffe6a8;
}

.sym--charter { color: var(--charter); }
.sym--applause { color: var(--applause); }
.sym--ovation { color: var(--ovation); }

.charter-readout {
  font-size: 1.35rem;
  font-variant-numeric: tabular-nums;
  color: var(--charter);
  white-space: nowrap;
}

/* --- The reset ----------------------------------------------------------- */

.expand {
  display: grid;
  gap: 10px;
  padding: 16px;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background:
    radial-gradient(120% 140% at 12% -20%, rgba(232, 220, 181, 0.1), transparent 62%),
    var(--panel);
}
.expand__head { display: flex; align-items: flex-start; gap: 12px; }
.expand__mark { display: inline-flex; color: var(--charter); flex: none; margin-top: 2px; }
.expand__title { display: grid; gap: 2px; }
.expand__title h3 { margin: 0; font-size: 1.05rem; }
.expand__sub { margin: 0; color: var(--muted); font-size: 0.86rem; }
.expand__note { margin: 0; color: var(--muted); font-size: 0.86rem; }
.expand .fold__summary { font-size: 0.86rem; }

.policy { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.policy__label { color: var(--muted); font-size: 0.86rem; }
.policy__select {
  flex: 1 1 12rem;
  min-height: 44px;
  padding: 0 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--bg-soft);
  color: var(--text);
  font: inherit;
}

/* --- The Charter shop ---------------------------------------------------- */

.charter-grid { display: grid; gap: 8px; }
@media (min-width: 760px) { .charter-grid { grid-template-columns: repeat(2, 1fr); } }

.charter-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    'mark name cost'
    'mark note cost'
    'mark kind cost';
  gap: 1px 12px;
  align-items: center;
  text-align: left;
  padding: 12px 14px;
  min-height: 44px;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
  font: inherit;
}
.charter-row__mark { grid-area: mark; display: inline-flex; color: var(--muted); }
.charter-row__name { grid-area: name; font-weight: 650; }
.charter-row__note { grid-area: note; color: var(--muted); font-size: 0.82rem; }
/* The one thing a player must not have to guess: what an Expansion takes back.
   Quiet, because it is a fact about the row rather than a warning about it. */
.charter-row__kind {
  grid-area: kind;
  color: var(--muted);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.75;
}
.charter-row__cost {
  grid-area: cost;
  font-variant-numeric: tabular-nums;
  color: var(--charter);
  white-space: nowrap;
}
.charter-row__cost--afford { color: var(--good); }
.charter-row--afford { border-color: color-mix(in srgb, var(--good) 45%, var(--line)); }
.charter-row--owned { border-color: color-mix(in srgb, var(--charter) 45%, var(--line)); }
/* A row that is yours for good gets a vellum rule down its edge — the whole
   split between permanent and re-bought, said once and read at a glance. */
.charter-row--permanent { border-left: 3px solid color-mix(in srgb, var(--charter) 60%, var(--line)); }
.charter-row:hover:not(:disabled) { border-color: var(--charter); }
.charter-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- The Shelf ------------------------------------------------------------ */

.card--shelf .shelf__line {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-variant-numeric: tabular-nums;
}
.shelf__mult { color: var(--charter); }
.shelf__next { color: var(--muted); font-size: 0.86rem; }
.shelf__pot { margin: 0; color: var(--muted); font-size: 0.82rem; font-variant-numeric: tabular-nums; }
.shelf__save {
  min-height: 44px;
  border-radius: var(--r-sm);
  border: 1px dashed var(--line);
  background: transparent;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
}
.shelf__save--on {
  border-style: solid;
  border-color: var(--charter);
  color: var(--charter);
  background: color-mix(in srgb, var(--charter) 8%, transparent);
}
.shelf__save:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.hint--empty { color: var(--muted); font-style: italic; }

/* ===========================================================================
   THE WORLDS — the gallery, and one world
   =========================================================================== */

.worldgrid { display: grid; gap: 12px; grid-template-columns: 1fr; margin-top: 12px; }
@media (min-width: 720px) { .worldgrid { grid-template-columns: repeat(2, 1fr); } }

.wcard {
  /* Each card sets these inline from its own world's palette. Declared here so
     the sheet is self-contained and a world with no palette still renders. */
  --c: var(--accent);
  --c2: var(--panel-2);
  display: flex;
  flex-direction: column;
  text-align: left;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
  font: inherit;
}
.wcard:hover:not(:disabled) { border-color: var(--c); }
.wcard:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* **The plate sits across the top at its own aspect ratio.** As a fixed-width
   column beside the body it stretched to whatever height the caption needed and
   every world came out distorted — the proscenium became a red slab. A card is
   a picture with a caption, not a table row. */
.wcard__art {
  display: block;
  aspect-ratio: 20 / 11;
  background: var(--c2);
  border-bottom: 1px solid var(--line);
}
.wcard__art svg { display: block; width: 100%; height: 100%; }

.wcard__body { display: grid; gap: 6px; padding: 12px 14px; align-content: start; flex: 1; }
.wcard__top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.wcard__name { font-size: 1.05rem; font-weight: 650; color: var(--c); }
.wcard__sub { font-size: 0.8rem; color: var(--muted); line-height: 1.4; }
.wcard__state {
  font-size: 0.7rem;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  color: var(--muted);
  white-space: nowrap;
  flex: none;
}
.wcard__state--own { border-color: var(--c); color: var(--c); }
.wcard__state--due { border-color: var(--warn); color: var(--warn); }
.wcard__stat { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.wcard__rate { font-size: 0.95rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.wcard__rate--none { font-size: 0.8rem; color: var(--muted); }
.wcard__stage { font-size: 0.7rem; color: var(--muted); text-align: right; }

/* The reservoir. The one number that says which pocket to open next. */
.wcard__res { display: grid; gap: 3px; }
.wcard__res-track {
  height: 5px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--line) 70%, transparent);
  overflow: hidden;
}
.wcard__res-fill { display: block; height: 100%; background: var(--c); }
.wcard__res-fill--full { background: var(--warn); }
.wcard__res-l { font-size: 0.7rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* The quietest line on the card, and it had no rule at all in the prototype —
   so "Exports a running order for the Foundry" was set larger than the world's
   own name. */
.wcard__exp {
  font-size: 0.72rem;
  color: var(--muted);
  border-top: 1px solid var(--line);
  padding-top: 8px;
  line-height: 1.35;
}
.wcard--due { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }
.wcard--lock { opacity: 0.62; cursor: default; }
.wcard--lock .wcard__art { filter: grayscale(0.5); }

/* On a phone each card is full width, so the plate at 20:11 eats most of the
   screen and only one card is ever comparable with another. Crop it rather than
   shrink it — the art is the point, but so is seeing two reservoirs at once. */
@media (max-width: 560px) { .wcard__art { aspect-ratio: 20 / 7; } }
@media (max-width: 420px) { .wcard__sub { display: none; } }

.worldback {
  min-height: 44px;
  padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--bg-soft);
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  margin-bottom: 12px;
}
.worldback:hover { color: var(--text); border-color: var(--accent); }
.worldback:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- One world ------------------------------------------------------------
   A world fills the panel rather than sitting in a box inside it. The first
   prototype put a navy border and padding around the red card, which reads as a
   widget *in* the Foundry rather than as somewhere else — and a frame the world
   did not choose is part of the presentation it is supposed to be re-grounding. */

.panel--worlds .wpanel {
  --c: var(--accent);
  --c2: var(--panel-2);
  --c3: var(--bg-soft);
  --brass: var(--line);
  --board: var(--panel);
  --ink: var(--text);
  --ink-quiet: var(--muted);
  display: grid;
  gap: 12px;
  padding: 18px;
  border-radius: var(--r-md);
  border: 1px solid var(--brass);
  color: var(--ink);
  background:
    radial-gradient(120% 90% at 50% -10%, color-mix(in srgb, var(--c) 16%, transparent), transparent 60%),
    linear-gradient(180deg, var(--c2) 0%, var(--c3) 100%);
}
.wpanel__plate { border-radius: var(--r-sm); overflow: hidden; border: 1px solid var(--brass); }
.wpanel__plate .wcard__art { border-bottom: 0; aspect-ratio: 20 / 6; }
.wpanel__head { display: grid; gap: 2px; text-align: center; }
.wpanel__title {
  margin: 0;
  font-size: 1.6rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--c);
  text-shadow: 0 0 22px color-mix(in srgb, var(--c) 45%, transparent);
}
.wpanel__sub { margin: 0; color: var(--ink-quiet); font-style: italic; font-size: 0.86rem; }
.wpanel__rate { font-size: 1.5rem; font-variant-numeric: tabular-nums; }
.wpanel__held { color: var(--ink-quiet); font-variant-numeric: tabular-nums; }
.wpanel__res, .wpanel__stage, .wpanel__note, .wpanel__slots {
  margin: 0;
  color: var(--ink-quiet);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}
.wpanel__resbar .progress__bar { background: var(--c); }
.wpanel__resbar .progress__bar--warn { background: var(--warn); }
.wpanel__sub-head {
  margin: 6px 0 0;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}

/* The prompt book: ruled lines, cue marks down the left margin. */
.book {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--board);
  border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--brass) 45%, transparent);
  overflow: hidden;
}
.book__cue {
  position: relative;
  display: grid;
  grid-template-columns: 2.2rem auto 1fr auto auto;
  grid-template-areas:
    'no mark name  count cost'
    'no mark effect buys buys';
  gap: 0 12px;
  align-items: center;
  padding: 10px 14px;
  border-bottom: 1px solid color-mix(in srgb, var(--brass) 20%, transparent);
  border-left: 3px solid transparent;
}
.book__cue:last-child { border-bottom: 0; }
/* The row fills as the next cue gets closer, so a cue at 24/25 does not look
   like one at 1/25 — the state is in the row, not only in the number. */
.book__fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: linear-gradient(90deg, color-mix(in srgb, var(--c) 16%, transparent), transparent);
  pointer-events: none;
  width: 0;
}
.book__no {
  grid-area: no;
  font-variant-numeric: tabular-nums;
  color: var(--brass);
  font-size: 1rem;
  text-align: center;
}
.book__mark { grid-area: mark; display: inline-flex; color: var(--c); }
.book__name { grid-area: name; font-weight: 600; }
.book__effect { grid-area: effect; color: var(--ink-quiet); font-size: 0.78rem; }
.book__count { grid-area: count; font-variant-numeric: tabular-nums; color: var(--c); font-size: 1rem; }
.book__cost { grid-area: cost; font-variant-numeric: tabular-nums; white-space: nowrap; }
.book__buys { grid-area: buys; display: flex; gap: 6px; justify-content: flex-end; margin-top: 4px; }
.book__buy, .book__keep {
  min-height: 44px;
  min-width: 44px;
  padding: 0 12px;
  border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--brass) 60%, transparent);
  background: transparent;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.book__keep { border-style: dashed; color: var(--ink-quiet); }
.book__keep--on { border-style: solid; border-color: var(--c); color: var(--c); }
.book__buy:focus-visible, .book__keep:focus-visible { outline: 2px solid var(--c); outline-offset: 2px; }
.book__cue--afford .book__buy { border-color: var(--good); color: var(--good); }
/* A cue that warms the house is lit from the left. */
.book__cue--warm { border-left-color: var(--c); }
.book__cue--empty { opacity: 0.55; }

.wsinks { display: grid; gap: 8px; }
@media (min-width: 640px) { .wsinks { grid-template-columns: repeat(3, 1fr); } }
.wsink {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: 'mark name' 'mark value' 'summary summary' 'cost cost';
  gap: 2px 10px;
  text-align: left;
  padding: 12px;
  min-height: 44px;
  border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--brass) 55%, transparent);
  background: color-mix(in srgb, var(--board) 70%, transparent);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.wsink__mark { grid-area: mark; display: inline-flex; color: var(--c); }
.wsink__name { grid-area: name; font-weight: 600; }
.wsink__value { grid-area: value; font-variant-numeric: tabular-nums; color: var(--c); font-size: 1.1rem; }
.wsink__summary { grid-area: summary; color: var(--ink-quiet); font-size: 0.78rem; }
.wsink__cost { grid-area: cost; font-variant-numeric: tabular-nums; margin-top: 4px; }
.wsink--afford { border-color: var(--good); }
.wsink:focus-visible { outline: 2px solid var(--c); outline-offset: 2px; }

/* Five nav groups is one more than this row has ever carried. Wrapping alone
   does nothing, and the reason is worth keeping: the slots are `flex: 1 1 0`,
   so they *shrink* to fit rather than overflowing — and a row that never
   overflows never wraps. They have to refuse to shrink first. */
@media (max-width: 900px) {
  .nav__groups { flex-wrap: wrap; row-gap: 6px; }
  .nav__list { flex-wrap: wrap; row-gap: 6px; }
  .nav__slot { flex: 0 0 auto; }
}

.wpanel__full {
  margin: 0;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  color: var(--warn);
  font-size: 0.82rem;
}

/* The world's own reservoir track, quieter than the game's: at a glance a
   strongly-coloured empty track reads as a *full* bar, which is the opposite of
   what it says. */
.wpanel__resbar { background: color-mix(in srgb, var(--brass) 25%, transparent); }

/* **The book, on a phone.** At 390px the five-column row overflowed: the price
   was clipped mid-digit and "Hold over" ran off the edge, while the blurb wrung
   itself into a four-word column. The cue keeps its number and its mark on the
   left and stacks everything else. */
@media (max-width: 620px) {
  .book__cue {
    grid-template-columns: 2rem auto 1fr;
    grid-template-areas:
      'no mark name'
      'no mark effect'
      'no count cost'
      'buys buys buys';
    row-gap: 4px;
  }
  .book__cost { justify-self: start; }
  .book__buys { justify-content: stretch; }
  .book__buy { flex: 1; }
  .book__cost { text-align: right; }
}

/* Designed and not built. Dimmer than a locked world and desaturated rather
   than greyed to nothing, so the plate still reads as *that* world's colours —
   a shelf of five different games, not one card and four grey rectangles. */
.wcard--soon { opacity: 0.5; cursor: default; }
.wcard--soon .wcard__art { filter: grayscale(0.55) brightness(0.85); }
.wcard--soon .wcard__state { border-style: dashed; }
