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

@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

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

/* No italic face is shipped for JetBrains Mono: code has no reason to go
   italic, so a fourth webfont download would buy nothing here. */

/* 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), so this is
   one download instead of the four-plus static files a fixed-weight
   family like the old Inter/Newsreader pairing needed. No italic style
   exists for it at all - italic content (blockquotes, figure captions)
   falls through to the next stack entry with a real italic instead of a
   synthesized oblique, same as font-synthesis: none already enforces
   everywhere else. */
@font-face {
  font-family: "Space Grotesk";
  src: url("/fonts/SpaceGrotesk-Variable.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

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

   One family, deliberately: Space Grotesk carries the site title, every
   heading, body copy, and nav/UI chrome alike - no serif/sans split, the
   single-voice minimalism is the point. JetBrains Mono is kept only where
   it's earned - code and inline code. There is no --font-serif token any
   more; every former serif use now reads var(--font-sans) directly.
   The --step scale below is a Bringhurst-style modular scale (The Elements
   of Typographic Style): one constant ratio (1.125, a "major second")
   applied repeatedly to a base size, same family as the --space-* scale
   further down - a quiet ratio, chosen so the biggest steps (h1/h2/the
   site title) don't shout:
     --step--2  0.79rem    table headers, overline labels
     --step--1  0.8889rem  meta, dates, labels, code, tables
     --step-0   1rem       body, nav
     --step-1   1.125rem   site title, lede, h3
     --step-2   1.2656rem  h2, homepage title
     --step-3   1.4238rem  post h1
   (px results depend on the root font-size set below.)

   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.
   ========================================================================== */
:root {
  color-scheme: light dark;

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

  --step--2: 0.79rem;
  --step--1: 0.8889rem;
  --step-0: 1rem;
  --step-1: 1.125rem;
  --step-2: 1.2656rem;
  --step-3: 1.4238rem;

  /* Shared shape + motion tokens (kept consistent across the whole UI). A
     standard "ease-out"-leaning curve reads calmer than the browser's
     default `ease` on the quick, small movements everything here uses
     (hover fades, the toggle, the back-to-top button) - it starts a touch
     faster and settles instead of easing in from a dead stop. */
  --radius: 6px; /* cards, code blocks, tables, buttons */
  --radius-sm: 3px; /* inline code, chips, small marks */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --transition: 0.15s var(--ease);

  /* Fluid space scale, T-shirt sized after Utopia's fluid type/space-scale
     convention (utopia.fyi) - a step's relative size is legible at the call
     site without cross-referencing a table. --space-m is exactly --step-0,
     so spacing rides the fluid root the same way type already does. Steps
     get proportionally larger as they grow (not a single constant ratio):
     the eye stops distinguishing small absolute differences at large sizes
     (4px vs 8px is obvious, 64px vs 65px isn't), the tiering rationale
     behind Tailwind/Refactoring UI's default spacing scale. Every
     margin/padding/gap in this file should use one of these steps; 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; /* = --step-0, "one line" */
  --space-l: 1.5rem;
  --space-xl: 2.25rem;
  --space-2xl: 2.75rem;
  --space-3xl: 4rem;

  /* Warm "paper" palette - the site's signature, not a neutral
     gray-on-white. Muted rather than bright/vivid: low saturation in both
     modes. Every warm token in this block shares a hue family around
     40-43° (orange-tan); --color-bg is no exception, which keeps the
     background reading as the warmest surface on the page rather than a
     colder outlier next to the border/underline/bg-code tokens below it.
     Both modes are pushed a bit further toward their extreme than a
     "neutral paper" bg would sit - paler in light mode, darker in dark -
     specifically to buy every foreground token more contrast without
     touching the foreground colors themselves. Light mode's bg has had
     two such nudges now (each "a bit more" than the last, deliberately
     incremental); dark mode instead got its second nudge on the text
     token below, since its bg was already close to true black. Dark
     mode's bg sits at L=0.08 (HSL lightness), darker than VS Code (~10%)
     or GitHub dark (~13%) and still clear of the dim "charcoal" territory
     above L=0.15. */
  --color-bg: light-dark(#f8f5ef, #151413);
  /* Dark mode's value brightened a step past its first pass - contrast
     against --color-bg: light 11.6:1, dark 12.84:1. */
  --color-text: light-dark(#3a3226, #ddd7cb);
  --color-heading: light-dark(#211c14, #f1ece3);
  /* Same hue as --color-bg, lightened/darkened just enough to clear WCAG
     AA's 4.5:1 with real margin (light: 5.16:1; dark: 6.66:1) - the
     weakest-contrast token on the page, since it's the one used most
     often for secondary text (timestamps, footer, ledes). */
  --color-muted: light-dark(#70665a, #a59a8d);
  --color-border: light-dark(#dad4c8, #504b44);
  /* Kept deliberately narrow: only ::selection (needs a light background
     regardless of theme) uses this - the site's one nod to Manchester
     City, not a second accent color competing with --color-accent below. */
  --color-sky: #6cabdd;
  /* A brick/oxblood red rather than Bayern's stadium red: their crest red
     (#dc052d) is calibrated for a kit and signage, too saturated and cool
     to sit on warm paper as body-size link text. Darkened and warmed a few
     degrees toward orange instead - reads as "considered accent," not
     "team color," and the warmth fits the palette's browns (light-mode
     contrast against --color-bg: 7.17:1; dark-mode: 6.96:1). */
  --color-accent: light-dark(#952e23, #e58576);
  --color-selection-text: #08263c;
  --color-bg-code: light-dark(#e9e7e2, #343230);
  --color-bg-hover: light-dark(#eae8e3, #383531);
  --color-underline: light-dark(#c9bfa8, #4a4237);
  --color-land: light-dark(#e6ddc8, #332c22); /* unvisited map land */

  /* rem, not ch: still scales with a reader's browser-level text-size
     preference the same way the font-size clamp above does. Flat at
     45.5rem through 2200px of viewport (where the font-size clamp above
     also caps), then grows independently on wider displays - a column
     stuck at ~910px forever regardless of screen size wastes most of a
     4K/ultrawide monitor. Same linear-interpolation technique as the
     font-size clamp, solved to cross 45.5rem at 2200px and 58rem at 3840px
     (4K), holding flat past that: 2560px (QHD) -> ~965px, 3440px
     (ultrawide) -> ~1099px, 3840px+ -> 1160px (~115 characters/line at the
     largest size - a real widening, short of spanning the whole screen). */
  --measure: clamp(45.5rem, 28.7317rem + 15.2439vw, 58rem);
}

: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 18px (1.125em) up to ~1600px of viewport,
     then grows in a straight line to 20px (1.25em) by ~2200px and holds
     there. em, not px, so a reader's browser-level text-size preference
     (not just pinch/ctrl-zoom) still scales the whole clamp; solved from
     font-size = 0.7917em + 0.3333vw, a line that crosses 1.125em at
     exactly 1600px of viewport and 1.25em at exactly 2200px, fixing the
     clamp's flat/fluid/flat breakpoints without a media query. Because
     everything else is sized in rem, the whole UI (text, nav, footer,
     --measure) scales together as this one value moves. */
  font-size: clamp(1.125em, 0.7917em + 0.3333vw, 1.25em);
  scroll-behavior: smooth;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Scrollbars pick up the theme instead of the UA default. */
  scrollbar-color: var(--color-underline) transparent;
}

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

body {
  margin: 0;
}

/* Cross-fade between pages where the browser supports cross-document
   view transitions; everyone else gets an instant navigation. */
@view-transition {
  navigation: auto;
}

@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

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-sans);
  font-size: var(--step-0);
  line-height: 1.7;
  /* Every weight and style actually used is a real shipped face (see the
     @font-face block above) - if a future edit asks for one that isn't,
     this makes it fall back cleanly instead of drawing a faux-bold/oblique
     the type designer never approved. */
  font-synthesis: none;
}

::selection {
  background-color: var(--color-sky);
  color: var(--color-selection-text);
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: 2px;
  /* A soft halo outside the outline reads as a deliberate focus treatment
     rather than the browser default poking through - most noticeable on
     the pill-shaped nav/toggle hit areas, where a bare outline alone can
     look like an afterthought against their already-rounded corners. */
  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;
  border-radius: 0 0 var(--radius-sm) 0;
}

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

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

.site-header {
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--space-l);
  /* Slightly tighter than the other three sides, on purpose: a hair less
     room is wanted right before the border-bottom below. */
  padding-block-end: var(--space-m);
  border-bottom: 1px solid var(--color-border);
  margin-block-end: var(--space-xl);
}

/* A plain static row: title on one side, nav + toggle on the other, all on
   one line at any width that has room for them. No fixed/pinned masthead -
   the whole header scrolls away with the rest of the page like everything
   else on it. */
.header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-m);
}

.site-title {
  font-family: var(--font-sans);
  /* Deliberately below h2's step-2: the title lives in header chrome, on
     every page, competing with nothing - it should read as present, not
     as another content heading. One step above .nav-link keeps it clearly
     the more important thing in that row without matching in-page text. */
  font-size: var(--step-1);
  font-weight: 700;
  line-height: 1.15;
  /* Space Grotesk's own geometry already reads tight and considered at
     weight 700 - less tracking to fight than a serif needed here before. */
  letter-spacing: -0.012em;
  color: var(--color-heading);
  text-decoration: none;
  transition: color var(--transition);
}

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

.site-nav {
  display: flex;
  align-items: center;
  min-width: 0;
}

.nav-list {
  display: flex;
  align-items: center;
  min-width: 0;
  gap: 0.15rem; /* sub-scale hairline: tighter than --space-3xs allows */
  list-style: none;
  margin: 0;
  /* Cancels the links' own inline padding so their labels, not their tap
     areas, line up with the toggle beside them. */
  padding: 0;
  margin-inline-start: calc(var(--space-xs) * -1);
  white-space: nowrap;
  /* No optical baseline correction needed any more: .site-title and
     .nav-link are now the same font at the same step (step-1), and
     measuring directly confirms their baselines already land within a
     fraction of a px of each other under .header-row's align-items. */
  /* Overrides the inherited --line-height: 1.7 from .site-body (tuned for
     paragraph prose, not a single line of nav text). Left inherited, the
     nav-link line-box was noticeably taller than its glyphs, so its
     optical center sat visibly higher than the row's geometric center -
     .theme-toggle, flex-centered on that same geometric center, read as
     hanging low relative to the text next to it. */
  line-height: 1;
}

.nav-link {
  display: block;
  color: var(--color-muted);
  text-decoration: none;
  padding: 0.3rem var(--space-xs); /* 0.3rem: sub-scale hairline */
  border-radius: var(--radius-sm);
  /* Matches .site-title's step-1: nav shouldn't read as an afterthought
     next to it. Still clearly secondary to content headings (h1 is two
     steps up) via weight and --color-muted, not size alone. */
  font-size: var(--step-1);
  font-weight: 500;
  transition: color var(--transition);
}

/* Color alone, no fill: the current page already has its own indicator (the
   underline below), so a hover-only page also getting a filled pill on top
   of that was two different "state" languages doing the same job in one
   small row. */
.nav-link:hover {
  color: var(--color-heading);
}

/* Color and weight only, no painted underline: a colored accent was the
   only color in an otherwise all-muted nav row, so it drew far more
   attention than "which page is current" needs. Matches how hierarchy is
   signaled everywhere else on the page - typography over paint. */
.nav-link[aria-current] {
  color: var(--color-heading);
  font-weight: 600;
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin-inline-start: var(--space-2xs);
  padding: var(--space-2xs);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-muted);
  cursor: pointer;
  line-height: 0;
  transition:
    color var(--transition),
    background-color var(--transition);
}

.theme-toggle:hover {
  color: var(--color-heading);
  background-color: var(--color-bg-hover);
}

/* 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-icon-sun {
  display: none;
}

:root[data-theme="dark"] .theme-icon-sun {
  display: block;
}

:root[data-theme="dark"] .theme-icon-moon {
  display: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-icon-sun {
    display: block;
  }

  :root:not([data-theme="light"]) .theme-icon-moon {
    display: none;
  }
}

/* === Footer === */
.site-footer {
  max-width: var(--measure);
  margin: var(--space-2xl) auto 0;
  padding: var(--space-l) var(--space-l) var(--space-xl);
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--color-muted);
  border-top: 1px solid var(--color-border);
}

/* One line: "Elsewhere" + icons on the left, RSS/Source on the right. */
.footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-l);
  flex-wrap: wrap;
}

.footer-elsewhere {
  display: flex;
  align-items: center;
  gap: var(--space-s);
}

.footer-label {
  font-weight: 500;
  color: var(--color-muted);
}

.footer-icons {
  display: flex;
  gap: var(--space-m);
  align-items: center;
}

.social-link {
  color: var(--color-muted);
  line-height: 0;
  transition: color var(--transition);
}

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

.footer-nav {
  display: flex;
  gap: var(--space-m);
}

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

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

/* === Article meta === */
.article-header {
  margin-block-end: var(--space-l);
}

/* .main-content .article-header h1, not .article-header h1: bare
   .article-header h1 has the exact same specificity as .main-content h1
   below and would lose the cascade tiebreak to it by source order. The
   extra .main-content ancestor is real (see page.html: .article-header
   always sits inside .main-content), so this raises specificity without
   becoming a coincidence. */
.main-content .article-header h1 {
  margin-block-end: var(--space-2xs);
}

.article-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-xs);
  color: var(--color-muted);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
}

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

.prose a {
  color: var(--color-text);
  text-decoration: underline;
  text-decoration-color: var(--color-underline);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.19em;
  transition:
    color var(--transition),
    text-decoration-color var(--transition);
}

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

.main-content h1 {
  font-family: var(--font-sans);
  font-size: var(--step-3);
  font-weight: 700;
  line-height: 1.2;
  /* Tracking eases back toward zero as the heading steps down (h1 tighter
     than h2, h3 left alone) - the same reasoning as .site-title, scaled to
     how much size there is to tighten. */
  letter-spacing: -0.02em;
  margin: var(--space-xl) 0 var(--space-s);
  color: var(--color-heading);
  text-wrap: balance;
}

/* Only /now currently wraps its h1 in a link (the nownownow.com
   convention) - same quiet treatment as .site-title, just inherited size. */
.main-content h1 a {
  color: inherit;
  text-decoration: none;
  transition: color var(--transition);
}

.main-content h1 a:hover {
  color: var(--color-accent);
}

.main-content h2 {
  font-family: var(--font-sans);
  font-size: var(--step-2);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.012em;
  margin: var(--space-xl) 0 var(--space-s);
  color: var(--color-heading);
  text-wrap: balance;
}

.main-content h3 {
  font-family: var(--font-sans);
  font-size: var(--step-1);
  font-weight: 700;
  line-height: 1.3;
  margin: var(--space-l) 0 var(--space-xs);
  color: var(--color-heading);
  text-wrap: balance;
}

.prose p {
  margin-block: 0.9em;
}

/* Nicer paragraph rag: avoids single-word last lines (orphans). */
.prose p,
.prose li {
  text-wrap: pretty;
}

.prose ul {
  margin-block: 0.9em;
  padding-inline-start: var(--space-l);
}

.prose ol {
  margin-block: 0.9em;
  padding-inline-start: var(--space-l);
}

.prose li {
  margin-block: 0.3em;
}

/* Mathematical bullets: composition, then dot product, then convolution
   the deeper the list nests. Browsers without ::marker keep the default
   discs; both are fine. */
.prose ul > li::marker {
  content: "\2218\a0\a0"; /* ∘ */
  color: var(--color-muted);
}

.prose ul ul > li::marker {
  content: "\b7\a0\a0"; /* · */
}

.prose ul ul ul > li::marker {
  content: "\2217\a0\a0"; /* ∗ */
}

.prose blockquote {
  font-style: italic;
  color: var(--color-muted);
  margin-block: var(--space-l);
  padding-inline: var(--space-l);
  /* Lets a leading/trailing quotation mark (Goldmark renders smart quotes
     by default) sit just outside the text's optical edge instead of
     indenting the whole first line to make room for it. Safari-only today;
     everywhere else the mark simply sits inline, which is still correct. */
  hanging-punctuation: first last;
}

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

/* === Footnotes ===
   Goldmark's default markup: an inline `sup#fnref:N > a.footnote-ref`,
   and a trailing `.footnotes` block (its own <hr>, then an <ol> of
   entries each ending in an a.footnote-backref arrow). */
.footnote-ref {
  color: var(--color-accent);
  text-decoration: none;
  font-size: 0.75em;
  vertical-align: super;
  padding-inline: 0.05em;
}

.prose .footnotes {
  margin-block-start: var(--space-2xl);
  padding-block-start: var(--space-m);
  border-top: 1px solid var(--color-border);
  font-size: var(--step--1);
  color: var(--color-muted);
}

/* Same section-boundary rule as the border-top above; the native one is
   redundant here. */
.footnotes hr {
  display: none;
}

.footnotes ol {
  margin: 0;
  padding-inline-start: var(--space-l);
}

.footnotes li {
  margin-block: 0.5em;
  line-height: 1.5;
}

.footnotes li p {
  margin: 0;
}

.footnote-backref {
  margin-inline-start: 0.35em;
  color: var(--color-muted);
  text-decoration: none;
  transition: color var(--transition);
}

.footnote-backref:hover {
  color: var(--color-accent);
}

/* === Post body easter eggs (single posts only, via .post-body) === */
/* End of proof. */
.post-body::after {
  content: "\220e"; /* ∎ */
  display: block;
  text-align: end;
  color: var(--color-muted);
  margin-block-start: var(--space-xl);
}

/* === Post navigation (older / newer) === */
.post-nav {
  display: flex;
  justify-content: space-between;
  gap: var(--space-l);
  margin-block-start: var(--space-2xl);
  /* Same role as .home-recent/.prose .footnotes below: padding after a
     section-boundary rule stays tighter (--space-m) than the --space-2xl
     gap before it - a deliberately asymmetric "hug the rule" shape shared
     by every section boundary on the site, not a value specific to this
     one. */
  padding-block-start: var(--space-m);
  border-top: 1px solid var(--color-border);
}

.post-nav-link {
  display: flex;
  flex-direction: column;
  gap: 0.3rem; /* sub-scale hairline: tighter than --space-3xs allows */
  max-width: 48%;
  text-decoration: none;
}

.post-nav-newer {
  margin-inline-start: auto;
  text-align: right;
}

.post-nav-label {
  font-size: var(--step--1);
  color: var(--color-muted);
}

.post-nav-title {
  font-weight: 500;
  color: var(--color-heading);
  transition: color var(--transition);
}

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

/* === Tables === */
.table-wrapper {
  margin-block: var(--space-l);
  overflow-x: auto;
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
}

.prose th {
  /* Vertical padding matches .prose td below - a header row and a body
     row in the same table get the same row height. */
  padding: var(--space-xs) var(--space-s);
  text-align: left;
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-muted);
  border-block-end: 1px solid var(--color-heading);
}

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

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

/* === Heading anchors === */
.main-content :is(h2, h3, h4) {
  scroll-margin-block-start: var(--space-xl);
}

/* Flash the element a fragment link points at (headings, footnotes) so the
   eye lands on it after the jump. */
.main-content :target {
  animation: target-highlight 2s ease-out;
  border-radius: var(--radius-sm);
}

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

a.heading-anchor {
  font-family: var(--font-sans);
  font-size: 0.8em;
  margin-inline-start: var(--space-2xs);
  color: var(--color-muted);
  text-decoration: none;
  opacity: 0;
  transition:
    opacity var(--transition),
    color var(--transition);
}

:is(h2, h3, h4):hover a.heading-anchor,
a.heading-anchor:focus-visible {
  opacity: 1;
}

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

/* === Homepage === */
/* Visually hidden, still announced to screen readers and available to crawlers. */
.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;
}

.home-hero {
  margin-block: var(--space-l) var(--space-m);
}

/* Lede paragraph under a page title (homepage tagline, now). Body size,
   not a step up - muted color alone marks it as a subtitle, so the page
   doesn't add a size that's used nowhere else. */
.lede {
  margin: 0 0 var(--space-s);
  font-size: var(--step-0);
  line-height: 1.6;
  color: var(--color-muted);
}

.home-recent {
  margin-block-start: var(--space-2xl);
  /* Matches .post-nav/.prose .footnotes: --space-m after every
     section-boundary rule, site-wide. */
  padding-block-start: var(--space-m);
  border-top: 1px solid var(--color-border);
}

.home-recent h2 {
  margin-block-start: 0;
}

.home-recent .blog-timeline {
  margin-block: var(--space-s) var(--space-xs);
}

/* Quiet trailing link after a section ("All posts", "more"). */
.more-link {
  display: inline-block;
  margin-block-start: var(--space-xs);
  color: var(--color-muted);
  text-decoration: none;
  font-size: var(--step--1);
  font-weight: 500;
  transition: color var(--transition);
}

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

/* === Now page === */
.now-updated {
  margin: 0 0 var(--space-m);
  color: var(--color-muted);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
}

/* Club crests dropped inline into prose via the {{< crests >}} shortcode
   (currently just /now, under "Away from the keyboard"). A deliberately
   large row rather than an inline detail - the crests are the point here,
   not a footnote to the sentence above them - but content-sized and
   left-aligned, not stretched to fill the measure. */
.badges {
  display: flex;
  align-items: center;
  gap: var(--space-l);
  margin-block: var(--space-l);
}

.badge {
  display: inline-block;
  line-height: 0;
  /* Flat, constant (no hover state) - at this size, full-saturation crest
     colors read as glossy/plastic against the matte paper palette. */
  filter: saturate(0.7);
}

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

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

/* Overline label: a quiet muted group label above a year's posts. Outranks
   the .main-content h2 heading style on purpose: the year is a label, not
   a section heading. */
.main-content .timeline-year-label {
  font-family: var(--font-sans);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--color-muted);
}

.main-content .timeline-year-label {
  margin: var(--space-l) 0 var(--space-xs);
}

.timeline-year .blog-timeline {
  margin-block-start: 0;
}

.timeline-year .blog-timeline-date {
  width: 3.25rem;
}

.blog-timeline li {
  margin: 0;
}

.blog-timeline-entry {
  display: flex;
  align-items: baseline;
  gap: var(--space-l);
  padding: var(--space-xs);
  margin-inline: calc(var(--space-xs) * -1);
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius);
  transition: background-color var(--transition);
}

.blog-timeline-entry:hover {
  background-color: var(--color-bg-hover);
}

.blog-timeline-date {
  /* ch, not rem: monospace digits are a fixed width, so the column can be
     sized in characters ("Aug 12, 2026" is 12) instead of guessing a rem
     value. */
  flex-shrink: 0;
  width: 12ch;
  text-align: right;
  color: var(--color-muted);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1.7;
}

.blog-timeline-content {
  display: block;
  flex: 1;
  min-width: 0;
  line-height: 1.55;
}

.blog-timeline-title {
  color: var(--color-heading);
  font-weight: 500;
  transition: color var(--transition);
}

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

/* Index only: the homepage list stays title-only, since it is a teaser
   under a page of prose rather than somewhere you pick what to read. */
.blog-timeline-summary {
  display: block;
  margin-block-start: 0.1rem; /* sub-scale hairline: tighter than --space-3xs allows */
  color: var(--color-muted);
  font-size: var(--step--1);
  line-height: 1.5;
  text-wrap: pretty;
}

/* Blog index: most entries are "large" (serif title, a summary line, room
   for a thumbnail); a post can opt into a single compact line instead (see
   the `compact` front matter param) for minor, low-effort posts. */
.blog-timeline-entry-lg {
  align-items: flex-start;
  padding-block: var(--space-s);
}

.blog-timeline-entry-lg .blog-timeline-date {
  /* Sits at the title's cap height, not the full first line box.
     Sub-scale hairline: tighter than --space-3xs allows. */
  padding-block-start: 0.2rem;
}

.blog-timeline-entry-lg .blog-timeline-title {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--step-1);
  font-weight: 700;
  line-height: 1.3;
}

.blog-timeline-entry-lg .blog-timeline-summary {
  margin-block-start: var(--space-3xs);
}

.blog-timeline-summary-inline {
  color: var(--color-muted);
  font-weight: 400;
  margin-inline-start: 0.5em;
}

.has-thumb .blog-timeline-entry {
  align-items: center;
}

.blog-timeline-thumb {
  flex-shrink: 0;
  width: 5.5rem;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--color-border);
}

.blog-timeline-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* === Back to top ===
   A plain fixed corner button, independent of the header, that appears once
   the header has scrolled away - so it never sits over a screen you can
   already see the top of. */
.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);
  flex-shrink: 0;
  /* The stronger hairline, not --color-border: this floats over the page
     rather than dividing it, and at --color-border it reads as a smudge
     against a background of exactly the same colour. */
  border: 1px solid var(--color-underline);
  border-radius: var(--radius);
  /* Opaque, so prose scrolling underneath never shows through. */
  background-color: var(--color-bg);
  color: var(--color-muted);
  text-decoration: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(
    0.35rem
  ); /* motion offset, not a layout gap - kept off the spacing scale */
  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-heading);
  background-color: var(--color-bg-hover);
}

/* Author-origin `display` outranks the UA's `[hidden]` rule, same as the
   theme toggle: the link ships hidden and only the script reveals it. */
.to-top[hidden] {
  display: none;
}

/* === Figures === */
.figure-block {
  margin-block: var(--space-l);
  text-align: center;
}

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

.figure-caption {
  margin-block-start: var(--space-xs);
  font-size: var(--step--1);
  color: var(--color-muted);
  font-style: italic;
}

/* Break a figure out of the prose measure and re-center it on the viewport
   instead of the text column, for diagrams/embeds that benefit from more
   width than a comfortable reading line allows. Relies on .prose's
   horizontal center coinciding with the viewport's (true as long as an
   ancestor doesn't off-center .site-container). */
.figure-wide {
  --breakout-width: min(92vw, 62rem);
  width: var(--breakout-width);
  margin-inline: calc(50% - 50vw + (100vw - var(--breakout-width)) / 2);
}

.figure-wide img {
  width: 100%;
}

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

/* Soften bright images against the dark background (filter values cannot
   ride light-dark(), so this one rule keeps the dual theme scope). */
:root[data-theme="dark"] .prose img {
  filter: brightness(0.85) contrast(1.05);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .prose img {
    filter: brightness(0.85) contrast(1.05);
  }
}

/* === Code === */
.highlight {
  /* Anchor for the language chip / copy button; the pre scrolls under them. */
  position: relative;
  border-radius: var(--radius);
  border: 1px solid var(--color-border);
  margin-block: var(--space-l);
  background-color: var(--color-bg-code);
}

.highlight pre {
  margin: 0;
  padding: var(--space-m);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.55;
  background-color: transparent;
  overflow-x: auto;
}

.code-lang,
.code-copy {
  position: absolute;
  top: var(--space-xs);
  right: var(--space-xs);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  line-height: 1;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.3rem var(--space-xs); /* 0.3rem: sub-scale hairline */
  border-radius: var(--radius-sm);
  background-color: var(--color-bg-code);
  color: var(--color-muted);
  transition: opacity var(--transition);
}

.code-copy {
  border: 1px solid var(--color-border);
  cursor: pointer;
  opacity: 0;
}

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

.highlight:hover .code-copy,
.code-copy:focus-visible {
  opacity: 1;
}

.highlight:hover .code-lang,
.highlight:focus-within .code-lang {
  opacity: 0;
}

code:not(pre code) {
  background-color: var(--color-bg-code);
  color: var(--color-text);
  padding: 0.08em 0.32em;
  border-radius: var(--radius-sm);
  font-size: 0.85em;
  font-weight: 500;
  border: 1px solid var(--color-border);
  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. Until (or unless) the script draws the SVG, the
   block shows the escaped diagram source as quiet monospace text. */
pre.mermaid {
  display: flex;
  justify-content: center;
  margin-block: var(--space-l);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.6;
  color: var(--color-muted);
}

pre.mermaid svg {
  max-width: 100%;
  height: auto;
}

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

/* .error-page .error-title, not .error-title alone: the bare class loses
   to .main-content h1's higher specificity (a class+type selector beats a
   lone class), which was silently shrinking "404" down to a regular post
   heading's size instead of the display numeral this is meant to be. */
.error-page .error-title {
  font-family: var(--font-sans);
  /* Display-only exception: a big numeral deliberately breaks from the
     --step-* text scale (Bringhurst's own scale governs text sizes; large
     display treatments are conventionally chosen separately) rather than
     extending the modular scale seven more steps to reach it. It does
     happen to land exactly on --space-3xl, which is a nice coincidence,
     not a reason to reuse a spacing token as a font-size. */
  font-size: 4rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  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;
  border-radius: var(--radius);
  font-weight: 500;
  margin-block-start: var(--space-m);
}

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

/* === Responsive === */
@media (max-width: 640px) {
  .site-header {
    padding-inline: var(--space-m);
    padding-block-end: var(--space-xs);
    margin-block-end: var(--space-l);
  }

  .theme-toggle {
    padding: var(--space-xs);
  }

  /* Nav scrolls sideways rather than wrapping or shrinking, so links stay
     tappable however many of them there are; the toggle sits outside the
     scroll area so it never scrolls away with them. */
  .nav-list {
    overflow-x: auto;
    scrollbar-width: none;
    /* Room for the underline so it never clips against the border.
       Sub-scale hairline: tighter than --space-3xs allows. */
    padding-block-end: 0.15rem;
  }

  .nav-list::-webkit-scrollbar {
    display: none;
  }

  .nav-link {
    padding: var(--space-xs);
  }

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

  .site-footer {
    padding-inline: var(--space-m);
  }

  .footer-row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-m);
  }

  .highlight {
    margin-inline: calc(var(--space-m) * -1);
    border-radius: 0;
    border-inline: none;
  }

  .blog-timeline-entry,
  .has-thumb .blog-timeline-entry {
    flex-direction: column;
    /* Cancels .has-thumb's row-only centering: stacked, everything should
       hug the left edge like any other entry. Matches .has-thumb's own
       specificity so it actually wins here. */
    align-items: stretch;
    gap: 0.125rem; /* sub-scale hairline: tighter than --space-3xs allows */
  }

  .blog-timeline-thumb {
    width: 100%;
    aspect-ratio: 16 / 9;
    margin-block-start: var(--space-2xs);
  }

  .blog-timeline-date,
  .timeline-year .blog-timeline-date {
    text-align: left;
    width: auto;
  }

  .post-nav {
    flex-direction: column;
  }

  .post-nav-link,
  .post-nav-newer {
    max-width: none;
    margin-inline-start: 0;
    text-align: left;
  }
}

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

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

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

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

  .prose a {
    text-decoration: none;
  }

  /* Show where external links point since paper has no hover to reveal
     anything on click - print is the one place this is worth the space. */
  .prose a[href^="http"]:not([href*="kirillbobyrev.com"])::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
    vertical-align: baseline;
    color: var(--color-muted);
    word-break: break-all;
    opacity: 1;
  }

  .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-bg-code,
   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;
}
/* LineLink */
.chroma .lnlinks {
  outline: none;
  text-decoration: none;
  color: inherit;
}
/* LineTableTD */
.chroma .lntd {
  vertical-align: top;
  padding: 0;
  margin: 0;
  border: 0;
}
/* LineTable */
.chroma .lntable {
  border-spacing: 0;
  padding: 0;
  margin: 0;
  border: 0;
}
/* LineNumbersTable */
.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);
}
/* LineNumbers */
.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);
}
/* Line */
.chroma .line {
  display: flex;
}
/* Error */
.chroma .err {
  color: light-dark(#cf222e, #f85149);
}
/* Keyword, KeywordDeclaration, KeywordNamespace, KeywordReserved, KeywordType */
.chroma .k,
.chroma .kd,
.chroma .kn,
.chroma .kr,
.chroma .kt {
  color: light-dark(#cf222e, #ff7b72);
}
/* KeywordConstant, KeywordPseudo */
.chroma .kc,
.chroma .kp {
  color: light-dark(#cf222e, #79c0ff);
}
/* NameAttribute, NameOther */
.chroma .na,
.chroma .nx {
  color: light-dark(#1f2328, #e6edf3);
}
/* NameClass, NameException */
.chroma .nc,
.chroma .ne {
  color: light-dark(#1f2328, #f0883e);
}
/* NameConstant */
.chroma .no {
  color: light-dark(#0550ae, #79c0ff);
}
/* NameDecorator */
.chroma .nd {
  color: light-dark(#0550ae, #d2a8ff);
}
/* NameEntity */
.chroma .ni {
  color: light-dark(#6639ba, #ffa657);
}
/* NameLabel */
.chroma .nl {
  color: light-dark(#990000, #79c0ff);
  font-weight: bold;
}
/* NameNamespace */
.chroma .nn {
  color: light-dark(#24292e, #ff7b72);
}
/* NameTag */
.chroma .nt {
  color: light-dark(#0550ae, #7ee787);
}
/* NameBuiltin */
.chroma .nb {
  color: light-dark(#6639ba, #e6edf3);
}
/* NameBuiltinPseudo */
.chroma .bp {
  color: light-dark(#6a737d, #e6edf3);
}
/* NameVariable, NameVariableClass, NameVariableGlobal, NameVariableInstance,
   NameVariableMagic */
.chroma .nv,
.chroma .vc,
.chroma .vg,
.chroma .vi,
.chroma .vm {
  color: light-dark(#953800, #79c0ff);
}
/* NameFunction, NameFunctionMagic */
.chroma .nf,
.chroma .fm {
  color: light-dark(#6639ba, #d2a8ff);
}
/* NameProperty */
.chroma .py {
  color: light-dark(#1f2328, #79c0ff);
}
/* Literal */
.chroma .l {
  color: light-dark(#1f2328, #a5d6ff);
}
/* LiteralDate */
.chroma .ld {
  color: light-dark(#1f2328, #79c0ff);
}
/* LiteralString, LiteralStringBacktick, LiteralStringChar, LiteralStringDoc,
   LiteralStringDouble, LiteralStringInterpol, LiteralStringOther,
   LiteralStringSingle */
.chroma .s,
.chroma .sb,
.chroma .sc,
.chroma .sd,
.chroma .s2,
.chroma .si,
.chroma .sx,
.chroma .s1 {
  color: light-dark(#0a3069, #a5d6ff);
}
/* LiteralStringAffix, LiteralStringDelimiter, LiteralStringEscape,
   LiteralStringHeredoc, LiteralStringRegex */
.chroma .sa,
.chroma .dl,
.chroma .se,
.chroma .sh,
.chroma .sr {
  color: light-dark(#0a3069, #79c0ff);
}
/* LiteralStringSymbol */
.chroma .ss {
  color: light-dark(#032f62, #a5d6ff);
}
/* LiteralNumber, LiteralNumberBin, LiteralNumberFloat, LiteralNumberHex,
   LiteralNumberInteger, LiteralNumberIntegerLong, LiteralNumberOct */
.chroma .m,
.chroma .mb,
.chroma .mf,
.chroma .mh,
.chroma .mi,
.chroma .il,
.chroma .mo {
  color: light-dark(#0550ae, #a5d6ff);
}
/* Operator, OperatorWord, OperatorReserved */
.chroma .o,
.chroma .ow,
.chroma .or {
  color: light-dark(#0550ae, #ff7b72);
}
/* Punctuation */
.chroma .p {
  color: light-dark(#1f2328, #e6edf3);
}
/* Comment, CommentHashbang, CommentMultiline, CommentSingle, CommentSpecial,
   CommentPreproc, CommentPreprocFile */
.chroma .c,
.chroma .ch,
.chroma .cm,
.chroma .c1,
.chroma .cs,
.chroma .cp,
.chroma .cpf {
  color: light-dark(#57606a, #8b949e);
}
/* GenericDeleted */
.chroma .gd {
  color: light-dark(#82071e, #ffa198);
}
/* GenericEmph */
.chroma .ge {
  color: light-dark(#1f2328, #e6edf3);
  font-style: italic;
}
/* GenericError */
.chroma .gr {
  color: light-dark(#1f2328, #ffa198);
}
/* GenericHeading, GenericSubheading */
.chroma .gh,
.chroma .gu {
  color: light-dark(#1f2328, #79c0ff);
}
/* GenericInserted */
.chroma .gi {
  color: light-dark(#116329, #56d364);
}
/* GenericOutput, GenericPrompt */
.chroma .go,
.chroma .gp {
  color: light-dark(#1f2328, #8b949e);
}
/* GenericStrong */
.chroma .gs {
  font-weight: bold;
}
/* GenericTraceback */
.chroma .gt {
  color: light-dark(#1f2328, #ff7b72);
}
/* GenericUnderline */
.chroma .gl {
  text-decoration: underline;
}
/* TextWhitespace */
.chroma .w {
  color: light-dark(#ffffff, #6e7681);
}
