/* GENERATED by scripts/bundle-css.mjs — do not edit, do not commit.
 *
 * 14 files concatenated in the order src/styles/tty.css imports them.
 * Edit the sources under src/styles/tty/ and the import list in src/styles/tty.css.
 */

/* ── tty/tokens/fonts.css ─────────────────────────────────────────── */
/* Silkscreen — pixel display face (SIL OFL, Jason Kottke). Chrome only:
   wordmark, headings, panel titles, button labels, status modes.
   Never smoothed, never set below 15px, never used for body copy. */
@font-face {
  font-family: "Silkscreen";
  src: url("https://cdn.jsdelivr.net/fontsource/fonts/silkscreen@latest/latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Silkscreen";
  src: url("https://cdn.jsdelivr.net/fontsource/fonts/silkscreen@latest/latin-700-normal.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* NOTE: no icon font is shipped. The glyph vocabulary (box drawing, blocks,
   braille spinner frames, geometric markers) comes from JetBrains Mono, which
   is what --font-glyph resolves to. See readme.md → ICONOGRAPHY. */

/* JetBrains Mono — reading + code + glyph face */
@font-face {
  font-family: "JetBrains Mono";
  src: url("https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono:vf@5.3.0/latin-wght-normal.woff2") format("woff2-variations");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono:vf@5.3.0/latin-wght-italic.woff2") format("woff2-variations");
  font-weight: 100 800;
  font-style: italic;
  font-display: swap;
}

/* ── tty/webfonts.css ─────────────────────────────────────────── */
/* Silkscreen, pinned.

   tokens/fonts.css declares this family against silkscreen's mutable jsdelivr
   "latest" tag. Two @font-face rules with the same family, weight and style
   resolve to the last one declared, and tty.css imports this file after the
   tokens, so these two rules win.

   5.3.0 is the fontsource version that mutable tag resolved to on 2026-09-21.
   Every other third-party reference on this site is pinned the same way:
   katex@0.16.47, flexsearch@0.7.31, prismjs@1.30.0 (.eleventy.js). JetBrains
   Mono needs no override — the token file already pins it at
   jetbrains-mono:vf@5.3.0. */
@font-face {
  font-family: "Silkscreen";
  src: url("https://cdn.jsdelivr.net/fontsource/fonts/silkscreen@5.3.0/latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Silkscreen";
  src: url("https://cdn.jsdelivr.net/fontsource/fonts/silkscreen@5.3.0/latin-700-normal.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ── tty/tokens/colors.css ─────────────────────────────────────────── */
:root {
  /* ── base neutrals: black-first, warm-neutral ink ───────────────── */
  --black-void: #000000;
  --black-000: #0A0A09;
  --black-100: #121211;
  --black-200: #1A1A18;
  --black-300: #242422;
  --black-400: #33332F;
  --grey-500: #4A4A45;
  --grey-600: #5E5E58;
  --grey-700: #8C8C84;
  --grey-800: #B4B4AC;
  --white-900: #E8E8E3;
  --white-max: #FFFFFF;

  /* ── primary: signal yellow ─────────────────────────────────────── */
  --yellow-bright: #FFE566;
  --yellow: #FFD400;
  --yellow-deep: #D9B400;
  --yellow-dim: #8A7300;
  --yellow-wash: rgba(255, 212, 0, 0.12);
  --yellow-wash-strong: rgba(255, 212, 0, 0.22);

  /* ── ANSI accents ───────────────────────────────────────────────── */
  --green: #5AF78E;
  --green-dim: #2E8F52;
  --green-wash: rgba(90, 247, 142, 0.12);
  --cyan: #5EDFFF;
  --cyan-dim: #2C7F94;
  --cyan-wash: rgba(94, 223, 255, 0.12);
  --magenta: #FF6AC1;
  --magenta-dim: #94396E;
  --magenta-wash: rgba(255, 106, 193, 0.12);
  --red: #FF5C57;
  --red-dim: #933531;
  --red-wash: rgba(255, 92, 87, 0.12);

  /* ── semantic surfaces ──────────────────────────────────────────── */
  --surface-root: var(--black-000);
  --surface-panel: var(--black-100);
  --surface-raised: var(--black-200);
  --surface-inset: var(--black-void);
  --surface-hover: var(--black-300);
  --surface-selected: var(--yellow-wash);
  --surface-scrim: rgba(0, 0, 0, 0.72);

  /* ── semantic text ──────────────────────────────────────────────── */
  --text-primary: var(--white-900);
  --text-secondary: var(--grey-800);
  --text-muted: var(--grey-700);
  --text-faint: var(--grey-600);
  --text-accent: var(--yellow);
  --text-on-accent: var(--black-void);
  --text-link: var(--yellow);
  --text-link-hover: var(--yellow-bright);

  /* ── semantic borders ───────────────────────────────────────────── */
  --border-faint: var(--black-300);
  --border-default: var(--black-400);
  --border-strong: var(--grey-500);
  --border-focus: var(--yellow);

  /* ── semantic status ────────────────────────────────────────────── */
  --status-ok: var(--green);
  --status-ok-dim: var(--green-dim);
  --status-info: var(--cyan);
  --status-info-dim: var(--cyan-dim);
  --status-warn: var(--yellow);
  --status-warn-dim: var(--yellow-deep);
  --status-error: var(--red);
  --status-error-dim: var(--red-dim);
  --status-special: var(--magenta);
  --status-special-dim: var(--magenta-dim);

  /* ── syntax (code blocks + log output) ──────────────────────────── */
  --syntax-keyword: var(--magenta);
  --syntax-string: var(--green);
  --syntax-number: var(--cyan);
  --syntax-comment: var(--grey-600);
  --syntax-function: var(--yellow);
  --syntax-punct: var(--grey-700);
}

/* ── tty/tokens/typography.css ─────────────────────────────────────────── */
:root {
  /* families */
  --font-display: "Silkscreen", ui-monospace, monospace;
  --font-body: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-code: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-glyph: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; /* box drawing, blocks, braille */

  /* display scale — Silkscreen, always uppercase, always tracked out.
     Fluid between the 360px phone and the 1440px desktop; the floor is the
     smallest size the pixel face stays legible at, never below 15px. */
  --display-xl: clamp(32px, calc(3.4vw + 20px), 56px); /* @kind font */
  --display-lg: clamp(26px, calc(2.0vw + 19px), 40px); /* @kind font */
  --display-md: clamp(20px, calc(1.1vw + 16px), 28px); /* @kind font */
  --display-sm: clamp(17px, calc(0.4vw + 15px), 20px); /* @kind font */
  --display-xs: 15px; /* @kind font */

  /* body / UI scale — JetBrains Mono. Reading sizes grow, UI sizes hold:
     a 13px table cell is 13px on every screen, the paragraph is not. */
  --text-xl: clamp(17px, calc(0.4vw + 15px), 20px); /* @kind font */
  --text-lg: clamp(16px, calc(0.15vw + 15px), 17px); /* @kind font */
  --text-md: 15px; /* @kind font */
  --text-sm: 13px; /* @kind font */
  --text-xs: 11px; /* @kind font */

  /* weights */
  --weight-regular: 400; /* @kind other */
  --weight-medium: 500; /* @kind other */
  --weight-bold: 700; /* @kind other */

  /* line heights — multiples of the cell so text sits on the grid */
  --leading-tight: 1.1; /* @kind other */
  --leading-ui: 1.35; /* @kind other */
  --leading-body: 1.7; /* @kind other */
  --leading-loose: 1.9; /* @kind other */

  /* tracking */
  --tracking-display: 0.08em; /* @kind other */
  --tracking-label: 0.14em; /* @kind other */
  --tracking-body: 0; /* @kind other */
  --tracking-tight: -0.01em; /* @kind other */
}

/* ── tty/tokens/spacing.css ─────────────────────────────────────────── */
:root {
  /* the character cell is the unit of the system */
  --cell-w: 9px; /* @kind spacing */
  --cell-h: 20px; /* @kind spacing */

  --sp-0: 0px;
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-8: 32px;
  --sp-10: 40px;
  --sp-12: 48px;
  --sp-16: 64px;
  --sp-20: 80px;
  --sp-24: 96px;

  /* control heights, all cell multiples */
  --control-sm: 24px;
  --control-md: 32px;
  --control-lg: 40px;

  /* radii — near-zero: pixels don't round */
  --radius-none: 0px;
  --radius-px: 1px;
  --radius-sm: 2px;
  --radius-pill: 999px;

  /* borders */
  --border-w: 1px;
  --border-w-thick: 2px;

  /* reading measure */
  --measure-prose: 68ch; /* @kind spacing */
  --measure-wide: 92ch; /* @kind spacing */
  --page-max: 1240px; /* @kind spacing */
}

/* ── tty/tokens/effects.css ─────────────────────────────────────────── */
:root {
  /* No soft drop shadows anywhere. Depth is a 1px rule or a hard offset block. */
  --shadow-none: none;
  --shadow-hard: 3px 3px 0 var(--black-void);
  --shadow-hard-accent: 3px 3px 0 var(--yellow-dim);
  --shadow-inset-top: inset 0 1px 0 rgba(255, 255, 255, 0.04);

  /* glow is reserved for focus + live status, never decoration */
  --glow-yellow: 0 0 0 1px var(--yellow), 0 0 12px rgba(255, 212, 0, 0.28);
  --glow-green: 0 0 8px rgba(90, 247, 142, 0.45);
  --glow-red: 0 0 8px rgba(255, 92, 87, 0.45);

  --focus-ring: 0 0 0 1px var(--black-void), 0 0 0 2px var(--yellow);

  /* textures */
  --texture-scanlines: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.022) 0px,
    rgba(255, 255, 255, 0.022) 1px,
    transparent 1px,
    transparent 3px
  ); /* @kind other */
  --texture-dither: repeating-conic-gradient(var(--black-300) 0% 25%, transparent 0% 50%) 0 0 / 4px 4px; /* @kind other */
  --texture-hatch: repeating-linear-gradient(45deg, var(--black-300) 0 1px, transparent 1px 5px); /* @kind other */
}

/* ── tty/tokens/motion.css ─────────────────────────────────────────── */
:root {
  /* TUI motion is stepped, not eased — frames, not curves */
  --ease-step-2: steps(2, end); /* @kind other */
  --ease-step-4: steps(4, end); /* @kind other */
  --ease-step-8: steps(8, end); /* @kind other */
  --ease-step-12: steps(12, end); /* @kind other */
  --ease-out: cubic-bezier(0.2, 0.8, 0.3, 1); /* @kind other */
  --ease-in-out: cubic-bezier(0.6, 0, 0.3, 1); /* @kind other */

  --dur-instant: 60ms; /* @kind other */
  --dur-fast: 120ms; /* @kind other */
  --dur-base: 200ms; /* @kind other */
  --dur-slow: 400ms; /* @kind other */
  --dur-screen: 520ms; /* @kind other */
  --dur-boot: 1600ms; /* @kind other */

  --blink-rate: 1060ms; /* @kind other */
  --spin-rate: 800ms; /* @kind other */
  --scan-rate: 6s; /* @kind other */
}

@keyframes tty-blink { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }
@keyframes tty-blink-soft { 0%, 60% { opacity: 1 } 61%, 100% { opacity: 0.25 } }
@keyframes tty-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.45 } }
@keyframes tty-caret-slide { from { width: 0 } to { width: 100% } }
@keyframes tty-scanline { from { transform: translateY(-100%) } to { transform: translateY(100vh) } }
@keyframes tty-wipe-in { from { clip-path: inset(0 100% 0 0) } to { clip-path: inset(0 0 0 0) } }
@keyframes tty-wipe-up { from { clip-path: inset(100% 0 0 0) } to { clip-path: inset(0 0 0 0) } }
@keyframes tty-glitch-x { 0%,100% { transform: translate(0) } 20% { transform: translate(-2px,1px) } 40% { transform: translate(2px,-1px) } 60% { transform: translate(-1px,0) } 80% { transform: translate(1px,1px) } }
@keyframes tty-flicker { 0%,100% { opacity: 1 } 42% { opacity: 1 } 43% { opacity: 0.6 } 45% { opacity: 1 } 78% { opacity: 0.85 } 80% { opacity: 1 } }
@keyframes tty-marquee { from { background-position: 0 0 } to { background-position: 14px 0 } }
@keyframes tty-spin-rot { to { transform: rotate(360deg) } }

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

/* ── tty/tokens/breakpoints.css ─────────────────────────────────────────── */
/* ─────────────────────────────────────────────────────────────────────────
   Breakpoints

   TTY has TWO layouts, not five:

     COMPACT   < 1280px   one pane at a time, menu button, 44px targets
     DESKTOP  >= 1280px   side-by-side panes, keyboard chrome, 24/32/40 density

   Tablets deliberately get COMPACT — a 1024px window running a five-pane TUI
   is a worse experience than a single pane with a pane bar.

   Components read these as custom properties, so inline styles stay responsive
   without media queries. Layout switches (single pane, hidden columns, menu)
   read the same edges through useBreakpoint().
   ───────────────────────────────────────────────────────────────────────── */

:root {
  /* named edges — min-width semantics */
  --bp-xs: 360px;   /* small phone      */
  --bp-sm: 430px;   /* large phone      */
  --bp-md: 768px;   /* tablet portrait  */
  --bp-lg: 1280px;  /* laptop — DESKTOP starts here */
  --bp-xl: 1440px;  /* desktop          */

  /* layout metrics, redefined per layout */
  --gutter: 28px;          /* page padding */
  --pane-gap: 10px;        /* gap between panes */
  --tap-min: 32px;         /* smallest interactive box */
  --app-header-h: 38px;
  --app-status-h: 26px;
  --panel-header-h: 30px;
  --panel-footer-h: 26px;
  --row-h: 30px;
  --row-h-dense: 24px;

  /* fluid rhythm — sections breathe less on a phone */
  --sp-section: clamp(40px, 6vw, 64px); /* @kind spacing */
  --sp-block: clamp(20px, 3vw, 34px); /* @kind spacing */
}

@media (max-width: 1279.98px) {
  :root {
    --gutter: 16px;
    --pane-gap: 8px;
    --tap-min: 44px;

    /* every control clears 44px; sm stays the compact variant, not a tiny one */
    --control-sm: 36px;
    --control-md: 44px;
    --control-lg: 48px;

    --app-header-h: 48px;
    --app-status-h: 34px;
    --panel-header-h: 40px;
    --panel-footer-h: 34px;
    --row-h: 44px;
    --row-h-dense: 40px;
  }
}

@media (max-width: 430px) {
  :root {
    --gutter: 14px;
    --pane-gap: 6px;
  }
}

/* A mouse in a narrow window does not need finger-sized rows, but it does need
   the compact layout — pull density back for fine pointers only. */
@media (max-width: 1279.98px) and (pointer: fine) {
  :root {
    --tap-min: 32px;
    --control-sm: 26px;
    --control-md: 34px;
    --control-lg: 40px;
    --row-h: 32px;
    --row-h-dense: 26px;
    --panel-header-h: 30px;
    --app-header-h: 40px;
  }
}

/* Panels and tables scroll their own overflow; the page never scrolls sideways. */
html, body { overflow-x: hidden; }

/* ── tty/tokens/base.css ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--surface-root);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: var(--leading-ui);
  font-variant-ligatures: none;
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--text-link);
  text-decoration: none;
  border-bottom: 1px solid var(--yellow-dim);
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
a:hover { color: var(--text-link-hover); border-bottom-color: var(--yellow); }
a:focus-visible { outline: none; box-shadow: var(--focus-ring); }

::selection { background: var(--yellow); color: var(--black-void); }

:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* pixel faces must never be smoothed */
.tty-display, h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: var(--tracking-display);
  text-transform: uppercase;
  -webkit-font-smoothing: none;
  font-smooth: never;
}

code, pre, kbd, samp { font-family: var(--font-code); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--black-100); }
::-webkit-scrollbar-thumb { background: var(--black-400); border: 2px solid var(--black-100); }
::-webkit-scrollbar-thumb:hover { background: var(--grey-500); }

/* ── tty/core.css ─────────────────────────────────────────── */
/* TTY core components — ported from components/core/, components/forms/Input.jsx,
   and components/navigation/Tabs.jsx + MenuList.jsx.
   The React originals carry inline styles; these are the same values as classes.

   Note on link resets: tokens/base.css gives every `a` a dim-yellow bottom border.
   Any component rendered as an `<a>` sets its own `border` and therefore overrides
   it on specificity. The explicit `:hover` rules below beat base.css's
   `a:hover { border-bottom-color: … }` because core.css is imported after it. */

.tty-spacer { flex: 1 1 auto; }

/* ── Button ─────────────────────────────────────────────────────────────── */
.tty-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: var(--control-md);
  padding: 0 14px;
  font: var(--weight-medium) var(--text-sm)/1 var(--font-body);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-primary);
  background: transparent;
  border: var(--border-w) solid var(--border-strong);
  border-radius: var(--radius-none);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.tty-btn:hover {
  color: var(--text-primary);
  background: var(--surface-hover);
  border-color: var(--border-strong);
}
/* Press nudges one pixel down-right; the key travels. */
.tty-btn:active { transform: translate(1px, 1px); }
.tty-btn[aria-disabled="true"],
.tty-btn:disabled {
  color: var(--text-faint);
  background: var(--surface-raised);
  border-color: var(--border-faint);
  cursor: not-allowed;
  transform: none;
}

.tty-btn--primary {
  color: var(--text-on-accent);
  background: var(--yellow);
  border-color: var(--yellow);
}
.tty-btn--primary:hover {
  color: var(--text-on-accent);
  background: var(--yellow-bright);
  border-color: var(--yellow-bright);
}

.tty-btn--ghost {
  color: var(--text-secondary);
  border-color: transparent;
}
.tty-btn--ghost:hover {
  color: var(--text-primary);
  background: var(--surface-raised);
  border-color: transparent;
}

.tty-btn--sm { height: var(--control-sm); padding: 0 10px; font-size: var(--text-xs); }
.tty-btn--lg { height: var(--control-lg); padding: 0 20px; font-size: var(--text-md); }
.tty-btn--block { display: flex; width: 100%; }

/* ── Tag ────────────────────────────────────────────────────────────────── */
.tty-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 22px;
  padding: 0 var(--sp-2);
  font: var(--text-xs)/1 var(--font-body);
  color: var(--text-secondary);
  background: var(--surface-raised);
  border: var(--border-w) solid var(--border-faint);
  border-radius: var(--radius-none);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.tty-tag::before { content: "#"; opacity: 0.5; }
.tty-tag:hover {
  color: var(--yellow);
  background: var(--yellow-wash);
  border-color: var(--border-faint);
}
.tty-tag__count { opacity: 0.6; }

.tty-tag--on {
  color: var(--text-on-accent);
  background: var(--yellow);
  border-color: var(--yellow);
}
.tty-tag--on:hover {
  color: var(--text-on-accent);
  background: var(--yellow-bright);
  border-color: var(--yellow-bright);
}

/* Deliberate deviation: the React Tag is a flat 22px. Under 1280px the target
   reads --tap-min, so a finger gets 44px and a mouse in a narrow window keeps
   32px. See the note at the head of Task 3. */
@media (max-width: 1279.98px) {
  .tty-tag { height: var(--tap-min); padding: 0 10px; font-size: var(--text-sm); }
}

/* ── Badge ──────────────────────────────────────────────────────────────── */
.tty-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 18px;
  padding: 0 6px;
  font: var(--weight-medium) var(--text-xs)/1 var(--font-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-on-accent);
  background: var(--grey-700);
  border: var(--border-w) solid var(--grey-700);
  border-radius: var(--radius-none);
  white-space: nowrap;
}
.tty-badge--outline { color: var(--grey-700); background: transparent; }

.tty-badge--accent { background: var(--yellow); border-color: var(--yellow); }
.tty-badge--accent.tty-badge--outline { color: var(--yellow); background: transparent; }
.tty-badge--ok { background: var(--status-ok); border-color: var(--status-ok); }
.tty-badge--ok.tty-badge--outline { color: var(--status-ok); background: transparent; }
.tty-badge--info { background: var(--status-info); border-color: var(--status-info); }
.tty-badge--info.tty-badge--outline { color: var(--status-info); background: transparent; }
.tty-badge--warn { background: var(--status-warn); border-color: var(--status-warn); }
.tty-badge--warn.tty-badge--outline { color: var(--status-warn); background: transparent; }
.tty-badge--error { background: var(--status-error); border-color: var(--status-error); }
.tty-badge--error.tty-badge--outline { color: var(--status-error); background: transparent; }
.tty-badge--special { background: var(--status-special); border-color: var(--status-special); }
.tty-badge--special.tty-badge--outline { color: var(--status-special); background: transparent; }

/* ── KeyHint ────────────────────────────────────────────────────────────── */
.tty-keyhint { display: inline-flex; align-items: center; gap: 6px; }
.tty-keyhint__keys { display: inline-flex; gap: 3px; }
/* `.tty-kbd` is the same key cap, for a key that sits inside a sentence rather
   than in a keyhint's key group — the "press / anywhere" hint on /posts/. One
   rule serves both, so a key never renders two ways on the same site. */
.tty-keyhint kbd,
.tty-kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  font: var(--text-xs)/1 var(--font-body);
  color: var(--text-secondary);
  background: var(--surface-raised);
  border: var(--border-w) solid var(--border-default);
  border-radius: var(--radius-none);
}
.tty-keyhint__label { font-size: var(--text-xs); color: var(--text-faint); }
.tty-keyhint--accent kbd {
  color: var(--text-on-accent);
  background: var(--yellow);
  border-color: var(--yellow);
}

/* ── Divider ────────────────────────────────────────────────────────────── */
.tty-rule {
  border: 0;
  border-top: var(--border-w) solid var(--border-faint);
  margin: 0;
}
.tty-divider { display: flex; align-items: center; gap: 10px; }
.tty-divider__label {
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-faint);
  -webkit-font-smoothing: none;
  font-smooth: never;
}
.tty-divider__line {
  flex: 1;
  border-top: var(--border-w) solid var(--border-faint);
}

/* ── Panel ──────────────────────────────────────────────────────────────── */
.tty-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--surface-panel);
  border: var(--border-w) solid var(--border-default);
  border-radius: var(--radius-none);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.tty-panel__head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
  min-height: var(--panel-header-h);
  padding: 0 10px;
  border-bottom: var(--border-w) solid var(--border-default);
  background: var(--surface-raised);
}
.tty-panel__title {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
  -webkit-font-smoothing: none;
  font-smooth: never;
}
.tty-panel__sub { font-size: var(--text-xs); color: var(--text-faint); }
.tty-panel__body { flex: 1; min-height: 0; padding: var(--sp-4); }
.tty-panel__foot {
  flex: none;
  min-height: var(--panel-footer-h);
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 4px 10px;
  border-top: var(--border-w) solid var(--border-faint);
  font-size: var(--text-xs);
  color: var(--text-faint);
}
.tty-panel--accent { border-color: var(--yellow); }
.tty-panel--accent .tty-panel__head {
  border-bottom-color: var(--yellow);
  background: var(--yellow-wash);
}
.tty-panel--accent .tty-panel__title { color: var(--yellow); }
.tty-panel--flush .tty-panel__body { padding: 0; }

/* ── Tabs (underline variant) ───────────────────────────────────────────── */
.tty-tabs {
  display: flex;
  align-items: stretch;
  gap: 0;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  border-bottom: var(--border-w) solid var(--border-default);
}
.tty-tabs::-webkit-scrollbar { display: none; }
.tty-tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: none;
  min-height: var(--tap-min);
  padding: 0 12px;
  font: var(--text-xs)/1 var(--font-body);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  background: transparent;
  border: 0;
  border-bottom: var(--border-w-thick) solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.tty-tabs__tab:hover {
  color: var(--text-primary);
  border-bottom-color: var(--border-strong);
}
/* `aria-current="page"` is the only state a tab in this site carries: the strip
   is a set of links in nav.njk, never a widget. The ported design system also
   styled `[aria-selected="true"]` (a real tab widget) and `[aria-pressed="true"]`
   (a toggle button); no markup in this repository ever set either, and the one
   caller that did — the archive page's sort toggle — is gone with the archive.
   Add a selector back here when markup needs it, not before. */
.tty-tabs__tab[aria-current="page"] {
  color: var(--yellow);
  border-bottom-color: var(--yellow);
}
/* .tty-tabs is a scroll container: `overflow-x: auto` makes `overflow-y` compute
   to `auto` too, so the outset `--focus-ring` box-shadow base.css puts on a
   focused tab is clipped at the top and bottom edges of the strip, and
   `scrollbar-width: none` hides every sign of it. Draw the ring inside the tab
   instead. A negative `outline-offset` paints the outline within the border box,
   where no scroll container can cut it, and the strip still scrolls sideways. */
.tty-tabs__tab:focus-visible {
  box-shadow: none;
  outline: var(--border-w-thick) solid var(--yellow);
  outline-offset: calc(var(--border-w-thick) * -1);
}

/* ── Input ──────────────────────────────────────────────────────────────── */
.tty-input {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--control-md);
  padding: 0 10px;
  background: var(--surface-inset);
  border: var(--border-w) solid var(--border-default);
  border-radius: var(--radius-none);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.tty-input:focus-within {
  border-color: var(--yellow);
  box-shadow: 0 0 0 1px var(--yellow-wash-strong);
}
.tty-input--lg { height: var(--control-lg); }
.tty-input__prefix {
  flex: none;
  font-family: var(--font-glyph);
  font-size: var(--text-sm);
  color: var(--text-faint);
}
.tty-input:focus-within .tty-input__prefix { color: var(--yellow); }
.tty-input input {
  flex: 1;
  min-width: 0;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  outline: none;
  background: transparent;
  color: var(--text-primary);
  font: var(--text-sm)/1 var(--font-body);
}
.tty-input input::placeholder { color: var(--text-faint); opacity: 1; }
/* The wrapper owns the focus language, so the inner field must not draw a ring
   of its own on top of it. */
.tty-input input:focus-visible { box-shadow: none; }
.tty-input input[type="search"]::-webkit-search-cancel-button { display: none; }

/* ── MenuList ───────────────────────────────────────────────────────────── */
.tty-menu { list-style: none; margin: 0; padding: 0; }
.tty-menu__item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  min-height: var(--row-h);
  padding: 0 10px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  background: transparent;
  border: 0;
  border-radius: var(--radius-none);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
/* Dense is a desktop density. `MenuList` is used as `dense={!bp.compact}` in both
   Archive.jsx and About.jsx, so the modifier must not reach a compact screen. The
   media query here is what enforces that, so a template can carry
   `tty-menu--dense` at every width — which is what the by-year rail on /posts/
   and the homepage rail both do. Under 1280px the row falls back to `--row-h`:
   44px for a finger, 32px for a fine pointer. */
@media (min-width: 1280px) {
  .tty-menu--dense .tty-menu__item { min-height: var(--row-h-dense); }
}
.tty-menu__item:hover { color: var(--text-primary); background: var(--surface-hover); }
/* A focused row draws its ring inside its own box. `.tty-search__panel` is
   `overflow: auto`, and the outset `--focus-ring` box-shadow is ink overflow,
   which a scroll container clips — the same defect as the tab strip above. An
   outline with a negative offset paints inside the border box, where nothing can
   cut it. A selected row is a yellow bar, so its ring turns black to stay
   visible. */
.tty-menu__item:focus-visible {
  box-shadow: none;
  outline: var(--border-w-thick) solid var(--yellow);
  outline-offset: calc(var(--border-w-thick) * -1);
}
.tty-menu__item[aria-selected="true"]:focus-visible,
.tty-menu__item.is-on:focus-visible { outline-color: var(--black-void); }
/* Selection is a full-width yellow bar with black text — the TUI list idiom. */
.tty-menu__item[aria-selected="true"],
.tty-menu__item.is-on {
  color: var(--text-on-accent);
  background: var(--yellow);
}
.tty-menu__cursor {
  flex: none;
  width: 8px;
  font-family: var(--font-glyph);
  opacity: 0;
}
.tty-menu__item:hover .tty-menu__cursor { opacity: 0.5; }
.tty-menu__item[aria-selected="true"] .tty-menu__cursor,
.tty-menu__item.is-on .tty-menu__cursor { opacity: 1; }
.tty-menu__glyph {
  flex: none;
  width: 14px;
  font-family: var(--font-glyph);
  opacity: 0.7;
}
.tty-menu__label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tty-menu__hint { flex: none; font-size: var(--text-xs); opacity: 0.45; }
.tty-menu__item[aria-selected="true"] .tty-menu__hint,
.tty-menu__item.is-on .tty-menu__hint { opacity: 0.7; }
.tty-menu__section {
  padding: 10px 10px 4px;
  font-family: var(--font-display);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-faint);
  -webkit-font-smoothing: none;
  font-smooth: never;
}

/* ── tty/content.css ─────────────────────────────────────────── */
/* TTY content components — ported from components/content/ and
   components/motion/CursorBlink.jsx.

   PostCard becomes one `<a class="tty-postrow">` so the whole row is a single
   link with a single tab stop. The React version nests no links either: tags
   inside a row are plain text, and the linked tags live in the filter rail. */

/* ── PostCard, row variant ──────────────────────────────────────────────── */
.tty-postrow {
  display: flex;
  gap: 18px;
  padding: 14px;
  color: inherit;
  background: transparent;
  text-decoration: none;
  border: 0;
  border-top: var(--border-w) solid var(--border-faint);
  border-left: var(--border-w-thick) solid transparent;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.tty-postrow:hover {
  background: var(--surface-raised);
  border-left-color: var(--yellow);
}
.tty-postrow__index {
  flex: none;
  width: 34px;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  color: var(--text-faint);
  -webkit-font-smoothing: none;
  font-smooth: never;
  transition: color var(--dur-fast) var(--ease-out);
}
.tty-postrow:hover .tty-postrow__index { color: var(--yellow); }
.tty-postrow__body { flex: 1; min-width: 0; }
.tty-postrow__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 17px;
  line-height: var(--leading-tight);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-primary);
  -webkit-font-smoothing: none;
  font-smooth: never;
  transition: color var(--dur-fast) var(--ease-out);
}
.tty-postrow:hover .tty-postrow__title { color: var(--yellow); }
/* The post's own description, and the only prose on /posts/ and on a tag page.
   It was 13px --text-muted: the smallest size on the site, in the grey reserved
   for dates and index numbers. It is body copy, so it reads as body copy. */
.tty-postrow__excerpt {
  margin: 7px 0 0;
  font-size: var(--text-md);
  line-height: 1.6;
  color: var(--text-secondary);
  text-wrap: pretty;
}
.tty-postrow__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 10px;
  font-size: var(--text-xs);
  color: var(--text-faint);
}
.tty-postrow__meta {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  font-size: var(--text-xs);
  color: var(--text-faint);
  white-space: nowrap;
}

/* On a phone the meta column squeezes the title, so it drops under the body. */
@media (max-width: 430px) {
  .tty-postrow { flex-wrap: wrap; gap: 10px; }
  .tty-postrow__meta {
    flex-direction: row;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding-left: 34px;
  }
}

/* ── TerminalWindow ─────────────────────────────────────────────────────── */
.tty-term {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface-inset);
  border: var(--border-w) solid var(--border-default);
  border-radius: var(--radius-none);
  box-shadow: var(--shadow-hard);
  overflow: hidden;
}
/* The CRT suggestion sits above the transcript and below the pointer. */
.tty-term::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--texture-scanlines);
}
.tty-term__bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  height: 28px;
  padding: 0 10px;
  background: var(--surface-raised);
  border-bottom: var(--border-w) solid var(--border-default);
}
.tty-term__tab {
  padding: 2px var(--sp-2);
  font-size: var(--text-xs);
  color: var(--yellow);
  background: var(--black-void);
  border: var(--border-w) solid var(--border-default);
}
.tty-term__dots { display: flex; gap: 5px; }
.tty-term__dot { width: 8px; height: 8px; }
.tty-term__dot--r { background: var(--red); }
.tty-term__dot--y { background: var(--yellow); }
.tty-term__dot--g { background: var(--green); }
.tty-term__name { flex: 1; font-size: var(--text-xs); color: var(--text-faint); }
.tty-term__name--center { text-align: center; }
.tty-term__body {
  flex: 1;
  min-height: 0;
  padding: var(--sp-4);
  font-family: var(--font-code);
  font-size: var(--text-sm);
  line-height: 1.65;
  overflow: auto;
}
.tty-term__line { display: flex; gap: 12px; }
.tty-term__prompt { color: var(--green); }
.tty-term__out { color: var(--text-muted); }
.tty-term__path { color: var(--cyan); }
.tty-term__date { color: var(--text-faint); }

/* ── CursorBlink ────────────────────────────────────────────────────────── */
.tty-caret {
  display: inline-block;
  width: 0.55em;
  height: 1em;
  vertical-align: -0.13em;
  background: var(--yellow);
  animation: tty-blink var(--blink-rate) var(--ease-step-2) infinite;
}
.tty-caret--bar { width: 2px; }

/* ── Foldout, as a native <details> ─────────────────────────────────────── */
.tty-foldout { border-top: var(--border-w) solid var(--border-faint); }
.tty-foldout__summary {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: var(--tap-min);
  padding: 0 2px;
  font: var(--text-xs)/1 var(--font-display);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  -webkit-font-smoothing: none;
  font-smooth: never;
  cursor: pointer;
  list-style: none;
}
.tty-foldout__summary::-webkit-details-marker { display: none; }
.tty-foldout__summary::before { content: "▸"; font-family: var(--font-glyph); }
.tty-foldout[open] > .tty-foldout__summary { color: var(--yellow); }
.tty-foldout[open] > .tty-foldout__summary::before { content: "▾"; }
.tty-foldout__hint { color: var(--text-faint); }
.tty-foldout__body {
  padding: 4px 0 16px;
  animation: tty-wipe-up var(--dur-fast) var(--ease-step-4);
}

/* ── Prose wrapper ──────────────────────────────────────────────────────────
   The reading size and the reading ink both come from here, and both were
   raised: --text-lg tops out at 17px, which is a UI size, and --text-secondary
   is #B4B4AC, 9.5:1 on the root surface — legible, but visibly grey against a
   near-black page. Every post body and the whole biography are set in this one
   rule, so this is where "bigger and brighter" lands.

   --tty-prose-size is declared in layout.css, next to --tty-nav-h: the post
   column in that file must read the same value, or `68ch` resolves to a
   different width there than the text is actually set in. --text-primary is
   #E8E8E3, 16.1:1 on the root surface. */
.tty-prose {
  max-width: var(--measure-prose);
  font-family: var(--font-body);
  font-size: var(--tty-prose-size);
  line-height: var(--leading-body);
  color: var(--text-primary);
  text-wrap: pretty;
}
.tty-prose--wide { max-width: var(--measure-wide); }
/* One step down, for a standfirst or a page description — not for a post body. */
.tty-prose--md { font-size: var(--text-lg); }

/* ── Figures break out of the reading measure ───────────────────────────────
   A plot is not body copy. `--measure-prose` is 68ch, which is about 694px at
   `--text-lg`. Three posts carry 15 figures, and twelve of them are multi-panel
   `kc-*` plots up to 2101x479px: inside 694px each panel is about 170px wide and
   the axis labels, firm names and legends stop being readable.

   So a figure grows past the text column: to `--measure-wide` (92ch, about
   938px) from 1280px up, and to the page column (`--page-max` less the two
   gutters, 1184px) from 1440px up, where there is room for the whole plot.

   Three selectors, and no post is edited:
     1. `.tty-figure` — the class a new post may use.
     2. `div[style*="justify-content: center"]` — the raw markup the 15 existing
        figures already carry.
     3. `p:has(> img:only-child)` — what markdown-it emits for `![alt](src)`,
        which is the only image syntax the Markdown author needs.

   `position: relative` plus a negative `left` does the offset, not a margin.
   The post column is centred in the page (`.tty-post` in Task 8), so pulling the
   box left by half the extra width centres the figure in the page, and Task 17's
   `margin: var(--sp-8) 0 0` on the same figures keeps working — the two rules set
   different properties and cannot fight. The caption below stays at the reading
   measure, because a centred caption 1184px wide reads worse, not better. */
@media (min-width: 1280px) {
  .tty-prose > .tty-figure,
  .tty-prose > div[style*="justify-content: center"],
  .tty-prose > p:has(> img:only-child) {
    position: relative;
    left: calc((var(--measure-prose) - var(--measure-wide)) / 2);
    width: var(--measure-wide);
    /* A guard, so a wider measure token can never overflow the page column. */
    max-width: calc(var(--page-max) - 2 * var(--gutter));
  }
}
@media (min-width: 1440px) {
  .tty-prose > .tty-figure,
  .tty-prose > div[style*="justify-content: center"],
  .tty-prose > p:has(> img:only-child) {
    left: calc((var(--measure-prose) - var(--page-max) + 2 * var(--gutter)) / 2);
    width: calc(var(--page-max) - 2 * var(--gutter));
  }
}

/* ── tty/layout.css ─────────────────────────────────────────── */
/* TTY page shell — ported from ui_kits/blog/Site.jsx plus the screen grids the
   Home, list and About screens share. The search overlay lives here too,
   because it is shell furniture, not a component. */

:root {
  /* Two glyphs outside the latin subset: the list markers in prose.css and the
     search glyph on the nav control below. tokens/fonts.css loads only the latin
     subset of JetBrains Mono, so `--font-glyph` alone hands them to an unknown
     system face at an unknown advance and cap height. No web font is added: the
     output stays static files with no new runtime dependency. This stack names
     the faces that do carry the blocks, and every caller gives the glyph a fixed
     one-character cell so a fallback advance cannot push it off the grid. */
  --font-marker: "JetBrains Mono", "DejaVu Sans Mono", "Noto Sans Symbols 2",
    "Segoe UI Symbol", "Apple Symbols", ui-monospace, monospace;

  /* --text-faint, raised to clear WCAG AA. The vendored value is --grey-600
     (#5E5E58), which is 3.04:1 on --surface-root: enough for large text only, and
     every one of its 28 usages is 12-15px metadata. Dates, index numbers, key caps
     and hint labels are all information, not decoration, so 3:1 is not defensible.

     #7A7A71 is 4.57:1 — over the 4.5:1 AA threshold for normal text, and still
     clearly fainter than --text-muted at 5.85:1, so the four-step grey hierarchy
     survives. One override fixes all 28 call sites at once; tokens/colors.css is
     vendored byte-for-byte and must not be edited, and layout.css is imported
     after it, so this wins the cascade.

     Do not pair this token with opacity. An opacity below 1 composites toward the
     background and undoes the fix: even --text-muted at opacity .8 lands at
     4.09:1. .tty-foot__row--quiet used to do exactly that. */
  --text-faint: #7A7A71;

  /* The long-form reading size. The vendored --text-lg tops out at 17px, which
     is a UI size, not a reading size: the biography and every post body were
     hard to read at it. This is the one size the prose wrapper and the post
     column both use, so `68ch` resolves to the same width in both — see the
     note on .tty-post__body below. 18px on a 360px phone, 20px from 1440px up,
     fluid between, in the same shape as the vendored type scale. */
  --tty-prose-size: clamp(18px, calc(0.185vw + 17.33px), 20px);

  /* 52px was hard-coded at four sites (.tty-nav's real height, the sticky
     rail's `top`, and two `scroll-margin-top` offsets whose only job is to
     clear the nav) plus a fifth in scripts/check-build.sh. A future height
     change that missed one would make an in-page anchor land with its target
     hidden under the pinned nav — silent and hard to attribute. One token,
     used at all four sites, fixes that.

     This is intentionally NOT the vendored --app-header-h (38/48/40px across
     breakpoints.css's tiers, see tokens/breakpoints.css) — this blog's nav is
     one height at every width, unlike the design system's own header, so a
     tier-varying token would be the wrong value here. */
  --tty-nav-h: 52px;
}

html { height: 100%; }

/* tokens/breakpoints.css ends with `html, body { overflow-x: hidden; }`. That line
   is why the page never scrolls sideways, and it must keep doing that job: the
   rule stays on <html>, where it propagates to the viewport.
   On <body> it does something else as well. Per CSS Overflow 3, a `visible` value
   beside a value that is neither `visible` nor `clip` computes to `auto`, so
   body's unset `overflow-y` becomes `auto` and <body> becomes a scroll container.
   A sticky box is offset against its nearest scroll container, and body's
   scrollport never scrolls, because body's height is auto. Every
   `position: sticky` under <body> therefore stops working: `.tty-nav` and
   `.tty-rail--sticky` below, and with them the `scroll-margin-top` offsets in
   Task 6 and Task 11, which exist only to clear a sticky nav.
   `clip` is the one value that pairs with `visible` and is left alone, so it
   clips the sideways overflow without creating a scrollport. The token files are
   vendored verbatim and must not be edited, so the override lands here;
   layout.css is imported after tokens/breakpoints.css and wins on equal
   specificity. An engine that does not know `overflow-x: clip` drops this line
   and keeps today's behaviour. */
body { overflow-x: clip; }

.tty-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--surface-root);
}

/* ── Sticky nav ─────────────────────────────────────────────────────────── */
.tty-nav {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 16px;
  height: var(--tty-nav-h);
  padding: 0 var(--gutter);
  background: var(--surface-root);
  border-bottom: var(--border-w) solid var(--border-faint);
}
.tty-nav__mark {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 1px;
  font-family: var(--font-display);
  font-size: 18px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--yellow);
  -webkit-font-smoothing: none;
  font-smooth: never;
  text-decoration: none;
  border: 0;
}
.tty-nav__mark:hover { color: var(--yellow-bright); }
.tty-nav__caret { background: var(--text-primary); }
.tty-nav > .tty-tabs { border-bottom: 0; align-self: stretch; }
.tty-nav__search {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  padding: 0 4px;
  background: transparent;
  border: 0;
  cursor: pointer;
}
/* U+2315, in the fallback stack --font-marker names, on a fixed one-character
   cell so a fallback advance cannot shift the key hint beside it. The glyph is
   the design system's icon idiom: a Unicode character, never an SVG, never an
   emoji. It tracks the key hint's own colour, so hover lights the whole control
   rather than half of it. */
.tty-nav__search-glyph {
  flex: none;
  width: 1ch;
  font-family: var(--font-marker);
  font-size: 18px;
  line-height: 1;
  text-align: center;
  color: var(--text-muted);
  transition: color var(--dur-fast) var(--ease-out);
}
.tty-nav__search:hover .tty-nav__search-glyph,
.tty-nav__search:focus-visible .tty-nav__search-glyph { color: var(--yellow); }

/* The wordmark alone is 150px at 18px Silkscreen; on a small phone the tab
   strip needs the rest of the row, so the mark steps down instead of wrapping.
   The `/` hint goes with it: there is no `/` key to press on a touch keyboard,
   and the glyph alone still reads as search. The button keeps its aria-label,
   so its accessible name does not change. */
@media (max-width: 430px) {
  .tty-nav { gap: 10px; }
  .tty-nav__mark { font-size: var(--display-xs); letter-spacing: 0.08em; }
  .tty-nav__search .tty-keyhint { display: none; }
}

/* ── Main column ────────────────────────────────────────────────────────── */
.tty-main {
  flex: 1;
  width: 100%;
  max-width: var(--page-max);
  margin: 0 auto;
  padding: var(--sp-section) var(--gutter) calc(var(--sp-section) + 8px);
}

/* A screen is a stack of sections at the section rhythm. */
.tty-screen { display: flex; flex-direction: column; gap: var(--sp-section); }
.tty-section { display: flex; flex-direction: column; gap: var(--sp-block); }

/* ── Display type ───────────────────────────────────────────────────────── */
.tty-eyebrow {
  font-size: var(--text-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.tty-h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--display-md);
  line-height: var(--leading-tight);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-primary);
  -webkit-font-smoothing: none;
  font-smooth: never;
}
/* The hero headline is one string, not one string per line. `max-width` in `ch`
   caps the measure against the headline's own font size, so it holds at every
   step of the --display-xl clamp; `balance` then spreads the words evenly over
   however many lines that measure allows. Together they replace the three
   hand-split `heroLine` fields and their hard `<br>` tags.
   `balance` is capped at a few lines by every engine that ships it, which is
   the right ceiling for a headline. Where it is unsupported the text still
   wraps at the same measure — only the line lengths are less even. */
.tty-h1--hero {
  max-width: 24ch;
  font-size: var(--display-xl);
  line-height: 1.05;
  letter-spacing: 0.04em;
  text-wrap: balance;
}
.tty-h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--display-md);
  line-height: var(--leading-tight);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--yellow);
  -webkit-font-smoothing: none;
  font-smooth: never;
}

/* ── Screen grids ───────────────────────────────────────────────────────── */
.tty-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 28px; }
.tty-split--rail { align-items: start; }
.tty-split--rail-wide { align-items: start; }

@media (min-width: 1280px) {
  .tty-split { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 48px; align-items: center; }
  .tty-split--rail { grid-template-columns: minmax(0, 1fr) 220px; gap: 48px; align-items: start; }
  .tty-split--rail-wide { grid-template-columns: minmax(0, 1fr) 280px; gap: 48px; align-items: start; }
}

.tty-rail { display: flex; flex-direction: column; gap: 20px; }
@media (min-width: 1280px) {
  .tty-rail--sticky { position: sticky; top: calc(var(--tty-nav-h) + var(--sp-6)); }
}

/* Two layouts, not five: the rail is an aside on desktop and a <details> under
   1280px. Both markups render; CSS picks one. The source stays DRY because both
   call the same macro. */
.tty-when-desktop { display: none; }
.tty-when-compact { display: block; }
@media (min-width: 1280px) {
  .tty-when-desktop { display: block; }
  .tty-when-compact { display: none; }
}

/* ── Key/value link list ("Elsewhere") ──────────────────────────────────── */
.tty-linklist { display: flex; flex-direction: column; gap: var(--sp-2); font-size: var(--text-sm); }
.tty-linklist__row { display: flex; align-items: center; gap: 16px; min-height: var(--tap-min); }
.tty-linklist__key {
  flex: none;
  width: 72px;
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ── Footer ─────────────────────────────────────────────────────────────── */
.tty-foot {
  border-top: var(--border-w) solid var(--border-faint);
  background: var(--surface-panel);
}
.tty-foot__inner {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 22px var(--gutter);
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.tty-foot__row {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  font-size: var(--text-xs);
  color: var(--text-faint);
}
/* No opacity here. It used to carry `opacity: 0.8`, which composited the text
   toward the background and dropped it to 3.32:1 even with the raised token.
   The colour already makes the row quiet; the opacity only made it unreadable. */
.tty-foot__row--quiet { color: var(--text-faint); }
.tty-foot__cmd { font-family: var(--font-code); }
.tty-foot__links { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tty-foot__meta { display: flex; align-items: center; gap: 6px; }
.tty-foot__mark {
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  -webkit-font-smoothing: none;
  font-smooth: never;
}

/* ── Search overlay ─────────────────────────────────────────────────────── */
.tty-search[hidden] { display: none; }
.tty-search {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  justify-content: center;
  padding: clamp(16px, 12vh, 120px) var(--gutter) 0;
}
.tty-search__scrim { position: absolute; inset: 0; background: var(--surface-scrim); }
.tty-search__panel {
  position: relative;
  width: 620px;
  max-width: 100%;
  height: fit-content;
  max-height: 80vh;
  overflow: auto;
  background: var(--surface-panel);
  border: var(--border-w) solid var(--yellow);
  box-shadow: var(--shadow-hard);
  animation: tty-wipe-up var(--dur-base) var(--ease-step-8);
}
.tty-search__field { padding: 10px; border-bottom: var(--border-w) solid var(--border-faint); }
.tty-search__foot {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 8px 12px;
  border-top: var(--border-w) solid var(--border-faint);
  font-size: var(--text-xs);
  color: var(--text-faint);
}
.tty-search__empty { padding: 14px; font-size: var(--text-sm); color: var(--text-faint); }

/* While the overlay is open the page behind it must not scroll.
   The lock has to land on <html>, not on <body>. tokens/breakpoints.css gives
   <html> a non-visible `overflow-x`, so the viewport takes its overflow from
   <html>; body's overflow no longer propagates, and `overflow: hidden` on a body
   whose height is auto clips nothing at all.
   `hidden` is what locks it, and `clip` must not be used here: `hidden` keeps the
   scrollport, so the scroll offset survives and the page is exactly where the
   reader left it when the overlay closes, while `clip` removes the scrollport and
   the page jumps back to the top.
   `:has()` keeps the DOM contract the script declares — one class, on <body>.
   `html.tty-search-open` is listed as well, so the lock still holds if the script
   sets the class on <html> too.
   `scrollbar-gutter: stable` reserves the scrollbar space at all times, so the
   page does not shift sideways the moment the lock removes the scrollbar. */
html { scrollbar-gutter: stable; }
html:has(body.tty-search-open),
html.tty-search-open { overflow: hidden; }
/* Do NOT add `body.tty-search-open { overflow: hidden; }` here. `overflow` is
   the shorthand for both axes, so it would set body's `overflow-x` to
   `hidden`, silently overriding the `body { overflow-x: clip }` rule near the
   top of this file — higher specificity, later in source — and unlike `clip`,
   `hidden` makes body a scroll container. tty-search.js sets this class on
   <html> too, and the two rules above already lock scrolling from there; body
   only needs to carry the class so `html:has(body.tty-search-open)` above has
   something to match — it must not carry its own overflow rule. */

/* ── List screen (ui_kits/blog/Archive.jsx) ─────────────────────────────────
   Three pages share these blocks: /posts/, every /posts/tag/<slug>/ page and
   /preprints/. The name says `list`, not `archive`: there is no archive view —
   /posts/ is the one complete record of every post. */
.tty-list__head {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: var(--sp-6);
}
.tty-list__count { font-size: var(--text-xs); color: var(--text-faint); }

.tty-list__years { display: flex; flex-direction: column; gap: var(--sp-8); }
/* The by-year rail jumps to these sections, so a year heading must clear the
   sticky nav (--tty-nav-h). Same arithmetic as the prose headings in
   prose.css — keep the two in step. */
.tty-list__year {
  display: flex;
  flex-direction: column;
  scroll-margin-top: calc(var(--tty-nav-h) + var(--sp-4));
}
.tty-list__year .tty-divider { margin-bottom: 12px; }
.tty-list__rows { display: flex; flex-direction: column; }

/* ── Post screen (ui_kits/blog/Post.jsx) ────────────────────────────────── */
.tty-post { display: flex; flex-direction: column; align-items: center; }
/* 68ch must be counted in the reading face, at the size the prose is actually
   set in. The head, the body and the foot all carry --tty-prose-size — the same
   size .tty-prose uses — so the three columns come out at the same width, and
   that width really is 68 characters of body copy. Every child sets its own
   font-size, so nothing else grows. */
.tty-post__head,
.tty-post__body,
.tty-post__foot { width: 100%; max-width: var(--measure-prose); font-size: var(--tty-prose-size); }
/* One measure, not two: .tty-prose must not constrain this column again. */
.tty-post__body .tty-prose { max-width: none; }

.tty-post__meta {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  font-size: var(--text-xs);
  color: var(--text-faint);
}
.tty-post__back { color: var(--text-muted); border-bottom-color: var(--border-default); }
.tty-post__back:hover { color: var(--yellow); }
/* The one live number on a reading page. It ships empty, so it holds its own
   width and the meta row does not reflow when the script fills it. */
.tty-post__progress {
  min-width: 4ch;
  text-align: right;
  color: var(--yellow);
  font-variant-numeric: tabular-nums;
}

.tty-post__head .tty-h1 {
  margin: 20px 0 0;
  font-size: var(--display-lg);
  line-height: 1.15;
  letter-spacing: 0.04em;
}
.tty-post__tags { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 18px; }
.tty-post__head .tty-rule { margin: 26px 0 30px; }

.tty-post__foot { margin-top: var(--sp-12); }
.tty-post__foot .tty-rule { margin-bottom: 20px; }
.tty-post__foot .tty-post__meta { gap: 16px; }
.tty-post__next {
  font-family: var(--font-display);
  font-size: var(--display-xs);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--yellow);
  -webkit-font-smoothing: none;
  font-smooth: never;
  border-bottom-color: var(--yellow-dim);
}
.tty-post__next:hover { color: var(--yellow-bright); }

/* ── Note banner (archived post, machine-translated post) ───────────────── */
.tty-note {
  margin: 0 0 var(--sp-8);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-panel);
  border: var(--border-w) solid var(--border-default);
  border-left: var(--border-w-thick) solid var(--border-strong);
}
.tty-note--warn { border-left-color: var(--status-warn); }
.tty-note__title {
  display: block;
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--status-warn);
  -webkit-font-smoothing: none;
  font-smooth: never;
}
.tty-note__body {
  margin: var(--sp-2) 0 0;
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-muted);
}

/* ── Hero (ui_kits/blog/Home.jsx) ───────────────────────────────────────── */
/* There was a `.tty-hero__lead` here. Its i18n string was the empty string, so
   the hero shipped an empty <p> that contributed nothing but its own margins.
   The rule and the element are both gone. To bring a sub-headline back, add the
   string, the <p> and this rule together — never the element alone. */
.tty-hero__actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 26px;
}
.tty-hero__title { margin: 18px 0 0; }

/* ── Paper row (/preprints/) ─────────────────────────────────────────────── */
/* The same shape as .tty-postrow, but the meta column carries a venue and the
   row leaves the site, so it ends in an outbound marker. */
.tty-paper {
  display: flex;
  gap: 18px;
  padding: 14px;
  color: inherit;
  background: transparent;
  text-decoration: none;
  border: 0;
  border-top: var(--border-w) solid var(--border-faint);
  border-left: var(--border-w-thick) solid transparent;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.tty-paper:hover { background: var(--surface-raised); border-left-color: var(--yellow); }
.tty-paper__index {
  flex: none;
  width: 34px;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  color: var(--text-faint);
  -webkit-font-smoothing: none;
  font-smooth: never;
  transition: color var(--dur-fast) var(--ease-out);
}
.tty-paper:hover .tty-paper__index { color: var(--yellow); }
.tty-paper__body { flex: 1; min-width: 0; }
.tty-paper__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 17px;
  line-height: var(--leading-tight);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-primary);
  -webkit-font-smoothing: none;
  font-smooth: never;
  transition: color var(--dur-fast) var(--ease-out);
}
.tty-paper:hover .tty-paper__title { color: var(--yellow); }
.tty-paper__authors {
  margin: 7px 0 0;
  font-size: var(--text-md);
  line-height: 1.6;
  color: var(--text-secondary);
  text-wrap: pretty;
}
/* Where the paper was published. It is the row's second most important fact
   after the title, so it gets the label treatment the design system uses for
   real metadata — uppercase, tracked out — rather than being set as more body
   copy. The host (the conference a workshop runs inside) is subordinate, so it
   steps down one level of ink. */
.tty-paper__venue {
  margin: 8px 0 0;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.tty-paper__host { color: var(--text-muted); }
.tty-paper__meta {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  font-size: var(--text-xs);
  color: var(--text-faint);
  white-space: nowrap;
}
.tty-paper__ext { font-family: var(--font-glyph); }

@media (max-width: 430px) {
  .tty-paper { flex-wrap: wrap; gap: 10px; }
  .tty-paper__meta {
    flex-direction: row;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding-left: 34px;
  }
}

/* ── tty/prose.css ─────────────────────────────────────────── */
/* Long-form reading inside .tty-prose.

   The design system Post screen sets h1 and h2 in Silkscreen and leaves the
   body plain. Three posts open sections with `# ` — 22 headings in all — and the
   posts nest headings four deep. So h1 and h2 both keep Silkscreen in yellow,
   h1 one step up the display scale, and h3 to h6 step down into JetBrains Mono.
   tokens/base.css puts every h1-h6 in the pixel face and uppercases them, so
   each of those rules has to switch the family and turn smoothing back on, and
   one more rule has to put the case back for inline code inside a heading.

   The system says there is no imagery, by design. That rule is about
   decoration. These posts carry real figures — screenshots and plots — so a
   figure gets a 1px rule and nothing else. */

.tty-prose > :first-child { margin-top: 0; }
.tty-prose > :last-child { margin-bottom: 0; }

.tty-prose p { margin: 0 0 var(--sp-6); }
.tty-prose strong { color: var(--text-primary); font-weight: var(--weight-bold); }
.tty-prose em { font-style: italic; }

/* ── Headings ───────────────────────────────────────────────────────────── */
.tty-prose h1,
.tty-prose h2,
.tty-prose h3,
.tty-prose h4,
.tty-prose h5,
.tty-prose h6 {
  /* --tty-nav-h is defined in layout.css, not the vendored token tree — see
     the comment there. Keep this in step with .tty-list__year's own
     scroll-margin-top in layout.css. */
  scroll-margin-top: calc(var(--tty-nav-h) + var(--sp-4));
}

/* Three posts use `# ` for their section headings. With no rule here those fall
   through to tokens/base.css and the UA default: 2em grey Silkscreen, which is
   both larger and dimmer than the h2 under it. h1 gets the top of the display
   scale so the hierarchy reads down. */
.tty-prose h1 {
  margin: var(--sp-12) 0 var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--display-md);
  line-height: var(--leading-tight);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--yellow);
  -webkit-font-smoothing: none;
  font-smooth: never;
}

.tty-prose h2 {
  margin: var(--sp-10) 0 var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--display-sm);
  line-height: var(--leading-tight);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--yellow);
  -webkit-font-smoothing: none;
  font-smooth: never;
}

.tty-prose h3 {
  margin: var(--sp-8) 0 var(--sp-3);
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  line-height: var(--leading-ui);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
  font-smooth: auto;
}

.tty-prose h4,
.tty-prose h5,
.tty-prose h6 {
  margin: var(--sp-6) 0 var(--sp-2);
  font-family: var(--font-body);
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  line-height: var(--leading-ui);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  -webkit-font-smoothing: antialiased;
  font-smooth: auto;
}

/* Every rule above uppercases the heading, and tokens/base.css uppercases h1-h6
   anyway. Inline code in a heading is a case-sensitive name, so it must keep the
   case the author typed. Five real cases exist today: `git blame`, `activate.sh`,
   `guards`, `scoped_guard` and `null_blk`. Uppercasing them makes the page state
   something false and gives a reader the wrong name to copy. The tracking has to
   go too, or the name reads with gaps in it. */
.tty-prose :is(h1, h2, h3, h4, h5, h6) code {
  text-transform: none;
  letter-spacing: 0;
}

/* ── Lists ──────────────────────────────────────────────────────────────── */
.tty-prose ul,
.tty-prose ol { margin: 0 0 var(--sp-6); padding-left: var(--sp-6); }
.tty-prose li { margin: 0 0 var(--sp-2); }
.tty-prose li > ul,
.tty-prose li > ol { margin: var(--sp-2) 0 0; }
.tty-prose ul { list-style: none; padding-left: var(--sp-5); }
.tty-prose ul > li { position: relative; }
/* A block marker on the cell grid, not a round bullet. U+25AA is outside the
   latin subset tokens/fonts.css loads, so this glyph comes from the system
   fallback, not from JetBrains Mono. Pin the size and the leading so fallback
   metrics cannot move it off the first text line of the item. */
.tty-prose ul > li::before {
  content: "▪";
  position: absolute;
  left: calc(var(--sp-5) * -1);
  font-family: var(--font-glyph);
  font-size: 0.85em;
  line-height: var(--leading-body);
  color: var(--text-faint);
}
.tty-prose ol { list-style: decimal; }
.tty-prose ol > li::marker { color: var(--text-faint); }

/* ── Quote ──────────────────────────────────────────────────────────────── */
/* A quotation is not a footnote: it stays at the reading size it sits in, so
   `1em`, not a fixed step off the type scale. It used to be pinned to --text-md
   (15px), which made every quote smaller than the body around it. */
.tty-prose blockquote {
  margin: var(--sp-8) 0;
  padding: 4px 0 4px 18px;
  border-left: var(--border-w-thick) solid var(--yellow);
  color: var(--text-primary);
  font-size: 1em;
  line-height: 1.75;
}
.tty-prose blockquote > :last-child { margin-bottom: 0; }

/* ── Rule ───────────────────────────────────────────────────────────────── */
.tty-prose hr {
  margin: var(--sp-10) 0;
  border: 0;
  border-top: var(--border-w) solid var(--border-faint);
}

/* ── Inline code ────────────────────────────────────────────────────────── */
/* `:not(pre) >` keeps the pill off the `<code>` inside an unlabelled fence. That
   `<code>` carries no class either, so the plain `code:not([class*="language-"])`
   form matched it and drew a 0.9em bordered band over a 242-line git diff. Real
   inline code always has a parent element inside .tty-prose — a p, li, td or
   heading — so the guard never costs a match. */
.tty-prose :not(pre) > code:not([class*="language-"]) {
  padding: 1px 5px;
  font-family: var(--font-code);
  font-size: 0.9em;
  color: var(--text-primary);
  background: var(--surface-raised);
  border: var(--border-w) solid var(--border-faint);
}

/* ── A fence with no language ────────────────────────────────────────────── */
/* Four fences in two posts carry no info string, so the syntax highlight plugin
   gives them no `language-*` class and every rule in prism-tty.css misses them.
   This rule gives that case the same chrome as a labelled block. `overflow: auto`
   is the part that must not be dropped: tokens/breakpoints.css ends with
   `html, body { overflow-x: hidden }`, so a block with no scroller of its own has
   its long lines clipped with no scrollbar anywhere — a 99-character diff line
   and a 120-character `git send-email` command are both affected. */
.tty-prose pre:not([class*="language-"]) {
  margin: 0 0 var(--sp-6);
  padding: 10px 12px;
  font-family: var(--font-code);
  /* Same size as a highlighted block — see the note in prism-tty.css. */
  font-size: var(--text-md);
  line-height: 1.7;
  tab-size: 2;
  hyphens: none;
  overflow: auto;
  color: var(--text-secondary);
  background: var(--surface-inset);
  border: var(--border-w) solid var(--border-faint);
  border-radius: var(--radius-none);
}
.tty-prose pre:not([class*="language-"]) > code {
  padding: 0;
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  color: inherit;
  background: none;
  border: 0;
  white-space: pre;
}

/* ── Figures ────────────────────────────────────────────────────────────── */
.tty-prose img {
  display: block;
  max-width: 100%;
  height: auto;
  margin: var(--sp-8) auto;
  border: var(--border-w) solid var(--border-faint);
}

/* ── Tables ─────────────────────────────────────────────────────────────── */
/* A table holds prose in this blog's posts — whole sentences in the first
   column, not figures — so its cells read at a reading size. Every table is
   already inside a .tty-scroll container, so the extra width costs nothing that
   was not already handled. */
.tty-prose table {
  width: 100%;
  margin: 0 0 var(--sp-6);
  border-collapse: collapse;
  font-size: var(--text-md);
}
.tty-prose thead th {
  height: 26px;
  padding: 0 10px;
  text-align: left;
  font: var(--text-xs)/1 var(--font-display);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-faint);
  background: var(--surface-raised);
  border-bottom: var(--border-w) solid var(--border-default);
  -webkit-font-smoothing: none;
  font-smooth: never;
}
.tty-prose tbody tr { height: var(--row-h); }
.tty-prose tbody tr:nth-child(even) { background: var(--surface-raised); }
.tty-prose tbody td {
  padding: var(--sp-2) 10px;
  border-bottom: var(--border-w) solid var(--border-faint);
  color: var(--text-primary);
}

/* ── Maths ──────────────────────────────────────────────────────────────── */
.tty-prose .katex { font-size: 1em; }
.tty-prose .katex-display {
  margin: var(--sp-6) 0;
  padding: var(--sp-2) 0;
  overflow-x: auto;
  overflow-y: hidden;
}

/* ── Markdown the posts actually contain ────────────────────────────────── */

/* A long URL or a long identifier must fold inside the 68ch measure rather than
   push the column wider than the page. */
.tty-prose { overflow-wrap: break-word; }
.tty-prose code:not([class*="language-"]) { overflow-wrap: anywhere; }

/* Table scroll container, added by the table_open rule in .eleventy.js.
   tokens/breakpoints.css clips page-level overflow, so a wide table needs a
   scroller of its own or its last columns are unreachable. */
.tty-scroll {
  margin: 0 0 var(--sp-6);
  overflow-x: auto;
  border-bottom: var(--border-w) solid var(--border-faint);
}
.tty-scroll > table { margin-bottom: 0; width: 100%; min-width: min-content; }
/* The header row stays on one line. Body cells wrap, so a table scrolls only
   when its min-content width really exceeds the measure. The wide table in the
   kernel-contribution post has a full sentence in its first column: nowrap
   there would make it about twice the reading measure and force a sideways
   scroll to read every row, which is worse than the Pico version. */
.tty-scroll > table thead th { white-space: nowrap; }

/* List markers. `▪` (U+25AA), `○` (U+25CB) and `●` (U+25CF) are Geometric
   Shapes, and none of the three is in the latin subset tokens/fonts.css loads.
   `--font-marker` is the fallback stack that names the faces which do carry the
   block; it is declared on `:root` in layout.css, because the nav's own search
   glyph needs the same stack. The marker gets a fixed one-character cell so a
   fallback advance cannot push it off the grid. */
.tty-prose ul > li::before {
  font-family: var(--font-marker);
  font-size: 0.85em;
  line-height: var(--leading-body);
  width: 1ch;
  text-align: center;
}

/* Task list items. The `[ ]` text is already stripped by the tty_task_glyphs
   rule, so these only replace the default block marker, and they inherit the
   face and the cell from the rule above. Both glyphs are in the design system's
   vocabulary. Order matters: --on has the same specificity as .tty-task, so it
   must come second to win. */
.tty-prose ul > li.tty-task::before { content: "○"; color: var(--text-muted); }
.tty-prose ul > li.tty-task--on::before { content: "●"; color: var(--yellow); }

/* Figure captions.
   The attribute selectors match the inline-styled blocks already written into
   the posts, so no post has to be edited. New posts should use the classes:
   <div class="tty-figure"> … </div> and <p class="tty-caption">…</p>. */
.tty-prose .tty-figure,
.tty-prose div[style*="justify-content: center"] {
  display: flex;
  justify-content: center;
  margin: var(--sp-8) 0 0;
}
.tty-prose .tty-figure img,
.tty-prose div[style*="justify-content: center"] img { margin: 0; }

/* --text-faint is 3.04:1 on --surface-root -- fails WCAG AA at this size
   (needs 4.5:1). A caption is real prose content on 15 figures, not
   metadata, so raise the usage to --text-muted (5.85:1 on root). The token
   is vendored and must not be edited. */
.tty-prose .tty-caption,
.tty-prose p[style*="text-align: center"] {
  max-width: var(--measure-prose);
  margin: var(--sp-3) auto var(--sp-8);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-muted);
  text-align: center;
  text-wrap: pretty;
}
.tty-prose .tty-caption em,
.tty-prose p[style*="text-align: center"] em { font-style: normal; }

/* The figure breakout is NOT here. Task 4 puts it in content.css, next to the
   .tty-prose measure it overrides, using `position: relative` and a negative
   `left`. Do not add a second implementation: this file is imported after
   content.css at equal specificity, so a `margin-left`/`transform` version
   would stack on top of the `left` offset, push every figure off centre, and
   clip it on the right. The rules above set `display`, `margin` and the caption
   only, which the Task 4 rules never touch. */

/* ── tty/prism-tty.css ─────────────────────────────────────────── */
/* Prism, mapped onto the TTY syntax tokens.
   Replaces the prism-tomorrow CDN theme and the prism-toolbar CDN stylesheet.
   The toolbar plugin's JavaScript only positions itself if these layout rules
   exist, so they live here rather than on the CDN — that also keeps the CDN
   from overriding our chrome, since .eleventy.js injects its block before
   </body>. */

pre[class*="language-"] {
  margin: 0 0 var(--sp-6);
  padding: 10px 12px;
  font-family: var(--font-code);
  /* 13px was the smallest size on the site, and these posts are largely shell
     transcripts and diffs. One step up, to the same size as a table cell. A
     block wider than the column still scrolls — `overflow: auto` below — which
     is what already happened to the two longest lines in the posts. */
  font-size: var(--text-md);
  line-height: 1.7;
  tab-size: 2;
  hyphens: none;
  overflow: auto;
  color: var(--text-secondary);
  background: var(--surface-inset);
  border: var(--border-w) solid var(--border-faint);
  border-radius: var(--radius-none);
  text-shadow: none;
}
code[class*="language-"] {
  font-family: var(--font-code);
  font-size: inherit;
  line-height: inherit;
  color: var(--text-secondary);
  background: transparent;
  text-shadow: none;
  white-space: pre;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
  background: var(--yellow);
  color: var(--black-void);
  text-shadow: none;
}

/* ── Token colours ──────────────────────────────────────────────────────── */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata { color: var(--syntax-comment); font-style: italic; }

.token.punctuation,
.token.operator,
.token.entity { color: var(--syntax-punct); }

.token.number,
.token.boolean,
.token.constant,
.token.symbol { color: var(--syntax-number); }

.token.string,
.token.char,
.token.attr-value,
.token.inserted { color: var(--syntax-string); }

.token.keyword,
.token.atrule,
.token.rule,
.token.selector,
.token.tag,
.token.important { color: var(--syntax-keyword); }

.token.function,
.token.function-name,
.token.class-name { color: var(--syntax-function); }

.token.property,
.token.attr-name,
.token.builtin,
.token.regex,
.token.url { color: var(--cyan); }

.token.variable,
.token.parameter { color: var(--text-primary); }

.token.deleted { color: var(--status-error); }

.token.bold,
.token.important { font-weight: var(--weight-bold); }
.token.italic { font-style: italic; }
.token.namespace { opacity: 0.7; }

/* ── Toolbar: language badge and copy button ─────────────────────────────── */
/* CodeBlock.jsx puts the label in a 26px strip above the code, inside the block
   border. The system never floats chrome over content, so this is a strip, not an
   overlay. prism-toolbar.js appends the toolbar *after* the `<pre>`, so the
   container is a column flex box and the strip is pulled up with `order: -1`.
   Nothing here is absolutely positioned, so nothing can cover line 1. */
div.code-toolbar {
  display: flex;
  flex-direction: column;
  margin: 0 0 var(--sp-6);
  background: var(--surface-inset);
  border: var(--border-w) solid var(--border-faint);
}
/* The container draws the frame now, so the inner pre gives its own back. */
div.code-toolbar > pre[class*="language-"] {
  margin: 0;
  background: transparent;
  border: 0;
}
div.code-toolbar > .toolbar {
  order: -1;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  height: 26px;
  padding: 0 10px;
  background: var(--surface-raised);
  border-bottom: var(--border-w) solid var(--border-faint);
  opacity: 1;
}
/* show-language adds the first item and copy-to-clipboard the second. The
   language takes CodeBlock's filename slot on the left; the button goes right. */
div.code-toolbar > .toolbar > .toolbar-item { display: flex; align-items: center; }
div.code-toolbar > .toolbar > .toolbar-item:first-child { margin-right: auto; }
div.code-toolbar > .toolbar > .toolbar-item > a,
div.code-toolbar > .toolbar > .toolbar-item > button,
div.code-toolbar > .toolbar > .toolbar-item > span {
  padding: 0;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  color: var(--text-faint);
  background: none;
  border: 0;
  border-radius: var(--radius-none);
  cursor: pointer;
  user-select: none;
}
/* --text-faint is 2.67:1 on --surface-raised (the code block's background) --
   fails WCAG AA (needs 4.5:1 at this size), and the Copy button is the only
   affordance on a code block: near-invisible until hovered. The token is
   vendored and must not be edited, so raise the usage instead. The language
   badge (the toolbar's <span>, from show-language) stays --text-faint on
   purpose -- it is metadata, deferred with the rest (post-row dates, index
   numbers, table headers, the eyebrow) as a recorded design-system property. */
div.code-toolbar > .toolbar > .toolbar-item > button {
  color: var(--text-muted);
}
div.code-toolbar > .toolbar > .toolbar-item > a:hover,
div.code-toolbar > .toolbar > .toolbar-item > button:hover {
  color: var(--yellow);
}
/* tokens/base.css drops the outline and draws focus with a box-shadow. Never set
   `box-shadow: none` on these items — that wins on specificity and leaves the
   Copy button with no keyboard focus indicator at all. */
div.code-toolbar > .toolbar > .toolbar-item > a:focus-visible,
div.code-toolbar > .toolbar > .toolbar-item > button:focus-visible {
  color: var(--yellow);
  box-shadow: var(--focus-ring);
}
/* The language badge is a label, not a control. CodeBlock.jsx sets it in accent. */
div.code-toolbar > .toolbar > .toolbar-item > span {
  color: var(--text-accent);
  cursor: default;
}
