/* gunnar console — design system
 *
 * The rule that shapes everything here: COLOUR MEANS STATUS. Nothing else on
 * the surface is chromatic. The dark theme's surfaces are a warm near-neutral,
 * cooled iron, never the blue-grey every forge UI reaches for; the light
 * theme's are NEUTRAL, because they are the login page's ground and that page
 * is the one the picture sets the colour of (see the surfaces block below).
 * The ink is warm in both. The only saturated pixels an operator ever sees
 * are pass / warn / bad / live. A green dot in this console is never
 * decoration, so it never has to be read twice.
 *
 * That leaves primary actions and the current selection without a hue. They
 * are expressed the way a terminal expresses them: contrast, surface and
 * weight. An INVERTED button is the primary — ink-on-surface turned over, so
 * it is dark on paper and bone on iron; a raised surface with stronger ink is
 * the current nav item.
 *
 * Light by default, dark by choice. The console used to be dark only, on the
 * argument that it sits next to a terminal; the owner asked for the opposite
 * default, so white is what a first visit paints and the dark design is kept
 * whole as a selectable theme rather than deleted. Two palettes, one design:
 * every rule below reads tokens, so a theme is a block of values and never a
 * second set of rules.
 *
 * The switch is `data-theme="dark"` on <html>, set by the header's toggle and
 * remembered in localStorage. `prefers-color-scheme` is deliberately NOT
 * consulted: a default the system can silently overturn is not a default, and
 * white was asked for by name.
 *
 * The status hues are the same in both themes; their LIGHTNESS is not. 0.79
 * green reads on near-black and washes out on white, so the light palette
 * carries its own values — same hue, same meaning, enough contrast to be read
 * on paper.
 */

:root {
  color-scheme: light;

  /* ── Surfaces: ONE white with the front door, and it is the front door's ──
   *
   * The login page at `/login` and this console are two different documents —
   * a server-rendered page with its own inline stylesheet, and the wasm SPA —
   * and their grounds had drifted apart: #F8F6F4 here against #F4F4F4 there.
   * Four steps lighter and, worse, four steps WARMER, because this ramp was
   * warm paper (hue 75, chroma 0.004) and that one is neutral. A visitor who
   * logs in, or who opens a public repository from the catalogue, crosses from
   * one document to the other and the page changes colour underneath them.
   *
   * So the ground below is `#f4f4f4` WRITTEN OUT, not a re-derivation of it.
   * That number is measured — it is the mean of the backdrop webp's right edge,
   * read down the column where the picture meets the words, and the comment in
   * `login.html` explains why the corner was the wrong place to sample it. A
   * value like that must be copied, never matched by eye, or the two documents
   * drift again the first time either is touched.
   *
   * The SURFACE is that same `#f4f4f4`, written out a second time rather than
   * pointed at `--bg`, because it is the same measured value and not a choice
   * that happens to agree with it today. It used to be pure white, `oklch(1 0
   * 0)`, laid on every panel, metric cell and code block — and that was a
   * SECOND white. The login page declares a `--surface` of white too, but it
   * never paints one: all a visitor sees there is `#f4f4f4`. A console that
   * painted cards whiter than its ground had two whites where the door has one,
   * and the owner asked, twice, for one.
   *
   * So a panel is no longer told apart from the page by being whiter. It never
   * relied on that alone: `.panel`, `.metrics` and `.readme pre` each carry a
   * `--line` border, and that edge is now the whole of the separation — the way
   * the login page's catalogue sits on its ground with no fill of its own.
   *
   * What DID rely on the fill was everything laid on a panel: a table row's
   * hover, a badge, a diff's file header, a hunk heading, the skeleton shimmer.
   * They were `raised` and `hover` BELOW WHITE, 0.046 and 0.076 in OKLab
   * lightness; left where they were, on a `#f4f4f4` panel they would be 0.013
   * and 0.043 — a row hover nobody could see. So the two tokens move down by
   * exactly what the surface moved (1 → 0.9672), and every step they make
   * against the panel they sit on is the step it always was. `raised` lands on
   * #e5e5e5, which is the login page's own control fill (`--raised: #e5e5e3`)
   * less its trace of warmth: a button here and a button at the door are the
   * same grey.
   *
   * The sidebar keeps its place, a shade below the ground (0.023). The nav
   * link's hover is `--surface`, so it lifts to the ground's white; the current
   * item is `raised`, so it sinks below the sidebar — two directions, both
   * visible, neither a second white.
   *
   * The INK stays warm, and that is not an inconsistency: `--ink`, `--line` and
   * the status hues below are already token-for-token identical to the login
   * page's. Warm ink on a neutral ground is what that page has always been, so
   * matching the ground is the whole of what was needed to make them one. */
  --bg: #f4f4f4;
  --surface: #f4f4f4;
  --surface-raised: oklch(0.9214 0 0);
  --surface-hover: oklch(0.8914 0 0);
  --line: oklch(0.885 0.005 75);
  --line-strong: oklch(0.775 0.006 75);
  /* The sidebar is a shade off the page in DARK only. On paper it is the
   * ground itself: a sidebar greyer than the page was a second white by
   * subtraction — the chrome read as dimmer than the content beside it, and
   * crossing from the login page or a public repository listing into the
   * console changed the colour of the left edge. One ground, everywhere. */
  --sidebar-bg: #f4f4f4;

  /* Ink — warm near-black, never pure black. */
  --ink: oklch(0.225 0.008 70);
  --ink-dim: oklch(0.435 0.008 70);
  --ink-mute: oklch(0.565 0.007 70);
  /* The primary button inverts; its hover goes one step PAST the ink, which on
   * paper means darker and on iron meant brighter. */
  --ink-extreme: oklch(0 0 0);

  /* Status — the entire colour budget. Lightness tuned for a white ground. */
  --pass: oklch(0.52 0.15 155);
  --warn: oklch(0.55 0.13 85);
  --bad: oklch(0.53 0.20 25);
  --live: oklch(0.52 0.15 240);

  /* Status washes, for row and badge backgrounds. */
  --pass-wash: oklch(0.52 0.15 155 / 0.12);
  --warn-wash: oklch(0.55 0.13 85 / 0.13);
  --bad-wash: oklch(0.53 0.20 25 / 0.11);
  --live-wash: oklch(0.52 0.15 240 / 0.11);

  /* Source highlighting — two tinted families, dark enough to read on paper. */
  --hl-string-ink: oklch(0.47 0.09 150);
  --hl-constant-ink: oklch(0.47 0.10 250);

  --radius: 8px;
  --radius-sm: 5px;

  --sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  /* gunnar's whole vocabulary is machine values: oids, SHA256 fingerprints,
   * byte counts. They get a real mono face and tabular figures everywhere. */
  --mono: ui-monospace, "JetBrains Mono", SFMono-Regular, Menlo, Consolas, monospace;

  --sidebar-w: 236px;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --fast: 150ms;
  --slow: 220ms;
}

/* ── The dark theme ────────────────────────────────────────────────────────
 *
 * The console's original palette, unchanged in value and no longer the
 * default: warm-tinted near-black surfaces (cooled iron, not the blue-black
 * every forge UI reaches for) and warm bone ink. Selected, never inferred —
 * see the note at the top of this file about `prefers-color-scheme`. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: oklch(0.155 0.006 70);
  --surface: oklch(0.198 0.007 70);
  --surface-raised: oklch(0.238 0.008 70);
  --surface-hover: oklch(0.268 0.008 70);
  --line: oklch(0.30 0.008 70);
  --line-strong: oklch(0.40 0.009 70);
  --sidebar-bg: oklch(0.135 0.006 70);

  --ink: oklch(0.95 0.006 75);
  --ink-dim: oklch(0.775 0.007 75);
  --ink-mute: oklch(0.635 0.007 75);
  --ink-extreme: oklch(1 0 0);

  --pass: oklch(0.79 0.15 155);
  --warn: oklch(0.83 0.15 85);
  --bad: oklch(0.71 0.19 25);
  --live: oklch(0.80 0.11 240);

  --pass-wash: oklch(0.79 0.15 155 / 0.13);
  --warn-wash: oklch(0.83 0.15 85 / 0.13);
  --bad-wash: oklch(0.71 0.19 25 / 0.14);
  --live-wash: oklch(0.80 0.11 240 / 0.13);

  --hl-string-ink: oklch(0.82 0.055 150);
  --hl-constant-ink: oklch(0.83 0.05 250);
}

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

html,
body {
  margin: 0;
  height: 100%;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 14.5px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

:where(h1, h2, h3, h4, p, figure, ul, ol, dl, dd) {
  margin: 0;
}

:where(ul, ol) {
  padding: 0;
  list-style: none;
}

a {
  color: var(--ink);
  text-decoration-color: var(--line-strong);
  text-underline-offset: 3px;
}

a:hover {
  text-decoration-color: var(--ink);
}

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

code,
kbd,
samp,
.mono {
  font-family: var(--mono);
  font-size: 0.92em;
  font-variant-numeric: tabular-nums;
}

.num {
  font-variant-numeric: tabular-nums;
}

/* ── Shell ────────────────────────────────────────────────────────────── */

.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  height: 100dvh;
}

.sidebar {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  padding: 1.25rem 0.75rem;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--line);
  overflow-y: auto;
}

/* The brand heads the sidebar: the wordmark, the badger full-width under it,
 * STICKY so the pair holds the top while the nav scrolls beneath — the brand
 * carries the sidebar's own background so scrolling links slide under it
 * rather than through it. */
.brand {
  position: sticky;
  top: -1.25rem; /* cancels the sidebar's own top padding */
  z-index: 2;
  background: var(--sidebar-bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 1.25rem 0.25rem 0.75rem;
  margin-top: -1.25rem;
  font-size: 1.05rem;
  font-weight: 640;
  letter-spacing: -0.015em;
  text-decoration: none;
  text-align: center;
}

.brand small {
  color: var(--ink-mute);
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 400;
  letter-spacing: 0;
}

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

.nav-group > h2 {
  padding: 0 0.6rem;
  margin-bottom: 0.4rem;
  color: var(--ink-mute);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.nav-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.42rem 0.6rem;
  border-radius: var(--radius-sm);
  color: var(--ink-dim);
  font-size: 0.9rem;
  text-decoration: none;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

/* The sidebar now IS the ground, so a hover can no longer lift to it — there
 * is nowhere lighter to go without minting the second white the door does not
 * have. Both states therefore sink, by the steps the panel tokens already
 * measure: hover to `raised`, and the current page one further to `hover`, so
 * "you are here" still outweighs "you are pointing at this". */
.nav-link:hover {
  background: var(--surface-raised);
  color: var(--ink);
}

/* The current page carries no hue: a raised surface, brighter ink and more
 * weight say "you are here" without spending the status budget. */
.nav-link[aria-current="page"] {
  background: var(--surface-hover);
  color: var(--ink);
  font-weight: 560;
}

.nav-link .count {
  color: var(--ink-mute);
  font-family: var(--mono);
  font-size: 0.78rem;
}

.workspace {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0 1.75rem;
  height: 52px;
  flex: none;
  border-bottom: 1px solid var(--line);
}

.topbar .spacer {
  flex: 1;
}

/* The theme switch. It sits beside the identity because both are facts about
 * THIS viewer rather than about the console, and it is quiet because changing
 * the colour of the page is never the errand anyone came here on. Fixed width
 * so the bar does not shift when the label swaps. */
.theme-toggle {
  flex: none;
  min-width: 6.2rem;
  font-variant-numeric: tabular-nums;
}

.page {
  flex: 1;
  overflow-y: auto;
  padding: 2rem 1.75rem 4rem;
}

/* Prose keeps a readable measure; tables and data below opt out. */
.page > p {
  max-width: 68ch;
  color: var(--ink-dim);
}

/* Present to a screen reader, absent to everyone else. Used by every table
 * caption in the console; without it those captions render as visible text
 * above their tables, which is how they shipped until a review caught it. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip {
  position: absolute;
  left: -9999px;
}

.skip:focus {
  left: 0.5rem;
  top: 0.5rem;
  z-index: 10;
  padding: 0.5rem 0.75rem;
  background: var(--surface-raised);
  border-radius: var(--radius-sm);
}

/* ── Page header ──────────────────────────────────────────────────────── */

.page-header {
  display: flex;
  align-items: flex-end;
  gap: 1rem;
  padding-bottom: 0.9rem;
  margin-bottom: 1.75rem;
  border-bottom: 1px solid var(--line);
}

.page-header h1 {
  font-size: 1.4rem;
  font-weight: 640;
  letter-spacing: -0.02em;
}

.page-header p {
  margin-top: 0.15rem;
  color: var(--ink-mute);
  font-size: 0.88rem;
}

.page-header .actions {
  margin-left: auto;
  display: flex;
  gap: 0.5rem;
}

/* ── Panels ───────────────────────────────────────────────────────────── */

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.panel > header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.8rem 1rem;
  border-bottom: 1px solid var(--line);
}

.panel > header h2 {
  font-size: 0.94rem;
  font-weight: 600;
}

.panel > header .actions {
  margin-left: auto;
  display: flex;
  gap: 0.4rem;
}

.panel-body {
  padding: 1rem;
}

/* Deliberately not a uniform card grid: the wide panel leads, the narrow ones
 * support it. */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 1.25rem;
  align-items: start;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

/* A panel whose feature does not exist on the server yet. Honest, not hidden:
 * the operator learns the protocol declares it and gunnar refuses it. */
.panel.disabled {
  border-style: dashed;
  background: none;
}

.panel.disabled > header h2,
.panel.disabled .panel-body {
  color: var(--ink-mute);
}

/* ── Metrics ──────────────────────────────────────────────────────────── */

.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.metric {
  padding: 0.85rem 1rem;
  background: var(--surface);
}

.metric dt {
  color: var(--ink-mute);
  font-size: 0.76rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.metric dd {
  margin-top: 0.2rem;
  font-family: var(--mono);
  font-size: 1.32rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.metric dd small {
  margin-left: 0.25rem;
  color: var(--ink-mute);
  font-size: 0.72rem;
}

/* ── Tables ───────────────────────────────────────────────────────────── */

.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}

thead th {
  padding: 0.5rem 1rem;
  color: var(--ink-mute);
  font-size: 0.73rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
}

tbody td {
  padding: 0.55rem 1rem;
  border-bottom: 1px solid var(--line);
  vertical-align: baseline;
}

tbody tr:last-child td {
  border-bottom: none;
}

/* A row header is a cell. Without this it keeps the user agent's centred bold
 * and no padding at all, sitting flush against padded cells on the same row —
 * which is what the Room table's meter names and the Machine page's mount
 * paths were doing. Weight stays heavier than a `td`: it is the row's name. */
tbody th[scope="row"] {
  padding: 0.55rem 1rem;
  border-bottom: 1px solid var(--line);
  vertical-align: baseline;
  text-align: left;
  font-weight: 600;
}

tbody tr:last-child th[scope="row"] {
  border-bottom: none;
}

tbody tr:hover td {
  background: var(--surface-raised);
}

/* Except in a diff, where the row's wash is the meaning. A cell background
 * paints over a row background whatever the specificity, so the diff tables
 * opt out of the hover entirely. */
table.hunks tbody tr:hover td,
table.source tbody tr:hover td {
  background: none;
}

td.oid,
td.num,
td.fp {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

td.num {
  text-align: right;
}

/* One action per row, at the edge, and never wrapped onto two lines. */
td.row-actions {
  width: 1%;
  text-align: right;
  white-space: nowrap;
}

/* A value the server does not populate. Rendered, not blank, so nobody reads
 * an empty cell as "never used". */
.unset {
  color: var(--ink-mute);
  font-style: italic;
}

/* ── Badges ───────────────────────────────────────────────────────────── */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: var(--surface-raised);
  color: var(--ink-dim);
  font-size: 0.76rem;
  font-weight: 550;
  white-space: nowrap;
}

.badge::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.badge.plain::before {
  display: none;
}

/* Colour never travels alone: every badge carries its word. */
.badge.pass {
  background: var(--pass-wash);
  color: var(--pass);
}

.badge.warn {
  background: var(--warn-wash);
  color: var(--warn);
}

.badge.bad {
  background: var(--bad-wash);
  color: var(--bad);
}

.badge.live {
  background: var(--live-wash);
  color: var(--live);
}

.badge.live::before {
  animation: pulse 2s var(--ease) infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

/* ── Buttons ──────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.38rem 0.8rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--ink);
  font: inherit;
  font-size: 0.87rem;
  font-weight: 550;
  cursor: pointer;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease),
    opacity var(--fast) var(--ease);
}

.btn:hover:not(:disabled) {
  background: var(--surface-hover);
  border-color: var(--ink-mute);
}

.btn:active:not(:disabled) {
  background: var(--surface);
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The primary action inverts instead of taking a hue. */
.btn.primary {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--bg);
}

.btn.primary:hover:not(:disabled) {
  background: var(--ink-extreme);
  border-color: var(--ink-extreme);
}

.btn.quiet {
  background: none;
  border-color: transparent;
  color: var(--ink-dim);
}

.btn.quiet:hover:not(:disabled) {
  background: var(--surface-raised);
  color: var(--ink);
}

/* Destructive is a status, so it may spend colour. */
.btn.danger {
  background: var(--bad-wash);
  border-color: color-mix(in oklch, var(--bad) 45%, transparent);
  color: var(--bad);
}

.btn.danger:hover:not(:disabled) {
  background: color-mix(in oklch, var(--bad) 22%, transparent);
}

.btn.busy {
  position: relative;
  color: transparent;
}

.btn.busy::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  color: var(--ink);
  animation: spin 700ms linear infinite;
}

.btn.primary.busy::after {
  color: var(--bg);
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* ── Forms ────────────────────────────────────────────────────────────── */

.field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.field label {
  color: var(--ink-dim);
  font-size: 0.82rem;
  font-weight: 550;
}

.field .hint {
  color: var(--ink-mute);
  font-size: 0.78rem;
}

input[type="text"],
input[type="search"],
input[type="password"],
select,
textarea {
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  font-size: 0.88rem;
}

input::placeholder {
  color: var(--ink-mute);
}

input:disabled,
select:disabled {
  opacity: 0.5;
}

.field.invalid input,
.field.invalid select {
  border-color: var(--bad);
}

.field .error {
  color: var(--bad);
  font-size: 0.8rem;
}

.row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}

/* ── States ───────────────────────────────────────────────────────────── */

/* Skeletons rather than a spinner in the middle of the content: the shape of
 * the answer is already known, so the page does not jump when it arrives. */
.skeleton {
  height: 0.85rem;
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    var(--surface-raised) 25%,
    var(--surface-hover) 50%,
    var(--surface-raised) 75%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s linear infinite;
}

.skeleton + .skeleton {
  margin-top: 0.55rem;
}

.skeleton.w-70 {
  width: 70%;
}

.skeleton.w-40 {
  width: 40%;
}

@keyframes shimmer {
  to {
    background-position: -400% 0;
  }
}

.empty {
  padding: 2.25rem 1rem;
  text-align: center;
  color: var(--ink-mute);
}

.empty h3 {
  margin-bottom: 0.3rem;
  color: var(--ink-dim);
  font-size: 0.95rem;
  font-weight: 600;
}

.empty p {
  max-width: 46ch;
  margin: 0 auto;
  font-size: 0.86rem;
}

/* A refusal from gunnar, shown verbatim. The console never rewords what the
 * server said; a paraphrase is a second source of truth. */
.failure {
  padding: 0.85rem 1rem;
  border: 1px solid color-mix(in oklch, var(--bad) 40%, transparent);
  border-radius: var(--radius);
  background: var(--bad-wash);
}

.failure h3 {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--bad);
  font-size: 0.88rem;
  font-weight: 600;
}

.failure .detail {
  margin-top: 0.35rem;
  font-family: var(--mono);
  font-size: 0.82rem;
  color: var(--ink-dim);
  overflow-wrap: anywhere;
}

.failure footer {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.6rem;
  color: var(--ink-mute);
  font-size: 0.78rem;
}

/* A count the console had to truncate itself, because gunnar's list RPCs
 * ignore page_size and answer with everything. */
.truncation {
  padding: 0.5rem 1rem;
  border-top: 1px solid var(--line);
  color: var(--ink-mute);
  font-size: 0.8rem;
}

/* ── Event feed ───────────────────────────────────────────────────────── */

.feed {
  display: flex;
  flex-direction: column;
}

.feed li {
  display: grid;
  grid-template-columns: 5.5rem 8.5rem minmax(0, 1fr);
  gap: 0.75rem;
  padding: 0.45rem 1rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.85rem;
}

.feed li:last-child {
  border-bottom: none;
}

.feed time {
  color: var(--ink-mute);
  font-family: var(--mono);
  font-size: 0.8rem;
}

.feed .what {
  color: var(--ink-dim);
  font-weight: 550;
}

.feed .where {
  font-family: var(--mono);
  overflow-wrap: anywhere;
}

/* A hole in the sequence. gunnar's event bus can drop under lag, and a stream
 * that reconnects starts at the live tail with no history, so the console
 * marks what it knows it missed rather than pretending the feed is complete. */
.feed li.gap {
  grid-template-columns: 1fr;
  justify-items: center;
  background: var(--warn-wash);
  color: var(--warn);
  font-size: 0.8rem;
  font-weight: 550;
}

/* ── Indexing ─────────────────────────────────────────────────────────────
 *
 * Post-ack index work: one row per repository whose pushed pack is still being
 * resolved. The panel is ABSENT when the box is idle rather than showing a zero
 * — a row that reads "0 indexing" every day is one an operator learns to skip,
 * and then misses the day it says something.
 *
 * The bar's transition is deliberately slow relative to the 2 s poll, so the
 * fill glides between samples instead of stepping; the global
 * prefers-reduced-motion rule below neutralises it for anyone who asked. */
.indexing {
  display: flex;
  flex-direction: column;
}

.indexing li {
  padding: 0.55rem 1rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.85rem;
}

.indexing li:last-child {
  border-bottom: none;
}

.indexing .row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.indexing .pct {
  margin-left: auto;
  font-family: var(--mono);
  color: var(--ink-dim);
}

.indexing .bar {
  height: 4px;
  margin: 0.45rem 0 0.35rem;
  background: var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.indexing .bar span {
  display: block;
  height: 100%;
  background: var(--live);
  transition: width 0.6s ease;
}

.indexing .meta {
  color: var(--ink-mute);
  font-family: var(--mono);
  font-size: 0.78rem;
  overflow-wrap: anywhere;
}

/* ── Motion & responsive ──────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

@media (max-width: 860px) {
  .app {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0, 1fr);
  }

  .sidebar {
    flex-direction: row;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 1rem;
    border-right: none;
    border-bottom: 1px solid var(--line);
    overflow-x: auto;
  }

  .nav-group {
    flex-direction: row;
    gap: 0.25rem;
  }

  .nav-group > h2 {
    display: none;
  }

  .nav-link {
    white-space: nowrap;
  }

  .split {
    grid-template-columns: minmax(0, 1fr);
  }

  .page {
    padding: 1.25rem 1rem 3rem;
  }
}

/* ── Browsing code ────────────────────────────────────────────────────── */

.crumbs {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  font-family: var(--mono);
  font-size: 0.86rem;
}

.crumbs .sep {
  color: var(--ink-mute);
}

.crumbs [aria-current="page"] {
  color: var(--ink-dim);
}

table.tree td.dir a::before,
table.tree td.file a::before,
table.tree td .up::before {
  display: inline-block;
  width: 1.1rem;
  color: var(--ink-mute);
  /* Shape, not colour: the status palette is spoken for. */
  content: "▸";
}

table.tree td.file a::before {
  content: "·";
}

table.tree td a {
  font-family: var(--mono);
  text-decoration: none;
}

table.tree td a:hover {
  text-decoration: underline;
}

table.tree td.dir a {
  font-weight: 550;
}

.source-wrap {
  overflow-x: auto;
  border-top: 1px solid var(--line);
}

table.source {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.6;
}

table.source td {
  padding: 0 0.75rem;
  border: none;
  vertical-align: top;
  white-space: pre;
}

table.source tr:hover td {
  background: var(--surface-raised);
}

td.ln {
  width: 1%;
  padding-right: 1rem !important;
  color: var(--ink-mute);
  text-align: right;
  user-select: none;
  font-variant-numeric: tabular-nums;
}

td.ln a {
  color: inherit;
  text-decoration: none;
}

td.ln a:hover {
  color: var(--ink);
}

td.src {
  width: 100%;
}

/* Six families, and most of them are not a hue.
 *
 * This console's rule is that colour carries status. Source text that spent
 * colour on grammar would leave a green dot on the same screen meaning less, so
 * the weight of the ink does most of the work here and only two families take a
 * tint at all. */
.hl-comment {
  color: var(--ink-mute);
  font-style: italic;
}

.hl-string {
  color: var(--hl-string-ink);
}

.hl-constant {
  color: var(--hl-constant-ink);
}

.hl-keyword {
  color: var(--ink);
  font-weight: 620;
}

.hl-entity {
  color: var(--ink);
}

.hl-punctuation {
  color: var(--ink-dim);
}

/* Broken syntax is a real fault, so it may spend a real status colour. */
.hl-invalid {
  color: var(--bad);
  text-decoration: underline wavy;
}

/* ── Diffs ────────────────────────────────────────────────────────────── */

.diff-stat {
  font-family: var(--mono);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}

.diff-stat.add {
  color: var(--pass);
}

.diff-stat.del {
  color: var(--bad);
}

.diffs {
  display: flex;
  flex-direction: column;
}

.file-diff {
  border-top: 1px solid var(--line);
}

.file-diff > header {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.6rem 1rem;
  background: var(--surface-raised);
}

.file-diff > header .path {
  font-size: 0.86rem;
  overflow-wrap: anywhere;
}

.diff-note {
  padding: 0.75rem 1rem;
  color: var(--ink-mute);
  font-size: 0.84rem;
}

table.hunks {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.55;
}

table.hunks td {
  padding: 0 0.75rem;
  border: none;
  vertical-align: top;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

table.hunks .hunk-head td {
  padding: 0.35rem 0.75rem;
  background: var(--surface-raised);
  color: var(--ink-mute);
  font-size: 0.78rem;
}

/* A washed background AND a leading marker: an added line must be readable as
 * added without relying on the colour at all. */
table.hunks tr.add {
  background: var(--pass-wash);
}

table.hunks tr.del {
  background: var(--bad-wash);
}

table.hunks tr.add .marker {
  color: var(--pass);
}

table.hunks tr.del .marker {
  color: var(--bad);
}

table.hunks .marker {
  display: inline-block;
  width: 1rem;
  color: var(--ink-mute);
  user-select: none;
}

/* ── Commit graph ──────────────────────────────────────────────────────────
 *
 * The ONE deliberate exception to "colour means status": in the graph, hue IS
 * the information — lane identity down the page. Per the console's own rule,
 * colour is still never the only signal: every row carries the words, every
 * node a tooltip, every stub its oid. Constant lightness/chroma against --bg,
 * eight hues, wrapped mod 8. */
:root {
  /* The light theme's lanes: skade's ice-blue family carried over by HUE and
   * darkened, because the dark theme's values are chosen to glow against
   * near-black and eight pale blues on white are eight invisible lanes. Same
   * order, same gold at lane 1, same wrap. */
  --lane-0: #0d7ea6;
  --lane-1: #8a6000; /* the gold one — it also glows, below */
  --lane-2: #15688f;
  --lane-3: #3f7f9e;
  --lane-4: #2c6f92;
  --lane-5: #4a7387;
  --lane-6: #0f7bad;
  --lane-7: #3f6879;
  --lane-1-glow: rgba(138, 96, 0, 0.35);
}

:root[data-theme="dark"] {
  /* skade's own hexes, verbatim from skade/.nornir/assets — the ice-blue
   * family, with skade's gold as lane 1: most walls are cold water, and every
   * eighth lane catches the light. */
  --lane-0: #6fd3f5;
  --lane-1: #ffcf7a; /* the gold one — it also glows, below */
  --lane-2: #2d9cd2;
  --lane-3: #bfe6ff;
  --lane-4: #4b9cc9;
  --lane-5: #9fc9e0;
  --lane-6: #7fd4ff;
  --lane-7: #86b3cc;
  --lane-1-glow: rgba(255, 207, 122, 0.55);
}

.graph-scroll {
  display: flex;
  align-items: flex-start;
  overflow: auto;
  max-height: 72vh;
  border-top: 1px solid var(--line);
}

.graph-gutter {
  flex: none;
  display: block;
}
.graph-gutter path {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
}
.graph-gutter path.lane-0 { stroke: var(--lane-0); }
.graph-gutter path.lane-1 { stroke: var(--lane-1); }
.graph-gutter path.lane-2 { stroke: var(--lane-2); }
.graph-gutter path.lane-3 { stroke: var(--lane-3); }
.graph-gutter path.lane-4 { stroke: var(--lane-4); }
.graph-gutter path.lane-5 { stroke: var(--lane-5); }
.graph-gutter path.lane-6 { stroke: var(--lane-6); }
.graph-gutter path.lane-7 { stroke: var(--lane-7); }
/* An edge that leaves the window: shorter, fading, and clickable — a fact
 * ("continues below"), not decoration. */
/* The gold lane shines. A real glow, kept subtle enough to read as light on
 * water rather than as a status. */
.graph-gutter path.lane-1 {
  filter: drop-shadow(0 0 3px var(--lane-1-glow));
}
.graph-gutter circle.lane-1 {
  filter: drop-shadow(0 0 4px var(--lane-1-glow));
}

.graph-gutter path.stub {
  opacity: 0.45;
  stroke-dasharray: 2 3;
  cursor: pointer;
}
.graph-gutter path.stub:hover { opacity: 1; }

/* Nodes sit on top of edges; the --bg ring keeps crossing lanes off the dot.
 * A merge is hollow — the shape says it before the colour does. */
.graph-gutter circle {
  stroke: var(--bg);
  stroke-width: 2;
  cursor: pointer;
}
.graph-gutter circle.lane-0 { fill: var(--lane-0); }
.graph-gutter circle.lane-1 { fill: var(--lane-1); }
.graph-gutter circle.lane-2 { fill: var(--lane-2); }
.graph-gutter circle.lane-3 { fill: var(--lane-3); }
.graph-gutter circle.lane-4 { fill: var(--lane-4); }
.graph-gutter circle.lane-5 { fill: var(--lane-5); }
.graph-gutter circle.lane-6 { fill: var(--lane-6); }
.graph-gutter circle.lane-7 { fill: var(--lane-7); }
.graph-gutter circle.hollow { fill: var(--bg); stroke-width: 2; }
.graph-gutter circle.hollow.lane-0 { stroke: var(--lane-0); }
.graph-gutter circle.hollow.lane-1 { stroke: var(--lane-1); }
.graph-gutter circle.hollow.lane-2 { stroke: var(--lane-2); }
.graph-gutter circle.hollow.lane-3 { stroke: var(--lane-3); }
.graph-gutter circle.hollow.lane-4 { stroke: var(--lane-4); }
.graph-gutter circle.hollow.lane-5 { stroke: var(--lane-5); }
.graph-gutter circle.hollow.lane-6 { stroke: var(--lane-6); }
.graph-gutter circle.hollow.lane-7 { stroke: var(--lane-7); }
.graph-gutter circle.hot { r: 5.5; }

.graph-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1;
  min-width: 0;
}
.graph-rows li {
  height: 28px; /* ROW_PITCH — must match graph::ROW_PITCH */
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0 0.75rem;
  white-space: nowrap;
  cursor: pointer;
  border-bottom: 1px solid transparent;
}
.graph-rows li.hot {
  background: var(--surface-hover);
}
.graph-rows li .oid {
  flex: none;
}
.graph-rows li .badge {
  flex: none;
}
.graph-subject {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.graph-author {
  flex: none;
  color: var(--ink-mute);
  margin-left: auto;
}
.graph-when {
  flex: none;
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}
.graph-foot {
  display: flex;
  align-items: center;
  gap: 1rem;
}
.graph-foot .truncation { margin: 0; }

/* The badger, per theme. He is drawn in BLACK on a light ground, so the dark
   theme cannot be the same file on a darker plate — half the animal would sink
   into the background. `gunnar_dark.webp` is the same drawing with its
   LIGHTNESS inverted and its hue kept, which is why the red eye survives where
   a plain negate would have turned it cyan. Each ground is the theme's own
   measured `--bg`: #f4f4f4 light, #0e0c09 dark.

   A background-image rather than an <img> because the console's theme is a
   `data-theme` attribute it sets itself; a <picture> element can only follow
   the operating system. The aspect ratio keeps the box the height the image
   used to give it.

   ★ **AND NO `prefers-color-scheme` ARM, for the reason at the top of this
   file: the PALETTE never follows the system, so a mark that did would be the
   one thing on the page disagreeing with the ground it sits on.** The arm that
   stood here until 2026-09-13 read `@media (prefers-color-scheme: dark) {
   :root:not([data-theme="light"]) … }`, and `data-theme="light"` is a value
   this console never writes — `apply_theme` REMOVES the attribute for light
   (see `app.rs`), because light is its absence and not a second value. So that
   guard was true in every state the console can produce, and on any machine
   whose desktop is dark it put the lightness-inverted badger, on his near-black
   plate, inside a white sidebar — on a first visit, and again after somebody
   explicitly chose Light. Two states exist here, and these two rules are them:
   attribute present, or attribute absent. */
.brand-mark {
  width: 100%;
  aspect-ratio: 1391 / 768;
  border-radius: 8px;
  display: block;
  background: url("/gunnar_light.webp") center / contain no-repeat;
}
:root[data-theme="dark"] .brand-mark {
  background-image: url("/gunnar_dark.webp");
}

/* ── The pre-receive gate ─────────────────────────────────────────────── */

/* The add-rule form: the pattern leads and takes the slack, the switches sit
 * beside it, the button lands on the same baseline as the inputs rather than
 * centred against a two-line field. */
.policy-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem 1.1rem;
  margin-top: 0.85rem;
}

.policy-form .field {
  flex: 1 1 17rem;
}

.switches {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  padding-bottom: 0.35rem;
}

/* Phrased as what the rule FORBIDS, so the form reads the same way round as
 * the rows under it. `allow_force_push = false` is never on screen. */
.switch {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--ink-dim);
  font-size: 0.84rem;
  font-weight: 550;
  cursor: pointer;
}

.switch input[type="checkbox"] {
  width: 0.95rem;
  height: 0.95rem;
  accent-color: var(--pass);
  cursor: pointer;
}

/* What one rule forbids, one badge per clause. A rule that forbids nothing
 * gets a single warn badge saying so — never an empty cell, which reads as
 * "loading" or "unknown" and is neither. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.pattern-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pattern-list li {
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  font-family: var(--mono);
  font-size: 0.8rem;
}

.gate-list {
  margin: 0;
  padding: 0 1rem 1rem;
  list-style: none;
}

.gate-list li + li {
  margin-top: 0.7rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--line);
}

.gate-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.gate-head code {
  font-family: var(--mono);
  font-size: 0.84rem;
}

.gate-list p {
  margin: 0.15rem 0 0;
  color: var(--ink-mute);
  font-size: 0.82rem;
}

/* ── The Room table: a wall is drawn, a reading is not ─────────────────────
 *
 * `Usage.Read` reports seven caps and only three of them are a door. The bar
 * is the difference, and it appears under a meter's name ONLY where gunnar
 * said it refuses (`pages::usage::Shape::Wall`) — never under a measured cap,
 * because a bar IS a ceiling and drawing one where nothing stops is exactly
 * the promise the product does not keep.
 *
 * The right-hand rule is the ceiling itself, drawn in the ink the refusals use
 * elsewhere: the track's end is a place the tenant arrives at, not the edge of
 * a box. `.indexing .bar` above is a PROGRESS bar and its track simply runs
 * out; the two look deliberately different because they mean different things.
 */
.room th[scope="row"] .wall {
  height: 4px;
  margin: 0.35rem 0 0.1rem;
  max-width: 12rem;
  background: var(--line);
  border-radius: var(--radius-sm);
  border-right: 2px solid var(--ink-mute);
  overflow: hidden;
}

.room th[scope="row"] .wall span {
  display: block;
  height: 100%;
  background: var(--ink-dim);
}

/* What a cap on the meter DOES, in monetize's own sentence. Quiet: it is a
 * property of the row, not a number in it, and it must not compete with the
 * figures it explains. */
.room td.does {
  color: var(--ink-mute);
  font-size: 0.82rem;
  max-width: 22rem;
}

/* ── Activity: thirty days of what MOVED ──────────────────────────────────
 *
 * Every meter in the Room table above is a STOCK meter and reads the same for
 * a namespace pushed to this morning and one last touched in March. This is
 * the other kind, and it is drawn as columns because the question is a shape —
 * is this rising, has it stopped, when did it stop — and not a number.
 *
 * The bars are ACHROMATIC, and that is the console's rule and not a taste:
 * colour means status here, so two ink weights carry "in" and "out" and the
 * only saturated pixel in the whole panel is the dot over a day transfers were
 * REFUSED on. A day worth looking at is the only day that is coloured.
 *
 * A day gunnar sent no row for gets no `span` at all, so it is empty ground
 * rather than a hairline: `min-height` below would otherwise draw a
 * zero-height bar, and a bar of height zero reads as a measurement that was
 * taken and came back empty. It was not taken. See `activity::bars`.
 */
.flow {
  padding: 0 1rem;
}

.flow h3 {
  margin: 0.9rem 0 0.4rem;
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

.flow .days {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 56px;
  margin: 0;
  padding: 0;
  list-style: none;
  border-bottom: 1px solid var(--line);
}

.flow .days li {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
  gap: 1px;
}

/* A row that exists but moved no bytes is a hairline, not nothing: something
 * happened that day and the panel says so. Only a day with NO row is blank. */
.flow .days li span {
  display: block;
  flex: 1 1 0;
  min-height: 1px;
  border-radius: 1px 1px 0 0;
}

.flow .days li .a { background: var(--ink-dim); }
.flow .days li .b { background: var(--ink-mute); }

.flow .days li.refused::after {
  content: "";
  position: absolute;
  top: -7px;
  left: 50%;
  width: 4px;
  height: 4px;
  margin-left: -2px;
  border-radius: 50%;
  background: var(--bad);
}

.flow .axis {
  display: flex;
  justify-content: space-between;
  margin: 0.3rem 0 0.2rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--ink-mute);
}

.flow + .flow { padding-top: 0.2rem; }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  margin: 0.2rem 0 0.5rem;
  font-size: 0.78rem;
  color: var(--ink-mute);
}

.legend i {
  display: inline-block;
  width: 10px;
  height: 4px;
  margin-right: 0.35rem;
  border-radius: 1px;
  vertical-align: middle;
}

.legend i.a { background: var(--ink-dim); }
.legend i.b { background: var(--ink-mute); }
.legend i.mark {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--bad);
}

/* The clone line under a repository's header: one pasteable URL. */
.clone-line { margin: -0.4rem 0 1rem; font-size: 0.86rem; color: var(--ink-mute); }
.clone-line code { user-select: all; }

/* The one line under a page title that depends on who is looking. */
.page-lead { margin: -0.4rem 0 1rem; color: var(--ink-mute); font-size: 0.88rem; }

/* ── The README, as the server typeset it ──────────────────────────────────
 *
 * `.readme-page` holds an inline SVG the BFF drew. It arrives with
 * `width="100%"` and a `viewBox` and no height, so `height: auto` is what lets
 * the browser take the aspect ratio from the viewBox — the rule that makes it
 * shrink on a phone rather than overflow. The `overflow-x: auto` is the belt to
 * that brace: a page that is somehow still too wide scrolls inside its own box
 * instead of widening the console.
 *
 * The typeset page is paper, and the question is WHICH paper. docbook's
 * column template sets `page(fill: white)`, so the SVG arrives with its own
 * pure-white rectangle under the glyphs — and a CSS background behind it would
 * change nothing anybody sees. In the dark theme that white is kept on purpose:
 * a PDF grade is paper, paper does not follow a console's palette, and a white
 * sheet in a dark panel is what a document looks like there.
 *
 * In the light theme it is the console's own `#f4f4f4`, because the owner
 * asked for one white on this site and a white sheet on a `#f4f4f4` panel is
 * the second one. The sheet is laid over `#f4f4f4` and MULTIPLIED onto it,
 * rather than having its rectangle found and repainted: multiply by white is
 * identity, so the page's white lands on exactly 244,244,244; black ink stays
 * black; and the template's pale code-block fill darkens by the same ratio, so
 * the box keeps the step it had against its sheet. `isolation` makes the sheet
 * its own group, so the blend reads this box's paper and never the panel's.
 * Nothing here depends on how typst writes the rectangle.
 *
 * It keeps a border so it reads as a document sitting in the panel: against
 * the dark theme that border is what keeps it from looking like a panel that
 * forgot the theme, and against the light one — where sheet and panel are now
 * the same white — it is the only edge the sheet has. */
.readme-page {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: #f4f4f4;
  isolation: isolate;
}

.readme-page svg {
  display: block;
  width: 100%;
  height: auto;
  mix-blend-mode: multiply;
}

:root[data-theme="dark"] .readme-page {
  background: #fff;
}

/* The typeset page carries a selectable TEXT LAYER (docbook 0.1.2): one
 * invisible `<text>` per shaped run, over the glyph paths, so the page can be
 * selected, copied and found with the browser's search. The SVG brings its own
 * `<style>` saying so — the rules travel with the page rather than living here —
 * and this is the belt to that brace: an ancestor of the panel that turned
 * selection off would otherwise win by specificity, and the failure mode is a
 * page that looks right and cannot be selected, which nobody would think to
 * test. `text` and not `*`, so the glyph paths stay unselectable and a drag does
 * not produce a selection of nothing. */
.readme-page svg .text-layer text {
  -webkit-user-select: text;
  user-select: text;
}

/* The default grade: sanitised HTML from the server's typst HTML target — real
 * elements with real text in them, which is why it leads and the picture is a
 * toggle.
 *
 * typst's HTML export reserves `<h1>` for the document title, so a README's own
 * `#` heading arrives as `<h2>` and everything shifts down one. The sizes below
 * are set against THAT, not against markdown's levels: `h2` is the document's
 * title here and is sized like one. */
.readme {
  max-width: 46rem;
}

.readme h1,
.readme h2,
.readme h3 {
  margin: 1.2em 0 0.5em;
  line-height: 1.25;
}

.readme h1 { font-size: 1.5rem; }
.readme h2 { font-size: 1.42rem; }
.readme h3 { font-size: 1.14rem; }
.readme h4 { font-size: 1rem; }
.readme p, .readme ul, .readme ol, .readme blockquote { margin: 0.7em 0; }
/* The console's reset takes `list-style` off every `ul`/`ol` (nav lists, badge
 * rows, the sidebar — none of which want a bullet). A README's lists DO: an
 * unmarked list of steps is a paragraph with odd line breaks. The markers come
 * back here, scoped to the one place the document is somebody else's. */
.readme ul { list-style: disc; padding-left: 1.4em; }
.readme ol { list-style: decimal; padding-left: 1.4em; }
.readme code { font-family: var(--mono); font-size: 0.9em; }
.readme pre {
  overflow-x: auto;
  padding: 0.8rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.readme blockquote {
  padding-left: 0.9rem;
  border-left: 3px solid var(--line);
  color: var(--ink-mute);
}
.readme table { border-collapse: collapse; }
.readme th, .readme td { padding: 0.35rem 0.6rem; border: 1px solid var(--line); }
.readme img { max-width: 100%; height: auto; }

/* A plain-text README (a bare `README` with no extension). */
.readme-text {
  overflow-x: auto;
  margin: 0;
  font-size: 0.86rem;
  line-height: 1.5;
}

/* Why this README is plainer than it should be, in the server's own words. */
.readme-note {
  margin: 0.8rem 0 0;
  color: var(--ink-mute);
  font-size: 0.82rem;
}

/* ── A note under a panel's heading ────────────────────────────────────────
 *
 * `.hint` existed only as `.field .hint` — a form's helper text — while three
 * panels were already writing `<p class="hint">` beside their tables and
 * getting a plain paragraph in body ink. This is the base rule; the form's
 * descendant selector still wins where it applies. */
.hint {
  margin: 0 0 0.6rem;
  color: var(--ink-mute);
  font-size: 0.82rem;
  max-width: 54rem;
}

/* ── The Machine page: the disks a box with no shell cannot otherwise show ──
 *
 * Three panels, three tenses: what the installer DECIDED (historical, and
 * carried as the key/value table gunnar declined to parse), what is true NOW
 * (measured on the call), what gunnar is CONFIGURED to do. The only chromatic
 * pixels on the whole page belong to the off-stripe badge, the signature badge
 * and the written-to badge, which is the console's rule — colour means status
 * — and on the first it means something sharper: a copy that shares a device
 * with what it copies is drawn in the refusal hue, one step ABOVE having no
 * copy at all.
 *
 * Everything else is ink weight. The fill bar is achromatic on purpose: a
 * filesystem filling up is a fact, and turning it red at a threshold this page
 * does not know would be this console inventing a policy gunnar has not got. */
.disks-head {
  margin: 0.2rem 0 0.55rem;
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* The sentence under a badge or a figure: why this is what it is. Never
 * competes with the value it explains, and always present where a badge is —
 * colour alone is the failure this palette was built against. */
.disks-note {
  margin: 0.3rem 0 0;
  color: var(--ink-mute);
  /* Explicit, because one of these sits inside a `th[scope="row"]`, which is
   * semibold: an OS error rendered in the row header's weight competes with
   * the row's own name. */
  font-weight: 400;
  font-size: 0.82rem;
  line-height: 1.45;
  max-width: 54rem;
  overflow-wrap: anywhere;
}

/* ★ The off-stripe health lines: one sentence per copy that needs another
 * failure domain, each naming the copy AND its limitation. A list rather than
 * a badge row on purpose — "present" is never the whole answer here, and a
 * layout that could only fit one word would force one. */
.off-stripe-health {
  display: grid;
  gap: 0.35rem;
  margin: 0.55rem 0 0;
}

/* How full a filesystem is. Unlike the Room table's `.wall` this needs no
 * ceiling rule drawn on it: the end of a disk IS the end of the track, and
 * unlike `.indexing .bar` it is not progress towards anything good. It is
 * drawn ONLY where both halves were measured — see `machine::fill_percent`,
 * which answers `None` rather than let a failed `statvfs`'s zeroes become a
 * bar at 0 % under the words "0 B of 0 B". */
table.disks th[scope="row"] .fill {
  height: 4px;
  margin: 0.35rem 0 0.1rem;
  max-width: 12rem;
  background: var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

table.disks th[scope="row"] .fill span {
  display: block;
  height: 100%;
  background: var(--ink-dim);
}

/* ★ **What publishing would hand over.** The block the visibility control opens
 * before it will publish anything: the branch names, in the warn wash rather
 * than the bad one, because this is a deliberate act and not a failure. It is
 * deliberately NOT `.truncation`'s muted small print — the whole point of the
 * two-press design is that this paragraph is read, and muted grey is what a
 * reader's eye skips on a page already full of metadata. */
.visibility .consequence {
  padding: 0.85rem 1rem;
  margin: 0 0 0.75rem;
  border: 1px solid color-mix(in oklch, var(--warn) 40%, transparent);
  border-radius: var(--radius);
  background: var(--warn-wash);
  overflow-wrap: anywhere;
}

.visibility .consequence p {
  font-size: 0.88rem;
}

.visibility .consequence .hint {
  margin-top: 0.4rem;
}

/* The panel's own paragraphs carry no border, unlike `.truncation`, which draws
 * a rule above every line it sits on and would fence the button off from the
 * sentence explaining it. */
.visibility p {
  margin: 0.55rem 0 0;
}

/* ── Twin panel and restore ──────────────────────────────────────────── */

.twin-alarm .panel {
  border-color: var(--bad);
}

.twin tr.alarm td {
  background: var(--bad-wash);
}

.restore .consequence {
  padding: 0.85rem 1rem;
  margin: 0 0 0.75rem;
  border: 1px solid color-mix(in oklch, var(--bad) 40%, transparent);
  border-radius: var(--radius);
  background: var(--bad-wash);
  overflow-wrap: anywhere;
}
