/* ═══════════════════════════════════════════════════════════════════════
   blog.css — the reading room and the article surface.

   ── TWO GROUNDS, NEITHER OF THEM A DEFAULT ──────────────────────────────

   This surface used to be committed to one dark theme, on the argument that a
   long-form reader is better served by one considered palette than by two
   adequate ones. The argument was right about *palettes* and wrong about
   *readers*: which ground is comfortable for eight minutes of prose depends
   on the room the reader is sitting in, not on the site.

   So there are two, and neither is the generic version of itself:

     INK   #0f0b09 — a warm brown-black, not the blue-black that every dark
                     theme defaults to. Lamp-lit rather than backlit.
     SAND  #f3ece3 — warm paper, not #fff. A white page is a spotlight; sand
                     holds the same text at a fraction of the glare.

   Both are the hub's warmth (hub.css runs the same brown-black ground and a
   coral accent), so /blog and / read as one site rather than two products.

   ── HOW A THEME IS CHOSEN ───────────────────────────────────────────────

   Three states, and the order matters:

     1. no [data-theme] attribute  → follow prefers-color-scheme
     2. [data-theme="light"|"dark"] → the reader chose; their choice wins
     3. no JS at all                → ink, and every word still readable

   The values live in ONE place (the --ink-* / --sand-* pairs below); the
   theme rules only re-point aliases at them. A colour defined only inside a
   media query is how a theme silently loses half its palette.

   ── THE PALETTE A POST OWNS ─────────────────────────────────────────────

   Every post owns one of eight colourways, seeded from its slug by
   signature.mjs. Because the page cannot know at build time which ground the
   reader will land on, each carrier element ships BOTH mixes inline as
   --cd1/--cd2 (ink) and --cl1/--cl2 (sand); the rules below resolve --c1/--c2
   from whichever ground is active. An inline --c1 would beat every theme rule
   and freeze the post into one ground — which is exactly the bug this shape
   exists to prevent.

   ── THE ARTWORK IS A PLATE ──────────────────────────────────────────────

   Signatures and the WebGL terrain are always painted on --plate (#0f0b09),
   in BOTH themes. The artwork is a photographic object sitting on the page,
   not a layer of the page. That is one deliberate constant: it means the
   generative colours never have to be re-tuned for sand, and a post's hero is
   recognisably the same image in either theme.

   Type stack and the #06060c-era structure are otherwise unchanged: same
   Bricolage / Inter Tight / Fraunces / JetBrains Mono, same reading room.
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* ── The two grounds, defined once. Nothing below restates a value. ── */
  --ink-bg:        #0f0b09;
  --ink-bg-2:      #171110;
  --ink-bg-3:      #211917;
  --ink-surface:   rgba(255, 242, 232, 0.038);
  --ink-surface-2: rgba(255, 242, 232, 0.075);
  --ink-line:      rgba(255, 238, 226, 0.10);
  --ink-line-2:    rgba(255, 238, 226, 0.20);
  --ink-text:      #f5ede4;
  --ink-text-2:    #b9ac9f;
  --ink-text-3:    #8d8076;
  --ink-text-hi:   #fffaf4;
  --ink-on-accent: #140d0a;
  --ink-accent:    #ff7a4d;
  --ink-accent-2:  #ffab4a;
  --ink-grid:      rgba(255, 238, 226, 0.024);
  --ink-shadow:    rgba(0, 0, 0, 0.55);
  --ink-scrim:     15, 11, 9;
  --ink-code:      #ded2c6;
  --ink-ok:        #5fd39b;
  /* Spine tick hues and syntax tokens are content colours, not chrome, so
     they get a real pair per ground rather than an opacity trick. */
  --ink-t-table:   #ffc247;
  --ink-t-quote:   #f472b6;
  --ink-t-image:   #5fd39b;
  --ink-hl-kw:     #c4a2ff;
  --ink-hl-str:    #7ee787;
  --ink-hl-num:    #ffc247;
  --ink-hl-fn:     #7ee0ff;
  --ink-hl-var:    #ff9ecb;
  --ink-hl-type:   #58d5ff;
  --ink-hl-com:    #8d8076;
  --ink-hl-meta:   #ff9f5a;
  --ink-hl-del:    #ff7a8a;

  --sand-bg:        #f3ece3;
  --sand-bg-2:      #ebe2d6;
  --sand-bg-3:      #e0d5c6;
  --sand-surface:   rgba(26, 19, 16, 0.035);
  --sand-surface-2: rgba(26, 19, 16, 0.065);
  --sand-line:      rgba(26, 19, 16, 0.13);
  --sand-line-2:    rgba(26, 19, 16, 0.28);
  --sand-text:      #1a1310;
  --sand-text-2:    #574b43;
  --sand-text-3:    #7a6d64;
  --sand-text-hi:   #0d0806;
  --sand-on-accent: #fffaf4;
  --sand-accent:    #b8410f;
  --sand-accent-2:  #8a5a08;
  --sand-grid:      rgba(26, 19, 16, 0.026);
  --sand-shadow:    rgba(74, 55, 44, 0.18);
  --sand-scrim:     243, 236, 227;
  --sand-code:      #2a211c;
  --sand-ok:        #15803d;
  --sand-t-table:   #a16207;
  --sand-t-quote:   #be185d;
  --sand-t-image:   #15803d;
  --sand-hl-kw:     #6d28d9;
  --sand-hl-str:    #15803d;
  --sand-hl-num:    #a16207;
  --sand-hl-fn:     #0e7490;
  --sand-hl-var:    #be185d;
  --sand-hl-type:   #1d4ed8;
  --sand-hl-com:    #857a71;
  --sand-hl-meta:   #c2410c;
  --sand-hl-del:    #be123c;

  /* ── Aliases. Default: ink. ── */
  --bg: var(--ink-bg);
  --bg-2: var(--ink-bg-2);
  --bg-3: var(--ink-bg-3);
  --surface: var(--ink-surface);
  --surface-2: var(--ink-surface-2);
  --line: var(--ink-line);
  --line-strong: var(--ink-line-2);
  --text: var(--ink-text);
  --text-2: var(--ink-text-2);
  --text-3: var(--ink-text-3);
  --text-hi: var(--ink-text-hi);
  --on-accent: var(--ink-on-accent);
  --accent: var(--ink-accent);
  --accent-2: var(--ink-accent-2);
  --grid-ink: var(--ink-grid);
  --shadow: var(--ink-shadow);
  --scrim: var(--ink-scrim);
  --code-ink: var(--ink-code);
  --ok: var(--ink-ok);
  --t-table: var(--ink-t-table);
  --t-quote: var(--ink-t-quote);
  --t-image: var(--ink-t-image);
  --hl-kw: var(--ink-hl-kw);
  --hl-str: var(--ink-hl-str);
  --hl-num: var(--ink-hl-num);
  --hl-fn: var(--ink-hl-fn);
  --hl-var: var(--ink-hl-var);
  --hl-type: var(--ink-hl-type);
  --hl-com: var(--ink-hl-com);
  --hl-meta: var(--ink-hl-meta);
  --hl-del: var(--ink-hl-del);
  --sig-mix: 1;   /* how much a signature/plate asserts itself, per ground */

  /* The plate. Constant across themes — see the header note. */
  --plate: #0f0b09;

  --font-display: 'Bricolage Grotesque', 'Inter Tight', sans-serif;
  --font-body: 'Inter Tight', system-ui, sans-serif;
  --font-serif: 'Fraunces', Georgia, serif;
  --font-mono: 'JetBrains Mono', ui-monospace, monospace;

  --measure: 68ch;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ── State 1: no choice recorded, OS says light. ──────────────────────
   Scoped with :not([data-theme="dark"]) so a reader who explicitly picked
   ink on a light-mode machine keeps ink. */
@media (prefers-color-scheme: light) {
  html:not([data-theme='dark']) {
    --bg: var(--sand-bg);
    --bg-2: var(--sand-bg-2);
    --bg-3: var(--sand-bg-3);
    --surface: var(--sand-surface);
    --surface-2: var(--sand-surface-2);
    --line: var(--sand-line);
    --line-strong: var(--sand-line-2);
    --text: var(--sand-text);
    --text-2: var(--sand-text-2);
    --text-3: var(--sand-text-3);
    --text-hi: var(--sand-text-hi);
    --on-accent: var(--sand-on-accent);
    --accent: var(--sand-accent);
    --accent-2: var(--sand-accent-2);
    --grid-ink: var(--sand-grid);
    --shadow: var(--sand-shadow);
    --scrim: var(--sand-scrim);
    --code-ink: var(--sand-code);
  --ok: var(--sand-ok);
  --t-table: var(--sand-t-table);
  --t-quote: var(--sand-t-quote);
  --t-image: var(--sand-t-image);
  --hl-kw: var(--sand-hl-kw);
  --hl-str: var(--sand-hl-str);
  --hl-num: var(--sand-hl-num);
  --hl-fn: var(--sand-hl-fn);
  --hl-var: var(--sand-hl-var);
  --hl-type: var(--sand-hl-type);
  --hl-com: var(--sand-hl-com);
  --hl-meta: var(--sand-hl-meta);
  --hl-del: var(--sand-hl-del);
    --ok: var(--sand-ok);
    --t-table: var(--sand-t-table);
    --t-quote: var(--sand-t-quote);
    --t-image: var(--sand-t-image);
    --hl-kw: var(--sand-hl-kw);
    --hl-str: var(--sand-hl-str);
    --hl-num: var(--sand-hl-num);
    --hl-fn: var(--sand-hl-fn);
    --hl-var: var(--sand-hl-var);
    --hl-type: var(--sand-hl-type);
    --hl-com: var(--sand-hl-com);
    --hl-meta: var(--sand-hl-meta);
    --hl-del: var(--sand-hl-del);
    --sig-mix: 0.82;
  }
}

/* ── State 2: the reader chose sand. Wins over the media query above by
   being both later and more specific (html[attr] = 0,1,1). ── */
html[data-theme='light'] {
  --bg: var(--sand-bg);
  --bg-2: var(--sand-bg-2);
  --bg-3: var(--sand-bg-3);
  --surface: var(--sand-surface);
  --surface-2: var(--sand-surface-2);
  --line: var(--sand-line);
  --line-strong: var(--sand-line-2);
  --text: var(--sand-text);
  --text-2: var(--sand-text-2);
  --text-3: var(--sand-text-3);
  --text-hi: var(--sand-text-hi);
  --on-accent: var(--sand-on-accent);
  --accent: var(--sand-accent);
  --accent-2: var(--sand-accent-2);
  --grid-ink: var(--sand-grid);
  --shadow: var(--sand-shadow);
  --scrim: var(--sand-scrim);
  --code-ink: var(--sand-code);
  --sig-mix: 0.82;
}

/* ── The per-post colourway, resolved for the active ground ───────────
   `body`, `.row` and `.next` are the only elements that carry an inline
   pair; everything else inherits --c1/--c2 from one of them. The list is
   explicit rather than a universal selector because `* { --c1: ... }` would
   defeat every `var(--c1, fallback)` in this file — the property would
   always be set, sometimes to nothing, which is worse than unset. */
body, .row, .next {
  --c1: var(--cd1, var(--accent));
  --c2: var(--cd2, var(--accent-2));
}
:root {
  --post-c1: var(--pd1, var(--accent));
  --post-c2: var(--pd2, var(--accent-2));
}
@media (prefers-color-scheme: light) {
  html:not([data-theme='dark']) :is(body, .row, .next) {
    --c1: var(--cl1, var(--accent));
    --c2: var(--cl2, var(--accent-2));
  }
  html:not([data-theme='dark']) {
    --post-c1: var(--pl1, var(--accent));
    --post-c2: var(--pl2, var(--accent-2));
  }
}
html[data-theme='light'] :is(body, .row, .next) {
  --c1: var(--cl1, var(--accent));
  --c2: var(--cl2, var(--accent-2));
}
html[data-theme='light'] {
  --post-c1: var(--pl1, var(--accent));
  --post-c2: var(--pl2, var(--accent-2));
}

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

html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a { color: inherit; text-decoration: none; }
::selection { background: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--text-hi); }

/* ── Background field ──────────────────────────────────────────────────
   A slow drifting glow over a faint grid. Fixed, so it reads as a ground
   the content moves across rather than something that scrolls with it. */
.field { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.field-grid {
  position: absolute; inset: -2px;
  background-image:
    linear-gradient(var(--grid-ink) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-ink) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, #000 20%, transparent 100%);
}
.field-glow {
  position: absolute; top: -22vh; left: 50%;
  width: min(1200px, 130vw); aspect-ratio: 1;
  transform: translateX(-50%);
  background: radial-gradient(circle, color-mix(in srgb, var(--accent-2) 14%, transparent), color-mix(in srgb, var(--accent) 7%, transparent) 42%, transparent 68%);
  filter: blur(30px);
  animation: drift 26s var(--ease) infinite alternate;
}
@keyframes drift {
  from { transform: translate(-52%, -3%) scale(1); }
  to   { transform: translate(-46%, 5%) scale(1.14); }
}
@media (prefers-reduced-motion: reduce) { .field-glow { animation: none; } }

/* ── Nav ──────────────────────────────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  padding: 16px clamp(18px, 4vw, 48px);
  background: rgba(var(--scrim), 0.72);
  backdrop-filter: blur(16px);
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s, background 0.3s;
}
.nav.is-stuck { border-bottom-color: var(--line); background: rgba(var(--scrim), 0.9); }
.nav-home { display: inline-flex; align-items: center; gap: 10px; font-weight: 600; letter-spacing: -0.01em; }
.nav-mark {
  width: 10px; height: 10px; border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 55%, transparent);
}
.nav-links { display: flex; gap: clamp(14px, 2.4vw, 30px); font-size: 0.86rem; color: var(--text-2); }
.nav-links a { position: relative; padding: 4px 0; transition: color 0.25s; }
.nav-links a::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: 0; height: 1px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transition: right 0.35s var(--ease);
}
.nav-links a:hover { color: var(--text); }
.nav-links a:hover::after, .nav-links a[aria-current]::after { right: 0; }
.nav-links a[aria-current] { color: var(--text); }
@media (max-width: 560px) { .nav-links a:nth-child(1), .nav-links a:nth-child(3) { display: none; } }

/* ── Layout ───────────────────────────────────────────────────────────── */
.wrap { position: relative; z-index: 1; max-width: 1500px; margin: 0 auto; padding: clamp(40px, 7vw, 90px) clamp(18px, 4vw, 48px) 0; }

.eyebrow {
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--text-3); margin: 0 0 20px;
  display: flex; align-items: center; gap: 12px;
}
.eyebrow::before { content: ''; width: 28px; height: 1px; background: linear-gradient(90deg, var(--accent), transparent); }

.mast { max-width: 900px; margin-bottom: clamp(40px, 6vw, 72px); }
.mast-title {
  font-family: var(--font-display);
  font-size: clamp(2.8rem, 7.5vw, 6rem);
  line-height: 0.95; letter-spacing: -0.045em; font-weight: 600;
  margin: 0 0 22px;
}
.mast-title em {
  font-family: var(--font-serif); font-style: italic; font-weight: 400;
  background: linear-gradient(115deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.mast-sub { color: var(--text-2); font-size: clamp(1rem, 1.4vw, 1.12rem); max-width: 62ch; margin: 0; }

/* ── Topic filters ────────────────────────────────────────────────────── */
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 28px; }
.chip {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em;
  color: var(--text-2); background: var(--surface);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 7px 15px; cursor: pointer;
  transition: color 0.25s, border-color 0.25s, background 0.25s, transform 0.25s var(--ease);
}
.chip:hover { color: var(--text); border-color: var(--line-strong); transform: translateY(-1px); }
.chip.is-on {
  color: var(--on-accent); border-color: transparent;
  background: linear-gradient(115deg, var(--accent), var(--accent-2));
  font-weight: 600;
}

/* ══════════════════════════════════════════════════════════════════════
   THE READING ROOM — the index's two panes
   ══════════════════════════════════════════════════════════════════════ */
.reading-room {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
  gap: clamp(24px, 4vw, 56px); align-items: start;
  padding-bottom: clamp(60px, 9vw, 120px);
}

/* ── The register ─────────────────────────────────────────────────────── */
.register { border-top: 1px solid var(--line); }

.row {
  position: relative; display: grid;
  grid-template-columns: 44px minmax(0, 1fr) auto 108px;
  gap: 20px; align-items: baseline;
  padding: 24px 8px 24px 0;
  border-bottom: 1px solid var(--line);
  transition: background 0.35s var(--ease), padding-left 0.35s var(--ease);
}
/* The accent bar that resolves on hover — the row's own palette, so the
   register visibly belongs to the same system as the signatures. */
.row::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: linear-gradient(180deg, var(--c1), var(--c2));
  transform: scaleY(0); transform-origin: top;
  transition: transform 0.45s var(--ease);
}
.row:hover, .row:focus-visible, .row.is-active { background: var(--surface); padding-left: 16px; outline: none; }
.row:hover::before, .row:focus-visible::before, .row.is-active::before { transform: scaleY(1); }

.row-n { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-3); transition: color 0.3s; }
.row:hover .row-n, .row.is-active .row-n { color: var(--c1); }
.row.is-read:hover .row-n, .row.is-read.is-active .row-n { color: var(--on-accent); }

.row-main { min-width: 0; }
.row-title {
  display: block; font-family: var(--font-display); font-weight: 500;
  font-size: clamp(1.08rem, 1.7vw, 1.42rem); letter-spacing: -0.02em;
  line-height: 1.25; margin-bottom: 6px;
}
.row-desc {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; color: var(--text-3); font-size: 0.88rem; line-height: 1.5;
}
.row-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; }
.row-topic {
  font-family: var(--font-mono); font-size: 0.63rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--c1); opacity: 0.85; white-space: nowrap;
}
.row-read, .row-date { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-3); white-space: nowrap; }
.row-date { text-align: right; }
.row-sig { display: none; }

/* Filtering: collapse the row rather than display:none, so the list
   re-forms as a movement instead of a jump cut. */
.row.is-hidden {
  max-height: 0; padding-top: 0; padding-bottom: 0;
  opacity: 0; overflow: hidden; border-bottom-color: transparent;
  transition: max-height 0.45s var(--ease), opacity 0.25s, padding 0.45s var(--ease);
}
.row:not(.is-hidden) { max-height: 320px; }

.empty { color: var(--text-3); padding: 40px 0; font-family: var(--font-mono); font-size: 0.85rem; }
.empty code { color: var(--accent); }

/* ── The preview pane ─────────────────────────────────────────────────── */
.pane {
  position: sticky; top: 96px;
  border: 1px solid var(--line); border-radius: 22px;
  overflow: hidden; background: linear-gradient(180deg, var(--surface), transparent);
}
.pane-sig { position: relative; aspect-ratio: 16 / 10; overflow: hidden; }
.pane-sig svg { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Cross-fade between signatures rather than a swap — a hard swap on hover
   reads as a glitch, a fade reads as a response. */
.pane-sig svg.is-out { opacity: 0; transition: opacity 0.4s var(--ease); }
.pane-sig svg.is-in { animation: sigIn 0.55s var(--ease) both; }
@keyframes sigIn { from { opacity: 0; transform: scale(1.04); } to { opacity: 1; transform: none; } }

.pane-body { padding: 26px 28px 30px; }
.pane-kicker {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-3); margin-bottom: 12px;
}
.pane-title {
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(1.4rem, 2.3vw, 2rem); letter-spacing: -0.03em;
  line-height: 1.15; margin: 0 0 12px;
}
.pane-desc { color: var(--text-2); font-size: 0.94rem; line-height: 1.65; margin: 0 0 20px; }
.pane-cta {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 0.76rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--accent);
}
.pane-cta span { transition: transform 0.3s var(--ease); }
.pane:hover .pane-cta span { transform: translateX(5px); }
.pane [data-pane-title], .pane [data-pane-desc], .pane [data-pane-kicker] { transition: opacity 0.25s; }
.pane.is-swapping [data-pane-title],
.pane.is-swapping [data-pane-desc],
.pane.is-swapping [data-pane-kicker] { opacity: 0; }

/* Below 1100px the pane stops earning its column: the register absorbs the
   signature as a strip, so nothing is lost, it just re-forms. */
@media (max-width: 1100px) {
  .reading-room { grid-template-columns: 1fr; }
  .pane { display: none; }
  .row { grid-template-columns: 36px minmax(0, 1fr) auto; padding: 20px 0; }
  .row-date { display: none; }
  .row-sig {
    display: block; grid-column: 1 / -1; height: 74px; margin-top: 14px;
    border-radius: 12px; overflow: hidden; opacity: 0.5;
    transition: opacity 0.4s, height 0.4s var(--ease);
  }
  .row-sig svg { width: 100%; height: 100%; }
  .row:hover .row-sig { opacity: 0.95; height: 96px; }
}
@media (max-width: 560px) {
  .row { grid-template-columns: 28px minmax(0, 1fr); }
  .row-meta { display: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   THE POST
   ══════════════════════════════════════════════════════════════════════ */
.progress {
  position: fixed; top: 0; left: 0; height: 2px; width: 0; z-index: 80;
  background: linear-gradient(90deg, var(--post-c1), var(--post-c2));
  box-shadow: 0 0 12px var(--post-c1);
  transition: width 0.1s linear;
}

/* ── The spine ────────────────────────────────────────────────────────
   One tick per block of the document, coloured by what the block IS. It
   answers "what is coming up" — a progress bar can only say how far in you
   are. Purely decorative for assistive tech (the outline rail is the real
   navigation), so it is aria-hidden and the ticks are not focusable. */
.spine {
  position: fixed; left: 14px; top: 50%; transform: translateY(-50%);
  z-index: 55; display: flex; flex-direction: column; gap: 3px;
  align-items: flex-start; padding: 10px 6px;
  max-height: 62vh;
}
.spine-eye {
  position: absolute; left: 0; width: 100%; height: 20px;
  border-left: 2px solid var(--post-c1);
  background: linear-gradient(90deg, var(--surface-2), transparent);
  border-radius: 2px; transition: top 0.15s linear, height 0.2s;
  pointer-events: none;
}
.tick {
  height: 3px; border-radius: 2px;
  width: calc(4px + var(--w) * 2px);
  background: var(--text-3); opacity: 0.4;
  transition: opacity 0.3s, background 0.3s, width 0.3s var(--ease);
  cursor: inherit;
}
.tick.t-heading { background: var(--post-c1); opacity: 0.85; height: 4px; }
.tick.t-code    { background: var(--post-c2); opacity: 0.6; }
.tick.t-table   { background: var(--t-table); opacity: 0.55; }
.tick.t-quote   { background: var(--t-quote); opacity: 0.5; }
.tick.t-image   { background: var(--t-image); opacity: 0.5; }
.spine:hover .tick { opacity: 0.95; }
@media (max-width: 1280px) { .spine { display: none; } }

/* ── Masthead ─────────────────────────────────────────────────────────── */
.post { position: relative; z-index: 1; max-width: 1360px; margin: 0 auto; padding: 0 clamp(18px, 4vw, 48px); }
.post-mast { max-width: 900px; padding: clamp(34px, 6vw, 72px) 0 0; }
.post-back {
  display: inline-block; font-family: var(--font-mono); font-size: 0.74rem;
  letter-spacing: 0.1em; color: var(--text-3); margin-bottom: 30px;
  transition: color 0.25s, transform 0.25s var(--ease);
}
.post-back:hover { color: var(--post-c1); transform: translateX(-4px); }
.post-kicker {
  display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center;
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-3); margin: 0 0 20px;
}
.post-topic { color: var(--post-c1); }
.post-title {
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(2.2rem, 5.4vw, 4.2rem); line-height: 1.02;
  letter-spacing: -0.04em; margin: 0 0 22px;
}
.post-lede {
  font-family: var(--font-serif); font-size: clamp(1.1rem, 1.7vw, 1.35rem);
  line-height: 1.6; color: var(--text-2); max-width: 60ch; margin: 0 0 24px;
}
.post-tags { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; padding: 0; margin: 0 0 40px; }
.post-tags li {
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.1em;
  color: var(--text-3); border: 1px solid var(--line); border-radius: 999px; padding: 5px 12px;
}
/* ── The sigil ─────────────────────────────────────────────────────────
   What used to be a 440px full-width hero. See the note in pages.mjs for the
   measurement that prompted the change; in short, the article's first line
   started below the fold on a laptop and this is what was in the way.

   Sized in `em` of the kicker beside it rather than px, so the mark and the
   metadata line stay optically matched if either type scale moves. */
.post-ident {
  display: flex; align-items: center; gap: clamp(14px, 2vw, 20px);
  margin-bottom: clamp(18px, 2.4vw, 26px);
}
.post-sigil {
  position: relative; flex: 0 0 auto;
  width: clamp(78px, 8vw, 110px); aspect-ratio: 1;
  border-radius: 16px; overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--post-c1) 22%, var(--line));
  background:
    radial-gradient(ellipse 80% 90% at 30% 40%, color-mix(in srgb, var(--post-c1) 20%, transparent), transparent 62%),
    radial-gradient(ellipse 70% 80% at 76% 64%, color-mix(in srgb, var(--post-c2) 15%, transparent), transparent 60%),
    var(--plate);
  /* The same lit edge the panel had, at the scale that still reads. */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--post-c1) 24%, transparent),
    0 12px 24px -14px var(--shadow);
}
.post-sigil svg { position: absolute; inset: 0; width: 100%; height: 100%; }
/* The kicker loses its own bottom margin inside the flex row — the row owns
   the spacing now, and leaving both makes the mark sit visibly high. */
.post-ident .post-kicker { margin: 0; }

/* ── Article grid: rail + prose ───────────────────────────────────────── */
.post-grid { display: grid; grid-template-columns: minmax(0, 1fr) 232px; gap: clamp(30px, 5vw, 72px); align-items: start; }

.rail { position: sticky; top: 100px; order: 2; }
.rail-h {
  font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--text-3); margin: 0 0 14px;
}
.rail-list { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--line); }
.rail-i a {
  display: block; padding: 7px 0 7px 16px; margin-left: -1px;
  border-left: 1px solid transparent;
  font-size: 0.82rem; line-height: 1.4; color: var(--text-3);
  transition: color 0.25s, border-color 0.25s, transform 0.25s var(--ease);
}
.rail-i.d3 a { padding-left: 28px; font-size: 0.78rem; }
.rail-i a:hover { color: var(--text-2); transform: translateX(2px); }
.rail-i a.is-current { color: var(--post-c1); border-left-color: var(--post-c1); }
.rail-left { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-3); margin: 16px 0 0; }
@media (max-width: 1000px) { .post-grid { grid-template-columns: 1fr; } .rail { display: none; } }

/* ── Prose ────────────────────────────────────────────────────────────
   Fraunces at a 68-character measure. Serif because this is an argument to
   be read, not a UI to be scanned — and because it is the clearest possible
   signal that this is not another sans-serif dev-blog template. */
.prose {
  order: 1; max-width: var(--measure);
  font-family: var(--font-serif); font-size: 1.075rem; line-height: 1.78;
  color: var(--text);
}
.prose > * { margin: 0 0 1.35em; }
.prose > :last-child { margin-bottom: 0; }

/* Scroll reveal. Blocks arrive rather than appear. `.is-in` is added by
   blog.js; without JS every block is visible from the start (see the
   no-js rule at the bottom of this file). */
.prose [data-reveal] { opacity: 0; transform: translateY(18px); transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); }
.prose [data-reveal].is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .prose [data-reveal] { opacity: 1; transform: none; transition: none; }
}

.a-h { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.03em; line-height: 1.18; scroll-margin-top: 96px; position: relative; }
.a-h2 { font-size: clamp(1.55rem, 2.8vw, 2.1rem); margin: 2.2em 0 0.7em; padding-top: 0.5em; border-top: 1px solid var(--line); }
.a-h3 { font-size: clamp(1.2rem, 2vw, 1.42rem); margin: 1.9em 0 0.6em; color: var(--text); }
.a-anchor {
  position: absolute; left: -1.1em; top: 50%; transform: translateY(-50%);
  color: var(--post-c1); opacity: 0; font-family: var(--font-mono);
  font-size: 0.7em; transition: opacity 0.25s;
}
.a-h:hover .a-anchor { opacity: 0.7; }
@media (max-width: 900px) { .a-anchor { display: none; } }

.prose strong { color: var(--text-hi); font-weight: 600; }
.prose em { font-style: italic; }
.prose a:not(.a-anchor) {
  color: var(--post-c1);
  background-image: linear-gradient(var(--post-c1), var(--post-c1));
  background-size: 100% 1px; background-repeat: no-repeat; background-position: 0 100%;
  padding-bottom: 1px; transition: background-size 0.3s var(--ease), color 0.25s;
}
.prose a:not(.a-anchor):hover { background-size: 100% 2px; color: var(--text-hi); }

.prose ul, .prose ol { padding-left: 1.35em; }
.prose li { margin-bottom: 0.5em; }
.prose li::marker { color: var(--post-c1); }

.prose blockquote {
  border-left: 2px solid var(--post-c1);
  padding: 4px 0 4px 22px; margin-left: 0;
  font-style: italic; color: var(--text-2);
}

.prose :not(pre) > code {
  font-family: var(--font-mono); font-size: 0.86em;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 6px; padding: 0.14em 0.42em; color: var(--accent);
}

.prose hr { border: 0; height: 1px; background: var(--line); margin: 2.6em 0; }

/* ── Code blocks ──────────────────────────────────────────────────────── */
.a-code {
  margin: 1.9em 0; border: 1px solid var(--line); border-radius: 14px;
  overflow: hidden; background: var(--bg-2); font-family: var(--font-mono);
}
.a-code-bar {
  display: flex; align-items: center; gap: 14px;
  padding: 9px 14px; background: var(--surface);
  border-bottom: 1px solid var(--line);
  font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
}
.a-code-lang { color: var(--post-c1); font-weight: 600; }
.a-code-lines { color: var(--text-3); margin-right: auto; }
.a-code-copy {
  font: inherit; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-3); background: transparent;
  border: 1px solid var(--line); border-radius: 6px;
  padding: 3px 10px; cursor: pointer; transition: color 0.25s, border-color 0.25s;
}
.a-code-copy:hover { color: var(--text); border-color: var(--line-strong); }
.a-code-copy.is-done { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.a-code pre { margin: 0; padding: 16px 18px; overflow-x: auto; }
.a-code code { font-family: var(--font-mono); font-size: 0.84rem; line-height: 1.65; color: var(--code-ink); }

/* A long block is folded until asked for. A 200-line file pasted mid-argument
   breaks the read; the reader decides when to take it on. */
.a-code.is-long pre { max-height: 420px; }
.a-code.is-long:not(.is-open) pre { mask-image: linear-gradient(180deg, #000 70%, transparent 100%); }
.a-code.is-open pre { max-height: none; }
.a-code-more {
  display: block; width: 100%; font: inherit; font-size: 0.7rem;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-3);
  background: var(--surface); border: 0; border-top: 1px solid var(--line);
  padding: 10px; cursor: pointer; transition: color 0.25s, background 0.25s;
}
.a-code-more:hover { color: var(--text); background: var(--surface-2); }

/* ── Tables & figures ─────────────────────────────────────────────────── */
.a-table { overflow-x: auto; margin: 1.9em 0; border: 1px solid var(--line); border-radius: 14px; }
.a-table table { border-collapse: collapse; width: 100%; font-family: var(--font-body); font-size: 0.9rem; }
.a-table th, .a-table td { padding: 11px 16px; text-align: left; border-bottom: 1px solid var(--line); }
.a-table th {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--post-c1);
  background: var(--surface); white-space: nowrap;
}
.a-table tr:last-child td { border-bottom: 0; }
.a-table tbody tr { transition: background 0.2s; }
.a-table tbody tr:hover { background: var(--surface); }

.a-figure { margin: 2em 0; }
.a-figure img { width: 100%; height: auto; border-radius: 14px; border: 1px solid var(--line); display: block; }
.a-figure figcaption,
.prose figcaption:not(.a-code-bar) {
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-3);
  text-align: center; margin-top: 10px;
}

/* ── End of post ──────────────────────────────────────────────────────── */
.post-end {
  display: flex; flex-wrap: wrap; gap: 24px; justify-content: space-between; align-items: center;
  max-width: var(--measure); margin: clamp(48px, 7vw, 80px) 0 0;
  padding-top: 28px; border-top: 1px solid var(--line);
}
.cross, .post-share { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.cross-h, .post-share-h {
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--text-3);
}
.cross a, .post-share a, .post-share button {
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-2);
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px;
  padding: 6px 14px; cursor: pointer;
  transition: color 0.25s, border-color 0.25s, transform 0.25s var(--ease);
}
.cross a:hover, .post-share a:hover, .post-share button:hover {
  color: var(--text); border-color: var(--post-c1); transform: translateY(-2px);
}
.post-share button.is-done { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); }

/* ── Continue reading ─────────────────────────────────────────────────
   One next step, full-bleed. Not a grid of six thumbnails: six choices at
   the end of an article is how a reader leaves. */
.next {
  position: relative; display: block; overflow: hidden;
  border: 1px solid var(--line); border-radius: 24px;
  margin: clamp(48px, 7vw, 90px) 0 0; min-height: 240px;
  transition: border-color 0.4s, transform 0.5s var(--ease);
}
.next:hover { border-color: var(--line-strong); transform: translateY(-3px); }
.next-sig { position: absolute; inset: 0; opacity: 0.42; transition: opacity 0.6s var(--ease), transform 0.9s var(--ease); }
.next-sig svg { width: 100%; height: 100%; }
.next:hover .next-sig { opacity: 0.7; transform: scale(1.06); }
.next-body {
  position: relative; display: block; padding: clamp(28px, 4vw, 48px);
  background: linear-gradient(90deg, rgba(var(--scrim), 0.94) 30%, rgba(var(--scrim), 0.45) 100%);
  max-width: 640px;
}
.next-k {
  display: block; font-family: var(--font-mono); font-size: 0.66rem;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--c1); margin-bottom: 14px;
}
.next-t {
  display: block; font-family: var(--font-display); font-weight: 600;
  font-size: clamp(1.4rem, 2.6vw, 2.1rem); letter-spacing: -0.03em;
  line-height: 1.15; margin-bottom: 12px;
}
.next-d {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; color: var(--text-2); font-size: 0.94rem; margin-bottom: 14px;
}
.next-m { display: block; font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-3); }

.also { max-width: var(--measure); margin: clamp(40px, 6vw, 64px) 0 0; }
.also-h {
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--text-3); margin: 0 0 12px;
}
.also-list { list-style: none; margin: 0; padding: 0; }
.also-list a {
  display: flex; justify-content: space-between; gap: 20px; align-items: baseline;
  padding: 13px 0; border-bottom: 1px solid var(--line);
  transition: padding-left 0.3s var(--ease), color 0.25s;
}
.also-list a:hover { padding-left: 10px; color: var(--post-c1); }
.also-list em { font-family: var(--font-mono); font-style: normal; font-size: 0.72rem; color: var(--text-3); white-space: nowrap; }

/* ── Footer ───────────────────────────────────────────────────────────── */
.foot {
  position: relative; z-index: 1; max-width: 1500px; margin: clamp(70px, 10vw, 130px) auto 0;
  padding: 26px clamp(18px, 4vw, 48px) 40px; border-top: 1px solid var(--line);
}
.foot-row { display: flex; flex-wrap: wrap; gap: 18px; justify-content: space-between; align-items: center; }
.foot-back, .foot-links a { font-family: var(--font-mono); font-size: 0.76rem; color: var(--text-3); transition: color 0.25s; }
.foot-back:hover, .foot-links a:hover { color: var(--text); }
.foot-links { display: flex; gap: 20px; }

/* ── Enhancement gate ─────────────────────────────────────────────────
   blog.js sets .js on <html> as its first act. Anything that starts hidden
   for an animation must therefore be hidden ONLY when JS is present —
   otherwise the crawler and the JS-disabled reader get a blank article,
   which is the exact failure this whole static pipeline exists to avoid. */
html:not(.js) .prose [data-reveal] { opacity: 1; transform: none; }
html:not(.js) .spine { display: none; }

/* ── Syntax tokens ────────────────────────────────────────────────────
   highlight.js emits .hljs-* classes; these colour them. Written here in the
   site's own palette rather than pulling a CDN theme stylesheet: one less
   render-blocking request, and no imported theme quietly fighting the page's
   ground. Anything hljs tags but we don't name inherits the base code colour,
   so an unmapped token is dim, never invisible. */
.hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-doctag { color: var(--hl-kw); }
.hljs-string, .hljs-regexp, .hljs-addition { color: var(--hl-str); }
.hljs-number, .hljs-symbol, .hljs-bullet { color: var(--hl-num); }
.hljs-title, .hljs-title.function_, .hljs-section { color: var(--hl-fn); }
.hljs-attr, .hljs-attribute, .hljs-variable, .hljs-template-variable { color: var(--hl-var); }
.hljs-built_in, .hljs-class .hljs-title, .hljs-type { color: var(--hl-type); }
.hljs-comment, .hljs-quote { color: var(--hl-com); font-style: italic; }
.hljs-meta, .hljs-meta .hljs-keyword { color: var(--hl-meta); }
.hljs-deletion { color: var(--hl-del); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: 600; }

/* ── Signature wash ───────────────────────────────────────────────────
   The soft glow behind every signature. It lives here rather than inside the
   SVG because the SVG is deliberately id-free (see signature.mjs): a
   <radialGradient> needs an id, and the index clones signatures, so duplicate
   ids would break the clone's paint. CSS has no such problem, and the wash
   picks up each context's own accent pair. */
.pane-sig, .next-sig {
  background:
    radial-gradient(ellipse 80% 90% at 28% 45%, color-mix(in srgb, var(--c1, var(--post-c1)) 18%, transparent), transparent 62%),
    radial-gradient(ellipse 70% 80% at 78% 60%, color-mix(in srgb, var(--c2, var(--post-c2)) 13%, transparent), transparent 60%),
    var(--plate);
}

/* ══════════════════════════════════════════════════════════════════════
   PER-POST COLOUR THEME
   ══════════════════════════════════════════════════════════════════════
   Every post owns an accent pair, derived from its slug by signature.mjs and
   set on the page as --c1 / --c2 (index) and --post-c1 / --post-c2 (article).
   The rules below spend those colours across the whole surface instead of
   confining them to the artwork, so a post *is* a colourway rather than the
   same dark page with a tinted picture at the top.

   Deliberately NOT MarkVault's reading themes. Those exist so a stranger can
   re-set type for an arbitrary shared document they didn't write. Here there
   is one author and one intent, so the palette is authored, not chosen — and
   it stays independent of MarkVault, which is a separate repo on its own
   release cycle. */

/* The identity mark: initials, not a name. */
.nav-initials {
  font-family: var(--font-mono); font-size: 0.82rem; font-weight: 600;
  letter-spacing: 0.14em;
  background: linear-gradient(115deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.nav-mark {
  background: linear-gradient(135deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
  box-shadow: 0 0 14px color-mix(in srgb, var(--c1, var(--accent)) 55%, transparent);
}
.nav-links a::after { background: linear-gradient(90deg, var(--c1, var(--accent)), var(--c2, var(--accent-2))); }

/* The ambient field takes the page's colours, so the whole surface — not just
   the artwork — belongs to the post you're looking at. Transitioned, because
   on the index it changes as you move down the register. */
.field-glow {
  background: radial-gradient(circle,
    color-mix(in srgb, var(--c2, var(--accent-2)) 15%, transparent),
    color-mix(in srgb, var(--c1, var(--accent)) 8%, transparent) 42%,
    transparent 68%);
  transition: background 0.8s var(--ease);
}
body.blog-post {
  --cd1: var(--pd1);
  --cd2: var(--pd2);
  --cl1: var(--pl1);
  --cl2: var(--pl2);
}

/* On ink the glow is light being added to a dark room, so the palette colour
   IS the light and a strong tint is right.

   On sand it cannot be. A dark accent bloomed over cream is subtractive — it
   reads as a smudge on the paper, and it drags every word underneath it down
   with it. So on sand the bloom becomes what light on paper actually looks
   like: warm and near-white, carrying only a whisper of the post's colour.
   The colourway is not lost, it is just spent where it belongs — the rules,
   the marks, the links — instead of on the ground. */
html[data-theme='light'] .field-glow,
html[data-theme='light'] .blog-post .field-glow {
  background: radial-gradient(circle,
    rgba(255, 253, 249, 0.95),
    color-mix(in srgb, var(--c1, var(--accent)) 5%, rgba(255, 250, 243, 0.45)) 46%,
    transparent 72%);
  filter: blur(70px);
}
@media (prefers-color-scheme: light) {
  html:not([data-theme='dark']) .field-glow,
  html:not([data-theme='dark']) .blog-post .field-glow {
    background: radial-gradient(circle,
      rgba(255, 253, 249, 0.95),
      color-mix(in srgb, var(--c1, var(--accent)) 5%, rgba(255, 250, 243, 0.45)) 46%,
      transparent 72%);
    filter: blur(70px);
  }
}

::selection { background: color-mix(in srgb, var(--c1, var(--post-c1, var(--accent))) 32%, transparent); color: var(--text-hi); }

/* ── Index chrome follows the active row ─────────────────────────────── */
.mast-title em,
.chip.is-on {
  background: linear-gradient(115deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
}
.mast-title em { -webkit-background-clip: text; background-clip: text; color: transparent; }
.eyebrow::before { background: linear-gradient(90deg, var(--c1, var(--accent)), transparent); }
.pane { border-color: color-mix(in srgb, var(--c1, var(--accent)) 22%, var(--line)); transition: border-color 0.6s var(--ease); }
.pane-cta { color: var(--c1, var(--accent)); }

/* ── Article chrome ───────────────────────────────────────────────────
   The accent carries the structure: the rule above each section, the tag
   chips, the quote bar, the code language label. Enough to colour the page
   without tinting the prose itself — body text stays near-white, because a
   coloured paragraph is harder to read and that is the one thing an article
   cannot trade away. */
.a-h2 { border-top-color: color-mix(in srgb, var(--post-c1) 28%, var(--line)); }
.a-h2::after {
  content: ''; position: absolute; top: -1px; left: 0; width: 56px; height: 2px;
  background: linear-gradient(90deg, var(--post-c1), var(--post-c2));
  border-radius: 2px;
}
.post-tags li {
  border-color: color-mix(in srgb, var(--post-c1) 26%, var(--line));
  color: color-mix(in srgb, var(--post-c1) 62%, var(--text-2));
}
.prose blockquote {
  border-left-color: var(--post-c2);
  background: linear-gradient(90deg, color-mix(in srgb, var(--post-c2) 7%, transparent), transparent 70%);
  border-radius: 0 10px 10px 0;
}
.prose :not(pre) > code {
  color: color-mix(in srgb, var(--post-c1) 76%, var(--text-hi));
  border-color: color-mix(in srgb, var(--post-c1) 20%, var(--line));
  background: color-mix(in srgb, var(--post-c1) 7%, transparent);
}
.a-code { border-color: color-mix(in srgb, var(--post-c1) 16%, var(--line)); }
.a-table th { color: var(--post-c1); }
.prose li::marker { color: var(--post-c2); }
.rail-list { border-left-color: color-mix(in srgb, var(--post-c1) 18%, var(--line)); }

@media (prefers-reduced-motion: reduce) {
  .field-glow, .pane { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   THE GROUND SWITCH
   ══════════════════════════════════════════════════════════════════════
   Two states, one control, and the label always names the ground you would
   be moving TO — a toggle that names the state you are already in is the
   single most reliably misread control in interface design.

   The knob is a small solid that fills with the post's own colourway, so
   even the chrome tells you which post you are in. */
.theme-toggle {
  position: relative;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 12px 5px 6px;
  font: inherit; font-family: var(--font-mono);
  font-size: 0.68rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.25s, border-color 0.25s, background 0.25s;
}
.theme-toggle:hover { color: var(--text); border-color: var(--line-strong); }
.theme-toggle-knob {
  position: relative;
  width: 18px; height: 18px; border-radius: 50%;
  background: linear-gradient(135deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 14%, transparent);
  overflow: hidden;
  flex: none;
}
/* The waxing/waning face. One element, one transform — a sun/moon icon swap
   needs two glyphs and a crossfade to say the same thing less clearly. */
.theme-toggle-knob::after {
  content: ''; position: absolute; inset: -1px;
  border-radius: 50%;
  background: var(--bg);
  transform: translateX(-46%);
  transition: transform 0.45s var(--ease);
}
html[data-theme='light'] .theme-toggle-knob::after { transform: translateX(46%); }
@media (prefers-color-scheme: light) {
  html:not([data-theme='dark']) .theme-toggle-knob::after { transform: translateX(46%); }
}
.theme-toggle-label { display: none; }
@media (min-width: 720px) { .theme-toggle-label { display: inline; } }

/* THE SWITCH IS INSTANT, AND THAT IS THE DESIGN.

   The obvious thing to do here is cross-fade the ground over ~400ms. It was
   built that way first and then removed, for two reasons:

   1. It did not work. Every colour on this page comes from a custom property,
      and the theme changes by re-pointing those properties. A transition on
      background-color whose value is var(--bg) does not interpolate when
      --bg changes — the property is untyped, so it changes discretely and the
      dependent colour holds, then snaps when the transition is cancelled.
      What shipped was not a fade but a delayed hard cut, which reads as lag.

   2. Even working, it would be wrong. Mid-fade, body text sits at whatever
      the blend of near-white and near-black happens to be — for a quarter of
      a second the page is at its least readable, in service of an effect no
      one asked for. An instant switch is legible in every frame.

   The one thing that IS animated is the toggle's own knob, which is a small,
   local, always-legible movement. See .theme-toggle-knob::after. */

/* The terrain canvas rules are gone with the hero that hosted them —
   terrain.js is no longer loaded on an article page (see pages.mjs). It still
   runs inside scripts/shoot-og.mjs to render share cards, which builds its own
   standalone document and does not read this stylesheet. */

/* ══════════════════════════════════════════════════════════════════════
   THE SCRUBBER — the spine becomes a transport
   ══════════════════════════════════════════════════════════════════════
   The spine already draws one tick per block of the document, typed by what
   the block is. Making it draggable turns that map into a transport: grab it
   and the article runs past under the cursor, the way a timeline scrubs.

   Why this and not a bigger progress bar: a progress bar can only report how
   far in you are. This is the only control on the page that lets you *travel*
   by structure — "the third code block", "the table near the end" — which is
   how anyone actually re-finds a passage in a long technical post. */
.spine {
  cursor: grab;
  touch-action: none;
  padding: 16px 18px 16px 10px;
  border-radius: 12px;
  transition: background 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
/* The grab target is the spine's own padding, deliberately generous — the
   ticks are 3px tall by design and must not be what you have to hit. It is
   NOT an ::before overlay: that pseudo-element would become the pointer
   target for every press and swallow tap-to-section, which is the other half
   of what this control does. */
.spine:hover { background: color-mix(in srgb, var(--text) 4%, transparent); }
.spine.is-scrubbing { cursor: grabbing; background: color-mix(in srgb, var(--text) 7%, transparent); }

.spine .tick { transition: opacity 0.3s, background 0.3s, width 0.3s var(--ease), transform 0.3s var(--ease); }
.spine:hover .tick, .spine.is-scrubbing .tick { width: calc(8px + var(--w) * 3px); }
/* The tick under the playhead swells — the scrub needs a "you are here" that
   survives the ticks all being the same 3px height. */
.spine .tick.is-at { transform: scaleY(2.2); opacity: 1; }

.spine-eye { z-index: 2; }
.spine.is-scrubbing .spine-eye {
  transition: none;   /* follow the pointer exactly; easing here reads as lag */
  border-left-width: 3px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--post-c1) 34%, transparent), transparent);
}

/* The readout that appears while scrubbing: what block you are over, and how
   far through. Anchored to the playhead, not to the viewport. */
.scrub-readout {
  position: absolute; left: calc(100% + 12px);
  transform: translateY(-50%);
  display: flex; align-items: baseline; gap: 10px;
  padding: 6px 12px; white-space: nowrap;
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.08em;
  color: var(--text-2);
  background: rgba(var(--scrim), 0.92);
  border: 1px solid color-mix(in srgb, var(--post-c1) 26%, var(--line));
  border-radius: 8px;
  backdrop-filter: blur(10px);
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s;
}
.scrub-readout b { color: var(--post-c1); font-weight: 600; text-transform: uppercase; }
.scrub-readout i { font-style: normal; color: var(--text-3); }
.spine.is-scrubbing .scrub-readout, .spine:hover .scrub-readout { opacity: 1; }

/* The hint. Shown once, then never again for that reader — an affordance you
   have already used is just noise.

   Chipped rather than set as bare text: it floats over the article's own
   paragraphs, and unstyled it read as a stray line of the post rather than as
   a label pointing at the control beside it. */
.spine-hint {
  position: absolute; pointer-events: none; left: calc(100% + 12px); bottom: -2px;
  white-space: nowrap;
  padding: 5px 10px;
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-2);
  background: rgba(var(--scrim), 0.92);
  border: 1px solid color-mix(in srgb, var(--post-c1) 24%, var(--line));
  border-radius: 7px;
  backdrop-filter: blur(10px);
  box-shadow: 0 10px 24px -14px var(--shadow);
  opacity: 0; transform: translateX(-4px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}
.spine.show-hint .spine-hint { transform: none; }
.spine.show-hint .spine-hint { opacity: 0.75; }

/* While scrubbing, the prose loses its reveal transitions — 40 blocks each
   running a 0.7s opacity transition as they fly past is a stutter, and the
   reveal has no meaning at scrub speed anyway. */
body.is-scrubbing .prose [data-reveal] { transition: none; opacity: 1; transform: none; }
body.is-scrubbing { user-select: none; }
html.is-scrubbing { scroll-behavior: auto; }

@media (max-width: 1280px) { .spine-hint, .scrub-readout { display: none; } }

/* ══════════════════════════════════════════════════════════════════════
   HIGHLIGHT → QUOTE
   ══════════════════════════════════════════════════════════════════════
   Select any passage and a bar offers three things a reader of a technical
   post actually wants: the text, a link that will scroll the NEXT reader to
   this exact sentence, and a card of it in the post's own colours.

   The link is a native text fragment (#:~:text=...). No server, no database,
   no per-highlight record — the quote is the address. */
.quotebar {
  position: absolute; z-index: 90;
  display: flex; align-items: stretch; gap: 1px;
  padding: 1px;
  background: rgba(var(--scrim), 0.94);
  border: 1px solid color-mix(in srgb, var(--post-c1) 30%, var(--line));
  border-radius: 10px;
  box-shadow: 0 18px 40px -16px var(--shadow);
  backdrop-filter: blur(14px);
  opacity: 0; transform: translate(-50%, 6px) scale(0.96);
  transform-origin: 50% 100%;
  pointer-events: none;
  transition: opacity 0.18s var(--ease), transform 0.24s var(--ease);
}
.quotebar.is-on { opacity: 1; transform: translate(-50%, 0) scale(1); pointer-events: auto; }
.quotebar button {
  display: inline-flex; align-items: center; gap: 7px;
  font: inherit; font-family: var(--font-mono);
  font-size: 0.7rem; letter-spacing: 0.08em;
  color: var(--text-2);
  background: transparent; border: 0; border-radius: 9px;
  padding: 8px 13px; cursor: pointer;
  transition: color 0.2s, background 0.2s;
}
.quotebar button:hover { color: var(--text-hi); background: color-mix(in srgb, var(--post-c1) 16%, transparent); }
.quotebar button.is-done { color: var(--ok); }
.quotebar button + button { box-shadow: inset 1px 0 0 var(--line); }
.quotebar svg { width: 13px; height: 13px; flex: none; }
/* The little beak. Purely so the bar reads as attached to the selection
   rather than floating near it. */
.quotebar::after {
  content: ''; position: absolute; top: 100%; left: 50%;
  width: 8px; height: 8px; margin: -4px 0 0 -4px;
  background: rgba(var(--scrim), 0.94);
  border-right: 1px solid color-mix(in srgb, var(--post-c1) 30%, var(--line));
  border-bottom: 1px solid color-mix(in srgb, var(--post-c1) 30%, var(--line));
  transform: rotate(45deg);
}

.prose ::selection {
  background: color-mix(in srgb, var(--post-c1) 26%, transparent);
  color: var(--text-hi);
}

/* Arriving on a shared quote. The browser's own ::target-text is a flat
   yellow that fights every palette on this site, so it is overridden and the
   passage is given a real entrance instead. */
::target-text {
  background: color-mix(in srgb, var(--post-c2) 30%, transparent);
  color: var(--text-hi);
}
.prose mark.quote-target {
  background: color-mix(in srgb, var(--post-c2) 26%, transparent);
  color: inherit;
  border-radius: 3px;
  padding: 0.08em 0.1em;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--post-c2) 55%, transparent);
  animation: quoteLand 2.4s var(--ease) 0.3s both;
}
@keyframes quoteLand {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--post-c2) 55%, transparent); }
  30%  { box-shadow: 0 0 0 7px color-mix(in srgb, var(--post-c2) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--post-c2) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .prose mark.quote-target { animation: none; } }

/* The quote card. Rendered as an SVG in the post's palette and offered as a
   download — the artwork is generated at share time from the selection, so
   there is no image to author, host or invalidate. */
.qcard {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  padding: 24px;
  background: rgba(var(--scrim), 0.86);
  backdrop-filter: blur(18px);
  opacity: 0; pointer-events: none;
  transition: opacity 0.3s var(--ease);
}
.qcard.is-on { opacity: 1; pointer-events: auto; }
.qcard-inner {
  width: min(680px, 100%);
  transform: translateY(14px) scale(0.97);
  transition: transform 0.45s var(--ease);
}
.qcard.is-on .qcard-inner { transform: none; }
.qcard-art {
  border-radius: 18px; overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--post-c1) 24%, var(--line));
  box-shadow: 0 40px 80px -30px var(--shadow);
  background: var(--plate);
}
.qcard-art svg { display: block; width: 100%; height: auto; }
.qcard-row { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 18px; }
.qcard-row button, .qcard-row a {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.08em;
  color: var(--text-2); background: var(--surface);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 8px 16px; cursor: pointer;
  transition: color 0.25s, border-color 0.25s, transform 0.25s var(--ease);
}
.qcard-row button:hover, .qcard-row a:hover {
  color: var(--text-hi); border-color: var(--post-c1); transform: translateY(-2px);
}

/* ══════════════════════════════════════════════════════════════════════
   LIFT — the one thing borrowed wholesale from the reference
   ══════════════════════════════════════════════════════════════════════
   On sand a shadow is a real material cue and does the work of a border. On
   ink a shadow is invisible, so the same gesture is spent as a lit edge
   instead. Same intent, two grounds, and neither is the other's leftovers. */
.pane, .next {
  box-shadow: 0 24px 50px -34px var(--shadow);
  transition: border-color 0.4s, transform 0.5s var(--ease), box-shadow 0.5s var(--ease);
}
.pane:hover, .next:hover {
  transform: translateY(-3px);
  box-shadow: 0 34px 66px -30px var(--shadow),
              0 0 0 1px color-mix(in srgb, var(--c1, var(--accent)) 22%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .pane:hover, .next:hover { transform: none; }
}

/* ── Enhancement gate, extended ───────────────────────────────────────
   Without JS there is no toggle to press and no scrubber to grab, so neither
   is advertised. The article itself is untouched. */
html:not(.js) .theme-toggle,
html:not(.js) .quotebar,
html:not(.js) .qcard,
html:not(.js) .spine-hint,
html:not(.js) .scrub-readout { display: none; }

/* ══════════════════════════════════════════════════════════════════════
   MOBILE
   ══════════════════════════════════════════════════════════════════════
   THE OVERFLOW BUG THIS SECTION EXISTS TO FIX

   At 390px the article was 578px wide and the page scrolled sideways. The
   cause is the one CSS Grid trap that catches everyone: a grid item's default
   `min-width: auto` means a track can never be smaller than the item's
   min-content, and the widest min-content in an article is a fenced code
   block — a shell command does not wrap, so `figure.a-code` measured 578px
   and dragged the whole column out with it.

   `overflow-x: auto` on the <pre> does NOT prevent this. It makes the block
   scrollable once it is too small, but it does not lower the min-content
   contribution that decides how small the track is allowed to get. Only
   min-width: 0 does that. */
.post-grid { grid-template-columns: minmax(0, 1fr) 232px; }
.prose, .post-grid > * { min-width: 0; }
@media (max-width: 1000px) {
  .post-grid { grid-template-columns: minmax(0, 1fr); }
}
/* Belt and braces: the two blocks that actually carry unbreakable content
   are told, directly, that they may never exceed their column. */
.a-code, .a-table, .a-figure img { max-width: 100%; }

/* A long inline token — a share URL, a flag, a path — must be allowed to
   break mid-string. Left alone it is one unbreakable word that widens the
   table it sits in until the page scrolls. */
.prose :not(pre) > code { overflow-wrap: anywhere; }
.prose a:not(.a-anchor) { overflow-wrap: break-word; }

/* ── Nav ──────────────────────────────────────────────────────────────
   The old rule hid Products and Portfolio below 560px, which is two of the
   four destinations on the site removed from every phone. A nav that scrolls
   keeps all four reachable and costs nothing; the fade on the right edge is
   what tells you there is more. */
@media (max-width: 720px) {
  .nav { gap: 12px; padding: 12px clamp(14px, 4vw, 24px); }
  .nav-links {
    display: flex; gap: 18px;
    overflow-x: auto; scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(90deg, #000 82%, transparent 100%);
    -webkit-mask-image: linear-gradient(90deg, #000 82%, transparent 100%);
  }
  .nav-links::-webkit-scrollbar { display: none; }
  .nav-links a { white-space: nowrap; }
  /* Overrides the old hide-two-links rule. */
  .nav-links a:nth-child(1), .nav-links a:nth-child(3) { display: inline-block; }
}
@media (max-width: 560px) {
  .nav-links { font-size: 0.8rem; }
}

/* ── Tap targets ─────────────────────────────────────────────────────
   The toggle rendered 38x30. Anything a thumb has to hit needs ~44px of
   height; the visible chip stays small, and the target is grown around it. */
.theme-toggle { position: relative; }
.theme-toggle::after {
  content: ''; position: absolute;
  left: -6px; right: -6px; top: 50%;
  height: 44px; min-width: 44px; transform: translateY(-50%);
}
@media (max-width: 720px) {
  .theme-toggle { padding: 6px; }
  .post-share a, .post-share button, .cross a, .chip { padding: 9px 15px; }
}

/* ── The article on a phone ──────────────────────────────────────────── */
@media (max-width: 720px) {
  .post, .wrap { padding-left: clamp(16px, 4.5vw, 24px); padding-right: clamp(16px, 4.5vw, 24px); }
  .post-sigil { width: clamp(64px, 17vw, 84px); border-radius: 13px; }
  /* 17px, not 16: this is the size the eye holds for eight minutes on a
     phone, and the measure is already narrow enough to carry it. */
  .prose { font-size: 1.06rem; line-height: 1.74; }
  .a-code pre { padding: 13px 14px; }
  .a-code code { font-size: 0.78rem; }
  .a-code-bar { padding: 8px 12px; font-size: 0.62rem; gap: 10px; }
  .a-table th, .a-table td { padding: 9px 12px; font-size: 0.85rem; }
  .post-end { flex-direction: column; align-items: flex-start; gap: 16px; }
  .next { min-height: 0; }
  .next-body { max-width: none; background: linear-gradient(180deg, rgba(var(--scrim), 0.72) 0%, rgba(var(--scrim), 0.94) 60%); }
  .next-sig { opacity: 0.5; }
}

/* ── The index on a phone ────────────────────────────────────────────── */
@media (max-width: 720px) {
  .reading-room { padding-bottom: 60px; }
  .row { grid-template-columns: 26px minmax(0, 1fr); gap: 12px; padding: 18px 0; }
  .row:hover, .row.is-active { padding-left: 8px; }
  .row-title { font-size: 1.06rem; }
  .row-sig { height: 64px; }
  .row:hover .row-sig { height: 64px; }
  .foot-row { flex-direction: column; align-items: flex-start; gap: 14px; }
}

/* The quote bar must never be wider than the phone it is on, and it is
   positioned from the selection's centre — so it needs a real clamp, not a
   max-width that its transform can carry back off screen. */
@media (max-width: 720px) {
  .quotebar { max-width: calc(100vw - 24px); }
  .quotebar button { padding: 10px 12px; font-size: 0.68rem; }
  .qcard { padding: 16px; }
}

/* ══════════════════════════════════════════════════════════════════════
   THE READING PANEL
   ══════════════════════════════════════════════════════════════════════
   Four variables — typeface, size, measure, leading — set by the reader and
   remembered. Every one of them resolves through a custom property that
   .prose reads, so the article re-flows without a single class on it and the
   defaults below are what a reader with no stored preference gets. */
:root {
  --reader-font: var(--font-serif);
  --reader-size: 1.075rem;
  --reader-measure: 68ch;
  --reader-leading: 1.78;
}
.prose {
  font-family: var(--reader-font);
  font-size: var(--reader-size);
  line-height: var(--reader-leading);
  max-width: var(--reader-measure);
}

.nav-tools { display: flex; align-items: center; gap: 10px; }

.reader-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 30px;
  font-family: var(--font-serif); font-size: 0.92rem; font-weight: 600;
  color: var(--text-3);
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 999px;
  cursor: pointer;
  transition: color 0.25s, border-color 0.25s;
}
.reader-btn:hover, .reader-btn[aria-expanded='true'] { color: var(--text); border-color: var(--line-strong); }
/* Same 44px thumb target as the ground switch. */
.reader-btn { position: relative; }
.reader-btn::after {
  content: ''; position: absolute; left: -5px; right: -5px;
  top: 50%; height: 44px; transform: translateY(-50%);
}

.reader-panel {
  position: fixed; z-index: 70;
  top: 62px; right: clamp(12px, 4vw, 48px);
  width: min(300px, calc(100vw - 24px));
  padding: 16px 18px 14px;
  background: rgba(var(--scrim), 0.96);
  border: 1px solid var(--line-strong);
  border-radius: 16px;
  box-shadow: 0 30px 60px -24px var(--shadow);
  backdrop-filter: blur(18px);
  transform-origin: top right;
  animation: rpIn 0.22s var(--ease) both;
}
.reader-panel[hidden] { display: none; }
@keyframes rpIn { from { opacity: 0; transform: translateY(-6px) scale(0.97); } }
@media (prefers-reduced-motion: reduce) { .reader-panel { animation: none; } }

.rp-head {
  margin: 0 0 14px;
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--text-3);
}
.rp-row { margin-bottom: 13px; }
.rp-label {
  display: block; margin-bottom: 6px;
  font-size: 0.72rem; color: var(--text-3);
}
.rp-opts { display: flex; gap: 5px; }
.rp-opts button {
  flex: 1;
  font: inherit; font-size: 0.76rem;
  color: var(--text-2);
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 8px 6px; cursor: pointer;
  transition: color 0.2s, background 0.2s, border-color 0.2s;
}
.rp-opts button:hover { color: var(--text); border-color: var(--line-strong); }
.rp-opts button[aria-pressed='true'] {
  color: var(--on-accent);
  background: linear-gradient(115deg, var(--post-c1), var(--post-c2));
  border-color: transparent;
  font-weight: 600;
}
/* The size row shows its own scale: four buttons labelled "A", each set at
   the size it selects. A row of identical labels reading S M L XL makes the
   reader do the translation the control could have done for them. */
.rp-row [data-rp='size'] button:nth-child(1) { font-size: 0.72rem; }
.rp-row [data-rp='size'] button:nth-child(2) { font-size: 0.86rem; }
.rp-row [data-rp='size'] button:nth-child(3) { font-size: 1rem; }
.rp-row [data-rp='size'] button:nth-child(4) { font-size: 1.16rem; }

.rp-reset {
  width: 100%; margin-top: 4px;
  font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-3);
  background: transparent; border: 0; border-top: 1px solid var(--line);
  padding: 11px 0 2px; cursor: pointer;
  transition: color 0.2s;
}
.rp-reset:hover { color: var(--text); }

@media (max-width: 720px) {
  .reader-panel { top: auto; bottom: 12px; right: 12px; left: 12px; width: auto; }
}

/* ══════════════════════════════════════════════════════════════════════
   SEARCH
   ══════════════════════════════════════════════════════════════════════ */
.findbar {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 18px; padding: 10px 16px;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 999px;
  transition: border-color 0.25s, background 0.25s;
}
.findbar:focus-within { border-color: color-mix(in srgb, var(--c1, var(--accent)) 45%, var(--line)); background: var(--surface-2); }
.findbar-i { width: 15px; height: 15px; color: var(--text-3); flex: none; }
.findbar-in {
  flex: 1; min-width: 0;
  font: inherit; font-size: 0.92rem; color: var(--text);
  background: transparent; border: 0; outline: none;
  padding: 2px 0;
}
.findbar-in::placeholder { color: var(--text-3); }
.findbar-in::-webkit-search-cancel-button { filter: grayscale(1) opacity(0.5); cursor: pointer; }
.findbar-n {
  font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-3);
  white-space: nowrap;
}
.row.is-nomatch {
  max-height: 0; padding-top: 0; padding-bottom: 0;
  opacity: 0; overflow: hidden; border-bottom-color: transparent;
  transition: max-height 0.4s var(--ease), opacity 0.2s, padding 0.4s var(--ease);
}
.empty-find {
  padding: 34px 0; color: var(--text-3);
  font-family: var(--font-mono); font-size: 0.82rem;
}
.empty-find b { color: var(--text); font-weight: 500; }

html:not(.js) .findbar, html:not(.js) .reader-btn, html:not(.js) .reader-panel { display: none; }

/* ══════════════════════════════════════════════════════════════════════
   A UTILITY, ONCE
   ══════════════════════════════════════════════════════════════════════ */
.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* ══════════════════════════════════════════════════════════════════════
   THE MASTHEAD STAT ROW
   ══════════════════════════════════════════════════════════════════════
   Three facts about the archive and, for a returning reader, a fourth about
   them. Monospaced and small: these are measurements, and setting them in the
   display face would make them compete with the headline they sit under. */
.mast-stats {
  display: flex; flex-wrap: wrap; gap: 0;
  margin: 30px 0 0; padding: 0;
  border-top: 1px solid var(--line);
}
.mast-stats > div {
  padding: 16px 28px 0 0; margin-right: 28px;
  border-right: 1px solid var(--line);
}
.mast-stats > div:last-child { border-right: 0; margin-right: 0; padding-right: 0; }
.mast-stats dt {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--text-3); margin-bottom: 6px;
}
.mast-stats dd {
  margin: 0; font-family: var(--font-display); font-weight: 600;
  font-size: 1.32rem; letter-spacing: -0.02em; color: var(--text);
}
/* The reader's own line is the one that gets the accent — it is the only
   figure on the row that is about them. */
.mast-stat-read dd {
  background: linear-gradient(115deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.mast-stats [hidden] { display: none; }
@media (max-width: 560px) {
  .mast-stats > div { padding-right: 18px; margin-right: 18px; }
  .mast-stats dd { font-size: 1.1rem; }
}

/* ══════════════════════════════════════════════════════════════════════
   READING STATE ON THE REGISTER
   ══════════════════════════════════════════════════════════════════════
   The row number becomes a status light. A piece you are part-way through
   gets a ring drawn to where you stopped; a piece you finished gets a tick
   instead of a full ring, because a full ring and a 94% ring are the two
   states hardest to tell apart and the two that most need telling apart. */
.row-idx { position: relative; display: block; width: 24px; height: 24px; }
.row-n {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  transition: color 0.3s, opacity 0.3s;
}

.row-ring {
  position: absolute; inset: 0; width: 24px; height: 24px;
  pointer-events: none;
}
.row-ring circle { fill: none; stroke-width: 1.6; }
.row-ring-t { stroke: var(--line-strong); opacity: 0.5; }
.row-ring-p {
  stroke: var(--c1);
  /* 2πr at r=9. Drawn from twelve o'clock, clockwise. */
  stroke-dasharray: 56.55;
  stroke-dashoffset: calc(56.55 * (1 - var(--p, 0)));
  stroke-linecap: round;
  transform: rotate(-90deg); transform-origin: 12px 12px;
  animation: ringIn 0.9s var(--ease) both;
}
@keyframes ringIn { from { stroke-dashoffset: 56.55; } }
@media (prefers-reduced-motion: reduce) { .row-ring-p { animation: none; } }

/* A finished piece gets a FILLED disc behind its number rather than a tick
   replacing it. Replacing the number was the first attempt and it cost the
   register the thing that makes it a register: the numbering ran 02, 04, 06
   with silent gaps wherever the reader had finished something. A disc and an
   arc are still unmistakably different at 24px, which was the whole reason
   not to use a 100% ring. */
.row-disc {
  position: absolute; inset: 0; border-radius: 50%;
  background: linear-gradient(135deg, var(--c1), var(--c2));
  opacity: 0.9;
  animation: discIn 0.5s var(--ease) both;
}
@keyframes discIn { from { transform: scale(0.4); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .row-disc { animation: none; } }
.row.is-read .row-n { color: var(--on-accent); font-weight: 600; }
/* A read piece recedes — it does not disappear. Re-reading is a thing people
   do, and dimming to the point of illegibility would punish it. */
.row.is-read .row-title { color: var(--text-2); }
.row.is-read:hover .row-title, .row.is-read.is-active .row-title { color: var(--text); }
.row.is-reading .row-n { font-size: 0.6rem; opacity: 0.75; }

/* The lens is a third, independent filter. It collapses a row exactly as the
   topic chips and the search box do, so all three compose and none of them
   can un-hide what another hid. */
.row.is-outlens {
  max-height: 0; padding-top: 0; padding-bottom: 0;
  opacity: 0; overflow: hidden; border-bottom-color: transparent;
  transition: max-height 0.4s var(--ease), opacity 0.2s, padding 0.4s var(--ease);
}

/* ══════════════════════════════════════════════════════════════════════
   THE LENSES
   ══════════════════════════════════════════════════════════════════════ */
.lenses { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.lens {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.06em;
  color: var(--text-3); background: transparent;
  border: 1px solid transparent; border-radius: 999px;
  padding: 6px 12px; cursor: pointer;
  transition: color 0.25s, border-color 0.25s, background 0.25s;
}
.lens em {
  font-style: normal; font-size: 0.64rem;
  padding: 1px 6px; border-radius: 999px;
  background: var(--surface-2); color: var(--text-3);
  transition: background 0.25s, color 0.25s;
}
.lens:hover { color: var(--text); border-color: var(--line); }
.lens.is-on { color: var(--text); border-color: var(--line-strong); background: var(--surface); }
.lens.is-on em {
  color: var(--on-accent);
  background: linear-gradient(115deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
}

/* ══════════════════════════════════════════════════════════════════════
   THE RESUME STRIP  (index)
   ══════════════════════════════════════════════════════════════════════
   One card, for one piece: the thing this reader is part-way through. Not a
   row of "recently viewed" thumbnails — there is exactly one correct answer
   to "where was I", and offering six of them is offering none. */
.resume-strip {
  position: relative; display: grid;
  grid-template-columns: 132px minmax(0, 1fr) auto;
  gap: 22px; align-items: center;
  margin: 0 0 26px; padding: 16px 22px 16px 16px;
  border: 1px solid color-mix(in srgb, var(--c1, var(--accent)) 26%, var(--line));
  border-radius: 18px;
  background: linear-gradient(100deg,
    color-mix(in srgb, var(--c1, var(--accent)) 9%, transparent), transparent 62%);
  overflow: hidden;
  opacity: 0; transform: translateY(10px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease),
              border-color 0.35s, background 0.35s;
}
.resume-strip.is-on { opacity: 1; transform: none; }
.resume-strip:hover { border-color: color-mix(in srgb, var(--c1, var(--accent)) 52%, var(--line)); }
@media (prefers-reduced-motion: reduce) {
  .resume-strip { transition: none; opacity: 1; transform: none; }
}

.rs-art {
  position: relative; display: block;
  height: 76px; border-radius: 12px; overflow: hidden;
  background: var(--plate);
}
.rs-art svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.rs-body { min-width: 0; }
.rs-k {
  display: block; font-family: var(--font-mono); font-size: 0.62rem;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--c1, var(--accent)); margin-bottom: 7px;
}
.rs-t {
  display: block; font-family: var(--font-display); font-weight: 500;
  font-size: clamp(1rem, 1.5vw, 1.2rem); letter-spacing: -0.02em;
  line-height: 1.25; margin-bottom: 10px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rs-meter {
  display: block; height: 3px; border-radius: 999px;
  background: var(--surface-2); overflow: hidden; margin-bottom: 7px;
}
.rs-meter-f {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
}
.rs-m { display: block; font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-3); }
.rs-cta {
  display: inline-flex; align-items: center; gap: 8px; white-space: nowrap;
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--text-2);
  padding: 9px 15px; border: 1px solid var(--line); border-radius: 999px;
  transition: color 0.25s, border-color 0.25s, background 0.25s;
}
.resume-strip:hover .rs-cta {
  color: var(--on-accent); border-color: transparent;
  background: linear-gradient(115deg, var(--c1, var(--accent)), var(--c2, var(--accent-2)));
}
.resume-strip:hover .rs-cta span { transform: translateX(3px); }
.rs-cta span { transition: transform 0.3s var(--ease); }

@media (max-width: 760px) {
  .resume-strip { grid-template-columns: 84px minmax(0, 1fr); gap: 14px; padding: 13px 15px; }
  .rs-art { height: 56px; }
  .rs-cta { display: none; }        /* the whole card is the link anyway */
}

/* ══════════════════════════════════════════════════════════════════════
   THE RESUME PROMPT  (article)
   ══════════════════════════════════════════════════════════════════════
   Offered, not obeyed. It never scrolls anyone anywhere on its own — see the
   note in blog.js. Bottom-left so it cannot collide with the reading panel,
   which opens top-right. */
.resume {
  position: fixed; z-index: 65;
  left: clamp(12px, 3vw, 30px); bottom: clamp(12px, 3vw, 26px);
  display: flex; align-items: center; gap: 12px;
  padding: 11px 12px 11px 14px;
  background: rgba(var(--scrim), 0.94);
  border: 1px solid var(--line-strong); border-radius: 14px;
  box-shadow: 0 26px 50px -22px var(--shadow);
  backdrop-filter: blur(18px);
  opacity: 0; transform: translateY(14px);
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
}
.resume.is-on { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .resume { transition: none; } }

/* A conic ring: the same "how far in" idea as the register's ring, at the
   size a toast can afford. */
.resume-ring {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  background:
    conic-gradient(var(--post-c1) calc(var(--p, 0) * 360deg), var(--surface-2) 0);
  -webkit-mask: radial-gradient(circle, transparent 6px, #000 6.5px);
  mask: radial-gradient(circle, transparent 6px, #000 6.5px);
}
.resume-text { font-size: 0.84rem; color: var(--text-2); white-space: nowrap; }
.resume-text b { color: var(--text); font-weight: 600; }
.resume-go {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.06em;
  color: var(--on-accent); font-weight: 600;
  background: linear-gradient(115deg, var(--post-c1), var(--post-c2));
  border: 0; border-radius: 999px; padding: 8px 13px; cursor: pointer;
  transition: filter 0.2s;
}
.resume-go:hover { filter: brightness(1.12); }
.resume-x {
  font: inherit; font-size: 1.1rem; line-height: 1;
  color: var(--text-3); background: transparent; border: 0;
  padding: 4px 6px; cursor: pointer; transition: color 0.2s;
}
.resume-x:hover { color: var(--text); }
@media (max-width: 560px) {
  .resume { left: 10px; right: 10px; bottom: 10px; }
  .resume-text { white-space: normal; font-size: 0.8rem; }
}

/* ══════════════════════════════════════════════════════════════════════
   THE COMMAND PALETTE
   ══════════════════════════════════════════════════════════════════════
   One accent pair, resolved per page: the focused post's colourway on the
   index, the article's own on a post. Declared here rather than inherited so
   the palette is never the one element on the page in the wrong colours. */
.cmdk { --k1: var(--accent); --k2: var(--accent-2); }
.blog-index .cmdk { --k1: var(--c1, var(--accent)); --k2: var(--c2, var(--accent-2)); }
.blog-post  .cmdk { --k1: var(--post-c1); --k2: var(--post-c2); }

/* ── The trigger in the nav ── */
.cmdk-btn {
  display: inline-flex; align-items: center; gap: 7px;
  height: 30px; padding: 0 9px 0 10px;
  color: var(--text-3); background: var(--surface);
  border: 1px solid var(--line); border-radius: 999px;
  cursor: pointer;
  transition: color 0.25s, border-color 0.25s, background 0.25s;
}
.cmdk-btn:hover { color: var(--text); border-color: var(--line-strong); background: var(--surface-2); }
.cmdk-btn svg { width: 13px; height: 13px; flex: none; }
.cmdk-btn kbd {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.04em;
  color: var(--text-3); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 5px;
  padding: 2px 5px;
}
/* Same 44px thumb target the other nav controls take. */
.cmdk-btn { position: relative; }
.cmdk-btn::after {
  content: ''; position: absolute; left: -4px; right: -4px;
  top: 50%; height: 44px; transform: translateY(-50%);
}
@media (max-width: 560px) { .cmdk-btn kbd { display: none; } .cmdk-btn { padding: 0 9px; } }

/* ── The dialog ── */
.cmdk { position: fixed; inset: 0; z-index: 90; }
.cmdk[hidden] { display: none; }
.cmdk-scrim {
  position: absolute; inset: 0;
  background: rgba(var(--scrim), 0.62);
  backdrop-filter: blur(6px);
  opacity: 0; transition: opacity 0.18s;
}
.cmdk.is-on .cmdk-scrim { opacity: 1; }

.cmdk-box {
  position: relative;
  width: min(640px, calc(100vw - 24px));
  margin: clamp(48px, 12vh, 130px) auto 0;
  background: rgba(var(--scrim), 0.97);
  border: 1px solid var(--line-strong); border-radius: 18px;
  box-shadow: 0 44px 90px -30px var(--shadow);
  backdrop-filter: blur(22px);
  overflow: hidden;
  opacity: 0; transform: translateY(-10px) scale(0.985);
  transition: opacity 0.18s var(--ease), transform 0.18s var(--ease);
}
.cmdk.is-on .cmdk-box { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .cmdk-scrim, .cmdk-box { transition: none; }
}
/* The page must not scroll behind an open palette — a scrim you can scroll
   through reads as a bug on every touch device. */
html.is-cmdk, html.is-cmdk body { overflow: hidden; }

.cmdk-head {
  display: flex; align-items: center; gap: 11px;
  padding: 15px 16px; border-bottom: 1px solid var(--line);
}
.cmdk-head-i { width: 16px; height: 16px; flex: none; color: var(--k1); }
.cmdk-in {
  flex: 1; min-width: 0;
  font: inherit; font-size: 1rem; color: var(--text);
  background: transparent; border: 0; outline: none;
}
.cmdk-in::placeholder { color: var(--text-3); }
.cmdk-esc {
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.08em;
  color: var(--text-3); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 5px; padding: 3px 6px;
}

.cmdk-list { max-height: min(52vh, 440px); overflow-y: auto; padding: 8px; }
.cmdk-group {
  margin: 10px 0 5px; padding: 0 8px;
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--text-3);
}
.cmdk-list > .cmdk-group:first-child { margin-top: 2px; }

.cmdk-i {
  display: grid; grid-template-columns: 24px minmax(0, 1fr) auto auto;
  gap: 11px; align-items: center;
  padding: 9px 10px; border-radius: 10px; cursor: pointer;
  scroll-margin: 8px;
}
.cmdk-i.is-at { background: color-mix(in srgb, var(--k1) 15%, var(--surface)); }

/* The kind marker. Drawn in CSS rather than shipped as four more SVGs —
   these are one glyph each and the palette is already the largest thing in
   this file. */
.cmdk-i-ico {
  width: 24px; height: 24px; border-radius: 7px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--text-3);
  font-family: var(--font-mono); font-size: 0.72rem; line-height: 1;
  transition: background 0.2s, color 0.2s;
}
.cmdk-i-ico::before { content: '#'; }
.cmdk-i[data-kind='post'] .cmdk-i-ico::before { content: '\00B6'; }      /* pilcrow: a piece */
.cmdk-i[data-kind='section'] .cmdk-i-ico::before { content: '\00A7'; }   /* section sign */
.cmdk-i[data-kind='cmd'] .cmdk-i-ico::before { content: '\232B'; }       /* a key */
.cmdk-i[data-kind='resume'] .cmdk-i-ico::before { content: '\25B6'; }    /* resume */
.cmdk-i.is-at .cmdk-i-ico {
  color: var(--on-accent);
  background: linear-gradient(115deg, var(--k1), var(--k2));
}

.cmdk-i-main { min-width: 0; }
.cmdk-i-t {
  display: block; font-size: 0.92rem; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The matched characters, wherever they landed. This is the only feedback
   that explains WHY a result is in the list. */
.cmdk-i-t b { color: var(--k1); font-weight: 600; }
.cmdk-i-s {
  display: block; margin-top: 2px; font-size: 0.76rem; color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cmdk-i-state {
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.06em;
  color: var(--k1); background: color-mix(in srgb, var(--k1) 14%, transparent);
  border-radius: 999px; padding: 3px 7px; white-space: nowrap;
}
.cmdk-i-hint {
  font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-3);
  white-space: nowrap;
}
.cmdk-none { padding: 30px 12px; color: var(--text-3); font-family: var(--font-mono); font-size: 0.8rem; }
.cmdk-none b { color: var(--text); font-weight: 500; }

.cmdk-foot {
  position: relative;
  display: flex; align-items: center; gap: 16px;
  padding: 10px 16px; border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3);
}
.cmdk-foot kbd {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 16px; padding: 1px 4px; margin-right: 3px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 4px; font-family: inherit; font-size: 0.6rem;
}
.cmdk-foot-n { margin-left: auto; }
/* The list is scrollable and its last row is usually cut mid-height. A hard
   edge there reads as a clipping bug; a short fade reads as "there is more". */
.cmdk-foot::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 100%;
  height: 26px; pointer-events: none;
  background: linear-gradient(transparent, rgba(var(--scrim), 0.97));
}

@media (max-width: 640px) {
  .cmdk-box { margin-top: 8vh; }
  .cmdk-i { grid-template-columns: 24px minmax(0, 1fr) auto; }
  .cmdk-i-hint { display: none; }
  .cmdk-foot span:not(.cmdk-foot-n) { display: none; }
}

/* Nothing here means anything without JS: the palette cannot open, the ring
   is never drawn, and the trigger would be a button that does nothing. */
html:not(.js) .cmdk, html:not(.js) .cmdk-btn { display: none; }

/* ══════════════════════════════════════════════════════════════════════
 * TL;DR · TAKEAWAYS · FAQ
 * ══════════════════════════════════════════════════════════════════════
 * Three blocks carried on posts from admin-service. Appended at the end of
 * this file rather than woven in, so they can be removed in one cut and so
 * they cannot disturb the rules above them.
 *
 * Every colour here comes from the existing custom properties, so all three
 * repaint with the ground switch like everything else on the page — see the
 * --cd1/--cl1 both-mixes rule at the top of this file. Nothing below resolves
 * a colourway itself.
 *
 * The FAQ is <details>, which hides answers VISUALLY while leaving them in the
 * document. That distinction is load-bearing: the same Q&A is emitted as
 * schema.org FAQPage, and Google requires the marked-up answer to be present
 * on the page. display:none on the text would break that; <details> does not.
 */

.post-tldr,
.post-takeaways {
  margin: 0 0 1.75rem;
  padding: 1rem 1.15rem;
  border-left: 2px solid var(--c1, currentColor);
  background: color-mix(in srgb, var(--c1, currentColor) 5%, transparent);
  border-radius: 0 6px 6px 0;
}

.post-tldr p,
.post-takeaways li { margin: 0; }

.post-tldr-h,
.post-takeaways-h {
  margin: 0 0 .4rem;
  font-size: .7rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  opacity: .65;
}

.post-takeaways ul {
  margin: 0;
  padding-left: 1.1rem;
  display: grid;
  gap: .4rem;
}

.post-faq {
  margin: 3rem 0 0;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line, rgba(128, 128, 128, .2));
}

.post-faq h2 { margin: 0 0 .9rem; }

.post-faq-item {
  border-bottom: 1px solid var(--line, rgba(128, 128, 128, .15));
  padding: .65rem 0;
}

.post-faq-item summary {
  cursor: pointer;
  font-weight: 500;
  /* The default marker sits badly against the text at this size; the rotating
     caret below replaces it. list-style covers Firefox, ::-webkit-details-marker
     covers Safari — both are needed. */
  list-style: none;
  display: flex;
  gap: .5rem;
  align-items: baseline;
}

.post-faq-item summary::-webkit-details-marker { display: none; }

.post-faq-item summary::before {
  content: '›';
  opacity: .5;
  transition: transform .18s ease;
  display: inline-block;
}

.post-faq-item[open] summary::before { transform: rotate(90deg); }

.post-faq-item p {
  margin: .55rem 0 .25rem 1rem;
  opacity: .85;
}

@media (prefers-reduced-motion: reduce) {
  .post-faq-item summary::before { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════
 * MOBILE + PERFORMANCE
 * ══════════════════════════════════════════════════════════════════════
 * Appended last so it overrides without disturbing anything above it, and so
 * the whole block can be removed in one cut.
 *
 * Every rule here is a response to something measured, not a precaution.
 */

/* ── 1 · Stop animating a full-viewport layer on phones ─────────────────
 *
 * `.field-glow` is a min(1200px, 130vw) square — so ~130% of the viewport
 * width — sitting in a `position: fixed` layer, painting a radial gradient,
 * and running a transform+scale animation FOREVER. Over it, `.field-grid`
 * composites a masked gradient.
 *
 * On a desktop GPU that is free. On a mid-range phone it is a permanently
 * animating, full-screen composited layer, which costs frames during scroll
 * and costs battery while the page merely sits open — for decoration behind
 * the text.
 *
 * The fix keeps the design and drops the cost: the gradient still paints, it
 * just stops moving. `prefers-reduced-motion` already handled this for the
 * few who set it; almost nobody does, and this is a performance problem
 * rather than a vestibular one.
 */
@media (max-width: 720px) {
  .field-glow {
    animation: none !important;
    /* Slightly smaller and dimmer without the motion to hide its edges. */
    width: min(900px, 120vw);
    opacity: .75;
  }
  .field-grid {
    /* The mask forces an extra compositing pass for a texture that is barely
       visible at this size. */
    mask-image: none;
    -webkit-mask-image: none;
    opacity: .5;
  }
}

/* ── 2 · Defer painting the expensive blocks ────────────────────────────
 *
 * `content-visibility: auto` lets the browser skip layout and paint for an
 * element until it approaches the viewport. It is the largest rendering win
 * available on a long article — and it is easy to apply in a way that makes
 * things worse, which the first attempt here did.
 *
 * WHAT NOT TO DO: applying it to every block after the third heading. `.prose`
 * is flat — h2, p, p, pre, p — so that meant every PARAGRAPH became its own
 * containment box carrying a 420px size estimate. A fifty-paragraph article
 * would report a scroll height several times its real one, the scrollbar would
 * shrink as the reader moved down it, and anchor links would land in the wrong
 * place. Deferring a paragraph saves almost nothing anyway: text layout is
 * cheap.
 *
 * WHAT ACTUALLY PAYS: the blocks that are individually large and expensive to
 * paint — code blocks, figures and tables. Those are the ones worth deferring,
 * their intrinsic size can be estimated honestly, and there are few enough of
 * them per article that a wrong estimate cannot distort the scrollbar.
 *
 * `:not(:first-of-type)` keeps the first one eager: on most posts it is at or
 * near the fold, and deferring something the reader is about to look at is a
 * net loss.
 */
.prose pre:not(:first-of-type),
.prose figure:not(:first-of-type),
.prose table {
  content-visibility: auto;
}

/* Sized per type, because a code block and a table are not the same shape.
   `auto` means "use the remembered size when there is one" — so these numbers
   only apply on a first visit, and only until the element has been seen once. */
.prose pre:not(:first-of-type)   { contain-intrinsic-size: auto 260px; }
.prose figure:not(:first-of-type) { contain-intrinsic-size: auto 320px; }
.prose table                      { contain-intrinsic-size: auto 240px; }

/* Never defer what a jump link targets, or what follows it — the browser
   cannot scroll accurately to an element whose height it has not resolved. */
.prose :target,
.prose :target ~ * {
  content-visibility: visible;
}

/* ── 3 · Touch targets ──────────────────────────────────────────────────
 *
 * NOT HERE — and that is the point worth recording.
 *
 * The reactions bar's styles are injected by engage.js into <head> at runtime,
 * which puts them AFTER this stylesheet in document order. At equal specificity
 * the later rule wins, so `.sd-engage button { min-height: 44px }` written here
 * would be silently overridden by the widget's own `padding` rule and the fix
 * would appear to do nothing. A media query does not raise specificity either,
 * so wrapping it would not have helped.
 *
 * The sizing therefore lives in engage.js, next to the markup it applies to.
 * See the CSS constant there.
 */

/* ── 4 · TL;DR, takeaways and FAQ on small screens ──────────────────────
 * These shipped without mobile rules. At phone widths the 1.15rem side padding
 * plus the 2px border pushed the text noticeably narrower than the prose it
 * sits in, which reads as a mistake rather than an aside.
 */
@media (max-width: 560px) {
  .post-tldr,
  .post-takeaways {
    padding: .85rem .9rem;
    margin-bottom: 1.4rem;
  }
  .post-takeaways ul { padding-left: .95rem; }
  .post-faq { margin-top: 2.25rem; }
  .post-faq-item summary {
    /* A 44px row, so the whole line is the tap target rather than the text. */
    min-height: 44px;
    align-items: center;
  }
  .post-faq-item p { margin-left: .75rem; }
}

/* ── 5 · Respect data saving ────────────────────────────────────────────
 * A reader on a metered connection does not need the decorative layer at all.
 */
@media (prefers-reduced-data: reduce) {
  .field-glow,
  .field-grid { display: none; }
}
