/*
 * blog.highport.space. GENERATED by `just blog` from web/styles/base.css and
 * web/site/styles/site.css. Edit those, not this.
 */

/*
 * Shared foundation for both hosts — the only file in this project where a colour is
 * defined. `docs/brand/STYLE_GUIDE.md` is the authority for what the colours mean;
 * this file is where they live and the one place §12's contrast arithmetic can be
 * enforced. A hex in `site.css`, `hub.css` or a component is a bug, because the
 * verification pass cannot protect what it cannot see.
 *
 * ── Type ──
 *
 * Three faces, self-hosted through Fontsource because `font-src 'self'` permits
 * nothing else (website-plan.md §6). §4 of the guide sanctions alternates for every
 * role and these are chosen from them:
 *
 *   Bungee                      display, wordmark, signage labels. Drawn for vertical
 *                               street signage, which is exactly the port-authority
 *                               enamel-sign voice the brand is after.
 *   Atkinson Hyperlegible Next  UI and body. The Braille Institute's low-vision family.
 *   Atkinson Hyperlegible Mono  data, code, and DNS records.
 *
 * The mono choice is the load-bearing one. A person copying a TXT record reads it
 * character by character against what their registrar shows, and `0`/`O` and `1`/`l`/`I`
 * are where that goes wrong. Atkinson's whole design premise is disambiguating exactly
 * those glyphs, and domain verification is the screen this product is most likely to
 * lose somebody on. That is worth more here than a period-correct terminal face.
 *
 * ── Colour ──
 *
 * Written in OKLCH. Equal steps in the L channel look equal, which is what makes the
 * dark and light ramps feel like the same palette rather than two palettes; the hex in
 * each comment is the guide's sRGB value for cross-reference.
 *
 * The light ramp is tuned against `--surface-sunk` (#E1D7BF), not against the page.
 * A first pass checked only bg/surface/raised and shipped four failures that an audit
 * of the rendered page then found — a code block, a ghost button, the footer and a
 * nav glyph, all sitting on the sunk tone. Every text token here now clears 4.5:1
 * against the *worst* of the four grounds, which is the only threshold that survives
 * a component being moved.
 *
 * Seven values differ from `STYLE_GUIDE.md` §3 as published, because seven of §12's
 * claimed pairings do not hold. Each is marked `§12 correction` at its definition with
 * the measured ratio it replaced. The hues are unchanged — only lightness moved, and
 * only far enough to clear the threshold with a little headroom.
 *
 * ── The two border tokens ──
 *
 * `--line` is decoration: the hairline between rows, the edge of a panel. It has no
 * contrast obligation. `--line-strong` is the visible boundary of a control, which
 * WCAG 1.4.11 requires to clear 3:1 against the surface behind it. The guide's single
 * `--border` was doing both jobs and measured 1.37 dark / 1.46 light — so an input
 * whose only boundary is its border failed, in both schemes. Never style a control's
 * edge with `--line`.
 */

/*
 * The faces are declared here rather than imported from Fontsource's own stylesheets,
 * which pull three subsets (latin, latin-ext, vietnamese) in both woff2 and woff. The
 * build target is es2022 and every engine that parses that also reads woff2, so the
 * woff half is 184 kB nothing will ever request, and the two extra subsets are ranges
 * no copy on either host uses. Declaring six faces by hand ships 6 files instead of 26.
 */

@font-face {
  font-family: 'Bungee';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/bungee-latin-400-normal.woff2') format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Atkinson Hyperlegible Next';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/atkinson-hyperlegible-next-latin-400-normal.woff2')
    format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Atkinson Hyperlegible Next';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/fonts/atkinson-hyperlegible-next-latin-500-normal.woff2')
    format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Atkinson Hyperlegible Next';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/fonts/atkinson-hyperlegible-next-latin-700-normal.woff2')
    format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Atkinson Hyperlegible Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/atkinson-hyperlegible-mono-latin-400-normal.woff2')
    format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Atkinson Hyperlegible Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/fonts/atkinson-hyperlegible-mono-latin-600-normal.woff2')
    format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* Dark first. The guide's §3 ruling — it is a spaceport at night — and `light dark`
     in this order makes dark the answer when the OS expresses no preference. */
  color-scheme: dark light;

  --font-display: 'Bungee', 'Trebuchet MS', system-ui, sans-serif;
  --font-ui: 'Atkinson Hyperlegible Next', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-mono: 'Atkinson Hyperlegible Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-sans: var(--font-ui);

  /* ── Ground and ink ─────────────────────────────────────────────────────────── */

  /* Deep Space Black #0B0B0D / Parchment #F5EEDD */
  --bg: light-dark(oklch(95% 0.024 88.2), oklch(15.1% 0.004 285.9));
  /* Orbital Navy #12141F / Manifest Eggshell #EDE3CC */
  --surface: light-dark(oklch(91.8% 0.032 87.3), oklch(19.5% 0.022 276.1));
  /* Bay Navy #1B1E2E / white */
  --surface-raised: light-dark(oklch(100% 0 90), oklch(24% 0.031 275.6));
  /* The inset tone, under code blocks and wells: one step *away* from the raised
     surface in both schemes, which is down in light and further down in dark. */
  --surface-sunk: light-dark(oklch(88% 0.034 87), oklch(17% 0.02 276));

  /* Parchment / Chocolate #241A12 */
  --text: light-dark(oklch(22.8% 0.022 60.1), oklch(95% 0.024 88.2));
  /* #B9B09A / #5A4B38 */
  --text-muted: light-dark(oklch(42.3% 0.036 73.2), oklch(75.9% 0.032 88.4));
  --text-faint: light-dark(oklch(47.7% 0.034 72), oklch(64% 0.03 88));

  /* Decoration only. #2A2E42 / #C9BDA0 */
  --line: light-dark(oklch(80.1% 0.042 88.2), oklch(30.7% 0.037 275.3));
  /*
   * §12 correction — a control's boundary, so 1.4.11 wants 3:1. Was #2A2E42 at 1.37
   * dark and #C9BDA0 at 1.46 light.
   *
   * Measured against the *worst* of the four grounds, which the first correction was
   * not: it checked the raised surface only and reported 3.22 / 3.23, while the light
   * value on `--surface-sunk` was 2.87 — under the threshold on the ground `.gate`,
   * `.dropzone` and every input inside a `.param-field` actually sit on. That is the
   * same mistake the text ramp above was already tuned against, and the reason this
   * comment now names the ground it was measured on.
   *
   * Light moved 59% → 56.5% for 3.18 on the sunk tone (3.94 / 3.58 / 4.56 elsewhere).
   * Dark was already the worst-case value: 3.22 raised, 3.83 / 3.57 / 3.73 elsewhere.
   */
  --line-strong: light-dark(oklch(56.5% 0.042 86.8), oklch(53.8% 0.037 273.6));

  /* ── Brand ──────────────────────────────────────────────────────────────────── */

  /* The accent is a *fill*: `--accent-ink` sits on it, and §6's primary button is the
     use that matters. §12 correction (dark) — Burnt Orange #C3542C put cream at 3.93;
     darkened to #B6481F for 4.61. Light keeps the guide's Rust #A5451F, at 5.23. */
  --accent: light-dark(oklch(51.2% 0.137 40.4), oklch(54.4% 0.152 39.3));
  --accent-ink: oklch(95% 0.024 88.2);
  /* The accent as *text* on the page, which pulls the opposite way from the fill.
     §12 correction (dark) — #C3542C measured 4.33 on black and less on the surfaces;
     lightened to #D7663E for 4.61 against the worst of the three grounds. */
  --accent-text: light-dark(oklch(49.2% 0.136 40.5), oklch(64% 0.153 39.6));
  --accent-soft: light-dark(oklch(90% 0.04 55), oklch(26% 0.05 40));

  /* Harvest Gold. §12 correction (light) — Mustard #B8811B measured 2.93 on cream,
     which fails as text *and* as a 3:1 border, though §3 calls it "AA text on cream";
     darkened to #8D5900 for 4.62. Dark keeps the guide's #C6943E, at 6.06. */
  --accent-2: light-dark(oklch(48.3% 0.106 68), oklch(69.9% 0.118 78));
  /* Sand, a light fill in both schemes — so what sits on it is dark in both. */
  --highlight: oklch(76.5% 0.066 62);
  --highlight-ink: oklch(22.8% 0.022 60.1);

  /* Beacon Blue #6FD3FF / Signal Cyan Ink. §12 correction (light) — #0E6E93 measured
     4.48 on the eggshell surface, four hundredths short; darkened to #086C90. */
  --link: light-dark(oklch(47.3% 0.096 231.9), oklch(82.3% 0.112 229));
  --focus: var(--link);

  /* ── State ──────────────────────────────────────────────────────────────────── */

  /* Phosphor Green #4CE6A1. §12 correction (light) — Avocado #6B7A33 measured 4.07 on
     cream; Deep Avocado is the guide's own answer, tuned here to #5C6A22 for 4.65. */
  --success: light-dark(oklch(47.3% 0.097 119.7), oklch(82.7% 0.163 159.8));
  --success-soft: light-dark(oklch(93% 0.04 120), oklch(24% 0.05 160));
  /* CRT Amber #FFB44D / #8A5A12 */
  --warning: light-dark(oklch(48.3% 0.102 71.2), oklch(82.3% 0.145 71.6));
  --warning-soft: light-dark(oklch(93.5% 0.045 75), oklch(25% 0.05 72));
  --warning-line: var(--warning);
  /* #FF6B5A / #B42318 */
  --danger: light-dark(oklch(50% 0.183 29.3), oklch(70.9% 0.184 29.1));
  --danger-soft: light-dark(oklch(94% 0.035 30), oklch(24% 0.06 29));

  /* Decorative and large text only — never body copy. Laser Pink, Electric Blue. */
  --neon-pink: light-dark(oklch(54% 0.201 359.4), oklch(69.1% 0.223 357.2));
  --neon-blue: light-dark(oklch(47.3% 0.096 231.9), oklch(75.1% 0.13 229.3));

  /* ── The readout ────────────────────────────────────────────────────────────── */

  /*
   * §11.2's phosphor, and §3's `--hp-phosphor` #4CE6A1 promoted into the token layer.
   *
   * **It is not `--success`, and the two being the same green in dark mode is exactly
   * why this token has to exist.** §3 names #4CE6A1 twice — once as the raw CRT colour
   * and once as the dark scheme's success state — so a readout painted from `--success`
   * would look right and mean something else: the next correction to the lamp green
   * would silently repaint every terminal panel in the product, and §11.6's promise
   * that colour is only spent on state would be spent on decoration instead. A readout
   * is decoration, a lamp is a signal, and the tokens keep them apart even where the
   * hue does not.
   *
   * **One value, not a `light-dark()` pair, because a screen is dark in both schemes.**
   * §7 reserves CRT treatment for surfaces that are literally screens, and a monitor in
   * a lit room is still a dark monitor — so the readout brings its own ground rather
   * than taking the page's, and green on parchment (1.35) never arises. That is the
   * same shape as `--highlight` / `--highlight-ink` above: a fill that does not move
   * between schemes, and an ink measured against it.
   *
   * §12, measured against the ground below rather than the page's four: phosphor 12.80,
   * `--text` 17.69 for the anything the readout prints in cream, `--warning` 11.58 for
   * the amber the status board's state word takes when it is not nominal. The ground
   * itself is 17.69 against light `--bg` and 1.04 against dark `--bg` — so in the dark
   * scheme the panel is told apart by its border and not by its fill, which is what
   * `.readout`'s `--line-strong` edge is for.
   */
  --phosphor: oklch(82.7% 0.163 159.8);
  --phosphor-ground: oklch(11% 0.012 276);

  /* ── The spectrum band ──────────────────────────────────────────────────────── */

  /* §7's structural motif: a thin stripe under the wordmark, at a section edge, as a
     progress fill. Never a ground for text. */
  --rainbow: linear-gradient(
    90deg,
    oklch(54.4% 0.152 39.3) 0%,
    oklch(69.9% 0.118 78) 22%,
    oklch(55% 0.1 130) 44%,
    oklch(75.1% 0.13 229.3) 68%,
    oklch(52% 0.13 265) 84%,
    oklch(52% 0.14 320) 100%
  );
  --beacon-sweep: linear-gradient(
    90deg,
    transparent 0%,
    oklch(75.1% 0.13 229.3) 45%,
    oklch(82.7% 0.163 159.8) 55%,
    transparent 100%
  );

  /* ── Measure, rhythm, shape ─────────────────────────────────────────────────── */

  --step--1: 0.8rem;
  --step-0: 1rem;
  --step-1: 1.25rem;
  --step-2: 1.563rem;
  /* Fluid above the body steps: the public site sets headings at these, and a hero
     that does not grow on a wide screen reads as a page that was never designed for
     one. The Hub uses the fixed steps, per §4. */
  --step-3: clamp(1.563rem, 1.35rem + 1.05vw, 1.953rem);
  --step-4: clamp(1.953rem, 1.6rem + 1.75vw, 2.441rem);
  --step-5: clamp(2.441rem, 1.85rem + 2.95vw, 3.6rem);

  --leading-body: 1.65;
  --leading-tight: 1.2;

  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-sm: 1rem;
  --space-md: 1.5rem;
  --space-lg: 2rem;
  --space-xl: 3rem;
  --space-2xl: 4rem;
  --space-3xl: 6rem;

  --radius-sm: 4px;
  --radius: 8px;
  --radius-lg: 14px;
  --radius-pill: 999px;

  /*
   * **`68ch` is about 100 characters, not 68.**
   *
   * `ch` is the advance of `0`, and Atkinson's digits are wide and tabular: measured on
   * the built site, `1ch` is 10.37px where the average prose character is 6.37px — a
   * ratio of 1.63 — so this cap lands at ~111 characters rather than the 65–75 the number
   * is reaching for.
   *
   * **It is the cap and not the column that was ever wrong**, and the column is what a
   * reader notices: a paragraph capped here inside a 68rem `.wrap` stopped two-thirds of
   * the way across and read as a line break somebody had typed. `site.css` lets the public
   * site's body copy fill its container instead, which is the shape that was asked for;
   * this token stays as the ceiling for the components that opt into a measure by name.
   *
   * Anything newly measured against it should be checked in characters, not assumed from
   * the token. A narrower correction was tried and reverted: it made the reading column
   * 480px against a 1088px card grid, which fixed the arithmetic and broke the page.
   *
   * The Hub does not apply this broadly: see `.hub-main :where(p, li)` in
   * `hub.css` for why component text is unmeasured there and the public site is not.
   */
  --measure: 68ch;
  --wrap: 68rem;

  /* Elevation on dark reads as a powered-on panel rather than a dropped card: a
     lighter surface plus a soft glow, per §5. */
  --shadow: 0 1px 0 oklch(0% 0 0 / 40%), 0 2px 8px oklch(0% 0 0 / 35%);
  --glow-accent: 0 0 0 1px oklch(54.4% 0.152 39.3 / 50%), 0 0 16px oklch(54.4% 0.152 39.3 / 32%);
  /*
   * **Decoration. The outline is the focus indicator.**
   *
   * At 45% alpha this measures 1.83–2.03:1 against the four light grounds and 3.06–3.13
   * against the dark ones — and 1.10 against a primary button's own accent face. 1.4.11
   * wants 3:1. For a while it was the *only* indicator, because `.button`, `.field` and
   * `.choice` each paired it with `outline: none`, which is how a keyboard user came to
   * have no conforming focus ring anywhere in the light scheme.
   *
   * Those rules now keep the global `:focus-visible` outline and add this behind it, so
   * the glow is the lit-panel flourish it was drawn to be and `--focus` at 4.60 (light,
   * worst ground) / 9.78 (dark) is what carries the requirement. Never pair this with
   * `outline: none` again.
   */
  --glow-focus: 0 0 0 3px light-dark(oklch(49.8% 0.098 230.7 / 45%), oklch(82.3% 0.112 229 / 45%));

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --duration: 200ms;
}

/*
 * Every colour above is a `light-dark()` pair, which resolves against the *used*
 * `color-scheme` — so setting that property is the whole theme switch, and one
 * declaration per direction makes an explicit choice beat the OS both ways. §3 asks
 * for exactly this: the OS preference is the default and `data-theme` overrides it.
 */
:root[data-theme='dark'] {
  color-scheme: dark;
}

:root[data-theme='light'] {
  color-scheme: light;
}

/* Light type on a dark ground reads lighter than it is and wants more room, so the
   default leading is the dark one and light mode tightens rather than the reverse. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) {
    --leading-body: 1.6;

    /* A dropped shadow on a light ground, where the dark scheme wants a lit edge. */
    --shadow: 0 1px 2px oklch(22.8% 0.022 60.1 / 8%), 0 4px 14px oklch(22.8% 0.022 60.1 / 7%);
  }
}

/* ── Reset and elements ─────────────────────────────────────────────────────── */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--step-0);
  line-height: var(--leading-body);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
  margin: 0 0 var(--space-xs);
  line-height: var(--leading-tight);
  text-wrap: balance;
}

h1 {
  font-family: var(--font-display);
  font-size: var(--step-5);
  font-weight: 400;
  letter-spacing: -0.015em;
}

h2 {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 400;
  letter-spacing: -0.01em;
}

h3 {
  font-size: var(--step-1);
  font-weight: 700;
}

h4 {
  font-size: var(--step-0);
  font-weight: 700;
}

p,
ul,
ol,
dl,
pre,
figure {
  margin: 0 0 var(--space-sm);
}

:is(p, li) {
  max-width: var(--measure);
}

a {
  color: var(--link);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

a:hover {
  text-decoration-thickness: 2px;
}

code,
samp,
kbd {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

code {
  padding: 0.12em 0.36em;
  border-radius: var(--radius-sm);
  background: var(--surface-sunk);
}

pre {
  padding: var(--space-sm);
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-sunk);
  font-size: var(--step--1);
  line-height: 1.55;
}

pre code {
  padding: 0;
  background: none;
}

svg {
  display: block;
}

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

::selection {
  background: var(--accent);
  color: var(--accent-ink);
}

/* ── Utilities ──────────────────────────────────────────────────────────────── */

.wrap {
  width: min(100% - 2 * var(--space-sm), var(--wrap));
  margin-inline: auto;
}

.skip-link {
  position: absolute;
  left: var(--space-sm);
  top: -4rem;
  z-index: 20;
  padding: var(--space-2xs) var(--space-sm);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
  transition: top var(--duration) var(--ease-out);
}

.skip-link:focus {
  top: var(--space-sm);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.muted {
  color: var(--text-muted);
}

.lede {
  max-width: 60ch;
  font-size: var(--step-1);
  line-height: 1.5;
  color: var(--text-muted);
}

.fineprint {
  font-size: var(--step--1);
  color: var(--text-faint);
}

/* §7's spectrum band as a rule. A decorative element, so it is hidden from the
   accessibility tree rather than described. */
.band {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--rainbow);
}

/* ── Brand ──────────────────────────────────────────────────────────────────── */

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 400;
  color: var(--text);
  text-decoration: none;
  letter-spacing: -0.01em;
}

.brand-mark {
  width: 1.55em;
  height: 1.55em;
  flex: none;
  border-radius: 6px;
}

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

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  padding: var(--space-xs) var(--space-md);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: var(--text);
  font: inherit;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition:
    transform var(--duration) var(--ease-out),
    box-shadow var(--duration) var(--ease-out),
    background-color var(--duration) var(--ease-out);
}

.button:hover:not(:disabled, [aria-disabled='true']) {
  background: var(--surface-sunk);
}

/* A switch that throws. The press is a real 1px of travel, per §6 — and it is
   transform rather than margin so it never reflows the row it sits in. */
.button:active:not(:disabled, [aria-disabled='true']) {
  transform: translateY(1px);
}

/* The glow is added *behind* the global outline rather than replacing it — see
   `--glow-focus` for what replacing it cost. */
.button:focus-visible {
  box-shadow: var(--glow-focus);
}

.button.primary {
  border-color: transparent;
  background: var(--accent);
  color: var(--accent-ink);
}

.button.primary:hover:not(:disabled, [aria-disabled='true']) {
  background: var(--accent);
  box-shadow: var(--glow-accent);
}

.button.danger {
  border-color: transparent;
  background: var(--danger);
  color: light-dark(oklch(98% 0.01 60), oklch(15.1% 0.004 285.9));
}

.button.subtle {
  border-color: transparent;
  background: none;
  color: var(--link);
  font-weight: 500;
  padding-inline: var(--space-2xs);
}

.button.subtle:hover:not(:disabled, [aria-disabled='true']) {
  background: var(--accent-soft);
}

.button:disabled,
.button[aria-disabled='true'] {
  opacity: 0.5;
  cursor: not-allowed;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  align-items: center;
}

/* ── Rack modules ───────────────────────────────────────────────────────────── */

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

.panel + .panel {
  margin-top: var(--space-md);
}

.panel-description {
  margin-top: calc(-1 * var(--space-3xs));
  color: var(--text-muted);
}

/* A callout is a full-bordered module with a tinted ground and a lamp, never a
   colour bar down one edge — see the note above `.lamp`. */
.callout {
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-raised);
}

.callout h2,
.callout h3 {
  font-family: var(--font-ui);
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: 0;
}

.callout p:last-child {
  margin-bottom: 0;
}

.callout.error {
  border-color: var(--danger);
  background: var(--danger-soft);
}

.callout.warning {
  border-color: var(--warning);
  background: var(--warning-soft);
}

.callout.success {
  border-color: var(--success);
  background: var(--success-soft);
}

.empty {
  padding: var(--space-lg) var(--space-md);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-lg);
  color: var(--text-muted);
}

/* An empty state that teaches rather than only reports is more than one line, so the box
   has to hold a stack. `p` collapses its own margins against the padding otherwise. */
.empty > :first-child {
  margin-top: 0;
}

.empty > :last-child {
  margin-bottom: 0;
}

.empty > * + * {
  margin-top: var(--space-xs);
}

.loading {
  color: var(--text-muted);
}

/* ── Status lamps ───────────────────────────────────────────────────────────── */

/*
 * The signature status indicator, and the reason there are no coloured edge stripes
 * anywhere in this stylesheet: a lamp carries the state and the label carries the
 * meaning, so nothing is encoded in colour alone. §6 and §12.
 */
.lamp {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2xs);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.lamp::before {
  content: '';
  width: 0.6em;
  height: 0.6em;
  flex: none;
  border-radius: 50%;
  background: currentcolor;
  box-shadow: 0 0 6px currentcolor;
  translate: 0 -0.05em;
}

.lamp.on {
  color: var(--success);
}

.lamp.pending {
  color: var(--warning);
}

.lamp.off {
  color: var(--danger);
}

.lamp.idle {
  color: var(--text-faint);
}

.lamp.idle::before {
  box-shadow: none;
}

/* ── The readout ────────────────────────────────────────────────────────────── */

/*
 * §11.2's other layer: the small mono block that speaks for Orbital Control, over a
 * ground that is a screen rather than a page.
 *
 * **It states facts and it is not a lamp.** The rule above this one is the signal;
 * this one is decoration, and the two are adjacent in the file so that the next person
 * to reach for green on a dark panel reads both. Nothing here takes a variant — a
 * readout that changed colour with the thing it reports would be a lamp wearing the
 * voice, which is the confusion §11.6 exists to prevent.
 *
 * **Uppercase in the stylesheet, sentence case in the markup**, as `.lamp` and
 * `.eyebrow` already are: a screen reader that spells out a run of capitals reads the
 * source, and the source is a sentence.
 *
 * **The border is what separates the panel in the dark scheme.** Its fill measures 1.04
 * against dark `--bg` — deliberately, because a CRT is not lighter than the room — so
 * in that scheme the edge carries the shape and the fill carries the light scheme.
 */
.readout {
  display: inline-block;
  max-width: 100%;
  margin: 0 0 var(--space-sm);
  padding: var(--space-2xs) var(--space-xs);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--phosphor-ground);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.5;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--phosphor);
}

/* The panel's own name, on its own line. Weight rather than a second colour: §11.2
   describes one phosphor-green block, and a cream line would need an ink that does not
   move between schemes for a reason no other component in this file has. */
.readout b {
  display: block;
  font-weight: 600;
}

/*
 * §11.2 asks for a blinking block cursor after the last line, and §7 asks that any
 * residual motion be gated — the blanket `prefers-reduced-motion` rule at the end of
 * this file is the gate, and it leaves the block drawn rather than removing it, which
 * is §9's requirement that reduced motion keep the meaning. It is `aria-hidden` at
 * every call site: it is the terminal's, not a fact the reader needs.
 */
.readout-cursor {
  display: inline-block;
  width: 0.6em;
  height: 1em;
  margin-left: 0.2em;
  vertical-align: -0.15em;
  background: currentcolor;
  animation: readout-blink 1.1s steps(1) infinite;
}

@keyframes readout-blink {
  50% {
    opacity: 0;
  }
}

/* ── Manifests ──────────────────────────────────────────────────────────────── */

.table-frame {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.data-table thead th {
  padding: var(--space-xs) var(--space-sm);
  border-bottom: 1px solid var(--line-strong);
  background: var(--surface-sunk);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: left;
  color: var(--text-muted);
  white-space: nowrap;
}

.data-table td {
  padding: var(--space-xs) var(--space-sm);
  border-bottom: 1px solid var(--line);
  vertical-align: baseline;
}

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

.data-table td.empty {
  border: none;
  padding: var(--space-lg) var(--space-sm);
  color: var(--text-muted);
  text-align: center;
}

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

.form {
  display: grid;
  gap: var(--space-md);
}

.form.narrow {
  max-width: 34rem;
}

.field {
  display: grid;
  gap: var(--space-3xs);
}

/* The label plate: small, tracked, uppercase, sitting above the control the way an
   embossed strip sits above a socket on a rack panel. */
.field label,
.field-label {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/*
 * A value quoted inside a label is exempt from the plate's uppercasing.
 *
 * The transform is styling for *words*; a `code` in a label is a value the person is being
 * asked to match — a domain, a space key, a record key — and uppercasing it shows them a
 * string that is not the one being compared. A confirmation gate that displays
 * `3MUKA75MR3WYV` and accepts only `3muka75mr3wyv` reads as a broken button, and the
 * person who typed exactly what they were shown is right.
 *
 * Case-insensitive comparison would have hidden this rather than fixed it: the value shown
 * would still be a lie about the value held.
 */
.field label code,
.field-label code {
  text-transform: none;
  letter-spacing: normal;
}

/*
 * `:not([type='radio']):not([type='checkbox'])` because every declaration below is
 * written for a control you type into, and `width: 100%` on a radio stretches it to the
 * column — 365px wide and 13px tall in the tile picker's "Show" group, with its label
 * shoved to the far side of the gap and no longer reading as its label. The opt-out used
 * to be a `class="checkbox"` the call site had to remember, and the radios never did.
 */
.field input:not([type='radio']):not([type='checkbox']),
.field select,
.field textarea {
  width: 100%;
  padding: var(--space-2xs) var(--space-xs);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: var(--text);
  font: inherit;
  transition: box-shadow var(--duration) var(--ease-out);
}

.field textarea {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.55;
  resize: vertical;
}

/*
 * A file input's own button, which is the last piece of user-agent chrome in these forms.
 * The box around it was already themed, so it read as a styled field with a stock macOS
 * button dropped inside it — most visibly on the editor's not-found upload.
 *
 * It cannot inherit `.button`: the pseudo-element is not a real element and takes only a
 * subset of properties, so this restates the plain button's face rather than composing
 * with it. `pointer` because the pseudo-element does not inherit the cursor either.
 */
.field input[type='file'] {
  padding: var(--space-3xs);
}

.field input[type='file']::file-selector-button {
  margin-right: var(--space-xs);
  padding: var(--space-3xs) var(--space-xs);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 700;
  cursor: pointer;
}

.field input[type='file']:enabled::file-selector-button:hover {
  background: var(--surface-sunk);
}

/* Same exclusion: a native radio has no border to recolour, so recolouring one here
   would do nothing. Every control keeps the global outline; the recoloured border and
   the glow are what make a *typed-into* field look energised. */
.field :is(input:not([type='radio']):not([type='checkbox']), select, textarea):focus-visible {
  border-color: var(--focus);
  box-shadow: var(--glow-focus);
}

.field :is(input, select, textarea):disabled,
.field textarea:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.field-error {
  color: var(--danger);
  font-size: var(--step--1);
  font-weight: 700;
}

.help {
  color: var(--text-muted);
  font-size: var(--step--1);
}

/* ── Lists ──────────────────────────────────────────────────────────────────── */

/*
 * A label and its value, one pair per row — never two pairs across.
 *
 * `auto 1fr` on the `dl` is a *label / value* grid and only works when the `dt` and `dd`
 * are its direct children. Wrap each pair in a `div`, as three call sites did, and the
 * same two tracks become two **entries** side by side: a 100-character AT-URI took the
 * wide column and a 59-character CID was folded into four lines in the narrow one, with
 * the third entry starting a new row underneath. Nothing about the values is two-column
 * material — they are identifiers, and an identifier wants the whole width.
 *
 * Below 52rem the label goes above its value, because `minmax(8rem, …)` for the label
 * plus a mono CID does not fit a phone. Same reasoning the `.panel` variant carried
 * before this rule absorbed it.
 */
.spec-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3xs) var(--space-sm);
  margin: 0;
}

@media (width >= 52rem) {
  .spec-list {
    grid-template-columns: minmax(8rem, max-content) minmax(0, 1fr);
    /*
     * `start`, not `baseline`. The values are `inline-block` code chips (below), and an
     * inline-block contributes the baseline of its **last** line box — so a label beside
     * a two-line AT-URI aligned itself with the URI's second line and read as belonging
     * to the row underneath.
     */
    align-items: start;
  }
}

.spec-list dt {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.spec-list dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}

.step-list {
  counter-reset: step;
  list-style: none;
  padding: 0;
  display: grid;
  gap: var(--space-md);
}

.step-list > li {
  counter-increment: step;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-sm);
  max-width: none;
}

.step-list > li::before {
  content: counter(step);
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: var(--highlight);
  color: var(--highlight-ink);
  font-family: var(--font-display);
  font-size: var(--step--1);
}

.choice-list {
  display: grid;
  gap: var(--space-sm);
  padding: 0;
  list-style: none;
}

.choice {
  display: grid;
  gap: var(--space-3xs);
  width: 100%;
  padding: var(--space-sm);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition:
    border-color var(--duration) var(--ease-out),
    box-shadow var(--duration) var(--ease-out);
}

.choice:hover {
  border-color: var(--accent);
  box-shadow: var(--glow-accent);
}

.choice:focus-visible {
  box-shadow: var(--glow-focus);
}

.choice-list h3 {
  margin: 0;
  font-size: var(--step-0);
}

.choice-list p {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--step--1);
}

/* ── Range controls ─────────────────────────────────────────────────────────── */

/*
 * The spending sliders, and the one place in this stylesheet where a native control is
 * rebuilt rather than tinted.
 *
 * `accent-color` alone leaves the platform thumb, which measures 16px — under 2.5.8's
 * 24×24 floor, on the two controls that decide whether a stranger can read somebody's
 * site and how much of their month it costs. At 320px the budget track is 180px wide
 * for ten discrete stops, so 18px of travel per stop: a 16px thumb is not a target
 * anybody drags accurately, and the value it lands on is money.
 *
 * Styling the thumb means styling the track too — `appearance: none` takes both — and
 * it means the two vendor pseudo-elements cannot be grouped, because an unknown
 * selector in a list invalidates the whole rule in both engines.
 */
input[type='range'] {
  appearance: none;
  width: 100%;
  height: 1.5rem;
  background: none;
  cursor: pointer;
}

input[type='range']::-webkit-slider-runnable-track {
  height: 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-sunk);
}

input[type='range']::-moz-range-track {
  height: 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-sunk);
}

/* 24px across, and the `margin-top` is what re-centres a webkit thumb on its track —
   that engine positions it from the track's top edge rather than its middle. */
input[type='range']::-webkit-slider-thumb {
  appearance: none;
  width: 1.5rem;
  height: 1.5rem;
  margin-top: calc((0.5rem - 1.5rem) / 2);
  border: 2px solid var(--accent-ink);
  border-radius: 50%;
  background: var(--accent);
  box-shadow: var(--shadow);
}

input[type='range']::-moz-range-thumb {
  width: 1.5rem;
  height: 1.5rem;
  border: 2px solid var(--accent-ink);
  border-radius: 50%;
  background: var(--accent);
  box-shadow: var(--shadow);
}

input[type='range']:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* ── Forced colours ─────────────────────────────────────────────────────────── */

/*
 * Windows High Contrast, and the one mode that discards `box-shadow` outright.
 *
 * Everything above survives it because of §6's rule that a state is a lamp *and* a
 * label: the glows go, the words stay, and `.site-dot`'s filled-versus-ring shape
 * outlives its colour. The focus ring is the exception — it was a `box-shadow` with
 * `outline: none` in front of it, so in this mode it was not dimmed, it was gone.
 *
 * The outline is restored above, which is most of the fix; this makes it thicker and
 * hands it to the system's own focus colour, because a forced palette has no `--focus`.
 */
@media (forced-colors: active) {
  :focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 2px;
  }

  /* Both are drawn with `background`, which this mode forces to the system's surface —
     so an unselected radio card and the selected one become the same rectangle. The
     border is the part a forced palette keeps. */
  .choice:focus-visible,
  .button:focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 2px;
  }
}

/* ── Motion ─────────────────────────────────────────────────────────────────── */

/*
 * §9: reduced motion keeps meaning intact rather than removing it. Nothing here
 * conveys state by movement alone — a lamp that pulses is still a lit lamp with a
 * label when the pulse is gone.
 */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/*
 * Chrome for the public site on highport.space. Every page reads without JavaScript.
 *
 * The page is a port-authority noticeboard: a control bar across the top under a
 * spectrum band, a starfield behind the hero, and everything below it set as rack
 * modules and manifests. `docs/brand/STYLE_GUIDE.md` §6 and §7 are the authority;
 * every colour comes from `base.css` and none is written here.
 */


/* ── Control bar ────────────────────────────────────────────────────────────── */

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

/* §7's spectrum band, sitting under the bar the way it sits under the wordmark.
   Decorative, so it is a pseudo-element and never reaches the accessibility tree. */
.site-header::after {
  content: '';
  display: block;
  height: 3px;
  background: var(--rainbow);
}

.header-inner {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 3vw, 2rem);
  min-height: 3.75rem;
  flex-wrap: wrap;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--space-3xs) var(--space-sm);
  margin-left: auto;
  flex-wrap: wrap;
}

/* Signage labels: short, tracked, uppercase. §4 — the techno voice is for labels of
   this length and never for a paragraph. */
.site-nav a {
  padding: var(--space-3xs) 0;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
  text-decoration: none;
}

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

.site-nav a[aria-current='page'] {
  color: var(--text);
  box-shadow: inset 0 -2px 0 var(--accent-text);
}

.site-nav .cta {
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-ink);
}

.site-nav .cta:hover {
  color: var(--accent-ink);
  box-shadow: var(--glow-accent);
}

/* No opacity here: dimming it to 0.7 put the glyph at 3.35:1. It is small and
   decorative, and it still has to be visible. */
.site-nav .ext {
  font-size: 0.8em;
}

main.prose,
main.home {
  flex: 1;
  padding-bottom: var(--space-2xl);
}

main.prose {
  padding-top: clamp(var(--space-lg), 6vw, var(--space-xl));
}

/* ── Prose ──────────────────────────────────────────────────────────────────── */

.prose {
  max-width: 46rem;
}

/*
 * The manual's screenshots are 1600px wide and the reading column is 736.
 *
 * Without this they render at natural size, and the document scrolls sideways —
 * taking the navigation and every paragraph with it, on every page that embeds
 * one. `max-width` alone would squash them, so `height: auto` is what keeps the
 * aspect ratio while the width is constrained.
 */
.prose img {
  max-width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.prose h1 {
  margin-bottom: 0.4em;
}

.prose h2 {
  margin-top: var(--space-xl);
  font-size: var(--step-2);
}

.prose h3 {
  margin-top: var(--space-md);
}

.prose ul,
.prose ol {
  padding-left: 1.3rem;
}

.prose li {
  margin-bottom: var(--space-3xs);
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}

.prose th,
.prose td {
  padding: var(--space-2xs) var(--space-xs);
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}

.prose thead th {
  border-bottom: 1px solid var(--line-strong);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* An indented, marked passage rather than a colour bar down one edge. */
.prose blockquote {
  margin: var(--space-md) 0;
  padding: var(--space-xs) var(--space-md);
  border-radius: var(--radius);
  background: var(--surface-sunk);
  color: var(--text-muted);
}

.prose blockquote > :last-child {
  margin-bottom: 0;
}

/* ── Hero ───────────────────────────────────────────────────────────────────── */

/*
 * The hero art is `brand/hero-{dark,light}.svg` — three warm bands sweeping out of the
 * bottom-left and fanning to the top right — and it is the background the hero copy
 * sits on.
 *
 * Which means the copy is written over the bands, and the bands are `#E3A23D`,
 * `#D9562B` and `#A8301F`. Cream over the gold at full strength measures 1.91:1. So
 * the art carries an opacity, and the number is derived rather than chosen: it is the
 * most vivid the artwork can be while every glyph over the worst band still clears
 * 4.5:1. Two things set it, and both are why this file also overrides the hero's ink.
 *
 *   The eyebrow is `--accent-text` — accent-coloured type over accent-coloured bands,
 *   which bottoms out at 0.16 and would have left the art a ghost. It is set in
 *   `--text` inside the hero for that reason alone.
 *
 *   The lede is normally `--text-muted`, which caps the art at 0.30. Also `--text`
 *   here. Everything in the hero is full-strength ink, which buys 0.60/0.55.
 *
 * Raising the opacity past those values, or restoring the muted lede, silently breaks
 * AA on the largest block of copy on the site. The numbers are in `STYLE_GUIDE.md` §12.
 *
 * There is no mask and no seam. The artwork's own ground *is* the page ground —
 * `#0B0B0D` exactly in dark, a cream gradient within two percent of `--bg` in light —
 * so a full-bleed background has no edge to hide. And because the box stays wider than
 * the artwork's 2.5:1 at every viewport, `cover` always resolves to fitting the width:
 * the bands enter and leave at the same fraction across on every screen.
 */
.hero-field {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-bottom: 1px solid var(--line);
  background-color: var(--bg);
}

.hero-field::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0.55;
  background-image: url('/brand/hero-dark.svg');
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) .hero-field::before {
    opacity: 0.6;
    background-image: url('/brand/hero-light.svg');
  }
}

:root[data-theme='light'] .hero-field::before {
  opacity: 0.6;
  background-image: url('/brand/hero-light.svg');
}

:root[data-theme='dark'] .hero-field::before {
  opacity: 0.55;
  background-image: url('/brand/hero-dark.svg');
}

.hero {
  padding-block: clamp(var(--space-xl), 7vw, var(--space-3xl));
}

/* A centred column, with the text left-aligned inside it. */
.hero > * {
  max-width: 46rem;
  margin-inline: auto;
}

/* Full-strength ink on everything in the hero — see the note above. */
.hero :is(p, h1, .eyebrow, .lede) {
  color: var(--text);
}

.eyebrow {
  margin-bottom: var(--space-2xs);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-text);
}

.hero h1 {
  margin-bottom: var(--space-sm);
}

.hero .lede {
  color: var(--text);
  opacity: 0.86;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-lg);
}

/* ── Sections ───────────────────────────────────────────────────────────────── */

.section {
  padding-block: clamp(var(--space-lg), 5vw, var(--space-xl));
}

.section + .section {
  border-top: 1px solid var(--line);
}

.section > h2 {
  max-width: 22ch;
}

.section > p {
  color: var(--text-muted);
}

/* ── Steps ──────────────────────────────────────────────────────────────────── */

.steps {
  display: grid;
  gap: var(--space-sm);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  list-style: none;
  padding: 0;
  margin: var(--space-md) 0 0;
  counter-reset: step;
}

.steps > li {
  counter-increment: step;
  padding: var(--space-md);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  max-width: none;
}

.steps > li::before {
  content: counter(step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  margin-bottom: var(--space-2xs);
  border-radius: 50%;
  background: var(--highlight);
  color: var(--highlight-ink);
  font-family: var(--font-display);
  font-size: 0.8rem;
}

.steps h3 {
  margin-bottom: var(--space-3xs);
  font-size: var(--step-1);
}

.steps p {
  color: var(--text-muted);
}

.steps p:last-child {
  margin-bottom: 0;
}

/* ── Docs ───────────────────────────────────────────────────────────────────── */

/*
 * A table on a docs page holds DNS rows, and a DNS row is a hostname beside a DID.
 * It does not fit a phone and it must not be allowed to widen the page: a body that
 * scrolls sideways takes the navigation and every paragraph with it to show one cell.
 * The wrapper is emitted for every table by `docs-pages.ts`.
 */
.table-scroll {
  overflow-x: auto;
  margin: var(--space-md) 0;
  /* Keeps a focused cell's ring from being clipped by the scroll box. */
  padding-bottom: var(--space-3xs);
}

.table-scroll table {
  margin: 0;
}

/* A cell holding nothing but a value is a value somebody is about to copy, and the
   one place a partial selection is worse than none. One click takes all of it. */
.prose td > code:only-child {
  user-select: all;
  white-space: nowrap;
}

.doc-band + .doc-band {
  margin-top: var(--space-xl);
}

.doc-band-note {
  margin-top: calc(-1 * var(--space-2xs));
  color: var(--text-muted);
}

/*
 * `.prose .doc-index`, not `.doc-index`, and the extra class is load-bearing.
 *
 * `.prose ol` above sets `padding-left: 1.3rem` for ordinary Markdown lists, and it
 * outranks a lone `.doc-index` — one class plus one element beats one class, whatever
 * the source order. So the `padding: 0` that used to be here never applied, and every
 * card on the index sat 1.3rem to the right of the heading above it with its right
 * edge still flush, in both themes, at every width.
 */
.prose .doc-index {
  list-style: none;
  padding: 0;
  display: grid;
  gap: 0;
}

/*
 * Rows, not twenty-six identical boxes.
 *
 * Every entry used to be the same bordered card, so the front door and a reference
 * page on parameter types were the same shape and finding the first one meant reading
 * all of them. The band's opening page is a card (`.doc-lead`); the rest are quiet
 * rows under it, separated by a hairline rather than boxed.
 */
.doc-index li {
  max-width: none;
  margin: 0;
  border-top: 1px solid var(--line);
}

.doc-index li:first-child,
.doc-index li.doc-lead + li {
  border-top: 0;
}

/*
 * The whole row is the target, which is also what makes it a comfortable one on a
 * phone — the title alone was a two-word tap.
 *
 * The negative inline margin cancels the padding optically: the hover area reaches
 * into the gutter while the text stays on the same left edge as the band heading
 * above it. Padding alone put every row 0.5rem to the right of its own heading,
 * which is the misalignment this file just finished removing from the cards.
 */
.doc-index a {
  display: grid;
  gap: var(--space-3xs);
  padding: var(--space-xs) var(--space-2xs);
  margin-inline: calc(-1 * var(--space-2xs));
  border-radius: var(--radius);
  text-decoration: none;
  transition: background-color var(--duration) var(--ease-out);
}

.doc-index a:hover {
  background: var(--surface);
}

.doc-index a:hover .doc-title {
  text-decoration: underline;
}

.doc-title {
  font-weight: 700;
  color: var(--link);
}

.doc-blurb {
  color: var(--text-muted);
  font-size: 0.94rem;
}

/* The way in. One per band, and the only thing on the index that looks like a card. */
.doc-lead {
  margin-bottom: var(--space-2xs);
}

.doc-lead a {
  gap: var(--space-2xs);
  padding: var(--space-sm);
  /* A card's own edge is what aligns with the heading, so it keeps its box. */
  margin-inline: 0;
  border: 1px solid var(--line);
  background: var(--surface);
}

.doc-lead a:hover {
  border-color: var(--line-strong);
  background: var(--surface-raised);
}

.doc-lead .doc-title {
  font-size: var(--step-1);
}

.doc-tag {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-text);
}

/*
 * The contents list, on the pages long enough to earn one (`TOC_MIN_WORDS`).
 *
 * Sunk rather than raised: it is a signpost the reader passes on the way into the
 * page, not a panel that competes with the first section for attention.
 */
.doc-toc {
  margin: var(--space-md) 0 var(--space-lg);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius);
  background: var(--surface-sunk);
}

.doc-toc-title {
  margin: 0 0 var(--space-2xs);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.prose .doc-toc ol {
  list-style: none;
  margin: 0;
  padding: 0;
  columns: 2;
  column-gap: var(--space-md);
}

.doc-toc li {
  margin-bottom: var(--space-3xs);
  break-inside: avoid;
  font-size: 0.94rem;
}

@media (width < 34rem) {
  .prose .doc-toc ol {
    columns: 1;
  }
}

/* Sits above the pager's rule, so it reads as a caption on the set rather than a
   heading over the two links. */
.doc-position {
  margin: var(--space-xl) 0 0;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.doc-position + .doc-pager {
  margin-top: var(--space-2xs);
}

.doc-pager {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: space-between;
  margin-top: var(--space-xl);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--line);
}

.doc-pager a {
  display: grid;
  max-width: 20rem;
  font-weight: 700;
  text-decoration: none;
}

.doc-pager a span {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.doc-pager .next {
  text-align: right;
  margin-left: auto;
}

/* ── Footer ─────────────────────────────────────────────────────────────────── */

.site-footer {
  border-top: 1px solid var(--line);
  background: var(--surface);
  font-size: 0.9rem;
  color: var(--text-faint);
}

.footer-inner {
  display: grid;
  gap: var(--space-2xs);
  padding-block: var(--space-md) var(--space-lg);
}

.footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-md);
}

.footer-nav a {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-decoration: none;
}

.footer-nav a:hover {
  text-decoration: underline;
}

/* ── Spotlight ──────────────────────────────────────────────────────────────── */

/*
 * Nine sites, three across. Three is stated rather than left to `auto-fit`: with a
 * 14rem floor the wrap fitted four, and a 3×3 grid that is sometimes 4+4+1 is not the
 * shape that was asked for. The two breakpoints below fold it rather than squeezing
 * three squares into a phone.
 */
.spotlight {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-sm);
  margin: var(--space-md) 0 var(--space-sm);
  padding: 0;
  list-style: none;
}

@media (width < 52rem) {
  .spotlight {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (width < 34rem) {
  .spotlight {
    grid-template-columns: minmax(0, 1fr);
  }
}

.spot {
  max-width: none;
}

.spot a {
  display: grid;
  gap: var(--space-3xs);
  padding: var(--space-2xs);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  transition:
    border-color var(--duration) var(--ease-out),
    box-shadow var(--duration) var(--ease-out);
}

.spot a:hover {
  border-color: var(--accent);
  box-shadow: var(--glow-accent);
}

.spot a:focus-visible {
  outline: none;
  box-shadow: var(--glow-focus);
}

/* Square, per the grid. The thumbnail is decoration — the name and the address below it
   are the content, which is why the image carries an empty `alt`. */
.spot-shot {
  aspect-ratio: 1;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  background: var(--surface-sunk);
}

.spot-name {
  margin-top: var(--space-3xs);
  font-weight: 700;
}

.spot-url {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* ── Body copy fills its column ─────────────────────────────────────────────── */

/*
 * **`base.css` caps every `p` and `li` at `--measure`, and on this site that is the wrong
 * default.** The cap is 68ch, which in Atkinson is ~111 characters rather than the 68 it
 * reads as — but the number was never the visible problem. The problem is that a capped
 * paragraph inside a 68rem `.wrap` stops two-thirds of the way across and leaves ~380px of
 * empty gutter, so the text reads as though someone typed a line break into it. Reported
 * exactly that way, twice.
 *
 * So the public site's body copy takes its width from the column it is in. The container
 * is already the design's decision about how wide text should be — `main.prose` is 46rem,
 * a `.wrap` section is 68rem — and having a second, narrower opinion layered inside it is
 * what produced the gutter.
 *
 * **Narrowing the containers instead was tried and reverted.** Setting the sections to the
 * reading measure did fix the arithmetic — 70 characters a line, no gutter anywhere — and
 * it made the page worse: a 480px column of prose above a 1088px card grid, the two no
 * longer any relation to each other. The cap was never what a reader was noticing.
 *
 * Direct children only. Text inside `.steps > li` and `.spot` is already sized by its card,
 * and those cards are the one place on the site where a measure is doing real work.
 */
main.prose :is(p, li),
main.home .wrap.section > :is(p, ul, ol) {
  max-width: none;
}
