@font-face {
  font-family: "Space Mono";
  src: url("/fonts/SpaceMono-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Space Mono";
  src: url("/fonts/SpaceMono-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Space Grotesk ships only as a variable font - one file covers the whole
   300-700 weight range (a font-weight range in @font-face, not a fixed
   value, is what tells the browser to read it as variable). No italic
   style exists for it - italic content (blockquotes, captions) falls
   through to the next stack entry with a real italic instead of a
   synthesized oblique, same as font-synthesis: none below. */
@font-face {
  font-family: "Space Grotesk";
  src: url("/fonts/SpaceGrotesk-Variable.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* Inter (rsms/inter, OFL, via @fontsource-variable/inter 5.3.0) sets the
   reading text. Variable on both wght and opsz, so font-optical-sizing
   (on by default) tightens small text and refines display sizes without
   extra files. Split by unicode-range: only the Latin files ever load for
   today's content; latin-ext/cyrillic are fetched only when a page uses
   those characters. Real italic files - no synthesized obliques. */
@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-latin-normal.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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: "Inter";
  src: url("/fonts/Inter-latin-italic.woff2") format("woff2");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
  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: "Inter";
  src: url("/fonts/Inter-latin-ext-normal.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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";
  src: url("/fonts/Inter-latin-ext-italic.woff2") format("woff2");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
  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";
  src: url("/fonts/Inter-cyrillic-normal.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-cyrillic-italic.woff2") format("woff2");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* ==========================================================================
   Design tokens

   Two faces, split by job. Space Grotesk is the display voice: headings,
   the wordmark, nav, Writing-index titles - short, large strings where its
   character is the point. Inter is the reading voice: body copy, lists,
   the lede and deck, quotes, dates, captions, tables - everything read in
   runs, where a text face with a true italic and even color matters more
   than personality. Space Mono is reserved for code (and the generated Open
   Graph card, see og-image.html). No letter-spaced/uppercase "label" metadata:
   secondary text is just smaller and quieter in the same typeface.
   Editorial-modernist restraint (iA, Anthropic's research blog, Increment,
   Stripe Press, Works in Progress) is the design system here, more than
   any one color or shape.

   Five sizes, each with one job, loosely derived from a 1.25 modular scale
   (Bringhurst, "The Elements of Typographic Style"):
     --text-sm    1rem       nav, dates, captions, footnotes, table text
     --text-base  1.125rem   body copy, lists, the lede; h3 at bold weight
     --text-md    1.2rem     the site title/wordmark only
     --text-lg    1.5rem     h2, Writing-index titles, pull-quotes, the deck
     --text-xl    3.25rem    every h1 - fixed, independent of --text-base

   Themes: every color is declared exactly once via light-dark(). The
   default color-scheme follows the OS; the header toggle forces one by
   setting html[data-theme] (persisted in localStorage), which flips
   color-scheme and with it every token. One accent, #d8402a, identical in
   both themes and used sparingly (a heading's trailing dot, a link on
   hover). Three text tiers: --color-heading, --color-text, --color-muted. */
:root {
  color-scheme: light dark;

  --font-display:
    "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-text:
    "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-mono:
    "Space Mono", "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas,
    "Courier New", monospace;

  --text-sm: 1rem;
  --text-base: 1.125rem;
  --text-md: 1.2rem;
  --text-lg: 1.5rem;
  --text-xl: 3.25rem;

  /* Flat geometry: the theme toggle's circle is the only border-radius
     anywhere. */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --transition: 0.15s var(--ease);

  /* utopia.fyi-style T-shirt space scale. Every margin/padding/gap below
     should use one of these; a genuinely sub-3xs hairline nudge is the
     only accepted exception, and should say so in a comment. */
  --space-3xs: 0.25rem;
  --space-2xs: 0.375rem;
  --space-xs: 0.5rem;
  --space-s: 0.75rem;
  --space-m: 1rem; /* "one line" at the root font-size */
  --space-l: 1.5rem;
  --space-xl: 2.25rem;
  --space-2xl: 2.75rem;
  --space-3xl: 4rem;

  /* Baseline rhythm for .prose only: .prose p's line-height (1.64) times
     --text-base, so a paragraph's line box and the grid unit are the same
     number. Structural gaps between prose blocks (paragraphs, lists, h2,
     hr, blockquote, figures) are multiples of this (Müller-Brockmann, Grid
     Systems in Graphic Design); tight same-group gaps (h3 to its
     paragraph, list items, dt/dd) stay on --space-*, since grouping, not
     grid rigor, is the point there. */
  --rhythm: calc(var(--text-base) * 1.64);

  --color-bg: light-dark(#f2efe8, #141310);
  --color-heading: light-dark(#14120d, #f4f1e9);
  /* Body copy - contrast against --color-bg: light 15.9:1, dark 16.4:1. */
  --color-text: light-dark(rgba(20, 18, 13, 0.92), rgba(235, 231, 221, 0.92));
  /* Everything secondary: nav, dates, captions, footnotes, table text
     (light 8.4:1, dark 8.0:1). */
  --color-muted: light-dark(rgba(20, 18, 13, 0.55), rgba(235, 231, 221, 0.52));
  --color-border: light-dark(rgba(20, 18, 13, 0.25), rgba(235, 231, 221, 0.22));
  /* Link underlines and the scrollbar: strong enough that a link is
     findable at a glance without the accent, which stays for hover. */
  --color-underline: light-dark(
    rgba(20, 18, 13, 0.4),
    rgba(235, 231, 221, 0.38)
  );
  --color-panel: light-dark(rgba(20, 18, 13, 0.05), rgba(235, 231, 221, 0.04));
  --color-bg-hover: light-dark(
    rgba(20, 18, 13, 0.08),
    rgba(235, 231, 221, 0.07)
  );
  /* The one color that does NOT flip with light-dark(): a considered ink
     color, not a UI accent, so it stays put across modes (light-mode
     contrast against --color-bg: 5.3:1; dark-mode: 5.9:1). */
  --color-accent: #d8402a;

  /* The reading column: measured at ~67 characters/line of Inter at
     --text-base on real prose (Butterick's ideal is ~66). rem-based, so it grows only with the fluid
     root font-size, not with the viewport - a wide screen doesn't earn a
     longer line. Mobile is bounded by the viewport and --gutter instead
     (~39 CPL). */
  --measure: 40rem;
  /* The wider frame: figures, diagrams, and tables break out to it, and
     the header row matches it, so chrome and the widest content share one
     outer edge with the reading column inset inside. */
  --measure-wide: min(92vw, 62rem);
}

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

:root[data-theme="dark"] {
  color-scheme: dark;
}

/* === Reset / base === */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  /* Fluid, not stepped: flat at 16px up to ~1600px of viewport, then grows
     in a straight line to 18px by ~2200px and holds there. em, not px, so
     a reader's browser-level text-size preference still scales the whole
     clamp; solved from font-size = 0.6667em + 0.3333vw, a line that
     crosses 1em (16px) at exactly 1600px of viewport and 1.125em (18px)
     at exactly 2200px. */
  font-size: clamp(1em, 0.6667em + 0.3333vw, 1.125em);
  scroll-behavior: smooth;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scrollbar-color: var(--color-underline) transparent;
}

.highlight pre,
.table-wrapper {
  scrollbar-width: thin;
}

body {
  margin: 0;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

.site-body {
  background-color: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-text);
  font-size: var(--text-base);
  line-height: 1.7;
  font-synthesis: none;
  /* Inter's own kerning and contextual alternates (e.g. hyphens and
     arrows centred on capitals) - on by default in most engines, stated
     so it doesn't depend on that. */
  font-kerning: normal;
  font-feature-settings: "calt";
}

/* The display voice - see the token comment. Everything else inherits
   --font-text from .site-body. */
.site-title,
.nav-link,
.page-heading,
.main-content h1,
.prose h2,
.prose h3,
.blog-timeline-date,
.blog-timeline-title,
.post-nav-link,
.social-link,
.error-button {
  font-family: var(--font-display);
}

/* Proportional figures read naturally inside running text; tabular ones
   only where numbers are meant to line up in a column. The Writing-index
   dates stay in Space Grotesk, whose tabular hyphens sit tighter than
   Inter's in an ISO date. */
.blog-timeline-date,
.article-meta,
.prose table {
  font-variant-numeric: tabular-nums;
}

::selection {
  /* The ink color as a highlight, text flipped to the page color - the
     one place --color-accent supplies a background instead of foreground. */
  background-color: var(--color-accent);
  color: var(--color-bg);
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-accent) 18%, transparent);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: var(--space-xs) var(--space-m);
  background-color: var(--color-heading);
  color: var(--color-bg);
  text-decoration: none;
}

.skip-link:focus {
  left: 0;
}

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

/* === Layout === */
.site-container {
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--space-l);
  padding-block-end: var(--space-3xl);
}

/* Solid, not glass: no backdrop-filter blur - nothing else in the theme
   claims depth, so an opaque --color-bg plus a hairline border does. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background-color: var(--color-bg);
  border-block-end: 1px solid var(--color-border);
}

/* --measure-wide, not --measure: see the token's comment. */
.header-row {
  max-width: var(--measure-wide);
  margin-inline: auto;
  padding-inline: var(--space-l);
  padding-block: var(--space-m);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-l);
}

.site-title {
  display: flex;
  align-items: center;
  text-decoration: none;
  color: var(--color-heading);
  font-weight: 700;
  font-size: var(--text-md);
  letter-spacing: -0.012em;
  transition: color var(--transition);
}

.site-title:hover {
  color: var(--color-accent);
}

.site-title-text {
  display: inline-flex;
  overflow: hidden;
  white-space: nowrap;
  line-height: 1.2;
}

/* Full name on desktop, the short mark on mobile (see the max-width: 640px
   block). The hidden one collapses to zero width rather than display: none,
   so the full name stays the link's accessible name either way. */
.site-title-full,
.site-title-short {
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
}

.site-title-full {
  max-width: 16em;
}

.site-title-short {
  max-width: 0;
}

.site-nav {
  display: flex;
  align-items: center;
  min-width: 0;
  gap: var(--space-l);
}

.nav-list {
  display: flex;
  align-items: center;
  min-width: 0;
  gap: var(--space-l);
  list-style: none;
  margin: 0;
  padding: 0;
  white-space: nowrap;
}

/* One size step below the wordmark. The current page reads from color and
   weight alone - no painted dot or underline. */
.nav-link {
  display: flex;
  align-items: center;
  color: var(--color-muted);
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: 500;
  padding: var(--space-2xs) 0;
  transition: color var(--transition);
}

.nav-link:hover {
  color: var(--color-accent);
}

.nav-link[aria-current] {
  color: var(--color-heading);
  font-weight: 700;
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  /* .site-nav's gap already spaces this from .nav-list like any two nav
     items; cancel the button's own padding so that hit-area padding
     doesn't add on top and widen this particular gap. */
  margin-inline-start: calc(-1 * var(--space-2xs));
  padding: var(--space-2xs);
  background: none;
  color: var(--color-muted);
  cursor: pointer;
  line-height: 0;
  transition: color var(--transition);
}

.theme-toggle:hover {
  color: var(--color-accent);
}

/* An author-origin `display` beats the UA's `[hidden] { display: none }`
   regardless of specificity, so restore it: the button ships hidden and only
   the theme script unhides it, keeping it away from no-JS readers. */
.theme-toggle[hidden] {
  display: none;
}

.theme-toggle-mark {
  position: relative;
  display: block;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  overflow: hidden;
}

.theme-toggle-mark::before {
  content: "";
  position: absolute;
  inset: 0 50% 0 0;
  background-color: currentColor;
}

/* === Page chrome: eyebrow / heading (About, Now, Writing) === */
.page-header {
  margin-block: var(--space-xl) var(--space-m);
}

.page-eyebrow {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-muted);
}

/* Medium weight and tight tracking; the trailing accent-dot is the only
   ornament. */
.page-heading,
.main-content h1 {
  margin: var(--space-m) 0 0;
  font-weight: 500;
  font-size: var(--text-xl);
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--color-heading);
  text-wrap: balance;
}

.accent-dot {
  color: var(--color-accent);
}

.lede {
  margin: var(--space-l) 0 0;
  line-height: 1.6;
}

.page-footnote {
  margin-block-start: var(--space-xl);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* Underlined-arrow social links under the About prose - the only place
   they appear; there is no sitewide footer. */
.link-row {
  margin-block-start: var(--space-2xl);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-l);
  font-weight: 500;
}

.social-link {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  color: var(--color-text);
  border-bottom: 2px solid var(--color-border);
  text-decoration: none;
  transition:
    color var(--transition),
    border-color var(--transition);
}

/* icon.html draws in currentColor, so the hover rule below recolors it. */
.social-link-arrow {
  flex: none;
  margin-inline-start: var(--space-2xs);
  opacity: 0.55;
}

.social-link:hover {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

/* === Article meta / post title === */
/* The bottom margin collapses against the first prose block's --rhythm
   top margin; --space-2xl wins that outright, leaving a deliberate pause
   before the body copy starts. */
.article-header {
  margin-block: var(--space-3xl) var(--space-2xl);
  text-align: center;
}

/* Same --text-xl as every other h1, just width-capped and centered. */
.main-content .article-header h1 {
  max-width: 22em;
  margin-inline: auto;
}

.article-meta {
  margin-block-start: var(--space-l);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-xs);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* Optional 1-2 sentence deck (front matter `deck`) - a step up from body
   size, quieter than the heading, width-capped like the h1 above it so a
   long deck still reads as a few short balanced lines rather than a
   full-width paragraph. */
.article-deck {
  max-width: 34em;
  margin: var(--space-l) auto 0;
  font-size: var(--text-lg);
  line-height: 1.45;
  text-wrap: balance;
}

/* A single quiet link back to the index after the prose - no tags, no
   "you may also like". */
.post-nav {
  margin-block-start: var(--space-3xl);
  padding-block-start: var(--space-l);
  border-block-start: 1px solid var(--color-border);
}

.post-nav-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--color-muted);
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: 500;
  transition: color var(--transition);
}

.post-nav-link:hover {
  color: var(--color-accent);
}

/* === Prose typography === */
.main-content {
  margin-block-start: var(--space-xs);
}

/* Quiet by default - the same color as the surrounding sentence; the
   accent only shows up once a link is actually being interacted with. */
.prose a {
  color: var(--color-text);
  text-decoration: underline;
  text-decoration-color: var(--color-underline);
  text-decoration-thickness: max(1px, 0.06em);
  text-underline-offset: 0.2em;
  text-decoration-skip-ink: auto;
  transition:
    color var(--transition),
    text-decoration-color var(--transition);
}

.prose a:hover {
  color: var(--color-accent);
  text-decoration-color: var(--color-accent);
}

.prose h2 {
  margin: calc(var(--rhythm) * 2) 0 var(--rhythm);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--color-heading);
  text-wrap: balance;
}

/* About/Now: the full 2x --rhythm gap between the lede and the first
   section heading makes a short page read as isolated blocks, so pull
   just that first h2 in. */
.page-header + .prose > h2:first-child {
  margin-block-start: var(--space-2xl); /* 44px */
}

/* Body size at bold weight rather than a size step of its own, bound
   tight to the paragraph under it (off --rhythm - it's a label, not a
   section break). */
.prose h3 {
  margin: var(--space-l) 0 var(--space-xs);
  font-weight: 700;
  font-size: var(--text-base);
  line-height: 1.4;
  color: var(--color-heading);
  text-wrap: balance;
}

/* Margins collapse to their max: without this the next block's --rhythm
   top margin would win over h3's tight bottom margin. */
.prose h3 + p,
.prose h3 + ul,
.prose h3 + ol {
  margin-block-start: var(--space-xs);
}

.prose p {
  /* One blank line between paragraphs. */
  margin-block: var(--rhythm);
  /* font-size/color look inherited, but aren't redundant: they also reach
     Mermaid's label <p>s, whose sizing the foreignObject rule below is
     compensating for. */
  font-size: var(--text-base);
  line-height: 1.64;
  color: var(--color-text);
  /* Bringhurst §2.1.5: a flush opening quote reads as a dent in the
     column. Safari-only today; a no-op elsewhere. */
  hanging-punctuation: first last;
}

/* pretty avoids orphans on the last line; hyphens evens the rag above it,
   which matters at this measure and more so on mobile. Needs html[lang]
   (set in baseof.html) for the hyphenation dictionary. */
.prose p,
.prose li {
  text-wrap: pretty;
  hyphens: auto;
}

/* Plain disc/decimal markers, muted. disc is set explicitly so nested
   lists don't switch to circles. */
.prose ol,
.prose ul {
  margin-block: var(--rhythm);
  padding-inline-start: 1.25em;
}

.prose ul {
  list-style: disc;
}

.prose li::marker {
  color: var(--color-muted);
}

/* Off --rhythm, like h3: list items group, they don't each open a break. */
.prose li {
  margin-block: 0.4em;
  line-height: 1.68;
}

/* A quiet pull-quote: left-aligned with the rest of the prose, a thin
   accent rule, one size step up from body copy. */
.prose blockquote {
  margin: var(--rhythm) 0;
  padding-inline-start: var(--space-l);
  border-inline-start: 2px solid var(--color-accent);
  font-size: var(--text-lg);
  line-height: 1.5;
  color: var(--color-heading);
  hanging-punctuation: first last;
}

.prose blockquote p {
  margin: 0;
  font-size: inherit;
  line-height: inherit;
  color: inherit;
}

.prose hr {
  border: 0;
  border-top: 1px solid var(--color-border);
  margin-block: var(--rhythm);
}

/* Definition lists aren't reachable from standard Markdown, but raw HTML
   passes through Goldmark unchanged, so a hand-written <dl> still gets a
   considered treatment instead of the UA default. */
.prose dl {
  margin-block: var(--rhythm);
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
}

.prose dt {
  font-weight: 700;
  color: var(--color-heading);
}

.prose dd {
  margin: var(--space-3xs) 0 0;
  line-height: 1.68;
}

/* Inter's 700 is heavy against 400 text; 600 marks emphasis without a
   dark blot in the paragraph's color. em needs nothing: Inter ships a
   real italic. */
.prose :is(strong, b) {
  font-weight: 600;
  color: var(--color-heading);
}

/* Footnotes (Goldmark's [^1]): a small superscript reference that doesn't
   open up the line spacing, and notes set a step down and quieter. */
.prose sup,
.prose sub {
  font-size: 0.75em;
  line-height: 0;
}

.prose .footnote-ref {
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  padding-inline: 0.1em;
}

.prose .footnotes {
  margin-block-start: calc(var(--rhythm) * 2);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.prose .footnotes hr {
  width: var(--space-3xl);
  margin-inline: 0;
}

.prose .footnotes :is(p, li) {
  font-size: inherit;
  color: inherit;
  margin-block: var(--space-2xs);
}

/* === Tables ===
   Same breakout as .figure-block/pre.mermaid: tabular data is exactly the
   kind of content --measure-wide exists for, not the ~700px reading
   column. overflow-x stays as a fallback for a table wider even than that. */
.table-wrapper {
  width: var(--measure-wide);
  margin-block: var(--rhythm);
  margin-inline: calc(50% - 50vw + (100vw - var(--measure-wide)) / 2);
  overflow-x: auto;
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
  line-height: 1.5;
}

.prose th {
  padding: 0 var(--space-l) var(--space-xs) 0;
  text-align: left;
  font-weight: 700;
  color: var(--color-muted);
  border-block-end: 1px solid var(--color-border);
}

.prose td {
  padding: var(--space-s) var(--space-l) var(--space-s) 0;
  border-block-end: 1px solid var(--color-border);
}

.prose tbody tr:hover td {
  background-color: var(--color-bg-hover);
}

/* === Heading anchors ===
   No visible "link to this section" icon - just scroll-margin so a
   fragment link (from elsewhere) doesn't land flush under the sticky
   header, and a brief highlight so the eye finds it. */
.main-content :is(h2, h3, h4) {
  scroll-margin-block-start: var(--space-xl);
}

.main-content :target {
  animation: target-highlight 2s ease-out;
}

@keyframes target-highlight {
  0%,
  30% {
    background-color: color-mix(in srgb, var(--color-accent) 22%, transparent);
  }
  100% {
    background-color: transparent;
  }
}

/* Club crests, via the {{< crests >}} shortcode. */
.badges {
  display: flex;
  align-items: center;
  gap: var(--space-l);
  margin-block: var(--space-l);
}

.badge {
  display: inline-block;
  line-height: 0;
  filter: saturate(0.7);
}

.badge img {
  width: 5rem;
  height: 5rem;
  object-fit: contain;
}

/* === Blog timeline === */
.blog-timeline {
  list-style: none;
  margin-block: var(--space-2xl) var(--space-s);
  padding: 0;
}

/* A chronology, not post cards: date then title on one baseline (stacked
   on mobile), no separators - whitespace groups the rows. The fixed-width
   ISO date plus sitewide tabular-nums lines the dates up into a column. */
.blog-timeline-entry {
  display: flex;
  align-items: baseline;
  gap: var(--space-m);
  padding-inline: var(--space-xs);
  padding-block: var(--space-m);
  margin-inline: calc(var(--space-xs) * -1);
  text-decoration: none;
  color: inherit;
}

.blog-timeline-date {
  flex: none;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.blog-timeline-title {
  min-width: 0;
  color: var(--color-heading);
  font-size: var(--text-lg);
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.008em;
  transition: color var(--transition);
}

.blog-timeline-entry:hover .blog-timeline-title {
  color: var(--color-accent);
}

/* === Back to top === */
.to-top {
  position: fixed;
  inset-block-end: var(--space-l);
  inset-inline-end: var(--space-l);
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--space-xl);
  height: var(--space-xl);
  border: 1px solid var(--color-underline);
  background-color: var(--color-bg);
  color: var(--color-muted);
  text-decoration: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.35rem);
  transition:
    opacity var(--transition),
    visibility var(--transition),
    transform var(--transition),
    color var(--transition),
    background-color var(--transition),
    border-color var(--transition);
}

.to-top[data-visible] {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.to-top:hover {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.to-top[hidden] {
  display: none;
}

/* === Figures ===
   Every figure breaks out of the prose measure to --measure-wide,
   re-centered on the viewport. Images keep their natural size inside it
   (small screenshots don't get blown up); embeds fill it. */
.figure-block {
  width: var(--measure-wide);
  margin-block: var(--rhythm);
  margin-inline: calc(50% - 50vw + (100vw - var(--measure-wide)) / 2);
  text-align: center;
}

.figure-block img {
  margin-inline: auto;
  border: 1px solid var(--color-border);
}

.figure-caption {
  margin-block-start: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.embed-frame {
  width: 100%;
  border: 1px solid var(--color-border);
  background-color: var(--color-bg);
}

/* === Code === */
.highlight {
  margin-block: var(--rhythm);
  background-color: var(--color-panel);
}

.highlight pre {
  margin: 0;
  padding: var(--space-m);
  font-family: var(--font-mono);
  /* Relative, matching inline code below. */
  font-size: 0.86em;
  line-height: 1.7;
  /* Go and Makefiles indent with literal tabs; the UA default of 8
     columns pushes real code off the right edge of the column. */
  tab-size: 4;
  overflow-x: auto;
}

code:not(pre code) {
  background-color: var(--color-panel);
  color: var(--color-heading);
  padding: 1px 5px;
  font-size: 0.86em;
  font-weight: 500;
  font-family: var(--font-mono);
}

/* Keep long KaTeX display equations scrollable instead of overflowing. */
.katex-display {
  overflow-x: auto;
  overflow-y: hidden;
  padding-block: var(--space-3xs);
}

/* Mermaid diagrams: the escaped source shows as quiet monospace text until
   (or unless) the script draws the SVG. Same breakout as .figure-block. */
pre.mermaid {
  display: flex;
  justify-content: center;
  width: var(--measure-wide);
  margin-block: var(--rhythm);
  margin-inline: calc(50% - 50vw + (100vw - var(--measure-wide)) / 2);
  padding: var(--space-xl) var(--space-l);
  background-color: var(--color-panel);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--color-muted);
}

/* Mermaid sometimes sizes an edge label's foreignObject a character or two
   too narrow for this font; let the label overflow rather than clip. */
pre.mermaid svg foreignObject {
  overflow: visible;
}

/* === 404 === */
.error-page {
  text-align: center;
  padding-block: var(--space-3xl);
}

.error-page .error-title {
  font-weight: 700;
  margin-block-end: var(--space-m);
}

.error-button {
  display: inline-block;
  padding: var(--space-s) var(--space-l);
  background-color: var(--color-heading);
  color: var(--color-bg);
  text-decoration: none;
  font-weight: 500;
  font-size: var(--text-sm);
  margin-block-start: var(--space-m);
}

.error-button:hover {
  background-color: var(--color-accent);
}

/* === Responsive === */
@media (max-width: 640px) {
  /* Mobile type scale: the display sizes step down; --text-sm and
     --text-base stay. --rhythm is pinned flat, deliberately tighter than
     the line-height formula it uses on desktop. */
  :root {
    /* 17px: 18px would drop a phone column to ~36 characters/line. */
    --text-base: 1.0625rem;
    --text-md: 1.125rem;
    --text-lg: 1.375rem;
    --text-xl: 2.25rem;
    --rhythm: 1.5rem;

    /* Page gutter, shared by .header-row, .site-container and .highlight
       so chrome and content stay flush at one edge. */
    --gutter: calc(var(--space-m) + var(--space-3xs)); /* 20px */
  }

  /* The short mark keeps the sticky header on one line at this width. */
  .site-title-full {
    max-width: 0;
  }

  .site-title-short {
    max-width: 3em;
  }

  .header-row {
    padding-inline: var(--gutter);
    padding-block: var(--space-xs);
    gap: var(--space-xs) var(--space-m);
  }

  .site-nav {
    gap: var(--space-m);
  }

  .nav-list {
    gap: var(--space-m);
  }

  /* 44px touch targets via an invisible overlay rather than min-height,
     which would grow the header row back. */
  .site-title,
  .nav-link,
  .theme-toggle {
    position: relative;
  }

  .site-title::after,
  .nav-link::after,
  .theme-toggle::after {
    content: "";
    position: absolute;
    inset: 50%;
    inline-size: max(100%, 2.75rem);
    block-size: max(100%, 2.75rem);
    transform: translate(-50%, -50%);
  }

  .site-container {
    padding-inline: var(--gutter);
  }

  .highlight {
    margin-inline: calc(var(--gutter) * -1);
  }

  pre.mermaid {
    padding-inline: var(--space-m);
  }

  .page-header {
    margin-block: var(--space-l) var(--space-m);
  }

  .page-header + .prose > h2:first-child {
    margin-block-start: var(--space-xl);
  }

  .article-header {
    margin-block: var(--space-xl) var(--gutter);
  }

  .article-meta {
    margin-block-start: var(--space-s);
  }

  /* Date above title: a long title won't fit beside the date. */
  .blog-timeline-entry {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2xs);
    padding-block: var(--gutter);
  }

  /* Four 80px crests overflow a ~350px column. */
  .badges {
    flex-wrap: wrap;
    gap: var(--space-m);
  }

  .badge img {
    width: 3rem;
    height: 3rem;
  }

  /* The sticky header's title already links back to the top. */
  .to-top {
    display: none;
  }

  /* 2x --rhythm reads as an oversized section gap at this width. */
  .prose h2 {
    margin-block-start: calc(var(--rhythm) * 1.5);
  }
}

/* === Print === */
@media print {
  @page {
    margin: 2.5cm;
  }

  :root,
  :root[data-theme="dark"] {
    color-scheme: light;
  }

  .site-header,
  .to-top {
    display: none;
  }

  .site-container {
    max-width: none;
    padding: 0;
  }

  .prose a {
    text-decoration: none;
  }

  .prose a[href^="http"]:not([href*="kirillbobyrev.com"])::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
    color: var(--color-muted);
    word-break: break-all;
  }

  .highlight pre {
    white-space: pre-wrap;
    word-break: break-word;
  }
}

/* === Reduced motion === */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

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

/* === Syntax highlighting (Chroma classes) ===
   Merged from `hugo gen chromastyles --style=github` (light) and
   `--style=github-dark` (dark): every token declares both colours via
   light-dark(). Backgrounds are stripped in favour of --color-panel,
   token classes that only one palette defines fall back to the other
   theme's base text colour, and dark-only bold/italic accents are
   dropped so both themes share one typography. Regenerate with those
   commands on bumps. */
.chroma {
  color: light-dark(#212529, #e6edf3);
  -webkit-text-size-adjust: none;
}
.chroma .lnlinks {
  outline: none;
  text-decoration: none;
  color: inherit;
}
.chroma .lntd {
  vertical-align: top;
  padding: 0;
  margin: 0;
  border: 0;
}
.chroma .lntable {
  border-spacing: 0;
  padding: 0;
  margin: 0;
  border: 0;
}
.chroma .lnt {
  white-space: pre;
  -webkit-user-select: none;
  user-select: none;
  margin-right: 0.4em;
  padding: 0 0.4em 0 0.4em;
  color: light-dark(#7f7f7f, #737679);
}
.chroma .ln {
  white-space: pre;
  -webkit-user-select: none;
  user-select: none;
  margin-right: 0.4em;
  padding: 0 0.4em 0 0.4em;
  color: light-dark(#7f7f7f, #6e7681);
}
.chroma .line {
  display: flex;
}
.chroma .err {
  color: light-dark(#cf222e, #f85149);
}
.chroma .k,
.chroma .kd,
.chroma .kn,
.chroma .kr,
.chroma .kt {
  color: light-dark(#cf222e, #ff7b72);
}
.chroma .kc,
.chroma .kp {
  color: light-dark(#cf222e, #79c0ff);
}
.chroma .na,
.chroma .nx {
  color: light-dark(#1f2328, #e6edf3);
}
.chroma .nc,
.chroma .ne {
  color: light-dark(#1f2328, #f0883e);
}
.chroma .no {
  color: light-dark(#0550ae, #79c0ff);
}
.chroma .nd {
  color: light-dark(#0550ae, #d2a8ff);
}
.chroma .ni {
  color: light-dark(#6639ba, #ffa657);
}
.chroma .nl {
  color: light-dark(#990000, #79c0ff);
  font-weight: bold;
}
.chroma .nn {
  color: light-dark(#24292e, #ff7b72);
}
.chroma .nt {
  color: light-dark(#0550ae, #7ee787);
}
.chroma .nb {
  color: light-dark(#6639ba, #e6edf3);
}
.chroma .bp {
  color: light-dark(#6a737d, #e6edf3);
}
.chroma .nv,
.chroma .vc,
.chroma .vg,
.chroma .vi,
.chroma .vm {
  color: light-dark(#953800, #79c0ff);
}
.chroma .nf,
.chroma .fm {
  color: light-dark(#6639ba, #d2a8ff);
}
.chroma .py {
  color: light-dark(#1f2328, #79c0ff);
}
.chroma .l {
  color: light-dark(#1f2328, #a5d6ff);
}
.chroma .ld {
  color: light-dark(#1f2328, #79c0ff);
}
.chroma .s,
.chroma .sb,
.chroma .sc,
.chroma .sd,
.chroma .s2,
.chroma .si,
.chroma .sx,
.chroma .s1 {
  color: light-dark(#0a3069, #a5d6ff);
}
.chroma .sa,
.chroma .dl,
.chroma .se,
.chroma .sh,
.chroma .sr {
  color: light-dark(#0a3069, #79c0ff);
}
.chroma .ss {
  color: light-dark(#032f62, #a5d6ff);
}
.chroma .m,
.chroma .mb,
.chroma .mf,
.chroma .mh,
.chroma .mi,
.chroma .il,
.chroma .mo {
  color: light-dark(#0550ae, #a5d6ff);
}
.chroma .o,
.chroma .ow,
.chroma .or {
  color: light-dark(#0550ae, #ff7b72);
}
.chroma .p {
  color: light-dark(#1f2328, #e6edf3);
}
.chroma .c,
.chroma .ch,
.chroma .cm,
.chroma .c1,
.chroma .cs,
.chroma .cp,
.chroma .cpf {
  color: light-dark(#57606a, #8b949e);
}
.chroma .gd {
  color: light-dark(#82071e, #ffa198);
}
.chroma .ge {
  color: light-dark(#1f2328, #e6edf3);
  font-style: italic;
}
.chroma .gr {
  color: light-dark(#1f2328, #ffa198);
}
.chroma .gh,
.chroma .gu {
  color: light-dark(#1f2328, #79c0ff);
}
.chroma .gi {
  color: light-dark(#116329, #56d364);
}
.chroma .go,
.chroma .gp {
  color: light-dark(#1f2328, #8b949e);
}
.chroma .gs {
  font-weight: bold;
}
.chroma .gt {
  color: light-dark(#1f2328, #ff7b72);
}
.chroma .gl {
  text-decoration: underline;
}
.chroma .w {
  color: light-dark(#ffffff, #6e7681);
}
