/* ==========================================================================
   Fonts — self-hosted variable woff2, latin subset (covers æ ø å).

   Paths are absolute because this file is served inside the bundle at
   /dist/*.css, so relative URLs would resolve against /dist/.
   `font-src 'self'` in public/_headers already permits these; no external
   request is made, and the CSP needs no change.

   The italic face is a separate @font-face, so the browser only downloads it
   on pages that actually set italic text.
   ========================================================================== */

@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 300 700;
  font-stretch: normal;
  font-display: swap;
  src: url("/fonts/source-serif-4-latin.woff2") format("woff2");
}

@font-face {
  font-family: "Source Serif 4";
  font-style: italic;
  font-weight: 300 700;
  font-stretch: normal;
  font-display: swap;
  src: url("/fonts/source-serif-4-latin-italic.woff2") format("woff2");
}

@font-face {
  font-family: Archivo;
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 75% 100%;
  font-display: swap;
  src: url("/fonts/archivo-latin.woff2") format("woff2");
}
/* ==========================================================================
   Tokens
   Three tiers, in dependency order:
     1. Primitives — the raw palette and scales. Never referenced by components.
     2. Semantic   — what a value MEANS here. This is the layer components use.
     3. Scales     — spacing, type, radius, layout.
   Components must consume tier 2 and 3 only. A component referencing a
   primitive is a bug: it cannot then be re-themed for dark mode.
   ========================================================================== */

/* --- 1. Primitives -------------------------------------------------------- */
:root {
  --white: #fff;
  --paper: #f7f8fa; /* cool paper — deliberately not cream */
  --gray-20: #e0e0e0;
  --gray-40: #ccc;
  --gray-50: #c0c0c0;
  --gray-60: #9aa7b4;
  --gray-70: #555;
  --gray-85: #dad8d8;
  --gray-90: #333;

  /* Dark surfaces, low to high elevation */
  --ink-900: #15202b;
  --ink-800: #1c2b3a;
  --ink-700: #26384a;
  --ink-600: #374b60;
  --ink-500: #4a6076;

  /* The single accent. One hue carries link, focus and selection in both
     themes; hue variety is reserved for the callouts, where it is semantic. */
  --indigo-800: #241d70;
  --indigo-700: #3730a3;
  --indigo-muted: #5b4d8f;
  --indigo-400: #a5a5fb;
  --indigo-300: #d4d4ff;
  --indigo-muted-dark: #bfb0e6;

  /* Callout families: a saturated title bar and a pale body, light and dark */
  --green-50: #97d5a4;
  --green-90: #ebf7ec;
  --green-700: #2a6039;
  --green-900: #1b3324;

  --sky-50: #9ec5e8;
  --sky-90: #eaf2fa;
  --sky-700: #27506f;
  --sky-900: #1b3040;

  --amber-50: #f0d08a;
  --amber-90: #fdf4e0;
  --amber-700: #5e4820;
  --amber-900: #3a2e14;
}

/* --- 2. Semantic ---------------------------------------------------------- */
:root {
  color-scheme: light;

  /* Three type roles. Display signposts, serif reads, mono labels the
     furniture — dates, tags, captions — because the subject is code.
     Role-named, so they live here: components consume these, never a
     font stack of their own. */
  --font-display: Archivo, "Helvetica Neue", Arial, sans-serif;
  --font-body: "Source Serif 4", Georgia, "Times New Roman", serif;

  /* The voice of the furniture — dates, tags, nav, captions, footer.
     Pointed at the display face rather than mono: monospace is the strongest
     "developer" signal in typography, and on this many small elements it made
     a writer's site read as a terminal. Repoint this one line to
     var(--font-mono) to get the old treatment back everywhere at once. */
  --font-meta: var(--font-display);
  --font-mono:
    SFMono-Regular, Consolas, Menlo, Monaco, "Andale Mono", "Liberation Mono",
    "Courier New", monospace;

  --surface: var(--paper); /* page background */
  --surface-raised: var(--white); /* cards sitting on the page */
  --surface-sunken: var(--gray-20); /* chips, wells */

  --border: var(--gray-40); /* solid component borders */
  --border-subtle: var(--gray-20); /* dashed structural rules */
  --border-strong: var(--gray-50); /* emphasised borders */

  --text: var(--gray-90);
  --text-muted: var(--gray-70);

  --link: var(--indigo-700);
  --link-active: var(--indigo-800);
  --link-visited: var(--indigo-muted);

  --focus: var(--indigo-700); /* keyboard focus ring */
  --selection: var(--indigo-700);
  --selection-text: var(--white);

  /* Callout accents, one pair per type: title bar / body */
  --accent-info: var(--green-50);
  --accent-info-subtle: var(--green-90);
  --accent-note: var(--sky-50);
  --accent-note-subtle: var(--sky-90);
  --accent-warning: var(--amber-50);
  --accent-warning-subtle: var(--amber-90);
}

/* --- 3. Scales ------------------------------------------------------------ */
:root {
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;

  /* Fluid type scale, 320px → 1280px viewport. Every size on the site comes
     from a step; nothing else is allowed to hardcode a font-size. */
  --step--2: 0.75rem; /* mono captions, fixed */
  --step--1: 0.875rem; /* metadata */
  --step-0: clamp(1.0625rem, 1.0417rem + 0.104vw, 1.125rem); /* body */
  --step-1: clamp(1.25rem, 1.2083rem + 0.208vw, 1.375rem); /* h4, lead */
  --step-2: clamp(1.5rem, 1.4167rem + 0.417vw, 1.75rem); /* h3 */
  --step-3: clamp(1.875rem, 1.75rem + 0.625vw, 2.25rem); /* h2 */
  --step-4: clamp(2.25rem, 2rem + 1.25vw, 3rem); /* h1 */
  --step-5: clamp(3rem, 2.5rem + 2.5vw, 4.5rem); /* display */

  --leading-tight: 1.1; /* display */
  --leading-snug: 1.2; /* headings */
  --leading-ui: 1.4; /* metadata, nav */
  --leading-body: 1.65; /* serif prose */

  --tracking-display: -0.02em;
  --tracking-mono: 0.02em;

  --radius-sm: 0.25rem;
  --radius: 0.5rem;

  /* Layout. `--measure` is the reading column; `--measure-wide` the breakout
     track that tables, code and figures escape into. See layout.css. */
  --measure: 34rem;
  --measure-wide: 52rem;
  --gutter: var(--space-4);

  --syntax-tab-size: 2;
}

/* --- Dark theme ----------------------------------------------------------
   Overrides the semantic tier only — never the primitives, never a component.
   If a component needs a rule here, it is reaching past the token layer.
   Keep this the ONLY prefers-color-scheme block: tests/e2e/theme-contrast
   splits the bundle at the first one to resolve the two token sets.

   The theme resolves in three ways, and the cascade sorts them out by
   specificity rather than by order:

     no [data-theme]        → the media query below follows the OS
     [data-theme="dark"]    → forced dark  (0,2,0 beats the media query's :root)
     [data-theme="light"]   → forced light (the :not() below stops matching)

   Forcing light needs no second copy of the light values: the media query
   simply excludes itself. Only the dark set is written twice, and
   tests/e2e/theme-contrast asserts the two copies stay identical.
   -------------------------------------------------------------------------- */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --surface: var(--ink-900);
    --surface-raised: var(--ink-800);
    --surface-sunken: var(--ink-700);

    --border: var(--ink-600);
    --border-subtle: var(--ink-700);
    --border-strong: var(--ink-500);

    --text: var(--gray-85);
    --text-muted: var(--gray-60);

    --link: var(--indigo-400);
    --link-active: var(--indigo-300);
    --link-visited: var(--indigo-muted-dark);

    --focus: var(--indigo-400);
    --selection: var(--indigo-400);
    --selection-text: var(--ink-900);

    --accent-info: var(--green-700);
    --accent-info-subtle: var(--green-900);
    --accent-note: var(--sky-700);
    --accent-note-subtle: var(--sky-900);
    --accent-warning: var(--amber-700);
    --accent-warning-subtle: var(--amber-900);
  }
}

/* The manual dark theme. Byte-for-byte the same declarations as the media
   query above — kept in sync by a test, because CSS has no way to name a
   declaration block and reuse it under a second selector. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --surface: var(--ink-900);
  --surface-raised: var(--ink-800);
  --surface-sunken: var(--ink-700);

  --border: var(--ink-600);
  --border-subtle: var(--ink-700);
  --border-strong: var(--ink-500);

  --text: var(--gray-85);
  --text-muted: var(--gray-60);

  --link: var(--indigo-400);
  --link-active: var(--indigo-300);
  --link-visited: var(--indigo-muted-dark);

  --focus: var(--indigo-400);
  --selection: var(--indigo-400);
  --selection-text: var(--ink-900);

  --accent-info: var(--green-700);
  --accent-info-subtle: var(--green-900);
  --accent-note: var(--sky-700);
  --accent-note-subtle: var(--sky-900);
  --accent-warning: var(--amber-700);
  --accent-warning-subtle: var(--amber-900);
}
/* ==========================================================================
   Base — element defaults plus the two site-wide utilities (.flow, .cluster).
   No components, no page-specific placement. Consumes tokens.css.
   ========================================================================== */

* {
  box-sizing: border-box;
}

html,
body {
  padding: 0;
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: var(--leading-body);
  font-optical-sizing: auto; /* Source Serif 4 carries an opsz axis */
  color: var(--text);
  background-color: var(--surface);
  -webkit-font-smoothing: antialiased;
}
html {
  overflow-y: scroll;
}

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

/* --- Vertical rhythm -----------------------------------------------------
   One rule owns the space between stacked things. Elements tune their own
   lead by setting --flow-space; nothing sets a bare margin-top. Components
   therefore never need to know what sits above them.
   -------------------------------------------------------------------------- */
.flow > * + * {
  margin-block-start: var(--flow-space, var(--space-5));
}

/* --- Headings ------------------------------------------------------------- */
h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
  margin: 0;
  clear: both; /* clear the floated aside rather than wrapping around it */
}
h1 {
  font-size: var(--step-4);
  line-height: var(--leading-tight);
  font-stretch: 95%;
}
h2 {
  font-size: var(--step-3);
  --flow-space: var(--space-8);
}
h3 {
  font-size: var(--step-2);
  --flow-space: var(--space-7);
}
h4 {
  font-size: var(--step-1);
  --flow-space: var(--space-6);
}
h5,
h6 {
  font-size: var(--step-0);
  --flow-space: var(--space-6);
}

/* Copy following a heading belongs to it — pull it up close. */
h1 + *,
h2 + *,
h3 + *,
h4 + *,
h5 + *,
h6 + * {
  --flow-space: var(--space-3);
}

p,
ul,
ol,
blockquote,
figure {
  margin: 0;
}
li + li {
  margin-block-start: var(--space-2);
}
ul,
ol {
  padding-inline-start: var(--space-5);
}

blockquote {
  border-inline-start: 2px solid var(--border-strong);
  padding-inline-start: var(--space-4);
  font-style: italic;
  color: var(--text-muted);
}

/* Serif italic rather than a small sans: a caption is an aside to the prose,
   and on a reading site it should look like it belongs to the text. */
figcaption {
  font-family: var(--font-body);
  font-style: italic;
  font-size: var(--step--1);
  line-height: var(--leading-ui);
  color: var(--text-muted);
  margin-block-start: var(--space-2);
}

hr {
  border: 0;
  border-top: 1px dashed var(--border-subtle);
  margin-block: var(--space-7);
}

/* --- Links ---------------------------------------------------------------
   Wrapped in :where() so these defaults carry ZERO specificity. `a[href]:visited`
   would otherwise score 0,2,1 and beat any single-class component rule — a
   component could set a link colour and silently lose the visited state.
   With :where(), one class is always enough to override.
   -------------------------------------------------------------------------- */
:where(a[href]) {
  color: var(--link);
  text-underline-offset: 0.15em;
  text-decoration-thickness: 1px;
}
:where(a[href]:visited) {
  color: var(--link-visited);
}
:where(a[href]:hover, a[href]:active) {
  color: var(--link-active);
  text-decoration-thickness: 2px;
}

/* --- Focus ---------------------------------------------------------------
   One visible ring for every interactive element. Keyboard users only, so it
   never appears on mouse click.
   -------------------------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

img {
  max-width: 100%;
  height: auto;
}

/* Tables are owned entirely by components/table.css. */

pre,
code {
  font-family: var(--font-mono); /* actual code stays mono */
}
code {
  font-size: 0.9em; /* mono runs large next to a serif; match the x-height */
  word-break: break-all;
}
pre:not([class*="language-"]) {
  margin: var(--space-2) 0;
  line-height: 1.375; /* 22px /16 */
  -moz-tab-size: var(--syntax-tab-size);
  -o-tab-size: var(--syntax-tab-size);
  tab-size: var(--syntax-tab-size);
  -webkit-hyphens: none;
  -ms-hyphens: none;
  hyphens: none;
  direction: ltr;
  text-align: left;
  white-space: pre;
  word-spacing: normal;
  word-break: normal;
}
pre code {
  font-size: inherit;
  word-break: normal;
}

/* --- Utilities ------------------------------------------------------------ */

/* A row of things that wraps: nav, tag lists, prev/next. */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  list-style: none;
  padding: 0;
  margin: 0;
}

/* Small labelling voice: dates, tags, captions, counters. */
.meta {
  font-family: var(--font-meta);
  font-size: var(--step--1);
  line-height: var(--leading-ui);
  color: var(--text-muted);
}

/* https://www.a11yproject.com/posts/how-to-hide-content/ */
.visually-hidden:not(:focus):not(:active) {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* The skip link is .visually-hidden until focused, then it must be seen. */
.skip-link:focus {
  position: absolute;
  z-index: 10;
  inset-block-start: var(--space-2);
  inset-inline-start: var(--space-2);
  padding: var(--space-2) var(--space-4);
  background-color: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-family: var(--font-meta);
  font-size: var(--step--1);
}

/* --- Motion ---------------------------------------------------------------
   The site is near-static; this is the floor for anything added later.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
/* ==========================================================================
   Layout — the page shell and the one layout primitive the site has.
   Consumes tokens.css.

   `.page-grid` is a three-track content grid:

     │ full                                                  │
     │      wide                                       wide  │
     │            ┌──── text (--measure) ────┐               │
     │            │  brødtekst               │               │
     │            └──────────────────────────┘               │

   Children land in `text` by default. Anything that needs room — tables,
   code listings, callouts — escapes into `wide`; a hero image takes `full`.
   No wrapper divs, and the outer tracks double as the responsive gutter,
   so there is no separate mobile padding rule to keep in sync.
   ========================================================================== */

.page-grid {
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [wide-start] minmax(0, calc((var(--measure-wide) - var(--measure)) / 2))
    [text-start] min(100% - var(--gutter) * 2, var(--measure)) [text-end]
    minmax(0, calc((var(--measure-wide) - var(--measure)) / 2)) [wide-end]
    minmax(var(--gutter), 1fr) [full-end];
}
/* The default placement, at ZERO specificity via :where(). This has to be the
   weakest rule in the system: every opt-out below — and every component that
   places itself — is a plain class (0,1,0) or an element selector (0,0,2), and
   both must win without anyone having to think about it. Written as
   `.page-grid > *` it scored 0,1,0 and silently beat `main > table`. */
:where(.page-grid) > * {
  grid-column: text;
}

/* Opt-outs, available to any child of the grid. */
.wide {
  grid-column: wide;
}
.full {
  grid-column: full;
}

/* Markdown emits these bare, with no class to hook, and they are the elements
   that most need the extra room on a site about code. Callouts are absent on
   purpose: they own a placement-modifier system, so components/callout.css
   sets their track. Two files setting grid-column on one element is how
   specificity fights start. */
main > table,
main > pre {
  grid-column: wide;
}

/* eleventy-img wraps every image in <picture>, so the grid item is the
   <picture> — a grid-column on the inner <img> would apply to nothing. */
main > picture {
  grid-column: wide;
}

/* --- Shell ---------------------------------------------------------------- */
main {
  padding-block: var(--space-7) var(--space-9);
}

header {
  padding-block: var(--space-5);
  border-bottom: 1px dashed var(--border-subtle);
}
.header-inner {
  display: flex;
  gap: var(--space-3) var(--space-6);
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
}

/* Nav and the theme toggle stay together when the header wraps. */
.header-tools {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
}

.home-link {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 700;
  font-stretch: 95%;
  letter-spacing: var(--tracking-display);
  color: var(--text);
}
.home-link:not(:hover) {
  text-decoration: none;
}

/* --- Nav ------------------------------------------------------------------
   Small and quiet: furniture rather than prose. Built on .cluster.
   -------------------------------------------------------------------------- */
.nav {
  font-family: var(--font-meta);
  font-size: var(--step--1);
}
.nav a[href]:not(:hover) {
  text-decoration: none;
}
.nav a[href][aria-current="page"] {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 0.3em;
}

/* --- Theme toggle --------------------------------------------------------
   Labelled with the theme it switches TO, so it reads as an action rather
   than a status. public/js/theme.js keeps the label and aria-label in sync.
   -------------------------------------------------------------------------- */
.theme-toggle {
  font-family: var(--font-meta);
  font-size: var(--step--1);
  line-height: var(--leading-ui);
  color: var(--text-muted);
  background-color: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
}
.theme-toggle:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

/* --- Footer --------------------------------------------------------------- */
.site-footer {
  padding-block: var(--space-6) var(--space-8);
  border-top: 1px dashed var(--border-subtle);
}
.site-footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4) var(--space-6);
  font-family: var(--font-meta);
  font-size: var(--step--1);
  color: var(--text-muted);
}
.site-footer-inner a[href]:not(:hover) {
  text-decoration: none;
}
/* ==========================================================================
   Tables.

   Markdown tables carry no class to hook, so these style the elements
   directly. This file is the only owner of table styling — base.css
   deliberately says nothing about tables.
   ========================================================================== */

table {
  width: 100%;
  margin: var(--space-4) 0;
  border-collapse: collapse;
  border: 1px solid var(--border-subtle);
}

th,
td {
  text-align: left;
  padding: var(--space-2);
  border-bottom: 1px solid var(--border-subtle);
  border-right: 1px solid var(--border-subtle);
}
/* ==========================================================================
   Post list — the card grid on the home page, archive and tag pages.
   Markup: _includes/postslist.njk

   Sits in the `wide` track (see layout.css) so two cards fit side by side
   while the surrounding prose stays at reading measure.
   ========================================================================== */

.postlist {
  grid-column: wide;
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
  gap: var(--space-4);
}

.postlist-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  background-color: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin: 0;
  transition:
    transform 0.15s ease,
    box-shadow 0.15s ease;
}

/* Hover lift. The shadow is a fixed ink-tinted rgba rather than a token
   because there is no shadow token; it reads as depth on both themes.
   The reduced-motion floor in base.css collapses the transition. */
.postlist-item:hover {
  transform: translateY(-3px);
  box-shadow: 0 10px 24px -12px rgba(20, 32, 43, 0.35);
}
/* A fixed ratio for every thumbnail. Left to their natural sizes the cards
   ran to wildly different heights and the grid looked accidental; cropping to
   a common landscape is what makes it read as a set. */
.postlist-item picture,
.postlist-item > a:first-child {
  display: block;
  width: 100%;
}
.postlist-item img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: var(--radius-sm);
}

/* Card titles sit in text colour, not link colour: a grid of a dozen indigo
   headings reads as noise. The accent returns on hover, where it means
   something. */
.postlist-link {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 700;
  font-stretch: 95%;
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
  color: var(--text);
}
.postlist-link:hover {
  color: var(--link-active);
}
.postlist-link:not(:hover) {
  text-decoration: none;
}
.postlist-link img {
  display: block;
  width: 100%;
}

.postlist-description {
  font-size: var(--step--1);
  line-height: var(--leading-ui);
  color: var(--text-muted);
  margin: 0;
}

/* Pushed to the foot of the card by the auto margin, so cards of differing
   height still line their dates up. */
.postlist-date {
  font-family: var(--font-meta);
  font-size: var(--step--2);
  color: var(--text-muted);
  margin-block-start: auto;
  padding-block-start: var(--space-2);
}
/* ==========================================================================
   Post furniture — the header image, tag row, standfirst, date and prev/next
   cards that surround a post's body.
   Markup: _includes/layouts/post.njk

   Everything here is mono: it is the labelling around the writing, not the
   writing. That is the one consistent signal separating furniture from prose.
   ========================================================================== */

/* The <picture> wrapper is placed in the wide track by layout.css; this
   styles the image itself. */
.post-image {
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  --flow-space: var(--space-6);
}

/* A centred, width-capped figure inside a post body. */
.post-figure {
  display: block;
  width: 100%;
  max-width: 640px;
  margin-inline: auto;
}

/* --- Masthead -------------------------------------------------------------
   Tags, title, standfirst, date — all at reading measure, so the article
   opens at the width it will be read at. Only the hero image, figures, code
   and callouts break out to `wide` below.
   -------------------------------------------------------------------------- */

/* One quiet line under the standfirst: date, then tags. Tags are not links
   the reader is meant to chase before reading, so they sit in the muted
   colour and only pick up the accent on hover. */
.post-byline {
  font-family: var(--font-meta);
  font-size: var(--step--1);
  line-height: var(--leading-ui);
  color: var(--text-muted);
  --flow-space: var(--space-4);
}
.post-byline-sep {
  padding-inline: var(--space-1);
  opacity: 0.6;
}
.post-byline .post-tag {
  color: var(--text-muted);
}
.post-byline .post-tag:hover {
  color: var(--link-active);
}
.post-byline .post-tag:not(:hover) {
  text-decoration: none;
}

.post-tag {
  text-transform: lowercase;
}

/* The title sits tight under its tags. */
.post-tags + h1 {
  --flow-space: var(--space-3);
}

/* The standfirst under the title: serif, larger than body, quieter. */
.post-subtitle {
  font-size: var(--step-1);
  line-height: var(--leading-snug);
  color: var(--text-muted);
  text-wrap: pretty;
  --flow-space: var(--space-4);
}

/* The dashed frame around the date is gone: --border-subtle on --surface is
   nearly invisible, so it read as two smudges rather than a device. The
   byline's own quietness is enough separation. */

/* --- Prev / next ----------------------------------------------------------
   Two cards rather than a list: at the foot of a long read the titles are the
   useful part, and they need room to be legible.
   -------------------------------------------------------------------------- */
.links-nextprev {
  list-style: none;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  border-top: 1px dashed var(--border-subtle);
  padding: var(--space-5) 0 0;
  margin: 0;
  --flow-space: var(--space-9);
}
.links-nextprev li {
  display: grid;
  gap: var(--space-2);
  align-content: start;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin: 0;
}
.links-nextprev-next {
  text-align: right;
}

/* A lone previous post must not stretch across both columns. */
.links-nextprev-next:only-child {
  grid-column: 2;
}

.links-nextprev-label {
  font-family: var(--font-meta);
  font-size: var(--step--2);
  color: var(--text-muted);
}
.links-nextprev a[href] {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-display);
  color: var(--text);
  text-wrap: balance;
}
.links-nextprev a[href]:hover {
  color: var(--link-active);
}
.links-nextprev a[href]:not(:hover) {
  text-decoration: none;
}

/* Side by side is unreadable on a phone. */
@media (max-width: 40em) {
  .links-nextprev {
    grid-template-columns: 1fr;
  }
  .links-nextprev-next,
  .links-nextprev-next:only-child {
    grid-column: 1;
    text-align: left;
  }
}
/* ==========================================================================
   Callout — the boxed asides in post bodies.

   The class names here are emitted by the `callout` and `tldr` paired
   shortcodes in eleventy.config.js. Nothing enforces that contract, so the
   two files must be changed together. See the matching note there.

   Structure: `.callout` is neutral — it carries the furniture (colours,
   padding, type size) and takes no position. A placement modifier says where
   it sits, a type modifier says what colour it is. Modifiers add; they never
   undo the base.
   ========================================================================== */

.callout {
  /* Defaults to the info palette; type modifiers override these two. */
  --callout-accent: var(--accent-info);
  --callout-accent-subtle: var(--accent-info-subtle);

  /* Placement is owned here rather than in layout.css, because the modifiers
     below re-place the box and a second file setting grid-column would win
     or lose on specificity rather than on intent. */
  grid-column: wide;

  font-size: var(--step--1);
  line-height: var(--leading-ui);
  border-radius: var(--radius-sm);
  overflow: hidden; /* clip the title bar to the rounded corner */
}
.callout p.callout-title {
  font-weight: bold;
  margin: 0;
  padding-left: var(--space-3);
  background-color: var(--callout-accent);
}
.callout div.callout-content {
  margin: 0;
  padding: var(--space-1) var(--space-3);
  background-color: var(--callout-accent-subtle);
}

/* --- Type -----------------------------------------------------------------
   All three declared symmetrically. `--info` repeats the base's defaults on
   purpose: the shortcode emits the class, so a reader grepping for it must
   find it, and the callout contract test requires every emitted class to have
   a rule. */
.callout--info {
  --callout-accent: var(--accent-info);
  --callout-accent-subtle: var(--accent-info-subtle);
}
.callout--note {
  --callout-accent: var(--accent-note);
  --callout-accent-subtle: var(--accent-note-subtle);
}
.callout--warning {
  --callout-accent: var(--accent-warning);
  --callout-accent-subtle: var(--accent-warning-subtle);
}

/* --- Placement ------------------------------------------------------------ */

/* Set beside the text it annotates, in the right-hand half of the wide track.
   It no longer floats: `main` is a grid, and floats do not apply to grid
   items, so text cannot wrap around it. Right-aligning at a narrow width
   keeps the marginal-note reading without pretending to be a float. */
.callout--aside {
  justify-self: end;
  width: min(100%, 22rem);
}

/* Full-width summary, above the text it summarises. Uses the note palette and
   a labelled title bar, so it reads as a standing summary rather than one more
   aside in the flow of the argument. */
.callout--tldr {
  --callout-accent: var(--accent-note);
  --callout-accent-subtle: var(--accent-note-subtle);

  border: 1px solid var(--border-strong);
  font-size: var(--step--1);
}
/* "TL;DR" is already an abbreviation in capitals — uppercasing and tracking
   it out only made it shout. */
.callout--tldr p.callout-title {
  font-family: var(--font-meta);
  font-size: var(--step--1);
  padding: var(--space-2) var(--space-4);
}
.callout--tldr div.callout-content {
  padding: var(--space-4);
}
.callout--tldr div.callout-content {
  padding: var(--space-4);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* A narrow aside is unreadable on a phone. Below the point where the inset
   stops making sense, it becomes a plain full-width block. */
@media (max-width: 40em) {
  .callout--aside {
    width: 100%;
    justify-self: stretch;
  }
}
/* ==========================================================================
   Archive — the typographic index on /blog/.
   Markup: content/blog.njk

   Replaces the card grid. The site's posts live in content/blog/YYYY/ and go
   back years, so chronology is the archive's real structure: the year is set
   at display size and sticks in the left margin while its posts scroll past.
   This is the reserved use of --step-5.
   ========================================================================== */

.archive-lede {
  color: var(--text-muted);
  --flow-space: var(--space-3);
}

.archive {
  --flow-space: var(--space-8);
}

/* 9rem, not the 5.5rem the prototype specified: four digits at --step-5 in
   Archivo 800 at 75% width run ~8rem, so the year ran under the first row's
   date. The rail has to be wider than the thing standing in it. */
.archive-year {
  display: grid;
  grid-template-columns: 9rem 1fr;
  gap: 0 var(--space-6);
}
.archive-year + .archive-year {
  margin-block-start: var(--space-7);
}

/* Sticks until the next year displaces it. Set in the border colour rather
   than the text colour: it is a marker in the margin, not a heading competing
   with the titles beside it. */
.archive-year-label {
  position: sticky;
  inset-block-start: var(--space-4);
  align-self: start;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-5);
  font-weight: 800;
  font-stretch: 75%;
  line-height: 1;
  letter-spacing: var(--tracking-display);
  color: var(--border-strong);
}

.archive-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
.archive-list li {
  margin: 0;
}

.archive-item {
  display: grid;
  grid-template-columns: 5rem 1fr;
  gap: 0 var(--space-4);
  align-items: baseline;
  padding: var(--space-4) var(--space-3);
  border-top: 1px dashed var(--border-subtle);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  transition: background-color 0.12s ease;
}
.archive-item:visited {
  color: var(--text);
}
.archive-item:hover {
  background-color: var(--surface-raised);
  color: var(--text);
}

/* Nudged down so the small date sits on the title's baseline rather than
   floating above it — `align-items: baseline` aligns the first line boxes,
   which at this size difference reads as misalignment. */
.archive-date {
  font-family: var(--font-meta);
  font-size: var(--step--2);
  color: var(--text-muted);
  padding-block-start: 0.35em;
}

.archive-title {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 700;
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}

/* The year rail costs more than it earns on a phone: drop it above the list
   and give the titles the full width. */
@media (max-width: 40em) {
  .archive-year {
    grid-template-columns: 1fr;
    gap: var(--space-2);
  }
  .archive-year-label {
    position: static;
    font-size: var(--step-3);
  }
  .archive-item {
    grid-template-columns: 4.5rem 1fr;
  }
}
/* ==========================================================================
   Home intro — portrait beside an oversized greeting.
   Markup: content/index.njk
   ========================================================================== */

.intro {
  display: grid;
  grid-template-columns: 210px 1fr;
  gap: var(--space-7);
  align-items: start;
}

.intro-portrait {
  inline-size: 210px;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
/* eleventy-img wraps the portrait in <picture>, which is display:inline by
   default — the <img> would size against an inline box instead of the square.
   Both need to fill. The source is 475x423, so `cover` crops it to square. */
.intro-portrait picture,
.intro-portrait img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* Shown until a real portrait is set as `portrait` in _data/metadata.js.
   Deliberately obvious rather than a blank box, so it cannot ship unnoticed. */
.intro-portrait:has(.intro-portrait-placeholder) {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: var(--space-3);
  background-image: repeating-linear-gradient(
    135deg,
    var(--surface-sunken) 0 11px,
    transparent 11px 22px
  );
  background-color: var(--surface-raised);
}
.intro-portrait-placeholder {
  font-family: var(--font-meta);
  font-size: var(--step--2);
  color: var(--text-muted);
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
}

.intro-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-inline-size: 0;
}
.intro-text p {
  margin: 0;
}

/* --step-4 is the page-title size. A greeting is not the page title, and at
   that size it drowned the one line that actually says who Christian is. */
.intro-greeting {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 700;
  font-stretch: 95%;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  color: var(--text);
}

.intro-lead p {
  font-size: var(--step-1);
  line-height: var(--leading-snug);
  color: var(--text-muted);
  text-wrap: pretty;
  margin: 0;
}

/* GitHub / e-mail / LinkedIn — same column as the rest of the intro, set off
   by a rule rather than by moving to a different track. */
.intro-contact {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-block-start: var(--space-3);
  padding-block-start: var(--space-5);
  border-top: 1px solid var(--border-subtle);
}
.intro-contact p {
  margin: 0;
}

/* A 210px portrait beside text is two unreadable columns on a phone. */
@media (max-width: 40em) {
  .intro {
    grid-template-columns: 1fr;
    gap: var(--space-5);
  }
  .intro-portrait,
  .intro-portrait img {
    inline-size: 100px;
  }
}