/* ─── tokens ─────────────────────────────────────────────────── */

:root {
  color-scheme: dark;
  --bg: #0a0a0b;
  --surface: #131316;
  --surface-2: #18181d;
  --elevated: #1f1f25;
  --border: #26262d;
  --border-strong: #2f2f37;
  --border-input: #5c6068;
  --text: #e8e8ec;
  --dim: #8a8a93;
  --faint: #808089;
  --accent: #fb923c;
  --accent-d: #f97316;
  --accent-dim: rgba(251, 146, 60, 0.10);
  --ink-on-accent: #1a1300;
  --pass: #34d399;
  --fail: #f87171;
  --blue: #60a5fa;
  --purple: #c084fc;
  --amber: #f59e0b;
  --cyan: #22d3ee;
  --rose: #fb7185;
  --sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace;
  --radius: 8px;
  --radius-sm: 4px;
  --gap: 0.75rem;
  --line: var(--border);
  --line-strong: var(--border-input);
  --muted: var(--faint);
  --muted-2: var(--dim);
  --font-ui: var(--sans);
  --font-mono: var(--mono);
  --measure: 64rem;
  /* The height of the header, in one place: the nav uses it and so does the tool
     frame, which fills exactly what is left of the window. */
  --nav-height: 53px;
}

/*
 * Daylight: the same design in light. Warm paper rather than white, and the accent
 * darkened until it can carry text and sit under white text — the one token that
 * cannot be reused from the dark scheme, because #fb923c on paper is a highlighter.
 * The rest are the dark colours moved down the scale, so a pass, a failure or a tag
 * keeps its meaning in either scheme.
 */
html[data-theme="light"] {
  color-scheme: light;
  --bg: #faf8f5;
  --surface: #ffffff;
  --surface-2: #f2efe9;
  --elevated: #ffffff;
  --border: #e6e1d7;
  --border-strong: #d2cabc;
  --border-input: #9c948a;
  --text: #1b1a17;
  --dim: #5b564e;
  --faint: #78726a;
  --accent: #c2410c;
  --accent-d: #9a3412;
  --accent-dim: rgba(194, 65, 12, 0.09);
  --ink-on-accent: #fff7ed;
  --pass: #047857;
  --fail: #b91c1c;
  --blue: #1d4ed8;
  --purple: #7e22ce;
  --amber: #b45309;
  --cyan: #0e7490;
  --rose: #be123c;
}

/* ─── fonts ──────────────────────────────────────────────────── */

/*
 * One file per script, one range per family.
 *
 * The files are the previous build's, byte for byte, and they cover 300 to 700 —
 * which is why the range is declared rather than a single weight: a heading asked
 * for at 700 while the face claims 300 to 600 is drawn at 600, and the whole type
 * scale then reads a step too light. 800 is asked for in one place, the post title;
 * the closest the family has is 700 and the browser takes it.
 */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

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

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/jetbrains-mono.woff2') format('woff2');
}

/* ─── base ───────────────────────────────────────────────────── */

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

:where(html) {
  font: 14px/1.5 var(--sans);
  color: var(--text);
  background: var(--bg);
  tab-size: 2;
  text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  scroll-behavior: smooth;
}

:where(body) { margin: 0; min-block-size: 100dvh; }

:where(h1, h2, h3, h4, h5, h6) {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

:where(p, ul, ol, dl, pre, figure, blockquote) { margin: 0; }
:where(ul, ol) { padding: 0; list-style: none; }
:where(a) { color: var(--accent); text-decoration: none; }
:where(a:hover) { text-decoration: underline; text-underline-offset: 3px; }
:where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
:where(code, kbd, samp, pre) { font-family: var(--mono); font-size: 0.9em; }

:where(:not(pre) > code) {
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 0.05em 0.4em;
  border-radius: var(--radius-sm);
  color: var(--accent);
  /*
   * Inline code must not be nowrap. These posts name registry paths, symbol
   * names and hashes far longer than a phone is wide, and one that cannot break
   * pushes the whole page sideways instead of wrapping inside its paragraph.
   */
  overflow-wrap: break-word;
}

:where(:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
:where(::selection) { background: var(--accent); color: var(--bg); }

*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }
*::-webkit-scrollbar-thumb:hover { background: #3f3f46; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  :where(html) { scroll-behavior: auto; }
}

.skip-link {
  position: absolute;
  inset-inline-start: 0.5rem;
  inset-block-start: 0.5rem;
  padding: 0.4rem 0.75rem;
  background: var(--accent);
  color: var(--bg);
  border-radius: var(--radius-sm);
  transform: translateY(-200%);
  z-index: 100;
}

.skip-link:focus { transform: none; }

body.page-host {
  background-image:
    radial-gradient(ellipse 80% 60% at 50% 0%, rgba(251, 146, 60, 0.07), transparent 60%),
    radial-gradient(ellipse 60% 40% at 50% 100%, rgba(251, 146, 60, 0.04), transparent 60%);
}

main {
  max-inline-size: var(--measure);
  margin-inline: auto;
  padding: 4rem 1.5rem 6rem;
  display: block;
}

/*
 * A page of prose has the live site's own frame: a wider measure than the blog list,
 * a rhythm of its own rather than margins between blocks, and more room above and
 * below. The three frames are the three pages a reader meets — the blog list, a page
 * like this one, and a post.
 */
main.page {
  max-inline-size: 70rem;
  padding: 6rem 2rem 5rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 4.5rem;
}

/* The frame's rhythm is the gap, so the margins between blocks must not add to it. */
main.page > * + * { margin-block-start: 0; }

main > * + * { margin-block-start: 2.5rem; }

.post-head + .description,
.post-head > .description { margin-block-start: 0.9rem; }

.foot {
  max-inline-size: var(--measure);
  margin-inline: auto;
  padding: 0 1.5rem 4rem;
  color: var(--faint);
  font-size: 0.88rem;
  border-block-start: 1px solid var(--border);
  padding-block-start: 1.5rem;
}

/* One rule, so a fallback that hides a pane always wins the cascade. */
[hidden] { display: none !important; }

/*
 * The inactive pane of a code pair is hidden in the markup, so the browser
 * paints one pane before the script runs and a reader without the script keeps
 * one pane rather than two stacked listings. site.js clears @hidden for the
 * pane a tab selects; where inert is unavailable it marks the document
 * progressive and leaves the hidden pane hidden.
 */
[data-progressive] .code-figure pre[data-pane][hidden] { display: none; }

/* ─── nav ────────────────────────────────────────────────────── */

[data-nav] {
  --nav-line: var(--line);
  --nav-text: var(--text);
  --nav-dim: var(--muted-2);
  --nav-faint: var(--muted);
  --nav-surface: var(--surface);
  --nav-hover: var(--surface-2);
  --nav-font: var(--font-ui);
  --nav-accent: var(--accent);
  display: flex;
  align-items: center;
  gap: 12px;
  min-block-size: var(--nav-height);
  padding: 8px 12px;
  background: color-mix(in oklab, var(--nav-surface) 96%, transparent);
  backdrop-filter: blur(8px);
  z-index: 50;
  border-block-end: 1px solid var(--nav-line);
  font: 400 11px/1 var(--nav-font);
  letter-spacing: 2px;
  text-transform: uppercase;
  white-space: nowrap;
}

/*
 * The header sticks, the nav inside it does not.
 *
 * A sticky child sticks within its parent's box, and the header is only as tall as the
 * nav — so when the nav was the sticky one, and the banner landmark wrapper was added
 * around it, the header became the box the nav could not leave and the nav scrolled away
 * with the page. The header is the element that spans the page, so the header is what
 * sticks; the nav keeps the blur and the translucent surface that make the text under it
 * legible while the page moves.
 */
.site-head {
  position: sticky;
  inset-block-start: 0;
  z-index: 50;
}

[data-nav][data-sticky] { position: relative; }

[data-nav] [data-home] {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-block-size: 36px;
  color: var(--nav-text);
}

[data-nav] [data-home]:hover,
[data-nav] [data-home]:focus-visible { color: var(--nav-accent); text-decoration: none; }

[data-nav] [data-mark] {
  inline-size: 16px;
  block-size: 16px;
  color: var(--nav-accent);
}

[data-nav] [data-menu] {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  border-radius: 6px;
}

[data-nav] [data-menu]::before {
  content: "/";
  color: var(--nav-faint);
  opacity: .6;
  margin-inline-end: 10px;
}

[data-nav] [data-menu] > a,
[data-nav] [data-menu] > button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  min-block-size: 36px;
  margin: 0;
  background: none;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--nav-dim);
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-decoration: none;
  cursor: pointer;
}

/* The chevron only appears when the menu is under the pointer. Collapsing the toggle to no
   width is the live site's own trick, and it is what stops the header looking like a row of
   dropdowns; a reader without a pointer gets it always, in the narrow rule below. */
[data-nav] [data-menu] > button[data-menu-toggle] {
  width: 0;
  min-inline-size: 0;
  padding: 4px 0;
  overflow: hidden;
  justify-content: center;
  transition: width .16s ease, padding .16s ease;
}

[data-nav] [data-menu]:hover > button[data-menu-toggle],
[data-nav] [data-menu]:focus-within > button[data-menu-toggle],
[data-nav] [data-menu] > button[data-menu-toggle][aria-expanded="true"] {
  width: 28px;
  padding: 4px;
}

@media (prefers-reduced-motion: reduce) {
  [data-nav] [data-menu] > button[data-menu-toggle] { transition: none; }
}

[data-nav] [data-menu] > a { color: var(--nav-accent); }
[data-nav] [data-menu][data-current] > a,
[data-nav] [data-menu][data-current] > button { color: var(--nav-accent); }
[data-nav] [data-menu] > a:hover,
[data-nav] [data-menu] > a:focus-visible,
[data-nav] [data-menu] > button:hover,
[data-nav] [data-menu] > button:focus-visible {
  background: var(--nav-hover);
  color: var(--nav-text);
  text-decoration: none;
}
[data-nav] [data-menu][data-current] > a:hover,
[data-nav] [data-menu][data-current] > button:hover { color: var(--nav-accent); }

[data-nav] [data-chevron] {
  width: 13px;
  height: 13px;
  color: var(--nav-faint);
  opacity: 0;
  transform: translateY(-1px);
  transition: opacity .14s ease, transform .14s ease;
}

[data-nav] [data-menu]:hover [data-chevron],
[data-nav] [data-menu] > button:focus-visible [data-chevron],
[data-nav] [data-menu] > button[aria-expanded="true"] [data-chevron] { opacity: 1; transform: none; }
[data-nav] [data-menu] > button[aria-expanded="true"] [data-chevron] { transform: rotate(180deg); }

/*
 * The dropdown, as the live site draws it: flush with the top of the header rather than
 * hanging under the button, a surface of its own, a 10px corner and a wide soft shadow.
 *
 * Its position is the part worth copying exactly. A menu that opened below the button
 * would start 44px down and read as a popover; one that opens at the top of the header
 * reads as a panel the header is part of, which is what the live site's does.
 */
[data-nav] [data-menu-list] {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 60;
  min-width: 220px;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--nav-surface);
  border: 1px solid var(--nav-line);
  border-radius: 10px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, .45);
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  white-space: nowrap;
  display: none;
}

/*
 * The menu itself carries the expanded state, and the button also does.
 *
 * Two rules because there are two ways to reach the panel: the script sets both attributes,
 * and a reader whose script did not run still gets the panel from the button's own state if
 * anything ever sets that. The button's rule uses a sibling combinator, so the list has to
 * be *after* the button in the markup — which is the live page's order and the order this
 * build renders.
 */
[data-nav] [data-menu][aria-expanded="true"] [data-menu-list],
[data-nav] [data-menu] > button[aria-expanded="true"] ~ [data-menu-list] { display: block; }

[data-nav] [data-menu-list] a,
[data-nav] [data-menu-list] li a {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  min-block-size: 36px;
  border-radius: 6px;
  color: var(--nav-dim);
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-decoration: none;
}

[data-nav] [data-menu-list] a svg { width: 16px; height: 16px; flex: none; }

[data-nav] [data-menu-list] a:hover,
[data-nav] [data-menu-list] a:focus-visible {
  background: var(--nav-hover);
  color: var(--nav-text);
  text-decoration: none;
}

/* The page you are on keeps its accent and gains a wash, so the menu says where you are
   rather than only where you could go. */
[data-nav] [data-menu-list] a[aria-current="page"] {
  color: var(--nav-accent);
  background: color-mix(in oklab, var(--nav-accent) 12%, transparent);
}

[data-nav] [data-menu-list] a[aria-current="page"]:hover { color: var(--nav-accent); }
[data-nav] [data-menu-list] li[data-sub] a { padding-inline-start: 28px; }

/* The tool menu names the tool in hand rather than a chevron alone, so its toggle never
   collapses: a header that hid the answer until a pointer arrived would name nothing. */
[data-nav] [data-toolmenu] > button[data-menu-toggle],
[data-nav] [data-toolmenu]:hover > button[data-menu-toggle],
[data-nav] [data-toolmenu]:focus-within > button[data-menu-toggle],
[data-nav] [data-toolmenu] > button[data-menu-toggle][aria-expanded="true"] {
  width: auto;
  padding: 4px 6px;
  overflow: visible;
  color: var(--nav-accent);
}

[data-nav] [data-toolmenu] [data-tool-mark] { inline-size: 15px; block-size: 15px; color: var(--nav-accent); }
[data-nav] [data-toolmenu] [data-chevron] { inline-size: 13px; block-size: 13px; opacity: 0; }
[data-nav] [data-toolmenu]:hover [data-chevron],
[data-nav] [data-toolmenu] > button[aria-expanded="true"] [data-chevron] { opacity: 1; }
[data-nav] [data-toolmenu] [data-tool-name] { font-weight: 600; }

[data-nav] [data-tools-list] { min-inline-size: 220px; }
[data-nav] [data-nav-actions] { margin-inline-start: auto; display: flex; align-items: center; gap: 10px; }

[data-nav] [data-nav-actions] {
  margin-inline-start: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

[data-nav] [data-nav-actions] a {
  color: var(--nav-dim);
  padding: 4px 6px;
  min-block-size: 36px;
  display: inline-flex;
  align-items: center;
  border-radius: 6px;
  border: 1px solid transparent;
}

[data-nav] [data-nav-actions] a:hover,
[data-nav] [data-nav-actions] a:focus-visible { color: var(--nav-accent); text-decoration: none; }

[data-nav] [data-theme-switch] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 36px;
  block-size: 36px;
  border-radius: 6px;
  border: 1px solid transparent;
  color: var(--nav-dim);
  transition: color 160ms ease, background 160ms ease;
}

[data-nav] [data-theme-switch]:hover,
[data-nav] [data-theme-switch]:focus-visible { color: var(--nav-accent); background: var(--nav-hover); }
[data-nav] [data-theme-switch] svg { inline-size: 16px; block-size: 16px; }

/*
 * One button, two icons. Which one is on screen is the scheme in force, so the
 * switch says what it is as well as what it would do, and it does so without the
 * script having to swap an image.
 */
html:not([data-theme="light"]) [data-theme-switch] [data-theme-icon="sun"] { display: none; }
html[data-theme="light"] [data-theme-switch] [data-theme-icon="moon"] { display: none; }

/* The wash behind the page is the accent at a few percent: the same idea in light,
   at a third of the strength, because paper shows a tint that ink does not. */
html[data-theme="light"] body.page-host {
  background-image:
    radial-gradient(ellipse 80% 60% at 50% 0%, rgba(194, 65, 12, 0.05), transparent 60%),
    radial-gradient(ellipse 60% 40% at 50% 100%, rgba(194, 65, 12, 0.03), transparent 60%);
}

/* The old dropdown rules were replaced above, at the head of the nav's own block, because
   two sets of them lived here and the later one had quietly won: a menu that opened 8px
   below the button on `--elevated` instead of flush at the top of the header on the nav's
   own surface. The port from the live page is one block now, in one place. */

[data-nav] [data-tools-list] li a {
  display: flex;
  align-items: center;
  gap: 6px;
}

[data-nav] [data-tools-list] li a svg { inline-size: 15px; block-size: 15px; flex: none; }

@media (max-width: 640px) {
  [data-nav] [data-nav-actions] a { display: none; }
  [data-nav] [data-wordmark] { display: none; }
}

/* ─── icons ──────────────────────────────────────────────────── */

/*
 * There is deliberately no blanket rule setting fill and stroke on every <svg>.
 * Those are inherited properties, so a rule aimed at a presenter overrides the
 * attributes on the symbol it references — every symbol in the sprite carries its
 * own presentation for exactly that reason. Each place an SVG appears sets what
 * it needs: a rule of its own for the icons in the chrome, another for the
 * presentational SVGs, and the illustration inside a post styles itself inline.
 */

/*
 * The sprite is not drawn. It is not a picture: it is where the shapes the page's
 * `<use>` elements point at live. `hidden` alone is not enough — a stylesheet that gives
 * every svg a display beats the attribute's own — so it says so here too.
 */
.icon-sprite { display: none; }

/*
 * A reference has no intrinsic shape, and an inlined copy had one.
 *
 * Lucide's own `<svg>` states `width="24" height="24"`, so a copy of it carried an intrinsic
 * aspect ratio and a rule that stretched one axis scaled the other with it. An `<svg>` holding
 * a `<use>` states neither, so the same rule stretches one axis alone: the explorer's twisty,
 * which fills its row's height, came out thirteen by thirty-two instead of thirteen square. A
 * square box is that aspect ratio said in CSS, and it is what every place that sizes an icon
 * now measures against.
 */
svg[data-lucide] { inline-size: 1em; block-size: 1em; }

.blog-card-meta .blog-card-more svg {
  inline-size: 15px;
  block-size: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ─── blog list ──────────────────────────────────────────────── */

.blog-list-head h1 {
  font-size: clamp(2.5rem, 4.5vw, 3rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.blog-list-head h1 em { font-style: normal; color: var(--accent); }

.blog-list-head p {
  margin-block-start: 0.75rem;
  color: var(--dim);
  font-size: 1.125rem;
  line-height: 1.55;
  max-inline-size: 60ch;
}

/* The live site's own rhythm: the hero block ends 42px above what follows, and the
   tools section 49px above the blog. Both are larger than the page-wide default, and
   adjacent margins collapse to the larger of the two, so the default stays as the
   fallback for pages that say nothing. */
/*
 * The blog list's own frame.
 *
 * The live page's top padding is what puts its title where it is; this page opens with a
 * masthead line above the title, so the padding gives up exactly what that line and its
 * gap take and the title lands where the live page's does. The line is not added on top of
 * the page — it is the page's first line, and the space it uses was already there.
 */
main.blog-list-page { padding-block-start: 1.3rem; }

.blog-list-head { margin-block-end: 3rem; }

/* The masthead line: the same small upper-case treatment the section labels use, so the
   page opens with the quietest type on it rather than the loudest. */
.blog-list-head .eyebrow {
  margin-block-end: 0.75rem;
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}

.tools { margin-block-end: 3.5rem; }

.tools h2,
.blog-list h2 {
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin-block-end: 0;
}

.tools { display: grid; gap: 1rem; min-inline-size: 0; }

/* The list of tool cards. Two to a row on a page wide enough to hold them; one column
   everywhere else, which is what the home page's section and a phone both want. */
.tool-list { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr)); }

.tool-list a.tool-card,
.tool-list .tool-soon {
  display: grid;
  gap: 0.9rem;
  align-content: start;
  padding: 0.9rem 1rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  /* The live site's own row is a shade that falls from one surface to the next; the
     hover lifts it and warms the hairline, which is the one thing that says a row is
     something to press without drawing a button. */
  background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 80%, transparent), color-mix(in oklab, var(--surface-2) 60%, transparent));
  color: var(--text);
  transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
}

.tool-list a.tool-card:hover,
.tool-list a.tool-card:focus-visible {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  transform: translateY(-1px);
  box-shadow: 0 12px 26px -20px rgb(0 0 0 / 0.8);
}

.tool-list a.tool-card:hover { border-color: var(--accent); text-decoration: none; }

/* The name, its note, and the description: one column, so a description cannot wrap
   around its own title and read as two things. */
.tool-card-text { display: grid; gap: 0.4rem; min-inline-size: 0; }

.tool-name { display: flex; align-items: center; gap: 0.55rem; font-size: 1.05rem; font-weight: 600; }
.tool-name svg { inline-size: 17px; block-size: 17px; color: var(--accent); flex-shrink: 0; }
.tool-list .tool-soon .tool-name { color: var(--dim); }
.tool-list .tool-soon .tool-name svg { color: var(--faint); }

.tool-list .slug {
  /* The note sits after the name, set in the monospace face the live site uses for it
     and away from the name so the two do not read as one label. */
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 400;
  color: var(--faint);
  margin-inline-start: auto;
  text-align: end;
}

/* The description is the card's own sentence, not a paragraph under the list, so it is
   the card's body text and it is allowed to be two lines. */
.tool-summary { color: var(--dim); font-size: 0.9rem; line-height: 1.6; max-inline-size: 52ch; }

.tool-list .tool-soon { border-style: dashed; }

/* ─── the figure on a card ───────────────────────────────────── */

/*
 * The machine a tool is, at work — one per tool, because the two tools do different things.
 *
 * A figure is a drawing, not an icon, and it says so with `data-figure`. It is wholly
 * decoration: `aria-hidden`, no text a reader has to read, and `prefers-reduced-motion`
 * stops every animation and leaves the still frame, which is the picture the motion is of.
 *
 * The SVG sets its own default `fill` and `stroke` on the element the shapes inherit from.
 * That is deliberate and was learned: a rule aimed at `.tool-figure rect` ties on specificity
 * with `.tf-case` and Chrome resolved the tie against the later rule, so every filled shape
 * computed to `fill: none` and the figure drew as outlines and nothing else.
 */
.tool-figure { margin: 0; display: grid; justify-items: center; }

.tool-figure svg {
  fill: none;
  stroke: none;
  inline-size: 100%;
  max-inline-size: 30rem;
  block-size: auto;
  overflow: visible;
}

/* The cartridge and the panel both figures are built from. */
.tf-case { fill: color-mix(in oklab, var(--surface-2) 72%, transparent); stroke: var(--border-strong); stroke-width: 1; }
.tf-label { fill: color-mix(in oklab, var(--accent) 14%, transparent); stroke: color-mix(in oklab, var(--accent) 40%, var(--border)); stroke-width: 1; }
.tf-brand { fill: var(--accent); font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 0.22em; }
.tf-notch { fill: var(--bg); stroke: var(--border-strong); stroke-width: 1; }
.tf-board { fill: color-mix(in oklab, var(--faint) 30%, transparent); }
.tf-board-0 { fill: color-mix(in oklab, var(--accent) 36%, transparent); }
.tf-board-1 { fill: color-mix(in oklab, var(--faint) 46%, transparent); }
.tf-pin { fill: color-mix(in oklab, var(--faint) 55%, transparent); }
.tf-panel { fill: color-mix(in oklab, var(--surface-2) 45%, transparent); stroke: var(--border); stroke-width: 1; }
.tf-head-bar { fill: color-mix(in oklab, var(--accent) 16%, transparent); }
.tf-title { fill: var(--text); font-family: var(--mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.1em; }
.tf-slot-name { fill: var(--dim); font-family: var(--sans); font-size: 9.5px; }
.tf-slot { fill: color-mix(in oklab, var(--faint) 38%, transparent); }
.tf-portrait { fill: color-mix(in oklab, var(--accent) 26%, transparent); stroke: color-mix(in oklab, var(--accent) 45%, transparent); stroke-width: 0.8; }

/* The read head: a bar carrying the glow its pass leaves behind. The glow is a path with no
   bottom radius rather than a rounded rectangle, because a rounded one draws a bump the eye
   reads as a shape of its own. */
.tf-head { fill: color-mix(in oklab, var(--accent) 85%, transparent); }
.tf-head-glow { fill: color-mix(in oklab, var(--accent) 14%, transparent); }

/* Rare Ratchet's stream: a frame, a magic word, and bytes with no shape. */
.tf-stream { fill: color-mix(in oklab, var(--bg) 80%, transparent); stroke: var(--border-strong); stroke-width: 1; }
.tf-magic { fill: color-mix(in oklab, var(--accent) 22%, transparent); }
.tf-magic-text { fill: var(--accent); font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.08em; }
.tf-byte { fill: color-mix(in oklab, var(--faint) 45%, transparent); }
.tf-byte-1 { fill: color-mix(in oklab, var(--faint) 30%, transparent); }
.tf-byte-2 { fill: color-mix(in oklab, var(--accent) 26%, transparent); }
.tf-byte-3 { fill: color-mix(in oklab, var(--faint) 55%, transparent); }
.tf-speck { fill: color-mix(in oklab, var(--accent) 55%, transparent); }
.tf-speck-1 { fill: color-mix(in oklab, var(--faint) 60%, transparent); }
.tf-speck-2 { fill: color-mix(in oklab, var(--accent) 80%, transparent); }
.tf-speck-3 { fill: color-mix(in oklab, var(--faint) 40%, transparent); }
.tf-flow { fill: none; stroke: color-mix(in oklab, var(--accent) 32%, transparent); stroke-width: 1.2; stroke-linecap: round; }
.tf-pulse { fill: var(--accent); }
.tf-block { fill: color-mix(in oklab, var(--accent) 30%, transparent); }
.tf-inflated { fill: color-mix(in oklab, var(--faint) 34%, transparent); }

/*
 * The timeline. Ten seconds, and the same shape for both machines: the hardware arrives, does
 * its pass, the reading appears, and then the last twentieth is the pause before it starts
 * again — a machine doing one pass, not a loop of decoration.
 *
 * Every element is named on its own keyframes rather than sharing one, because the whole
 * point of a figure is that one thing follows another: 8% of ten seconds is what a person
 * reads as "and then".
 */
.tf-case, .tf-label, .tf-brand, .tf-notch, .tf-board, .tf-pin, .tf-stream, .tf-magic, .tf-magic-text { opacity: 0; animation: tf-hardware 10s linear infinite; }
.tf-head { opacity: 0; animation: tf-head 10s linear infinite; }
.tf-head-glow { opacity: 0; animation: tf-head-glow 10s linear infinite; }
.tf-panel { opacity: 0; animation: tf-panel 10s linear infinite; }
.tf-head-bar, .tf-title { opacity: 0; animation: tf-heading 10s linear infinite; }

/* The cartridge's machine: a pass down it, and the roster filling as the head goes. */
.tf-portrait-0, .tf-slot-0, .tf-slot-name-0 { opacity: 0; animation: tf-read-0 10s linear infinite; }
.tf-portrait-1, .tf-slot-1, .tf-slot-name-1 { opacity: 0; animation: tf-read-1 10s linear infinite; }
.tf-portrait-2, .tf-slot-2, .tf-slot-name-2 { opacity: 0; animation: tf-read-2 10s linear infinite; }
.tf-portrait-3, .tf-slot-3, .tf-slot-name-3 { opacity: 0; animation: tf-read-3 10s linear infinite; }
.tf-portrait-4, .tf-slot-4, .tf-slot-name-4 { opacity: 0; animation: tf-read-4 10s linear infinite; }
.tf-portrait-5, .tf-slot-5, .tf-slot-name-5 { opacity: 0; animation: tf-read-5 10s linear infinite; }

/* The region's machine: a stream coming apart into files, names appearing one by one. */
.tf-byte, .tf-speck { opacity: 0; animation: tf-stream-in 10s linear infinite; }
.tf-flow-0 { opacity: 0; animation: tf-flow-line-0 10s linear infinite; }
.tf-flow-1 { opacity: 0; animation: tf-flow-line-1 10s linear infinite; }
.tf-flow-2 { opacity: 0; animation: tf-flow-line-2 10s linear infinite; }
.tf-pulse { opacity: 0; animation: tf-pulse 10s linear infinite; }
.tf-block-0, .tf-inflated-0, .tf-slot-name-0 { opacity: 0; animation: tf-file-0 10s linear infinite; }
.tf-block-1, .tf-inflated-1, .tf-slot-name-1 { opacity: 0; animation: tf-file-1 10s linear infinite; }
.tf-block-2, .tf-inflated-2, .tf-slot-name-2 { opacity: 0; animation: tf-file-2 10s linear infinite; }
.tf-block-3, .tf-inflated-3, .tf-slot-name-3 { opacity: 0; animation: tf-file-3 10s linear infinite; }
.tf-block-4, .tf-inflated-4, .tf-slot-name-4 { opacity: 0; animation: tf-file-4 10s linear infinite; }

@keyframes tf-hardware {
  0% { opacity: 0; }
  4%, 96% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes tf-head {
  0%, 4% { opacity: 0; transform: translateY(0); }
  8% { opacity: 1; }
  70% { opacity: 1; transform: translateY(138px); }
  76%, 100% { opacity: 0; transform: translateY(138px); }
}

@keyframes tf-head-glow {
  0%, 5% { opacity: 0; transform: translateY(0); }
  10% { opacity: 1; }
  68% { opacity: 0.8; transform: translateY(130px); }
  76%, 100% { opacity: 0; transform: translateY(130px); }
}

@keyframes tf-panel {
  0%, 6% { opacity: 0; }
  12%, 96% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes tf-heading {
  0%, 12% { opacity: 0; transform: translateY(-4px); }
  20%, 96% { opacity: 1; transform: none; }
  100% { opacity: 0; transform: none; }
}

/* One wrestler at a time: the picture and the name land together, and each one arrives after
   the last, which is the order a reader fills a roster in. */
@keyframes tf-read-0 { 0%, 22% { opacity: 0; transform: translateX(-6px); } 30%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-read-1 { 0%, 30% { opacity: 0; transform: translateX(-6px); } 38%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-read-2 { 0%, 38% { opacity: 0; transform: translateX(-6px); } 46%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-read-3 { 0%, 46% { opacity: 0; transform: translateX(-6px); } 54%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-read-4 { 0%, 54% { opacity: 0; transform: translateX(-6px); } 62%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-read-5 { 0%, 62% { opacity: 0; transform: translateX(-6px); } 70%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }

/* A compressed region is bytes in no order a reader can see, so the whole stream arrives at
   once rather than one block at a time. */
@keyframes tf-stream-in {
  0% { opacity: 0; }
  6%, 96% { opacity: 1; }
  100% { opacity: 0; }
}

/* A channel is drawn as the stream it carries is read, and the pulse travels the first one
   from the region to the table: a byte moving, which is what inflating is. */
@keyframes tf-flow-line-0 { 0%, 14% { opacity: 0; } 20%, 96% { opacity: 1; } 100% { opacity: 0; } }
@keyframes tf-flow-line-1 { 0%, 30% { opacity: 0; } 36%, 96% { opacity: 1; } 100% { opacity: 0; } }
@keyframes tf-flow-line-2 { 0%, 46% { opacity: 0; } 52%, 96% { opacity: 1; } 100% { opacity: 0; } }

@keyframes tf-pulse {
  0%, 16% { opacity: 0; transform: translateX(0); }
  20% { opacity: 1; }
  30% { opacity: 1; transform: translateX(34px); }
  34%, 100% { opacity: 0; transform: translateX(34px); }
}

/* One file at a time: its block, its name and its extent, in that order. */
@keyframes tf-file-0 { 0%, 16% { opacity: 0; transform: translateX(-5px); } 24%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-file-1 { 0%, 30% { opacity: 0; transform: translateX(-5px); } 38%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-file-2 { 0%, 44% { opacity: 0; transform: translateX(-5px); } 52%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-file-3 { 0%, 58% { opacity: 0; transform: translateX(-5px); } 66%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes tf-file-4 { 0%, 72% { opacity: 0; transform: translateX(-5px); } 80%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .tool-figure * { animation: none !important; opacity: 1 !important; stroke-dashoffset: 0 !important; transform: none !important; }
}

.tools-note {
  margin-block-start: 1rem;
  color: var(--dim);
  font-size: 0.94rem;
  line-height: 1.7;
  max-inline-size: 62ch;
}

.blog-list { display: grid; }

/*
 * The timeline: a date in the gutter, a dot on a line, the card beside it.
 *
 * The rows are separated by their own vertical padding rather than by a gap, which is
 * the live site's shape and the reason the first date starts below the section label
 * and not level with it. The line is drawn by each row from its dot down to the next
 * row's dot, and everything is placed from the same two lengths, so the rail and the
 * line cannot drift apart.
 */
.timeline {
  --timeline-pad: 2.25rem;
  --timeline-date: 4rem;
  --timeline-rail-gap: 2.75rem;
  --timeline-bullet: 0.75rem;
  display: grid;
}

.timeline-item {
  position: relative;
  display: grid;
  grid-template-columns: var(--timeline-date) minmax(0, 1fr);
  column-gap: var(--timeline-rail-gap);
  align-items: start;
  padding-block: var(--timeline-pad);
}

/*
 * The rail runs between the bullets, not through them: it starts at the bottom edge of
 * one and stops at the top edge of the next. An earlier version ran from centre to
 * centre and left the line drawn across the circle, because a row's own `::after` paints
 * after the dot that sits in it — so the dot could not cover the line however opaque it
 * was. Stopping at the edges means the two never meet, which is also what it looks like.
 */
.timeline-item::after {
  content: "";
  position: absolute;
  inset-inline-start: calc(var(--timeline-date) + 0.5625rem + var(--timeline-bullet) / 2);
  inset-block-start: calc(var(--timeline-pad) + 0.5rem + var(--timeline-bullet));
  inset-block-end: calc(-1 * (var(--timeline-pad) + 0.5rem));
  border-inline-start: 1px solid var(--border);
}

.timeline-item:last-child::after { display: none; }

.timeline-date {
  grid-column: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
  padding-block-start: 0.2rem;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
}

.timeline-date-day { font-size: 0.78rem; color: var(--text); text-transform: lowercase; }
.timeline-date-year { font-size: 0.72rem; color: var(--faint); }

.timeline-dot {
  position: absolute;
  inset-inline-start: calc(var(--timeline-date) + 0.5625rem);
  inset-block-start: calc(var(--timeline-pad) + 0.5rem);
  z-index: 1;
  inline-size: var(--timeline-bullet);
  block-size: var(--timeline-bullet);
  border-radius: 50%;
  border: 2px solid var(--accent);
  background: var(--bg);
}

.timeline .blog-card { grid-column: 2; }

@media (max-width: 640px) {
  .timeline { --timeline-pad: 1.5rem; --timeline-date: 3rem; --timeline-dot: 1rem; }
  .timeline-date-day { font-size: 0.72rem; }
  .timeline-dot { margin-block-start: 0.35rem; }
}

/*
 * A card, with the polish that makes a list feel worked rather than assembled: a soft
 * radius, a surface that is a wash rather than a slab, a hairline that lifts a shade on
 * hover, and a shadow that arrives with the lift instead of being there all along. The
 * transition names the properties it moves, so nothing else about the card animates by
 * accident.
 */
.blog-card {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem 1.05rem;
  background:
    linear-gradient(180deg, color-mix(in oklab, var(--surface) 62%, transparent), color-mix(in oklab, var(--surface) 38%, transparent));
  transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
}

.blog-card:hover,
.blog-card:focus-within {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  transform: translateY(-2px);
  box-shadow: 0 14px 30px -18px rgb(0 0 0 / 0.75);
}

.blog-card-text { display: flex; flex-direction: column; gap: 0.5rem; }
.blog-card:hover { border-color: var(--border-strong); }
.blog-card h3 { font-size: 1.45rem; font-weight: 600; line-height: 1.33; }
.blog-card h3 a { color: var(--text); }
.blog-card h3 a:hover { color: var(--accent); text-decoration: none; }
.blog-card-desc { color: var(--dim); font-size: 0.9375rem; line-height: 1.55; }

.blog-card-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.blog-card-tags span {
  font-size: 0.76rem;
  letter-spacing: 0.02em;
  padding: 0.22rem 0.62rem;
  border-radius: 999px;
  border: 1px solid color-mix(in oklab, var(--border) 80%, transparent);
  background: color-mix(in oklab, var(--elevated) 55%, transparent);
  color: var(--dim);
}

.blog-card-tags .blog-card-badge {
  font-weight: 600;
  color: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 40%, transparent);
  background: var(--accent-dim);
}

.blog-card-meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  color: var(--faint);
}

.blog-card-meta .sep { opacity: 0.6; }
.blog-card-more {
  margin-inline-start: auto;
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.blog-card:hover .blog-card-more svg { transform: translateX(2px); }
.blog-card-more svg { transition: transform 160ms ease; }

@media (prefers-reduced-motion: reduce) {
  .blog-card-more svg { transition: none; }
  .blog-card:hover .blog-card-more svg { transform: none; }
}

/* ─── a page of prose ────────────────────────────────────────── */

/*
 * A page is not a post. A post's sections are arguments, so they carry headings at
 * reading size; a page's sections are places, so they carry the small upper-case label
 * the live site uses for them — the same label the tools and blog lists use on the home
 * page. Until this split, a page's `h2` was a post's `h2` and every section shouted.
 */
.page-body { display: grid; gap: 2.5rem; line-height: 1.5; }

.page-body h2 {
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin-block-end: 1rem;
}

.page-body h3 { font-size: 1.125rem; font-weight: 600; color: var(--text); margin-block-start: 1.5rem; }
.page-body section { display: grid; gap: 1rem; align-content: start; }
.page-body p { color: var(--text); }

/* ─── article ────────────────────────────────────────────────── */

/* A section page names itself in the largest type on the site; a post names itself
   in the heaviest. Both measurements come from the previous build, which is where
   the scale came from in the first place. */

.page-head h1 {
  font-size: clamp(2.5rem, 5.5vw, 4.5rem);
  letter-spacing: -0.035em;
  line-height: 1.05;
}

.page-head .lead { margin-block-start: 1.5rem; }

/* The line of keywords under a page's introduction, which is the live site's shape:
   the smallest type on the page, upper case, and read as one breath. */
.page-head .meta {
  margin-block-start: 1.75rem;
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
}

.post-head h1 {
  font-size: clamp(2.1rem, 5vw, 3rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
  margin-block-start: 0.75rem;
}

.post-meta { color: var(--faint); font-size: 0.85rem; display: flex; flex-wrap: wrap; gap: 0.25rem; }
.post-meta .sep { opacity: 0.6; }
.post-head .description { margin-block-start: 0.9rem; color: var(--dim); line-height: 1.65; }
.post-revised { margin-block-start: 0.6rem; color: var(--faint); font-size: 0.82rem; }

.eyebrow {
  font-size: 0.75rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--faint);
  margin-block-end: 0.5rem;
}

.lead { color: var(--dim); font-size: 1.15rem; line-height: 1.55; max-inline-size: 56ch; }

.post-toc {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 1rem 1.2rem;
  font-size: 0.9rem;
}

.post-toc-title {
  font-size: 0.74rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--faint);
  margin-block-end: 0.6rem;
}

.post-toc ol { display: grid; gap: 0.25rem; counter-reset: toc; }
.post-toc li { counter-increment: toc; display: flex; gap: 0.5rem; }
.post-toc li::before { content: counter(toc) "."; color: var(--faint); font-variant-numeric: tabular-nums; }
.post-toc a { color: var(--dim); }
.post-toc a:hover { color: var(--accent); text-decoration: none; }

.post-content { display: grid; gap: 1.1rem; line-height: 1.75; color: var(--dim); }

/*
 * A grid item's min-width defaults to auto, which means it refuses to shrink
 * below its content: one wide <pre> or table then stretches the whole track and
 * the page scrolls sideways instead of the block scrolling inside itself. The
 * page must never be wider than the screen, on any width.
 */


.post-content > * { min-width: 0; }
.code-figure > * { min-width: 0; }
.table-scroll { min-width: 0; }

.post-content h2 { font-size: 1.875rem; line-height: 1.2; color: var(--text); margin-block-start: 1.6rem; scroll-margin-top: 5rem; }
.post-content h3 { font-size: 1.375rem; line-height: 1.25; color: var(--text); margin-block-start: 1.2rem; scroll-margin-top: 5rem; }
.post-content a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.post-content strong { color: var(--text); font-weight: 500; }
.post-content ul, .post-content ol { padding-inline-start: 1.4rem; display: grid; gap: 0.4rem; }
.post-content li { min-width: 0; }
.post-content ul { list-style: disc; }
.post-content ol { list-style: decimal; }
.post-content li::marker { color: var(--faint); }
.post-content .lead { color: var(--dim); }

.closing {
  border-block-start: 1px solid var(--border);
  padding-block-start: 1.4rem;
  display: grid;
  gap: 0.9rem;
}

.closing h2 { margin-block-start: 0; }

/* ─── callout ────────────────────────────────────────────────── */

.callout {
  border: 1px solid var(--border);
  border-inline-start-width: 3px;
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
  background: var(--surface);
  font-size: 0.95rem;
  display: grid;
  gap: 0.4rem;
}

.callout[data-kind="info"],
.callout[data-kind="note"] { border-inline-start-color: var(--blue); }
.callout[data-kind="tip"]    { border-inline-start-color: var(--pass); }
.callout[data-kind="warn"],
.callout[data-kind="warning"] { border-inline-start-color: var(--amber); }
.callout[data-kind="error"]  { border-inline-start-color: var(--fail); }
.callout[data-kind="ok"]     { border-inline-start-color: var(--pass); }

.callout-title {
  color: var(--text);
  font-weight: 500;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.post-tldr {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 1rem 1.2rem;
  display: grid;
  gap: 0.5rem;
}

.post-tldr-label {
  font-size: 0.74rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--accent);
}

.post-tldr ul { padding-inline-start: 1.1rem; list-style: disc; display: grid; gap: 0.4rem; font-size: 0.94rem; }
.post-tldr li::marker { color: var(--faint); }

/* ─── tables ─────────────────────────────────────────────────── */

.table-figure { display: grid; gap: 0.7rem; }

/* The wrapper scrolls, so the table keeps its column algorithm and the header
   stays sticky while you scroll a wide comparison to the side. */
.table-scroll {
  overflow-x: auto;
  border-radius: var(--radius-sm);
}

.table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.table-figure table {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}

.table-figure th,
.table-figure td {
  border: 1px solid var(--border);
  padding: 0.5rem 0.75rem;
  text-align: left;
  vertical-align: top;
}

.table-figure th {
  background: var(--surface);
  color: var(--text);
  font-weight: 500;
  white-space: nowrap;
  position: sticky;
  top: 0;
  z-index: 1;
}

.table-figure tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.014); }
.table-figure code { white-space: nowrap; }

.post-content figcaption,
.table-figure figcaption {
  color: var(--faint);
  font-size: 0.85rem;
  line-height: 1.6;
}

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

.code-figure {
  display: grid;
  gap: 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #08080a;
  overflow: hidden;
}

.code-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.75rem;
  border-block-end: 1px solid var(--border);
  background: var(--surface);
  min-width: 0;
}

/* A flex item will not shrink below its content without this, so a long
   language label held the bar open and the bar held the whole page open. */
.code-bar > * { min-width: 0; }

.code-kind {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.code-tabs { display: flex; gap: 0.25rem; }

.code-tabs button {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  padding: 0.25rem 0.6rem;
  border-radius: var(--radius-sm);
  color: var(--faint);
}

.code-tabs button:hover { color: var(--text); }
.code-tabs button[aria-selected="true"] { color: var(--accent); background: var(--accent-dim); }

.code-copy {
  margin-inline-start: auto;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
  padding: 0.25rem 0.55rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}

.code-copy:hover { color: var(--accent); border-color: var(--accent); }

.code-figure pre {
  margin: 0;
  padding: 1rem 1.15rem;
  overflow: auto;
  color: var(--dim);
  line-height: 1.7;
  font-size: 0.82rem;
}

.code-figure figcaption {
  padding: 0 1.15rem 0.9rem;
  color: var(--faint);
  font-size: 0.85rem;
  line-height: 1.6;
}

/* peek popovers */
.peek-pop {
  margin: 0 1.15rem 1rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--elevated);
  padding: 0.7rem 0.9rem;
  font-size: 0.8rem;
}

.peek-addr { font-family: var(--mono); color: var(--accent); margin-block-end: 0.4rem; display: flex; gap: 0.6rem; }
.peek-addr span { color: var(--faint); }
.peek-pop pre { padding: 0; overflow-x: auto; font-size: 0.76rem; }

button.peek {
  font: inherit;
  color: var(--accent);
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}

/* syntax colours (from content.json's code_html) */
.code-figure .keyword { color: var(--purple); }
.code-figure .property { color: var(--cyan); }
.code-figure .number { color: var(--amber); }
.code-figure .string { color: var(--pass); }
.code-figure .comment { color: var(--faint); font-style: italic; }
.code-figure .function { color: var(--blue); }

/* ─── timeline ───────────────────────────────────────────────── */

.timeline-hero { display: grid; gap: 0.6rem; margin: 0; }
.timeline-hero svg { inline-size: 100%; block-size: auto; }
.timeline-axis, .timeline-tick { stroke: var(--border-strong); stroke-width: 1; }
.timeline-year { fill: var(--faint); font: 400 10px var(--mono); }
.timeline-stem { stroke: var(--border-strong); stroke-width: 1; }
.timeline-node { fill: var(--accent); }
.timeline-pill { fill: var(--surface); stroke: var(--border-strong); }
.timeline-label { fill: var(--dim); font: 400 10px var(--sans); }
.timeline-kind-hl .timeline-pill { stroke: var(--accent); fill: var(--accent-dim); }
.timeline-kind-hl .timeline-label { fill: var(--accent); }
.timeline-kind-us .timeline-node { fill: var(--accent); }
.timeline-kind-browser .timeline-pill { stroke-dasharray: 3 3; }

/* ─── select ─────────────────────────────────────────────────── */

.select { position: relative; display: inline-block; }

.select > select {
  appearance: none;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 32px 8px 12px;
  font: inherit;
  cursor: pointer;
}

.select > [data-surface],
.select > [data-menu] { display: none; }

.select[data-upgraded] > select {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.select[data-upgraded] > [data-surface] {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 12px;
  font: inherit;
  cursor: pointer;
  min-inline-size: 140px;
  justify-content: space-between;
}

.select[data-upgraded] > [data-surface]:hover { border-color: var(--border-strong); }

.select[data-open] > [data-menu] {
  display: block;
  position: absolute;
  inset-inline: 0;
  top: calc(100% + 4px);
  background: var(--elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 4px;
  z-index: 60;
  max-block-size: 240px;
  overflow: auto;
}

.select > [data-menu] [role="option"] { padding: 6px 10px; border-radius: var(--radius-sm); cursor: pointer; }
.select > [data-menu] [role="option"]:hover,
.select > [data-menu] [role="option"][aria-selected="true"] { background: var(--surface-2); color: var(--accent); }

[data-chevron] {
  inline-size: 14px;
  block-size: 14px;
  transition: transform 160ms ease;
}

.select[data-open] [data-chevron] { transform: rotate(180deg); }

@media (prefers-reduced-motion: reduce) {
  [data-chevron] { transition: none; }
}

