/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5   (E was 4 until the chrome became one material)
 * Hallmark · macrostructure: Workbench (one instrument, docked panel) · genre: atmospheric
 * theme: custom (Nightwater) · nav: N5 floating pill dock · footer: Ft1 single line
 * tone: austere · anchor hue: cold blue (arc #4272FF, live events only)
 * states: default · hover · focus-visible · active · disabled · loading · empty · error · live
 * contrast: pass (40-41; measured, design/bar.md #15) · tokens: pass (48) · chrome: pass (47)
 * honest: pass (46) · responsive: pass (49) · mobile: pass (34, 50-57; 320/375/414/768/1280/1920)
 * Every value here resolves from site/tokens.css. No raw colour, size, radius or duration lives in this
 * file: if a value is needed that has no token, it is lifted into tokens.css first (design/bar.md #10).
 * No projected shadow anywhere; inset rings only (bar #11).
 *
 * ONE MATERIAL (2026-09-03). Every HUD surface is the same pane, and it is declared once, in .pane:
 * the scene behind it veiled (--hud-veil), a fill that is a whisper of --text, and the --line-2 rim,
 * which is lighter than the fill at every depth. Chrome used to be opaque dark boxes on a lit tank,
 * which read as holes punched in the scene; glass reads as something laid ON it. The veil is what
 * makes a light fill legal over a lit tank, and bar #12 was amended in place to allow it (bar #15 is
 * re-measured over the veil's output, which is now the brightest pixel any HUD text can sit on).
 */

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

html, body {
  height: 100%;
  /* The page is one instrument, not a document: it never scrolls in either axis (bar #18). `clip`, not
     `hidden`, so no scroll container is created at all. */
  overflow: clip;
  background: var(--ground);
  color: var(--text);
  font-family: var(--font-body);
  font-stretch: var(--wdth-body);
  -webkit-text-size-adjust: 100%;
}

#gl { display: block; width: 100vw; height: 100dvh; }

.hud {
  /* Derived, not raw: the brand row (--s-5 top) plus the CA line under it, which is where the hero and
     the docked panel both begin. Composing tokens keeps the one magic number out of the file. */
  --hud-top: calc(var(--s-5) + var(--ca-h) + var(--s-4));
  /* The right column starts under the stat strip, which is taller than the brand row the hero hangs
     off; the two columns have two tops for that reason and no other. */
  --panel-top: calc(var(--s-5) + var(--stats-h) + var(--s-4));
  --hud-top-m: calc(var(--hud-inset-m) + var(--s-7) + var(--s-2));
  --dot: calc(var(--s-1) * 1.5);
  position: fixed;
  inset: 0;
  /* The HUD is a layer over the scene: it must not eat the orbit drag except on its own controls. */
  pointer-events: none;
}
/* `hidden` has to actually hide. The UA rule is `[hidden] { display: none }` at UA priority, so any
   author `display` beats it: `.pill` and `.chip` declare `inline-flex`, and hud.js has been setting
   `hidden` on pills since bar #17's overlap resolution was written. Measured 2026-09-04: a pill with
   `hidden` set still computed `display: flex` and still occupied 163 px. One rule fixes the whole HUD,
   and it is scoped rather than `!important`. */
.hud [hidden] { display: none; }
.hud > *, .hud button, .hud a, .hud input, .hud .panel { pointer-events: auto; }
.hud .pills, .hud .chips, .hud .status-rail, .hud .note, .hud .hero, .hud .keeper, .hud .keeper-bubble, .hud .keeper-leader { pointer-events: none; }
.hud .pills button { pointer-events: auto; }

/* ── controls: the eight states, once, for every control ──────────────────────────────────────────
   default · hover · focus-visible · active · disabled · loading · error · live(success).
   The focus ring never animates: it has to be there the instant focus lands. */
/* :where() so the reset carries ZERO specificity. As `button, input, a.btn` it was 0,1,1 and beat the
   0,1,0 of `.btn`, so the one ask that is a link (Buy) rendered as bare centred text instead of the
   filled CTA every other panel gets. */
:where(button, input, a.btn) { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
:where(button, a, input):focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
  border-radius: var(--r-action);
}
:where(button, a.btn)[disabled], :where(button, a.btn):disabled { opacity: .45; cursor: not-allowed; }
:where(button)[data-state="loading"] { opacity: .6; cursor: progress; }

/* ── the pane: the one chrome material, declared once ─────────────────────────────────────────────
   Six surfaces, one declaration, so nothing can drift into its own weight of edge or its own fill.
   The rim is --line-2 on every one of them: an outer edge is an outer edge, whatever it contains.
   Radius and padding are the only things a pane sets for itself, below. */
.stats, .ca, .pill, .chip, .panel, .prints, .dock-wrap, .agent, .keeper-bubble {
  background: var(--hud);
  border: 1px solid var(--line-2);
  backdrop-filter: var(--hud-veil);
  -webkit-backdrop-filter: var(--hud-veil);
  box-shadow: inset 0 1px 0 var(--ring);
}
/* The reading pane is the same material one step up: copy needs a quieter field than a four-word pill,
   so the fill lifts and the veil goes darker and softer under it. */
/* The prints stage is the same reading material as the panel: it is a surface to look at something on,
   not a pill. Without this it had no fill at all and a woodblock print floated over open water. */
.panel, .prints { background: var(--hud-2); backdrop-filter: var(--hud-veil-2); -webkit-backdrop-filter: var(--hud-veil-2); }

/* ── brand ─────────────────────────────────────────────────────────────────────────────────────── */
.brand { position: absolute; left: var(--hud-inset); top: var(--s-5); display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.brand .mark {
  width: var(--s-5); height: var(--s-5); border-radius: var(--r-content);
  background: linear-gradient(160deg, var(--water-hi), var(--water));
  box-shadow: inset 0 0 0 1px var(--ring);
}
.brand b { font: var(--w-display) var(--size-brand) / 1 var(--font-display); font-stretch: var(--wdth-display); letter-spacing: var(--track-sub); }
/* THE HALL: tickers as links, the current one in the text colour and the rest in the secondary, at the
   interface tier like everything else in the header. No pill, no border — it sits inside the brand
   block as a row of names, because a nav that announces itself louder than the wordmark is a nav that
   thinks it is the product. */
/* ON ITS OWN LINE. Inline, the hall added 184 px to the brand row and pushed the contract-address pill
   under the keeper and into the stats bar (measured at 1440: pill 546→1071, keeper from 751, stats from
   973). A second line costs one row of header height, which the hero gives up below; the pill keeps its
   place and the hall reads as what it is, a list of rooms rather than a suffix to a name. */
.brand .hall { display: flex; gap: var(--s-3); flex-basis: 100%; margin-top: var(--s-1); padding-left: calc(var(--s-5) + var(--s-2)); }   /* the mark's width (.brand .mark) plus the brand gap, so the hall starts under the wordmark */
/* The hero sits under the brand; with a hall present the brand is a row taller and the hero moves down
   by that row rather than colliding with it. Toggled by hud.js on the root, so a one-tank page is
   untouched. */
.hud.has-hall .hero { top: calc(var(--hud-top) + var(--s-5)); }
.brand .hall a { font: var(--w-medium) var(--size-label) / 1 var(--font-mono); letter-spacing: var(--track-stat-label); text-transform: uppercase; color: var(--text-2); text-decoration: none; }
.brand .hall a:hover { color: var(--text); }
.brand .hall a.is-current { color: var(--text); }
.brand i {
  font: var(--w-medium) var(--size-eyebrow) / 1 var(--font-mono); font-style: normal;
  color: var(--text-2); letter-spacing: var(--track-eyebrow); text-transform: uppercase; margin-left: var(--s-1);
}

/* ── stat strip ────────────────────────────────────────────────────────────────────────────────── */
/* The strip and the panel are one right-hand column: same right edge, same width, same --s-5 of internal
   padding, so two panes read as two panes and not as two unrelated objects that happen to be near each
   other. --min-width, not width, so a longer price grows the pane instead of overflowing it. */
.stats {
  position: absolute; right: var(--hud-inset); top: var(--s-5);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-5);
  min-width: var(--panel-w); padding: var(--s-3) var(--s-5); border-radius: var(--r-content);
}
/* No divider between cells. Four hairlines inside a hairline box was five edges doing the work of one:
   the pane is the container and space is the separator. A cell is a label over a value, and the pair
   reads as a pair because nothing else is near it. */
.stats .cell { padding: 0; }
.stats small {
  display: block; font: var(--w-medium) var(--size-stat-label) / 1 var(--font-mono);
  letter-spacing: var(--track-stat-label); text-transform: uppercase; color: var(--text-2); margin-bottom: var(--s-2);
  /* Secondary text is the one ink the pane does not carry on its own: re-measured 2026-09-03 it is
     2.44:1 on a chrome pane with a light shaft behind it (5.56:1 over the room, which is where this
     label actually lives), so it keeps the shadow system.md prescribes for HUD text that can cross the
     water. The real protection is bar #16: the stat bar and the footer sit outside the tank's projected
     rectangle at every solved view, which is the camera's job (site/tank.js), not this file's. */
  text-shadow: var(--hud-text-shadow);
}
.stats strong { font: var(--w-semibold) var(--size-stat) / 1 var(--font-mono); letter-spacing: var(--track-sub); font-variant-numeric: tabular-nums; }
/* The EST tag is gone with the thing it tagged: the only extrapolated figure on the page was the
   countdown to the next buyback run, and there is no cadence to extrapolate from (design/bar.md #23,
   amended 2026-09-03). `--size-est` is folded into the interface tier: at 8.5px it sat over moving WebGL, and
   the fix for hard-to-read is never smaller. */

/* ── the CA, three placements ──────────────────────────────────────────────────────────────────── */
/* The address is a pane too, not bare mono text floating on the scene: at the head and in the footer it
   is the same small glass pill the dock and the stats are made of, which is what makes the four corners
   of the page read as one set. */
.ca {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-2) var(--s-3) var(--s-2) var(--s-4); border-radius: var(--r-action);
  font: var(--w-medium) var(--size-label) / 1 var(--font-mono); color: var(--text-2); letter-spacing: var(--track-mono);
}
.ca .ca-full { color: var(--text); font-weight: var(--w-medium); word-break: break-all; }
.ca .ca-abbr { display: none; color: var(--text); font-weight: var(--w-medium); }
/* Placement 1 is the last member of the brand row (hud.js appends it there): wordmark, chain, contract
   on one line, so the hero starts one row higher and the tank gets the row back. */
.ca.at-head { margin-left: var(--s-4); }
/* COPY sits INSIDE a pane, so it takes a fill and no rim: a bordered chip inside a bordered pill is two
   edges arguing about which one is the object. */
.copy {
  border-radius: var(--r-action); padding: var(--s-1) var(--s-3); background: var(--hud-3);
  color: var(--text); font: var(--w-medium) var(--size-est) / 1 var(--font-mono);
  letter-spacing: var(--track-stat-label); text-transform: uppercase; white-space: nowrap;
}
.copy:hover { background: var(--line); }
.copy:active { background: var(--line-2); }
.copy[data-state="success"] { background: var(--line-2); }
.copy[data-state="error"] { background: var(--hud-3); color: var(--text-2); }

/* ── hero: the one giant word ──────────────────────────────────────────────────────────────────── */
.hero { position: absolute; left: var(--hud-inset); top: var(--hud-top); max-width: var(--hero-max-w); }
.hero .big {
  font: var(--w-display) var(--size-display) / var(--lead-display) var(--font-display);
  font-stretch: var(--wdth-display); letter-spacing: var(--track-display);
  font-variant-numeric: tabular-nums;
  /* The number is the only element that can cross the lit water on a wide screen. */
  text-shadow: var(--hud-text-shadow);
  overflow-wrap: anywhere; min-width: 0;
}
.hero .sub { margin-top: var(--s-2); font: var(--w-body) var(--size-sub) / var(--lead-sub) var(--font-body); letter-spacing: var(--track-sub); text-shadow: var(--hud-text-shadow); }
.hero .sub .dim { color: var(--text-2); }
/* empty state: no ledger yet (replaying, or pre-launch). The giant slot holds a live count, never a
   word: `waiting` set at 128px wraps and reads as a headline, so the empty state drops to the h3 size
   and the secondary colour until a real number arrives. */
.hero.is-empty .big { font-size: var(--size-h3); line-height: var(--lead-h3); color: var(--text-2); }
.hero.is-empty .sub { color: var(--text-2); }

/* ── focus: the tank fills the frame, so the bare text has to go ───────────────────────────────────
   Bar #16 keeps the brand, the hero and the footer OUTSIDE the tank's projected rectangle at every
   solved view, which is what lets them be bare text with a shadow instead of panes. A close-up of one
   fish is the one view where that rectangle is the whole viewport, and the guarantee cannot hold: the
   hero measured about 1.2:1 over lit water. Everything that is bare text steps out for the duration,
   and everything that stays is a veiled pane, whose contrast tools/glass-contrast.js already measures.
   The hero's number is a count of the whole tank, which is not what the viewer is looking at; the
   wallet's own live figures are on the panel beside it. The annotation stays: it is opaque ink on
   --pill-on-glass at 17.9:1, and it is the one thing pointing at the fish. */
.hud.focused .brand, .hud.focused .hero, .hud.focused .pills, .hud.focused .foot .block { display: none; }

/* ── in-scene pills ────────────────────────────────────────────────────────────────────────────── */
.pills { position: absolute; inset: 0; }
.pill {
  position: absolute; transform: translate(-50%, -50%);
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-4); border-radius: var(--r-action);
  font: var(--w-medium) var(--size-label) / 1 var(--font-body); letter-spacing: var(--track-body);
  color: var(--text); white-space: nowrap;
}
.pill::before { content: ""; width: var(--dot); height: var(--dot); border-radius: var(--r-action); background: var(--text-2); }
/* The `on-glass` inversion is retired (2026-09-03). It existed because an opaque dark pill on lit water
   was the wrong object, so the one pill that always sat on the glass flipped to white with ink text —
   which made three identical labels render as two different components for a reason only the camera
   knew. The veil under the pane holds contrast over the light shafts now (bar #15, re-measured), so all
   three station pills are the same pane wherever the camera puts them. */
.pill:hover { border-color: var(--text); }
.pill:active { transform: translate(-50%, -50%) translateY(1px); }

/* ── the annotation ────────────────────────────────────────────────────────────────────────────── */
/* Positioned from the scene each frame (hud.js placeNote), pointing at one fish. The hairline is the
   leader: it runs back from the text toward the fish it names. */
.note {
  position: absolute;
  font: italic var(--w-body) var(--size-note) / 1.1 var(--font-body); font-stretch: var(--wdth-note);
  /* Ink on --pill-on-glass, which is exactly what system.md prescribes for "a label that must sit on the
     lit water": it flips to light with ink text rather than taking a darker fill. Measured, both bare
     options fail at our density: --text on --water-hi is 1.15:1 and on --water 2.4:1, and ink alone is
     legible against water but vanishes against the fish, which are the same ink and cover most of the
     field. On the backing it is 17.5:1 over any part of the tank. It stays an annotation, not a pill:
     italic, --r-content rather than the pill radius, and a leader running back to the fish it names. */
  color: var(--text-on-cta); letter-spacing: var(--track-sub);
  background: var(--pill-on-glass); border-radius: var(--r-content);
  /* Now that the station pills are glass, this is the ONLY inverted label on the page, which is what an
     annotation should be: it is the one thing pointing at one fish, not a control. The padding matches
     the pane's rhythm so it does not read as a tighter object than everything around it. */
  padding: var(--s-2) var(--s-3) var(--s-2) var(--s-4);
}
.note::before {
  content: ""; position: absolute; left: calc(var(--s-3) * -1); top: 50%;
  width: var(--s-4); height: 1px; background: var(--pill-on-glass);
}

/* The narrated line: the annotation's material and the annotation's machinery (hud.js placeLine), under
   the fish the line is about instead of above it, and only ever one of the two on screen at a time. It
   is a sentence rather than three words, so it wraps inside a measure and is centred on its fish. */
.note.line {
  max-width: min(var(--hero-max-w), calc(100% - var(--s-8)));
  white-space: normal; line-height: var(--lead-sub); text-align: center;
}
.note.line::before { content: none; }   /* the leader points UP to the fish from an annotation beside it; this one is under its own fish */

/* ── the agent's receipts pane ─────────────────────────────────────────────────────────────────────
   The same glass pane as everything else, in the left column above the footer, and it exists only while
   there is a feed (hud.js keeps it `hidden` otherwise, and the stage is measured around it either way).
   Absolutely positioned against the footer at 1440: an abspos child takes no grid track, so the
   footer's three columns are untouched, and it inherits --foot-lift and the open sheet for free. */
/* The height cap is MEASURED, not a vh fraction: --agent-max-h is the room the right column has under
   the docked panel, published by hud.js measureStage (solveStage). At a guessed 44vh it cut the last
   receipt through the middle of a sentence at 1440x900 and overlapped the hero at 1280x800.
   RIGHT-docked since 2026-09-06, under the panel: on the left it stood in the tank's own band and cost
   the stage 316 px of width at 1440 while the column under the panel stood empty. It is narrower than
   the panel on purpose (--agent-w) and shares its right edge, so the right side reads as one column. */
.agent {
  position: absolute; right: 0; bottom: calc(100% + var(--s-4));
  width: var(--agent-w); max-height: var(--agent-max-h, 50vh);
  display: flex; flex-direction: column; min-height: 0;
  padding: var(--s-4); border-radius: var(--r-content);
}
/* The frame holds the glass and the contents scroll inside it, for the reason the panel and the dock
   both split: a mask or a scroll on the same element as a `backdrop-filter` cancels the filter in
   Chrome, and the pane would lose its veil over lit water. */
.agent .agent-body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
/* Collapsed to its eyebrow and its link, or shelved altogether: at 1440 when a tall panel (the FAQ)
   leaves it no room in the column, at 390 while the sheet is open (hud.js measureStage sets both, from
   the room the pane would have, never from its own height). The readings are one tap away through the
   link, and the status rail names the agent's state whichever step it is at. */
.agent.is-collapsed .agent-body > :not(.eyebrow, .proof) { display: none; }
.agent.is-shelved { display: none; }
.agent .status { margin-top: var(--s-2); }
.agent .msg {
  margin-top: var(--s-3);
  font: var(--w-body) var(--size-body) / var(--lead-body) var(--font-body);
  letter-spacing: var(--track-body); color: var(--body-text);
}
/* One receipt is a mono head (what it cost, which model, how long ago) over the line it bought. The
   list carries no bullet and no rule: the pane is the container and space is the separator, which is
   the same argument the stat strip's missing dividers make. */
.agent .receipts { margin-top: var(--s-3); list-style: none; display: flex; flex-direction: column; gap: var(--s-2); }
.agent .receipts .row { display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap; }
.agent .receipts b { font: var(--w-semibold) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-mono); font-variant-numeric: tabular-nums; color: var(--text); }
.agent .receipts .k { font: var(--w-medium) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-2); }
.agent .receipts p { margin-top: var(--s-2); font: var(--w-body) var(--size-body) / var(--lead-body) var(--font-body); letter-spacing: var(--track-body); color: var(--body-text); }

/* ── the keeper ────────────────────────────────────────────────────────────────────────────────────
   The mascot, as a 2D sticker on the plinth's front-left corner (site/keeper.js). Positioned by hud.js
   placeKeeper every frame from the scene's own projection, sized from the stage, and never over the
   glass: the pure rules are keeperBox and bubbleBox, and this file only says what a pose looks like.
   Six pictures in one box, the active one opaque: a pose change is a crossfade of --t-keeper-fade and
   nothing else moves. The idle pose breathes (one transform, --t-keeper-breathe); no other pose does.
   He is decoration and he takes no pointer: everything he reacts to is printed elsewhere on the page. */
.keeper { position: absolute; pointer-events: none; }
.keeper-bob { position: absolute; inset: 0; }
/* HE IS ALWAYS IN THE WATER. The float used to run on the idle pose alone, so the moment anything
   happened on the page he froze mid-reaction — the opposite of what a reaction should look like. It runs
   on every pose now, and the poses ride on top of it. */
.keeper-bob { animation: keeper-breathe var(--t-keeper-breathe) ease-in-out infinite; }
/* Two motions at different periods, deliberately coprime, so they never line up into an obvious loop:
   he rises and falls, and separately drifts and tips a little, the way anything neutrally buoyant does. */
.keeper-pose { position: absolute; inset: 0; opacity: 0; transition: opacity var(--t-keeper-fade) linear;
  animation: keeper-drift 7.7s ease-in-out infinite; }
.keeper-pose.is-on { opacity: 1; }
.keeper[data-flip="yes"] .keeper-pose { transform: scaleX(-1); }
.keeper picture { position: absolute; inset: 0; }
/* THE TAIL BEATS, slowly and never in time with the float. Its origin is the joint itself, set from the
   split metadata, so it turns about where it is actually attached. 9.1s against the float's period, and
   a small angle: a big swing on a painted tail reads as a windscreen wiper. */
.keeper-tail { animation: keeper-tail 3.9s ease-in-out infinite; }
.keeper img {
  display: block; width: 100%; height: 100%; object-fit: contain; object-position: center bottom;
  /* The voxel art is a side elevation facing LEFT (design/mascot/BRIEF.md, 2026-09-07). Which way he
     should face depends on where he is standing, so keeper.js decides and stamps it on the node. */
}
@keyframes keeper-breathe { 0%, 100% { transform: none; } 50% { transform: translateY(calc(var(--keeper-breathe) * -1)); } }
/* ALL OF THIS WAS TOO SMALL TO SEE. At his size on the page a degree of rotation is invisible, and a
   shark hanging in water is never still. Roughly tripled, with the periods shortened so he is never
   holding a pose — big enough to read at a glance, short of a wobble. */
@keyframes keeper-drift {
  0%, 100% { translate: 0 0; rotate: -2.4deg; }
  35%      { translate: calc(var(--keeper-breathe) * 1.6) calc(var(--keeper-breathe) * -1.1); rotate: 1.8deg; }
  70%      { translate: calc(var(--keeper-breathe) * -1.2) calc(var(--keeper-breathe) * 0.7); rotate: 3.0deg; }
}
@keyframes keeper-tail {
  0%, 100% { rotate: -11deg; }
  50%      { rotate: 13deg; }
}
/* Everything he does is decoration, so all of it stops when the reader has asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  .keeper-bob, .keeper-pose, .keeper-tail { animation: none; }
}
/* His line, in the same glass pane as every other piece of chrome, with a 1 px leader running from its
   nearest edge to his mouth (hud.js placeKeeper draws both from bubbleBox). Placed where it clears the
   glass: beside him at 390, on the plinth's face under the water at 1440. */
.keeper-bubble {
  position: absolute; max-width: var(--keeper-bubble-w);
  padding: var(--s-2) var(--s-3); border-radius: var(--r-content);
  font: var(--w-body) var(--size-body) / var(--lead-sub) var(--font-body); letter-spacing: var(--track-body);
  color: var(--text); pointer-events: none;
}
.keeper-leader { position: absolute; height: 1px; background: var(--text-2); transform-origin: 0 50%; pointer-events: none; }
/* The tail, drawn on the bubble itself when it sits beside his mouth. A small square rotated behind the
   pane so it inherits the pane's own background and border, which is what makes it read as part of the
   bubble rather than a triangle parked next to it. */
.keeper-bubble.has-tail::after {
  content: ''; position: absolute; width: 12px; height: 12px;
  left: var(--tail-x, 50%); top: var(--tail-y, 100%);
  translate: -50% -50%; rotate: 45deg;
  background: inherit; border: inherit; border-radius: 2px; z-index: -1;
}
/* A close-up of one fish is the one view where the glass is the whole viewport, and a keeper pushed out
   of it would be a keeper with no room: he steps out with the rest of the bare chrome (see .focused). */
.hud.focused .keeper, .hud.focused .keeper-bubble, .hud.focused .keeper-leader { display: none; }

/* ── event chips ───────────────────────────────────────────────────────────────────────────────── */
/* Positioned from the scene each frame (hud.js placeChips): the chip hangs on the projected water
   surface so a buy's chip and its fish are the same event, not two things in different places. */
.chips { position: absolute; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: var(--s-2); }
.status-rail { position: absolute; left: 50%; transform: translateX(-50%); bottom: calc(var(--dock-h) + var(--s-8)); display: flex; flex-direction: column; align-items: center; gap: var(--s-2); }
.chip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-4); border-radius: var(--r-action); color: var(--text);
  font: var(--w-semibold) var(--size-label) / 1 var(--font-mono); letter-spacing: var(--track-mono); white-space: nowrap;
  animation: chip-in var(--t-chip) var(--ease-settle) both;
}
.chip::before { content: ""; width: var(--dot); height: var(--dot); border-radius: var(--r-action); background: var(--text-2); }
/* The accent contract: the buy chip is the only chrome that is ever accent, and it leaves with the event.
   It is also the one pane that stops being glass while it is up: an event that just happened is not
   chrome, and the veil under a solid accent fill would only cost frames. */
.chip.accent { background: var(--accent); border-color: transparent; color: var(--text-on-accent); backdrop-filter: none; -webkit-backdrop-filter: none; }
.chip.accent::before { background: var(--text-on-accent); }
.chip[data-state="error"] { color: var(--text-2); }
.chip[data-state="loading"] { color: var(--text-2); }
@keyframes chip-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ── panel ─────────────────────────────────────────────────────────────────────────────────────── */
/* A panel taller than its box scrolls, but a silent clip reads as broken copy rather than as "there is
   more" — the FAQ's last answer was cut mid-sentence with no cue. The fade is applied only while there
   is something below, so it disappears at the end instead of dimming the last line forever. */
.panel-body.is-clipped, .agent-body.is-clipped { mask-image: linear-gradient(to bottom, #000 calc(100% - var(--s-6)), transparent); }

.panel {
  /* --hud-inset, not --s-5: the panel's right edge and the stat strip's right edge are the same edge,
     and they used to miss each other by 8 px for no reason anybody could see. Everything on the page
     now hangs off the one inset the brand, the hero and the footer already used. */
  position: absolute; right: var(--hud-inset); top: var(--panel-top); width: var(--panel-w);
  max-height: calc(100dvh - var(--panel-top) - var(--s-8) - var(--dock-h));
  display: flex; flex-direction: column; min-height: 0;
  border-radius: var(--r-content);
  animation: panel-in var(--t-chip) var(--ease-settle) both;
}
/* The frame holds the glass; the copy scrolls inside it and carries the bottom fade. */
.panel-body {
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: var(--s-5) var(--s-5) var(--s-6);
}
@keyframes panel-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.panel[hidden] { display: none; }
.panel .handle { display: none; }
.eyebrow { font: var(--w-medium) var(--size-eyebrow) / 1 var(--font-mono); letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--text-2); }
.panel h3 {
  margin-top: var(--s-3); font: var(--w-display) var(--size-h3) / var(--lead-h3) var(--font-display);
  font-stretch: var(--wdth-h3); letter-spacing: var(--track-h3);
  overflow-wrap: anywhere; min-width: 0;
}
/* THE DISCLOSURE. Every value is a token: the summary is the interface tier like everything else, and
   it is deliberately NOT smaller than the body it hides — a control that opens the page's own
   explanation should not read as a footnote. The marker is a rotating caret rather than the browser's
   default triangle, which is the one piece of chrome a native <details> otherwise brings with it. */
.panel .more { margin-top: var(--s-3); border-top: 1px solid var(--line); padding-top: var(--s-3); }
.panel .more summary {
  display: flex; align-items: center; gap: var(--s-2); cursor: pointer; list-style: none;
  font: var(--w-semibold) var(--size-body) / var(--lead-body) var(--font-body);
  letter-spacing: var(--track-body); color: var(--text);
}
.panel .more summary::-webkit-details-marker { display: none; }
.panel .more summary::after {
  content: ""; width: 6px; height: 6px; margin-left: auto;
  border-right: 1.5px solid var(--text-2); border-bottom: 1.5px solid var(--text-2);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--t-chip) var(--ease-settle);
}
.panel .more[open] summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.panel .more summary:hover { color: var(--text); }
/* No focus rule of its own: :where(button, a, input):focus-visible already defines the page's one ring,
   and <summary> is focusable, so it is given the same treatment rather than a second, slightly different
   ring. A second focus style is how a page ends up with two. */
.panel .more summary:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; border-radius: var(--r-action); }
@media (prefers-reduced-motion: reduce) { .panel .more summary::after { transition: none; } }
.panel p { margin-top: var(--s-3); font: var(--w-body) var(--size-body) / var(--lead-body) var(--font-body); letter-spacing: var(--track-body); color: var(--body-text); }

.faq { margin-top: var(--s-4); }
.faq dt { margin-top: var(--s-4); font: var(--w-semibold) var(--size-body) / var(--lead-sub) var(--font-body); letter-spacing: var(--track-body); color: var(--text); }
.faq dd { margin-top: var(--s-2); font: var(--w-body) var(--size-body) / var(--lead-body) var(--font-body); letter-spacing: var(--track-body); color: var(--body-text); }

.status {
  margin-top: var(--s-4); padding: var(--s-4); border-radius: var(--r-content);
  background: var(--hud-3); border: 1px solid var(--line);
  font: var(--w-medium) var(--size-status) / var(--lead-status) var(--font-mono);
  color: var(--text-2); letter-spacing: var(--track-mono);
}
.status .row { display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap; }
.status .k { color: var(--text-2); text-transform: uppercase; letter-spacing: var(--track-label); }
.status b { color: var(--text); font-weight: var(--w-semibold); font-variant-numeric: tabular-nums; }
.status .panel-ca { margin-top: var(--s-2); }
/* The panel's addresses show the short form: one line, no orphan. The full value stays in the DOM rather
   than being replaced, so selecting it and reading it aloud both still give the whole address. */
.status .panel-ca .ca-full { display: none; }
.status .panel-ca .ca-abbr { display: inline; }

.btn {
  display: flex; align-items: center; justify-content: center; text-align: center;
  margin-top: var(--s-4); min-height: var(--cta-h); padding: 0 var(--s-4);
  border-radius: var(--r-action); background: var(--text); color: var(--text-on-cta);
  font: var(--w-semibold) var(--size-cta) / 1 var(--font-body); letter-spacing: var(--track-sub);
  text-decoration: none; white-space: nowrap;
}
.btn:hover { background: var(--water-hi); }
.btn:active { transform: translateY(1px); }

.proof {
  display: block; margin-top: var(--s-3);
  font: var(--w-medium) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-mono);
  color: var(--text-2); text-decoration: none;
}
.proof span { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.proof:hover span { color: var(--water-hi); }
.faq .proof { display: block; margin-top: var(--s-2); }   /* its own line: appended after the answer text it had no space before it, so "…drops one in.Read it" ran together */

/* ── find my fish ──────────────────────────────────────────────────────────────────────────────── */
.find { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-4); }
.find input {
  flex: 1 1 60%; min-width: 0; height: var(--cta-h-m); padding: 0 var(--s-3);
  border: 1px solid var(--line-2); border-radius: var(--r-action); background: var(--hud-3); color: var(--text);
  font: var(--w-medium) var(--size-label) / 1 var(--font-mono); letter-spacing: var(--track-mono);
}
.find input::placeholder { color: var(--text-3); }
.find input:hover { border-color: var(--text-2); }
.find .go {
  flex: 0 0 auto; height: var(--cta-h-m); padding: 0 var(--s-4);
  border: 1px solid var(--line-2); border-radius: var(--r-action); color: var(--text);
  font: var(--w-semibold) var(--size-cta) / 1 var(--font-body); letter-spacing: var(--track-sub);
}
.find .go:hover { border-color: var(--text); }
.find .go:active { transform: translateY(1px); }
/* empty: nothing typed yet, so the submit is inert rather than misleading. Three channels, not one —
   the dim fill, the cursor, and the native `disabled` hud.js sets (which is what carries it to a
   screen reader). `pointer-events: none` used to stand in for the last two and told nobody. */
.find[data-state="empty"] .go { opacity: .45; cursor: not-allowed; }
/* The result slot keeps one line of height at rest, so an error or a card appearing does not shove the
   ask button down the panel (hallmark gate 39). */
.find .result { flex: 1 1 100%; min-height: 1lh; }
.find .msg { margin-top: var(--s-3); font: var(--w-body) var(--size-body) / var(--lead-body) var(--font-body); color: var(--body-text); }
/* error: an address with no fish. It says so; it never gets a fish. */
.find .result[data-state="error"] .msg { color: var(--text-2); }
/* live: a real holder, drawn from the ledger. */
.find .result[data-state="live"] .addr { display: block; margin-top: var(--s-4); font: var(--w-semibold) var(--size-label) / 1 var(--font-mono); letter-spacing: var(--track-mono); color: var(--text); }
.find .card { display: block; width: 100%; margin-top: var(--s-3); border-radius: var(--r-content); border: 1px solid var(--line); }
/* The two derived rows (NAME, SWIMS) are in the same instrument block as the four read ones, so this
   line is what separates what the chain said from what we made of it. Secondary colour and the panel's
   own body size: a caveat set smaller than the thing it caveats is a caveat nobody reads. */
.find .flavour { margin-top: var(--s-3); font: var(--w-body) var(--size-body) / var(--lead-body) var(--font-body); letter-spacing: var(--track-body); color: var(--text-2); }

/* ── crumbs (in focus only) ────────────────────────────────────────────────────────────────────── */
/* Two actions in the find form's own weight, never the panel's filled CTA: the panel keeps exactly one
   of those (bar #20), and it is still "Make my card". */
.find .crumbs { flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-4); }
.find .crumbs .go { margin: 0; }
.find .crumbs .back { color: var(--text-2); }
.find .crumbs .back:hover { color: var(--text); border-color: var(--text); }
/* The crumb disclosure. It is the panel's paragraph, at the panel's paragraph size, in the panel's
   paragraph colour, on its own full-width line: a reader who takes crumbs for a reward mechanism is a
   reader this page misled, and grey 10px under a button is how that happens. */
.find .note-plain {
  flex: 1 1 100%; margin: 0;
  font: var(--w-body) var(--size-body) / var(--lead-body) var(--font-body);
  letter-spacing: var(--track-body); color: var(--body-text);
}

/* ── footer: CA · dock · block ─────────────────────────────────────────────────────────────────── */
.foot {
  position: absolute; left: var(--hud-inset); right: var(--hud-inset); bottom: var(--s-5);
  /* minmax(0, 1fr) on the outer tracks, not 1fr: a bare 1fr floors at the CA's min-content width and
     makes both side tracks that wide, which stole 24px from the dock at 1440 and pushed it off centre.
     With a 0 floor the dock always gets its full width and stays centred on the viewport. */
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--s-4);
}
/* EXPLICIT COLUMNS, because one of these three children is hidden at every width and auto-placement then
   slides the other two left. Hiding the foot address at desktop (below) did exactly that: the dock took
   column one and the block indicator took column two, so the dock stopped being centred and the last tab
   ran under the block number. Caught by Astra Pro reading the rendered page, 2026-09-08 — the CSS was
   correct in isolation and wrong the moment a sibling went display:none. */
.foot .ca.at-foot { grid-column: 1; }
.foot .dock-wrap { grid-column: 2; }
.foot .block { grid-column: 3; }
/* ONE ADDRESS, ONE PLACE, AT EVERY WIDTH. `at-head` and `at-foot` render the SAME contract address and
   both were visible at desktop, so the page printed its own identity twice while a THIRD address (the
   tank's wallet, a genuinely different thing with a different job) sat in the panel between them. Three
   mono strings, two of them identical, is what made the addresses read as decoration.
   They are a responsive PAIR now: the head one is the canonical position beside the wordmark, and the
   foot one exists only because the mobile block hides the head one (L586). Neither width shows both. */
.foot .ca.at-foot { min-width: 0; display: none; }
.foot .block { justify-self: end; text-shadow: var(--hud-text-shadow); font: var(--w-medium) var(--size-eyebrow) / 1 var(--font-mono); letter-spacing: var(--track-stat-label); text-transform: uppercase; color: var(--text-3); }

/* The pane is the WRAPPER and the scrolling is the row INSIDE it. At 390 the row is wider than the
   window, so a pane on the row was a pane that ran off the screen and scrolled away from under its own
   tabs; the frame should hold still while its contents move. The split is also what keeps the veil
   alive at 390: measured, a `mask-image` on the same element as a `backdrop-filter` cancels the filter
   in Chrome, and the dock's tabs sat on raw water at 1.31:1. Mask on the row, veil on the frame. */
/* min-width: 0 on both, or the scroll container's automatic min-content floor blows the footer's grid
   column past the viewport and takes the CA line out with it at 390. */
.dock-wrap { max-width: 100%; min-width: 0; border-radius: var(--r-action); }
.dock { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-2); min-width: 0; overflow-x: auto; scrollbar-width: none; }
.dock::-webkit-scrollbar { display: none; }
.tab {
  /* --s-4 horizontal, not --s-5: seven tabs at 24px of padding each make a 638px dock, which does not
     leave room for the CA and the block number either side of it at 1440. */
  flex: 0 0 auto; padding: var(--s-3) var(--s-4); border-radius: var(--r-action);
  color: var(--text-2); font: var(--w-medium) var(--size-dock) / 1 var(--font-body); letter-spacing: var(--track-sub);
  white-space: nowrap;   /* a tab that wraps to two lines is not a tap target (hallmark gate 49) */
  text-shadow: var(--hud-text-shadow);
}
.tab:hover { color: var(--text); }
.tab:active { transform: translateY(1px); }
.tab.active { background: var(--text); color: var(--text-on-cta); font-weight: var(--w-semibold); text-shadow: none; }

/* ── mobile: sheet, scrolling dock, two stats, one line hero ───────────────────────────────────── */
@media (max-width: 900px) {
  /* THE LADDER'S 390 VALUES BELONG TO THE TIER, not to sixteen separate -m tokens. Redefining the three
     tiers here is what makes every component below shrink together and in proportion; before this the
     mobile block set eight individual font sizes by hand, which is how a ladder drifts back apart. */
  :root { --tier-metric: 40px; --tier-heading: 20px; }

  /* Below the tablet break the panel is already the sheet: 358px of docked panel plus a hero does not
     fit beside the scene, and a narrower panel would break the type scale. */
  .panel {
    right: 0; left: 0; top: auto; bottom: 0; width: auto;
    height: var(--sheet-peek); max-height: var(--sheet-h);
    border-radius: var(--r-content) var(--r-content) 0 0;
    border-left: 0; border-right: 0; border-bottom: 0; border-top: 1px solid var(--line-2);
  }
  .panel-body { padding: 0 var(--s-5) var(--s-5); }
  /* The handle is the sheet's close affordance at 390, not decoration: it is a real button with a tap
     target taller than the bar it draws. */
  .panel .handle { display: block; flex: 0 0 auto; width: calc(var(--s-5) + var(--s-3)); height: var(--s-1); padding: 0; border-radius: var(--r-action); background: var(--line-2); margin: var(--s-3) auto; }
  .panel .handle:hover { background: var(--text-2); }
  .panel .handle:active { background: var(--text); }
  /* Sheet closed is the default at 390: the dock and the CA sit at the bottom of the window and the
     scene owns everything from the hero down to them. Opening the sheet lifts them above it. */
  /* The sheet rests open at --sheet-peek (system.md), so the dock and the CA sit above it. Closing the
     sheet by tapping the active tab drops them to the bottom of the window. */
  /* --foot-lift is the dead band the stage cap gave back (hud.js measureStage). The footer rises by it,
     so the CA line and the dock sit under the tank instead of under a hole, and what is left of the
     slack collects in one place — between the floating dock and the sheet, where it reads as the scene
     continuing rather than as a gap in the middle of the composition. It is 0 at 320, where the window
     is already shorter than the tank wants. */
  .foot {
    left: var(--hud-inset-m); right: var(--hud-inset-m);
    bottom: calc(var(--s-5) + var(--foot-lift, 0px));
    grid-template-columns: 1fr; justify-items: stretch; gap: var(--s-2);
  }
  .hud.sheet-open .foot { bottom: calc(var(--sheet-peek) + var(--s-3) + var(--foot-lift, 0px)); }
  .foot .block { display: none; }              /* the block number lives in the Tank panel at this width */
  /* The receipts pane becomes a row of the footer's own stack, above the CA line: at 390 there is no
     left column to dock it in, and the footer is already where this page keeps its readouts. It costs
     the stage HEIGHT here instead of width, and it is measured as part of the footer's box, so the tank
     is framed into what is left exactly as it is at 1440 (hud.js measureStage). */
  .agent { position: static; width: auto; max-height: none; order: -1; padding: var(--s-3) var(--s-4); }
  .brand .ca.at-head { display: none; }
  .foot .ca.at-foot { display: inline-flex; }   /* the head one is hidden here, so this is the only one */
  /* Every pixel the pane takes here is a pixel of tank, so at 390 it is one wrapping strip rather than a
     stack of blocks: the eyebrow and the three readings run inline (the status block gives up its own
     inner surface, because at this width the strip IS the pane), the receipt keeps the two figures the
     contract names, and its line and the ones behind it are reached through the link. Measured at 390:
     the pane costs ~100 px and the stage keeps a full width tank. */
  .agent-body { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--s-4); row-gap: var(--s-2); }
  .agent .status { display: contents; }
  .agent .msg { flex: 1 1 100%; margin-top: 0; font-size: var(--size-body-m); }
  /* The newest receipt and the link to all of them share the last line. */
  .agent .receipts { flex: 1 1 auto; margin-top: 0; }
  .agent .proof { flex: 0 1 auto; margin-top: 0; }
  .agent .receipts p { display: none; }
  .agent .receipts li:nth-child(n+2) { display: none; }
  /* When the pane has something to say about its own state (out of credit, needing its human, a stale
     beat) that sentence outranks the newest receipt at this width, and the tank keeps the height the
     receipt would have cost. The link is still there, and it is still the whole feed. */
  .agent-body:has(.msg) .receipts { display: none; }
  /* Collapsed to its eyebrow and its link, or shelved altogether while the sheet is open (hud.js
     measureStage sets both, from the band the pane would leave the tank at its full height): the window
     is too short for the strip and a tank at the same time, and the tank is the page. The readings are
     one tap away through the link, or a tap on the active tab away (it closes the sheet, and the pane
     comes back), and the status rail names the agent's state whichever step it is at. */
  .dock {
    /* The end fade is the cue that the dock keeps going; mask alpha only, so currentColor carries no
       colour decision into this file. It rides the scrolling ROW, not the frame: on the frame it took
       the frame's own glass with it (and, measured, its veil as well). */
    -webkit-mask-image: linear-gradient(90deg, currentColor 82%, transparent);
    mask-image: linear-gradient(90deg, currentColor 82%, transparent);
  }
  .tab { padding: var(--s-2) var(--s-4); font-size: var(--size-dock-m); }
  .brand { left: var(--hud-inset-m); top: var(--hud-inset-m); }
  .brand i { display: none; }                  /* the chain name and the two stat cells do not both fit at 390 */
  /* The strip keeps its pane at 390. It used to drop the fill and the rim and leave two bare mono
     columns leaning on a text-shadow over the brightest part of the scene; two cells fit inside the
     pane beside the mark at this width, so there is no reason left to take the surface away. */
  .stats { right: var(--hud-inset-m); top: var(--hud-inset-m); min-width: 0; gap: var(--s-4); padding: var(--s-3) var(--s-4); }
  .stats .cell.wide-only { display: none; }    /* Price and Feed only; Water and Fish live in the Tank panel */
  .stats strong { font-size: var(--size-stat-m); }
  .hero { left: var(--hud-inset-m); top: var(--hud-top-m); max-width: calc(100% - var(--hud-inset-m) * 2); }
  .hero .big { font-size: var(--size-display-m); }
  .hero .sub { margin-top: var(--s-2); font-size: var(--size-sub-m); }
  .hero .sub br, .hero .sub .dim { display: none; }   /* one line at 390 (system.md, mobile) */
  .foot .ca-full { display: none; }
  .foot .ca-abbr { display: inline; }             /* full CA stays in the Glass panel */
  .foot .ca.at-foot { font-size: var(--size-est); }
  .panel h3 { font-size: var(--size-h3-m); }
  .panel p, .faq dd, .faq dt { font-size: var(--size-body-m); }
  .panel .status { font-size: var(--size-status); }
  .btn { min-height: var(--cta-h-m); }
  .note { font-size: var(--size-note-m); top: 40%; left: 46%; }
  .keeper-bubble { font-size: var(--size-body-m); }
  /* Capped at three at 390 so a burst cannot stack over the whole tank. */
  .chips .chip:nth-child(n+4) { display: none; }
  .status-rail { bottom: calc(var(--dock-h) + var(--s-8) + var(--foot-lift, 0px)); }
  .hud.sheet-open .status-rail { bottom: calc(var(--sheet-peek) + var(--dock-h) + var(--s-8) + var(--foot-lift, 0px)); }
  .pill { font-size: var(--size-label); }
  /* Pills now track the scene and resolve their own overlaps, so all three can stay at 390. */
}

@media (prefers-reduced-motion: reduce) {
  /* State 8: the scene is still and every event is a chip; the chip itself still slides. */
  .panel { animation-duration: var(--t-chip); }
  .btn:active, .tab:active, .copy:active, .pill:active, .find .go:active { transform: none; }
}

/* ── the gallery ───────────────────────────────────────────────────────────────────────────────────
   The keeper's paintings, inside the agent pane because they are the most expensive thing it spends on.
   A horizontal strip that scrolls rather than wraps: the pane is narrow and a wrapped grid of squares
   turns it into a wall. Every value here is a token, so restyling the page restyles this with it. */
.agent .gallery {
  margin-top: var(--s-2); display: flex; gap: var(--s-2);
  overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x proximity;
  padding-bottom: var(--s-2);
}
.agent .gallery figure { flex: 0 0 auto; width: 92px; scroll-snap-align: start; }
.agent .gallery a { display: block; border-radius: var(--r-1); overflow: hidden; }
.agent .gallery img { display: block; width: 92px; height: 92px; object-fit: cover; background: var(--pill-on-glass); }
/* A charge with no picture behind it. It keeps its place in the row rather than vanishing. */
.agent .gallery .missing {
  width: 92px; height: 92px; border-radius: var(--r-1); background: var(--pill-on-glass);
  display: grid; place-items: center; color: var(--text-2);
  font: var(--w-medium) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-label);
}
.agent .gallery figcaption { margin-top: var(--s-1); display: flex; flex-direction: column; gap: 2px; }
.agent .gallery figcaption b {
  font: var(--w-semibold) var(--size-status) / 1.2 var(--font-mono); letter-spacing: var(--track-mono);
  color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.agent .gallery figcaption .k {
  font: var(--w-medium) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-label);
  color: var(--text-2); font-variant-numeric: tabular-nums;
}

/* The wallet card's own painting, when the keeper has made one of that fish. Bigger than a gallery
   thumbnail because the card is where somebody looks at their own, and because the agent pane it also
   lives in collapses on a short viewport. Tokens throughout, like the strip. */
.card-portrait { margin-top: var(--s-3); }
.card-portrait a { display: block; border-radius: var(--r-1); overflow: hidden; }
.card-portrait img { display: block; width: 100%; height: auto; background: var(--pill-on-glass); }
.card-portrait figcaption {
  margin-top: var(--s-2);
  font: var(--w-body) var(--size-body) / var(--lead-body) var(--font-body);
  letter-spacing: var(--track-body); color: var(--body-text);
}

/* While the stage is open it IS the page: everything else on screen is chrome about the tank, and the
   tank is behind the stage. Leaving the hero, the keeper, the in-scene pills and the spending pane drawn
   over a gallery is four things competing with the one thing you came to look at. The dock stays,
   because that is how you leave. */
.hud.prints-open .hero,
.hud.prints-open .pills,
.hud.prints-open .agent,
.hud.prints-open .keeper,
.hud.prints-open .keeper-bubble,
.hud.prints-open .keeper-leader,
.hud.prints-open .note,
.hud.prints-open .line { display: none; }

/* ── the prints stage ──────────────────────────────────────────────────────────────────────────────
   The gallery is not a panel. A square woodblock print in a 320px column is a thumbnail with a caption,
   so this insets from the viewport instead and leaves the tank visible around all four edges: the fish
   being painted are swimming behind it, and that is the reason it is not a modal. Everything is a token,
   so the aesthetic pass restyles it with the rest of the page. */
.prints {
  position: absolute; inset: var(--hud-top) var(--hud-inset) calc(var(--dock-h) + var(--s-6));
  display: flex; flex-direction: column; min-height: 0;
  border-radius: var(--r-content);
  animation: panel-in var(--t-chip) var(--ease-settle) both;
}
.prints[hidden] { display: none; }
.prints-body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--s-5) var(--s-6) var(--s-6); }

/* One print, and its record beside it. The print keeps its square: it is a woodblock, and cropping the
   indigo bands off the top and bottom would be cropping the composition. */
/* Centred and bounded, not stretched. Pinning the record to the far right of a 1,376px stage left a
   third of the screen as black nothing between the print and its own caption. The pair reads as one
   object now, and the print is capped by HEIGHT as well as width so the archive under it stays on
   screen instead of being scrolled off the bottom. */
.print-work {
  display: grid; grid-template-columns: minmax(0, auto) minmax(220px, 300px);
  gap: var(--s-6); align-items: start; justify-content: center;
  max-width: 1080px; margin: var(--s-4) auto 0;
}
.print-frame {
  display: block; padding: var(--s-4); background: var(--pill-on-glass); border-radius: var(--r-1);
}
/* The paper mat is part of the object, so the frame is sized off the image rather than the other way
   round: the print keeps its square and never gets cropped, which would cut the indigo bands off a
   woodblock composition. */
.print-frame img { display: block; width: auto; height: auto; max-width: min(640px, 100%); max-height: 56vh; border-radius: 2px; }
.print-frame.is-missing {
  display: grid; place-items: center; aspect-ratio: 1; color: var(--text-2);
  font: var(--w-medium) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-label);
}

/* The name is the loud thing, because it is what a holder recognises. Everything under it is mono,
   because everything under it is evidence. */
.print-name { margin: 0; }
.print-meta { margin-top: var(--s-2); display: flex; flex-wrap: wrap; gap: var(--s-3); }
.print-meta span, .print-wallet, .print-count {
  font: var(--w-medium) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--text-2); font-variant-numeric: tabular-nums;
}
.print-wallet { margin-top: var(--s-2); text-transform: none; }
.print-cost { margin-top: var(--s-5); display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap; }
.print-cost .k { font: var(--w-medium) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-label); color: var(--text-2); }
.print-cost b { font: var(--w-semibold) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-mono); color: var(--text); font-variant-numeric: tabular-nums; }

/* The counter sits between the two controls at a fixed width, so the buttons do not move as the names
   under them change length. */
.print-nav { margin-top: var(--s-5); display: flex; align-items: center; gap: var(--s-3); }
.print-nav .go { min-width: 40px; }
.print-count { min-width: 6ch; text-align: center; }
.prints .btn { margin-top: var(--s-4); width: 100%; }

/* The archive lines up under the work rather than starting at the stage's far left edge, so the two
   read as one column instead of two unrelated things on the same surface. */
.prints .eyebrow + .print-wall, .print-wall { max-width: 1080px; margin: var(--s-3) auto 0; }
.prints > .prints-body > .eyebrow:not(:first-child) { max-width: 1080px; margin-left: auto; margin-right: auto; margin-top: var(--s-6); }
.print-wall { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: var(--s-2); }
.print-thumb { padding: 0; border: 0; background: none; border-radius: var(--r-1); overflow: hidden; cursor: pointer; opacity: 0.65; transition: opacity var(--t-chip) var(--ease-settle); }
.print-thumb img { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; }
.print-thumb.is-missing {
  aspect-ratio: 1; background: var(--pill-on-glass); display: grid; place-items: center; opacity: 1;
  color: var(--text-2); font: var(--w-medium) var(--size-status) / 1 var(--font-mono); letter-spacing: var(--track-label);
}
.print-thumb:hover, .print-thumb.is-on { opacity: 1; }
.print-thumb.is-on { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Narrow: the record goes under the print, and the print gets the full width it can have. */
@media (max-width: 900px) {
  .print-work { grid-template-columns: minmax(0, 1fr); gap: var(--s-4); }
  .print-frame { max-width: none; }
}
