/* Plain, dependency-free CSS. Monospace; weight and space over color.
   Every design value is a :root token — extend a scale, never hardcode. */

:root {
  /* Layout */
  --measure: 900px; /* max content width */

  /* light-dark() carries both sides; [data-theme] pins one to override. */
  color-scheme: light dark;
  /* Neutral poles. --bg/--fg the current side, --*-invert the other (unused). */
  --pole-bg-light: oklch(0.985 0.009 84);
  --pole-bg-dark: oklch(0.2543 0.0194 218.85);
  --pole-fg-light: oklch(0.2099 0.0039 286.06);
  --pole-fg-dark: oklch(0.9221 0.0071 88.65);
  --bg: light-dark(var(--pole-bg-light), var(--pole-bg-dark));
  --fg: light-dark(var(--pole-fg-light), var(--pole-fg-dark));
  --bg-invert: light-dark(var(--pole-bg-dark), var(--pole-bg-light));
  --fg-invert: light-dark(var(--pole-fg-dark), var(--pole-fg-light));
  --muted: light-dark(oklch(0.5477 0.0046 106.55), oklch(0.6361 0.0074 97.41));
  /* Blockquotes: dimmed but still content, not meta. ~40% of the muted→fg gap. */
  --quote: light-dark(oklch(0.41 0.005 106.5), oklch(0.75 0.007 97.4));
  --rule: light-dark(oklch(0.9094 0.0055 95.1), oklch(0.2812 0.0039 84.58));
  --photo-edge: light-dark(rgb(255 255 255 / 0.55), rgb(0 0 0 / 0.22));
  /* photo rim — a --border-rim halo. Alpha, never opaque: it must compose
     over any theme bg. */

  /* Opacity tints — percentages consumed by color-mix() */
  --tint-line: 30%; /* quiet (tier-2) underline — visible at rest in both themes */
  --tint-surface: 6%; /* subtle filled surface */
  --surface: color-mix(in srgb, var(--fg) var(--tint-surface), transparent);
  --tint-surface-deep: 12%; /* the surface's emphatic step */
  --surface-deep: color-mix(
    in srgb,
    var(--fg) var(--tint-surface-deep),
    transparent
  );
  --tint-glass: 94%; /* sticky chrome — near-opaque page bg over --blur-glass */
  --glass: color-mix(in srgb, var(--bg) var(--tint-glass), transparent);
  /* Theater near-black. .lightbox sets --bg to this (so --glass recomputes)
     and flips color-scheme: dark. */
  --lightbox-bg: oklch(0.1504 0.0027 67.55);

  /* Typography */
  --font-mono:
    ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
    "DejaVu Sans Mono", monospace;
  /* Reading alternative — system serifs only, no web font. Code stays mono. */
  --font-serif:
    "Iowan Old Style", "Palatino", Charter, ui-serif, Georgia, serif;
  /* Active body family; [data-font="serif"] aliases it to the serif stack. */
  --font-body: var(--font-mono);
  --text-sm: 0.85rem;
  --text-base: 1rem;
  --text-md: 1.15rem; /* no consumer — one type size site-wide, */
  --text-lg: 1.35rem; /* scale kept complete */
  --weight-normal: 400;
  --weight-bold: 700;
  --leading-tight: 1.3; /* headings */
  --leading-snug: 1.5; /* code */
  --leading-normal: 1.65; /* body */
  --leading-relaxed: 1.7; /* long-form articles */

  /* Spacing scale */
  --space-3xs: 0.2rem;
  --space-2xs: 0.4rem;
  --space-xs: 0.6rem;
  --space-sm: 0.85rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;
  --space-3xl: 4.5rem;
  --gap-section: 3.25rem; /* rhythm between major sections */

  /* Rounded-corner scale */
  --radius-sm: 3px;
  --radius-md: 6px;
  --radius-lg: 10px;

  /* Borders & motion */
  --border-thin: 1px;
  --border-thick: 2px; /* emphatic strokes (the focus ring) */
  --border-rim: 0.5ch; /* photo halo — the one typed-width stroke */
  --blur-glass: 8px; /* backdrop blur behind --glass chrome */
  --duration-snap: 75ms; /* lightbox page-turn phase (2 ≈ one turn) */
  --duration-fast: 120ms;
  --duration-med: 240ms; /* sprung transform motion needs room to settle */
  --duration-blink: 2s; /* the opener's `/` blink cycle */
  --ease: ease;
  /* Damped spring, ζ = 0.8. Transforms only; fades stay on --ease. */
  --ease-spring: linear(
    0,
    0.095,
    0.291,
    0.498,
    0.676,
    0.81,
    0.903,
    0.961,
    0.993,
    1.009,
    1.015,
    1.015,
    1.012,
    1.009,
    1.006,
    1.004,
    1
  );

  /* Components */
  /* Inline gaps ride ch — spacing as typed. Horizontal axis only. */
  --link-pad-x: 0.2em; /* hover-highlight breathing room; em: painted outside the flow */
  --link-glow: transparent; /* halation on the lit chip — only the tubes light it */
  --glow-blur: 6px; /* halation softness */
  --gap-inline: 1ch; /* between inline grouped links/tags — one typed space */
  --gap-dir: 0.5ch; /* breath after a path's slash (palette dir prefixes) */
  --indent-sub: 2ch; /* TUI tree indent — two typed spaces */
  --tool-size: 2rem; /* tools chip floor. rem, not ch/em: it must not move when the family swaps */
  --masthead-nav-all-w: 12ch; /* "all tags" overlay; the nav reserves it as end padding */
  --preview-w: 21rem; /* photo preview pane width cap (gutter caps further) */
  --toc-w: 16rem; /* TOC sidebar width cap, right gutter */
  --preview-ratio: 3 / 2; /* every pane slot crops to this (cover) */
  --shots-row: clamp(8rem, 55vw, 15rem);
  /* target row height the justified photo layout packs toward */
  --hero-max: min(
    22rem,
    55vh
  ); /* hero height cap — a portrait hero never shoves the prose past the fold */
  --hero-mosaic-h: min(
    26rem,
    60vh
  ); /* mosaic hero total height — definite, so rows split and photos crop */

  /* Three steps per hue, both themes. Keep complete even where unused. */
  --red: oklch(0.5771 0.2152 27.33);
  --red-light: oklch(0.7106 0.1661 22.22);
  --red-deep: oklch(0.4437 0.1613 26.9);
  --orange: oklch(0.6461 0.1943 41.12);
  --orange-light: oklch(0.7576 0.159 55.93);
  --orange-deep: oklch(0.4698 0.143 37.3);
  --yellow: oklch(0.7952 0.1617 86.05);
  --yellow-light: oklch(0.9052 0.1657 98.11);
  --yellow-deep: oklch(0.5538 0.1207 66.44);
  --green: oklch(0.6271 0.1699 149.21);
  --green-light: oklch(0.8003 0.1821 151.71);
  --green-deep: oklch(0.4479 0.1083 151.33);
  /* Green tube's signature (not success --green). In :root so the picker can
     preview a tube when it isn't active. */
  --phosphor: oklch(0.753 0.1866 151.9); /* accent fill */
  --phosphor-ink: oklch(
    0.1681 0.0309 159.04
  ); /* text knocked out on phosphor */
  --phosphor-glow: rgba(92, 245, 155, 0.55); /* the halation bloom */
  /* Amber (P3) — the second tube. Same roles, same reason for living here. */
  --phosphor-amber: oklch(0.8238 0.1408 70.17);
  --phosphor-amber-ink: oklch(
    0.1588 0.0342 70.88
  );
  --phosphor-amber-glow: rgba(255, 176, 84, 0.5);
  /* Tube walls — in :root so the picker can preview one when inactive. */
  --phosphor-bg: oklch(0.1298 0.0301 143.6); /* P1 green wall */
  --phosphor-amber-bg: oklch(0.1251 0.0256 90.11); /* P3 amber wall */
  --steel: oklch(0.5396 0.089 240);
  --steel-light: oklch(0.6673 0.0878 242.29);
  --steel-deep: oklch(0.4023 0.0656 243.51);
  --blue: oklch(0.5537 0.2458 266.04);
  --blue-light: oklch(0.7058 0.1481 270.71);
  --blue-deep: oklch(0.4167 0.2127 264.75);
  --violet: oklch(0.5413 0.2466 293.01);
  --violet-light: oklch(0.709 0.1592 293.54);
  --violet-deep: oklch(0.432 0.2106 292.76);
  --fuchsia: oklch(0.5916 0.218 0.58);
  --fuchsia-light: oklch(0.7253 0.1752 349.76);
  --fuchsia-deep: oklch(0.5246 0.199 3.96);

  /* Terracotta — the default: bone paper / olive dark, its own palette. A
     triad colour-codes intents (gold tags, sage years); only the mark differs
     per side. Round-trip verified (oklch skill). */
  --terra-bg-light: oklch(0.952 0.0165 91.55); /* bone paper */
  --terra-fg-light: oklch(0.2634 0.0109 99.32); /* near-black ink */
  --terra-muted-light: oklch(0.5559 0.0219 90.65);
  --terra-quote-light: oklch(0.3976 0.0168 93.21); /* dimmed-but-legible */
  --terra-rule-light: oklch(0.8913 0.025 91.63);
  --terra-bg-dark: oklch(0.3513 0.0173 135.08); /* desaturated olive */
  --terra-fg-dark: oklch(0.921 0.0205 94.54); /* warm cream ink */
  --terra-muted-dark: oklch(0.6806 0.0285 107.13);
  --terra-quote-dark: oklch(0.8078 0.0234 101.43);
  --terra-rule-dark: oklch(0.3879 0.0204 134.05);
  /* Signature accent, a rung per side (-hover the lighter suggest step); the
     sides deliberately don't share a hue. Pick a mark on the GAMUT, not by
     eye — a signature reads through CHROMA, and at knockout-safe L ~.50 sRGB
     caps chroma unevenly (green ~.14, blue ~.10, teal ~.08, violet→magenta
     ~.22). Never teal. Run the sweep before proposing a hue. */
  --terra-mark-light: oklch(0.4804 0.1708 345.14); /* plum */
  --terra-mark-light-hover: oklch(0.5504 0.1854 344.94);
  --terra-mark-dark: oklch(0.6664 0.1366 40.49); /* terracotta */
  --terra-mark-dark-hover: oklch(0.714 0.1218 48.17);
  /* Gold tags (71), sage years (118) — each clears both marks in hue, each
     dark enough for the cream knockout. */
  --terra-gold-light: oklch(0.5623 0.1014 71.31); /* tags, light side */
  --terra-gold-light-hover: oklch(0.6352 0.1082 71.94);
  --terra-gold-dark: oklch(0.6869 0.1134 73.96); /* tags, dark side */
  --terra-gold-dark-hover: oklch(0.7345 0.1093 75.39);
  --terra-sage-light: oklch(0.5024 0.0735 119.3); /* years, light side */
  --terra-sage-light-hover: oklch(0.5765 0.0776 119.14);
  --terra-sage-dark: oklch(0.7043 0.094 118.26); /* years, dark side */
  --terra-sage-dark-hover: oklch(0.7553 0.0932 118.04);
  /* selection wash — soft camel gold, dark ink over it */
  --terra-select-light: oklch(0.7479 0.109 70.58);
  --terra-select-dark: oklch(0.7509 0.105 73.93);
  /* knockout ink on the accent fills — warm cream, not pure white */
  --terra-knockout: oklch(0.9618 0.0168 88);
  /* Warm surface step between rule (L .89) and muted text (L .56); the dark
     step is hue 97, so it contrasts the olive bg rather than blending. */
  --terra-stone-light: oklch(0.8583 0.0256 89.23);
  --terra-stone-dark: oklch(0.4298 0.0288 96.97);
  /* The ramp's open hue slot (~66), light: a status/glow niche. Unused. */
  --terra-apricot-light: oklch(0.805 0.1163 66.5);
  --terra-apricot-dark: oklch(0.8439 0.0979 70.32);
  /* feTurbulence tile so the bg reads as stock. Inline data URI, no request. */
  --terra-grain: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='160'%20height='160'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.9'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='saturate'%20values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR%20type='linear'%20slope='1.7'%20intercept='-0.35'/%3E%3CfeFuncG%20type='linear'%20slope='1.7'%20intercept='-0.35'/%3E%3CfeFuncB%20type='linear'%20slope='1.7'%20intercept='-0.35'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect%20width='100%25'%20height='100%25'%20filter='url(%23g)'/%3E%3C/svg%3E");

  /* Calm at rest, vivid on interaction; by intent, never at random, all for
     white knockout. A *-text token is that intent RESTING as text: dark steps
     up to -light, the fills stay full. */
  --accent: var(
    --violet
  ); /* the default link hover */
  --accent-category: var(
    --violet
  ); /* section titles, More →, heads — own intent so it can diverge */
  --accent-category-text: light-dark(
    var(--accent-category),
    var(--violet-light)
  );
  --accent-social: var(
    --fuchsia
  ); /* masthead social + toggles — the name's hue, one fuchsia family */
  --accent-tag: var(--steel); /* tag links */
  --accent-tag-text: light-dark(var(--accent-tag), var(--steel-light));
  --accent-year: var(--orange); /* year links */
  --accent-draft: var(
    --red
  ); /* draft chrome — DEV ONLY: production never renders drafts */
  --accent-error: var(
    --red
  ); /* error/danger — only where the calm must yield, never for emphasis */
  --accent-error-text: light-dark(var(--red), var(--red-light));
  /* stays red in the tubes, like draft red — an error breaks the calm */
  --subhead-text: light-dark(var(--steel-deep), var(--steel));
  /* the subhead rung (h2/h3, Related) resting as text */
  --accent-fg: oklch(1 0 0);
  /* -light hover suggests, full accent commits; both knock out to --accent-fg. */
  --accent-hover: var(--violet-light);
  --accent-category-light: var(
    --violet-light
  );
  --accent-social-light: var(--fuchsia-light);
  --accent-tag-light: var(--steel-light);
  --accent-year-light: var(--orange-light);
  --accent-draft-light: var(--red-light); /* dev-only */
  /* The site name's hue — a head like the section titles, but fuchsia. */
  --mark: var(--fuchsia);
  --mark-light: var(--fuchsia-light);
  --mark-fg: var(--accent-fg);

  /* Highlighter yellow; links reassign these to wear their own accent. */
  --selection: var(--yellow);
  --selection-fg: oklch(
    0.2099 0.0039 286.06
  ); /* dark ink both themes — the white knockout fails on yellow */
}

/* Override (site.js): pinning color-scheme resolves every light-dark() token
   to that side; no attribute follows the system. */
:root[data-theme="light"] {
  color-scheme: light;
}
:root[data-theme="dark"] {
  color-scheme: dark;
}
/* Variant blocks pin a side and wire its neutrals + triad into intermediate
   --terra tokens; the shared prefix block consumes them. Flat — no tube. */
:root[data-theme="terracotta-light"] {
  color-scheme: light;
  --terra-bg: var(--terra-bg-light);
  --terra-fg: var(--terra-fg-light);
  --terra-muted: var(--terra-muted-light);
  --terra-quote: var(--terra-quote-light);
  --terra-rule: var(--terra-rule-light);
  --terra-accent: var(--terra-mark-light);
  --terra-accent-hover: var(--terra-mark-light-hover);
  --terra-tag: var(--terra-gold-light);
  --terra-tag-hover: var(--terra-gold-light-hover);
  --terra-year: var(--terra-sage-light);
  --terra-year-hover: var(--terra-sage-light-hover);
  --terra-stone: var(--terra-stone-light);
  --terra-select: var(--terra-select-light);
}
:root[data-theme="terracotta-dark"] {
  color-scheme: dark;
  --terra-bg: var(--terra-bg-dark);
  --terra-fg: var(--terra-fg-dark);
  --terra-muted: var(--terra-muted-dark);
  --terra-quote: var(--terra-quote-dark);
  --terra-rule: var(--terra-rule-dark);
  --terra-accent: var(--terra-mark-dark);
  --terra-accent-hover: var(--terra-mark-dark-hover);
  --terra-tag: var(--terra-gold-dark);
  --terra-tag-hover: var(--terra-gold-dark-hover);
  --terra-year: var(--terra-sage-dark);
  --terra-year-hover: var(--terra-sage-dark-hover);
  --terra-stone: var(--terra-stone-dark);
  --terra-select: var(--terra-select-dark);
}
/* Auto (no override) IS terracotta, following the system, zero JS.
   Paper/Slate pin data-theme, miss :not, and fall through to base :root. */
:root:not([data-theme]) {
  --terra-bg: light-dark(var(--terra-bg-light), var(--terra-bg-dark));
  --terra-fg: light-dark(var(--terra-fg-light), var(--terra-fg-dark));
  --terra-muted: light-dark(var(--terra-muted-light), var(--terra-muted-dark));
  --terra-quote: light-dark(var(--terra-quote-light), var(--terra-quote-dark));
  --terra-rule: light-dark(var(--terra-rule-light), var(--terra-rule-dark));
  --terra-accent: light-dark(var(--terra-mark-light), var(--terra-mark-dark));
  --terra-accent-hover: light-dark(
    var(--terra-mark-light-hover),
    var(--terra-mark-dark-hover)
  );
  --terra-tag: light-dark(var(--terra-gold-light), var(--terra-gold-dark));
  --terra-tag-hover: light-dark(
    var(--terra-gold-light-hover),
    var(--terra-gold-dark-hover)
  );
  --terra-year: light-dark(var(--terra-sage-light), var(--terra-sage-dark));
  --terra-year-hover: light-dark(
    var(--terra-sage-light-hover),
    var(--terra-sage-dark-hover)
  );
  --terra-stone: light-dark(var(--terra-stone-light), var(--terra-stone-dark));
  --terra-select: light-dark(
    var(--terra-select-light),
    var(--terra-select-dark)
  );
}
:root:not([data-theme]),
:root[data-theme^="terracotta"] {
  --bg: var(--terra-bg);
  --fg: var(--terra-fg);
  --muted: var(--terra-muted);
  --quote: var(--terra-quote);
  --rule: var(--terra-rule);
  /* Resting text → the triad: mark the structure, gold tags, sage years. */
  --accent-category-text: var(--terra-accent);
  --accent-tag-text: var(--terra-tag);
  --subhead-text: var(--terra-accent);
  --mark: var(--terra-accent);
  /* Every invert fill + its -light hover step, knocked out to warm cream. */
  --accent: var(--terra-accent);
  --accent-hover: var(--terra-accent-hover);
  --accent-category: var(--terra-accent);
  --accent-category-light: var(--terra-accent-hover);
  --accent-social: var(--terra-accent);
  --accent-social-light: var(--terra-accent-hover);
  --accent-tag: var(--terra-tag);
  --accent-tag-light: var(--terra-tag-hover);
  --accent-year: var(--terra-year);
  --accent-year-light: var(--terra-year-hover);
  --mark-light: var(--terra-accent-hover);
  --accent-fg: var(--terra-knockout);
  --mark-fg: var(--terra-knockout);
  /* soft camel selection, near-black ink — cream would wash out on the gold */
  --selection: var(--terra-select);
  --selection-fg: var(--terra-fg-light);
}
/* Fixed ::after (survives navigation; static) over bg, photos, chrome.
   Dialogs get no copy. */
:root:not([data-theme])::after,
:root[data-theme^="terracotta"]::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9991;
  pointer-events: none;
  background: var(--terra-grain);
  background-size: 160px 160px;
  mix-blend-mode: overlay;
  /* overlay bites hard on olive, barely on cream — light needs far more. */
  opacity: 0.4;
}
:root[data-theme="terracotta-light"]::after {
  opacity: 0.85;
}
/* Auto's light side needs the pinned grain (light-dark() has no opacity). */
@media (prefers-color-scheme: light) {
  :root:not([data-theme])::after {
    opacity: 0.85;
  }
}
/* TRAP: mix-blend-mode does not survive the top-layer boundary in Safari —
   it falls back to normal compositing and paints a heavy gray veil. Any
   dialog copy must self-blend (background-blend-mode). */

/* Green (P1) and amber (P3), differing only in colour. Variant blocks set
   --term-*; the shared prefix block consumes them and MUST follow them in
   source order (exact and prefix selectors tie on specificity). */
:root[data-theme="phosphor-green"] {
  --term-bg: var(--phosphor-bg);
  --term-fg: oklch(0.8868 0.2098 150.33);
  --term-muted: oklch(
    0.549 0.129 152.24
  ); /* ΔL .34 below --term-fg. Muted separates in two currencies: metric
     (ΔL/ΔC) and categorical (a different colour NAME). Green gets no
     categorical assist, so it pays the whole gap in lightness. */
  --term-quote: oklch(0.684 0.16 151); /* quote step, ~40% muted→fg */
  --term-rule: oklch(0.3286 0.0721 149.8);
  --term-phosphor: var(--phosphor);
  --term-ink: var(--phosphor-ink);
  --term-glow: var(--phosphor-glow);
  --term-hover: oklch(0.867 0.1809 154.02); /* the -light invert step */
  --term-text-glow: rgba(80, 255, 140, 0.45); /* resting body bloom */
  --term-pool: rgba(40, 200, 100, 0.05); /* the centre phosphor wash */
  --term-img-hue: 75deg; /* greenscreen hue-rotate off sepia */
}
/* Amber: the P3 tube. Same structure, warmer phosphor. */
:root[data-theme="phosphor-amber"] {
  --term-bg: var(--phosphor-amber-bg);
  --term-fg: oklch(0.8813 0.102 75.85);
  --term-muted: oklch(
    0.6213 0.1101 70.77
  ); /* categorical: dark chroma-held amber reads BROWN, so a moderate ΔL
     already separates it. */
  --term-quote: oklch(0.725 0.106 73); /* quote step, ~40% muted→fg */
  --term-rule: oklch(0.2865 0.0541 74.8);
  --term-phosphor: var(--phosphor-amber);
  --term-ink: var(--phosphor-amber-ink);
  --term-glow: var(--phosphor-amber-glow);
  --term-hover: oklch(0.9043 0.0842 77.47);
  --term-text-glow: rgba(255, 180, 90, 0.45);
  --term-pool: rgba(220, 150, 40, 0.06);
  --term-img-hue: 5deg; /* sepia is already amber — barely rotate */
}
/* Overrides the neutral + resting-text tokens, so the cascade follows. */
:root[data-theme^="phosphor"] {
  color-scheme: dark;
  --bg: var(--term-bg);
  --fg: var(--term-fg);
  --muted: var(--term-muted);
  --quote: var(--term-quote);
  --rule: var(--term-rule);
  /* MUST name --term-bg, never var(--bg): .lightbox sets --bg to
     var(--lightbox-bg), so that cycles. */
  --lightbox-bg: var(--term-bg);
  /* Resting text → phosphor (heads, tags, the name, the cmd prompt + value). */
  --accent-category-text: var(--fg);
  --accent-tag-text: var(--fg);
  --mark: var(--fg);
  /* Full opacity: scanlines darken a faint line and the bloom washes its
     edges, so 30% vanishes. Still 1px. */
  --tint-line: 100%;
  /* Every invert → one phosphor family; nothing keeps its own hue. Draft
     red stays (dev-only). */
  --accent: var(--term-phosphor);
  --accent-hover: var(--term-hover);
  --accent-category: var(--term-phosphor);
  --accent-category-light: var(--term-hover);
  --accent-social: var(--term-phosphor);
  --accent-social-light: var(--term-hover);
  --accent-tag: var(--term-phosphor);
  --accent-tag-light: var(--term-hover);
  --accent-year: var(--term-phosphor);
  --accent-year-light: var(--term-hover);
  --mark-light: var(--term-hover);
  --accent-fg: var(--term-ink);
  --mark-fg: var(--term-ink);
  --selection: var(--term-phosphor);
  --selection-fg: var(--term-ink);
  /* A soft dark raster line every 3px — faint enough never to fight text. */
  --scanlines: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0) 0,
    rgba(0, 0, 0, 0) 2px,
    rgba(0, 20, 6, 0.35) 3px,
    rgba(0, 20, 6, 0.35) 3px
  );
  /* Three radials (topmost first): centre pool, curvature highlight,
     corner-dark vignette. CRT is corner-dark + centre-lit, never edge-glow;
     a monochrome tube has no shadow mask, so no RGB triads. */
  --vignette:
    radial-gradient(ellipse at center, var(--term-pool) 0%, transparent 55%),
    radial-gradient(
      ellipse 120% 60% at center 42%,
      rgba(255, 255, 255, 0.018) 0%,
      transparent 60%
    ),
    radial-gradient(ellipse at center, transparent 50%, rgba(0, 8, 2, 0.5) 100%);
  /* Lights the --link-glow box-shadow slot; low, so it's bloom not a ring. */
  --link-glow: var(--term-glow);
  /* Graded glow: body soft, heads/name brighter, --muted unglowed. */
  --glow-text: 0 0 2px var(--term-text-glow);
  --glow-text-strong: 0 0 7px var(--term-text-glow);
  --glow-photo: 0 0 7px var(--term-glow);
  /* Persistence: fast attack, slow release — the lit cell cools, not snaps,
     leaving an afterglow wake. */
  --term-decay: 450ms;
  background: var(--term-bg);
}
:root[data-theme^="phosphor"] body {
  font-family: var(--font-mono) !important; /* phosphor terminals are mono */
  text-shadow: var(
    --glow-text
  ); /* graded resting bloom */
}
/* Heads/name/h1 take the stronger glow; body stays soft. Static bloom. */
:root[data-theme^="phosphor"] :is(h1, h2, h3, .section-title, .name) {
  text-shadow: var(--glow-text-strong);
}
/* Phosphor-screen. Standard filters only — an SVG url(#…) filter vanishes
   the images in Safari. */
:root[data-theme^="phosphor"] img {
  mix-blend-mode: hard-light;
  filter: grayscale(1) contrast(0.62) brightness(1.32) sepia(1)
    hue-rotate(var(--term-img-hue)) saturate(1.5) drop-shadow(var(--glow-photo));
}
/* Fixed pseudo, so they survive navigation; a second copy per open dialog,
   since top-layer sheets sit above the page pseudo. */
:root[data-theme^="phosphor"]::after,
:root[data-theme^="phosphor"] dialog[open]::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9991;
  pointer-events: none;
  background: var(--vignette), var(--scanlines);
}
/* Safari won't paint a fixed overlay behind floating mobile toolbars — it
   clips at the toolbar line. Fade the edges to nothing so there's no line to
   clip. px (not %) tracks real toolbar height. */
@media (pointer: coarse) {
  :root[data-theme^="phosphor"]::after,
  :root[data-theme^="phosphor"] dialog[open]::after {
    -webkit-mask-image: linear-gradient(
      to bottom,
      transparent 0,
      #000 56px,
      #000 calc(100% - 120px),
      transparent 100%
    );
    mask-image: linear-gradient(
      to bottom,
      transparent 0,
      #000 56px,
      #000 calc(100% - 120px),
      transparent 100%
    );
  }
}

/* site.js, pre-applied in the head so there's no flash. Code keeps
   --font-mono regardless — its chip geometry rides the character grid. */
:root[data-font="serif"] {
  --font-body: var(--font-serif);
}

/* Reduced motion: transitions/animations effectively instant. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* Theme/font cross-fade, a typed same-document view transition (navigations
   stay instant). Reduced motion is handled in site.js — the universal rule
   can't reach these pseudo-elements. */
:root:active-view-transition-type(theme)::view-transition-old(root),
:root:active-view-transition-type(font)::view-transition-old(root) {
  animation: fade-out var(--duration-fast) var(--ease) both;
}
:root:active-view-transition-type(theme)::view-transition-new(root),
:root:active-view-transition-type(font)::view-transition-new(root) {
  animation: fade-in var(--duration-fast) var(--ease) both;
}

@keyframes fade-out {
  to {
    opacity: 0;
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
}

* {
  box-sizing: border-box;
}

/* Reserve the scrollbar gutter so width doesn't jump between pages. */
html {
  font-size: 16px;
  scrollbar-gutter: stable;
  /* Clear the sticky bar: its stuck height + a --space-lg breath. */
  scroll-padding-top: calc(1lh + 2 * var(--space-sm) + var(--space-lg));
}
/* Dev only: the draft banner sits above the bar, so anchors clear both. */
:root:has(.draft-flag) {
  scroll-padding-top: calc(
    2lh + 2 * var(--space-sm) + 2 * var(--space-3xs) + var(--space-lg)
  );
}
/* One knob scales the site: every length rides rem or ch. Breakpoint rems
   resolve against the browser's initial 16px, not ours, so thresholds hold. */
@media (max-width: 48rem) {
  html {
    font-size: 15px;
  }
}
@media (max-width: 34rem) {
  html {
    font-size: 14px;
  }
}
@media (max-width: 22rem) {
  html {
    font-size: 13px;
  }
}

body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  color: var(--fg);
  background: var(--bg);
  margin: 0;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Sticky footer via the footer's auto top margin. dvh (vh fallback)
     tracks iOS dynamic toolbars under viewport-fit=cover. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* Draft chrome — DEV ONLY; production never renders drafts. Loud on purpose. */
.draft-flag {
  margin: 0;
  padding: var(--space-3xs) var(--space-md);
  background: var(--accent-draft);
  color: var(--accent-fg);
  font-weight: var(--weight-bold);
  text-align: center;
  position: sticky;
  top: 0;
  z-index: 100;
}
/* Rests loud red and bold, not muted like tags; inverts in its red intent. */
.draft-mark {
  --link-fg: var(--accent-draft);
  --accent: var(--accent-draft);
  --accent-hover: var(--accent-draft-light);
  font-weight: var(--weight-bold);
}
/* Forced colors strips fills — give the banner a real border instead. */
@media (forced-colors: active) {
  .draft-flag {
    border-block-end: var(--border-thick) solid;
  }
}

main {
  width: 100%; /* flex item: fill to max-width; auto margins alone shrink to content */
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--space-3xl) var(--space-lg) 0;
}

footer {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  margin-top: auto; /* sticky footer (see body) */
  padding: var(--gap-section) var(--space-lg) var(--space-2xl);
  color: var(--muted);
  font-size: var(--text-sm);
  /* Copyright left, palette strip right, one baseline. */
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-md);
}
footer p {
  margin: 0;
}

/* Swatches butted into one bar, equal shares of the space right of the
   copyright; their -light/-deep steps hang below as two absolute rows, like
   a descender. Colours from the data-hue map. */
.palette {
  display: inline-flex;
  flex: 1;
  min-width: 0;
}
.swatch {
  position: relative;
  flex: 1;
  height: 0.5em;
  margin-top: -0.087em;
  background: var(--c);
  max-width: var(--space-md);
}
.swatch::before,
.swatch::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 0.175em;
}
.swatch::before {
  top: 100%;
  background: var(--c-light);
}
.swatch::after {
  top: calc(100% + 0.175em);
  top: calc(100% + 0.25cap);
  background: var(--c-deep);
}
/* Keyed by data-hue. Fixed red→fuchsia — a flip recolours, never reorders. */
.swatch[data-hue="red"] {
  --c: var(--red);
  --c-light: var(--red-light);
  --c-deep: var(--red-deep);
}
.swatch[data-hue="orange"] {
  --c: var(--orange);
  --c-light: var(--orange-light);
  --c-deep: var(--orange-deep);
}
.swatch[data-hue="yellow"] {
  --c: var(--yellow);
  --c-light: var(--yellow-light);
  --c-deep: var(--yellow-deep);
}
.swatch[data-hue="green"] {
  --c: var(--green);
  --c-light: var(--green-light);
  --c-deep: var(--green-deep);
}
.swatch[data-hue="steel"] {
  --c: var(--steel);
  --c-light: var(--steel-light);
  --c-deep: var(--steel-deep);
}
.swatch[data-hue="blue"] {
  --c: var(--blue);
  --c-light: var(--blue-light);
  --c-deep: var(--blue-deep);
}
.swatch[data-hue="violet"] {
  --c: var(--violet);
  --c-light: var(--violet-light);
  --c-deep: var(--violet-deep);
}
.swatch[data-hue="fuchsia"] {
  --c: var(--fuchsia);
  --c-light: var(--fuchsia-light);
  --c-deep: var(--fuchsia-deep);
}

/* ── Easter egg: the footer palette ────────────────────────────────────────
   Hover shimmers; a click bursts the page (site.js). Decorative, JS-only.
   Never under a tube — hue-rotating breaks the monochrome illusion. */
.palette {
  cursor: pointer;
}
@media (hover: hover) {
  :root:not([data-theme^="phosphor"]) .palette:hover {
    animation: fx-hue 2s linear infinite;
  }
}
/* Under a tube: monochrome, stepping bright → dim — variety within one hue. */
:root[data-theme^="phosphor"] .swatch {
  background: var(--term-phosphor);
}
:root[data-theme^="phosphor"] .swatch:nth-child(2) {
  background: color-mix(in srgb, var(--term-phosphor) 92%, #000);
}
:root[data-theme^="phosphor"] .swatch:nth-child(3) {
  background: color-mix(in srgb, var(--term-phosphor) 84%, #000);
}
:root[data-theme^="phosphor"] .swatch:nth-child(4) {
  background: color-mix(in srgb, var(--term-phosphor) 76%, #000);
}
:root[data-theme^="phosphor"] .swatch:nth-child(5) {
  background: color-mix(in srgb, var(--term-phosphor) 68%, #000);
}
:root[data-theme^="phosphor"] .swatch:nth-child(6) {
  background: color-mix(in srgb, var(--term-phosphor) 60%, #000);
}
:root[data-theme^="phosphor"] .swatch:nth-child(7) {
  background: color-mix(in srgb, var(--term-phosphor) 54%, #000);
}
:root[data-theme^="phosphor"] .swatch:nth-child(8) {
  background: color-mix(in srgb, var(--term-phosphor) 48%, #000);
}
:root[data-theme^="phosphor"] .swatch::before {
  background: var(--term-hover);
}
:root[data-theme^="phosphor"] .swatch::after {
  background: color-mix(in srgb, var(--term-phosphor) 50%, #000);
}

/* A body class site.js toggles on a palette click; skipped under reduced motion. */
.fx-disco {
  animation: fx-hue 2s linear 3;
}
@keyframes fx-hue {
  to {
    filter: hue-rotate(360deg);
  }
}

/* One overlay site.js drives by class. Two layers so they can fire together:
   ::before snow, ::after the power-on flash. */
.fx-overlay {
  position: fixed;
  inset: 0;
  z-index: 9995;
  pointer-events: none;
}
.fx-overlay::before,
.fx-overlay::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
}
/* Four screened layers: jittering noise, a rolling hold band, a retrace
   line, a raster grid so it reads as a tube. Stepped, so it judders. */
.fx-overlay.is-static::before {
  background:
    url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='120'%20height='120'%3E%3Cfilter%20id='n'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.6'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='saturate'%20values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR%20type='linear'%20slope='2.2'%20intercept='-0.6'/%3E%3CfeFuncG%20type='linear'%20slope='2.2'%20intercept='-0.6'/%3E%3CfeFuncB%20type='linear'%20slope='2.2'%20intercept='-0.6'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect%20width='100%25'%20height='100%25'%20filter='url(%23n)'%20opacity='0.9'/%3E%3C/svg%3E")
      repeat,
    linear-gradient(
      to bottom,
      transparent 44%,
      rgba(255, 255, 255, 0.08) 50%,
      transparent 56%
    ),
    linear-gradient(
      to bottom,
      transparent 46%,
      rgba(200, 255, 215, 0.18) 49%,
      rgba(200, 255, 215, 0.18) 51%,
      transparent 54%
    ),
    repeating-linear-gradient(
      to bottom,
      rgba(255, 255, 255, 0.6) 0,
      rgba(255, 255, 255, 0.6) 1px,
      transparent 1px,
      transparent 4px
    );
  background-size:
    180px 180px,
    100% 240%,
    100% 16%,
    100% 100%;
  mix-blend-mode: screen;
  animation:
    fx-static 0.6s ease-out forwards,
    fx-snow 0.6s steps(6) forwards;
}
/* Energise flash on power-on. --boot-color is the tube being entered. */
.fx-overlay.is-boot::after {
  background: var(--boot-color, var(--term-phosphor, #2bcf70));
  animation: fx-boot 0.5s ease-out forwards;
}
@keyframes fx-static {
  0% {
    opacity: 0;
  }
  8% {
    opacity: 0.5;
  }
  55% {
    opacity: 0.26;
  }
  100% {
    opacity: 0;
  }
}
@keyframes fx-snow {
  0% {
    background-position:
      0 0,
      0 -30%,
      0 -120%,
      0 0;
  }
  20% {
    background-position:
      13% 7%,
      0 0%,
      0 60%,
      0 0;
  }
  40% {
    background-position:
      -8% 21%,
      0 35%,
      0 260%,
      0 0;
  }
  60% {
    background-position:
      24% -12%,
      0 70%,
      0 440%,
      0 0;
  }
  80% {
    background-position:
      -17% 9%,
      0 100%,
      0 620%,
      0 0;
  }
  100% {
    background-position:
      6% 18%,
      0 130%,
      0 800%,
      0 0;
  }
}
/* Opacity only: energise, dim, a lock-flash pulse, gone. */
@keyframes fx-boot {
  0% {
    opacity: 0.9;
  }
  30% {
    opacity: 0.12;
  }
  62% {
    opacity: 0.42;
  }
  100% {
    opacity: 0;
  }
}
/* ── Content-driven power on/off ──────────────────────────────────────────
   <main> itself does the CRT geometry; site.js sets transform-origin to the
   viewport centre before each. */

/* Power-ON: blooms from a hot line, horizontal first. Not `forwards`. */
main.fx-bloom-page {
  animation: fx-bloom-page 0.42s ease-out;
}
@keyframes fx-bloom-page {
  0% {
    transform: scaleX(0.3) scaleY(0.012);
    opacity: 0.5;
    filter: brightness(2.6);
  }
  26% {
    transform: scaleX(1.06) scaleY(0.05);
    opacity: 0.85;
    filter: brightness(1.9);
  }
  55% {
    transform: scaleX(0.99) scaleY(0.7);
    opacity: 1;
    filter: brightness(1.2);
  }
  100% {
    transform: scaleX(1) scaleY(1);
    opacity: 1;
    filter: brightness(1);
  }
}
/* Crushes to a line, pinches to a dot, winks out. site.js swaps the theme in
   at the pinch; `forwards` holds the dot until then. */
main.fx-collapse {
  animation: fx-collapse 0.4s ease-in forwards;
}
@keyframes fx-collapse {
  0% {
    transform: scaleY(1) scaleX(1);
    filter: brightness(1);
    opacity: 1;
  }
  42% {
    transform: scaleY(0.012) scaleX(1.02);
    filter: brightness(2.4);
  }
  74% {
    transform: scaleY(0.012) scaleX(0.06);
    filter: brightness(3);
  }
  100% {
    transform: scaleY(0.02) scaleX(0.02);
    filter: brightness(3.4);
    opacity: 0;
  }
}
/* The destination theme, fading up to full size after the dot dies. */
main.fx-reveal {
  animation: fx-reveal 0.32s ease-out;
}
@keyframes fx-reveal {
  0% {
    opacity: 0;
    transform: scale(0.97);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}
/* The hot dot the collapse pinches to. --boot-color is the tube being left. */
.fx-overlay.is-poweroff {
  background: radial-gradient(
    circle at center,
    color-mix(in srgb, var(--boot-color, #2bcf70) 35%, #fff) 0%,
    var(--boot-color, #2bcf70) 4%,
    transparent 13%
  );
  animation: fx-dot 0.46s ease-in forwards;
}
@keyframes fx-dot {
  0% {
    opacity: 0;
    transform: scale(1);
  }
  62% {
    opacity: 0;
    transform: scale(1);
  }
  76% {
    opacity: 0.9;
    transform: scale(1);
  }
  88% {
    opacity: 1;
    transform: scale(1.6);
    filter: brightness(1.5);
  }
  100% {
    opacity: 0;
    transform: scale(1.9);
  }
}

/* Heads rest violet, no underline; everything else takes a quiet --tint-line
   underline. Hover/focus inverts to the intent accent. */
a,
.btn-link {
  color: var(--link-fg, inherit);
  text-decoration-line: underline;
  text-decoration-style: solid;
  text-decoration-thickness: var(--border-thin);
  text-underline-offset: 0.18em;
  text-decoration-color: color-mix(
    in srgb,
    currentColor var(--tint-line),
    transparent
  );
  /* Two side copies extend the highlight without layout shift, transparent
     at rest so only colour fades. Third is the halation slot — box-shadow,
     not a filter, which would ghost. */
  box-shadow:
    calc(-1 * var(--link-pad-x)) 0 0 transparent,
    var(--link-pad-x) 0 0 transparent,
    0 0 var(--glow-blur) transparent;
  transition:
    background-color var(--duration-fast) var(--ease),
    color var(--duration-fast) var(--ease),
    box-shadow var(--duration-fast) var(--ease);
}
/* Dimmed and inert, no invert or tooltip (the font toggle under a tube). */
.btn-link:disabled {
  opacity: 0.6;
  pointer-events: none;
}

/* A head like the section titles, but fuchsia; the generic rules fill it. */
.name a,
a.name {
  --link-fg: var(--mark);
  --accent: var(--mark);
  --accent-hover: var(--mark-light);
  --accent-fg: var(--mark-fg);
  text-decoration-line: none;
}

/* Hover suggests (-light), focus/click/roving commit (full accent). Focus
   MUST stay after hover in source order so it wins when both apply. */
a:hover,
.btn-link:hover {
  background: var(--accent-hover);
  color: var(--accent-fg);
  border-radius: var(--radius-sm);
  box-shadow:
    calc(-1 * var(--link-pad-x)) 0 0 var(--accent-hover),
    var(--link-pad-x) 0 0 var(--accent-hover),
    0 0 var(--glow-blur) var(--link-glow);
  text-decoration-color: transparent;
}
a:focus-visible,
a:active,
.btn-link:focus-visible,
.btn-link:active,
:is(a, .btn-link).is-active {
  background: var(--accent);
  color: var(--accent-fg);
  border-radius: var(--radius-sm);
  box-shadow:
    calc(-1 * var(--link-pad-x)) 0 0 var(--accent),
    var(--link-pad-x) 0 0 var(--accent),
    0 0 var(--glow-blur) var(--link-glow);
  text-decoration-color: transparent;
}

/* The invert IS the focus indicator, so drop the UA ring — but keep a
   transparent outline so forced-colors still paints a real one. */
a:focus-visible,
.btn-link:focus-visible {
  outline: var(--border-thick) solid transparent;
}

/* Registered so a keyframe can animate it: wobbles the glow copy only. */
@property --glow-blur {
  syntax: "<length>";
  inherits: true;
  initial-value: 6px;
}
/* Same keyframe, via filter: brightness(). 1 = no-op. */
@property --glow-bright {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}
@keyframes term-glow-flicker {
  0% {
    --glow-blur: 6px;
    --glow-bright: 1;
  }
  14% {
    --glow-blur: 9px;
    --glow-bright: 1.11;
  }
  22% {
    --glow-blur: 5px;
    --glow-bright: 0.92;
  }
  47% {
    --glow-blur: 8px;
    --glow-bright: 1.06;
  }
  63% {
    --glow-blur: 6px;
    --glow-bright: 0.97;
  }
  81% {
    --glow-blur: 9.5px;
    --glow-bright: 1.13;
  }
  100% {
    --glow-blur: 7px;
    --glow-bright: 1;
  }
}
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme^="phosphor"]
    :is(a, .btn-link):is(:hover, :focus-visible, :active, .is-active) {
    animation: term-glow-flicker 1.5s linear infinite;
    filter: brightness(var(--glow-bright));
  }
}

/* Phosphor persistence — slow cool on return to rest, fast attack. */
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme^="phosphor"] :is(a, .btn-link) {
    transition:
      background-color var(--term-decay) ease-out,
      color var(--term-decay) ease-out,
      box-shadow var(--term-decay) ease-out;
  }
  :root[data-theme^="phosphor"]
    :is(a, .btn-link):is(:hover, :focus-visible, :active, .is-active) {
    transition:
      background-color var(--duration-fast) var(--ease),
      color var(--duration-fast) var(--ease),
      box-shadow var(--duration-fast) var(--ease);
  }
}

/* Resting color must ride --link-fg so the inverted hover wins any order. */
.link-muted {
  --link-fg: var(--muted);
}

/* Hover accents by intent (the default --accent covers everything else). */
.section-title,
.more a {
  --accent: var(--accent-category);
  --accent-hover: var(--accent-category-light);
}
/* Masthead tools. Box is 2×--link-pad-x short of --tool-size because the hover
   box-shadow pays it back per side, landing a true square. rem, not ch, so a
   font toggle re-centers the glyph without resizing the box. */
.masthead-tools a,
.theme-toggle,
.font-toggle {
  --link-fg: var(--muted);
  --accent: var(--accent-social);
  --accent-hover: var(--accent-social-light);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  block-size: var(--tool-size);
  min-inline-size: calc(var(--tool-size) - 2 * var(--link-pad-x));
}
/* Restore the hide: the display rules above beat the UA [hidden] rule. */
.masthead-tools [hidden] {
  display: none;
}

/* The glyph is a specimen: "Aa" set in the family the button switches *to*. */
.font-specimen {
  font-family: var(--font-serif);
  font-weight: var(--weight-bold);
  letter-spacing: -0.15ch;
}
:root[data-font="serif"] .font-specimen {
  font-family: var(--font-mono);
}
/* Tooltip too: set in the family it switches *to*. */
.font-toggle .tip {
  font-family: var(--font-serif);
}
:root[data-font="serif"] .font-toggle .tip {
  font-family: var(--font-mono);
}

/* A <button> dressed as a link — inherits the link rules, inverts on hover. */
.btn-link {
  font: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

/* Inline icon glyphs — masked, ride currentColor, so they invert with the link.
   Import spec: 24-unit grid, stroke-width 2 (Streamline ships 1.5 — bump it).
   Mask reads alpha only; stroke color in the file is moot. */
.ico {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  background: currentColor;
  -webkit-mask: var(--ico) center / contain no-repeat;
  mask: var(--ico) center / contain no-repeat;
}
.ico-left {
  margin-right: 0.35em;
}
.ico-right {
  margin-left: 0.35em;
}
.ico-github {
  --ico: url("../icons/github.svg");
}
.ico-linkedin {
  --ico: url("../icons/linkedin.svg");
}
.ico-sun {
  --ico: url("../icons/sun.svg");
} /* theme toggle → light */
.ico-moon {
  --ico: url("../icons/moon.svg");
} /* theme toggle → dark */
.ico-terminal {
  --ico: url("../icons/terminal.svg");
} /* theme toggle → tube */

/* .tip — real text lifted into a tooltip chip on hover/focus, in the element's
   own intent accent. No-CSS + screen readers get it inline. */
:is(a, .btn-link):has(.tip) {
  position: relative;
  text-decoration-line: none;
}
/* Lift while shown: the tube's hover brightness() makes a stacking context
   that traps the tooltip under the next row. */
:is(a, .btn-link):has(.tip):is(:hover, :focus-visible) {
  z-index: 2;
}
.tip {
  position: absolute;
  top: calc(100% + var(--space-3xs));
  left: 50%;
  translate: -50%;
  padding-inline: var(--link-pad-x);
  /* Hover weight here; focus steps to the full accent below. */
  background: var(--accent-hover);
  color: var(--accent-fg);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  /* Fixed weight + upright — never inherits an active tag's bold. */
  font-weight: var(--weight-normal);
  font-style: normal;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(-0.2em);
  pointer-events: none;
  transition:
    opacity var(--duration-fast) var(--ease),
    transform var(--duration-fast) var(--ease);
  z-index: 1;
}
/* Rise on the spring, fade on --ease. Hover reveal is pointer-only — a touch
   tap sticks :hover and strands the chip. */
:is(a, .btn-link):focus-visible .tip {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--duration-fast) var(--ease),
    transform var(--duration-med) var(--ease-spring);
}
@media (hover: hover) {
  :is(a, .btn-link):hover .tip {
    opacity: 1;
    transform: none;
    transition:
      opacity var(--duration-fast) var(--ease),
      transform var(--duration-med) var(--ease-spring);
  }
}
:is(a, .btn-link):focus-visible .tip {
  background: var(--accent);
}
/* Forced colors kills chips + masked glyphs — surface the titles inline. */
@media (forced-colors: active) {
  :is(a, .btn-link):has(.tip) .ico {
    display: none;
  }
  .tip {
    position: static;
    opacity: 1;
    transform: none;
    translate: none;
  }
}
/* Right-align: the tools hug the page edge, so a centred chip would spill. */
.masthead-tools .tip {
  left: auto;
  right: 0;
  translate: none;
}

/* inline-block so hover highlights fill the line height. Prose opts back to
   inline to wrap mid-sentence; structural links in a <p> opt in by class. */
a {
  display: inline-block;
}
:is(p, li, figcaption, blockquote) a {
  display: inline;
}
.name a,
a.name,
.more a,
.meta a {
  display: inline-block;
}

/* Selection rides --accent, so each link's matches its own hover invert. */
::selection {
  background: var(--selection);
  color: var(--selection-fg);
}
a,
.btn-link {
  --selection: var(--accent);
  --selection-fg: var(--accent-fg);
}

/* display: contents promotes the three rows into <main> — required, since a
   sticky child only sticks while its parent box is in view and the masthead's
   own box ends at the nav. Bottom gap moves to .masthead-nav-wrap. */
.masthead {
  display: contents;
}
/* Name + social, sticky to the viewport top. Glass wash bled to the measure
   edge by negative margins paid back as padding — resting geometry intact. */
.masthead-top {
  display: grid;
  /* name · / · tools. The 1fr soaks slack, pushing tools right. .masthead-bar
     is display:contents, so name + tools land here by grid-column. */
  grid-template-columns: auto auto 1fr;
  /* Name baselines with the slash chip; tool chips re-center via align-self. */
  align-items: baseline;
  position: sticky;
  top: 0;
  z-index: 2;
  margin: calc(-1 * var(--space-sm)) calc(-1 * var(--space-lg));
  padding: var(--space-sm) var(--space-lg);
  background: var(--glass);
  -webkit-backdrop-filter: blur(var(--blur-glass));
  backdrop-filter: blur(var(--blur-glass));
}
/* Collapses into the grid on desktop; becomes a real row at the breakpoint. */
.masthead-bar {
  display: contents;
}
.masthead-tools {
  grid-row: 1;
  grid-column: 3;
  justify-self: end;
  align-self: center;
}
/* Dev only: drop below the sticky draft banner so the bar isn't hidden. */
body:has(.draft-flag) .masthead-top {
  top: calc(1lh + 2 * var(--space-3xs));
}
.name {
  font-size: var(--text-base);
  font-weight: var(--weight-bold);
  margin: 0;
}
/* Wraps the name. */
.masthead-id {
  grid-row: 1;
  grid-column: 1;
  display: flex;
  align-items: baseline;
}
/* The palette opener — reversed chip, blinking `/`; hover/focus steadies it
   and faux-types a sample query (.cmd-type). */
.cmd-open {
  /* grid-row pins it onto the one masthead row: its DOM order is after the
     col-3 tools, so sparse auto-placement would drop it to a phantom row 2. */
  grid-row: 1;
  grid-column: 2;
  margin-inline-start: var(--gap-inline);
  --accent: var(--mark);
  /* Already inverted at rest — pin hover, or the lighter layer repaints it. */
  --accent-hover: var(--mark);
  /* Two side box-shadow copies. Named so the scrolled shed can drop it. */
  --chip-pad: 0.45ch;
  --chip-shadow:
    calc(-1 * var(--chip-pad)) 0 0 var(--accent),
    var(--chip-pad) 0 0 var(--accent);
  background: var(--accent);
  color: var(--mark-fg);
  border-radius: var(--radius-sm);
  box-shadow: var(--chip-shadow);
  white-space: nowrap; /* the typed preview grows on one line */
  transition:
    background-color var(--duration-fast) var(--ease),
    box-shadow var(--duration-fast) var(--ease),
    color var(--duration-fast) var(--ease);
}
/* Empty at rest; a typed character earns the gutter after the prompt. */
.cmd-type {
  font-style: italic;
  opacity: 0.6;
}
.cmd-type:not(:empty) {
  margin-left: var(--gap-inline);
}
/* Stuck + masthead idle → shed the chip to a bare blinking `/`; engaging the
   masthead restores it (:has on main — the rows are its children). Desktop
   only: the restore's :focus-within would outrank the mobile bar on any tap. */
@media (min-width: 34.0625rem) {
  .is-scrolled .cmd-open {
    background: none;
    box-shadow: none;
    color: var(--mark);
  }
  main:has(
      .masthead-top:hover,
      .masthead-top:focus-within,
      .bio:hover,
      .masthead-nav-wrap:hover,
      .masthead-nav-wrap:focus-within
    )
    .cmd-open {
    background: var(--accent);
    box-shadow: var(--chip-shadow);
    color: var(--mark-fg);
  }
}
/* Keep the .tip text for assistive tech + forced colors, but never pop it. */
.cmd-open:is(:hover, :focus-visible) .tip {
  opacity: 0;
  transform: none;
}
.cmd-blink {
  animation: cursor-blink var(--duration-blink) infinite;
}
/* Steady on hover/focus, or pinned for a view-transition capture. */
.cmd-open:is(:hover, :focus-visible, .is-steady) .cmd-blink {
  animation: none;
}
/* Slash travel — the `/` morphs between chip and prompt. INVARIANT: exactly
   one slash carries the name per state, or the browser can't pair them. */
.cmd-open {
  view-transition-name: cmd-slash;
}
body:has(.cmd[open]) .cmd-open {
  view-transition-name: none;
}
.cmd[open] .cmd-prompt {
  view-transition-name: cmd-slash;
}
:root:active-view-transition-type(palette)::view-transition-old(root),
:root:active-view-transition-type(palette)::view-transition-new(root) {
  animation-duration: var(--duration-fast);
}
:root:active-view-transition-type(palette)::view-transition-group(cmd-slash),
:root:active-view-transition-type(palette)::view-transition-old(cmd-slash),
:root:active-view-transition-type(palette)::view-transition-new(cmd-slash) {
  animation-duration: var(--duration-med);
}
:root:active-view-transition-type(palette)::view-transition-group(cmd-slash) {
  animation-timing-function: var(--ease-spring);
}
/* Theme/font flips capture the slash as a stationary group — hold it to the
   root fade's tempo so the chip can't trail the page. */
:root:active-view-transition-type(theme)::view-transition-group(cmd-slash),
:root:active-view-transition-type(theme)::view-transition-old(cmd-slash),
:root:active-view-transition-type(theme)::view-transition-new(cmd-slash),
:root:active-view-transition-type(font)::view-transition-group(cmd-slash),
:root:active-view-transition-type(font)::view-transition-old(cmd-slash),
:root:active-view-transition-type(font)::view-transition-new(cmd-slash) {
  animation-duration: var(--duration-fast);
}
/* Weighted toward visible: on 64% of the cycle, off 24%, 6% each fade. */
@keyframes cursor-blink {
  0%,
  64% {
    opacity: 1;
  }
  70%,
  94% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
/* Stop outright: the universal reduced-motion rule would loop it at 0.01ms. */
@media (prefers-reduced-motion: reduce) {
  .cmd-blink {
    animation: none;
  }
}
.social {
  display: flex;
  align-items: center;
  /* rem, not ch: ch would shift the cluster on a font-family toggle. */
  gap: var(--space-xs);
}
/* Icon-only links — bump the glyph for presence. */
.social .ico,
.theme-toggle .ico {
  width: 1.2em;
  height: 1.2em;
  vertical-align: -0.25em;
}
/* In a tube the brand marks become [gh] / [in] — .ico drops its mask + box for
   a ::before. Scoped to .social so the theme toggle keeps its glyph. */
:root[data-theme^="phosphor"] .social .ico {
  -webkit-mask: none;
  mask: none;
  background: none;
  width: auto;
  height: auto;
  vertical-align: baseline;
}
:root[data-theme^="phosphor"] .ico-github::before {
  content: "[gh]";
}
:root[data-theme^="phosphor"] .ico-linkedin::before {
  content: "[in]";
}
/* Social | theme toggle — drops to its own line as a unit when narrow. */
.masthead-tools {
  display: flex;
  align-items: center;
  /* rem, not ch — fixed footprint across a font-family toggle. */
  gap: var(--space-xs);
}
.masthead-sep {
  color: var(--muted);
  /* Fixed box: the pipe's advance differs mono vs serif. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--space-2xs);
}
.bio {
  margin: var(--space-2xs) 0 0;
}
.masthead-back {
  margin: 0;
}
/* Section links — their own line under the bio; the tag row scrolls below. */
.masthead-cats {
  margin-top: var(--space-lg); /* a blank line's breath below the bio */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: calc(var(--gap-inline) + var(--gap-dir)); /* 1.5ch */
}
/* Where the tag row is dropped this is the last row, so it takes the gap. */
.masthead-cats:last-child {
  margin-bottom: var(--gap-section);
}
/* Anchors the pinned "all tags" link over the scrolling tag row. */
.masthead-nav-wrap {
  position: relative;
  margin-top: var(--space-lg); /* a blank line, matching the bio breath */
  /* The masthead's box-bottom gap — it has no box, this is its last row. */
  margin-bottom: var(--gap-section);
}
.masthead-nav {
  display: flex;
  flex-wrap: nowrap;
  align-items: baseline;
  gap: var(--gap-inline);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* overflow-x clips the first chip's shadow bleed — pad it, pay it back. */
  padding-inline-start: var(--link-pad-x);
  margin-inline-start: calc(-1 * var(--link-pad-x));
  /* Same on the block axis: overflow-x forces overflow-y to clip, cutting
     each tag's count tooltip. */
  padding-bottom: var(--space-xl);
  margin-bottom: calc(-1 * var(--space-xl));
}
/* "all tags" — pinned right over the scrolling row, the gradient dissolving
   tags beneath it. Ships hidden (no-JS just scrolls); site.js sets
   .nav-overflows, which also reserves end padding to clear the overlay. */
.masthead-nav-all {
  display: none;
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  padding-inline-start: var(--space-xl);
  background: linear-gradient(to right, transparent, var(--bg) var(--space-xl));
}
.masthead-nav-wrap.nav-overflows .masthead-nav-all {
  display: block;
}
.masthead-nav-wrap.nav-overflows .masthead-nav {
  padding-inline-end: var(--masthead-nav-all-w);
}
.masthead-nav-all a {
  --link-fg: var(--muted);
  --accent: var(--accent-tag);
  --accent-hover: var(--accent-tag-light);
  white-space: nowrap;
}
/* Section links rest in the category hue, like the heads they point at. */
.masthead-cats .nav-link {
  --link-fg: var(--accent-category-text);
}
/* Active section — bold is the "you are here" mark, which is what lets the
   per-page eyebrow head go. Mono bold keeps the advance: no reflow. */
.masthead-cats .nav-link[aria-current] {
  font-weight: var(--weight-bold);
}
/* Active tag (on its own /tags/<term>/ page) — bold, resting steel. */
.masthead-nav .tag[aria-current] {
  --link-fg: var(--accent-tag-text);
  font-weight: var(--weight-bold);
}
.masthead-nav > * {
  white-space: nowrap;
}

@media (max-width: 34rem) {
  /* main goes flex so the promoted masthead pieces can reorder (the `/` leads)
     while staying children of the page-tall <main> that anchors the sticky.
     Every margin here is single-sided, so non-collapsing flex keeps rhythm. */
  main {
    padding-top: var(--space-lg);
    display: flex;
    flex-direction: column;
  }
  .cmd-open {
    order: -5;
  }
  .masthead-bar {
    order: -4;
    margin-top: var(--space-md);
  }
  .bio {
    order: -3;
    margin-top: var(--space-lg);
  }
  .masthead-cats {
    order: -1;
  }
  .masthead-nav-wrap {
    order: -1;
  }
  /* .masthead-top stops being a box so its children promote into <main>.
     Static so site.js's is-scrolled reader knows it isn't sticky. */
  .masthead-top {
    display: contents;
    position: static;
  }
  /* Name left, tools hard right — one non-sticky row. */
  .masthead-bar {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    column-gap: var(--space-md);
  }
  .masthead-tools {
    grid-column: 2;
  }
  .cmd-open:not([hidden]) {
    display: block;
  }
  /* The `/` opener — its own full-width sticky bar, the only sticky part of the
     header. Stays a plain prompt in EVERY state (no invert; a tap would flip it
     to a fuchsia chip). Scoped to `.masthead-top` to outrank the global
     `:is(a, .btn-link):has(.tip)` rule (0,2,0) that forces position:relative —
     without the bump, `.cmd-open` (0,1,0) loses and the bar silently un-sticks. */
  .masthead-top .cmd-open,
  .masthead-top .cmd-open:is(:hover, :focus-visible, :active, .is-active) {
    grid-column: auto;
    /* Swallows main's padding-top so the bar rests already stuck. */
    margin: calc(-1 * var(--space-lg)) calc(-1 * var(--space-lg)) 0;
    padding: var(--space-sm) var(--space-lg);
    position: sticky;
    top: 0;
    z-index: 2;
    text-align: left;
    background: var(--glass);
    -webkit-backdrop-filter: blur(var(--blur-glass));
    backdrop-filter: blur(var(--blur-glass));
    border-radius: 0;
    box-shadow: none;
    color: var(--mark);
    text-decoration-line: none;
    outline: none;
    transition: none;
  }
  /* Dev only: clear the draft banner so the bar isn't tucked under it. */
  body:has(.draft-flag) .cmd-open {
    top: calc(1lh + 2 * var(--space-3xs));
  }
  /* Static label inline instead, held in every state so a tap can't knock
     it out. */
  .cmd-open .cmd-type {
    display: none;
  }
  .cmd-open .tip,
  .cmd-open:is(:hover, :focus-visible, :active, .is-active) .tip {
    position: static;
    opacity: 1;
    transform: none;
    translate: none;
    margin-inline-start: var(--gap-inline);
    padding: 0;
    background: none;
    color: var(--muted);
    font-size: inherit;
  }
  /* No slash morph here — the opener is a full-width bar, so the snapshot
     would zoom a big rectangle down to the tiny prompt. */
  .cmd-open,
  .cmd[open] .cmd-prompt {
    view-transition-name: none;
  }
}

/* Command palette — a <dialog> sheet of the page bg. The inner box shares
   main's measure + padding, so the field lands where the name sits. */
.cmd {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  border: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Keeps the arrows' scroll-into-view clear of the sticky filter bar. */
  scroll-padding-top: calc(1lh + 2 * var(--space-sm));
}
/* Fade in on open, instant close. Do NOT add a closing fade via
   allow-discrete/@starting-style — Safari paints a transparent sheet. */
:is(.cmd, .lightbox)[open] {
  animation: fade-in var(--duration-fast) var(--ease);
}

/* ── Theme swatches ───────────────────────────────────────────────────────
   Each `↯ theme` option paints its theme's own colours from :root (--p-*,
   keyed by data-preview) without that theme being active. */
.cmd-sw {
  display: inline-block;
  width: 2.5ch; /* on the ch grid */
  height: 0.9em;
  margin-right: var(--gap-inline);
  vertical-align: -0.1em;
  /* bg + two accent stripes. A tube has no --p-accent2, so they merge. */
  background: linear-gradient(
    90deg,
    var(--p-bg) 52%,
    var(--p-accent) 52%,
    var(--p-accent) 76%,
    var(--p-accent2, var(--p-accent)) 76%
  );
}
/* Auto has no accent — it splits the terracotta sides (what it renders). */
.cmd-sw-auto {
  background: linear-gradient(
    135deg,
    var(--terra-bg-light) 50%,
    var(--terra-bg-dark) 50%
  );
}
/* --p-accent = the theme's mark; --p-accent2 = its next most prevalent. */
[data-preview="light"] {
  --p-bg: var(--pole-bg-light);
  --p-fg: var(--pole-fg-light);
  --p-accent: var(--fuchsia);
  --p-accent2: var(--violet);
}
[data-preview="dark"] {
  --p-bg: var(--pole-bg-dark);
  --p-fg: var(--pole-fg-dark);
  --p-accent: var(--fuchsia-light);
  --p-accent2: var(--violet-light);
}
[data-preview="terracotta-light"] {
  --p-bg: var(--terra-bg-light);
  --p-fg: var(--terra-fg-light);
  --p-accent: var(--terra-mark-light);
  --p-accent2: var(--terra-gold-light);
}
[data-preview="terracotta-dark"] {
  --p-bg: var(--terra-bg-dark);
  --p-fg: var(--terra-fg-dark);
  --p-accent: var(--terra-mark-dark);
  --p-accent2: var(--terra-gold-dark);
}
[data-preview="phosphor-green"] {
  --p-bg: var(--phosphor-bg);
  --p-fg: var(--phosphor);
  --p-accent: var(--phosphor);
}
[data-preview="phosphor-amber"] {
  --p-bg: var(--phosphor-amber-bg);
  --p-fg: var(--phosphor-amber);
  --p-accent: var(--phosphor-amber);
}
/* Lock the page under any open dialog. The lightbox isn't a scroll container,
   so scroll chains to the root; killing the root scrollport stops it.
   scrollbar-gutter: stable (on html) keeps the width from jumping. */
html:has(dialog[open]) {
  overflow: hidden;
}
.cmd-box {
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--space-3xl) var(--space-lg);
}
/* Must stay after the base rule — equal specificity, source order wins. */
@media (max-width: 34rem) {
  .cmd-box {
    padding-top: var(--space-lg);
  }
}
/* The filter field — a bare input behind a `/` prompt. The caret is the
   focus indicator; there's nothing else to focus. */
.cmd-field {
  display: flex;
  align-items: baseline;
  /* Prompt + a typed space = the rows' gutter, so the first character lands
     on the parent-title column. */
  gap: var(--gap-inline);
  /* Negative margins paid back as padding: resting geometry untouched, wash
     bleeds into the box's gutters. */
  position: sticky;
  top: 0;
  z-index: 1;
  margin: calc(-1 * var(--space-sm)) calc(-1 * var(--space-lg))
    calc(var(--space-lg) - var(--space-sm));
  padding: var(--space-sm) var(--space-lg) var(--space-sm)
    calc(var(--space-lg) - var(--indent-sub));
  background: var(--glass);
  -webkit-backdrop-filter: blur(var(--blur-glass));
  backdrop-filter: blur(var(--blur-glass));
}
.cmd-prompt {
  color: var(--mark);
  font-weight: var(--weight-bold);
}
.cmd-input {
  /* type=search exempts the field from contact autofill; shed its dressing. */
  appearance: none;
  font: inherit;
  color: var(--mark);
  background: none;
  border: 0;
  padding: 0;
  outline: none;
  flex: 1;
  caret-color: var(--mark);
}
.cmd-input::placeholder {
  color: var(--mark-light);
  opacity: 1;
}
.cmd-input::-webkit-search-cancel-button,
.cmd-input::-webkit-search-decoration {
  display: none;
}
/* The close link — the input's flex:1 carries it to the field's right edge. */
.cmd-close {
  --link-fg: var(--muted);
}
.cmd-line[hidden] {
  display: none;
} /* .row's display: flex would beat [hidden] */
/* Each arrow is a real button that steps the selection. */
.cmd-hint {
  display: flex;
  gap: var(--gap-inline);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-2xs);
}
.cmd-step {
  --link-fg: var(--muted);
}
/* At a selection limit a step arrow disables itself — dimmed and inert. */
.cmd-step:disabled {
  opacity: 0.45;
  cursor: default;
  pointer-events: none;
  text-decoration-line: none; /* a dead control isn't a link */
}
/* "esc to go back" — OPTIONS mode only; routes through stepOut. */
.cmd-esc {
  --link-fg: var(--muted);
  margin-left: var(--space-sm);
  text-decoration-line: none; /* a caption, not a prose link; hover still inverts */
}
.cmd-esc-note {
  opacity: 0.7;
}
.cmd-esc[hidden] {
  display: none;
}
/* Entries + tag pages indent under their landmarks — two typed spaces. */
.cmd-sub {
  padding-left: var(--indent-sub);
}
.cmd-top {
  font-weight: var(--weight-bold);
}
/* A page-less landmark head (Contact) — a plain label, not in the walk. */
.cmd-head {
  color: var(--fg);
}
.cmd-dir {
  color: var(--muted);
  margin-right: var(--gap-dir);
}
/* External rows carry a trailing ↗ — muted at rest, knocks out with the row. */
.cmd-ext-mark {
  color: var(--muted);
  margin-left: var(--gap-inline);
}
.cmd-row:is(:hover, :focus-visible, .is-active) .cmd-ext-mark {
  color: inherit;
}
/* Show the dir prefix only while filtering, when the landmark heads drop out.
   Matching reads dataset.text, so a hidden prefix still scopes the list. */
.cmd:not(.is-filtering) .cmd-dir {
  display: none;
}
.cmd-row:is(:hover, :focus-visible, .is-active) .cmd-dir {
  color: inherit;
}
/* Command bar — the site's real verbs, as a mode switch: verbs at rest,
   swapped for a command's options when selected. Actions, not destinations:
   ↯ prompt, no underline, fuchsia invert. */
.cmd-bar {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--gap-inline);
  padding-block: var(--space-3xs);
  margin-bottom: var(
    --space-2xs
  ); /* a breath before Home */
}
.cmd-prompt-bar {
  color: var(--mark);
  font-weight: var(--weight-bold);
}
/* The muted verb label leading an options line (theme / font). */
.cmd-glabel {
  color: var(--muted);
  margin-right: var(--gap-dir);
}
/* Each atom is an action chip — btn-link for the invert, no underline. */
.cmd-atom {
  --link-fg: var(--muted);
  --accent: var(--mark);
  --accent-hover: var(--mark-light);
  --accent-fg: var(--mark-fg);
  text-decoration-line: none;
  white-space: nowrap;
}
/* Spell the hide: the UA [hidden] rule loses to the inline-block default. */
.cmd-atom[hidden] {
  display: none;
}
/* Vertical menu: ↑↓ walk the options (site.js vMove), ←→ are inert here. */
.cmd-opts {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3xs);
}
.cmd-opthead {
  display: flex;
  align-items: baseline;
  gap: var(--gap-inline);
}
.cmd-optlist {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3xs);
  padding-left: var(--indent-sub);
}
/* inline-flex so the invert highlights the option, not the whole measure. */
.cmd-opt {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-inline);
}
/* Hover ≠ focus here: the bar doesn't move the selection on hover, so both can
   paint at once and must read apart. The base two-weight invert separates by a
   lightness step alone, which collapses in terracotta — so hover is a wash
   against focus's solid fill. :not() hands a hovered+focused atom to focus. */
.cmd-atom:hover:not(:focus-visible):not(.is-active) {
  --cmd-hover-wash: color-mix(in srgb, var(--accent) 22%, transparent);
  background: var(--cmd-hover-wash);
  color: var(--fg);
  box-shadow:
    calc(-1 * var(--link-pad-x)) 0 0 var(--cmd-hover-wash),
    var(--link-pad-x) 0 0 var(--cmd-hover-wash);
  text-decoration-color: transparent;
}
/* The trailing VS Code ↗ row — walkable, but applies no theme. */
.cmd-opt-extra {
  margin-top: var(--space-2xs);
}
/* `‹ back` — a nav atom that steps out to the verbs (stepOut). */
.cmd-back {
  /* Out of the optlist indent so ‹ lines up with the ↯ head prompt. */
  margin-left: calc(-1 * var(--indent-sub));
  margin-bottom: var(--space-2xs);
}
.cmd-back-mark {
  color: var(--muted);
  margin-right: var(--gap-inline);
}
.cmd-back:is(:hover, :focus-visible, .is-active) .cmd-back-mark {
  color: inherit;
}
/* Selected ≠ focused: current rests as a quiet tint. :where() holds it BELOW
   the interactive inverts so they always win. */
.cmd-opt:where(.is-current) {
  --link-fg: var(--fg);
  /* --terra-stone is only defined under terracotta; the fallback covers the rest. */
  --sel-surface: var(--terra-stone, var(--surface-deep));
  background: var(--sel-surface);
  border-radius: var(--radius-sm);
  box-shadow:
    calc(-1 * var(--link-pad-x)) 0 0 var(--sel-surface),
    var(--link-pad-x) 0 0 var(--sel-surface);
}
.cmd-opt.is-current::after {
  content: "✓";
}
/* Accent ✓ — the tell that separates selected from focus in a tube, where
   chip and fill share a hue. Knocks out under focus. */
.cmd-opt:where(.is-current)::after {
  color: var(--accent);
}
.cmd-opt:is(:hover, :focus-visible, .is-active)::after {
  color: var(--accent-fg);
}
/* A disabled option is a word, not a glyph, so it strikes through. Never
   strike the current one — that reads as damage. */
.cmd-opt:disabled:not(.is-current) {
  text-decoration-line: line-through;
}
/* "You are here" — aria-current rests on a quiet chip. :where() keeps it under
   a:hover so every invert wins. Forced colors strips tints — border instead. */
.cmd-row:where([aria-current]) {
  background: var(--surface-deep);
  border-radius: var(--radius-sm);
  box-shadow:
    calc(-1 * var(--link-pad-x)) 0 0 var(--surface-deep),
    var(--link-pad-x) 0 0 var(--surface-deep);
}
@media (forced-colors: active) {
  .cmd-row[aria-current] {
    border: var(--border-thin) solid;
  }
}
.cmd-empty {
  color: var(--muted);
  margin: 0;
}

/* Photo preview pane — the left gutter previews the active row's entry: hero,
   plus two beneath when it has more. One partial rendered twice (body level,
   and inside the palette dialog). Every slot crops to --preview-ratio, so
   layout never jumps. Hidden below 75rem, hero-only to 88rem. The `top` here
   is only the pre-JS resting value; site.js tracks the row. */
.preview {
  display: none;
}
@media (min-width: 75rem) {
  .preview {
    display: block;
    position: fixed;
    top: var(--space-3xl);
    right: calc(50% + var(--measure) / 2 + var(--space-lg));
    width: min(
      var(--preview-w),
      calc(50vw - var(--measure) / 2 - 2 * var(--space-lg))
    );
    margin: 0;
    visibility: hidden;
    /* Required backdrop: position:fixed makes this its own stacking context,
       so the tube's hard-light images would blend against transparency and
       read washed. Invisible — the gutter already shows this colour. */
    background: var(--bg);
  }
  .preview.has-photo {
    visibility: visible;
  }
  .preview-link {
    display: block;
    /* Kill it here — a descendant can't cancel propagated decoration. */
    text-decoration-line: none;
  }
  .preview-link:is(:hover, :focus-visible, :active) {
    background: none;
    box-shadow: none;
  }
  /* Clipped to one line so a long title never wraps the pane taller; held
     muted so the invert never knocks it out. */
  .preview-title {
    display: block;
    margin-bottom: var(--space-2xs);
    color: var(--muted);
    font-size: var(--text-sm);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .preview-link:is(:hover, :focus-visible, :active) .preview-title {
    color: var(--muted);
    background: none;
  }
  /* Crop to --preview-ratio so portrait and landscape land identically. */
  .preview img {
    display: block;
    width: 100%;
    aspect-ratio: var(--preview-ratio);
    object-fit: cover;
    outline: var(--border-rim) solid var(--photo-edge);
    border-radius: var(--radius-sm);
    /* Rim-colour fill holds the slot's shape while the hero fetches. */
    background-color: var(--photo-edge);
  }
  /* Secondary row — two more picks, equal columns, same crop. */
  .preview-more {
    display: none;
    margin-top: var(--space-2xs);
    gap: var(--space-2xs);
  }
  .preview-more img {
    width: auto;
    min-width: 0;
    flex: 1 1 0;
  }
  .preview-more img[hidden] {
    display: none;
  }
}
/* The secondary row joins only once the gutter has room for a grid. */
@media (min-width: 88rem) {
  .preview.has-more .preview-more {
    display: flex;
  }
}

/* Table of contents — fixed right-gutter sidebar, ≥75rem only. site.js
   lights the active section. */
.toc {
  display: none;
}
@media (min-width: 75rem) {
  .toc {
    display: block;
    position: fixed;
    /* Lands the first item on the article's first body line. */
    top: calc(
      var(--space-3xl) + var(--leading-normal) * 1rem + var(--space-md)
    );
    /* The preview pane's formula, mirrored into the opposite gutter. */
    left: calc(50% + var(--measure) / 2 + var(--space-lg));
    width: min(
      var(--toc-w),
      calc(50vw - var(--measure) / 2 - 2 * var(--space-lg))
    );
    max-height: calc(100vh - var(--space-3xl) - var(--space-lg));
    overflow-y: auto;
    /* overflow-y forces overflow-x to clip, cutting a hover chip's shadow
       bleed — pad the inline axis, pay it back as negative margin. */
    padding-inline: var(--space-2xs);
    margin-inline: calc(-1 * var(--space-2xs));
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
    scrollbar-width: thin;
  }
  /* Title item rides .toc-list; headings come from .Fragments.ToHTML. */
  .toc-list,
  .toc #TableOfContents ul {
    list-style: none;
    margin: 0;
    padding: 0;
  }
  /* h3s indent one step under their h2. */
  .toc #TableOfContents ul ul {
    margin-left: var(--indent-sub);
  }
  .toc li {
    margin: var(--space-3xs) 0;
  }
  /* Muted rest via --link-fg so the global invert knocks out cleanly. */
  .toc a {
    --link-fg: var(--muted);
    text-decoration-line: none;
  }
  .toc a.toc-current {
    --link-fg: var(--fg);
    font-weight: var(--weight-bold);
  }
  /* The Related jump (toc.html) — a blank line below the heading tree. */
  .toc-related {
    margin-top: var(--space-sm);
  }
}

/* Index sections */
.index-section {
  margin-bottom: var(--gap-section);
}

.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
  line-height: var(
    --leading-normal
  ); /* pin against .post's relaxed leading — heads sit alike on every page */
}
/* Heading hue, no underline. --link-fg for linked, color for the plain span. */
.section-title {
  font-weight: var(--weight-bold);
  color: var(--accent-category-text);
  --link-fg: var(--accent-category-text);
  text-decoration-line: none;
}

.section-intro {
  /* No top margin — the tag row's --gap-section bottom sets the gap above. */
  margin: 0 0 var(--gap-section);
}

.row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3xs) var(--gap-inline);
  padding-block: var(--space-3xs);
  line-height: var(
    --leading-normal
  ); /* list row, never prose — pin against .post leading so the hover chip is
        the same height everywhere */
}
/* Keep the roving walk's scrollIntoView off the viewport edge. */
main :is(.row, .section-head, .more) {
  scroll-margin-block: var(--space-lg);
}
/* Head owns the seam: first row sheds top padding so head → text is --space-sm. */
.section-head + .row {
  padding-block-start: 0;
}
.row .meta {
  color: var(--muted);
  white-space: nowrap;
  /* Stay hard right even when the cluster wraps to its own line. */
  margin-left: auto;
}

.more {
  margin-top: var(--space-xs);
  line-height: var(--leading-normal);
} /* list grammar — pin against .post leading, like .row */

.job-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 0 var(--space-2xs);
}
.career-acquired {
  color: var(--muted);
}
.career-title {
  text-align: right;
}

@media (max-width: 34rem) {
  .job-row {
    grid-template-columns: 1fr;
  }
  .career-title {
    text-align: left;
    color: var(--muted);
  }
}

.tag {
  --link-fg: var(--muted);
  --accent: var(--accent-tag);
  --accent-hover: var(--accent-tag-light);
}
/* The .tip anchor rule drops the underline — a tag is text, re-assert it. */
.tag:has(.tip) {
  text-decoration-line: underline;
}
.tag .tip {
  left: 0;
  right: auto;
  translate: none;
}
/* ...but flip right inside a meta cluster so it can't spill past the measure. */
.meta .tag .tip {
  left: auto;
  right: 0;
}
/* Years read like tags. Same class covers the unlinked span. */
.year {
  --link-fg: var(--muted);
  --accent: var(--accent-year);
  --accent-hover: var(--accent-year-light);
}
/* Right-align so the chip can't spill past the measure; re-assert the underline
   the .tip anchor rule drops. */
.year .tip {
  left: auto;
  right: 0;
  translate: none;
}
.year:has(.tip) {
  text-decoration-line: underline;
}
/* A year page's heading: a wrapping time strip, latest year first. */
.year-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3xs) var(--gap-inline);
  margin: 0 0 var(--space-xl); /* the strip owns the heading's bottom margin */
}
.year-title {
  color: var(--accent-year);
}
.year-strip .year-title {
  margin: 0;
}
.year-empty {
  color: var(--muted);
}
.draft-title {
  color: var(--accent-draft);
}
.meta.tags {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--gap-inline);
}

/* One type size site-wide: every h1 outside the masthead is body size. */
h1 {
  font-size: var(--text-base);
  color: var(--fg);
  margin: 0 0 var(--space-xl);
}

.post h1 {
  margin-bottom: var(--space-3xs);
}
/* Title row — takes over the h1's bottom spacing so wrapped tags hug it. */
.title-row {
  padding-block: 0;
  margin-bottom: var(--space-3xs);
  /* leading is pinned to normal by .row — it's a list row promoted, not prose */
}
.title-row h1 {
  margin-bottom: 0;
}
.post .meta {
  color: var(--muted);
  margin-top: 0;
}
/* Coords + visit → sit a blank line below the title row, like a dateline. */
.post .meta.coords,
.post .meta.visit {
  margin-top: var(--space-md);
}
/* Seam on the header's *last* element only — internal gaps stay tight. */
.title-row:not(:has(+ .meta)),
.post .meta.coords:not(:has(+ .meta.visit)),
.post .meta.visit {
  margin-bottom: var(--space-xl);
}
.post {
  line-height: var(--leading-relaxed);
}
/* Undo the UA's centred th; top-align so wrapped cells keep the baseline. */
.post :is(th, td) {
  text-align: left;
  vertical-align: top;
}
.post .index-section {
  margin-top: var(--gap-section);
}

.career-outline {
  margin-top: var(--space-xl);
}

.career-lede {
  margin: 0 0 var(--space-2xs);
}
.career-arcs {
  margin-bottom: var(--space-xl);
}

/* /career sign-in — global field + button grammar; only layout here. */
.login {
  max-width: 48ch;
  margin-block: var(--space-2xl);
  display: flex;
  flex-direction: column;
}
/* Flex column doesn't collapse margins — zero them and set rhythm here. */
.login > * {
  margin: 0;
}
.login-h {
  font-weight: var(--weight-bold);
}
.login-form {
  margin-top: var(--space-lg);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xs);
}
.login-label {
  color: var(--muted);
}
.login-controls {
  display: flex;
  align-items: center;
  gap: var(--gap-inline);
  align-self: stretch;
}
.login-controls input {
  flex: 1 1 18ch;
  min-width: 0;
}
/* Incorrect-password note — error intent. index.js reveals it on ?error=1. */
.login-error {
  margin-top: var(--space-sm);
  color: var(--accent-error-text);
}

/* Heading ladder — rungs separate by weight, style, and colour, never size:
   h1 bold upright, h2/.subhead bold italic, h3 italic, h4 italic muted. */
h2,
.subhead {
  font-size: var(--text-base);
  line-height: var(--leading-tight);
  font-weight: var(--weight-bold);
  font-style: italic;
  color: var(--fg);
}
h2 {
  margin-top: var(--space-xl);
}
h3,
h4 {
  font-size: var(--text-base);
  line-height: var(--leading-tight);
  font-style: italic;
  font-weight: var(
    --weight-normal
  ); /* drop the UA heading bold */
  margin-top: var(--space-xl);
}
h3 {
  color: var(--fg);
}
h4 {
  color: var(--muted);
}
/* List leading so the hover chip matches the rows it heads. */
.subhead {
  --link-fg: var(--fg);
  text-decoration-line: none;
  line-height: var(--leading-normal);
}
/* Split head: "More in" prefix + #term link are siblings — flow them left. */
.section-head.has-prefix {
  justify-content: flex-start;
  gap: var(--gap-inline);
}
/* :where() is load-bearing: at 0,2,0 this would outrank a:hover (0,1,1) and
   strand the invert steel-on-steel. */
.subhead:where(.head-tag) {
  color: var(--accent-tag);
}
.subhead.head-tag {
  --accent: var(--accent-tag);
  --accent-hover: var(--accent-tag-light);
}

/* Blockquote — quoted by *position*, never by restyling. */
blockquote {
  /* Gap left of the rule matches its right padding — a uniform inset. */
  margin: var(--space-lg) 0 var(--space-lg) var(--space-md);
  padding-left: var(--space-md);
  border-left: var(--border-thick) solid var(--quote);
  color: var(--quote);
}
/* Attribution — muted, upright (drop the UA italic). */
blockquote cite {
  color: var(--muted);
  font-style: normal;
}

img {
  max-width: 100%;
  height: auto;
}

/* Photo sets — justified rows. Preferred width = --ar × target row height, so
   flex wrap breaks the lines itself, no media queries; grow ∝ --ar levels the
   heights (×10 keeps grow sums ≥ 1, so a lone portrait claims its space). Growth
   uncapped: every row fills the measure, a stranded photo goes full-bleed. --ar
   is set inline per image by shot-img.html. */
.shots {
  display: flex;
  flex-wrap: wrap; /* figcaption (width: 100%) drops to its own line */
  align-items: flex-start;
  gap: var(--space-sm);
  margin: var(--space-2xl) 0; /* double prose seam outside a run of photo rows */
}
/* Consecutive figures read as one set. Margins collapse — set both sides. */
.shots:has(+ .shots) {
  margin-bottom: var(--space-sm);
}
.shots + .shots {
  margin-top: var(--space-sm);
}
/* Child combinators: post-JS the lightbox button is the flex item. */
.shots > img,
.shots > picture,
.shots > .lightbox-link {
  min-width: 0;
  flex: calc(var(--ar, 1) * 10) 1 calc(var(--ar, 1) * var(--shots-row));
}
.shots img {
  outline: var(--border-rim) solid var(--photo-edge);
  border-radius: var(--radius-sm);
  /* Rim colour fills the box so a photo has shape while it loads. */
  background-color: var(--photo-edge);
}
/* Art-directed shot: the <picture> is the flex item and carries --ar inline. */
.shots picture img {
  width: 100%;
}
/* width: 100% drops the caption to its own flex line. */
.shots figcaption {
  width: 100%;
  color: var(--muted);
  font-size: var(--text-sm);
}

/* Entry hero — same justified engine as any set. Default is full-bleed. */
.shots.hero {
  margin-top: var(
    --space-xl
  ); /* the article header's seam, not the double prose seam */
}

/* `hero_layout: native` — natural size, centered, capped. Both selectors are
   load-bearing: site.js may wrap the img, changing which element is the item. */
.shots.hero.native > img:only-child,
.shots.hero.native > .lightbox-link:only-child {
  flex: 0 1 auto;
  width: auto;
  max-width: 100%;
  max-height: var(--hero-max);
  margin-inline: auto; /* center in the measure */
}
.shots.hero.native > .lightbox-link:only-child img {
  width: auto; /* override .lightbox-link img { width: 100% } so the cap drives
                  the size and the ratio holds */
  max-width: 100%;
  max-height: var(--hero-max);
}

/* `hero_layout: mosaic` — 1 big left, 2 stacked right; definite height so the
   rows split evenly and photos crop to fill. */
.shots.hero.mosaic {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: var(--space-sm);
  height: var(--hero-mosaic-h);
}
.shots.hero.mosaic > img,
.shots.hero.mosaic > .lightbox-link {
  flex: none; /* grid items — drop the .shots flex sizing */
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  margin: 0;
}
.shots.hero.mosaic > :first-child {
  grid-column: 1;
  grid-row: 1 / 3; /* big left tile, spans both rows */
}
.shots.hero.mosaic img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Lightbox (baseof.html, wired by site.js). JS wraps each shots image in a
   .lightbox-link <button>, which then stands in as the .shots flex item (site.js
   copies --ar up) and wears no chrome — the frame stays on the image. */
.lightbox-link {
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm); /* the focus ring follows the frame */
  background: none;
  cursor: pointer;
}
.lightbox-link img {
  display: block;
  width: 100%;
}
/* Images can't invert, so the ring is the indicator — offset clear of the halo. */
.lightbox-link:focus-visible {
  outline: var(--border-thick) solid var(--accent);
  outline-offset: var(--border-rim);
}
/* Full-bleed: no padding — the chrome overlays the photo. */
.lightbox {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  border: 0;
  padding: 0;
  /* --bg override drives the whole chrome cascade (--glass recomputes from it);
     color-scheme:dark resolves the light-dark() neutrals light-on-black. */
  color-scheme: dark;
  --bg: var(--lightbox-bg);
  background: var(--bg);
  color: var(--fg);
  overflow: hidden;
  overscroll-behavior: contain;
}
/* Author display beats the UA's dialog:not([open]) rule — restate the hide. */
.lightbox:not([open]) {
  display: none;
}
/* Solid sheet of --bg, never --glass: a photo edge bleeding through a
   translucent wash muddies it. Safe-area insets clear the notches. */
.lightbox-close,
.lightbox-chrome {
  position: absolute;
  z-index: 1;
  background: var(--bg);
}
/* First in the dialog and autofocused. */
.lightbox-close {
  top: calc(var(--space-sm) + env(safe-area-inset-top));
  right: calc(var(--space-sm) + env(safe-area-inset-right));
  padding: var(--space-2xs) var(--space-sm);
  border-radius: var(--radius-sm);
  --link-fg: var(--muted);
}
/* Caption left, pager right, one row. flex-end so a multi-line caption grows
   upward off the pager's baseline. Pin with margin-left:auto on the bar, NOT
   justify:space-between — that yanks a lone pager (no caption) to the left. */
.lightbox-chrome {
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: var(--space-sm) calc(var(--space-md) + env(safe-area-inset-right))
    calc(var(--space-sm) + env(safe-area-inset-bottom))
    calc(var(--space-md) + env(safe-area-inset-left));
  display: flex;
  flex-flow: row nowrap;
  align-items: flex-end;
  column-gap: var(--space-md);
}
/* Short windows: --scale is width-driven, so tighten padding by hand here. */
@media (max-height: 30rem) {
  .lightbox-chrome {
    padding-top: var(--space-2xs);
    padding-bottom: calc(var(--space-2xs) + env(safe-area-inset-bottom));
  }
  .lightbox-close {
    top: calc(var(--space-2xs) + env(safe-area-inset-top));
  }
}
.lightbox-chrome[hidden] {
  display: none;
}
/* max-width/height keeps the image whole and centred; object-fit would crop. */
.lightbox-stage {
  position: absolute;
  inset: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* site.js owns every touch here — the UA's scroll/zoom must not compete. */
  touch-action: none;
}
/* Full-bleed, so no frame — the .shots halo would clip against the viewport. */
.lightbox-img {
  max-width: 100%;
  max-height: 100%;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  /* Gesture settles ride the spring on transform (translate/scale/tilt are one
     property). site.js suspends both with .is-dragging so drags have no lag. */
  transform-origin: center;
  transition:
    transform var(--duration-med) var(--ease-spring),
    opacity var(--duration-fast) var(--ease);
}
.lightbox-img.is-dragging {
  transition: none;
}
/* Page turns — two crossfade phases chained in site.js via animationend. */
.lightbox-img.is-leave {
  animation: lightbox-leave var(--duration-snap) var(--ease) forwards;
}
.lightbox-img.is-enter {
  animation: lightbox-enter var(--duration-snap) var(--ease) backwards;
}
@keyframes lightbox-leave {
  to {
    opacity: 0;
  }
}
@keyframes lightbox-enter {
  from {
    opacity: 0;
  }
}
/* Pinned bottom-right; flex-shrink:0 so the caption yields width, not the
   pager. Same --text-sm as the caption so their baselines sit level. */
.lightbox-bar {
  margin: 0;
  margin-left: auto;
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: var(--gap-inline);
  color: var(--muted);
  font-size: var(--text-sm);
}
.lightbox-step {
  --link-fg: var(--muted);
}
/* Author display:flex beats the UA [hidden]{display:none} — restate the hide. */
.lightbox-bar[hidden] {
  display: none;
}
.lightbox-count {
  font-size: var(--text-sm);
}
/* Desc toggle — inline after the caption; its leading gap collapses when hidden. */
.lightbox-desc-toggle {
  --link-fg: var(--muted);
  margin-left: var(--gap-inline);
}
/* No max-width: flows to the viewport, capped only by the pager's column;
   min-width:0 lets it wrap instead of overflowing. */
.lightbox-caption {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  color: var(--muted);
  font-size: var(--text-sm);
}
.lightbox-caption[hidden] {
  display: none;
}
/* `hide`: caption collapses, bar bg clears. The pager stays — navigation
   never vanishes. */
.lightbox.desc-hidden .lightbox-chrome {
  background: transparent;
}
.lightbox.desc-hidden .lightbox-cap-text {
  display: none;
}
.lightbox.desc-hidden .lightbox-desc-toggle {
  margin-left: 0;
}

/* Code — the site is all monospace, so code differentiates by a surface-tint
   chip: the link-highlight geometry (bg + two box-shadow side copies) so glyphs
   hold the character grid. Never link-like: no underline, accent, or invert. */
code {
  /* Always monospace, even under the serif body family. */
  font-family: var(--font-mono);
  background: var(--surface);
  border-radius: var(--radius-sm);
  box-shadow:
    calc(-1 * var(--link-pad-x)) 0 0 var(--surface),
    var(--link-pad-x) 0 0 var(--surface);
  /* One chip per token: never split mid-chip at a line break. Long commands
     belong in blocks. */
  white-space: nowrap;
}
/* Inside a block the pre is the chip. */
pre code {
  background: none;
  border-radius: 0;
  box-shadow: none;
  white-space: inherit;
}

pre {
  overflow-x: auto;
  padding: var(--space-md);
  background: var(--surface);
  border-radius: var(--radius-md);
  line-height: var(--leading-snug);
}

/* Forced colors strips the tint fills — use a real border there. */
@media (forced-colors: active) {
  code,
  pre {
    border: var(--border-thin) solid;
  }
  pre code {
    border: 0;
  }
}

/* Copy on code blocks (site.js, JS-only). */
.codeblock {
  position: relative;
}
.copy-code {
  position: absolute;
  top: var(--space-2xs);
  right: var(--space-xs);
  /* Hover/focus wears the action/mark intent, never green — green is reserved
     for the `copied` flash, so interacting and succeeding never share a colour. */
  --link-fg: var(--muted);
  --accent: var(--mark);
  --accent-hover: var(--mark-light);
  /* Glass chip so a code line scrolling under the corner stays legible. */
  padding-inline: var(--link-pad-x);
  background: var(--glass);
  -webkit-backdrop-filter: blur(var(--blur-glass));
  backdrop-filter: blur(var(--blur-glass));
  border-radius: var(--radius-sm);
}
.copy-code.is-copied {
  /* box-shadow override required: else the side copies keep painting --mark
     (hover still applies under the pointer) and the pill grows fuchsia ears. */
  background: var(--green);
  color: var(--accent-fg);
  text-decoration-color: transparent;
  box-shadow:
    calc(-1 * var(--link-pad-x)) 0 0 var(--green),
    var(--link-pad-x) 0 0 var(--green),
    0 0 var(--glow-blur) var(--link-glow);
}

hr {
  border: none;
  border-top: var(--border-thin) solid var(--rule);
  /* Section rhythm + a blank line (1lh) each side, so the rule sits clear. */
  margin: calc(var(--gap-section) + 1lh) 0;
}

/* ───────────────────────────────────────────────────────────────────────────
   FORM CONTROLS

   Primitives wear the site chip + two-weight invert grammar, all on tokens — so
   the tubes recolour them for free; the terminal block at the foot of this
   section adds only the character/line costume, not colour.

   :where() keeps this whole layer at ZERO specificity, so any styled component
   (.cmd-input, .btn-link) wins without !important. */

/* Fields, select, textarea — a bordered surface chip. */
:where(
  input:where(
    [type="text"],
    [type="email"],
    [type="url"],
    [type="password"],
    [type="number"],
    [type="tel"],
    [type="search"],
    [type="date"]
  ),
  textarea,
  select
):where(:not(.cmd-input)) {
  font: inherit;
  color: var(--fg);
  background: var(--surface);
  border: var(--border-thin) solid var(--rule);
  border-radius: var(--radius-sm);
  padding: var(--space-3xs) var(--space-xs);
  accent-color: var(--accent); /* caret + native affordances → link intent */
  transition:
    border-color var(--duration-fast) var(--ease),
    box-shadow var(--duration-fast) var(--ease);
}
:where(input, textarea, select):where(:not(.cmd-input)):focus-visible {
  border-color: var(--accent);
  outline: var(--border-thick) solid var(--accent);
  outline-offset: 2px;
}
/* Hover suggests in the -light step; never on a disabled control. */
:where(input, textarea, select):where(:not(.cmd-input, :disabled)):hover {
  border-color: var(--accent-hover);
}
/* The native select arrow ignores the theme — draw a currentColor chevron. */
:where(select):where(:not(.cmd-input)) {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: 1.7em;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    right 0.85em center,
    right 0.5em center;
  background-size: 0.34em 0.34em;
  background-repeat: no-repeat;
}
:where(textarea) {
  display: block;
  width: 100%;
  line-height: var(--leading-snug);
}
::placeholder {
  color: var(--muted);
  opacity: 1;
}

/* Buttons — a pushable chip. Underline treatment is pre-set here so the terminal
   costume only flips the line on. Never .btn-link (a link in disguise). */
:where(
  button,
  input:where([type="button"], [type="submit"], [type="reset"])
):where(:not(.btn-link)) {
  font: inherit;
  font-weight: var(--weight-bold);
  color: var(--fg);
  background: var(--surface);
  border: var(--border-thin) solid var(--rule);
  border-radius: var(--radius-sm);
  padding: var(--space-3xs) var(--space-sm);
  cursor: pointer;
  text-decoration-line: none;
  text-decoration-thickness: var(--border-thin);
  text-underline-offset: 0.18em;
  text-decoration-color: color-mix(
    in srgb,
    currentColor var(--tint-line),
    transparent
  );
  transition:
    background-color var(--duration-fast) var(--ease),
    border-color var(--duration-fast) var(--ease),
    color var(--duration-fast) var(--ease);
}
:where(
    button,
    input:where([type="button"], [type="submit"], [type="reset"])
  ):where(:not(.btn-link)):hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
  color: var(--accent-fg);
  text-decoration-color: transparent;
}
/* Focus/active/pressed commit in the full accent — the "on" toggle. */
:where(
    button,
    input:where([type="button"], [type="submit"], [type="reset"])
  ):where(:not(.btn-link)):is(
    :focus-visible,
    :active,
    .is-on,
    [aria-pressed="true"]
  ) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
  text-decoration-color: transparent;
  outline: var(--border-thick) solid transparent; /* forced-colors keeps a ring */
}

/* Checkbox + radio — box drawn from tokens; the mark is a ::before. */
:where(input:where([type="checkbox"], [type="radio"])):where(:not(.cmd-input)) {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  display: inline-grid;
  place-content: center;
  inline-size: 1.25em;
  block-size: 1.25em;
  margin: 0;
  vertical-align: -0.25em;
  background: var(--surface);
  border: var(--border-thin) solid var(--rule);
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--ease),
    border-color var(--duration-fast) var(--ease);
}
:where(input[type="checkbox"]):where(:not(.cmd-input)) {
  border-radius: var(--radius-sm);
}
:where(input[type="radio"]):where(:not(.cmd-input)) {
  border-radius: 50%;
}
:where(input:where([type="checkbox"], [type="radio"]))::before {
  content: "";
  inline-size: 0.72em;
  block-size: 0.72em;
  transform: scale(0);
  background: var(--accent-fg);
  transition: transform var(--duration-fast) var(--ease-spring);
}
:where(input[type="checkbox"])::before {
  /* a checkmark clipped from the filled box */
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
}
:where(input[type="radio"])::before {
  border-radius: 50%;
  inline-size: 0.62em;
  block-size: 0.62em;
}
:where(input:where([type="checkbox"], [type="radio"]):checked):where(
  :not(.cmd-input)
) {
  background: var(--accent);
  border-color: var(--accent);
}
:where(input:where([type="checkbox"], [type="radio"]):checked)::before {
  transform: scale(1);
}
:where(input:where([type="checkbox"], [type="radio"])):where(
    :not(.cmd-input)
  ):focus-visible {
  outline: var(--border-thick) solid var(--accent);
  outline-offset: 2px;
}

/* Range — thin rule track, accent thumb drawn per engine. */
:where(input[type="range"]) {
  appearance: none;
  -webkit-appearance: none;
  width: min(100%, 14rem);
  height: 1.2em;
  background: none;
  cursor: pointer;
  accent-color: var(--accent);
}
:where(input[type="range"])::-webkit-slider-runnable-track {
  height: var(--border-thick);
  background: var(--rule);
  border-radius: var(--radius-sm);
}
:where(input[type="range"])::-moz-range-track {
  height: var(--border-thick);
  background: var(--rule);
  border-radius: var(--radius-sm);
}
:where(input[type="range"])::-moz-range-progress {
  height: var(--border-thick);
  background: var(--accent);
  border-radius: var(--radius-sm);
}
:where(input[type="range"])::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  inline-size: 1em;
  block-size: 1em;
  margin-top: calc((var(--border-thick) - 1em) / 2); /* centre on the track */
  background: var(--accent);
  border: none;
  border-radius: 50%;
}
:where(input[type="range"])::-moz-range-thumb {
  inline-size: 1em;
  block-size: 1em;
  background: var(--accent);
  border: none;
  border-radius: 50%;
}
:where(input[type="range"]):focus-visible {
  outline: var(--border-thick) solid var(--accent);
  outline-offset: 2px;
}
:where(input[type="range"]):hover::-webkit-slider-thumb {
  background: var(--accent-hover);
}
:where(input[type="range"]):hover::-moz-range-thumb {
  background: var(--accent-hover);
}
/* Stop marker at a slider's default. Opt in by setting --range-default to the
   value's FRACTION of the range, (value - min) / (max - min) ∈ 0–1. Inset half a
   thumb each side so the tick lands under the thumb centre. More stops = more
   background layers, same recipe. */
:where(input[type="range"]) {
  --range-stop: color-mix(in srgb, var(--muted) 60%, transparent);
}
:where(input[type="range"][style*="--range-default"]) {
  background: linear-gradient(var(--range-stop), var(--range-stop)) no-repeat
    calc(0.5em + (100% - 1em) * var(--range-default)) center /
    var(--border-thick) 0.7em;
}

/* ── Terminal costume. Colour comes free from the token recolour; this only
   swaps *shapes*: no rounded corners, checkbox/radio become [ ]/[x] and ( )/(•),
   buttons wear < > brackets. Base inverts still fire, so only rest is restated. */
:root[data-theme^="phosphor"]
  :where(input, textarea, select, button):where(:not(.btn-link, .cmd-input)) {
  border-radius: 0;
}
/* Bracket glyphs on the ::before. They ride currentColor, so color must be set
   to --fg — else the UA default reads white on the dark tube. */
:root[data-theme^="phosphor"]
  :where(input:where([type="checkbox"], [type="radio"])):where(
    :not(.cmd-input)
  ) {
  inline-size: auto;
  block-size: auto;
  border: 0;
  background: none;
  color: var(--fg);
  vertical-align: baseline;
  font-family: var(--font-mono);
}
:root[data-theme^="phosphor"]
  :where(input:where([type="checkbox"], [type="radio"]))::before {
  content: "[\00a0]";
  inline-size: auto;
  block-size: auto;
  background: none;
  clip-path: none;
  transform: none;
  border-radius: 0;
  font-weight: var(--weight-bold);
}
:root[data-theme^="phosphor"] :where(input[type="radio"])::before {
  content: "(\00a0)";
}
:root[data-theme^="phosphor"]
  :where(input:where([type="checkbox"], [type="radio"]):checked):where(
    :not(.cmd-input)
  ) {
  background: none;
}
:root[data-theme^="phosphor"] :where(input[type="checkbox"]:checked)::before {
  content: "[X]"; /* capital — terminal-authentic, reads bold */
  transform: none;
}
:root[data-theme^="phosphor"] :where(input[type="radio"]:checked)::before {
  content: "(\2022)"; /* (•) */
  transform: none;
}
/* Underline the label's *text span* only, never the [ ]/[x] indicator. Enabled
   options only — the disabled block below strikes the inert ones. */
:root[data-theme^="phosphor"]
  label:where(
    :has(input:where([type="checkbox"], [type="radio"]):not(:disabled))
  )
  > span {
  text-decoration-line: underline;
  text-decoration-thickness: var(--border-thin);
  text-underline-offset: 0.18em;
  text-decoration-color: color-mix(
    in srgb,
    currentColor var(--tint-line),
    transparent
  );
}
/* Button → < label > brackets. This resting rule is 0,2,0, so the base invert
   can't reach it — the fill is restated below at terminal scope. */
:root[data-theme^="phosphor"]
  :where(
    button,
    input:where([type="button"], [type="submit"], [type="reset"])
  ):where(:not(.btn-link)) {
  border: 0;
  background: none;
  /* A lit button keeps the standard chip radius, unlike the other primitives. */
  border-radius: var(--radius-sm);
  padding-inline: var(--gap-dir);
  text-decoration-line: underline;
}
:root[data-theme^="phosphor"]
  :where(
    button,
    input:where([type="button"], [type="submit"], [type="reset"])
  ):where(:not(.btn-link))::before {
  content: "<\00a0";
}
:root[data-theme^="phosphor"]
  :where(
    button,
    input:where([type="button"], [type="submit"], [type="reset"])
  ):where(:not(.btn-link))::after {
  content: "\00a0>";
}
:root[data-theme^="phosphor"]
  :where(
    button,
    input:where([type="button"], [type="submit"], [type="reset"])
  ):where(:not(.btn-link)):hover {
  background: var(--accent-hover);
  color: var(--accent-fg);
  text-decoration-color: transparent;
  /* Halation. Rides --glow-blur so the flicker block can wobble it. */
  box-shadow: 0 0 var(--glow-blur) var(--link-glow);
}
:root[data-theme^="phosphor"]
  :where(
    button,
    input:where([type="button"], [type="submit"], [type="reset"])
  ):where(:not(.btn-link)):is(
    :focus-visible,
    :active,
    .is-on,
    [aria-pressed="true"]
  ) {
  background: var(--accent);
  color: var(--accent-fg);
  text-decoration-color: transparent;
  box-shadow: 0 0 var(--glow-blur) var(--link-glow);
}
/* …except the lightbox wrapper: an *image* button — no brackets, underline, or
   lit chip. Outscores the :where()-scoped rules above. */
:root[data-theme^="phosphor"] .lightbox-link {
  padding: 0;
  text-decoration-line: none;
}
:root[data-theme^="phosphor"] .lightbox-link::before,
:root[data-theme^="phosphor"] .lightbox-link::after {
  content: none;
}
:root[data-theme^="phosphor"]
  .lightbox-link:is(:hover, :focus-visible, :active) {
  background: none;
  box-shadow: none;
}
:root[data-theme^="phosphor"]
  :where(input[type="range"])::-webkit-slider-thumb {
  border-radius: 0;
}
:root[data-theme^="phosphor"] :where(input[type="range"])::-moz-range-thumb {
  border-radius: 0;
}
/* Hover/focus blooms the indicator, from the input or its label. Rides
   --glow-blur so the flicker block can wobble it; steady here. */
:root[data-theme^="phosphor"]
  :where(input:where([type="checkbox"], [type="radio"])):where(
    :not(.cmd-input, :disabled)
  ):is(:hover, :focus-visible),
:root[data-theme^="phosphor"]
  label:has(input:where([type="checkbox"], [type="radio"]):not(:disabled)):hover
  :where(input:where([type="checkbox"], [type="radio"])) {
  text-shadow: 0 0 var(--glow-blur) var(--term-glow);
}
/* Phosphor flicker — interaction-only, reduced-motion opt-out (the resting
   screen never moves). Animates only --glow-blur / --glow-bright. */
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme^="phosphor"]
    :where(
      button,
      input:where([type="button"], [type="submit"], [type="reset"])
    ):where(:not(.btn-link)):is(
      :hover,
      :focus-visible,
      :active,
      .is-on,
      [aria-pressed="true"]
    ),
  :root[data-theme^="phosphor"]
    :where(input:where([type="checkbox"], [type="radio"])):where(
      :not(.cmd-input, :disabled)
    ):is(:hover, :focus-visible),
  :root[data-theme^="phosphor"]
    label:has(
      input:where([type="checkbox"], [type="radio"]):not(:disabled)
    ):hover
    :where(input:where([type="checkbox"], [type="radio"])) {
    animation: term-glow-flicker 1.5s linear infinite;
    filter: brightness(var(--glow-bright));
  }
}

/* ── Disabled — the one global treatment: 60% opacity + strike + inert, for
   :disabled and aria-disabled alike. The dim goes on the whole option (the
   wrapping <label> when present) so it never compounds. The :root prefix lifts
   these to 0,2,0 to beat the terminal bracket buttons' underline; being last
   wins the source-order tie.
   Never .btn-link — striking its ↑ or Aa glyph reads as damage, not disabled;
   it carries its own dim below. */
:root :where(button:not(.btn-link), input, select, textarea):disabled,
:root :where([aria-disabled="true"]):where(:not(.btn-link)),
:root :where(label):has(:disabled) {
  opacity: 0.6;
  text-decoration-line: line-through;
}
:root :where(label):has(:disabled) :disabled {
  opacity: 1; /* label already dims it — don't stack opacities */
}
/* Inert reaches every disabled item, .btn-link included. */
:root :where(button, input, select, textarea):disabled,
:root :where([aria-disabled="true"]) {
  cursor: default;
  pointer-events: none;
}
