@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, tables and long code 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. Two tiers,
     as in Distill or a research paper's figure column: prose holds its
     measure, while the material you look at rather than read grows with
     the screen. Fixed at 62rem (992px) up to ~1440px of viewport, then
     tracks the viewport (64vw) to an 84rem cap (~1500px, reached at
     ~2200px), so a large monitor spends its width on figures instead of
     empty margin. 92vw keeps a gutter on narrow screens. Keep the `sizes`
     attribute in responsive-img.html in step with this. */
  --measure-wide: min(92vw, clamp(62rem, 64vw + 4rem, 84rem));

  /* Clearance below the sticky header (~72px tall here, ~44px on mobile)
     for anything that scrolls or sticks under it: fragment-link targets
     and the table-of-contents rail. */
  --header-offset: calc(var(--space-3xl) + var(--space-l));
}

: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. Flush left on the prose edge like every
   other page heading: one alignment axis from title to last paragraph. */
.article-header {
  margin-block: var(--space-3xl) var(--space-2xl);
}

.article-meta {
  margin-block-start: var(--space-l);
  display: flex;
  align-items: 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, balanced so a long deck still reads as
   a few even lines. */
.article-deck {
  margin: var(--space-l) 0 0;
  font-size: var(--text-lg);
  line-height: 1.45;
  text-wrap: balance;
}

/* === Table of contents ===
   Long posts only (see page.html). Narrow viewports: a collapsed
   <details> between the header and the body. Wide ones (the media query
   below): pinned in the empty margin left of the reading column, open,
   with the section in view marked the same way the nav marks the current
   page - color and weight, no painted indicator. */
.article-body {
  position: relative;
}

.toc {
  margin-block-end: var(--space-xl);
  font-size: var(--text-sm);
  line-height: 1.4;
}

.toc-summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 500;
  color: var(--color-muted);
  list-style: none;
  transition: color var(--transition);
}

.toc-summary::-webkit-details-marker {
  display: none;
}

.toc-summary:hover {
  color: var(--color-accent);
}

/* A drawn chevron, turned down while open; currentColor so hover recolors it. */
.toc-summary::before {
  content: "";
  width: 0.4em;
  height: 0.4em;
  border-inline-end: 1.5px solid currentColor;
  border-block-end: 1.5px solid currentColor;
  rotate: -45deg;
  transition: rotate var(--transition);
}

.toc[open] .toc-summary::before {
  rotate: 45deg;
}

.toc ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.toc > nav > ul {
  margin-block-start: var(--space-s);
}

.toc li {
  margin-block: var(--space-xs);
}

/* h3 entries: indented under their h2, not a separate list style. */
.toc li ul {
  padding-inline-start: var(--space-s);
}

.toc a {
  color: var(--color-muted);
  text-decoration: none;
  transition: color var(--transition);
}

.toc a:hover {
  color: var(--color-accent);
}

.toc a[aria-current] {
  color: var(--color-heading);
  font-weight: 600;
}

/* Wide enough that the rail fits between the reading column and the
   header logo's edge: --measure-wide reaches its 62rem floor at ~68rem of
   viewport; narrower than that, the rail would get squeezed. Past ~1440px
   the frame keeps widening, and the rail with it. */
@media (min-width: 68rem) {
  /* Out of flow, as tall as the article body, so the sticky child inside
     it can travel the whole post. Its left edge lines up with the header
     logo (.header-row's --measure-wide frame, inset by its padding); its
     width is whatever margin is left, minus a gutter. */
  .toc-rail {
    position: absolute;
    inset-block: 0;
    inset-inline-end: calc(100% + var(--space-l));
    width: calc(
      (var(--measure-wide) - 2 * var(--space-l) - 100%) / 2 - var(--space-l)
    );
  }

  .toc {
    position: sticky;
    inset-block-start: var(--header-offset);
    max-height: calc(100vh - var(--header-offset) - var(--space-l));
    overflow-y: auto;
    margin: 0;
    scrollbar-width: none;
  }

  /* Always open here (the script in page.html keeps [open] in sync); the
     summary becomes a plain label. */
  .toc-summary {
    cursor: default;
    pointer-events: none;
  }

  .toc-summary::before {
    display: none;
  }

  /* The rail owns the left margin now, so whatever breaks out of the
     reading column (figures, diagrams, wide tables) bleeds right only,
     out to the same --measure-wide outer edge it reaches on both sides
     elsewhere, and starts flush with the prose instead of centered. */
  .has-toc :is(.figure-block, pre.mermaid) {
    width: calc(50% + var(--measure-wide) / 2);
    margin-inline: 0;
  }

  .has-toc .figure-block {
    text-align: start;
  }

  .has-toc .figure-block img {
    margin-inline: 0;
  }

  .has-toc .prose :is(.table-wrapper, .katex-display) {
    max-width: calc(50% + var(--measure-wide) / 2);
    margin-inline-start: 0;
    translate: none;
  }
}

/* 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);
}

/* Asymmetric on purpose: two lines of air above, well under one below, so
   the heading reads as the start of the section under it rather than
   floating halfway between two (gestalt proximity). The bottom gap must
   stay visibly smaller than the gap between paragraphs, which is --rhythm. */
.prose h2 {
  margin: calc(var(--rhythm) * 2) 0 var(--space-s);
  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 the heading's tight bottom margin. */
.prose h2 + :is(p, ul, ol) {
  margin-block-start: var(--space-s);
}

.prose h3 + :is(p, ul, 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 ===
   Sized to the content, not to a fixed frame: never narrower than the
   reading column (so a small table's rules line up with the prose edges),
   wider only as far as its columns actually need, capped at --measure-wide.
   A three-column table of numbers stretched to the full wide frame puts
   ~300px of empty rule between related values; this keeps them together.
   Centering an element of unknown width on the column: start it at the
   column's midpoint, then pull it back by half its own width. overflow-x
   stays as a fallback for a table wider even than --measure-wide. */
.table-wrapper {
  width: max-content;
  min-width: 100%;
  max-width: var(--measure-wide);
  margin-block: var(--rhythm);
  margin-inline-start: 50%;
  translate: -50% 0;
  overflow-x: auto;
}

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

/* Gutters split evenly between neighbouring cells, so a right-aligned
   number column sits as close to its left neighbour as a left-aligned one
   does; the outer cells stay flush with the column edges. */
.prose :is(th, td) {
  padding-inline: var(--space-s);
  border-block-end: 1px solid var(--color-border);
}

.prose :is(th, td):first-child {
  padding-inline-start: 0;
}

.prose :is(th, td):last-child {
  padding-inline-end: 0;
}

.prose th {
  padding-block: 0 var(--space-xs);
  text-align: left;
  font-weight: 700;
  color: var(--color-muted);
  vertical-align: bottom;
}

.prose td {
  padding-block: var(--space-s);
}

.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(--header-offset);
}

.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;
}

/* Past phone widths, a block whose longest line doesn't fit the reading
   column grows to fit it rather than scrolling: rightward only, so the
   code's left edge stays on the prose line the eye is already following,
   out to the --measure-wide outer edge. Blocks that fit stay column-width.
   (Phones keep the full-bleed negative-gutter treatment instead, see the
   max-width: 640px block.) */
@media (min-width: 641px) {
  .highlight {
    width: max-content;
    min-width: 100%;
    max-width: calc(50% + var(--measure-wide) / 2);
  }
}

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);
}

/* Past phone widths, an equation too long for the column grows around its
   own center (same content-sized centering as .table-wrapper) before it
   falls back to scrolling. .prose-scoped to outrank KaTeX's own stylesheet,
   whose `margin: 1em 0` would otherwise reset the inline-start offset. */
@media (min-width: 641px) {
  .prose .katex-display {
    width: max-content;
    min-width: 100%;
    max-width: var(--measure-wide);
    margin-inline-start: 50%;
    translate: -50% 0;
  }
}

/* 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 */

    --header-offset: var(--space-3xl);
  }

  /* 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,
  .toc-rail {
    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);
}
