/*
 * Application styles.
 *
 * No colours here: every value comes from `theme.css`, which is generated from
 * `src/theme/tokens.ts`. A hex in this file would be a colour outside the design system, and one that
 * would not change when the theme does — which is exactly how the dev harness drifted.
 *
 * Layout only, plus the two things a design system cannot express: how the shell's regions divide the
 * viewport, and how the observation canvas fills the space the header leaves.
 */

* {
  box-sizing: border-box;
}

/*
 * Motion and type-scale constants.
 *
 * `--nouma-ease` is murmur's curve, copied exactly rather than re-approximated: every transition in
 * this stylesheet used to pick its own, and three near-identical bezier curves read as three
 * different products. One curve, two durations — `data` for values arriving from the swarm,
 * `interact` for things the visitor does.
 *
 * `--nouma-fs-floor` is the smallest size this interface will render, and it is 10px rather than 11
 * because a chart axis tick or a diagnostic weight is a mark to be scanned, not a sentence to be read.
 * Anything a visitor has to identify — a panel label, a field name, a table header, a button — is
 * clamped to `max(11px, var(--nouma-fs-floor))` instead. The distinction is deliberate: raising the
 * floor itself to 11px would also raise the annotations, and a chart whose captions are the same
 * weight as its values stops being a chart.
 */

/*
 * State tones — one vocabulary for four meanings.
 *
 * Four places had each invented their own: `data-tone` on the shell status, on a notice, on the
 * economy's settlement badge, and on an entrance card. They had drifted. `ok` was
 * `--nouma-accent-alt` in one and `--nouma-accent` in another, which is the same meaning wearing two
 * different colours; and a notice had no `warn` rule at all, so an amber warning was whatever the
 * default border happened to be.
 *
 * These are **aliases, not new colours** — the rule at the top of this file still holds. Each points at
 * a token `theme.css` already defines, so a tone follows the theme.
 *
 * `dim` is the one that had to be corrected rather than aliased. It pointed at `--nouma-muted`, which
 * no theme emits: the token is `--nouma-text-muted`, and the shorter name had survived from before
 * `tokens.ts` grew the prefix. An undefined custom property makes the declaration invalid at computed-
 * value time, so every "dimmed" figure and every secondary label inherited the body colour and read as
 * a primary value. The consumers now name the token that exists.
 *
 * `ok` is `accentAlt` and not `accent` deliberately. `accent` is the theme's own voice — headings, the
 * brand, every ornament — so using it for "this is fine" would make a healthy state and a title
 * indistinguishable. `accentAlt` is the theme's second voice, and "second voice" is exactly what a
 * status is.
 *
 * `warn` borrows `burn`, whose token comment reads "Value being destroyed". That is an economic
 * meaning, not a general one, and the borrow is a compromise: it is amber on every theme, which is what
 * a warning wants, and adding a fifth semantic colour to `tokens.ts` would change a published
 * interface for a shade. If a future screen needs to distinguish "burning" from "warned", that is the
 * moment to split them.
 */
:root {
  --nouma-tone-ok: var(--nouma-accent-alt);
  --nouma-tone-warn: var(--nouma-burn);
  --nouma-tone-alarm: var(--nouma-alarm);
  --nouma-tone-dim: var(--nouma-text-muted);
}
:root {
  --nouma-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --nouma-dur-interact: 200ms;
  --nouma-dur-data: 600ms;
  --nouma-fs-floor: 10px;
}

/*
 * Panel chrome — three radii and four steps of padding.
 *
 * The file had accumulated six radii (1, 2, 3, 4, 6 and a capsule) and seven paddings, which is not a
 * scale but a set of local decisions that happened to land near each other. The rule is now a
 * function of the element rather than of the screen it is on: 2px for anything a visitor operates or
 * that sits tight against a border — chips, inputs, buttons, table cells; 4px for anything that
 * contains other content — panels, cards, notices, code blocks; 999px only for a capsule, because a
 * pill is a shape and not a radius. Directional radii keep their direction and take a value from that
 * set, so a tab still has two square bottom corners.
 *
 * Padding follows the same idea in four steps: 12px for a tight row or a cell, 16px for a panel or a
 * card, 20px for a header or a section, 24px for a page's own margin. Most values were already one of
 * these; the point is that two elements of the same kind can no longer disagree, which is what made
 * the notice, the stat card and the step look like three families rather than one.
 */

html,
body {
  margin: 0;
  height: 100%;
  background: var(--nouma-background);
  color: var(--nouma-text);
  font-family: var(--nouma-font-body);
  -webkit-font-smoothing: antialiased;
}

#app {
  display: flex;
  flex-direction: column;
  /*
   * `height`, not `min-height`: with `min-height` the shell grew to the height of its content, so
   * `.shell-content` never had an overflow to scroll and `body { overflow: hidden }` simply clipped
   * the rest. A bounded shell is what gives the content region a scrollport.
   */
  height: 100%;
  /* The drawer is positioned against this, not against the viewport, so it never covers the header and
     scrolls with the document rather than floating over it. */
  position: relative;
  /*
   * The closed drawer sits off-canvas via `transform: translateX(100%)`, and a transform moves paint
   * without moving the box — so the panel still widened the document by its own width and put a
   * horizontal scrollbar under every screen. Measured: `scrollWidth` 1889 against `clientWidth` 1409.
   *
   * `clip` rather than `hidden`: `hidden` would make this a scroll container, which changes how the
   * sticky temperature band and the drawer's own `inset-block: 0` resolve. `clip` only trims overflow.
   */
  overflow-x: clip;
}

/* ---------------------------------------------------------------- shell */

.shell-header {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--nouma-border);
  background: var(--nouma-surface);
  /* Wraps rather than overflowing. At 1440px the twelve nav entries, the language picker, a status
     message and the theme button do not fit on one line, and the fallback was a horizontal scrollbar
     across the whole document — with `Observation hall` broken over two lines in the middle of it. */
  flex-wrap: wrap;
  row-gap: 10px;
}

/* A screen whose own bar is fixed to the top of the viewport. The header is not merely covered — it
   is covered, so `display: none` is the honest state: it also removes the nav, the language picker,
   the theme button and the menu from the tab order, which is where they were doing harm. The screen
   that declares this is responsible for offering the same reach — the observation hall's bar carries
   its own `sections` chip, which opens the shell's drawer. */
html[data-surface="full"] .shell-header {
  display: none;
}

.shell-brand,
.shell-nav a,
.shell-theme,
.shell-language {
  /* A nav entry is one label. Wrapping "Observation hall" onto two lines makes the header taller than
     the screens it labels and moves every other entry off the baseline. */
  white-space: nowrap;
}

.shell-brand {
  color: var(--nouma-accent);
  font-family: var(--nouma-font-display);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-decoration: none;
}

/*
 * The mobile menu button.
 *
 * Hidden by default: the header carries the full navigation on wide screens, and a second way into
 * the same destinations would only add noise. It appears below 900px, where the nav, the language
 * picker and the theme switch move into the drawer instead.
 */
.shell-menu {
  display: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  background: transparent;
  color: var(--nouma-text-muted);
  line-height: 0;
  cursor: pointer;
}

.shell-menu:hover {
  color: var(--nouma-text);
  border-color: var(--nouma-accent-alt);
}

.shell-nav {
  display: flex;
  gap: 18px;
  margin-inline-end: auto;
  /* The nav may wrap internally; the header decides where. `min-width: 0` is the fix for the flex
     default of `min-width: auto`, which is what let a long status message elsewhere in the header
     push the nav's contents wider than the nav and spill a scrollbar onto the document. */
  flex-wrap: wrap;
  min-width: 0;
}

.shell-nav a {
  color: var(--nouma-text-muted);
  font-size: max(13px, var(--nouma-fs-floor));
  letter-spacing: 0.04em;
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
}

.shell-nav a:hover {
  color: var(--nouma-text);
}

/* The active link is marked by the router, never by the click handler. */
.shell-nav a[aria-current="page"] {
  color: var(--nouma-accent);
  border-bottom-color: var(--nouma-accent);
}

.shell-right {
  display: flex;
  align-items: center;
  gap: 16px;
  /* `min-width: 0` again: without it this group's width is its content's width, so a long status
     message widens the group past the header and every other control slides off the edge. */
  min-width: 0;
  flex-wrap: wrap;
  row-gap: 8px;
}

.shell-status {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--nouma-text-muted);
  /* A status message is a diagnostic, not content: `GET /api/v1/chain/breeding -> HTTP 502` is 37
     characters, and letting it set the header's width pushed the navigation off screen. Truncated
     with the full text in `title`, so the detail is still reachable on hover. */
  max-width: 30ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shell-status[data-tone="ok"] {
  color: var(--nouma-tone-ok);
}

.shell-status[data-tone="warn"] {
  color: var(--nouma-tone-warn);
}

.shell-status[data-tone="alarm"] {
  color: var(--nouma-tone-alarm);
}

.shell-theme {
  padding: 6px 12px;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  background: transparent;
  color: var(--nouma-text-muted);
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  cursor: pointer;
}

.shell-theme:hover {
  color: var(--nouma-text);
  border-color: var(--nouma-accent-alt);
}

/*
 * The language picker.
 *
 * Deliberately the same shape as `.shell-theme`: they sit next to each other, and two controls in one
 * row that look like different kinds of thing read as belonging to different parts of the page.
 *
 * `appearance: none` because a native `<select>` on a light ground renders as an OS-styled widget with
 * its own background, which on paper is a grey box in a header made of hairlines. The chevron is drawn
 * with a background gradient so no icon file is needed, and it is on the *inline-end* side so it
 * follows the text direction instead of staying on the right in Arabic.
 */
.shell-language {
  padding: 5px 22px 5px 10px;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  background-color: transparent;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 10px top 50%, right 6px top 50%;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
  color: var(--nouma-text-muted);
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  appearance: none;
  cursor: pointer;
}

.shell-language:hover {
  color: var(--nouma-text);
  border-color: var(--nouma-accent-alt);
}

/* The chevron and its padding follow the direction; the text itself is always upright. */
[dir="rtl"] .shell-language {
  padding: 5px 10px 5px 22px;
  background-position: left 10px top 50%, left 6px top 50%;
}

.shell-language option {
  background: var(--nouma-surface);
  color: var(--nouma-text);
}

/*
 * The drawer.
 *
 * Anchored with `inset-inline-end`, so it slides in from the right in English and the left in Arabic
 * with no branch anywhere: the same two rules serve both, which is the whole point of the logical
 * properties the rest of this file was converted to.
 *
 * The scrim is deliberately faint. It exists to catch the click that closes the drawer and to signal
 * that the page is behind something — not to hide it. The swarm keeps ticking underneath, and a dark
 * wash would make the one thing worth watching look switched off.
 */
.drawer-scrim {
  position: fixed;
  inset: 0;
  background: rgba(26, 26, 24, 0.28);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--nouma-dur-interact) var(--nouma-ease);
  z-index: 49;
}

.drawer-scrim.drawer-open {
  opacity: 1;
  pointer-events: auto;
}

.drawer {
  position: fixed;
  top: 0;
  bottom: 0;
  right: 0;
  height: 100vh;
  height: 100dvh;
  width: min(32rem, 92vw);
  display: flex;
  flex-direction: column;
  background: var(--nouma-surface);
  border-inline-start: 1px solid var(--nouma-border);
  box-shadow: var(--nouma-shadow-2, 0 12px 30px rgba(60, 46, 28, 0.18));
  /* Off-canvas in the inline direction, so the closed state is a translation the browser applies
     rather than a `display` change that would drop the panel's scroll position. */
  transform: translateX(100%);
  /* A closed drawer must not be reachable by Tab. It is still in the document — the transform is
     purely visual — so without this a keyboard visitor would tab into a fly's detail panel that is
     scrolled off the side of the screen. `visibility` is transitioned so it flips at the *end* of the
     closing animation rather than hiding the panel while it slides out. */
  visibility: hidden;
  transition: transform var(--nouma-dur-interact) var(--nouma-ease), visibility 0s linear var(--nouma-dur-interact);
  z-index: 50;
}

[dir="rtl"] .drawer {
  right: auto;
  left: 0;
  transform: translateX(-100%);
}

.drawer.drawer-open,
[dir="rtl"] .drawer.drawer-open {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--nouma-dur-interact) var(--nouma-ease), visibility 0s;
}

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

.drawer:focus {
  outline: none;
}

.drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--nouma-border);
}

.drawer-title {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 16px;
  font-weight: 600;
  color: var(--nouma-text);
}

.drawer-close {
  padding: 2px 10px 4px;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  background: transparent;
  color: var(--nouma-text-muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

.drawer-close:hover {
  color: var(--nouma-text);
  border-color: var(--nouma-accent-alt);
}

.drawer-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 16px;
}

/* The way from the inspector to the record. Right-aligned and quiet: it is an exit from the panel, not
   the first thing to read in it. */
.hud-record-link {
  display: block;
  margin: 0 0 12px;
  text-align: right;
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.04em;
  color: var(--nouma-accent);
  text-decoration: none;
}

.hud-record-link:hover,
.hud-record-link:focus-visible {
  text-decoration: underline;
}

/*
 * A fly's clickable target.
 *
 * The canvas is one element, so there is nothing to style per fly — this is the affordance instead:
 * a pointer cursor over the stage, because every visible mark is a control.
 */
.swarm-stage.is-clickable {
  cursor: pointer;
}

.shell-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /*
   * This is the scroll container for every screen.
   *
   * `app/index.html` sets `body { overflow: hidden }` so the home page's full-bleed canvas cannot
   * scroll; that rule propagates to the viewport, which left the shell with no scrollport at all.
   * Measured on `#/economy`: the document was 1641px tall in a 900px viewport and the wheel moved
   * `scrollY` not at all — the bottom third of the page was permanently unreachable. Scrolling here
   * rather than on `body` keeps the header pinned without a `position: sticky` and keeps the
   * drawer's fixed positioning intact.
   *
   * `contain` stops a scroll that reaches the end from chaining out to the viewport behind it.
   * `app/hud.css` re-hides this on the home screen (`.shell-content:has(.world-page)`), where the
   * canvas owns the whole viewport and must not scroll.
   */
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ------------------------------------------------------- sections drawer */

/*
 * What `openSections()` renders into `.drawer-body`: the navigation that no longer fits in a mobile
 * header, then the language and theme pickers, in the same order they used to sit in the header.
 * `.drawer` itself is untouched — these are its contents, not a second panel.
 */
.sections {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.sections-nav {
  display: flex;
  flex-direction: column;
}

.sections-link {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px;
  text-decoration: none;
  border-radius: 4px;
  /* The active marker is a rule on the inline-start edge; transparent when inactive so the label
     does not shift sideways when the page changes. */
  border-inline-start: 2px solid transparent;
}

.sections-link:hover {
  background: var(--nouma-background);
}

.sections-link[aria-current="page"] {
  border-inline-start-color: var(--nouma-accent);
  background: var(--nouma-background);
}

.sections-label {
  font-size: max(13px, var(--nouma-fs-floor));
  color: var(--nouma-text);
  letter-spacing: 0.02em;
}

.sections-hint {
  font-size: max(11px, var(--nouma-fs-floor));
  line-height: 1.5;
  color: var(--nouma-text-muted);
  /* Some sections have no hint at all; `min-height: 0` keeps the empty span from reserving a line. */
  min-height: 0;
}

.sections-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 16px;
  border-top: 1px solid var(--nouma-border);
}

.sections-heading {
  margin: 0;
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--nouma-text-muted);
  font-weight: 400;
}

.sections-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.sections-choice {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  padding: 6px 12px;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  background: transparent;
  color: var(--nouma-text-muted);
  cursor: pointer;
}

.sections-choice:hover {
  color: var(--nouma-text);
  border-color: var(--nouma-accent-alt);
}

.sections-choice[aria-current="true"] {
  color: var(--nouma-accent);
  border-color: var(--nouma-accent);
}

/* ------------------------------------------------------------- home page */

.home-page {
  /* `width: 100%` because `margin: 0 auto` on a flex item defeats `align-items: stretch`, which made this
     `max-width` inert. See `.economy-page` for the measurement and the full explanation. */
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  padding: 56px 24px 72px;
}

.home-header h1 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 44px;
  letter-spacing: 0.2em;
  color: var(--nouma-accent);
}

.home-tagline {
  margin: 12px 0 0;
  color: var(--nouma-text-muted);
  font-size: 15px;
  line-height: 1.7;
}

/* Sections share a rhythm and a hairline, so the page reads as three movements rather than as an
   undifferentiated scroll. */
.home-window,
.home-migration,
.home-narrative {
  margin-top: 40px;
  padding-top: 24px;
  border-top: 1px solid var(--nouma-border);
}

.home-window h2,
.home-migration h2 {
  margin: 0 0 16px;
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--nouma-gene);
}

.home-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  margin: 0 0 28px;
}

.home-figures dt {
  margin: 0;
  color: var(--nouma-text-muted);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.home-figures dd {
  margin: 5px 0 0;
  color: var(--nouma-text);
  font-family: var(--nouma-font-mono);
  font-size: 20px;
}

.home-entry {
  display: inline-block;
  padding: 12px 20px;
  border: 1px solid var(--nouma-accent);
  border-radius: 2px;
  color: var(--nouma-accent);
  font-size: max(14px, var(--nouma-fs-floor));
  text-decoration: none;
}

.home-entry:hover {
  background: var(--nouma-surface-raised);
}

.home-migration-body {
  margin: 0;
  color: var(--nouma-text);
  font-family: var(--nouma-font-mono);
  font-size: max(14px, var(--nouma-fs-floor));
}

.home-narrative p {
  margin: 0 0 18px;
  color: var(--nouma-text-muted);
  font-size: max(14px, var(--nouma-fs-floor));
  line-height: 1.9;
}

/* The one paragraph that is a disclosure rather than narrative. */
.home-note {
  padding-top: 14px;
  border-top: 1px dashed var(--nouma-border);
  color: var(--nouma-burn);
  font-size: max(12px, var(--nouma-fs-floor));
  line-height: 1.8;
}

/* ------------------------------------------------------ observation hall */

.swarm-page {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* The canvas fills what is left. `min-height: 0` above is what lets a flex child shrink below its
   content size, without which the stage would grow the page instead of scrolling nothing. */
.swarm-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  background: var(--nouma-background);
}

.swarm-stage canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The band reports one value, so it is one colour. A gradient would imply a range. */
.temperature-band {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 3px;
  background: var(--band, var(--nouma-accent-alt));
  transition: background var(--nouma-dur-data) var(--nouma-ease);
}

.swarm-readout {
  position: absolute;
  top: 16px;
  inset-inline-start: 20px;
  display: flex;
  gap: 28px;
  margin: 0;
  pointer-events: none;
}

.swarm-readout dt {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 0;
  color: var(--nouma-text-muted);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* The dot sits on the label's baseline; it is a state, not a bullet. */
.swarm-readout dt svg {
  flex: none;
}

.swarm-readout dd {
  margin: 4px 0 0;
  color: var(--nouma-text);
  font-family: var(--nouma-font-mono);
  font-size: 15px;
}

/* The legend for the canvas's encoding.
 *
 * It has to be HTML rather than `fillText`: a canvas string is invisible to a screen reader and
 * unreachable by a dictionary, so a legend drawn on the swarm would have stayed English in a
 * seven-language product. A solid panel rather than a tint, because it sits over a canvas whose
 * ground changes with the theme and the text has to stay readable on all three.
 *
 * `pointer-events: none` keeps it out of the way of the canvas's click-to-inspect: a legend is not
 * a control, and a viewer who clicks a swatch is trying to click a fly.
 */
.swarm-legend {
  position: absolute;
  bottom: 14px;
  inset-inline-start: 20px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  max-width: min(280px, calc(100% - 40px));
  padding: 11px 13px;
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  background: var(--nouma-surface);
  pointer-events: none;
}

.swarm-legend-row {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--nouma-text-muted);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.04em;
  line-height: 1.35;
}

/* Size: three cores, so "bigger means more" is read at a glance rather than measured. The middle one
   is the size an ordinary fly is drawn at on a laptop. */
.swarm-legend-sizes {
  display: flex;
  flex: none;
  align-items: center;
  gap: 5px;
  width: 42px;
}

.swarm-legend-sizes i {
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--nouma-text);
  opacity: 0.55;
}

/* Fill: the live temperature ramp, at the alpha an ordinary core is drawn with. The strip and the
   dots are the same colour by construction, because both read the theme's ramp. */
.swarm-legend-ramp {
  flex: none;
  width: 42px;
  height: 9px;
  border-radius: 5px;
  opacity: 0.775;
}

/* Ring: one per behaviour, stroked exactly as the engine strokes them — same colour, same width,
   same inset from the core. */
.swarm-legend-rings {
  display: flex;
  flex: none;
  align-items: center;
  gap: 5px;
  width: 42px;
}

/* An ellipse, not a circle: the canvas draws each fly's behaviour ring in the ring's own plane, so it
   arrives squashed by the tilt. A round swatch beside a round mark would be a legend that describes a
   shape the canvas stopped drawing. */
.swarm-legend-rings i {
  width: 12px;
  height: 6px;
  border: 1px solid var(--ring);
  border-radius: 50%;
  opacity: 0.7;
}

.swarm-legend-note {
  margin: 2px 0 0;
  padding-top: 7px;
  border-top: 1px solid var(--nouma-border);
  color: var(--nouma-text-muted);
  font-size: max(10px, calc(var(--nouma-fs-floor) - 1px));
  line-height: 1.4;
  opacity: 0.85;
}

.swarm-controls {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 20px;
  border-top: 1px solid var(--nouma-border);
  background: var(--nouma-surface);
}

.swarm-module,
.swarm-intensity {
  font-family: var(--nouma-font-mono);
  font-size: max(12px, var(--nouma-fs-floor));
  color: var(--nouma-text);
  background: var(--nouma-surface-raised);
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  padding: 8px 12px;
}

.swarm-poke {
  padding: 8px 16px;
  border: 1px solid var(--nouma-burn);
  border-radius: 2px;
  background: transparent;
  color: var(--nouma-burn);
  font-family: var(--nouma-font-mono);
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.06em;
  cursor: pointer;
}

.swarm-poke:hover:not(:disabled) {
  background: var(--nouma-surface-raised);
}

.swarm-poke:disabled {
  opacity: 0.5;
  cursor: progress;
}

.swarm-poke-result {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

/* --------------------------------------------------- chronicle / inspector */

.page-subtitle {
  margin: 6px 0 22px;
  color: var(--nouma-text-muted);
  font-size: max(13px, var(--nouma-fs-floor));
  line-height: 1.7;
  word-break: break-word;
}

/* Shown while a lazily-loaded screen is fetched. It replaces the previous screen, so it has to be
   something other than a blank page — a blank page looks like a broken link. */
.page-loading {
  padding: 64px 24px;
  color: var(--nouma-text-muted);
  font-family: var(--nouma-font-mono);
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.1em;
}

/*
 * The skeleton inside it.
 *
 * One line of text held the shape of nothing: the content region collapsed to twelve pixels and the
 * real screen then pushed the page down as it mounted. A title bar and three bars hold roughly the
 * outline of a screen, so the swap happens in place.
 *
 * The bars are drawn in `--nouma-surface-raised` rather than a grey of their own, because a skeleton
 * that is a different colour from the surface it sits on is a second palette to keep in step with
 * three themes. The pulse is one opacity keyframe and nothing else — a sweeping gradient would be
 * prettier and would also be the only moving thing on a page whose whole point is measured data.
 */
.page-loading-title,
.page-loading-line {
  display: block;
  border-radius: 2px;
  background: var(--nouma-surface-raised);
}

.page-loading-title {
  width: min(320px, 46%);
  height: 26px;
  margin-bottom: 18px;
}

.page-loading-lines {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 720px;
}

.page-loading-line {
  height: 12px;
}

/* Uneven on purpose: three bars of equal length read as a table of values rather than as text that
   has not arrived yet. */
.page-loading-line:nth-child(2) {
  width: 88%;
}

.page-loading-line:nth-child(3) {
  width: 64%;
}

.page-loading-status {
  margin: 22px 0 0;
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.1em;
  color: var(--nouma-text-muted);
}

@media (prefers-reduced-motion: no-preference) {
  .page-loading-title,
  .page-loading-line {
    animation: nouma-skeleton 1.8s var(--nouma-ease) infinite;
  }
}

@keyframes nouma-skeleton {
  0%,
  100% {
    opacity: 0.55;
  }
  50% {
    opacity: 1;
  }
}

/* ------------------------------------------------------------------ docs page */

.docs-page {
  width: 100%;
  max-width: 860px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.docs-page h2 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--nouma-accent);
}

.docs-section {
  margin-top: 34px;
}

.docs-section h3 {
  margin: 0 0 6px;
  font-size: max(13px, var(--nouma-fs-floor));
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--nouma-gene);
}

.docs-intro {
  margin: 0 0 14px;
  color: var(--nouma-text-muted);
  font-size: max(12px, var(--nouma-fs-floor));
  line-height: 1.8;
}

.docs-list {
  margin: 0;
  padding-inline-start: 18px;
  color: var(--nouma-text-muted);
  font-size: max(12px, var(--nouma-fs-floor));
  line-height: 2;
}

.docs-list code {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text);
}

.docs-list a {
  color: var(--nouma-accent);
  text-decoration: none;
  border-bottom: 1px solid var(--nouma-border);
}

.docs-list a:hover {
  border-bottom-color: var(--nouma-accent);
}

.docs-where {
  margin-top: 34px;
  padding-top: 16px;
  border-top: 1px solid var(--nouma-border);
  color: var(--nouma-text-muted);
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
}

/* ----------------------------------------------- contract panels (M5/M6/M7/M10) */

.contract-page {
  width: 100%;
  max-width: 860px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.contract-page h2 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--nouma-accent);
}

.contract-explain {
  margin-bottom: 22px;
  color: var(--nouma-text-muted);
  font-size: max(12px, var(--nouma-fs-floor));
}

.contract-explain summary {
  cursor: pointer;
  letter-spacing: 0.06em;
}

.contract-explain ul {
  margin: 10px 0 0;
  padding-inline-start: 18px;
  line-height: 1.9;
}

/* -------------------------------------------------------------- canary page */

.canary-page {
  width: 100%;
  max-width: 860px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.canary-page h2 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--nouma-accent);
}

/*
 * A caveat about data the protocol does not measure.
 *
 * Styled to read as prose rather than as a figure: it sits beside the metric cards, and anything that
 * looks like one would be read as a value — which is the mistake this component exists to prevent.
 */
.notice {
  padding: 16px;
  border: 1px solid var(--nouma-border);
  border-inline-start: 3px solid var(--nouma-tone-warn);
  border-radius: 4px;
  background: var(--nouma-surface);
}

/* Three tones now, not one. The default rule above is amber because a notice usually reports something
   withheld; an alarm overrides it. `ok` is here for completeness so a caller passing it does not fall
   through to the warning colour by accident — which is what used to happen to `warn`. */
.notice[data-tone="ok"] {
  border-inline-start-color: var(--nouma-tone-ok);
}

.notice[data-tone="warn"] {
  border-inline-start-color: var(--nouma-tone-warn);
}

.notice[data-tone="alarm"] {
  border-inline-start-color: var(--nouma-tone-alarm);
}

.notice-title {
  margin: 0 0 8px;
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--nouma-tone-warn);
}

.notice[data-tone="ok"] .notice-title {
  color: var(--nouma-tone-ok);
}

.notice[data-tone="alarm"] .notice-title {
  color: var(--nouma-tone-alarm);
}

.notice-lines {
  margin: 0;
  padding-inline-start: 18px;
  color: var(--nouma-text-muted);
  font-size: max(12px, var(--nouma-fs-floor));
  line-height: 1.8;
}

.chronicle-page {
  width: 100%;
  max-width: 860px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.chronicle-page h2 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--nouma-accent);
}

/*
 * The fly detail, as it appears inside the drawer.
 *
 * No max-width and no centring, unlike the page classes above: the drawer is already a narrow column,
 * and a centred 860px block inside a 30rem panel would be a rule about a viewport that no longer
 * applies. The subtitle is the one piece that needs constraining — it is a long sentence about the
 * shared connectome.
 */
.fly-detail .page-subtitle {
  margin-top: 0;
}

.fly-detail-panel {
  margin-top: 4px;
}

/* ------------------------------------------------------------------ spec engine
 *
 * The Spec Engine renders eight of the twelve screens, and until now **none** of the classes it emits
 * had a rule. The pages were correct and unstyled: a `<ul>` of bare `<li>`, figures with no grouping,
 * hashes in the body font. Every complaint about this interface looking unfinished traced back here —
 * not to the palette, which was fine, but to half the product never having been designed.
 *
 * The vocabulary is deliberately small: a card, a labelled statistic, a badge, a hash, a feed row.
 * Anything more would be inventing a different design for each screen, which is how the twelve drifted
 * apart in the first place.
 *
 * Numbers are tabular everywhere. A balance that reflows as it ticks is unreadable, and that is not a
 * hypothetical on this product.
 */

.fly-card {
  margin: 0;
  padding: 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  background: var(--nouma-surface);
}

.fly-card > header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.fly-card strong {
  font-family: var(--nouma-font-display);
  font-size: 15px;
  font-weight: 600;
  color: var(--nouma-text);
}

/* The house is a membership, not a value — a chip rather than another line of text. */
.fly-card .house,
.kind {
  padding: 1px 7px;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--nouma-text-muted);
}

/*
 * Behaviour, in its own colour.
 *
 * The same four colours the canvas draws as rings, from the same tokens — so a fly that reads `rest`
 * here and a fly ringed in moss on the canvas are legibly the same fact. `data-behaviour` is emitted
 * by the component; colouring by class alone would need four classes and would let the fifth
 * behaviour fall through unstyled.
 */
.fly-card .behaviour {
  margin: 8px 0 0;
  font-family: var(--nouma-font-mono);
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.04em;
}

.fly-card .behaviour[data-behaviour="agitate"] { color: var(--nouma-behaviour-agitate); }
.fly-card .behaviour[data-behaviour="explore"] { color: var(--nouma-behaviour-explore); }
.fly-card .behaviour[data-behaviour="aggregate"] { color: var(--nouma-behaviour-aggregate); }
.fly-card .behaviour[data-behaviour="rest"] { color: var(--nouma-behaviour-rest); }

.fly-card .metrics {
  margin: 6px 0 0;
  font-family: var(--nouma-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: max(12px, var(--nouma-fs-floor));
  color: var(--nouma-text);
}

/* The life line: stage, age, and the Tick it was born in. Quiet on purpose — it is the fly's history
   rather than its present state, and the behaviour line below it is what is happening now. */
.fly-card .life {
  margin: 6px 0 0;
  font-family: var(--nouma-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

.fly-card .genome,
.tx {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
  overflow-wrap: anywhere;
}

/* ---- labelled statistics -------------------------------------------------- */

.stat-label,
.stat-value {
  display: block;
}

.stat-label {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--nouma-text-muted);
}

.stat-value {
  margin-top: 2px;
  font-family: var(--nouma-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 15px;
  color: var(--nouma-text);
}

/* A row of statistics: fixed auto-fit so a two-figure panel and a six-figure one both read as a row
   rather than as a column that happens to be short. */
.swarm-summary,
.connectome-mini {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 14px 20px;
  margin: 0 0 18px;
  padding: 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  background: var(--nouma-surface);
}

.connectome-mini span {
  font-family: var(--nouma-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: max(12px, var(--nouma-fs-floor));
  color: var(--nouma-text);
}

/* The one sentence that must not be mistaken for a measurement — see M3: the connectome is shared,
   and a panel that presented it as this fly's own would describe something that does not exist. */
.connectome-mini small {
  grid-column: 1 / -1;
  font-size: max(11px, var(--nouma-fs-floor));
  line-height: 1.6;
  color: var(--nouma-text-muted);
}

/* ---- feeds and panels ----------------------------------------------------- */

.chronicle-feed {
  margin: 0;
  padding: 0;
  list-style: none;
}

.chronicle-feed > li {
  padding: 12px 0;
  border-top: 1px solid var(--nouma-border);
}

.chronicle-feed > li:first-child {
  border-top: none;
}

.chronicle-panel,
.contract-panel,
.canary-panel {
  margin: 0 0 22px;
}

.stack-title {
  margin: 0 0 10px;
  font-family: var(--nouma-font-display);
  font-size: max(14px, var(--nouma-fs-floor));
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--nouma-accent);
}

.poke-control {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 14px;
}

.burn-sparkline {
  display: block;
  width: 100%;
  height: 24px;
}

/* Free text from a spec. Constrained to a readable measure, because a paragraph that spans a wide
   panel is the fastest way to make a page look unfinished. */
.text {
  margin: 0 0 12px;
  max-width: 62ch;
  line-height: 1.7;
}

/* ---------------------------------------------------------------- drive panel
 *
 * Why a fly did what it did. The `D_i` breakdown the engine used to decide, which the economy has
 * computed every Tick and nothing has ever displayed.
 *
 * The bar has two forms and that is the whole design. A unipolar drive grows from the left; a bipolar
 * one grows from **its own centre line**, so a negative trust score and no trust at all are different
 * pictures. murmur's `.etho-fill` works the same way — `left` and `width` both transition, so the bar
 * extends out of the middle rather than sliding across it.
 */
.drive-panel {
  margin: 0 0 22px;
  padding: 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  background: var(--nouma-surface);
}

.drive-why {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: max(14px, var(--nouma-fs-floor));
  font-weight: 600;
  color: var(--nouma-text);
}

.drive-amount {
  margin: 4px 0 14px;
  font-family: var(--nouma-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: max(13px, var(--nouma-fs-floor));
  color: var(--nouma-accent);
}

.drive-row {
  display: grid;
  grid-template-columns: 4.5rem 1fr 3rem;
  gap: 10px;
  align-items: center;
  margin-bottom: 6px;
}

.drive-lab,
.drive-val {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

.drive-val {
  text-align: end;
  font-variant-numeric: tabular-nums;
  color: var(--nouma-text);
}

.drive-bar {
  position: relative;
  height: 10px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--nouma-text) 8%, transparent);
}

/* The centre line. Only for the bipolar rows — a mark at the middle of a left-anchored bar would
   suggest a midpoint that means nothing there. */
.drive-bar[data-bipolar="true"]::before {
  content: "";
  position: absolute;
  inset-block: -1px;
  inset-inline-start: 50%;
  width: 1px;
  background: color-mix(in srgb, var(--nouma-text) 30%, transparent);
}

.drive-fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--drive-pct, 0%);
  border-radius: 2px;
  background: var(--nouma-accent);
  transition:
    width var(--nouma-dur-data) var(--nouma-ease),
    inset-inline-start var(--nouma-dur-data) var(--nouma-ease);
}

.drive-bar[data-bipolar="true"] .drive-fill {
  inset-inline-start: 50%;
}

/* Trust below neutral grows the other way. `inset-inline-start: auto` with `inset-inline-end: 50%`
   keeps it on the correct side under RTL too, which a hard `left`/`right` pair would not. */
.drive-bar[data-bipolar="true"] .drive-fill[data-side="neg"] {
  inset-inline-start: auto;
  inset-inline-end: 50%;
  background: var(--nouma-alarm);
}

.drive-weights,
.drive-note {
  margin: 10px 0 0;
  font-family: var(--nouma-font-mono);
  font-size: var(--nouma-fs-floor);
  letter-spacing: 0.04em;
  color: var(--nouma-text-muted);
}

.drive-note {
  margin-top: 4px;
  opacity: 0.75;
}

/* ------------------------------------------------------------------- the lab
 *
 * The form is obrain's research figures: lettered panels, each with a title that states a finding,
 * laid out as small multiples rather than one chart with a legend. See `app/pages/lab.ts` for why.
 *
 * Three columns with the first wider, because panel `a` is a composition over time (needs width to
 * show a trend) and `b`/`c` are distributions (need only enough width to show a spread). It collapses
 * to one column below ~900px, where three narrow panels would be three unreadable ones.
 */
.lab-page {
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.lab-header h2 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--nouma-accent);
}

.lab-grid {
  display: grid;
  grid-template-columns: 1.9fr 1fr 1fr;
  gap: 26px;
  align-items: start;
}

@media (max-width: 900px) {
  .lab-grid {
    grid-template-columns: 1fr;
  }

  /*
   * The header narrows to brand + menu button.
   *
   * Seventeen nav entries wrapped onto four lines and made the header 193px tall — a fifth of a
   * 900px screen spent on chrome. The nav, the language picker, the theme switch and the status line
   * all move into the drawer, leaving a single row about 52px high. `nowrap` because with only two
   * items left there is nothing to wrap, and wrapping would silently bring the tall header back if a
   * third control were ever added.
   */
  .shell-nav {
    display: none;
  }

  .shell-language {
    display: none;
  }

  .shell-theme {
    display: none;
  }

  .shell-status {
    display: none;
  }

  .shell-header {
    padding: 12px 16px;
    gap: 12px;
    flex-wrap: nowrap;
  }

  .shell-menu {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* `.shell-nav`'s `margin-inline-end: auto` was what pushed the controls to the far end of the
       header; with the nav gone that auto margin goes with it, so the button takes it over and the
       row reads brand-left, menu-right. */
    margin-inline-start: auto;
  }

  /*
   * Seventeen links in one column is a 1000px scroll, which would put the language and theme rows
   * below the fold — on the one screen size where the header no longer offers them. Two columns
   * with the hints dropped fits the whole index above the preference rows.
   */
  .sections-nav {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px;
  }

  .sections-hint {
    display: none;
  }

  .sections-link {
    padding: 12px;
  }
}

.lab-panel {
  margin: 0;
}

/* A panel that needs the whole row. The raster is 48 rows tall and reads left-to-right as time, so a
   third of the width would show the flies and hide the rhythm. */
.lab-panel-wide {
  grid-column: 1 / -1;
}

/* The letter is a marker, not a word: heavier, smaller, and separated from the title it belongs to. */
.lab-letter {
  display: inline-block;
  margin-inline-end: 8px;
  font-weight: 700;
  color: var(--nouma-accent);
}

.lab-title {
  margin: 0 0 10px;
  font-family: var(--nouma-font-body);
  font-size: max(13px, var(--nouma-fs-floor));
  font-weight: 600;
  line-height: 1.4;
  color: var(--nouma-text);
}

.lab-body {
  min-height: 160px;
}

.lab-chart {
  display: block;
  width: 100%;
  height: auto;
  /* A hairline under the chart rather than a full frame: obrain's figures mark the axes and leave the
     rest open, which is what keeps three of them readable side by side. */
  border-block-end: 1px solid var(--nouma-border);
}

.lab-axis line {
  stroke: var(--nouma-border);
  stroke-width: 1;
}

/* The zero rule on the dose chart. Heavier than an axis, because it is the line a reader measures
   against: a stimulus that added nothing and one that added 400 spikes are only distinguishable
   relative to it. */
.lab-zero {
  stroke: var(--nouma-text-muted);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.5;
}

/* The "do this and the chart fills in" line. Distinguished from a caption because it is an
   instruction rather than a description of what is on screen. */
.lab-hint {
  margin: 6px 0 0;
  font-size: max(11px, var(--nouma-fs-floor));
  line-height: 1.6;
  color: var(--nouma-accent-alt);
}

.lab-caption,
.lab-axis-labels,
.lab-empty {
  margin: 8px 0 0;
  font-family: var(--nouma-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--nouma-fs-floor);
  line-height: 1.6;
  color: var(--nouma-text-muted);
}

.lab-empty {
  padding: 24px 0;
  text-align: center;
}

.lab-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

.lab-legend li {
  display: flex;
  align-items: center;
  gap: 6px;
}

.lab-swatch {
  width: 9px;
  height: 9px;
  border-radius: 2px;
}

/* Descent edges, drawn under the dots. Thin and low-contrast on purpose: forty-eight dots plus up to
   ninety-six lines is a dense panel, and a line at full contrast competes with the marks it connects —
   the edge is context for a dot, not a subject of its own. */
.lab-descent {
  fill: none;
  stroke: var(--nouma-accent-alt);
  stroke-width: 0.8;
  opacity: 0.35;
}

/* The lineage dots are clickable. The visual mark stays small — it is one of forty-eight on a narrow
   panel — while a transparent companion circle carries the hit area; see `lab.ts` for why the target
   is not simply the mark. */
.lab-dot {
  cursor: pointer;
}

.lab-dot:hover {
  opacity: 1;
  r: 4;
}

.lab-hit {
  cursor: pointer;
}

/* ----------------------------------------------------------------- the ribbon
 *
 * The last N Ticks as one strip, one segment per Tick. Same shape as murmur's `.ins-ribbon`: a flex
 * row whose children split the width, so the browser does the layout and each segment is a real
 * element that can carry a tooltip.
 *
 * `flex: 1 1 0` with `min-width` is what makes the strip readable at any length — sixty Ticks and
 * twelve are the same picture at different resolutions, rather than one being a smear.
 */
.ribbon {
  margin: 0;
}

.ribbon-strip {
  display: flex;
  gap: 1px;
  height: 22px;
  align-items: stretch;
  border-radius: 2px;
  overflow: hidden;
}

.ribbon-segment {
  flex: 1 1 0;
  min-width: 2px;
  /* A Tick with no recorded behaviour keeps the neutral track rather than a colour: `rest` and
     "no data" are not the same claim, and only one of them is a measurement. */
  background: var(--nouma-border);
  opacity: 0.85;
}

.ribbon-caption {
  margin: 6px 0 0;
  font-family: var(--nouma-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--nouma-fs-floor);
  color: var(--nouma-text-muted);
}

/* The caption is a link to the lab, so it has to look like one without shouting — the strip above it
   is the content, and a full-weight link under it would compete with the picture. */
.ribbon-link {
  color: var(--nouma-text-muted);
  text-decoration: none;
  border-block-end: 1px dotted currentColor;
}

.ribbon-link:hover {
  color: var(--nouma-accent);
}

/* -------------------------------------------------------------- economy page */

/*
 * The one screen that reads the swarm as a market.
 *
 * Its layout follows from that: a single large figure with a caption under it, because the claim is
 * "this much cancelled out before anything reached a chain" and a figure that shares a row with three
 * others makes no claim. The statistics underneath are a `dl` rather than a table because they are not
 * rows of records — they are four different measures of the same subject.
 */

/*
 * Wider than the other pages, deliberately.
 *
 * The card wall is the screen. At 900px it fits four cards in one row and leaves the right third of a
 * desktop viewport empty — which makes a panel of eight measurements look like a page that ran out of
 * content, when in fact it had more to say and nowhere to put it.
 *
 * `width: 100%` is load-bearing, not decoration. `.shell-content` is a flex column, and **on a flex item
 * `margin: 0 auto` cancels `align-items: stretch`** — the auto margins take the remaining space and the
 * item falls back to `fit-content`. So `max-width` alone was inert: this page sat at 919px inside a 1425px
 * column and the grid inside it could only ever fit three columns, while the stylesheet said four.
 */
.economy-page {
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.economy-page h2 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--nouma-accent);
}

.economy-panel {
  display: flex;
  flex-direction: column;
  gap: 24px;
  margin-top: 24px;
}

/* ---- the settlement gate -------------------------------------------------
 *
 * Printed beside the figures rather than as a footnote, because it changes what they mean. A headline
 * that says "netted" under `read-only` is a different claim from the same headline under `live`, and a
 * reader who has scrolled past a footnote has already misread the number.
 */

.economy-gate {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.economy-state {
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--nouma-border);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.economy-state[data-tone="warn"] {
  border-color: var(--nouma-tone-warn);
  color: var(--nouma-tone-warn);
}

/* Was `--nouma-accent`, which is the theme's own voice. A settlement badge saying "live" and a heading
   were therefore the same colour — see the tone vocabulary at the top of this file. */
.economy-state[data-tone="ok"] {
  border-color: var(--nouma-tone-ok);
  color: var(--nouma-tone-ok);
}

.economy-state[data-tone="alarm"] {
  border-color: var(--nouma-tone-alarm);
  color: var(--nouma-tone-alarm);
}

.economy-tick {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

/* ---- the headline -------------------------------------------------------- */

.economy-headline {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.economy-figure {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}

.economy-value {
  font-family: var(--nouma-font-display);
  /* Fluid rather than fixed: an 18-decimal total can be eleven digits before the point, and a fixed
     size that fits `1,234` would wrap `1,234,567,890`. */
  font-size: clamp(30px, 7vw, 54px);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--nouma-text);
}

.economy-unit {
  font-size: max(14px, var(--nouma-fs-floor));
  letter-spacing: 0.1em;
  color: var(--nouma-text-muted);
}

.economy-figure-label {
  margin-top: 6px;
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--nouma-accent);
}

/* ---- the card wall ------------------------------------------------------- */

/*
 * Four measurements, each as a card carrying a micro-label, a figure and a shape.
 *
 * This replaces a `dl` of four numbers. A definition list is a table, and a table is what made this
 * screen read as a report about a system rather than a view of one — it also asks the reader to hold
 * four numbers in their head to compare them, where a ring at 0.26 and a ring at 0.74 are comparable at
 * a glance. The shape is the point of the card; the figure and the label are what make it legible.
 */

.economy-wall {
  display: grid;
  /*
   * Four across, two down — which is what eight cards want.
   *
   * The width is chosen against the *count*, not guessed: `minmax(150px, 1fr)` in this same space makes
   * seven columns, which puts eight cards into a row of seven and an orphan on the second line. A grid
   * that leaves one card alone reads as a mistake, because it is one.
   */
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 12px;
}

.stat-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  background: var(--nouma-surface);
}

/* The micro-label. Monospaced and widely tracked, which is the convention every panel in this app
   uses for a name that sits above a value rather than beside it. */
.stat-label {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--nouma-text-muted);
}

.stat-body {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.stat-value {
  font-family: var(--nouma-font-display);
  font-size: 24px;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--nouma-text);
}

/* A card's qualifier: the denominator a proportion needs, or the word for what the figure means. A
   micro-label rather than a sentence — a card that grows a paragraph has stopped being a card. */
.stat-note {
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

/* Shapes are inline SVGs; they must not be squeezed by the flex row they sit in. */
.stat-body > svg,
.entrance-card > svg {
  flex: none;
}

.economy-caption {
  margin: -12px 0 0;
  font-size: max(12px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

/* ---- the spread ---------------------------------------------------------- */

.economy-distribution {
  margin: 0;
}

.economy-bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 56px;
  padding: 0 1px;
  border-bottom: 1px solid var(--nouma-border);
}

.economy-bar {
  flex: 1;
  /* The custom property is set inline per bar; the fallback keeps a bar visible if it is not. */
  height: var(--economy-bar, 20%);
  min-height: 2px;
  background: var(--nouma-accent);
  opacity: 0.35;
  border-radius: 2px 2px 0 0;
  transition: opacity var(--nouma-dur-interact) var(--nouma-ease);
}

.economy-bar:hover {
  opacity: 0.85;
}

.economy-distribution figcaption {
  margin-top: 8px;
  font-size: max(12px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

/* ---- flows --------------------------------------------------------------- */

.economy-section {
  margin: 0;
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--nouma-text-muted);
}

.economy-flows {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--nouma-border);
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  overflow: hidden;
}

/* Grid rather than flex so the columns line up across rows. The why-line spans the full width under it,
   which is what makes a row readable as "this fly, to this fly, this much, because of this". */
.economy-flow {
  display: grid;
  grid-template-columns: minmax(90px, 1fr) auto auto;
  grid-template-areas:
    "pair amount kind"
    "why  why    why";
  align-items: baseline;
  gap: 4px 12px;
  padding: 12px 16px;
  background: var(--nouma-surface);
  font-size: max(13px, var(--nouma-fs-floor));
}

.economy-flow-pair {
  grid-area: pair;
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--nouma-font-mono);
}

.economy-flow-arrow {
  color: var(--nouma-text-muted);
}

.economy-flow-amount {
  grid-area: amount;
  font-family: var(--nouma-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--nouma-text);
}

.economy-flow-kind {
  grid-area: kind;
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--nouma-accent);
}

.economy-flow-why {
  grid-area: why;
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
  /* Truncated with the title attribute carrying the rest: the weights are diagnostic, not the point of
     the row, and letting them wrap would make every row three lines tall. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.economy-more {
  margin: 8px 0 0;
  font-size: max(12px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

/* ---- withheld ------------------------------------------------------------ */

.economy-withheld {
  padding: 16px;
  border: 1px solid var(--nouma-border);
  border-inline-start: 3px solid var(--nouma-burn);
  border-radius: 4px;
}

.economy-withheld-list {
  margin: 8px 0 0;
  padding-inline-start: 18px;
  font-size: max(12px, var(--nouma-fs-floor));
  line-height: 1.6;
  color: var(--nouma-text-muted);
}

.economy-withheld-list code {
  font-family: var(--nouma-font-mono);
  color: var(--nouma-text);
}

/* ---------------------------------------------------------------- map page */

/*
 * The parchment map.
 *
 * The colour and the geometry both live in the SVG (see `pages/map.ts`), because they are properties of
 * the *picture* — a house has a position and a ring has an outline whether or not any stylesheet
 * loaded. What is here is the frame around it: the page margins and the caption, which are properties
 * of the document.
 */

.map-page {
  /*
   * The map is a fixed 900×560 SVG, so the page only has to be wide enough that the frame is not the
   * widest thing on it. The extra width is for the caption line, which was wrapping mid-phrase.
   *
   * `width: 100%` because `margin: 0 auto` on a flex item defeats `align-items: stretch`; see
   * `.economy-page` for the full explanation.
   */
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.map-page h2 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--nouma-accent);
}

.map-figure {
  margin: 24px 0 0;
}

/*
 * A parchment wash under the drawing.
 *
 * The SVG paints its own ground; this only softens the edge between the drawing and the page, so the
 * map reads as a sheet laid on the surface rather than as a rectangle floating in it.
 */
.map-svg {
  border-radius: 4px;
  box-shadow: 0 0 0 1px var(--nouma-border);
}

.map-caption {
  margin-top: 12px;
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.06em;
  color: var(--nouma-text-muted);
}

/* The ring is the token; a hover says it is selectable without moving anything. */
.map-svg [data-fly] circle {
  transition: stroke-width var(--nouma-dur-interact) var(--nouma-ease);
}

.map-svg [data-fly]:hover circle {
  stroke-width: 2.5;
}

/* ------------------------------------------------------------ ledger + entrances */

/*
 * Two screens that are mostly lists, so the styling is mostly about making a list readable: a nav that
 * stays out of the way of the content it selects, and a definition list whose terms and values sit on
 * one line rather than stacking.
 */

.ledger-page,
.entrances-page {
  /*
   * As wide as the volume nav needs to sit on one line. The nav is nine entries with a symbol each, and
   * at 960px it wrapped onto two rows with the last three stranded on the second — which read as a
   * second, smaller group rather than as the rest of the list.
   *
   * `width: 100%` because `margin: 0 auto` on a flex item defeats `align-items: stretch`; see
   * `.economy-page` for the full explanation.
   */
  width: 100%;
  max-width: 1240px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

/* ------------------------------------------------------------ the agent screen */

/*
 * A page that is mostly a transcript, so it is measured in characters rather than in columns: the
 * bodies are JSON and shell, and a line of either wrapped at 900px is harder to read than one that
 * runs to its own length.
 */
.agent-page {
  width: 100%;
  max-width: 980px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.agent-header {
  margin-bottom: 24px;
}

.agent-title {
  margin: 0 0 10px;
  font-family: var(--nouma-font-display);
  font-size: 30px;
  letter-spacing: 0.04em;
  color: var(--nouma-accent);
}

.agent-lede {
  margin: 0;
  max-width: 68ch;
  color: var(--nouma-text-muted);
  line-height: 1.7;
}

.agent-controls {
  display: flex;
  gap: 10px;
  margin-bottom: 18px;
}

.agent-flow {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin: 0 0 36px;
  padding: 0;
  list-style: none;
}

.agent-step {
  padding: 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  background: var(--nouma-surface);
  /* The left edge states the step's outcome, so a reader scanning the column sees the shape of the
     exchange before reading any of it. */
  border-inline-start: 3px solid var(--nouma-border);
  transition: border-color var(--nouma-dur-interact) var(--nouma-ease);
}

.agent-step[data-state="ok"] {
  border-inline-start-color: var(--nouma-tone-ok);
}

.agent-step[data-state="failed"] {
  border-inline-start-color: var(--nouma-tone-alarm);
}

.agent-step[data-state="running"] {
  border-inline-start-color: var(--nouma-tone-warn);
}

.agent-step-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}

.agent-step-no {
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

.agent-step-title {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 17px;
  letter-spacing: 0.04em;
  color: var(--nouma-text);
}

.agent-step-meta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-inline-start: auto;
  font-family: var(--nouma-font-mono);
  font-size: max(11px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

.agent-step-note {
  margin: 8px 0 12px;
  font-size: max(13px, var(--nouma-fs-floor));
  line-height: 1.65;
  color: var(--nouma-text-muted);
}

.agent-run {
  padding: 8px 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  background: transparent;
  color: var(--nouma-text);
  font: inherit;
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color var(--nouma-dur-interact) var(--nouma-ease);
}

.agent-run:hover:not(:disabled) {
  border-color: var(--nouma-accent);
}

.agent-run:disabled {
  cursor: progress;
  color: var(--nouma-text-muted);
}

.agent-run-primary {
  border-color: var(--nouma-accent);
  color: var(--nouma-accent);
}

/* The code ground is the page's, not the card's: an inset block on the same surface as the step
   would read as a second card. `--nouma-bg` was never a token, so this declaration was invalid and
   the block had no ground at all — the name that exists is `--nouma-background`. */
.agent-step-body {
  margin: 14px 0 0;
  padding: 12px 16px;
  border-radius: 4px;
  background: var(--nouma-background);
  font-family: var(--nouma-font-mono);
  font-size: max(12px, var(--nouma-fs-floor));
  line-height: 1.6;
  color: var(--nouma-text);
  /* A response body is a wall of JSON. Wrapping it is the difference between reading it and scrolling
     sideways through it. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 320px;
  overflow-y: auto;
}

.agent-code {
  padding-top: 24px;
  border-top: 1px solid var(--nouma-border);
}

.agent-code-title {
  margin: 0 0 8px;
  font-family: var(--nouma-font-display);
  font-size: 20px;
  letter-spacing: 0.04em;
  color: var(--nouma-accent);
}

.agent-code-note {
  margin: 0 0 14px;
  font-size: max(13px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

.agent-tabs {
  display: flex;
  gap: 2px;
  margin-bottom: 0;
}

.agent-tab {
  padding: 8px 16px;
  border: 1px solid var(--nouma-border);
  border-bottom: none;
  border-radius: 4px 4px 0 0;
  background: transparent;
  color: var(--nouma-text-muted);
  font: inherit;
  font-family: var(--nouma-font-mono);
  font-size: max(12px, var(--nouma-fs-floor));
  cursor: pointer;
}

.agent-tab.is-active {
  background: var(--nouma-surface);
  color: var(--nouma-text);
  border-color: var(--nouma-border);
}

.agent-code-body {
  border: 1px solid var(--nouma-border);
  border-radius: 0 4px 4px 4px;
  background: var(--nouma-surface);
  overflow: hidden;
}

.agent-snippet {
  margin: 0;
  padding: 16px;
  font-family: var(--nouma-font-mono);
  font-size: max(12.5px, var(--nouma-fs-floor));
  line-height: 1.75;
  color: var(--nouma-text);
  overflow-x: auto;
  /* Focusable so the fallback for a blocked clipboard — selecting the text — is reachable by Tab. */
  outline-offset: -3px;
}

.agent-copy {
  margin-top: 12px;
  padding: 8px 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  background: transparent;
  color: var(--nouma-text-muted);
  font: inherit;
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
}

.agent-copy:hover {
  border-color: var(--nouma-accent);
  color: var(--nouma-text);
}

@media (max-width: 900px) {
  .agent-page {
    padding: 20px 16px 48px;
  }

  .agent-step-meta {
    margin-inline-start: 0;
  }

  .agent-snippet {
    font-size: max(11.5px, var(--nouma-fs-floor));
  }
}

/* ------------------------------------------------------------ the fly's mark */

/*
 * The mark is the identity, so it is the one thing on this drawer that is allowed to move.
 *
 * A fly's satellites orbit it very slowly — 44 seconds for a full turn, slow enough that it reads as
 * "this is alive" rather than as a loading spinner, and slow enough that it never competes with the
 * numbers underneath. The turned ring is decorative only: the *positions* the glyph encodes are in the
 * static geometry, and a rotation preserves those relationships, so nothing is misreported by animating
 * it. That is the condition for animating any data mark here at all.
 */
.fly-detail-mark {
  display: flex;
  justify-content: center;
  margin: 4px 0 14px;
  min-height: 46px;
}

.fly-detail-mark svg {
  overflow: visible;
}

@media (prefers-reduced-motion: no-preference) {
  .fly-detail-mark svg {
    animation: nouma-orbit 44s linear infinite;
  }
}

@keyframes nouma-orbit {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

/*
 * The status light breathes, at roughly one breath per six seconds.
 *
 * This is the "the system is running" signal that the reference dashboards carry with a pulsing dot. It
 * is deliberately confined to the status light and to `ok`: a breathing *alarm* would read as a fault
 * that is still happening, and a breathing number would make the reader think the value was changing.
 */
@media (prefers-reduced-motion: no-preference) {
  .shell-status[data-tone="ok"] svg {
    animation: nouma-breath 6s ease-in-out infinite;
  }
}

@keyframes nouma-breath {
  0%,
  100% {
    opacity: 0.55;
  }
  50% {
    opacity: 1;
  }
}

.ledger-page h2,
.entrances-page h2 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--nouma-accent);
}

/* ---- the volume nav ---- */

.ledger-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  margin: 24px 0 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--nouma-border);
}

.ledger-volume {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border-radius: 2px;
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--nouma-text-muted);
  transition: color var(--nouma-dur-interact) var(--nouma-ease);
}

/* The volume's symbol dims and lifts with its label, so an entry reads as one control rather than as a
   glyph that happens to sit next to a word.
   
   At 0.55 it did not read as dimmed, it read as *absent* — nine tiny pale marks on parchment at 12px,
   which is the size at which this vocabulary stops being recognisable at all. The resting state is now
   high enough to identify the shape without hovering, and the hover raises it to full rather than
   bringing it back from nothing. */
.ledger-volume svg {
  flex: none;
  opacity: 0.9;
  transition: opacity var(--nouma-dur-interact) var(--nouma-ease);
}

.ledger-volume:hover svg,
.ledger-volume[aria-current="page"] svg {
  opacity: 1;
}

.ledger-volume:hover {
  color: var(--nouma-text);
}

/* The selected volume is the screen's subject; the others are ways to leave it. */
.ledger-volume[aria-current="page"] {
  color: var(--nouma-accent);
  background: var(--nouma-surface);
}

.ledger-title h3 {
  margin: 0;
  font-family: var(--nouma-font-display);
  font-size: 18px;
  letter-spacing: 0.06em;
  color: var(--nouma-text);
}

/*
 * `.ledger-blurb` used to sit here — the intro paragraph of a volume. The ledger page writes
 * `.ledger-note` for that sentence now (see `app/pages/ledger.ts`), so the rule dressed nothing.
 */
/*
 * Terms and values on one line each.
 *
 * A `dl` grid rather than a table: these are not rows of records, they are named readings, and the
 * two-column shape is what makes "burn rate — 2.50%" read as one statement.
 */
/*
 * One reading per row: a name, a state dot, and the value.
 *
 * `gap: 1px` over a border-coloured background is how the rules between cells are drawn — the grid shows
 * the background through the gaps, which keeps the lines aligned without a border on every cell and the
 * doubled edges that come with it.
 *
 * Two nested grids rather than one, because the list has to be able to become two *columns* of pairs on a
 * wide screen and `column-count` cannot do that without splitting a pair across the break. The outer grid
 * holds whole pairs; the inner one holds a pair's two halves.
 */
.ledger-entries {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  margin: 0;
  background: var(--nouma-border);
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  overflow: hidden;
}

/* Two columns of pairs once there is room. Below this the pairs would be narrower than the values they
   hold, and a transaction hash would wrap three times to fit. */
@media (min-width: 1100px) {
  .ledger-entries {
    grid-template-columns: 1fr 1fr;
  }
}

.ledger-entry {
  display: grid;
  grid-template-columns: minmax(140px, auto) 1fr;
  gap: 1px;
  min-width: 0;
}

/*
 * A pair with no partner takes the whole row.
 *
 * The 1px gaps over a border-coloured background *are* the rules between cells, which means an empty
 * grid cell shows that background — an odd number of pairs left a brown rectangle in the bottom-right
 * corner, reading as a missing entry rather than as empty space. A lone last pair spanning both columns
 * removes the gap and the ambiguity with it.
 */
@media (min-width: 1100px) {
  .ledger-entry:last-child:nth-child(odd) {
    grid-column: 1 / -1;
  }
}

.ledger-entries dt {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  background: var(--nouma-surface);
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--nouma-text-muted);
}

.ledger-entries dt svg {
  flex: none;
}

.ledger-entries dd {
  margin: 0;
  padding: 12px 16px;
  background: var(--nouma-surface);
  font-family: var(--nouma-font-mono);
  /* A transaction hash must not widen the grid; it wraps inside its own column instead. */
  overflow-wrap: anywhere;
  color: var(--nouma-text);
}

.ledger-note,
.ledger-loading {
  margin: 12px 0 0;
  font-size: max(12px, var(--nouma-fs-floor));
  color: var(--nouma-text-muted);
}

/* The reason a read failed. Monospace because it is usually a status code or a transport message,
   and it is the one line a reader would quote when reporting the failure. */
.ledger-error-detail {
  margin: 0;
  font-family: var(--nouma-font-mono);
  font-size: max(12px, var(--nouma-fs-floor));
  line-height: 1.6;
  color: var(--nouma-text-muted);
  overflow-wrap: anywhere;
}

.ledger-retry {
  margin-top: 12px;
  padding: 8px 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 2px;
  background: var(--nouma-surface);
  color: var(--nouma-text);
  font: inherit;
  font-size: max(12px, var(--nouma-fs-floor));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color var(--nouma-dur-interact) var(--nouma-ease);
}

.ledger-retry:hover:not(:disabled) {
  border-color: var(--nouma-accent);
}

/* The pressed state is also the in-flight state — the label reads "Retrying…" — so the button has to
   look inert rather than merely un-clickable, or the second click looks like it was ignored. */
.ledger-retry:disabled {
  cursor: progress;
  color: var(--nouma-text-muted);
}

/* ---- the entrance cards ---- */

.entrances-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 12px;
  margin-top: 24px;
}

.entrance-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 16px;
  border: 1px solid var(--nouma-border);
  border-radius: 4px;
  background: var(--nouma-surface);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--nouma-dur-interact) var(--nouma-ease);
}

/* Only a card with somewhere to go reacts to a pointer. */
a.entrance-card:hover {
  border-color: var(--nouma-accent);
}

.entrance-title {
  font-size: max(11px, var(--nouma-fs-floor));
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--nouma-accent);
}

.entrance-value {
  font-family: var(--nouma-font-display);
  font-size: 26px;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--nouma-text);
}

/*
 * A card with no reading is dimmed, not hidden.
 *
 * `data-tone="dim"` marks a figure that means "not built" rather than "measured zero" — the neural
 * proofs card is the case that matters. Dimming says "there is nothing here yet" without pretending
 * there is no such thing.
 */
.entrance-card[data-tone="dim"] .entrance-value {
  color: var(--nouma-tone-dim);
}

.entrance-detail {
  font-size: max(12px, var(--nouma-fs-floor));
  line-height: 1.5;
  color: var(--nouma-text-muted);
}

.entrance-blurb {
  margin-top: 2px;
  padding-top: 8px;
  border-top: 1px solid var(--nouma-border);
  font-size: max(11px, var(--nouma-fs-floor));
  line-height: 1.5;
  color: var(--nouma-text-muted);
}

/* ---------------------------------------------------------------- 640px breakpoint */

/*
 * The 900px breakpoint folds the header; this one folds the content.
 *
 * They answer different questions, which is why they are not one rule. At 900px there is no longer
 * room for twelve nav entries beside a brand, but a two-column card wall still reads well. At 640px
 * each column of that wall is narrower than the figure it has to hold, so the wall stops being a
 * comparison and becomes two skinny lists. A page that is mostly a column of full-width cards is
 * both easier to scan and — the reason this is a hard requirement rather than a preference — cannot
 * push the document wider than the phone.
 *
 * The page margin drops to 16px here because 24px on a 390px screen spends an eighth of the width on
 * nothing. Everything else is a step down from the desktop value, never a new value: headings fall
 * from 22 to 19, the agent title from 30 to 24, and the fluid headline is capped lower so an
 * eighteen-decimal total cannot set its own page width.
 */
@media (max-width: 640px) {
  .home-page,
  .docs-page,
  .contract-page,
  .canary-page,
  .chronicle-page,
  .lab-page,
  .economy-page,
  .map-page,
  .ledger-page,
  .entrances-page,
  .agent-page {
    padding: 24px 16px 40px;
  }

  .page-loading {
    padding: 40px 16px;
  }

  .home-header h1 {
    font-size: 32px;
    letter-spacing: 0.14em;
  }

  .docs-page h2,
  .contract-page h2,
  .canary-page h2,
  .chronicle-page h2,
  .lab-header h2,
  .economy-page h2,
  .map-page h2,
  .ledger-page h2,
  .entrances-page h2 {
    font-size: 19px;
  }

  .agent-title {
    font-size: 24px;
  }

  .agent-code-title {
    font-size: 18px;
  }

  .economy-value {
    font-size: clamp(26px, 7vw, 36px);
  }

  .stat-value {
    font-size: 22px;
  }

  .entrance-value {
    font-size: 22px;
  }

  /*
   * Single column, explicitly rather than by shrinking `minmax`.
   *
   * `repeat(auto-fit, minmax(280px, 1fr))` happens to produce one column at 390px and two at 640px,
   * so the wall changes shape at a width nobody chose. Pinning it here makes the breakpoint the
   * decision, and makes the card's width the page's width at every phone size in between.
   */
  .economy-wall,
  .entrances-grid,
  .swarm-summary,
  .connectome-mini,
  .sections-nav {
    grid-template-columns: 1fr;
  }

  /*
   * The ledger pair is deliberately *not* stacked at this breakpoint.
   *
   * It looks like a two-column grid and is one, but the columns are a name and its value, not two
   * cards. Stacking them measured 2071px of page at 640 against 1474px with the pair intact — a 40%
   * growth to solve a problem that does not exist there, because the value column already wraps a
   * transaction hash. At 390px stacking is 96px shorter, which is the one width where it helps; a
   * second breakpoint for 96px would buy less than the rule costs in surprise, so the pair stays.
   */

  /*
   * The flow row keeps its three columns, and the amount is allowed to break inside a figure.
   *
   * An amount is one unbreakable run of digits, so it sets the auto column's minimum to its own
   * width; an eighteen-decimal obligation can therefore widen the row past the page. `anywhere`
   * lets the column report a smaller minimum, which is the difference between a wrapped number and a
   * document-wide scrollbar. The row is not stacked because a pair, an amount and a kind are one
   * reading — splitting them into three lines triples the height of the only list on the screen that
   * is meant to be scanned.
   */
  .economy-flow-amount {
    overflow-wrap: anywhere;
  }

  /* The controls are three separate widgets with a 14px gap; without wrapping the poke button is
     pushed past the right edge of the stage rather than onto a second line. */
  .swarm-controls,
  .swarm-readout {
    flex-wrap: wrap;
  }

  .swarm-readout {
    gap: 12px 20px;
  }

  /*
   * A code block is the one thing allowed to be wider than the page — but only inside itself.
   * `overflow-x: auto` on the `<pre>` is already in the base rule; the explicit `max-width` is what
   * stops the block's intrinsic width from becoming the grid column's minimum and widening the page
   * around it, which is the failure mode a scroll container does not fix on its own.
   */
  .agent-snippet,
  .agent-step-body {
    max-width: 100%;
  }

  .docs-list code {
    overflow-wrap: anywhere;
  }
}

/* ------------------------------------------------------------- reduced motion */

/* Respect a reduced-motion preference: the particles are the point, but a pulsing glow is not worth
   making someone ill over. */
@media (prefers-reduced-motion: reduce) {
  .temperature-band {
    transition: none;
  }
}
