/* site.css — shared structural styles for every public site.
 *
 * ── The token contract ────────────────────────────────────────────────────
 * Twelve tokens come from the theme preset. This file may not contain a
 * literal colour, font or radius, and CI enforces that every var() it uses is
 * either one of these twelve or declared by this file below:
 *
 *   --bg --surface --text --muted --accent --accent-contrast --border
 *   --font-body --font-heading --radius --header-bg --header-text
 *
 * Everything in the :root block below is declared HERE, not by the presets.
 * That is deliberate: a scale is structure, and structure is this file's job.
 * A preset may still override any of them — the cascade puts it second — but
 * it never has to, so adding a scale is not a breaking change for a consumer.
 *
 * ── The design ────────────────────────────────────────────────────────────
 * One signature: a hairline rule in --accent down the left of the article,
 * with the article's own structure hung off it — the eyebrow at the top, a
 * tick at each h2. It is a table of contents drawn in the margin, and it is
 * the reason --accent is the one token that carries structure rather than
 * decoration. Everything else stays quiet so that it reads.
 *
 * There are no drop shadows outside the sticky header. There used to be eight,
 * all hardcoded rgba(0,0,0,α), which meant the four dark presets rendered flat
 * while the six light ones floated — the fleet did not share a look, it shared
 * a stylesheet that only worked on half of it.
 */

:root {
  /* Type — a 1.2 scale off a 17px body. Nine ad-hoc sizes inside a 1.6x range
     is what "not quite polished" looks like at the CSS level; a reader cannot
     name the problem but can see that nothing lines up. */
  --step--2: 0.75rem;      /* eyebrow, filter labels */
  --step--1: 0.875rem;     /* meta, captions, tagline, footer */
  --step-0:  1.0625rem;    /* body */
  --step-1:  1.25rem;      /* card title, lead-in */
  --step-2:  1.5rem;       /* h3 */
  --step-3:  1.8125rem;    /* h2 */
  --step-4:  clamp(2.125rem, 1.6rem + 2.4vw, 2.8rem);   /* article / hero h1 */

  --lh-tight: 1.15;
  --lh-snug:  1.4;
  --lh-body:  1.65;

  /* Tracking. Uppercase needs opening out; large display type needs closing
     up, or it reads loose at the size the clamp reaches on a wide screen. */
  --track-eyebrow: 0.09em;
  --track-display: -0.02em;

  /* Space — 4px base. Every one-off in the previous file (0.28 0.35 0.4 0.55
     0.65 0.85 0.9 1.1 1.25 1.6 1.75 2.2 2.25 2.5 3) maps onto one of these. */
  --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: 4.5rem;

  /* Radius — MULTIPLY the theme's radius, never add to it. The previous file
     used calc(var(--radius) + 4|6|8|10px) in nine places, which inverted the
     token: `terminal` asks for 0 and got 10px-rounded cards anyway, `valheim`
     asks for 14px and got 24px pillows. Multiplying respects what the preset
     asked for at both ends of the range. */
  --r-1: calc(var(--radius) * 0.5);
  --r-2: var(--radius);
  --r-pill: 999px;
  /* Not derived from --radius, and deliberately so: --r-pill and --r-circle
     are SHAPES, not degrees of rounding. A pill is a pill at every preset, and
     an avatar is a circle or it is not one. A preset that wants square corners
     everywhere overrides these two by name, which is a visible line in a diff
     — the thing scaling them off --radius would have hidden. */
  --r-circle: 50%;

  /* Rules. --rule-accent is the band under the header: the site's colour, at
     the top of every page, tying the header to the article spine below it.
     Five presets used to hand-roll this as their own border-bottom, which is
     the usual sign a rule belongs in the shared layer. Set it to 0 to opt out. */
  --rule: 1px;
  --rule-accent: 2px;
  --rule-spine: 1px;

  /* Elevation. --shadow-color is expected to be overridden per preset, because
     no single value is right on both #faf7f2 and #060a06 — on a near-black
     background the honest answer is `transparent` and the border does the
     work. The inset border-coloured line in --elev-1 always renders, so the
     header keeps a defined edge whatever the preset chooses. */
  --shadow-color: rgba(0, 0, 0, 0.16);
  --elev-1: 0 1px 0 var(--border), 0 6px 16px -8px var(--shadow-color);

  /* Measure. 68ch of the BODY face, set on the prose element itself. The old
     rule was 70ch nested inside an 860px box, and because ch is measured in
     the body face rather than in px that resolved to roughly 78 characters on
     a wide screen — a full line longer than it claimed to be. */
  --measure: 68ch;
  --container: 1100px;
  --column: 54rem;

  --ease: cubic-bezier(0.2, 0, 0.1, 1);
  --dur: 0.18s;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  line-height: var(--lh-body);
  font-size: var(--step-0);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  line-height: var(--lh-tight);
  color: var(--text);
  letter-spacing: var(--track-display);
}

a { color: var(--accent); }
img { max-width: 100%; height: auto; }

/* Dates, counts and stats sit in columns often enough that proportional
   figures visibly ripple. */
time, .card-meta, .aside-meta, .byline-date { font-variant-numeric: tabular-nums; }

/* ── Focus ──────────────────────────────────────────────────────────────
   One rule, everything interactive, drawn in the site's own accent. Before
   this the only focus ring in the file was on the hamburger label, and .btn
   set `border: none` without replacing the outline — so a keyboard user lost
   the search button, the subscribe button and every card entirely. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-1);
}

/* Visible only once tabbed to. First stop on every page, so a keyboard reader
   is not walked through the whole nav to reach the article. */
.skip-link {
  position: absolute;
  left: var(--space-2);
  top: calc(-1 * var(--space-8));
  z-index: 100;
  padding: var(--space-2) var(--space-4);
  background: var(--accent);
  color: var(--accent-contrast);
  border-radius: var(--r-2);
  text-decoration: none;
  font-weight: 600;
  transition: top var(--dur) var(--ease);
}
.skip-link:focus-visible { top: var(--space-2); }

/* ── Icons (inline SVG sprite, see partials/icon_sprite.html) ─────────── */
.icon {
  width: 1em; height: 1em;
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
  vertical-align: -0.15em;
  flex-shrink: 0;
}

.container {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--space-5);
}

/* ── The eyebrow ────────────────────────────────────────────────────────
   Small, tracked-out, uppercase, in the accent. It labels a thing's kind
   before its name — category above a headline, section above a table. It is
   the only type role outside the scale, because at this size the tracking is
   doing more work than the size is. */
.eyebrow,
.card-category {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  width: fit-content;
  font-family: var(--font-heading);
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
}

/* ── Header ─────────────────────────────────────────────────────────────
   Sized to be looked past. It was a 5rem logo inside 1.05rem of padding, then
   a search form and a wrapping row of nav pills — on a phone, roughly half the
   viewport before any content; on desktop, a wasted band.

   The cost is vertical, so that is what is cut: the logo, the vertical padding
   and — the one that actually made it tall — the ROW gap. These items wrap
   onto two or three lines, and a symmetric `gap` paid the same generous space
   between the rows as between the items. Row and column gaps are set
   separately, and the row gap is small.

   The backdrop-filter that used to be here was dead on nine of the ten
   presets: --header-bg is opaque everywhere except `valheim`, so the browser
   was promoting a compositing layer to blur nothing. It now lives behind a
   guard, applied by any preset whose header background is translucent.

   Below 820px the search form and nav collapse behind the hamburger. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--header-bg);
  border-bottom: var(--rule-accent) solid var(--accent);
  box-shadow: var(--elev-1);
}
.header-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  row-gap: var(--space-1);
  column-gap: var(--space-4);
  padding: var(--space-2) var(--space-5);
}
.site-brand { text-decoration: none; display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
/* Name then tagline, the way it always was. A previous version flipped these
   with column-reverse, on a misreading of "the tagline should be on top" —
   which was about the line under the hero moving to the top of the PAGE, not
   about the header brand putting its subtitle above its own name. Putting a
   subtitle above the thing it is subtitling has no reading that helps. */
.site-brand-text { display: flex; flex-direction: column; min-width: 0; }
/* A circle that LETTERBOXES rather than crops. `border-radius: 50%` on an
   auto-width image is an ellipse, and `object-fit: cover` inside a square box
   eats the ends of a wordmark — both of which are how a rounded logo ends up
   cutting off the thing it exists to show. A fixed square, `contain`, and a
   surface fill behind it gives the round shape with nothing lost.

   `terminal` overrides this back to 0 in its own preset file. That preset's
   whole argument is that nothing on the page is rounded, and a hard 50% here
   would overrule it. */
.site-logo {
  height: 2.6rem;
  width: 2.6rem;
  flex-shrink: 0;
  object-fit: contain;
  background: var(--surface);
  padding: 2px;
  border-radius: var(--r-circle);
}
.site-title {
  font-family: var(--font-heading);
  font-size: var(--step-1);
  font-weight: 700;
  color: var(--header-text);
  line-height: var(--lh-snug);
  letter-spacing: 0;
}
.site-tagline { font-size: var(--step--1); color: var(--muted); line-height: var(--lh-snug); }

/* Row one, right: the companion sites and the search form, on one line with
   the brand. The companions sit before the search rather than after the
   categories because they are the other half of the product and the old header
   ranked them last — after every category and every static page, at the end of
   a wrapping row, which is where a link goes to be ignored. */
.header-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}
.fleet-links { display: flex; align-items: center; gap: var(--space-2); }
/* Old markup, kept working until every consumer moves: the search form as a
   direct child of .header-inner rather than inside .header-actions. `.site-nav`
   used to carry the `margin-left: auto` that pushed everything right, and it is
   a full-width second row now, so without this the form lands against the
   brand on any repo that has taken the pin bump but not the template. */
.header-inner > .search-form { margin-left: auto; }

/* Row two: the site's own navigation, centred, on its own band. Full-width via
   flex-basis rather than a grid row, so it participates in the same wrap the
   header has always used and a consumer that renders no nav costs nothing. */
.site-nav {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  row-gap: var(--space-1);
  column-gap: var(--space-1);
  padding-top: var(--space-1);
}
.site-nav a {
  color: var(--header-text);
  text-decoration: none;
  font-size: var(--step--1);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-pill);
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-nav a:hover {
  color: var(--accent-contrast);
  background: var(--accent);
}
.search-form { display: flex; align-items: center; gap: var(--space-1); }
.search-form .input {
  padding: var(--space-1) var(--space-3);
  font-size: var(--step--1);
  width: 10rem;
  transition: width var(--dur) var(--ease);
}
.search-form .btn { padding: var(--space-1) var(--space-3); }

/* There used to be a rule here shrinking the field to its button between 821px
   and 1000px, against a wrap I estimated at "around 780px of content" and never
   measured. Row one is brand, companions and search — the categories moved to
   row two in the same change — so there is a wide band of empty space to the
   left of the field at exactly those widths, and it was collapsing a search box
   to avoid a wrap that does not happen.
   
   If row one ever does run out of room, .header-inner wraps, which is a
   degradation and not a break. */

/* ── The hamburger ──────────────────────────────────────────────────────
   A checkbox, not a script. The previous version toggled a class from
   JavaScript and shipped a control that rendered, looked interactive, and did
   nothing — a failure mode a checkbox does not have, because the browser
   toggles :checked itself.

   The reveal below uses `~`, so .nav-checkbox must come BEFORE .search-form
   and .site-nav in the markup. See macros/header.html. */
.nav-checkbox {
  /* Clipped, never display:none — that would drop it out of the tab order and
     make the menu unreachable by keyboard. This keeps Space working. */
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.nav-toggle {
  display: none;
  margin-left: auto;
  align-items: center;
  justify-content: center;
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  background: transparent;
  color: var(--header-text);
  border: var(--rule) solid var(--border);
  border-radius: var(--r-2);
  cursor: pointer;
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 1.15rem;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
}
.nav-toggle-bars::before,
.nav-toggle-bars::after { content: ''; position: absolute; left: 0; }
.nav-toggle-bars::before { top: -6px; }
.nav-toggle-bars::after { top: 6px; }
.nav-checkbox:checked + .nav-toggle {
  background: var(--accent);
  color: var(--accent-contrast);
  border-color: var(--accent);
}
/* The checkbox is invisible, so its focus ring has to be drawn on the label. */
.nav-checkbox:focus-visible + .nav-toggle {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (max-width: 820px) {
  /* Already in <title> and the meta description; in a collapsed header it is
     the first thing worth its row back. */
  .site-tagline { display: none; }

  .nav-toggle { display: inline-flex; }
  /* `.search-form` is listed alongside the wrapper, and both are revealed
     below, because a pin bump lands in four repos and they do not all update
     their base.html in the same hour. A consumer still on the old markup —
     search form as a bare sibling of the checkbox — collapses and reopens
     correctly; one on the new markup gets the ordered panel. Neither ships a
     header with a search field stuck open at 375px, which is what leaving this
     selector alone would have done to whoever bumped last. */
  .header-actions,
  .search-form,
  .site-nav { display: none; }

  /* Open: a panel under the header, not a second row of pills. Full-width
     stacked rows with real tap targets — what a mobile menu looks like
     everywhere else. The header grows to hold it rather than the panel
     overlaying the page: an overlay needs a stacking context and a scroll
     lock to behave, and neither is worth it for a list of six links.

     Reading order in the panel is search → companions → categories → More,
     which is the same order as the two desktop rows read left-to-right and
     top-to-bottom. A menu that reorders itself at a breakpoint is a second
     information architecture to learn, for no gain. */
  .nav-checkbox:checked ~ .header-actions,
  .nav-checkbox:checked ~ .search-form,
  .nav-checkbox:checked ~ .site-nav {
    display: flex;
    flex-basis: 100%;
    margin-left: 0;
  }
  .nav-checkbox:checked ~ .header-actions,
  .nav-checkbox:checked ~ .search-form {
    order: 1;
    padding-top: var(--space-2);
  }
  .nav-checkbox:checked ~ .header-actions {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }
  .nav-checkbox:checked ~ .header-actions .search-form { order: 1; display: flex; }
  .nav-checkbox:checked ~ .header-actions .search-form .input,
  .nav-checkbox:checked ~ .search-form .input { flex: 1; width: auto; }
  .nav-checkbox:checked ~ .header-actions .fleet-links {
    order: 2;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin-top: var(--space-2);
  }

  .nav-checkbox:checked ~ .site-nav {
    order: 2;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    row-gap: 0;
    padding: var(--space-2) 0 var(--space-1);
    margin-top: var(--space-2);
  }
  .nav-checkbox:checked ~ .site-nav > a {
    padding: var(--space-3) var(--space-2);
    font-size: var(--step-0);
    border-radius: var(--r-2);
  }
  /* The cross-site links are outlined pills at desktop width; as full-width
     rows that outline reads as a text field, so it becomes a rule instead. */
  .nav-checkbox:checked ~ .site-nav .fleet-link,
  .nav-checkbox:checked ~ .header-actions .fleet-link {
    border: none;
    border-top: var(--rule) solid var(--border);
    border-radius: 0;
    padding: var(--space-3) var(--space-2);
    font-size: var(--step-0);
  }

  /* A companion's submenu is a floating panel at desktop width and an inline
     block here. Absolute positioning inside a header that grows to hold its
     own menu would put the panel over the page it is supposed to sit above.

     The scrim is off entirely: the menu is inline in a panel the hamburger
     already opened, so a full-page overlay would only get in the way of the
     rest of that panel. */
  .fleet-sub { width: 100%; }
  .fleet-sub-summary {
    display: flex;
    border: none;
    border-top: var(--rule) solid var(--border);
    border-radius: 0;
    padding: var(--space-3) var(--space-2);
    font-size: var(--step-0);
    justify-content: space-between;
  }
  .fleet-menu-state:checked ~ .fleet-sub-summary { background: none; color: var(--accent); }
  .fleet-menu-state:checked ~ .fleet-sub-scrim { display: none; }
  .fleet-menu-state:checked ~ .fleet-sub-panel {
    position: static;
    min-width: 0;
    border: none;
    box-shadow: none;
    background: none;
    padding: 0 0 var(--space-2) var(--space-4);
  }
}

/* ── Fleet cross-links (macros/fleet.html) ──────────────────────────────
   A link to a sibling site leaves this hostname, so it is drawn differently
   from the site's own nav. Outlined rather than louder: it should read as a
   way across, not as the most important thing in the header. */
.site-nav .fleet-link,
.header-actions .fleet-link {
  color: var(--accent);
  text-decoration: none;
  font-size: var(--step--1);
  padding: var(--space-1) var(--space-3);
  border: var(--rule) solid var(--accent);
  border-radius: var(--r-pill);
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-nav .fleet-link:hover,
.header-actions .fleet-link:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

/* ── Companion submenus and "More" (macros/fleet.html) ──────────────────
   A radio group and a full-page scrim, not a <details>.

   <details> had one flaw and it is the one people notice: nothing closes it
   except clicking the summary again, so clicking anywhere else on the page
   leaves the menu hanging open. `open` is state CSS cannot clear, so it was not
   fixable where it stood. A radio CAN be cleared — by a label pointing at
   another radio in the same group — which is what .fleet-sub-scrim is.

   The scrim's z-index is the load-bearing number. It sits BELOW .site-header
   (30) and above the page, so an open menu is dismissed by a click anywhere in
   the document except the header itself — and the other menu controls, which
   live in the header, stay clickable, so switching menus is one click. Raise
   this above 30 and switching silently becomes two. */
.fleet-menu-state {
  /* Clipped, never display:none: that would take it out of the tab order and
     make the menu keyboard-unreachable. Same reasoning as .nav-checkbox. */
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.fleet-sub { position: relative; }
.fleet-sub-summary {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  font-size: var(--step--1);
  color: var(--accent);
  border: var(--rule) solid var(--accent);
  border-radius: var(--r-pill);
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.fleet-sub-summary:hover,
.fleet-menu-state:checked ~ .fleet-sub-summary {
  background: var(--accent);
  color: var(--accent-contrast);
}
/* The radio is invisible, so its focus ring is drawn on the label. */
.fleet-menu-state:focus-visible ~ .fleet-sub-summary {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Drawn in borders rather than pulled from the icon sprite: this file is
   included by consumers that do not all inline the sprite, and a chevron that
   renders as an empty box on one of them is worse than no chevron. */
.fleet-caret {
  width: 0.4em;
  height: 0.4em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-0.1em, -0.1em);
  transition: transform var(--dur) var(--ease);
}
.fleet-menu-state:checked ~ .fleet-sub-summary .fleet-caret {
  transform: rotate(-135deg) translate(-0.15em, -0.15em);
}

.fleet-sub-scrim { display: none; }
.fleet-menu-state:checked ~ .fleet-sub-scrim {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 29;
  cursor: default;
}

.fleet-sub-panel { display: none; }
.fleet-menu-state:checked ~ .fleet-sub-panel {
  display: flex;
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: 40;
  min-width: 12rem;
  flex-direction: column;
  padding: var(--space-1);
  background: var(--header-bg);
  border: var(--rule) solid var(--border);
  border-radius: var(--r-2);
  box-shadow: var(--elev-1);
}
.fleet-sub-panel a {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-1);
  color: var(--header-text);
  text-decoration: none;
  font-size: var(--step--1);
  white-space: nowrap;
}
.fleet-sub-panel a:hover { background: var(--accent); color: var(--accent-contrast); }
.fleet-sub-home { color: var(--accent); }
.fleet-sub-panel .fleet-sub-home + a {
  margin-top: var(--space-1);
  border-top: var(--rule) solid var(--border);
  padding-top: var(--space-3);
}

/* "More" is a row-two control sitting among the site's own links, so it is
   outlined like a cross-site link — it leads off this hostname and should not
   be dressed as a category. */
.fleet-more { margin-left: var(--space-2); }
/* Anchored right: "More" is the last control on a centred row, so a panel
   opening leftward is the one that stays on screen. */
.fleet-more .fleet-menu-state:checked ~ .fleet-sub-panel { left: auto; right: 0; }

/* ── Hero ───────────────────────────────────────────────────────────────
   The image sits beside the headline, not above it. Full-bleed at 18/8 it was
   about 490px tall on an 1100px page, so the entire first screen was one
   article's photograph and the grid of everything else began below the fold.
   A thumbnail still tells you the lead has a picture; it just stops being the
   page. .hero-link-media is set by the template only when there is an image —
   without it the single text child would be laid into the image column.

   It is no longer a raised card. The lead is the largest type on the page and
   sits directly above a grid of small type; that is already the hierarchy, and
   wrapping it in a surface said the same thing a second time. What marks it
   now is the accent rule at its foot, which is the header's band and the
   article's spine appearing a third time. */
.hero { margin: var(--space-5) 0 var(--space-6); }

/* ── The site's own sentence, and the companion tools ───────────────────
   Two things a home page owes a reader who arrived from a search result and
   has never seen the name: what this site is, and what it can do for them
   right now. Neither was on the page. The first is one line under the hero;
   the second is the toolbox, which existed and was reachable only by noticing
   a link in the header and guessing what was behind it.

   The tools sit ABOVE the article grid because a reader who wants a number
   wants it now, and below the hero because the lead article is still the
   thing this site is for. */
/* The first line on the page, above the hero. It was below it, which put the
   sentence explaining the site after the article the site is currently leading
   with — and, because it fell back to the tagline, repeated a string already in
   the header two lines further up. It has its own copy now and renders nothing
   without it. */
.site-intro {
  max-width: var(--measure);
  margin: var(--space-5) 0 var(--space-4);
  color: var(--muted);
  font-size: var(--step-1);
  line-height: var(--lh-snug);
}
.tools-band { margin: 0 0 var(--space-7); }
.tool-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: var(--space-4);
}

/* One or two tools do not make a row; they make a note beside the lead. Above
   900px the hero keeps the width it had and the tools take a rail; below it
   they fall under, which is the same thing the home rail already does. */
.hero-with-tools { display: grid; grid-template-columns: minmax(0, 1fr) 17rem; gap: var(--space-5); align-items: start; }
.hero-tools { display: flex; flex-direction: column; gap: var(--space-3); padding-top: var(--space-5); }
@media (max-width: 900px) {
  .hero-with-tools { grid-template-columns: minmax(0, 1fr); }
  .hero-tools { padding-top: 0; }
}

/* Outlined and arrowed, like a companion link in the header — because that is
   what it is. Dressing it as an article card would sell a guide and deliver a
   form, and the tools are worth more than the click that comes back. */
.tool-card {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  padding: var(--space-4);
  text-decoration: none;
  color: var(--text);
  border: var(--rule) solid var(--border);
  border-radius: var(--r-2);
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.tool-card:hover { border-color: var(--accent); background: var(--surface); }
.tool-card-icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--r-1);
  background: var(--surface);
  color: var(--accent);
}
.tool-card:hover .tool-card-icon { background: var(--accent); color: var(--accent-contrast); }
.tool-card-body { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
/* Margin reset because this is a <span> on the blog, where the tools are a
   secondary block, and an <h2> on the toolbox index, where they are the page.
   Same component, same class, and the element is the consumer's call. */
.tool-card-title { margin: 0; font-family: var(--font-heading); font-size: var(--step-1); line-height: var(--lh-snug); }
.tool-card-answer { color: var(--muted); font-size: var(--step--1); }
.tool-card-answer strong { color: var(--accent); font-weight: 600; }
.tool-card-arrow { color: var(--accent); }
.tool-card-summary { color: var(--muted); font-size: var(--step--1); line-height: var(--lh-snug); }
.hero-link {
  display: grid;
  gap: var(--space-4);
  padding: 0 0 var(--space-5);
  border-bottom: var(--rule-spine) solid var(--accent);
  text-decoration: none;
  color: inherit;
}
.hero-link-media {
  grid-template-columns: 9rem minmax(0, 1fr);
  align-items: center;
  gap: var(--space-5);
}
/* A hard ceiling in absolute units, not only a ratio. An aspect-ratio alone
   still scales with the column, which is how this stayed overpowering after
   being "made smaller" once already. max-height means it cannot dominate the
   page whatever the column does. */
.hero-image {
  width: 100%;
  max-width: 9rem;
  max-height: 7rem;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--r-2);
}
.hero-text { min-width: 0; }
/* One step down from the article's own h1.

   --step-4 is the size for the title of the thing you are READING. A hero is a
   link to something else, sharing a page with a grid of other links, and set at
   the same size it competes with the article it is advertising and pushes the
   grid off the screen. .article-title keeps --step-4; this is the one place the
   two were wrongly equal. */
.hero-title { font-size: var(--step-3); margin: var(--space-2) 0; }
.hero-excerpt { color: var(--muted); font-size: var(--step-0); margin: 0; max-width: var(--measure); }
.hero-link:hover .hero-title { color: var(--accent); }
.hero-empty {
  padding: var(--space-7) 0;
  border-bottom: var(--rule-spine) solid var(--accent);
}

@media (max-width: 720px) {
  /* Still side by side on a phone, just narrower. Stacking it put a
     full-width image back above the headline, which is the thing being
     removed. */
  .hero-link-media { grid-template-columns: 6.5rem minmax(0, 1fr); gap: var(--space-4); }
  .hero-image { max-width: 6.5rem; max-height: 5rem; }
}

/* ── Home: content column + skim rail ───────────────────────────────────
   The rail is titles only and holds articles that are not already in the grid
   below it, so it adds reach rather than repeating what is on screen. It
   collapses to a normal block under the aside breakpoint — sticky positioning
   is dropped there too, or it would pin mid-scroll on a phone.

   One column by default. The second is added by .home-layout-rail, which the
   template sets only when there is a rail — the column used to be
   unconditional while the rail was not, so a site without one rendered its
   content off-centre beside a blank 16rem strip. */
.home-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-7);
  align-items: start;
}
.home-layout-rail { grid-template-columns: minmax(0, 1fr) 16rem; }
.home-main { min-width: 0; }

/* The rail carries the spine too, so the page has one rule language: a thing
   bounded by an accent rule is a column of reading. */
/* padding-top, not a margin on .aside-heading: the rail is sticky, so the gap
   has to belong to the sticky box or it scrolls away with the heading and the
   rule starts flush against the top of the viewport again. */
.home-aside {
  position: sticky;
  top: var(--space-8);
  padding-left: var(--space-4);
  padding-top: var(--space-5);
  border-left: var(--rule-spine) solid var(--accent);
}
.aside-heading {
  font-family: var(--font-heading);
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--muted);
  margin: 0 0 var(--space-2);
}
/* A second block in the same rail. It needs air above it and a rule to say it
   is a different list, or the two read as one list with a word in the middle. */
.aside-heading-second {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: var(--rule) solid var(--border);
}
.aside-list { list-style: none; margin: 0; padding: 0; }
.aside-list li + li { border-top: var(--rule) solid var(--border); }
.aside-list a {
  display: block;
  padding: var(--space-3) 0;
  color: var(--text);
  text-decoration: none;
  font-size: var(--step--1);
  line-height: var(--lh-snug);
}
.aside-list a:hover { color: var(--accent); }
.aside-meta { display: block; font-size: var(--step--2); color: var(--muted); margin-top: var(--space-1); }

@media (max-width: 980px) {
  .home-layout,
  .home-layout-rail { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
  .home-aside { position: static; }
}

/* ── Cards ────────────────────────────────────────────────────────────
   A hairline and an image, no surface fill and no shadow. Six near-identical
   raised rectangles in a grid is the shape every blog template ships; what
   distinguishes one card from the next should be its photograph and its
   headline, which is what is left once the chrome is gone.

   Hover is the accent rule appearing along the top edge — the same rule as the
   header band, the hero foot and the article spine. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-6) var(--space-5);
  margin: var(--space-6) 0;
}
.card {
  position: relative;
  border-top: var(--rule) solid var(--border);
  padding-top: var(--space-4);
}
/* Drawn as a pseudo-element rather than by swapping border-color, so the
   accent can be thicker than the hairline without the card shifting by a
   pixel on hover. */
.card::before {
  content: '';
  position: absolute;
  top: calc(-1 * var(--rule));
  left: 0;
  width: 0;
  height: var(--rule-spine);
  background: var(--accent);
  transition: width var(--dur) var(--ease);
}
.card:hover::before,
.card:focus-within::before { width: 100%; }
.card-link { display: flex; flex-direction: column; min-height: 100%; text-decoration: none; color: inherit; gap: var(--space-3); }
.card-media { overflow: hidden; border-radius: var(--r-2); }
.card-image {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  display: block;
  border-radius: var(--r-2);
  transition: transform var(--dur) var(--ease);
}
.card:hover .card-image { transform: scale(1.02); }
.card-image-placeholder {
  background: var(--surface);
  border: var(--rule) solid var(--border);
  border-radius: var(--r-2);
  aspect-ratio: 16 / 10;
  display: flex;
  align-items: center;
  justify-content: center;
}
.card-image-placeholder .icon { width: 2.25rem; height: 2.25rem; color: var(--muted); }
.card-body { display: flex; flex-direction: column; gap: var(--space-2); flex: 1; }
.card-title { margin: 0; font-size: var(--step-1); line-height: var(--lh-snug); }
.card:hover .card-title { color: var(--accent); }
.card-excerpt { color: var(--muted); font-size: var(--step--1); margin: 0; }
.card-meta {
  display: flex; gap: var(--space-3); flex-wrap: wrap;
  font-size: var(--step--2); color: var(--muted);
  margin-top: auto; padding-top: var(--space-1);
}
.card-meta span, .card-meta time { display: inline-flex; align-items: center; gap: var(--space-1); }

/* ── Article: the spine ─────────────────────────────────────────────────
   The article is not a panel floating on the page — it is a column of reading
   with a rule down its left edge. The rule is the article's extent, and the
   ticks hung off it at each h2 are its joints, so the margin says how much is
   left and where the seams are. That is the one thing in this stylesheet that
   is allowed to be a flourish, and it earns it by being informative.

   .article-body and each h2 share a left edge, so the tick's offset back to
   the spine is exactly the padding — do not add padding to .article-body or
   the ticks will detach. */
.article-narrow { max-width: var(--column); margin: 0 auto; }
.article-full { width: 100%; }
.article {
  max-width: var(--column);
  margin: var(--space-6) auto var(--space-7);
  padding-left: var(--space-5);
  border-left: var(--rule-spine) solid var(--accent);
}
.article-header { margin-bottom: var(--space-5); }
.article-title { font-size: var(--step-4); margin: var(--space-2) 0 var(--space-4); }
.article-hero { margin: 0 0 var(--space-6); }
.article-hero img {
  width: 100%;
  border-radius: var(--r-2);
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.article-hero figcaption { font-size: var(--step--1); color: var(--muted); margin-top: var(--space-2); }
.article-hero figcaption a { color: var(--muted); }

.article-body { max-width: var(--measure); }
.article-body h2 { position: relative; font-size: var(--step-3); margin: var(--space-7) 0 var(--space-4); }
.article-body h2::before {
  content: '';
  position: absolute;
  left: calc(-1 * var(--space-5));
  top: 0.6em;
  width: var(--space-3);
  height: var(--rule-spine);
  background: var(--accent);
}
.article-body h3 { font-size: var(--step-2); margin: var(--space-6) 0 var(--space-3); }
.article-body p, .article-body li { font-size: var(--step-0); line-height: var(--lh-body); }
.article-body > :first-child { margin-top: 0; }
.article-body pre {
  background: var(--surface);
  border: var(--rule) solid var(--border);
  border-radius: var(--r-2);
  padding: var(--space-4);
  overflow-x: auto;
  font-size: var(--step--1);
}
.article-body code { background: var(--surface); padding: 0.1em 0.35em; border-radius: var(--r-1); font-size: 0.92em; }
.article-body pre code { background: none; padding: 0; }
/* Hung off the spine rather than given a bar of its own: a second vertical
   rule three words from the first is noise, so a pull quote steps OUT to the
   spine and takes it over for its height. */
.article-body blockquote {
  margin: var(--space-6) 0 var(--space-6) calc(-1 * var(--space-5));
  padding: var(--space-2) 0 var(--space-2) calc(var(--space-5) - var(--rule-spine));
  border-left: var(--rule-accent) solid var(--accent);
  color: var(--text);
  font-size: var(--step-1);
  line-height: var(--lh-snug);
}
.article-body blockquote p:last-child { margin-bottom: 0; }

.article-tags { margin: var(--space-6) 0 var(--space-2); display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tag, .chip {
  display: inline-block;
  border: var(--rule) solid var(--border);
  border-radius: var(--r-pill);
  padding: var(--space-1) var(--space-3);
  font-size: var(--step--1);
  color: var(--muted);
  text-decoration: none;
}
a.tag:hover, a.chip:hover { border-color: var(--accent); color: var(--accent); }

@media (max-width: 600px) {
  /* The spine survives on a phone but the tick does not — at this width the
     h2 needs its full measure more than the margin needs its marker. */
  .article { padding-left: var(--space-4); }
  .article-body h2::before { display: none; }
  .article-body blockquote {
    margin-left: calc(-1 * var(--space-4));
    padding-left: calc(var(--space-4) - var(--rule-spine));
  }
}

/* ── Tables ─────────────────────────────────────────────────────────────
   Promoted from game-db, where they had to live because this file had no table
   rules at all — which also meant a markdown table in a blog article rendered
   unstyled, and `brewhouse` was patching `article table` on its own for
   recipes. Wide content is a fleet problem, not one site's.

   The table scrolls inside its container, never the page body. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: var(--rule) solid var(--border);
  border-radius: var(--r-2);
  margin: var(--space-5) 0;
  background: var(--surface);
}
.stat-table, .article-body table {
  border-collapse: collapse;
  font-size: var(--step--1);
}
.stat-table { width: 100%; }

/* A markdown table cannot be wrapped in .table-scroll, because nothing puts a
   container around it — the renderer emits a bare <table> into .article-body.
   So it becomes its own scroller: display:block makes overflow-x apply at all,
   fit-content keeps it from collapsing its columns to the measure, and
   max-width caps it at the column so a wide recipe or spec table scrolls
   inside the article instead of scrolling the page sideways. */
.article-body table {
  display: block;
  width: fit-content;
  max-width: 100%;
  overflow-x: auto;
  margin: var(--space-5) 0;
}
.stat-table th, .stat-table td,
.article-body th, .article-body td {
  padding: var(--space-3) var(--space-4);
  text-align: left;
  vertical-align: top;
}
.stat-table thead th, .article-body thead th {
  font-family: var(--font-heading);
  font-size: var(--step--2);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--muted);
  border-bottom: var(--rule) solid var(--border);
  white-space: nowrap;
}
.stat-table tbody tr + tr,
.article-body tbody tr + tr { border-top: var(--rule) solid var(--border); }
/* A url field falls back to rendering the whole href when it has no host to
   show. Break it rather than let it push the table wider than its scroller. */
.stat-table td a { word-break: break-word; }
.stat-table th[scope='row'] {
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
  width: 34%;
}
.compare-table th[scope='row'] { width: auto; }
.stat-sublist { list-style: none; margin: 0; padding: 0; }
.stat-sublist li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-1) 0;
  max-width: 22rem;
}
.stat-subkey { color: var(--text); text-transform: capitalize; }
.stat-subval { color: var(--accent); font-weight: 600; font-variant-numeric: tabular-nums; }

@media (max-width: 720px) {
  .stat-table th[scope='row'] { width: 45%; white-space: normal; }
  .stat-table th, .stat-table td { padding: var(--space-2) var(--space-3); }
  .stat-sublist li { max-width: none; }
}

/* ── Byline / author ────────────────────────────────────────────────── */
.avatar-fallback {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; border-radius: 50%;
  background: var(--surface); border: var(--rule) solid var(--border);
  color: var(--muted);
}
.avatar-fallback .icon { width: 50%; height: 50%; }
.byline { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) 0; }
.byline-avatar { border-radius: 50%; }
.byline-text { display: flex; flex-direction: column; font-size: var(--step--1); }
.byline-name { font-weight: 600; text-decoration: none; }
.byline-quals, .byline-date { color: var(--muted); font-size: var(--step--2); }
.author-box {
  background: var(--surface);
  border: var(--rule) solid var(--border);
  border-radius: var(--r-2);
  padding: var(--space-5);
}
.author-box img { border-radius: 50%; }
.author-box h3 { margin: var(--space-2) 0 var(--space-1); font-size: var(--step-1); }
.author-box h3 a { text-decoration: none; color: var(--text); }
.author-quals { color: var(--accent); font-size: var(--step--1); margin: 0 0 var(--space-2); }
.author-header { display: flex; gap: var(--space-5); align-items: center; }
.author-avatar { border-radius: 50%; }

/* ── Human contributors ─────────────────────────────────────────────────
   Structurally distinct from the persona byline above, not a restyle of it.
   The photo is a square-cropped photograph rather than a circular illustrated
   avatar, the byline carries a job title and credentials the persona has no
   truthful equivalent for, and only a person gets the .writer-box at the foot
   of an article. Colours come from theme variables like everything else.

   The persona byline sits ON the spine; a writer's steps out to a rule of its
   own, which is the same distinction drawn in the margin. */
.writer-byline {
  align-items: flex-start;
  margin-left: calc(-1 * var(--space-5));
  padding-left: calc(var(--space-5) - var(--rule-accent));
  border-left: var(--rule-accent) solid var(--accent);
}
.writer-photo { border-radius: var(--r-2); object-fit: cover; flex-shrink: 0; }
.writer-name { color: var(--text); }
.writer-role { color: var(--accent); font-size: var(--step--1); font-weight: 600; margin: 0; }
.writer-credentials { color: var(--muted); font-size: var(--step--2); margin: 0; }
.writer-box {
  display: flex; gap: var(--space-5); align-items: flex-start;
  margin: var(--space-7) 0 0;
  padding-top: var(--space-5);
  border-top: var(--rule) solid var(--border);
}
.writer-box-name { margin: 0 0 var(--space-1); font-size: var(--step-1); }
.writer-box-name a { color: var(--text); text-decoration: none; }
.writer-bio { font-size: var(--step--1); color: var(--text); }
.writer-bio p:last-child { margin-bottom: 0; }
.writer-links { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-4); padding: 0; margin: var(--space-3) 0 0; font-size: var(--step--1); }
.writer-header { display: flex; gap: var(--space-6); align-items: flex-start; }
.writer-header h1 { margin: 0 0 var(--space-2); }
.writer-header-photo { border-radius: var(--r-2); }
.writer-header-text { max-width: 44rem; }

@media (max-width: 600px) {
  .writer-box, .writer-header { flex-direction: column; gap: var(--space-4); }
  .writer-byline { margin-left: calc(-1 * var(--space-4)); padding-left: calc(var(--space-4) - var(--rule-accent)); }
}

/* ── Listings / misc ────────────────────────────────────────────────── */
.listing-header { margin: var(--space-7) 0 var(--space-2); }
.listing-header h1 { font-size: var(--step-4); }
.listing-header p { color: var(--muted); }
.related h2, .related-guides h2 { margin-top: var(--space-8); font-size: var(--step-2); }
.empty-note { color: var(--muted); }
.page-title { font-family: var(--font-heading); font-size: var(--step-4); margin: var(--space-6) 0 var(--space-1); }
.page-sub { color: var(--muted); margin: 0 0 var(--space-5); }
.muted { color: var(--muted); }
.section { margin: var(--space-7) 0; }
.section-title {
  font-family: var(--font-heading);
  font-size: var(--step-2);
  margin: 0 0 var(--space-4);
  padding-bottom: var(--space-2);
  border-bottom: var(--rule) solid var(--border);
}
.breadcrumb { color: var(--muted); font-size: var(--step--1); margin: var(--space-5) 0 0; }
.breadcrumb a { color: var(--muted); }
.breadcrumb a:hover { color: var(--accent); }

.related-guides { margin-top: var(--space-7); }
.guide-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.guide-item {
  padding: var(--space-4) 0;
  border-top: var(--rule) solid var(--border);
}
.guide-link { font-weight: 600; text-decoration: none; color: var(--text); }
.guide-link:hover { color: var(--accent); }
.guide-summary { margin: var(--space-1) 0 0; font-size: var(--step--1); color: var(--muted); }

/* ── Forms & buttons (search, newsletter, contact) ─────────────────── */
.form { display: grid; gap: var(--space-3); max-width: 480px; margin: var(--space-5) 0; }
.field { display: grid; gap: var(--space-1); }
.field label { font-size: var(--step--1); color: var(--muted); }
.input, .textarea {
  font: inherit;
  padding: var(--space-2) var(--space-3);
  border: var(--rule) solid var(--border);
  border-radius: var(--r-2);
  background: var(--bg);
  color: var(--text);
}
.textarea { resize: vertical; min-height: 6rem; }
.btn {
  display: inline-flex; align-items: center; gap: var(--space-2); justify-content: center;
  background: var(--accent); color: var(--accent-contrast); border: none;
  border-radius: var(--r-pill);
  padding: var(--space-2) var(--space-5);
  font: inherit; font-weight: 600;
  cursor: pointer; text-decoration: none;
}
.btn:hover { filter: brightness(1.08); }
.form-banner {
  background: var(--surface);
  border: var(--rule) solid var(--border);
  border-left: var(--rule-accent) solid var(--accent);
  border-radius: var(--r-2);
  padding: var(--space-3) var(--space-4);
  margin: var(--space-4) 0;
  color: var(--text);
}
.newsletter-form { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.newsletter-form .input { min-width: 14rem; }
/* WRAPS, and that is the whole point of it. A flex item's default
   `min-width: auto` means the button never shrinks below its own text, so a row
   that cannot wrap pushes the button past the edge of a narrow footer rather
   than reflowing — which is exactly what it did. The field has a flex-basis it
   can fall below and the button drops underneath instead. */
.newsletter-row { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.newsletter-row .input { flex: 1 1 9rem; min-width: 0; }
.newsletter-row .btn { flex-shrink: 0; }
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Off-screen to the eye, present to a screen reader. Distinct from .hp-field,
   which hides a honeypot from everyone including assistive tech. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Ads ────────────────────────────────────────────────────────────────
   The min-heights reserve space so a unit does not shift the layout when it
   loads (CLS) — do not remove them.

   The label is new. An unlabelled block between two paragraphs, in a page
   whose whole job is to look like editorial, is indistinguishable from
   editorial until it renders. Saying so costs one line of small type and is
   the difference between a publication and a content farm. */
.ad-slot {
  min-height: 100px;
  margin: var(--space-6) auto;
  text-align: center;
}
.ad-slot::before {
  content: 'Advertisement';
  display: block;
  font-size: var(--step--2);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--muted);
  text-align: left;
  padding-bottom: var(--space-1);
  border-bottom: var(--rule) solid var(--border);
  margin-bottom: var(--space-3);
}
.ad-slot-header, .ad-slot-footer { max-width: var(--container); }
.ad-slot-sidebar { min-height: 250px; }
/* Reserve height for the in-flow placements too, or they shift the article
   when the unit loads — the CLS problem .ad-slot exists to prevent. */
.ad-slot-below_hero, .ad-slot-in_article { max-width: var(--column); min-height: 280px; }

/* ── Footer ───────────────────────────────────────────────────────────
   ONE row that wraps, not a three-column grid.

   The grid version was a regression and this is the correction. It took more
   vertical room than the flex row it replaced, spread three short columns
   across the full width so the space between them read as emptiness rather
   than as structure, and put a hairline above the copyright that cut a small
   block in half for no reason. Column headings over two links each is the
   shape of a footer with thirty links in it, and this one has five.

   The subscribe button also fell off the page. The row holding the field and
   the button did not wrap, and a flex item's default `min-width: auto` means a
   button never shrinks below its own text — so once the column was narrower
   than field-plus-button, the button went past the edge. It wraps now, which
   is the fix that holds at every width rather than at the ones tested. */
.site-footer {
  border-top: var(--rule) solid var(--border);
  margin-top: var(--space-7);
  background: var(--header-bg);
}
.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-6);
  padding: var(--space-4) var(--space-5);
  font-size: var(--step--1);
  color: var(--muted);
}
/* The brand end of the row takes the slack, so the links and the form sit
   against the right edge instead of floating in the middle of it. */
.footer-brand { display: flex; align-items: center; gap: var(--space-3); margin-right: auto; min-width: 0; }
.footer-preview-image { height: 2.2rem; width: auto; flex-shrink: 0; border-radius: var(--r-1); }
.footer-inner nav { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
.footer-inner a { color: var(--muted); text-decoration: none; }
.footer-inner a:hover { color: var(--accent); }
/* No rule above it. It is the same block, and a hairline across a footer four
   lines tall divides nothing. */
.footer-bottom {
  padding: 0 var(--space-5) var(--space-4);
  font-size: var(--step--2);
  color: var(--muted);
}
.footer-note { color: var(--muted); font-size: var(--step--1); max-width: 60ch; margin: 0; }

/* ── Motion ─────────────────────────────────────────────────────────────
   Last, so it wins on specificity ties. Covers the presets too: `terminal`
   animates a blinking cursor on .site-title::after, which is exactly the kind
   of persistent motion this preference exists to stop. */
@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;
  }
  .card:hover .card-image { transform: none; }
}
