/* ==========================================================================
   Madison Meredith — main stylesheet

   Ported from the "Madison Meredith" design system. Every token VALUE below is
   a verbatim copy of the system's tokens/*.css and should stay that way — edit
   them in the design system and re-copy, never by hand here.

   One structural deviation: the system ships dark on :root with light opting in.
   This site serves the two the other way round (§1 / §6), because :root is what
   renders when scripting is off, and light is also where the theme script lands
   with no stored choice and no OS preference. With the system's order those two
   disagreed. Nothing was recolored to do it — the same two value sets simply
   changed places, so a re-copy is still a paste per block, not a translation.

   Contents
     1.  Tokens: colors
     2.  Tokens: typography
     3.  Tokens: spacing
     4.  Tokens: effects
     5.  Tokens: motion
     6.  Tokens: dark theme
     7.  Base
     8.  Layout
     9.  Components
     10. Pages
     11. Media queries
   ========================================================================== */


/* ==========================================================================
   1. Tokens: colors
   ========================================================================== */

/* Green is the ground, rose is the accent, roughly 90/10. Light is the default
   theme here: :root holds the light values, [data-theme="dark"] (§6) overrides
   them. Three rose stops do three different jobs and are not interchangeable:
     --rose-light #E3B5BE  ambient only — hairlines, section tints, subtle fills. Never text, never a button.
     --rose-mid   #C4707F  display moment, hover, dark-mode button fill (carries DARK label text at AA).
     --rose-deep  #8E2F47  light-mode buttons and links (carries WHITE label text at AA).
   No gradients live in this system. */
:root {
  /* ——— raw palette ——— */
  --rose-light: #E3B5BE;
  --rose-mid: #C4707F;
  --rose-deep: #8E2F47;

  --green-ground: #12241C;  /* dark page ground */
  --green-forest: #1E3A2B;  /* dark elevated surface · light primary text */
  --green-sage: #4A5F52;    /* light secondary text */

  --paper: #FDFCFB;         /* light page ground — near-white, a trace of warmth, never pure white */
  --paper-2: #F4F1EE;       /* light elevated surface */
  --bone: #E8DDD8;          /* dark primary text — warm, never pure white */
  --bone-2: #A8B5AC;        /* dark secondary text */
  --white: #FFFFFF;         /* button label on --rose-deep only */

  /* ——— light (default) ——— */
  --shell: var(--paper);
  --shell-2: var(--paper-2);
  --shell-3: color-mix(in oklab, var(--rose-light) 28%, var(--paper-2));
  --card: var(--paper-2);
  --ink: var(--green-forest);
  --ink-2: var(--green-sage);
  --ink-3: var(--green-sage);
  --ink-4: var(--rose-light);
  --ink-5: color-mix(in oklab, var(--rose-light) 52%, var(--paper));

  /* value-inverting ramps: 100 is the deepest tint, 700 the lightest — component code is theme-agnostic */
  --rose-100: color-mix(in oklab, var(--rose-light) 30%, var(--paper));
  --rose-300: var(--rose-light);
  --rose-500: var(--rose-mid);
  --rose-700: var(--rose-deep);
  --green-100: color-mix(in oklab, var(--green-forest) 9%, var(--paper));
  --green-300: color-mix(in oklab, var(--green-forest) 26%, var(--paper));
  --green-500: var(--green-sage);
  --green-700: var(--green-forest);

  /* ——— text ——— */
  --text-heading: var(--ink);
  --text-body: var(--ink-2);
  --text-muted: var(--ink-2);
  --text-inverse: var(--bone);
  --text-subtitle: var(--rose-deep);  /* subtitles / kickers */
  --text-display: var(--rose-deep);   /* at most one display moment per page; the mid stop is under 3:1 on paper */
  --text-link: var(--rose-deep);
  --text-link-hover: var(--rose-mid);
  --text-accent: var(--rose-deep);

  /* The deep stop clears AA on both paper and paper-2, so nothing steps back in light mode. In dark it has
     to: the mid stop reaches only 3.5:1 on the elevated surface and the light stop may never carry text, so
     rose text falls back to the ink and the underline carries the accent. §6 is where that happens. Any
     element that paints a card / sunken band takes data-surface and its subtree inherits these. */
  --text-link-on-surface: var(--rose-deep);
  --text-subtitle-on-surface: var(--rose-deep);
  --text-accent-on-surface: var(--rose-deep);

  /* ——— surfaces ——— */
  --surface-page: var(--shell);
  --surface-sunken: var(--shell-2);
  --surface-card: var(--card);
  --surface-band-rose: var(--rose-100);  /* ambient rose tint — never behind long prose */
  --surface-band-green: color-mix(in oklab, var(--green-forest) 9%, var(--paper));
  --surface-inverse: var(--green-forest);
  --surface-code: var(--green-ground);

  /* ——— lines ——— */
  --border-soft: var(--ink-5);
  --border-strong: var(--ink-4);
  --border-rose: var(--rose-300);     /* at most one or two hairline rules per page */

  /* ——— interactive ——— */
  --action-primary: var(--rose-deep);
  --action-primary-hover: color-mix(in oklab, #000 16%, var(--rose-deep));
  --action-label: var(--white);                  /* white label on the deep stop */
  --action-secondary: var(--green-forest);
  --action-secondary-hover: color-mix(in oklab, #000 14%, var(--green-forest));
  --action-secondary-label: var(--bone);

  /* ——— status: carried by the two hues only ——— */
  --status-note: var(--green-500);
  --status-warn: var(--rose-500);
  --status-hot: var(--rose-500);

  --mark-highlight: var(--rose-100);
  --focus-ring: var(--rose-mid);
}

[data-surface] {
  --text-subtitle: var(--text-subtitle-on-surface);
  --text-accent: var(--text-accent-on-surface);
  --text-link: var(--text-link-on-surface);
  --text-link-hover: var(--text-link-on-surface);
}


/* ==========================================================================
   2. Tokens: typography
   ========================================================================== */

:root {
  --font-display: "Urbanist", system-ui, sans-serif;  /* headings, wordmark */
  --font-ui: "Figtree", system-ui, sans-serif;        /* body, subtitles, labels */
  --font-body: var(--font-ui);
  --font-mono: "JetBrains Mono", ui-monospace, Menlo, monospace;  /* code only */

  --text-2xs: 0.75rem;    /* 12px  fine print */
  --text-xs: 0.8125rem;   /* 13px  tags */
  --text-sm: 0.9375rem;   /* 15px  captions, metadata */
  --text-base: 1.0625rem; /* 17px  body copy */
  --text-md: 1.125rem;    /* 18px  subtitles */
  --text-lg: 1.3125rem;   /* 21px  lede */
  --text-xl: 1.625rem;    /* 26px  h3 */
  --text-2xl: 2.125rem;   /* 34px  h2 */
  --text-3xl: 2.75rem;    /* 44px  h1 */
  --text-4xl: 3.75rem;    /* 60px  page display */
  --text-5xl: 5.25rem;    /* 84px  hero wordmark */

  --leading-tight: 1.1;
  --leading-snug: 1.25;
  --leading-normal: 1.5;
  --leading-relaxed: 1.7;

  --tracking-tight: -0.015em;
  --tracking-normal: 0;
  --tracking-wide: 0.02em;

  --weight-light: 300;
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  --measure-prose: 68ch;
  --measure-narrow: 46ch;
}


/* ==========================================================================
   3. Tokens: spacing
   ========================================================================== */

:root {
  --space-2xs: 0.25rem;  /* 4   */
  --space-xs: 0.5rem;    /* 8   */
  --space-sm: 0.75rem;   /* 12  */
  --space-md: 1rem;      /* 16  */
  --space-lg: 1.5rem;    /* 24  */
  --space-xl: 2rem;      /* 32  */
  --space-2xl: 3rem;     /* 48  */
  --space-3xl: 4.5rem;   /* 72  */
  --space-4xl: 7rem;     /* 112 — between page sections */

  --gutter: clamp(1.25rem, 5vw, 3rem);
  --width-content: 44rem;  /* 704  — prose column */
  --width-wide: 68rem;     /* 1088 — header/footer/grid */
  --stack-flow: 1.15em;    /* default vertical rhythm in prose */

  /* An ADDITION: the system sizes one screen, not a 3,000-word essay, so the post
     page reads at its own measure. There is no --width-post to go with it — the
     post column is --width-wide, the same box the header and footer sit in, and
     the surplus between the two is what a blockquote spends. See "Post page" in
     §10. This is 68ch at the post's body size, which is the system's own
     --measure-prose in characters: the line got longer because the type did. */
  --measure-post: 52rem;   /* 832 — the reading measure on a post */
}


/* ==========================================================================
   4. Tokens: effects
   ========================================================================== */

:root {
  --radius-xs: 6px;
  --radius-sm: 10px;
  --radius-md: 14px;
  --radius-lg: 22px;
  --radius-xl: 28px;
  --radius-pill: 999px;

  --border-hairline: 1px;
  --border-base: 1px;
  --border-thick: 2px;

  /* elevation is green-black and low-contrast — depth, never glow. Dark mode deepens it in §6. */
  --shadow-xs: 0 1px 2px rgb(18 36 28 / 0.05);
  --shadow-sm: 0 2px 6px -2px rgb(18 36 28 / 0.10), 0 1px 2px rgb(18 36 28 / 0.05);
  --shadow-md: 0 8px 20px -10px rgb(18 36 28 / 0.14), 0 2px 6px -3px rgb(18 36 28 / 0.07);
  --shadow-lg: 0 20px 44px -20px rgb(18 36 28 / 0.20), 0 6px 14px -8px rgb(18 36 28 / 0.09);

  --ring-focus: 0 0 0 3px color-mix(in oklab, var(--focus-ring) 45%, transparent);
}


/* ==========================================================================
   5. Tokens: motion
   ========================================================================== */

:root {
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-in-out: cubic-bezier(0.6, 0.05, 0.35, 0.95);

  --dur-instant: 80ms;
  --dur-fast: 160ms;   /* colour, underline */
  --dur-base: 260ms;   /* lift, shadow */
  --dur-slow: 460ms;   /* enter / reveal */

  --lift: -2px;
}


/* ==========================================================================
   6. Tokens: dark theme

   Opt in with data-theme="dark" on <html>. Light is the default (§1). Both
   ramps invert in value — 100 is always the deepest tint and 700 the lightest
   — so component styles are identical in either theme.

   The button stop inverts here: --rose-mid carries near-black label text at AA,
   where the light theme's --rose-deep carries white. One stop can never serve
   both modes.

   This block also carries the on-surface step-back. On the elevated green
   (#1E3A2B) the mid stop reaches only 3.5:1 and the light stop may never carry
   text, so rose text on a card or a sunken band falls back to the ink. Light
   needs no such move, which is why those three tokens are inert in §1 and
   load-bearing here.
   ========================================================================== */

[data-theme="dark"] {
  --shell: var(--green-ground);
  --shell-2: var(--green-forest);
  --shell-3: color-mix(in oklab, var(--bone) 10%, var(--green-forest));
  --card: var(--green-forest);
  --ink: var(--bone);
  --ink-2: var(--bone-2);
  --ink-3: var(--bone-2);
  --ink-4: color-mix(in oklab, var(--bone-2) 34%, var(--shell));
  --ink-5: color-mix(in oklab, var(--bone-2) 16%, var(--shell));

  --rose-100: color-mix(in oklab, var(--rose-mid) 14%, var(--shell));
  --rose-300: color-mix(in oklab, var(--rose-mid) 52%, var(--shell));
  --rose-500: var(--rose-mid);
  --rose-700: var(--bone);         /* text on a rose tint: the light stop may never carry text, so the ink does */
  --green-100: var(--green-forest);
  --green-300: color-mix(in oklab, var(--bone-2) 30%, var(--shell));
  --green-500: var(--bone-2);
  --green-700: var(--bone);

  --text-inverse: var(--shell);
  --text-subtitle: var(--rose-mid);
  --text-display: var(--rose-mid);  /* the deep stop is too dark on the green ground — the mid stop carries display here */
  --text-link: var(--rose-mid);
  --text-link-hover: var(--rose-light);
  --text-accent: var(--rose-mid);
  /* the step-back described above: rose text on an elevated surface becomes ink */
  --text-link-on-surface: var(--ink);
  --text-subtitle-on-surface: var(--ink);
  --text-accent-on-surface: var(--ink);

  --surface-band-green: var(--shell-2);
  --surface-inverse: var(--ink);
  --surface-code: color-mix(in oklab, #000 34%, var(--green-ground));

  --action-primary: var(--rose-mid);
  --action-primary-hover: var(--rose-light);
  --action-label: var(--green-ground);
  --action-secondary: var(--shell-2);
  --action-secondary-hover: var(--shell-3);
  --action-secondary-label: var(--ink);

  --mark-highlight: var(--rose-300);

  --shadow-xs: 0 1px 2px rgb(0 0 0 / 0.28);
  --shadow-sm: 0 2px 6px -2px rgb(0 0 0 / 0.42), 0 1px 2px rgb(0 0 0 / 0.28);
  --shadow-md: 0 8px 20px -10px rgb(0 0 0 / 0.55), 0 2px 6px -3px rgb(0 0 0 / 0.38);
  --shadow-lg: 0 20px 44px -20px rgb(0 0 0 / 0.66), 0 6px 14px -8px rgb(0 0 0 / 0.42);

  color-scheme: dark;
}


/* ==========================================================================
   7. Base
   ========================================================================== */

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

/* background-color is a local addition, not in the system's base.css: it keeps
   the overscroll area from flashing white. Keep it through any re-copy. */
html {
  background-color: var(--shell);
  color-scheme: light;
}

body {
  -webkit-font-smoothing: antialiased;
  background-color: var(--surface-page);
  color: var(--text-body);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  margin: 0;
  transition: background-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}

h1,
h2,
h3,
h4,
h5 {
  color: var(--text-heading);
  font-family: var(--font-display);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  margin: 0;
}

h1 {
  font-size: var(--text-3xl);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
}

h2 {
  font-size: var(--text-2xl);
}

h3 {
  font-size: var(--text-xl);
}

code,
kbd,
pre,
samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

a {
  color: var(--text-link);
  text-decoration-color: var(--border-rose);
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color var(--dur-fast) var(--ease-out), text-decoration-color var(--dur-fast) var(--ease-out);
}

a:hover {
  color: var(--text-link-hover);
  text-decoration-color: currentcolor;
}

a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Also a local addition to base.css. Keep it through any re-copy. */
img {
  max-width: 100%;
}

/* A local deviation from base.css, and one worth pushing upstream — the system's
   rule paints --shell as the label, which is the page ground the fill is a tint
   of, so it fails in both themes (1.18:1 light, 2.15:1 dark). It also leans on
   --mark-highlight, a stop tuned for the other job that token has: the faint
   wash behind .prose mark. That wash is right for inline emphasis and far too
   weak for a selection — in light it clears the page by 1.18:1 and a card by
   1.07:1. The ambient stop is the one whose stated job is a subtle fill, and
   because the rose ramp inverts by value it needs no dark override: ink lands
   6.85:1 on it in light, 5.67:1 in dark. --mark-highlight keeps .prose mark. */
::selection {
  background-color: var(--rose-300);
  color: var(--ink);
}


/* ==========================================================================
   8. Layout
   ========================================================================== */

.site {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.site__body {
  flex: 1;
}

/* Standard page column. Bottom spacing comes from the footer's own margin,
   matching the design, so no page sets a bottom padding. */
.site-main {
  margin-left: auto;
  margin-right: auto;
  max-width: var(--width-wide);
  padding-left: var(--gutter);
  padding-right: var(--gutter);
  padding-top: var(--space-2xl);
  width: 100%;
}

.site-main--content {
  max-width: var(--width-content);
}

/* Full-bleed tinted section. The design allows at most two per page; the home
   page spends one, on its archive.

   --surface-band-rose is the only one of the three band fills that differs from
   --card in BOTH themes, which is what makes it the one tint an untinted card
   grid can sit on: --surface-sunken is --card's own value in both, and
   --surface-band-green is in dark. */
.band--rose {
  background-color: var(--surface-band-rose);
}

.band--sunken {
  background-color: var(--surface-sunken);
}

/* The hero is not a tinted band: it carries no fill at all and is closed by a
   single rose hairline, which is one of the one-or-two the page is allowed. */
.band--hero {
  border-bottom: var(--border-hairline) solid var(--border-rose);
}

/* The third of the three band tints, and the one that only works where no card
   sits on it: --surface-band-green is --card's own value in dark, so a card here
   would be the same fill as the thing behind it. The home page's closing band
   carries no card, which is what makes this available to it. */
.band--green {
  background-color: var(--surface-band-green);
}

.band__inner {
  margin-left: auto;
  margin-right: auto;
  max-width: var(--width-wide);
  padding-bottom: var(--space-2xl);
  padding-left: var(--gutter);
  padding-right: var(--gutter);
  padding-top: var(--space-2xl);
}

.band--hero .band__inner {
  padding-top: var(--space-3xl);
}

/* The button that closes a band. Was .post-list__more, back when the home page
   ended in a post list rather than a card grid — it is spacing only, and both
   home bands close the same way, so one name covers it. */
.band__more {
  margin-top: var(--space-lg);
}

.card-grid {
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
}

.card-grid--wide {
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
}


/* ==========================================================================
   9. Components
   ========================================================================== */

/* --- Site header ---------------------------------------------------------- */

.site-header {
  background-color: var(--surface-page);
  border-bottom: var(--border-hairline) solid var(--border-soft);
}

.site-header__inner {
  align-items: center;
  display: flex;
  gap: var(--space-lg);
  justify-content: space-between;
  margin-left: auto;
  margin-right: auto;
  max-width: var(--width-wide);
  padding: var(--space-md) var(--gutter);
}

/* There is no logo mark in this system — the name is the mark. */
.site-header__wordmark {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  text-decoration: none;
}

.site-nav {
  align-items: center;
  display: flex;
  gap: var(--space-lg);
}

.site-nav__link {
  color: var(--ink-2);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  font-weight: var(--weight-regular);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}

.site-nav__link:hover {
  color: var(--text-link);
}

.site-nav__link--active {
  color: var(--text-link);
  font-weight: var(--weight-medium);
}

/* An unchromed mark, not a pill: no fill, no border, nothing but the icon.
   Which icon shows is decided in CSS rather than JS, so it is correct before
   first paint and still correct with scripting off — the moon is the default
   because :root is light and the button's offer is dark. */
.theme-toggle {
  background: none;
  border: 0;
  color: var(--ink-2);
  cursor: pointer;
  display: grid;
  padding: 0;
  place-items: center;
  transition: color var(--dur-fast) var(--ease-out);
}

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

.theme-toggle__icon {
  display: block;
}

.theme-toggle__icon--sun {
  display: none;
}

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

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

/* --- Site footer ---------------------------------------------------------- */

.site-footer {
  background-color: var(--surface-sunken);
  border-top: var(--border-hairline) solid var(--border-soft);
  margin-top: var(--space-3xl);
}

/* A page that ends in a PAINTED band runs it straight into the footer's fill.
   The margin above is page-ground breathing room, which is right when the last
   band is unfilled and wrong when it is painted: --surface-band-green is the
   footer's own fill in dark, so the margin cut a stripe of page ground out of
   what reads as one continuous surface — the three-stripes problem that keeps
   the graveyard off a band (see .graveyard in §10).

   It has to be the footer that gives the margin up. .site__body is a flex item,
   so it establishes an independent formatting context and a negative margin on
   the band cannot escape it to cancel this one. :has() is what lets the footer
   know what preceded it, so every page that does NOT end in a painted band keeps
   its spacing. All three band fills are listed rather than just the one in use,
   so the next painted closing band gets this for free. */
.site__body:has(> main > .band--green:last-child) + .site-footer,
.site__body:has(> main > .band--rose:last-child) + .site-footer,
.site__body:has(> main > .band--sunken:last-child) + .site-footer {
  margin-top: 0;
}

.site-footer__inner {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg);
  justify-content: space-between;
  margin-left: auto;
  margin-right: auto;
  max-width: var(--width-wide);
  padding: var(--space-lg) var(--gutter);
}

.site-footer__copyright {
  color: var(--ink-3);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
}

.site-footer__social {
  align-items: center;
  display: flex;
  gap: var(--space-md);
}

/* Hovers to --text-link rather than a raw rose stop so it inherits the footer's
   data-surface remap: rose-deep in light, and the ink in dark, where rose on
   this surface is the thing the remap exists to prevent. The raw --rose-500 the
   marks used to take reaches only 2.96:1 on the light sunken fill — under even
   the 3:1 bar a non-text control gets. */
.site-footer__icon {
  color: var(--ink-2);
  display: grid;
  place-items: center;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}

.site-footer__icon:hover {
  color: var(--text-link);
}

/* --- Button --------------------------------------------------------------- */

/* Every variant sets --button-label rather than `color` directly, because most
   buttons on this site are anchors and the base `a:hover` recolor (0,1,1) beats
   a bare variant class (0,1,0) — so the label would drift to the link color on
   hover, over a fill chosen to sit under the resting label. Routing it through
   one property lets the a:hover below hand back the variant's own value without
   restating five of them. A button moves its fill on hover, never its label. */
.button {
  align-items: center;
  border: var(--border-base) solid transparent;
  border-radius: var(--radius-pill);
  color: var(--button-label);
  cursor: pointer;
  display: inline-flex;
  font-family: var(--font-ui);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  gap: 8px;
  justify-content: center;
  line-height: 1.2;
  padding: 10px 20px;
  text-decoration: none;
  transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}

.button:hover,
a.button:hover {
  color: var(--button-label);
  transform: translateY(var(--lift));
}

/* Press settles back rather than pushing further — no color change. */
.button:active {
  transform: translateY(0);
}

.button--sm {
  font-size: var(--text-sm);
  padding: 7px 14px;
}

.button--lg {
  font-size: var(--text-md);
  padding: 14px 28px;
}

/* Raised tones carry a resting shadow and deepen it on hover — depth, never
   glow. The rose bloom the previous system used is gone. */
.button--primary,
.button--secondary {
  box-shadow: var(--shadow-sm);
}

.button--primary:hover,
.button--secondary:hover {
  box-shadow: var(--shadow-md);
}

.button--primary:active,
.button--secondary:active {
  box-shadow: var(--shadow-sm);
}

/* The label is a token, not a literal: the rose stop inverts by theme, so the
   deep stop takes white in light and the mid stop takes near-black in dark. */
.button--primary {
  --button-label: var(--action-label);
  background-color: var(--action-primary);
}

.button--primary:hover {
  background-color: var(--action-primary-hover);
}

.button--secondary {
  --button-label: var(--action-secondary-label);
  background-color: var(--action-secondary);
  border-color: var(--border-soft);
}

.button--secondary:hover {
  background-color: var(--action-secondary-hover);
}

/* Flat tones lift into a shadow only on hover. */
.button--soft,
.button--outline,
.button--ghost {
  box-shadow: none;
}

.button--soft:hover,
.button--outline:hover,
.button--ghost:hover {
  box-shadow: var(--shadow-sm);
}

.button--soft:active,
.button--outline:active,
.button--ghost:active {
  box-shadow: none;
}

.button--soft {
  --button-label: var(--rose-700);
  background-color: var(--rose-100);
  border-color: var(--border-rose);
}

.button--soft:hover {
  background-color: var(--rose-300);
}

.button--outline {
  --button-label: var(--ink);
  background-color: var(--card);
  border-color: var(--border-soft);
}

/* --shell-3, not --shell-2: --shell-2 and --card resolve to the same value in
   both themes, so hovering to it moved nothing. This is the one stop that is
   distinct from the card surface either way. */
.button--outline:hover {
  background-color: var(--shell-3);
}

.button--ghost {
  --button-label: var(--ink-2);
  background-color: transparent;
}

.button--ghost:hover {
  background-color: var(--shell-2);
}

/* --- Card ----------------------------------------------------------------- */

.card {
  background-color: var(--surface-card);
  border: var(--border-base) solid var(--border-soft);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  color: inherit;
  display: block;
  padding: var(--space-lg);
  text-decoration: none;
  transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}

.card--sunken {
  background-color: var(--surface-sunken);
}

/* The rose tint is the one surface that takes a rose edge rather than a soft one. */
.card--rose {
  background-color: var(--surface-band-rose);
  border-color: var(--border-rose);
}

.card--green {
  background-color: var(--surface-band-green);
}

/* Never nest a shadowed card inside another — flatten the inner one. */
.card--flat {
  box-shadow: none;
}

.card--interactive:hover {
  box-shadow: var(--shadow-lg);
  transform: translateY(var(--lift));
}

/* --- Tag ------------------------------------------------------------------ */

.tag {
  align-items: center;
  background-color: var(--shell-2);
  border: var(--border-base) solid transparent;
  border-radius: var(--radius-pill);
  color: var(--ink-2);
  cursor: default;
  display: inline-flex;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  gap: 5px;
  line-height: 1;
  padding: 6px 12px;
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-out);
}

/* The component's `count` slot: a second child inside the pill, sharing the 5px
   gap the block above already reserves for it. Only the topics index passes one.

   LOCAL DEVIATION, and the second of the kind that fixes a bug the system ships
   rather than moving a value that changed nothing (see ::selection for the first).
   The component dims this to `opacity: 0.55`, which measures 2.38:1 in light and
   2.84:1 in dark against the pill's own fill — 13px text at well under AA. There is
   no opacity that rescues it: --ink-2 on --shell-2 is only 6.12:1 light / 5.82:1
   dark to begin with, so the budget runs out before the dimming is worth looking at
   (0.90 reaches 4.87:1 / 5.03:1 and is indistinguishable from no dimming at all).

   The count is therefore set back by WEIGHT instead of by alpha, which costs no
   contrast: it keeps the label's color at the full 6.12:1 / 5.82:1 and reads as
   secondary because the pill around it is --weight-medium. Push the opacity upstream
   when the Tag component is next touched. */
.tag__count {
  font-weight: var(--weight-regular);
}

/* LOCAL DEVIATION from the Tag component, worth raising upstream.
   The neutral tag's fill is --shell-2, which resolves to the same value as
   --card: a neutral pill is therefore invisible on a paper card in either
   theme, and on a green card in dark as well. The component already reserves
   a 1px border slot, so filling it in on a painted surface restores the pill
   without moving anything. Scoped to [data-surface] so tags on the page
   ground, where the fill already reads, are untouched. */
[data-surface] .tag {
  border-color: var(--border-soft);
}

a.tag {
  cursor: pointer;
}

a.tag:hover {
  background-color: var(--shell-3);
}

.tag--rose {
  background-color: var(--rose-100);
  color: var(--rose-700);
}

a.tag--rose:hover {
  background-color: var(--rose-300);
}

.tag--green {
  background-color: var(--green-100);
  color: var(--green-700);
}

a.tag--green:hover {
  background-color: var(--green-300);
}

/* --- Eyebrow -------------------------------------------------------------- */

/* A sentence-case subtitle, never an uppercase kicker. */
.eyebrow {
  color: var(--text-subtitle);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  line-height: 1.5;
}

.eyebrow--muted {
  color: var(--text-muted);
}

.eyebrow--accent {
  color: var(--text-accent);
}

.eyebrow--gap {
  margin-bottom: var(--space-sm);
}

/* --- Callout -------------------------------------------------------------- */

/* Green for asides, rose for anything the reader must not miss. There is no
   shadow: a callout is a flat tint inside prose, not an elevated thing.
   The default and --aside are the same green treatment, which is why the
   aside markup already in the posts needs no edit. */
.callout {
  background-color: var(--green-100);
  border: var(--border-base) solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: var(--space-md) var(--space-lg);
}

.callout__label {
  color: var(--green-700);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  margin-bottom: 4px;
}

.callout__body {
  color: var(--ink-2);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.callout--warning,
.callout--correction {
  background-color: var(--rose-100);
  border-color: var(--border-rose);
}

.callout--warning .callout__label,
.callout--correction .callout__label {
  color: var(--rose-700);
}

/* --- Section heading ------------------------------------------------------ */

.section-heading {
  margin-bottom: var(--space-lg);
}

.section-heading__row {
  align-items: flex-end;
  display: flex;
  gap: var(--space-lg);
  justify-content: space-between;
}

.section-heading__title {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  margin: 0;
}

.section-heading__action {
  color: var(--text-link);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  white-space: nowrap;
}

/* The one .section-heading__action that is a <button> rather than an anchor —
   the home page's shuffle control, which the script inserts. Clearing the UA
   button styling is all it needs; the pairing itself is the settled one the
   pager and the graveyard links already borrowed.

   The hover is written out because the base a:hover the anchors take does not
   reach a button, and :focus-visible is already handled globally. */
button.section-heading__action {
  align-items: center;
  background: none;
  border: 0;
  cursor: pointer;
  display: inline-flex;
  gap: var(--space-2xs);
  padding: 0;
  transition: color var(--dur-fast) var(--ease-out);
}

button.section-heading__action:hover {
  color: var(--text-link-hover);
}

/* On a surface every rose text token steps back — including --text-link-hover,
   which leaves this control resting and hovering on one color. No color fixes
   it: measured on the home page's rose band the mid stop reaches 2.51:1 in light
   and 3.76:1 in dark, and --rose-700, the stop whose whole job is text on a rose
   tint, is already the resting value in light and is the ink itself in dark. So
   the hover is carried by an underline instead — .text-link's own on-surface
   answer, in the shape .text-link--quiet already uses. currentcolor, so it
   inherits whatever the surface stepped the label back to and cannot be the one
   part of the control that fails.

   It decorates the label rather than the button because the button is an
   inline-flex holding an icon, and a decoration there draws through the icon
   too. */
[data-surface] button.section-heading__action:hover .section-heading__action-label {
  text-decoration-color: currentcolor;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.section-heading__rule {
  border: 0;
  border-top: var(--border-hairline) solid var(--border-soft);
  margin: var(--space-md) 0 0;
}

/* A DEVIATION from the component, and the same shape of move the rose text
   tokens make one line up in §1 — a hairline drawn INSIDE a surface steps up a
   stop, because it has to clear that surface's own fill.

   A card's border only has to clear what sits BEHIND the card, so --border-soft
   is right there. This rule has nothing behind it. Measured in dark,
   --border-soft is tuned for the page ground (1.32:1) and collapses off it:
   1.11:1 on the rose band and 1.01:1 on --shell-2, which is the green band —
   invisible, with the heading above it looking perfectly normal. --border-strong
   is what the page ground's soft line measures like from a surface: 1.47:1 /
   1.63:1 in dark, 1.48:1 / 1.50:1 in light, against 1.32:1 / 1.33:1 for the
   untinted sections. So every section rule on the page now reads at one weight
   instead of three. Worth pushing upstream. */
[data-surface] .section-heading__rule {
  border-top-color: var(--border-strong);
}

/* --- Text link ------------------------------------------------------------ */

/* On an elevated surface the color steps back to the ink, so the rose accent
   is carried by the underline alone. That is why the decoration color is a
   rose stop rather than currentcolor. */
.text-link {
  color: var(--text-link);
  font-weight: var(--weight-medium);
  text-decoration-color: var(--rose-300);
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color var(--dur-fast) var(--ease-out), text-decoration-color var(--dur-fast) var(--ease-out);
}

.text-link:hover {
  color: var(--text-link-hover);
  text-decoration-color: var(--rose-500);
}

.text-link--quiet {
  text-decoration-line: none;
}

.text-link--quiet:hover {
  text-decoration-line: underline;
}

.text-link__external {
  font-size: 0.85em;
  margin-left: 0.25em;
  opacity: 0.7;
}

.back-link {
  color: var(--text-link);
  display: inline-block;
  font-size: var(--text-sm);
  margin-bottom: var(--space-lg);
  text-decoration: none;
}

.back-link:hover {
  color: var(--text-link-hover);
}

/* --- Media placeholder ---------------------------------------------------- */

/* No imagery was supplied with the design; gaps are labeled, not filled. */
.media-placeholder {
  border: var(--border-base) dashed var(--ink-4);
  border-radius: var(--radius-md);
  color: var(--ink-3);
  display: grid;
  font-size: var(--text-sm);
  line-height: 1.6;
  padding: var(--space-md);
  place-items: center;
  text-align: center;
}

/* --- Lede ----------------------------------------------------------------- */

.lede {
  color: var(--ink-3);
  font-size: var(--text-lg);
  font-weight: var(--weight-light);
  line-height: var(--leading-normal);
  margin: 0 0 var(--space-xl);
  max-width: 56ch;
}

.lede--spacious {
  margin-bottom: var(--space-2xl);
}

/* --- Post card ------------------------------------------------------------ */

.post-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.post-card__title {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  margin: 0;
}

.post-card__summary {
  color: var(--text-body);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  margin: 0;
}

.post-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: 2px;
}

.post-card__cta {
  color: var(--text-link);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  margin-top: auto;
  padding-top: var(--space-sm);
}

/* --- Post list item ------------------------------------------------------- */

.post-list-item {
  align-items: baseline;
  border-bottom: var(--border-hairline) solid var(--border-soft);
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: 10.5rem 1fr;
  padding-bottom: var(--space-lg);
  padding-top: var(--space-lg);
}

.post-list-item__rail {
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
}

.post-list-item__reading-time {
  margin-top: 2px;
  opacity: 0.8;
}

.post-list-item__title {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  margin: 0;
}

.post-list-item__link {
  color: var(--ink);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}

.post-list-item:hover .post-list-item__link {
  color: var(--text-link);
}

.post-list-item__summary {
  color: var(--text-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  margin: 6px 0 0;
  max-width: 60ch;
}

.post-list-item__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}

/* --- Post header ---------------------------------------------------------- */

.post-header {
  max-width: var(--measure-prose);
}

.post-header__title {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  margin: var(--space-sm) 0 0;
  text-wrap: balance;
}

.post-header__standfirst {
  color: var(--ink-3);
  font-family: var(--font-ui);
  font-size: var(--text-lg);
  font-weight: var(--weight-light);
  line-height: var(--leading-normal);
  margin: var(--space-md) 0 0;
  max-width: 52ch;
}

.post-header__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-lg);
}

.post-header__rule {
  border: 0;
  border-top: var(--border-hairline) solid var(--border-soft);
  margin: var(--space-xl) 0 0;
}

.read-similar {
  border-top: var(--border-hairline) solid var(--border-soft);
  margin: var(--space-3xl) 0 0;
  padding-top: var(--space-xl);
}

/* --- Prose ---------------------------------------------------------------- */

/* Long-form reading is the product, so the article styles get one owner. */
.prose {
  color: var(--text-body);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  max-width: var(--measure-prose);
}

/* No :first-child margin reset here. The leading paragraph's own margin is
   what separates the body from the post header rule above it. */
.prose > * + * {
  margin-top: var(--stack-flow);
}

.prose h2 {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  margin-top: 2em;
}

.prose h3 {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  margin-top: 1.6em;
}

.prose a {
  color: var(--text-link);
  text-decoration-color: var(--rose-300);
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.prose a:hover {
  color: var(--text-link-hover);
  text-decoration-color: var(--rose-500);
}

.prose strong {
  color: var(--ink);
  font-weight: var(--weight-semibold);
}

.prose em {
  font-style: italic;
}

.prose ul,
.prose ol {
  padding-left: 1.4em;
}

.prose li + li {
  margin-top: 0.4em;
}

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

/* Scoped off .pull-quote__text so a display quote keeps its own treatment. */
.prose blockquote:not(.pull-quote__text) {
  background-color: var(--shell-2);
  border-radius: var(--radius-md);
  color: var(--ink-2);
  font-style: italic;
  margin: 1.8em 0;
  padding: var(--space-md) var(--space-lg);
}

/* Inline code only — a code panel owns its own colors. */
.prose :not(pre) > code {
  background-color: var(--shell-2);
  border-radius: var(--radius-xs);
  color: var(--rose-700);
  font-size: 0.86em;
  padding: 0.1em 0.36em;
}

.prose hr {
  border: 0;
  border-top: var(--border-hairline) solid var(--border-soft);
  margin: 2.6em 0;
}

.prose img {
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  max-width: 100%;
}

.prose mark {
  background-color: var(--mark-highlight);
  border-radius: var(--radius-xs);
  color: var(--ink);
  padding: 0.05em 0.2em;
}

/* --- Pull quote ----------------------------------------------------------- */

/* Declared after .prose so its margins win over the prose stack rhythm. */
.pull-quote {
  margin: var(--space-2xl) 0;
  max-width: 34ch;
}

.pull-quote--right {
  margin-left: auto;
}

.pull-quote__text {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-light);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  margin: 0;
}

.pull-quote__text::before {
  color: var(--rose-300);
  content: "\201C";
}

.pull-quote__text::after {
  color: var(--rose-300);
  content: "\201D";
}

.pull-quote__attribution {
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  margin-top: var(--space-sm);
}

/* Non-breaking space: a trailing literal space in `content` is dropped. */
.pull-quote__attribution::before {
  content: "\2014\00A0";
}

/* --- Code block ----------------------------------------------------------- */

.code-block {
  margin: var(--space-lg) 0;
}

.code-block__filename {
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  margin-bottom: 6px;
}

.code-block__pre {
  background-color: var(--surface-code);
  border: 0;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  color: var(--bone);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.65;
  margin: 0;
  overflow-x: auto;
  padding: var(--space-md) var(--space-lg);
}

.code-block__language {
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  margin-top: 6px;
}

/* Syntax highlighting — an ADDITION to the design system, which ships no syntax
   palette: --surface-code is its only code token and its CodeBlock is a plain
   <pre>. _plugins/rouge_code_blocks.rb runs each panel through Rouge and these
   are the classes Rouge emits.

   ONE palette, no dark-mode block, and its OWN stops rather than semantic aliases
   — all of it follows from the same fact: the panel is dark in BOTH themes.
   --surface-code is --green-ground in light and a darkened mix of it in dark, and
   .code-block__pre already sets color: var(--bone), a raw value. Every semantic
   alias inverts with the theme, so --ink here would be dark green on a dark panel
   in light mode, and --rose-300 would dim toward the ground in dark. The stops
   below are theme-stable, which is the whole reason they are declared here and
   not in §1 — they are not the site's palette, they are this panel's.

   They are also deliberately LOUD. The rest of the site is a quiet 90/10, but a
   code panel is the one surface that is dark in both themes and carries no prose,
   so it can take saturation the page never could without a third hue leaking into
   the design. These are the two hues pushed to the top of their range against a
   near-black ground, not new ones: rose for the language's own words, green for
   the data, warm neutrals for everything the author named.

   That also settles what the earlier palette could not. It ruled the greens out
   because --green-sage reaches 2.36:1 here — true of that stop, but it was a stop
   picked to be SECONDARY TEXT ON PAPER. A green chosen for this ground instead is
   the brightest thing on the panel. Measured against the LIGHT theme's panel
   (#12241C); the dark theme's is a 34% mix toward black, strictly darker, so every
   figure is a floor and not a midpoint:

     --bone           12.18:1  names the author writes: selectors, properties,
                               functions, variables — the largest family, and the
                               only one left neutral so the rest can shout. It is
                               the one role with no --code-* stop of its own,
                               because it needs no new value: it IS the panel's
                               own color, set above, and aliasing that to a
                               --code-name nothing else read only put one
                               selector's declarations in two places.
     --code-number    12.67:1  numbers
     --code-string     9.56:1  strings and their delimiters
     --code-punct      6.48:1  punctuation and operators
     --code-keyword    6.15:1  reserved words, at-rules, decorators, builtins
     --code-comment    5.38:1  commentary, italic

   The split is by authorship, unchanged: the author's names are neutral, the
   language's own words are rose, the data is green, the scaffolding recedes.

   Note that the first of those roles has no selectors below and is the largest:
   the whole name family — .n .na .nc .nf .nl .nt .nv .nx .py — is deliberately
   unstyled and inherits .code-block__pre's own color. Rouge's default is the
   design's default, so writing those rules out would only restate the cascade.

   The lists below cover Rouge's token families rather than only the classes this
   corpus happens to emit, so adding a language needs no CSS. Several therefore have
   no panel behind them today; that is intent, not oversight. */
.code-block__pre {
  --code-keyword: #FF6E9C;
  --code-string: #3FE0A0;
  --code-number: #C8F274;
  --code-punct: #93A99B;
  --code-comment: #7C9C88;
}

/* Reserved words, at-rules, decorators, doctype and other preproc directives —
   and the builtin values a language supplies, CSS's `absolute`, JS's `console`,
   which the earlier palette filed with the literals. They belong here: a builtin
   is a word the language owns, not data the author typed, and on a CSS panel it
   is most of what keeps the rose half of the palette on screen at all. */
.code-block__pre .k,
.code-block__pre .kc,
.code-block__pre .kd,
.code-block__pre .kn,
.code-block__pre .kp,
.code-block__pre .kr,
.code-block__pre .kt,
.code-block__pre .nd,
.code-block__pre .cp,
.code-block__pre .nb {
  color: var(--code-keyword);
}

/* Strings and their delimiters. */
.code-block__pre .s,
.code-block__pre .s1,
.code-block__pre .s2,
.code-block__pre .sb,
.code-block__pre .sc,
.code-block__pre .sd,
.code-block__pre .se,
.code-block__pre .sh,
.code-block__pre .si,
.code-block__pre .sr,
.code-block__pre .ss,
.code-block__pre .sx,
.code-block__pre .dl {
  color: var(--code-string);
}

/* Numbers, split off the strings above — which the earlier palette grouped them
   with, having only one stop to spend on both. At this saturation there is room
   in the green half to tell a quoted value from a counted one. */
.code-block__pre .l,
.code-block__pre .ld,
.code-block__pre .m,
.code-block__pre .mb,
.code-block__pre .mf,
.code-block__pre .mh,
.code-block__pre .mi,
.code-block__pre .mo {
  color: var(--code-number);
}

/* Scaffolding: punctuation and operators carry no meaning worth a hue, and they
   outnumber every other token several times over — a color here would only shout.
   Dimmer than the neutral the names take, which is what buys the neon its room. */
.code-block__pre .p,
.code-block__pre .pi,
.code-block__pre .o,
.code-block__pre .ow {
  color: var(--code-punct);
}

.code-block__pre .c,
.code-block__pre .c1,
.code-block__pre .cd,
.code-block__pre .cm,
.code-block__pre .cs {
  color: var(--code-comment);
  font-style: italic;
}

.code-block__pre .gh,
.code-block__pre .gu {
  font-weight: 600;
}

/* Rouge's .err is deliberately absent, not forgotten. It means "my lexer did not
   recognize this", NOT "this code is wrong", and every panel that produces it does so
   at the lexer's limit rather than the author's: Rouge's CSS lexer predates CSS
   Nesting, and its JSON lexer wants a whole document where a post shows a fragment.
   Left unstyled it inherits --bone with the names and reads as ordinary code, which
   is what it is.
   There is no color to spare for an alarm in any case — a red would be a third hue. */

/* --- Pagination ----------------------------------------------------------- */

/* An ADDITION to the design system, which ships no pager — built from its own
   tokens the way the icons were, and borrowing two settled pairings rather than
   inventing a third: the steps are styled as .section-heading__action, and the
   current number reuses .tag--rose's --rose-700 on --rose-100, which the system
   already carries at AA in both themes.

   Three columns rather than a flex row, so the numbers stay centered on the page
   whether or not both steps are present — page 1 has no Newer, the last has no
   Older, and either would drag a space-between row off center.

   No rule of its own: .post-list-item already carries a border-bottom, so the
   list hands the pager one, and adding a second drew two hairlines with an empty
   band between them. */
.pagination {
  align-items: center;
  display: grid;
  gap: var(--space-md);
  grid-template-columns: 1fr auto 1fr;
  margin-top: var(--space-xl);
}

.pagination__step {
  color: var(--text-link);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  white-space: nowrap;
}

.pagination__step:hover {
  color: var(--text-link-hover);
}

.pagination__step--newer {
  grid-column: 1;
  justify-self: start;
}

.pagination__step--older {
  grid-column: 3;
  justify-self: end;
}

.pagination__pages {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  grid-column: 2;
  justify-content: center;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Quiet until hovered, the way .site-nav__link is: a whole row of rose numerals
   would spend the page's accent budget on chrome. tabular-nums keeps the pills
   from resizing between 1 and 11. */
.pagination__page {
  border-radius: var(--radius-pill);
  color: var(--ink-2);
  display: inline-flex;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  justify-content: center;
  line-height: 1;
  min-width: 2.25rem;
  padding: 9px 10px;
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}

/* --shell-2, not --shell-3: the pill rests on the page ground, not on a card,
   and --shell-2 is a step off --shell in both themes. (--shell-3 is what the
   outline button needs, because that one rests on --card.) */
.pagination__page:hover {
  background-color: var(--shell-2);
  color: var(--text-link);
}

.pagination__page--current {
  background-color: var(--rose-100);
  color: var(--rose-700);
  font-weight: var(--weight-medium);
}

.pagination__gap {
  align-self: center;
  color: var(--ink-3);
  font-size: var(--text-sm);
  padding: 0 var(--space-2xs);
}


/* ==========================================================================
   10. Pages
   ========================================================================== */

/* --- Home ----------------------------------------------------------------- */

.hero__title {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 6.5vw, var(--text-4xl));
  font-weight: var(--weight-light);
  letter-spacing: -0.02em;
  line-height: 1.06;
  margin: 0;
  max-width: 24ch;
  text-wrap: balance;
}

.hero__lede {
  color: var(--ink-3);
  font-size: var(--text-lg);
  font-weight: var(--weight-light);
  line-height: var(--leading-normal);
  margin: var(--space-lg) 0 0;
  max-width: 52ch;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-xl);
}

/* The page's closing band. It is a full peer of the two sections above it — the
   same .section-heading, the same 34px title, the same rule under it — so there
   is no .hello__title: at that size the block was character-for-character
   .section-heading__title, and a duplicate is a second place to edit rather than
   a different heading. Like .hero, the block itself carries no declarations:
   .band__inner already sizes the column and .band__more already spaces a band's
   closing button. Only the lede is its own.

   --measure-narrow's first use on the site: two sentences want the system's own
   narrow measure, not the 52ch the hero lede runs to. */
.hello__lede {
  color: var(--ink-2);
  font-size: var(--text-md);
  line-height: var(--leading-normal);
  margin: var(--space-sm) 0 0;
  max-width: var(--measure-narrow);
}

/* --- Post page ------------------------------------------------------------ */

/* An ADDITION to the design system, which sizes one screen rather than a
   3,000-word essay. Everything here is scoped to .site-main--post, so the shared
   components it restyles — .eyebrow, .tag, .code-block, .post-list-item — are
   untouched on every other page. Two things are going on:

   THE COLUMN. The post takes .site-main's own --width-wide rather than the 44rem
   --width-content a standard page takes, so its box is the one the header and the
   footer already sit in. --measure-post is narrower than that box, and the
   surplus is spent by exactly one thing: a blockquote, which reaches the column's
   content edge and therefore lands flush with the wordmark above it. Everything
   else is capped at the measure and centered, so the page still reads as a single
   column and the quote is the only thing that ever breaks out of it.

   --post-bleed is that surplus, halved — derived rather than picked, which is why
   the quote lands flush instead of at an invented width, and why it collapses to
   0 on its own once the viewport stops being wider than the measure. That is why
   there is no breakpoint for it below.

   The measure is rem and not the system's ch-based --measure-prose because the
   bleed is arithmetic across three elements at three different font sizes, and
   `ch` resolves against each one separately. 52rem is 68ch at the body size set
   below — the same measure in characters the rest of the site reads at, longer in
   pixels only because the type is.

   THE SCALE. Each size is the system's own step multiplied by ~1.12 and rounded
   to a whole pixel, so the ratios between them stay the ones the system set.

   Two of the results land on a value the system already has a token for — 17px is
   --text-base and 13px is --text-xs — and both are still written out here rather
   than referencing those tokens, on purpose. The rule that generates them is "one
   step up from --text-sm / --text-2xs", not "equal to --text-base / --text-xs";
   the equality is a coincidence of where the steps happen to fall. Reference the
   token and a future retune of --text-base silently drags the post's meta size
   with it, which is the one relationship that is NOT what this ramp means. */
.site-main--post {
  --post-text-caption: 0.8125rem; /* 13 — the system's --text-2xs is 12 */
  --post-text-tag: 0.875rem;      /* 14 — the system's --text-xs  is 13 */
  --post-text-meta: 1.0625rem;    /* 17 — the system's --text-sm  is 15 */
  --post-text-body: 1.1875rem;    /* 19 — the system's --text-base is 17 */
  --post-text-lede: 1.5rem;       /* 24 — the system's --text-lg  is 21 */
  --post-text-h3: 1.8125rem;      /* 29 — the system's --text-xl  is 26 */
  --post-text-h2: 2.375rem;       /* 38 — the system's --text-2xl is 34 */
  --post-text-h1: 3.125rem;       /* 50 — the system's --text-3xl is 44 */

  --post-bleed: max(0rem, calc((min(100vw, var(--width-wide)) - var(--gutter) * 2 - var(--measure-post)) / 2));
}

.site-main--post .post-header,
.site-main--post .prose,
.site-main--post .read-similar {
  margin-left: auto;
  margin-right: auto;
  max-width: var(--measure-post);
}

/* The back link stays inline so it keeps its own hit area rather than
   blockifying to the full measure; it is nudged to the measure's left edge. */
.site-main--post .back-link {
  margin-left: var(--post-bleed);
}

/* The one thing that spends the column's surplus. The child combinator is what
   keeps a display quote out of it: .pull-quote__text is a blockquote too, but it
   sits inside a <figure> and so is a grandchild. Same specificity as the .prose
   rule in §9 that sets the top and bottom margins, and later, so only the inline
   pair moves. */
.site-main--post .prose > blockquote {
  margin-left: calc(var(--post-bleed) * -1);
  margin-right: calc(var(--post-bleed) * -1);
}

/* A display quote stays on the measure with the prose — it is a moment inside the
   reading column, not a break out of it, and it carries no fill to mark an edge
   with the way a plain blockquote does. What it does drop is the component's own
   34ch: that is a measure for a quote set BESIDE prose, which is what
   .pull-quote--right is for, and it left the block ending well short of the text
   on either side of it. --right still works, there being 52rem to sit in. */
.site-main--post .prose > .pull-quote {
  max-width: none;
}

.site-main--post .eyebrow,
.site-main--post .back-link,
.site-main--post .callout__label,
.site-main--post .callout__body,
.site-main--post .code-block__filename,
.site-main--post .code-block__pre,
.site-main--post .post-list-item__rail,
.site-main--post .pull-quote__attribution {
  font-size: var(--post-text-meta);
}

.site-main--post .code-block__language {
  font-size: var(--post-text-caption);
}

.site-main--post .tag {
  font-size: var(--post-text-tag);
}

.site-main--post .prose,
.site-main--post .post-list-item__summary {
  font-size: var(--post-text-body);
}

.site-main--post .post-header__standfirst {
  font-size: var(--post-text-lede);
}

.site-main--post .prose h3,
.site-main--post .post-list-item__title {
  font-size: var(--post-text-h3);
}

.site-main--post .prose h2,
.site-main--post .pull-quote__text {
  font-size: var(--post-text-h2);
}

.site-main--post .post-header__title {
  font-size: var(--post-text-h1);
}

/* --- Projects ------------------------------------------------------------- */

.project-card {
  display: flex;
  flex-direction: column;
}

.project-card__media {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  margin-bottom: var(--space-md);
}

.project-card__title {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  letter-spacing: -0.01em;
  line-height: 1.2;
  margin: 6px 0 var(--space-sm);
}

.project-card__blurb {
  color: var(--ink-2);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  margin: 0 0 var(--space-md);
}

.project-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-bottom: var(--space-md);
}

.project-card__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: auto;
}

/* The graveyard is separated from the live grid by space and its own heading
   rule, not by a band. `.site-footer` is already `--surface-sunken` and carries
   a `margin-top`, so a sunken band here would put a stripe of page ground
   between two identical fills — three stripes where the page wanted one break. */
.graveyard {
  padding-top: var(--space-3xl);
}

/* --- Project list item ---------------------------------------------------- */

/* A fourth addition to the system, which ships no row for a project: this is
   `PostListItem`'s row, hairline and rhythm carrying a project instead of a
   post. It keeps that block's `border-bottom` and vertical padding and drops
   everything the data doesn't fill — a project has no date and no reading
   time, so there is nothing for the 10.5rem rail to hold and the row is a
   single column at every width. The title is not a link either, the collection
   emitting no pages, and the row ends in a links slot a post row has no use
   for. Those take `.section-heading__action`, the settled small-link pairing
   the pager already borrowed for Newer/Older, rather than inventing a third. */
.project-list-item {
  border-bottom: var(--border-hairline) solid var(--border-soft);
  padding-bottom: var(--space-lg);
  padding-top: var(--space-lg);
}

.project-list-item__title {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  margin: 0;
}

.project-list-item__summary {
  color: var(--text-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  margin: 6px 0 0;
  max-width: 60ch;
}

.project-list-item__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}

.project-list-item__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

/* --- About ---------------------------------------------------------------- */

.about {
  align-items: start;
  display: grid;
  gap: var(--space-3xl);
  grid-template-columns: minmax(0, 1fr) 17rem;
}

.about__aside {
  display: grid;
  gap: var(--space-lg);
  position: sticky;
  top: 5.5rem;
}

/* One rule for both states: the <img> when `portrait:` is set, the dashed
   placeholder until it is. Sizing them together is what keeps the column from
   reflowing when the real image lands — and `object-fit: cover` means the file
   dropped in needs no cropping first, whatever shape it arrives in. */
.about__portrait {
  aspect-ratio: 1;
  border-radius: var(--radius-lg);
  object-fit: cover;
  width: 100%;
}

.about__details {
  display: grid;
  font-size: var(--text-sm);
  gap: var(--space-sm);
}

.about__detail {
  display: grid;
  gap: 2px;
}

.about__label {
  color: var(--ink-3);
}

.about__value {
  color: var(--ink-2);
}

.about__topics {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

/* The way out of the topics the sidebar had no room for. */
.about__more {
  display: inline-block;
  font-size: var(--text-sm);
  margin-top: var(--space-sm);
}

/* --- Topics --------------------------------------------------------------- */

/* The whole tag vocabulary as one wrapped run of pills. The row shape is not new:
   PostHeader and PostListItem both lay their tags out as a wrapped flex row with
   --space-xs between, and this is that row with a list's own defaults cleared off
   it. Nothing here sets a width — at 68rem the 30 pills settle into a block a
   few lines deep, which is shorter than any column arrangement of the same set. */
.tag-index {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* --- Standalone page ------------------------------------------------------ */

.page__title {
  font-size: var(--text-3xl);
  margin-bottom: var(--space-xl);
  text-wrap: balance;
}


/* ==========================================================================
   11. Media queries
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-instant: 0ms;
    --dur-fast: 0ms;
    --dur-base: 0ms;
    --dur-slow: 0ms;
    --lift: 0px;
  }
}

/* The About sidebar is 17rem plus a 4.5rem gap; below this it starves the
   prose column, so it drops underneath instead. */
@media (max-width: 54rem) {
  .about {
    gap: var(--space-2xl);
    grid-template-columns: minmax(0, 1fr);
  }

  .about__aside {
    position: static;
  }

  /* The portrait is square, sized off the column it normally sits in, so once the
     aside stops being a column it has to be told that width by hand — the 17rem
     above, not a second number. */
  .about__portrait {
    max-width: 17rem;
  }
}

@media (max-width: 40rem) {
  .site-header__inner {
    flex-wrap: wrap;
    gap: var(--space-sm);
  }

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

/* The 10.5rem date rail leaves too little measure for the title on a phone. */
@media (max-width: 36rem) {
  .post-list-item {
    gap: var(--space-xs);
    grid-template-columns: minmax(0, 1fr);
  }

  /* Numbers move above the steps and take the full width to wrap into, rather
     than being hidden: prev/next alone can't reach page 12 of an archive. */
  .pagination {
    grid-template-columns: 1fr 1fr;
    row-gap: var(--space-lg);
  }

  .pagination__pages {
    grid-column: 1 / -1;
    grid-row: 1;
  }

  .pagination__step--newer,
  .pagination__step--older {
    grid-row: 2;
  }

  .pagination__step--older {
    grid-column: 2;
  }
}
