/*
Theme Name: Inked and Faded
Theme URI: https://inkedandfaded.com/
Author: Jordan Mane
Description: Hand-coded lightweight theme for Inked and Faded. No page builder, no
  jQuery, no webfont blocking the render. Built for Core Web Vitals and for display-ad
  networks: every ad slot reserves its height so injected units cannot shift layout.
Version: 0.9.9
Requires at least: 6.4
Requires PHP: 8.1
License: GPL-2.0-or-later
Text Domain: inked-and-faded
*/

/* ==========================================================================
   1. Tokens
   Light palette on bare :root; dark redefines the same names only.
   ========================================================================== */

:root {
  color-scheme: light dark;

  /* Brand palette, 2026-09-08:  #000000  #0C1821  #1B2A41  #324A5F
     A four-step navy ramp with no light end, so the light-mode surfaces are
     derived cool neutrals rather than the warm paper the site used before.
     #324A5F is the accent because it is the only step with enough lift to work
     as an interactive colour on white while still clearing AA.
     Backup palette, unused: #000000 #0E402D #295135 #5A6650. */
  --c-bg:            #f7f8fa;
  --c-surface:       #ffffff;
  --c-surface-alt:   #edf0f4;
  --c-border:        #dce2ea;
  --c-border-strong: #b3c0cf;
  --c-text:          #0c1821;   /* brand */
  --c-text-soft:     #3d4c5a;
  --c-text-faint:    #5a6875;
  --c-accent:        #324a5f;   /* brand */
  --c-accent-hover:  #1b2a41;   /* brand */
  --c-accent-soft:   #e9eef4;
  --c-on-accent:     #ffffff;
  /* Secondary colour, added 2026-09-09 on Jordan's note that the site reads as
     "very blue" with nothing to break it up. Old-school tattoo flash runs on a
     four-colour set - black, red, green, yellow - and yellow is the one that
     reads as "reference" rather than "warning" or "danger", which is exactly
     the job of the [iaf_takeaways] "short answer" box: it is the one thing on
     the page that should not look like every other panel. Used sparingly, on
     that one component, not scattered through the UI. */
  --c-accent-2:       #b8860b;
  --c-accent-2-hover: #8f6608;
  --c-accent-2-soft:  #fbf2dc;
  --c-on-accent-2:    #1c1305;
  --c-focus:         #2563eb;
  --c-ink:           #000000;   /* brand - footer ground */
  /* Constant dark ground. NOT redefined in the dark blocks: the newsletter
     widget and the cookie banner are dark in both themes by design, and
     using --c-text for them inverted them into white-on-white. */
  --c-panel:         #0c1821;
  --c-panel-text:    #ffffff;
  --c-panel-soft:    #c2cedb;
  --c-panel-faint:   #93a3b3;
  --c-panel-border:  #40566b;

  /* Type. Still a system stack: a webfont is render-blocking network cost on a
     site whose whole plan is display advertising. Modernised by leading with
     the variable UI faces and dropping the Georgia headings for a tight
     grotesque. A self-hosted variable face bundled into the theme is the next
     step up and costs one request. */
  --font-body: "Inter", ui-sans-serif, system-ui, -apple-system,
               "Segoe UI Variable Text", "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --font-heading: "Inter Tight", "Inter", ui-sans-serif, system-ui,
                  "Segoe UI Variable Display", "Segoe UI", Roboto,
                  "Helvetica Neue", Arial, sans-serif;

  --step--1: clamp(0.84rem, 0.81rem + 0.15vw, 0.92rem);
  --step-0:  clamp(1.05rem, 1.00rem + 0.25vw, 1.15rem);
  --step-1:  clamp(1.28rem, 1.17rem + 0.50vw, 1.50rem);
  --step-2:  clamp(1.60rem, 1.40rem + 0.95vw, 2.05rem);
  --step-3:  clamp(2.05rem, 1.70rem + 1.60vw, 3.00rem);

  --w-page:    1240px;
  --w-prose:   680px;
  --space:     clamp(1rem, 0.85rem + 0.7vw, 1.5rem);
  --radius:    10px;
  --radius-sm: 6px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --c-bg:            #0c1821;   /* brand */
    --c-surface:       #12212d;
    --c-surface-alt:   #1b2a41;   /* brand */
    --c-border:        #223649;
    --c-border-strong: #324a5f;   /* brand */
    --c-text:          #e8edf3;
    --c-text-soft:     #b0bdcb;
    --c-text-faint:    #8c9bab;
    --c-accent:        #86adcd;
    --c-accent-hover:  #a9c6de;
    --c-accent-soft:   #16283a;
    --c-on-accent:     #0c1821;
    --c-ink:           #000000;
    --c-accent-2:       #e0b04a;
    --c-accent-2-hover: #edc372;
    --c-accent-2-soft:  #2a2210;
    --c-on-accent-2:    #1c1305;
  }
}

:root[data-theme="dark"] {
  --c-bg:            #0c1821;
  --c-surface:       #12212d;
  --c-surface-alt:   #1b2a41;
  --c-border:        #223649;
  --c-border-strong: #324a5f;
  --c-text:          #e8edf3;
  --c-text-soft:     #b0bdcb;
  --c-text-faint:    #8c9bab;
  --c-accent:        #86adcd;
  --c-accent-hover:  #a9c6de;
  --c-accent-soft:   #16283a;
  --c-on-accent:     #0c1821;
  --c-ink:           #000000;
  --c-accent-2:       #e0b04a;
  --c-accent-2-hover: #edc372;
  --c-accent-2-soft:  #2a2210;
  --c-on-accent-2:    #1c1305;
}

/* ==========================================================================
   2. Reset and base
   ========================================================================== */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.65;
  text-rendering: optimizeLegibility;
}

img, video, svg { max-width: 100%; height: auto; }
/* Images must carry width/height so the browser can reserve the box.
   functions.php forces that on every attachment. */

a { color: var(--c-accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--c-accent-hover); }

:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
  border-radius: 2px;
}

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  line-height: 1.12;
  margin: 0 0 0.5em;
  font-weight: 700;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
h1 { font-size: var(--step-3); letter-spacing: -0.04em; }
h2 { font-size: var(--step-2); }
h3 { font-size: var(--step-1); letter-spacing: -0.02em; }

p, ul, ol, blockquote, table, figure { margin: 0 0 1.15em; }

hr { border: 0; border-top: 1px solid var(--c-border); margin: 2rem 0; }

.screen-reader-text {
  position: absolute !important;
  width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute; left: -9999px;
  background: var(--c-surface); color: var(--c-text);
  padding: 0.6rem 1rem; z-index: 100;
}
.skip-link:focus { left: 0.5rem; top: 0.5rem; }

/* ==========================================================================
   3. Layout - one ceiling, set once
   ========================================================================== */

.wrap {
  width: 100%;
  max-width: var(--w-page);
  margin-inline: auto;
  padding-inline: var(--space);
}

.site-main { padding-block: clamp(1.5rem, 1rem + 2vw, 3rem); }

/* A floor under the content, added v0.6.0.
   A page with three paragraphs used to end a third of the way down the screen,
   which put the footer halfway up the viewport and left the sticky sidebar
   stranded in mid-air beside nothing. This is not a fixed height - tall pages
   are unaffected - it is a minimum, so every page fills at least a screen and
   the furniture always has something to sit against.

   The body is the other half: a flex column with the main growing means the
   footer is pinned to the bottom of the viewport on a genuinely short page
   rather than floating in the middle of it. */
body {
  min-height: 100vh;
  min-height: 100svh;            /* mobile browser chrome; ignored where unsupported */
  display: flex;
  flex-direction: column;
}
.site-main { flex: 1 0 auto; }
.site-footer { flex: none; }

.site-main > .wrap,
.site-main > .layout { min-height: min(62vh, 40rem); }

/* Article + sidebar. The sidebar exists to hold a sticky ad unit; below
   1024px it drops under the content rather than squeezing the prose. */
.layout {
  display: grid;
  gap: clamp(1.5rem, 1rem + 2vw, 3rem);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 1024px) {
  /* Same reasoning as the rail layout in section 21: the article track is sized
     to the prose rather than to 1fr, because .prose caps the text at --w-prose
     and pins it left, so a 1fr track just pools its surplus into a dead strip
     beside the sidebar. `has-sidebar` is only ever on single.php, so capping it
     to the prose cannot reach an archive's card grid. */
  .layout.has-sidebar {
    grid-template-columns: minmax(0, var(--w-prose)) 300px;
    justify-content: center;
  }
}

/* ⚠ The sidebar only grows past 300px once the prose has its full measure.
   Written as a second breakpoint because grid has no way to say "shrink this
   track before that one": with minmax on both, the shortfall landed on the
   prose, and the article came out NARROWER at 1024px than it was before any of
   this. Priority has to be expressed as a width the wider track is allowed at
   all. */
@media (min-width: 1120px) {
  .layout.has-sidebar { grid-template-columns: minmax(0, var(--w-prose)) minmax(300px, 360px); }
}

/* Prose column. Line length is capped here, once, rather than by narrowing
   individual blocks - conventions.md. */
.prose { max-width: var(--w-prose); }
.prose > * { max-width: 100%; }
.entry-content.prose { margin-inline: 0; }

/* Full-bleed escape for figures inside the prose column. */
.prose .alignwide { max-width: min(100%, 860px); }

/* ==========================================================================
   4. Header
   ========================================================================== */

.site-header {
  border-bottom: 1px solid var(--c-border);
  background: var(--c-surface);
  position: sticky; top: 0; z-index: 20;
}

.site-header__inner {
  display: flex;
  /* wrap is load-bearing: the mobile nav drawer and the header search both use
     flex-basis:100% to claim their own row. Without it they silently squeeze
     onto the first line instead - which is what the search did on a phone until
     2026-09-09, and what the open nav drawer would have done too. */
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem var(--space);
  min-height: 60px;
  padding-block: 0.5rem;
  /* ⚠ This row is deliberately NOT inside a .wrap. It spans the viewport and
     carries its own gutter, so the wordmark sits against the left edge and the
     search against the right, rather than both being pulled into the 1240px
     article column. header.php has the matching note. */
  width: 100%;
  padding-inline: var(--space);
}

.site-brand {
  font-family: var(--font-heading);
  font-size: var(--step-1);
  font-weight: 600;
  color: var(--c-text);
  text-decoration: none;
  letter-spacing: -0.02em;
  margin-right: auto;
}
.site-brand:hover { color: var(--c-text); }

/* The wordmark. Sized by HEIGHT, because the two files are one aspect ratio and
   the header's own height is what the logo has to sit inside. The custom-logo
   class is the same rule so a Customizer upload does not blow the header open.
   ⚠ The <picture> needs display:block or the inline-box baseline gap adds a
   few stray pixels under the image and pushes the header taller than the nav. */
.site-brand--logo { display: block; line-height: 0; }
.site-brand--logo picture { display: block; }
.site-brand--logo img,
.custom-logo {
  display: block;
  width: auto;
  height: 30px;
  max-width: min(58vw, 260px);
  object-fit: contain;
}
.custom-logo-link { margin-right: auto; display: block; line-height: 0; }

@media (min-width: 768px) {
  .site-brand--logo img,
  .custom-logo { height: 34px; }

  /* ⚠ The brand's auto margin is dropped here and NOT in the base rule. On a
     phone it is the only thing holding the search and menu buttons against the
     right edge. On desktop those buttons are gone, so leaving it would split
     the free space between two auto margins and float the nav into the middle
     of the row; `.header-search { margin-left: auto }` does the whole job. */
  .site-brand,
  .custom-logo-link { margin-right: 0; }
}

/* The two mobile header buttons. Square, icon-only, same size as each other so
   they read as a pair rather than two unrelated controls. */
.icon-btn {
  display: none;
  place-items: center;
  width: 2.4rem; height: 2.4rem;
  padding: 0;
  background: none;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  color: var(--c-text);
  font: inherit;
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease;
}
.icon-btn:hover { border-color: var(--c-accent); color: var(--c-accent); }
.icon-btn svg { display: block; }

/* Hamburger, drawn in CSS: three bars that fold into a cross when open. Two
   elements' worth of box, one real - ::before and ::after are the outer bars and
   the span itself is the middle one, so the whole thing animates as one piece. */
.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  display: block;
  width: 17px; height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.18s ease;
}
.nav-toggle__bars { position: relative; }
.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-toggle[aria-expanded="true"] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after  { transform: translateY(-6px) rotate(-45deg); }

@media (prefers-reduced-motion: reduce) {
  .nav-toggle__bars, .nav-toggle__bars::before, .nav-toggle__bars::after { transition: none; }
}

.site-nav ul {
  list-style: none; display: flex; gap: 1.25rem;
  margin: 0; padding: 0;
}
.site-nav a {
  color: var(--c-text-soft);
  text-decoration: none;
  font-size: var(--step--1);
  font-weight: 500;
}
.site-nav a:hover,
.site-nav .current-menu-item > a { color: var(--c-accent); }

@media (max-width: 767px) {
  .icon-btn { display: grid; }
  .search-toggle { order: 3; }
  .nav-toggle { order: 4; }

  /* The drawer opens by animating grid-template-rows 0fr -> 1fr, which is the
     one way to transition to a content height CSS does not know in advance.
     display:none cannot animate at all, and a max-height guess either clips a
     long menu or eases against a number the menu never reaches.

     visibility, not display, keeps the links out of the tab order while shut -
     and unlike display it can be transitioned, so it holds until the collapse
     has finished playing. */
  /* ⚠ Row gap OFF here. `.site-header__inner` is a wrapping flex row, so a
     closed panel still creates a flex line and still pays the 0.5rem row gap
     for it - twice, once per panel. Measured: the header stayed 109px tall with
     both panels shut, when the brand row alone is 60. The panels carry their
     own padding when open, so nothing is lost. */
  .site-header__inner { row-gap: 0; }

  .site-nav,
  .header-search {
    order: 5;
    flex-basis: 100%;
    display: grid;
    /* minmax(0, Nfr), not a bare Nfr. A bare `fr` track is minmax(AUTO, Nfr),
       and that auto floor is the item's min-content height - so `0fr` collapsed
       to 17px of menu rather than to nothing. Pinning the minimum at 0 is what
       actually makes the track close. */
    grid-template-rows: minmax(0, 0fr);
    visibility: hidden;
    transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1),
                visibility 0s linear 0.3s;
  }
  .site-nav > *,
  .header-search > * { overflow: hidden; min-height: 0; }

  .site-nav.is-open,
  .header-search.is-open {
    grid-template-rows: minmax(0, 1fr);
    visibility: visible;
    transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1),
                visibility 0s linear 0s;
  }

  .site-nav ul { flex-direction: column; gap: 0.25rem; padding-block: 0.35rem 0.75rem; }
  .site-nav a { display: block; padding: 0.5rem 0; }
  .header-search .search-form { padding-bottom: 0.75rem; }

  @media (prefers-reduced-motion: reduce) {
    .site-nav, .header-search { transition: none; }
    .site-nav.is-open, .header-search.is-open { transition: none; }
  }
}

/* Desktop: no icon buttons, and the search is simply always there. */
@media (min-width: 768px) {
  .search-toggle { display: none; }
}

/* ==========================================================================
   5. Entry
   ========================================================================== */

.entry-header { margin-bottom: 1.5rem; }

/* The featured image. Rounded to the same radius as the cards and the callouts,
   so the one full-width image on the page is not the only square corner on it.
   The radius goes on the figure with overflow clipping rather than on the img,
   because the img is replaced content and a cropped one can paint over its own
   corners. */
.entry-media {
  border-radius: var(--radius);
  overflow: hidden;
}
.entry-media img { display: block; width: 100%; height: auto; }

/* The disclosure, added 2026-09-09, moved below the image 2026-09-09 (second
   pass): it started as an overlay on the image itself, and Jordan's note was
   that it should sit just clear of it instead, not on top - the image is the
   thing being looked at, the disclosure is a footnote to it. Plain flow now,
   right-aligned under the image, rather than an absolutely-positioned chip. */
.entry-media-wrap { margin-bottom: 1.5rem; }
.entry-media-wrap .entry-media { margin-bottom: 0.5rem; }
.entry-media-disclosure { text-align: right; }
.entry-media-disclosure .disclosure {
  display: inline-block;
  margin: 0;
}

.entry-meta {
  display: flex; flex-wrap: wrap; gap: 0.5rem 1rem;
  align-items: center;
  font-size: var(--step--1);
  color: var(--c-text-faint);
  margin-bottom: 0.75rem;
}
.entry-meta a { color: var(--c-text-soft); }

.eyebrow {
  display: inline-block;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-accent);
  text-decoration: none;
}

/* Byline. venture-profile.md section 4: an anonymous AI-run affiliate site is
   the worst possible combination of signals, so the author is a real element,
   not a line of grey text. */
.byline {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.85rem 0;
  border-block: 1px solid var(--c-border);
  margin-bottom: 1.5rem;
  font-size: var(--step--1);
}
.byline img { border-radius: 50%; flex: none; }
.byline__name { font-weight: 600; color: var(--c-text); }
.byline__meta { color: var(--c-text-faint); }

/* ⚠ The related block needs its own top margin. Its heading sits outside
   .entry-content, so it never picked up the 2rem that every other h2 on the
   page gets, and it landed hard against the author box's bottom rule. */
.related { margin-top: 2.75rem; }

.entry-content h2 { margin-top: 2rem; }
.entry-content h3 { margin-top: 1.5rem; }
.entry-content li { margin-bottom: 0.35em; }

.entry-content figure figcaption {
  font-size: var(--step--1);
  color: var(--c-text-faint);
  margin-top: 0.4rem;
}

/* In-body product shot. Supplier pack shots came in portrait (a tall bottle
   on white) and were capped at 380px so they did not swamp the paragraph
   introducing them - until Jordan's note 2026-09-09 that a tall image was
   costing too much vertical space on the page. tools/pad-product-shot.py now
   reframes every one onto a 1366x768 landscape canvas instead (the bottle
   stays its natural shape, centred on white, the FRAME is what goes wide),
   so the old narrow cap is gone: this fills its column instead.

   ⚠ The radius goes on the img, not on the figure the way .entry-media does it.
   The figcaption lives inside the figure, so clipping the figure would round the
   caption's box instead of the picture's. The reason .entry-media needs the
   figure is that its img is cropped and can paint over its own corners; this one
   is not cropped, so the img can carry the radius itself. */
.entry-content .product-shot {
  max-width: 100%;
  margin-inline: auto;
}
.entry-content .product-shot img {
  display: block;
  width: 100%; height: auto;
  border-radius: var(--radius);
}
.entry-content .product-shot figcaption { text-align: center; }

.entry-content table {
  width: 100%; border-collapse: collapse; font-size: var(--step--1);
}
.entry-content th, .entry-content td {
  padding: 0.55rem 0.7rem;
  border-bottom: 1px solid var(--c-border);
  text-align: left;
}
.entry-content thead th {
  background: var(--c-surface-alt);
  border-bottom-color: var(--c-border-strong);
}
/* Wide content scrolls inside its own box; the page body never scrolls
   sideways. */
.table-scroll { overflow-x: auto; margin-bottom: 1.15em; }

blockquote {
  /* No thick left-border accent bar - conventions.md calls it an AI-design
     tell. A surface and a border instead. */
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: 1rem 1.15rem;
  font-style: italic;
  color: var(--c-text-soft);
}
blockquote > :last-child { margin-bottom: 0; }

/* ==========================================================================
   6. Affiliate disclosure
   Sits adjacent to the content it qualifies, never only in the footer.
   ========================================================================== */

.disclosure {
  font-size: var(--step--1);
  color: var(--c-text-soft);
  background: var(--c-accent-soft);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: 0.7rem 0.9rem;
  margin-bottom: 1.5rem;
}
.disclosure p { margin: 0; }

/* ==========================================================================
   7. Table of contents
   ========================================================================== */

.toc {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: 1rem 1.15rem;
  margin-bottom: 1.75rem;
  font-size: var(--step--1);
}
.toc__title {
  font-family: var(--font-heading);
  font-weight: 600; margin: 0 0 0.5rem;
  font-size: var(--step-0);
}
.toc ol { margin: 0; padding-left: 1.1rem; }
.toc li { margin-bottom: 0.25rem; }
.toc a { color: var(--c-text-soft); text-decoration: none; }
.toc a:hover { color: var(--c-accent); text-decoration: underline; }

/* ==========================================================================
   8. Cards
   Fixed column counts, never auto-fit, and a stranded last card stretches -
   conventions.md: grid rows must be full.
   ========================================================================== */

.card-grid {
  display: grid; gap: var(--space);
  grid-template-columns: 1fr;
  list-style: none; margin: 0; padding: 0;
}
@media (min-width: 600px)  { .card-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .card-grid { grid-template-columns: repeat(3, 1fr); } }

@media (min-width: 600px) and (max-width: 1023px) {
  .card-grid > :last-child:nth-child(2n + 1) { grid-column: 1 / -1; }
}
@media (min-width: 1024px) {
  .card-grid > :last-child:nth-child(3n + 1) { grid-column: 1 / -1; }
}

.card {
  display: flex; flex-direction: column;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  overflow: hidden;
}
.card__media { aspect-ratio: 16 / 9; background: var(--c-surface-alt); }
.card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card__body { padding: 0.9rem 1rem 1.1rem; display: flex; flex-direction: column; gap: 0.4rem; }
.card__title { font-size: var(--step-1); margin: 0; }
.card__title a { color: var(--c-text); text-decoration: none; }
.card__title a:hover { color: var(--c-accent); }
.card__excerpt { color: var(--c-text-soft); font-size: var(--step--1); margin: 0; }
.card__meta { margin-top: auto; padding-top: 0.5rem; font-size: var(--step--1); color: var(--c-text-faint); }

/* ==========================================================================
   9. Ad slots
   Every slot reserves its height. An injected unit that changes layout is a
   CLS penalty on a site whose whole plan is display revenue.
   ========================================================================== */

.ad-slot {
  display: flex; align-items: center; justify-content: center;
  margin-block: 2rem;
  background: var(--c-surface-alt);
  border-radius: var(--radius);
  overflow: hidden;
}
.ad-slot::before {
  content: "Advertisement";
  font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--c-text-faint);
}
.ad-slot--leaderboard { min-height: 100px; }
@media (min-width: 768px) { .ad-slot--leaderboard { min-height: 250px; } }
.ad-slot--in-content { min-height: 280px; }
/* The sidebar column is wider than 300px on a rail layout, but the unit that
   gets injected into it is a standard 300x600. Cap the reservation so the
   space it holds matches the space it will actually use. */
.ad-slot--sidebar { min-height: 600px; max-width: 300px; margin-inline: auto; }

.sidebar__sticky { position: sticky; top: 80px; }

/* No rule between the article and the sidebar (Jordan, 2026-09-10), and none
   between the article and the rail either - see .rail-col. Both were added so
   the columns read as running the full height of the article; the gutter does
   that job now. Do not put them back without asking. */

/* ==========================================================================
   10. Pagination, footer
   ========================================================================== */

.pagination { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 2.5rem; }
.pagination .page-numbers {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.25rem; height: 2.25rem; padding-inline: 0.5rem;
  border: 1px solid var(--c-border); border-radius: var(--radius);
  text-decoration: none; color: var(--c-text-soft);
}
.pagination .page-numbers:hover { border-color: var(--c-border-strong); color: var(--c-accent); }
.pagination .page-numbers.current {
  background: var(--c-accent); border-color: var(--c-accent); color: var(--c-on-accent);
}

/* Footer, rebuilt v0.6.0.
   It was one row - a menu left, a legal line right - which is the right amount
   of footer for a site with nothing in it. On a content site the footer is an
   internal-linking surface, so it is now a brand block plus four link columns
   plus a base row, on the constant dark ground the theme already uses for the
   newsletter and the consent panel. */

/* Two-tone dark grey, replacing the flat brand-ink ground from v0.6.0 - the
   solid navy-black read as a slab with nothing going on. A gradient between
   two neutral greys instead of two brand-navy steps, so the footer reads as
   its own surface rather than a darker copy of the article background. */
/* ⛔ padding-block STARTS AT ZERO and that is deliberate. The newsletter band
   is the first child, so any top padding here renders as an empty mid-grey bar
   above it - which is exactly what it did until 2026-09-09. The room the footer
   content needs is on .site-footer__inner instead, below the band. */
.site-footer {
  position: relative;
  isolation: isolate;
  margin-top: 4rem;
  padding-block: 0 1.5rem;
  background: linear-gradient(180deg, #24262a 0%, #17181a 100%);
  color: var(--c-panel-soft);
  font-size: var(--step--1);
}

/* The dot-map. A tiled field of small dots rather than the flash-mark tile -
   masked the other way round from before: gone at the top edge, densest by
   the base row, so it reads as texture rising from the bottom rather than a
   pattern stamped over the whole panel. Plain CSS radial-gradient, no SVG or
   PHP tile needed, so --iaf-flash stays footer-free (still used nowhere else -
   grep confirmed before this change). */
.site-footer__field {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.5) 1px, transparent 1.5px);
  background-size: 16px 16px;
  opacity: 0.1;
  -webkit-mask-image: linear-gradient(to top, #000 0%, #000 30%, transparent 85%);
  mask-image: linear-gradient(to top, #000 0%, #000 30%, transparent 85%);
  pointer-events: none;
}

/* The padding-top is load-bearing: it is the only gap between the newsletter
   band's bottom hairline and the wordmark, and without it the brand block sits
   hard against the strip. */
.site-footer__inner {
  display: grid;
  gap: clamp(1.75rem, 1rem + 2.5vw, 3rem);
  grid-template-columns: 1fr;
  padding-top: clamp(2.5rem, 1.75rem + 2.5vw, 3.75rem);
}
@media (min-width: 900px) {
  .site-footer__inner { grid-template-columns: minmax(0, 19rem) minmax(0, 1fr); }
  .site-footer__base { grid-column: 1 / -1; }
}

.site-footer a { color: var(--c-panel-soft); text-decoration: none; }
.site-footer a:hover { color: #fff; text-decoration: underline; }

/* --- newsletter band ---
   Its own strip, first thing inside <footer> and flush with its top edge, on a
   ground one step lighter than the footer's own gradient so it reads as a
   deliberate band rather than part of the field behind it. A hairline on each
   edge is what actually sells "strip breaking up the page from the footer" -
   without it the band and the footer below just look like one continuous panel.

   Rebuilt 2026-09-09 as a strip one control tall: heading left, form right, and
   in the footer variant nothing else in it at all - the description and the
   small print are both skipped by the partial, which is what lets this be one
   row rather than three. Still a grid rather than a flex row because the two
   sides have to centre against each other whatever the heading wraps to. */
.site-footer__newsletter {
  position: relative;
  z-index: 1;
  background: #2e3034;
  border-block: 1px solid rgba(255, 255, 255, 0.08);
  padding-block: 0.85rem;
}
.site-footer__newsletter .widget--signup-footer {
  display: grid;
  /* ⚠ The form column is a real track, not `auto`. On auto it sizes to the
     input's content and the form's own `width: min(24rem, 100%)` has nothing
     to resolve the percentage against, so the field lands ~80px narrower than
     asked for. */
  grid-template-columns: minmax(0, 1fr) min(24rem, 42%);
  grid-template-areas: "title form";
  align-items: center;
  column-gap: 2rem;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
}
.site-footer__newsletter .widget__title {
  grid-area: title;
  margin: 0;
  font-size: var(--step-0);
  line-height: 1.2;
}
/* Nothing sets `grid-area: note` any more - the footer variant renders no small
   print at all. Left as a guard: if one is ever put back it lands in the flow
   rather than in an undeclared area, and this is the note saying it will need a
   second row declared before it looks right. */
.site-footer__newsletter .widget__small { margin: 0; font-size: 0.72rem; color: var(--c-panel-faint); }
/* ⛔ flex-direction is the whole reason this rule exists. `.widget--signup form`
   sets column for the sidebar, and this selector does not out-specify it by
   accident - state row explicitly or the input takes its flex-basis as a height
   and the band grows to 300px again. */
.site-footer__newsletter .widget--signup-footer form {
  grid-area: form;
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  gap: 0.5rem;
  width: min(24rem, 100%);
  margin: 0;
}
.site-footer__newsletter .widget--signup-footer input[type="email"] {
  flex: 1 1 10rem;
  min-width: 0;
  padding: 0.5rem 0.8rem;
  font: inherit;
  font-size: var(--step--1);
  /* ⛔ NOT --c-text. This band is dark in both themes, so in dark mode
     --c-text resolves to near-white and the address the reader types vanishes
     into the white field. --c-panel is the constant dark ink, which is exactly
     why the sidebar variant already uses it here. Same trap as the note in the
     token block at the top of this file, walked into a second time. */
  color: var(--c-panel);
  background: #fff;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
}
.site-footer__newsletter .widget--signup-footer button {
  flex: none;
  padding: 0.5rem 1.1rem;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--c-panel);
  background: #fff;
  border: 1px solid #fff;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.site-footer__newsletter .widget--signup-footer button:hover { background: #e9eef4; border-color: #e9eef4; }

/* One column below the point where the heading and a 24rem form stop fitting
   side by side. */
@media (max-width: 719px) {
  .site-footer__newsletter { padding-block: 1.1rem; }
  .site-footer__newsletter .widget--signup-footer {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "title"
      "form";
    row-gap: 0.6rem;
  }
  .site-footer__newsletter .widget--signup-footer form { width: 100%; }
}

/* --- brand block --- */
.site-footer__brand { display: flex; flex-direction: column; gap: 1rem; align-items: flex-start; }
.site-footer .custom-logo-link img { max-height: 3rem; width: auto; }

.site-footer__logo {
  display: inline-flex; align-items: center; gap: 0.65rem;
  text-decoration: none;
}
.site-footer__logo:hover { text-decoration: none; }

/* The shipped wordmark, sized by height like the header's - the file is
   940x160 and only ever scales, never crops. Larger here than in the header
   (30/34px): the header logo shares a row with the nav and the search, the
   footer one is the only thing in its column and reads as undersized at the
   header's height. */
.site-footer__logo--img { display: block; line-height: 0; }
.site-footer__logo--img img {
  display: block;
  width: auto;
  height: 42px;
  max-width: min(70vw, 250px);
  object-fit: contain;
}
.site-footer__logo-mark {
  display: grid; place-items: center;
  width: 2.6rem; height: 2.6rem;
  border: 1px solid var(--c-panel-border);
  border-radius: var(--radius-sm);
  background: var(--c-panel);
  font-family: var(--font-heading);
  font-size: 0.95rem; font-weight: 700; letter-spacing: 0.04em;
  color: #fff;
}
.site-footer__logo-name {
  font-family: var(--font-heading);
  font-size: var(--step-1); font-weight: 700; letter-spacing: -0.015em;
  color: #fff;
}
/* Placeholder until Customize > Site Identity > Logo is set. Marked in the
   markup as well as here so it is obvious in view-source that it is standing in
   for something, not the finished mark. */
.site-footer__logo.is-placeholder .site-footer__logo-mark { border-style: dashed; }

.site-footer__tagline { margin: 0; color: var(--c-panel-faint); max-width: 32ch; }

/* --- the four link columns --- */
.site-footer__cols {
  display: grid;
  gap: 1.75rem 1.5rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 720px)  { .site-footer__cols { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

.site-footer__col-title {
  margin: 0 0 0.7rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-panel-faint);
}
.site-footer__col-title a { color: inherit; }
.site-footer__col-title a:hover { color: #fff; }

.site-footer__col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.site-footer__col a { line-height: 1.35; display: inline-block; }

/* --- base row --- */
.site-footer__base {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.6rem 1.5rem;
  margin-top: 0.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--c-panel-border);
}
.site-footer__legal-nav ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem;
}
.site-footer__legal { margin: 0 0 0 auto; color: var(--c-panel-faint); }

/* The way back into cookie preferences, now that the plugin's floating badge is
   hidden. A button, not a link, because it opens a dialog. */
.site-footer__consent {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--c-panel-soft);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  cursor: pointer;
}
.site-footer__consent:hover { color: #fff; }

/* ⛔ The plugin's own floating re-consent badge, removed 2026-09-09 on request:
   it sat over the content bottom-left on every page. Hiding it is only safe
   because the base row above puts the same control somewhere a reader can find
   it - withdrawing consent has to stay as easy as giving it. Do not delete that
   button without putting this one back. */
.cookieadmin_re_consent { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   11. Breadcrumbs
   The trail is data from iaf_breadcrumb_trail(); the same array feeds the
   BreadcrumbList in inc/schema.php.
   ========================================================================== */

.breadcrumbs {
  font-size: var(--step--1);
  color: var(--c-text-faint);
  margin-bottom: 1.25rem;
}
.breadcrumbs ol {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.15rem 0.4rem;
}
.breadcrumbs li { display: flex; align-items: center; gap: 0.4rem; }
.breadcrumbs li + li::before {
  content: "/";
  color: var(--c-border-strong);
}
.breadcrumbs a { color: var(--c-text-soft); text-decoration: none; }
.breadcrumbs a:hover { color: var(--c-accent); text-decoration: underline; }
.breadcrumbs [aria-current="page"] {
  color: var(--c-text-faint);
  /* The current crumb is often the full post title. Let it truncate rather
     than wrap the trail onto three lines on a phone. */
  max-width: 22ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (min-width: 600px) { .breadcrumbs [aria-current="page"] { max-width: 45ch; } }

/* ==========================================================================
   12. Front page
   Entry points, not a feed - see the note at the top of front-page.php.
   ========================================================================== */

.hero {
  padding-block: clamp(1.5rem, 1rem + 3vw, 3.5rem);
  border-bottom: 1px solid var(--c-border);
  margin-bottom: 2rem;
}
.hero__title {
  font-size: var(--step-3);
  line-height: 1.06;
  letter-spacing: -0.02em;
  max-width: 20ch;              /* the h1 is a phrase now, not a two-word brand */
  margin: 0 0 0.6rem;
  text-wrap: balance;
}
.hero__tagline {
  font-size: var(--step-1);
  color: var(--c-text-soft);
  max-width: var(--w-prose);
  margin: 0 0 1.5rem;
}
.hero__search { max-width: 34rem; }

.home-section { margin-bottom: clamp(2rem, 1.5rem + 2vw, 3.5rem); }
.home-section__title {
  font-size: var(--step-2);
  margin: 0 0 1.25rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--c-border);
}

/* Topic cards.
   Replaced .topic-list on 2026-09-09. The old version was a dense grid of
   small bordered boxes: correct information, no pull. These are three per row,
   photographic, and sized so a topic is a destination rather than a line item.

   The grid is six columns and each card spans two, which is what makes a
   short last row close up: two cards span three each, one card spans six.
   That is the same "grid rows must be full" rule .card-grid follows, done with
   spans instead of a media-query pile.

   Every card is legible with no image at all - the data-topic gradients below
   are the resting state, and the photograph, when there is one, sits under the
   same scrim either way. */

.topic-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--space);
  grid-template-columns: 1fr;
}
@media (min-width: 600px) {
  .topic-grid { grid-template-columns: repeat(6, 1fr); }
  .topic-card { grid-column: span 3; }
}
@media (min-width: 900px) {
  .topic-card { grid-column: span 2; }
  /* Last row of two, and last row of one. */
  .topic-card:nth-child(3n + 1):nth-last-child(2),
  .topic-card:nth-child(3n + 1):nth-last-child(2) ~ .topic-card { grid-column: span 3; }
  .topic-card:nth-child(3n + 1):last-child { grid-column: span 6; }
}
@media (min-width: 600px) and (max-width: 899px) {
  .topic-card:nth-child(2n + 1):last-child { grid-column: span 6; }
}

.topic-card__link {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: clamp(12rem, 9rem + 9vw, 17rem);
  padding: 1.25rem 1.4rem;
  border-radius: var(--radius);
  overflow: hidden;
  isolation: isolate;
  text-decoration: none;
  /* Constant dark ground under the scrim: these cards are dark in both themes
     by design, the same decision the newsletter widget and the cookie banner
     already make. White type on them then needs no per-theme contrast check. */
  background: var(--c-panel);
}

.topic-card__img {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.topic-card__link:hover .topic-card__img { transform: scale(1.05); }

/* The scrim is the reason the type survives whatever photograph lands behind
   it: near-opaque at the bottom where the words are, thinning upwards so the
   picture is still a picture. */
.topic-card__scrim {
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(
    to top,
    rgba(12, 24, 33, 0.94) 0%,
    rgba(12, 24, 33, 0.80) 32%,
    rgba(12, 24, 33, 0.46) 66%,
    rgba(12, 24, 33, 0.28) 100%
  );
}

.topic-card__body {
  position: relative; z-index: 3;
  display: flex; flex-direction: column; gap: 0.15rem;
}
.topic-card__name {
  font-family: var(--font-heading);
  font-size: var(--step-2);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: #fff;
}
.topic-card__count {
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--c-panel-soft);
}

/* No image: a two-tone wash cycled by data-topic so neighbouring cards never
   repeat. Every stop is one of the four brand steps - #000000 #0C1821 #1B2A41
   #324A5F - and nothing here reaches for the unused green backup palette. Five
   navy cards read as a set; three navy and two green read as a mistake.
   Painted on the scrim, which already sits above the (absent) image. */
.topic-card[data-topic="0"] .topic-card__scrim { background-image: linear-gradient(150deg, #1b2a41 0%, #0c1821 100%); }
.topic-card[data-topic="1"] .topic-card__scrim { background-image: linear-gradient(150deg, #324a5f 0%, #1b2a41 100%); }
.topic-card[data-topic="2"] .topic-card__scrim { background-image: linear-gradient(150deg, #0c1821 0%, #324a5f 100%); }
.topic-card[data-topic="3"] .topic-card__scrim { background-image: linear-gradient(150deg, #1b2a41 0%, #000000 100%); }
.topic-card[data-topic="4"] .topic-card__scrim { background-image: linear-gradient(150deg, #324a5f 0%, #0c1821 100%); }
.topic-card[data-topic="5"] .topic-card__scrim { background-image: linear-gradient(150deg, #0c1821 0%, #1b2a41 100%); }
/* ...but only when there is no image. With one, the gradient above is dropped
   and the plain scrim rule wins back. */
.topic-card:has(.topic-card__img) .topic-card__scrim { background-image: none; }

/* The wash sits between the scrim and the words. */
.topic-card__link.btn-wash::after { z-index: 2; }
.topic-card__link { --wash-color: #1b2a41; }

@media (prefers-reduced-motion: reduce) {
  .topic-card__img { transition: none; }
  .topic-card__link:hover .topic-card__img { transform: none; }
}

/* ==========================================================================
   13. Author archive
   ========================================================================== */

.author-header { margin-bottom: 2rem; }
.author-card {
  display: flex; gap: 1.25rem; align-items: flex-start;
  padding: 1.25rem;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
}
.author-card img { border-radius: 50%; flex: none; }
.author-card .entry-title { font-size: var(--step-2); margin: 0 0 0.4rem; }
.author-card__bio { color: var(--c-text-soft); }
.author-card__bio p { margin-bottom: 0.5em; }
.author-card__meta {
  margin: 0.5rem 0 0;
  font-size: var(--step--1);
  color: var(--c-text-faint);
}
.author-card__meta a { color: var(--c-text-soft); }

@media (max-width: 599px) {
  .author-card { flex-direction: column; gap: 0.85rem; }
}

/* ==========================================================================
   14. Search form
   searchform.php shipped in v0.1.0 with no styles at all, so the form fell
   back to browser defaults - on the front-page hero, the 404 page, the search
   results header and the empty-loop state. Focus is handled by the global
   :focus-visible rule; these are the resting styles only.
   ========================================================================== */

.search-form {
  display: flex;
  gap: 0.5rem;
}

/* The input is wrapped on every instance so one box model serves the header,
   the hero and the 404 page - and so the animated ghost line has something to
   be positioned against. */
.search-form__field {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;                 /* a flex child will not shrink below its
                                   intrinsic width without this */
  display: flex;
}

.search-form input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.55rem 0.75rem;
  font: inherit;
  font-size: var(--step-0);
  color: var(--c-text);
  background: var(--c-surface);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  -webkit-appearance: none;
  appearance: none;
}
.search-form input[type="search"]::placeholder { color: var(--c-text-faint); }
.search-form input[type="search"]:hover { border-color: var(--c-text-faint); }

.search-form button[type="submit"] {
  flex: none;
  padding: 0.55rem 1.1rem;
  font: inherit;
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--c-on-accent);
  background: var(--c-accent);
  border: 1px solid var(--c-accent);
  border-radius: var(--radius);
  cursor: pointer;
}
/* The hover colour change is the .btn-wash job now - see section 14b. This is
   only the fallback for a browser that gets no wash, and for :focus-visible. */
.search-form button[type="submit"]:focus-visible {
  background: var(--c-accent-hover);
  border-color: var(--c-accent-hover);
}

/* --- The typing hero box ---------------------------------------------------
   Ghost line, not a rewritten placeholder attribute. Three reasons: a
   placeholder cannot carry a caret, an assistive technology reading a
   placeholder that rewrites itself six times a minute gets nonsense, and if
   the script never runs the real placeholder is still sitting in the markup.

   So the attribute stays, JS only paints this layer over it, and the
   placeholder is hidden by colour - never by deleting it. */
.search-form__ghost {
  position: absolute; inset: 0;
  display: none;
  align-items: center;
  padding: 0.55rem 0.75rem;
  font-size: var(--step-0);
  line-height: 1;
  color: var(--c-text-faint);
  white-space: nowrap;
  overflow: hidden;
  pointer-events: none;         /* the input underneath still takes the click */
}
.search-form--typing.is-typing .search-form__ghost { display: flex; }
.search-form--typing.is-typing input[type="search"]::placeholder { color: transparent; }
.search-form--typing:focus-within .search-form__ghost { display: none; }

.search-form__caret {
  display: inline-block;
  width: 2px;
  height: 1.15em;
  margin-left: 2px;
  background: var(--c-accent);
  animation: iaf-caret 1.06s steps(1, end) infinite;
}
@keyframes iaf-caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  /* theme.js never starts the animation under reduced motion, so .is-typing is
     never set and the ghost never shows. This is only the belt to that brace. */
  .search-form__ghost { display: none !important; }
  .search-form__caret { animation: none; }
}

/* ==========================================================================
   14b. Hover wash
   A solid colour floods a control from the point the cursor entered it, and
   drains back to that point on the way out. theme.js writes --wash-x/--wash-y
   from the pointer and --wash-d from the element's own diagonal; the defaults
   here mean a browser with no JavaScript still gets the effect, just always
   from the centre.
   ========================================================================== */

.btn-wash {
  position: relative;
  overflow: hidden;
  isolation: isolate;           /* so z-index:-1 lands above the background
                                   rather than behind the page */
}
.btn-wash::after {
  content: "";
  position: absolute;
  z-index: -1;
  top: var(--wash-y, 50%);
  left: var(--wash-x, 50%);
  width: var(--wash-d, 22rem);
  height: var(--wash-d, 22rem);
  margin: calc(var(--wash-d, 22rem) / -2);
  border-radius: 50%;
  background: var(--wash-color, var(--c-accent-hover));
  transform: scale(0);
  transition: transform 0.42s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
}
.btn-wash:hover::after,
.btn-wash:focus-visible::after { transform: scale(1); }

@media (prefers-reduced-motion: reduce) {
  .btn-wash::after { transition: none; }
}
/* A touch screen has no cursor to enter from, and :hover sticks after a tap.
   Skip the whole thing there and let the resting colour stand. */
@media (hover: none) {
  .btn-wash::after { content: none; }
}

/* ==========================================================================
   15. Editorial components  (inc/components.php)
   Each of these exists because the 2026 content audit found the site needed
   it - see docs/2026-09-08_content-validation.md.
   ========================================================================== */

/* Callouts.
   Redesigned 2026-09-08. The previous version used a thick coloured left-hand
   border, which has become a visual tell of machine-written copy - it is the
   default admonition style in a dozen doc generators and every LLM reaches for
   it. This does the same job differently: a flat tinted panel, a hairline ring,
   and the label carried on a small chip that sits ON the top edge rather than
   inside the box. No left bar anywhere. */

.callout {
  position: relative;
  margin-block: 2.25rem 1.75rem;
  padding: 1.35rem 1.25rem 1.15rem;
  border-radius: var(--radius);
  border: 1px solid var(--c-border);
  background: var(--c-surface-alt);
}
.callout__title {
  position: absolute;
  top: 0;
  left: 1rem;
  transform: translateY(-50%);
  margin: 0;
  padding: 0.28rem 0.7rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-on-accent);
  background: var(--c-text-soft);
  white-space: nowrap;
}
.callout__body > *:last-child { margin-bottom: 0; }
.callout__body { color: var(--c-text); }

.callout--note    { background: var(--c-surface-alt); }
.callout--tip     { background: #eaf3ee; border-color: #cbe0d5; }
.callout--tip .callout__title    { background: #2f6f52; }
.callout--warning { background: #fbf1e6; border-color: #ecd8bd; }
.callout--warning .callout__title { background: #92591b; }
/* Safety is the loudest thing in an article by design - it is the one that
   says stop reading and see a doctor. */
.callout--safety  { background: var(--c-accent-soft); border-color: var(--c-accent); }
.callout--safety .callout__title  { background: var(--c-accent); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .callout--tip     { background: #142a20; border-color: #234836; }
  :root:not([data-theme="light"]) .callout--warning { background: #2a2016; border-color: #4a3a23; }
  :root:not([data-theme="light"]) .callout--tip .callout__title     { background: #3f8a68; color: #06120c; }
  :root:not([data-theme="light"]) .callout--warning .callout__title { background: #c08239; color: #1b1206; }
  :root:not([data-theme="light"]) .callout__title   { color: #0c1821; }
}
:root[data-theme="dark"] .callout--tip     { background: #142a20; border-color: #234836; }
:root[data-theme="dark"] .callout--warning { background: #2a2016; border-color: #4a3a23; }
:root[data-theme="dark"] .callout--tip .callout__title     { background: #3f8a68; color: #06120c; }
:root[data-theme="dark"] .callout--warning .callout__title { background: #c08239; color: #1b1206; }
:root[data-theme="dark"] .callout__title   { color: #0c1821; }

/* Key takeaways - the answer up front, and the one box on the page that gets
   the secondary colour. Every other panel on the site sits on a neutral or
   navy ground; this is the only thing that should read as "different" at a
   glance, because it is the shortcut a skimming reader is looking for. */
.takeaways {
  margin-block: 1.75rem;
  padding: 1.15rem 1.3rem;
  background: var(--c-accent-2-soft);
  border: 1px solid var(--c-accent-2);
  border-radius: var(--radius);
}
.takeaways__title {
  margin: 0 0 0.6rem;
  font-family: var(--font-heading);
  font-size: var(--step-1);
  font-weight: 600;
  color: var(--c-text);
}
.takeaways ul { margin: 0; padding-left: 1.1rem; }
.takeaways li { margin-bottom: 0.4rem; }
.takeaways li:last-child { margin-bottom: 0; }
.takeaways li::marker { color: var(--c-accent-2-hover); }

/* The affiliate tie-in, added 2026-09-09: brand-profile.md §4 now requires
   that a post carrying an affiliate link surface it here too, not only at the
   bottom [iaf_verdict]. Optional - iaf_shortcode_takeaways() only prints this
   when the shortcode is given a cta_url. */
.takeaways__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.85rem;
  padding-top: 0.85rem;
  border-top: 1px solid color-mix(in srgb, var(--c-accent-2) 35%, transparent);
  font-weight: 600;
  color: var(--c-accent-2-hover);
  text-decoration: none;
}
.takeaways__cta:hover { color: var(--c-accent-2); text-decoration: underline; }

/* Product box. */
.product {
  display: flex;
  flex-wrap: wrap;
  gap: 1.15rem;
  margin-block: 1.75rem;
  padding: 1.15rem;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
}
.product__media { flex: none; width: 120px; }
.product__media img {
  width: 100%; height: auto; border-radius: var(--radius);
  background: var(--c-surface-alt);
}
.product__body { flex: 1 1 16rem; min-width: 0; }
.product__eyebrow {
  margin: 0 0 0.25rem;
  font-size: var(--step--1); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--c-accent);
}
.product__name { margin: 0 0 0.5rem; font-size: var(--step-1); }
.product__note > *:last-child { margin-bottom: 0; }
.product__foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1rem;
  margin-top: 0.9rem;
}
.product__price { font-weight: 600; }
/* Dating the check is the point - the audit found six dead ASINs on pages
   that gave no clue how old their links were. */
.product__checked { font-size: var(--step--1); color: var(--c-text-faint); }

.button {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.5rem 1.15rem;
  font-weight: 600;
  color: var(--c-on-accent);
  background: var(--c-accent);
  border: 1px solid var(--c-accent);
  border-radius: var(--radius);
  text-decoration: none;
}
.button:hover {
  color: var(--c-on-accent);
  background: var(--c-accent-hover);
  border-color: var(--c-accent-hover);
}

/* Pros and cons - two columns that do not look identical. */
.proscons {
  display: grid; gap: 1rem;
  grid-template-columns: minmax(0, 1fr);
  margin-block: 1.75rem;
}
@media (min-width: 600px) { .proscons { grid-template-columns: 1fr 1fr; } }
.proscons__col {
  padding: 1rem 1.15rem;
  border-radius: var(--radius);
  border: 1px solid var(--c-border);
}
.proscons__col--pros { background: #eaf3ee; border-color: #cbe0d5; }
.proscons__col--cons { background: var(--c-surface-alt); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .proscons__col--pros {
    background: #142a20; border-color: #234836;
  }
}
:root[data-theme="dark"] .proscons__col--pros {
  background: #142a20; border-color: #234836;
}
.proscons__title {
  margin: 0 0 0.5rem;
  font-size: var(--step--1); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--c-text-soft);
}
.proscons ul { margin: 0; padding-left: 1.1rem; }
.proscons li { margin-bottom: 0.3rem; }
.proscons li:last-child { margin-bottom: 0; }

/* Verdict. */
.verdict {
  margin-block: 2rem;
  padding: 1.15rem 1.3rem;
  border: 1px solid var(--c-accent);
  border-radius: var(--radius);
  background: var(--c-accent-soft);
}
.verdict__title {
  display: inline-block;
  margin: 0 0 0.5rem;
  padding: 0.28rem 0.7rem;
  border-radius: 999px;
  font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--c-on-accent);
  background: var(--c-accent);
}
.verdict__pick {
  margin: 0 0 0.5rem;
  font-family: var(--font-heading);
  font-size: var(--step-2);
  font-weight: 600;
}
.verdict__body > *:last-child { margin-bottom: 0; }

/* ==========================================================================
   16. Author box, category pillar, chips
   ========================================================================== */

.author-box {
  display: flex; gap: 1rem; align-items: flex-start;
  margin-top: 2.5rem;
  padding: 1.15rem;
  border-top: 1px solid var(--c-border);
  border-bottom: 1px solid var(--c-border);
}
.author-box img { border-radius: 50%; flex: none; }
.author-box__label {
  margin: 0; font-size: var(--step--1);
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--c-text-faint);
}
.author-box__name { margin: 0.1rem 0 0.35rem; font-weight: 600; }
.author-box__name a { color: var(--c-text); text-decoration: none; }
.author-box__name a:hover { color: var(--c-accent); }
.author-box__bio { margin: 0; color: var(--c-text-soft); font-size: var(--step--1); }

/* The category pillar - one post promoted above the grid. */
.pillar {
  display: grid; gap: 1.25rem;
  grid-template-columns: minmax(0, 1fr);
  margin-bottom: 2.5rem;
  padding-bottom: 2rem;
  border-bottom: 1px solid var(--c-border);
}
@media (min-width: 768px) { .pillar { grid-template-columns: 1.2fr 1fr; align-items: center; } }
.pillar__media { display: block; border-radius: var(--radius); overflow: hidden; }
.pillar__media img { width: 100%; height: auto; display: block; aspect-ratio: 16 / 9; object-fit: cover; }
.pillar__title { font-size: var(--step-2); margin: 0.3rem 0 0.5rem; }
.pillar__title a { color: var(--c-text); text-decoration: none; }
.pillar__title a:hover { color: var(--c-accent); }
.pillar__excerpt { color: var(--c-text-soft); margin: 0 0 0.5rem; }

.archive-header__intro { margin-top: 0.5rem; color: var(--c-text-soft); }

.chip-row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 2rem; }
.chip {
  display: inline-flex; align-items: center;
  padding: 0.35rem 0.8rem;
  font-size: var(--step--1);
  border: 1px solid var(--c-border-strong);
  border-radius: 999px;
  color: var(--c-text-soft);
  text-decoration: none;
}
.chip:hover { border-color: var(--c-accent); color: var(--c-accent); }

/* ==========================================================================
   17. Numbered lists - brand-coloured, bold numerals
   Counters rather than list-style, because a native marker cannot be styled.
   Scoped to .entry-content so the TOC, pagination and plugin lists are left
   alone.
   ========================================================================== */

.entry-content ol:not(.no-counter) {
  list-style: none;
  counter-reset: iaf-ol;
  padding-left: 0;
}
.entry-content ol:not(.no-counter) > li {
  counter-increment: iaf-ol;
  position: relative;
  padding-left: 2.4rem;
  margin-bottom: 0.6em;
}
.entry-content ol:not(.no-counter) > li::before {
  content: counter(iaf-ol) ".";
  position: absolute;
  left: 0;
  top: 0;
  min-width: 1.8rem;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.05em;
  letter-spacing: -0.02em;
  color: var(--c-accent);
  font-variant-numeric: tabular-nums;
}
.entry-content ol ol { counter-reset: iaf-ol; margin-top: 0.5em; }

.entry-content ul:not(.no-marker) { list-style: none; padding-left: 0; }
.entry-content ul:not(.no-marker) > li {
  position: relative;
  padding-left: 2.4rem;
  margin-bottom: 0.6em;
}
.entry-content ul:not(.no-marker) > li::before {
  content: "";
  position: absolute;
  left: 0.55rem;
  top: 0.62em;
  width: 7px;
  height: 7px;
  border-radius: 2px;
  background: var(--c-accent);
}

/* ==========================================================================
   18. Header search
   ========================================================================== */

.site-header__inner { gap: 0.75rem; }
.site-brand { margin-right: 0; }

.header-search { margin-left: auto; }
.header-search .search-form { gap: 0; }
/* The wrapper added in v0.5.0 is flex:1 by default, which would stretch the
   header row and strand the button. Here the input sets its own width. */
.header-search .search-form__field { flex: 0 0 auto; }
.header-search input[type="search"] {
  width: clamp(8rem, 18vw, 15rem);
  padding: 0.42rem 0.7rem;
  font-size: var(--step--1);
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  border-right: none;
}
.header-search button[type="submit"] {
  padding: 0.42rem 0.85rem;
  font-size: var(--step--1);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

/* 768-899px: too narrow for the search to share the first row, but wide enough
   that the mobile icon buttons are gone - so it takes its own row and stays
   open, with no toggle. Below 768 the block in section 4 owns it instead. */
@media (min-width: 768px) and (max-width: 899px) {
  .header-search { order: 5; flex-basis: 100%; margin-left: 0; }
  .header-search .search-form__field { flex: 1 1 auto; }
  .header-search input[type="search"] { width: 100%; }
  .site-brand { margin-right: auto; }
}
@media (max-width: 767px) {
  .header-search { margin-left: 0; }
  .header-search .search-form__field { flex: 1 1 auto; }
  .header-search input[type="search"] { width: 100%; }
  .site-brand { margin-right: auto; }
}

/* ==========================================================================
   19. Post header meta - author, published and updated on one line
   ========================================================================== */

.entry-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.85rem;
  font-size: var(--step--1);
  color: var(--c-text-faint);
}
.entry-meta__author { display: inline-flex; align-items: center; gap: 0.45rem; }
.entry-meta__author img { border-radius: 50%; flex: none; }
.entry-meta__author a { color: var(--c-text); font-weight: 600; text-decoration: none; }
.entry-meta__author a:hover { color: var(--c-accent); }
.entry-meta__sep { color: var(--c-border-strong); }

/* ==========================================================================
   20. Affiliate disclosure - one line, not a panel
   Amazon requires the association statement and the FTC requires it adjacent
   to the content. One sentence satisfies both, so the box is gone.
   ========================================================================== */

.disclosure {
  margin: 0 0 1.5rem;
  padding: 0;
  border: 0;
  background: none;
}
.disclosure__link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: var(--step--1);
  line-height: 1.4;
  /* Lighter than body text on purpose - it is a required notice, not a claim
     the reader has to weigh. */
  color: var(--c-text-faint);
  text-decoration: none;
}
.disclosure__link:hover,
.disclosure__link:focus-visible { color: var(--c-accent); text-decoration: none; }
.disclosure__icon { flex: none; display: block; }

/* ==========================================================================
   21. Table of contents - collapsible inline, then a floating left rail
   ========================================================================== */

.toc {
  margin-block: 1.5rem;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  overflow: hidden;
}
.toc__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
  padding: 0.85rem 1.1rem;
  font: inherit;
  font-family: var(--font-heading);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--c-text);
  background: none;
  border: 0;
  cursor: pointer;
  text-align: left;
}
.toc__chev {
  flex: none;
  width: 0.7em;
  height: 0.7em;
  border-right: 2px solid var(--c-text-faint);
  border-bottom: 2px solid var(--c-text-faint);
  transform: rotate(45deg);
  transition: transform 0.26s ease;
}
.toc__toggle[aria-expanded="false"] .toc__chev { transform: rotate(-45deg); }

/* The slide.
   0fr -> 1fr on a one-row grid, which is the only way to transition to an
   AUTO height without measuring it in JavaScript. The measured approach
   (read scrollHeight, set a pixel max-height) goes wrong on exactly this
   panel: the list reflows when the viewport changes and the cached pixel
   height is then either clipping the last item or leaving a gap under it.
   The fr unit is recomputed by the layout engine every time, so it cannot
   drift out of sync with the content.

   Three things have to hold for it to animate rather than snap:
   the child needs `min-height: 0` and `overflow: hidden` or it refuses to be
   compressed below its content; the padding lives on the child rather than on
   .toc__panel, because padding on the animating element stays behind as a
   visible strip at 0fr; and the panel must never carry [hidden], which is
   display:none and skips the transition entirely. theme.js drives this with
   aria-expanded alone for that reason. */
.toc__panel {
  display: grid;
  grid-template-rows: 1fr;
}
.toc__inner {
  min-height: 0;
  overflow: hidden;
}
.toc__panel ol {
  margin: 0;
  padding: 0 1.1rem 1rem 2.25rem;
}
.toc__toggle[aria-expanded="false"] + .toc__panel { grid-template-rows: 0fr; }

/* Collapsed content leaves the tab order and the accessibility tree.
   ⚠ No transition on this rule. It is also the state the panel is put into on
   page load, before the slide is armed, and a delayed visibility change there
   leaves the list readable to assistive technology for a quarter of a second
   after a collapse that never animated. The delay belongs only to the armed
   rules below. */
.toc__toggle[aria-expanded="false"] + .toc__panel .toc__inner {
  visibility: hidden;
}

/* Animate only after theme.js has settled the initial state. Without this gate
   a TOC that ships collapsed plays its closing slide on every page load. */
.toc[data-iaf-ready] .toc__panel { transition: grid-template-rows 0.26s ease; }
.toc[data-iaf-ready] .toc__panel .toc__inner { transition: visibility 0s linear 0s; }
.toc[data-iaf-ready] .toc__toggle[aria-expanded="false"] + .toc__panel .toc__inner {
  transition: visibility 0s linear 0.26s;
}

.toc li { margin-bottom: 0.3rem; }
.toc a { color: var(--c-text-soft); text-decoration: none; }
.toc a:hover { color: var(--c-accent); text-decoration: underline; }

@media (prefers-reduced-motion: reduce) {
  .toc[data-iaf-ready] .toc__panel,
  .toc__chev { transition: none; }
}

/* The rail.
   It fades in once the inline TOC scrolls away, but it is a real grid column
   rather than a position:fixed overlay. The first attempt was fixed and
   positioned into the left gutter, and it sat ON TOP of the article at every
   viewport narrow enough to matter: at 1400px the gutter is 80px and the rail
   is 230px, so it covered 200px of prose. A column cannot overlap the text it
   sits beside, which is the property actually wanted here.

   It is display:none below 1400px, because that is the width at which a third
   column stops leaving the prose enough room. */
/* The COLUMN is what sticks now, not the table of contents inside it - added
   v0.6.0 so a pillar panel can sit under the TOC and travel down the page with
   it. The TOC keeps its own ceiling so a 15-heading article scrolls the list
   rather than the column. */
.rail-col {
  display: none;
  /* align-self was `start` through v0.8.1: the box sized to its own content,
     the grid CELL still ran the full row height regardless (that is how grid
     tracks work), and the gap between the two was empty space with nothing
     marking it - which read as the rail simply stopping short of the article,
     on a wide screen where it is most visible. Stretching (the grid default,
     restated here since sticky positioning needs it to survive edits nearby)
     makes the box itself reach that full height; max-height below still clamps
     it once the article outgrows the viewport, so fillColumn()'s budget in
     theme.js is untouched either way. A border-right used to run that full
     height beside the article; it was removed 2026-09-10 at Jordan's request,
     with the sidebar's matching border-left. */
  align-self: stretch;
  position: sticky;
  top: 84px;
  /* Matches the budget fillColumn() works to in theme.js - 84px of sticky top
     plus its 24px of breathing room. If these two disagree the JS can accept a
     block the CSS then clips. */
  max-height: calc(100vh - 108px);
  overflow: hidden;
}
.rail-col > * + * { margin-top: 1rem; }

/* ⚠ VISIBLE FROM PAGE LOAD as of v0.7.0. It used to ship at opacity 0 and fade
   in only once the in-article table of contents had scrolled past, which left
   the whole left column empty until the reader scrolled - the one moment the
   navigation is most use is the moment it was absent.

   The in-article TOC is KEPT rather than removed. It is the copy Google reads
   for jump-to links in the result snippet, because those anchors have to be in
   the article body; a sidebar nav does not earn them. What changed instead is
   that theme.js collapses the in-article one on any viewport wide enough to
   show the rail, so the same list is never visible twice - it stays in the DOM,
   behind its own disclosure button, which is a pattern Google treats as
   ordinary content rather than hidden text. */
.toc-rail {
  max-height: calc(100vh - 150px);
  overflow-y: auto;
  padding: 1rem 1.1rem;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  font-size: var(--step--1);
}
.toc-rail__title {
  margin: 0 0 0.6rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-text-faint);
}
/* Reading-position states, rewritten v0.6.0 to move rather than snap.
   Two things changed. The marker is a pseudo-element instead of a border-left,
   because a border cannot be transitioned from 2px to 3px without the text
   beside it jumping a pixel - a scaled pseudo-element can. And the current item
   no longer changes font-weight: weight is not interpolable, so it landed as a
   hard step in the middle of an otherwise smooth transition, and on a system
   font stack it also reflowed the line. Colour, ground and indent carry the
   emphasis instead, and all three are animatable. */
.toc-rail ol { list-style: none; margin: 0; padding: 0; }
.toc-rail li + li { margin-top: 0.15rem; }
.toc-rail a {
  position: relative;
  display: block;
  padding: 0.28rem 0.4rem 0.28rem 0.75rem;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--c-text-soft);
  text-decoration: none;
  line-height: 1.35;
  transition: color 0.3s ease, background-color 0.3s ease, padding-left 0.3s ease;
}
.toc-rail a::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  border-radius: 2px;
  background: var(--c-border);
  transform: scaleX(1);
  transform-origin: left center;
  transition: background-color 0.3s ease, transform 0.3s ease;
}
.toc-rail a:hover { color: var(--c-accent); }
.toc-rail a:hover::before { background: var(--c-accent); }
.toc-rail a.is-current {
  color: var(--c-text);
  background: var(--c-accent-soft);
  padding-left: 1rem;
}
.toc-rail a.is-current::before { background: var(--c-accent); transform: scaleX(1.6); }

@media (prefers-reduced-motion: reduce) {
  .toc-rail a, .toc-rail a::before { transition: none; }
}

/* Three columns: rail, prose, sidebar.
   Gated at 1400px on the first attempt, which was wrong in a way that made the
   rail look broken - it simply never appeared on an ordinary laptop. The real
   constraint is not the viewport, it is how much width the grid has to divide,
   and .wrap caps that at --w-page regardless of how wide the screen is. So the
   rail pages are allowed a wider ceiling, and the breakpoint drops to 1200. */
@media (min-width: 1200px) {
  .wrap.has-rail { max-width: min(1440px, 100%); }

  /* ⚠ The middle track is sized to the prose, NOT to 1fr.
     With 1fr it took every pixel the other two columns did not want, while
     .prose still capped the text at --w-prose and pinned it left. On a 1440
     screen that left a 105px dead strip between the last word of the article
     and the sidebar: a column-shaped blank that read as a broken third column.
     Widening the prose to fill it is the wrong fix - the measure is already
     75 to 79 characters at the widest line, which is the top of the readable
     range, and 786px would push it past 90.
     So the middle track takes exactly the width the prose will use, and the
     surplus goes to the two columns that have something to put in it. The rail
     is capped at 280px because a table of contents does not improve past that;
     everything left over lands in the sidebar, which holds the related posts,
     the pillars, the topics and the newsletter. */
  .layout.has-sidebar.has-rail {
    grid-template-columns: 220px minmax(0, var(--w-prose)) minmax(300px, 1fr);
    gap: 2.25rem;
  }
  .rail-col { display: block; }
}

/* Same priority problem as the sidebar above, same answer. The rail is worth
   280px, but not at the cost of the article: at 1200 the grid has no slack, and
   a rail allowed to take its maximum took it out of the prose. It gets the
   extra 60px only where there is room to give it. */
@media (min-width: 1400px) {
  .layout.has-sidebar.has-rail {
    grid-template-columns: 280px minmax(0, var(--w-prose)) minmax(300px, 1fr);
  }
}
@media (prefers-reduced-motion: reduce) { .toc-rail { transition: none; } }

/* ==========================================================================
   21a. Back to top
   Two controls, one job, deliberately not the same control. The floating one is
   an article convenience: it appears once the reader is a screen down and gets
   out of the way before the footer, where the permanent one takes over. The
   footer one is a destination, always there, centred, and an <a href="#top"> so
   it works with no JavaScript at all.
   ========================================================================== */

.to-top {
  position: fixed;
  right: clamp(0.75rem, 2vw, 1.5rem);
  bottom: clamp(0.75rem, 2vw, 1.5rem);
  z-index: 30;
  display: grid;
  place-items: center;
  width: 2.9rem; height: 2.9rem;
  padding: 0;
  border: 1px solid var(--c-accent);
  border-radius: 50%;
  background: var(--c-accent);
  color: var(--c-on-accent);
  cursor: pointer;
  box-shadow: 0 6px 20px rgb(0 0 0 / 0.18);
  opacity: 0;
  transform: translateY(0.5rem) scale(0.94);
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
              background-color 0.2s ease;
}
.to-top.is-visible {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.to-top:hover { background: var(--c-accent-hover); border-color: var(--c-accent-hover); }
.to-top svg { display: block; }

@media (prefers-reduced-motion: reduce) {
  .to-top { transition: opacity 0.2s ease; transform: none; }
}

/* The permanent pill. Sits below the base row, centred, on the footer ink. */
.site-footer__totop {
  grid-column: 1 / -1;
  display: flex;
  justify-content: center;
  margin-top: 0.35rem;
}
.to-top-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 1.25rem;
  border: 1px solid var(--c-panel-border);
  border-radius: 999px;
  background: var(--c-panel);
  color: var(--c-panel-soft);
  font-size: var(--step--1);
  font-weight: 600;
  text-decoration: none;
  --wash-color: #1b2a41;
}
.site-footer a.to-top-pill:hover { color: #fff; text-decoration: none; }
.to-top-pill svg { display: block; }

/* ==========================================================================
   21b. Space-filling blocks
   Any block marked data-iaf-fill ships hidden and is revealed by theme.js only
   while the column it lives in still fits the viewport without scrolling. The
   brief was to use free space where a screen has it, never to invent a scroll
   where it does not - so these are additive by construction, and a narrow or
   short screen simply never sees them. The [hidden] attribute is what does the
   hiding; the reset in the theme head already makes that stick.
   ========================================================================== */

.topic-chips {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 0.4rem;
}
.topic-chips li { margin: 0; border: 0; padding: 0; }
.topic-chips a {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.24rem 0.6rem;
  border: 1px solid var(--c-border);
  border-radius: 999px;
  background: var(--c-bg);
  color: var(--c-text-soft);
  font-size: var(--step--1);
  text-decoration: none;
  transition: color 0.24s ease, border-color 0.24s ease, background-color 0.24s ease;
}
.topic-chips a:hover { color: var(--c-accent); border-color: var(--c-accent); }
.topic-chips a[aria-current="true"] {
  color: var(--c-on-accent);
  background: var(--c-accent);
  border-color: var(--c-accent);
}
.topic-chips__count { font-size: 0.72em; opacity: 0.75; font-variant-numeric: tabular-nums; }

/* In the rail the widgets are narrower and quieter than in the sidebar - it is
   a 220px gutter, not a column, and it sits beside the reading line. */
.rail-col .widget {
  padding: 0.9rem 1rem;
  font-size: var(--step--1);
}
.rail-col .widget__title { font-size: 0.7rem; }
.rail-col .widget li + li { margin-top: 0.4rem; padding-top: 0.4rem; }
.rail-col .topic-chips a { padding: 0.18rem 0.5rem; }

/* ==========================================================================
   22. Sidebar widgets
   ========================================================================== */

.widget {
  margin-bottom: 1.75rem;
  padding: 1.15rem;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
}
.widget__title {
  margin: 0 0 0.85rem;
  font-family: var(--font-heading);
  font-size: var(--step-1);
  font-weight: 700;
  letter-spacing: -0.02em;
}
.widget ul { list-style: none; margin: 0; padding: 0; }
.widget li + li {
  margin-top: 0.85rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--c-border);
}
.widget a {
  display: block;
  color: var(--c-text);
  text-decoration: none;
  font-weight: 500;
  line-height: 1.35;
}
.widget a:hover { color: var(--c-accent); }
.widget__meta {
  display: block;
  margin-top: 0.2rem;
  font-size: var(--step--1);
  color: var(--c-text-faint);
  font-weight: 400;
}

/* Newsletter sits on the brand ink so it reads as the one ask on the page. */
.widget--signup {
  background: var(--c-panel);
  border-color: var(--c-panel);
  color: var(--c-panel-text);
}
.widget--signup .widget__title { color: var(--c-panel-text); }
.widget--signup p {
  color: var(--c-panel-soft);
  font-size: var(--step--1);
  margin: 0 0 0.9rem;
}
.widget--signup form { display: flex; flex-direction: column; gap: 0.5rem; }
.widget--signup input[type="email"] {
  padding: 0.55rem 0.75rem;
  font: inherit;
  font-size: var(--step--1);
  color: var(--c-panel);
  background: #ffffff;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
}
.widget--signup button {
  padding: 0.55rem 0.9rem;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--c-panel);
  background: #ffffff;
  border: 1px solid #ffffff;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.widget--signup button:hover { background: #e9eef4; border-color: #e9eef4; }
.widget--signup .widget__small {
  font-size: 0.72rem;
  color: var(--c-panel-faint);
  margin: 0.55rem 0 0;
}

.sidebar__sticky { position: sticky; top: 84px; }

/* ==========================================================================
   23. Footer
   Nothing here any more. The footer moved onto the constant brand ink in
   v0.6.0 and is now identical in both themes by design, the same call the
   newsletter widget, the consent panel and the topic cards already make - so
   section 10 defines it once and this section has nothing left to override.
   Kept as a marker so the numbering does not shuffle.
   ========================================================================== */

/* ==========================================================================
   24. Cookie consent banner  (CookieAdmin plugin)
   The plugin renders its own markup and ships its own stylesheet, so this is
   an override rather than a design. Bottom centre, wide and short rather than
   a square block, on the brand palette. The selectors are the plugin's, and
   !important is needed because its stylesheet loads after the theme's.
   ========================================================================== */

.cookieadmin_law_container {
  position: fixed !important;
  left: 50% !important;
  right: auto !important;
  bottom: clamp(0.75rem, 2vw, 1.5rem) !important;
  top: auto !important;
  transform: translateX(-50%) !important;
  width: min(1100px, calc(100vw - 2rem)) !important;
  max-width: min(1100px, calc(100vw - 2rem)) !important;
  margin: 0 !important;
  padding: 0.9rem 1.15rem !important;
  border: 1px solid var(--c-panel-border) !important;
  border-radius: var(--radius) !important;
  background: var(--c-panel) !important;
  color: var(--c-panel-text) !important;
  box-shadow: 0 12px 40px rgb(0 0 0 / 0.35) !important;
  font-family: var(--font-body) !important;
  z-index: 9999 !important;
}

/* The plugin sets `.cookieadmin_consent_inside { background-color: #FFFFFF }`.
   That inner wrapper sits on top of the dark container, so styling only the
   container left white-on-white text. Everything that could paint a ground
   inside the banner is forced transparent here.

   ⚠ It also sets a BORDER on that wrapper - measured on the live site as
   `0.667px solid rgb(171,184,195)`, which reads as a pale outline floating
   inside the dark panel about a centimetre in from its real edge. Killing the
   background never touched it. Borders off as well as grounds. */
.cookieadmin_law_container .cookieadmin_consent_inside,
.cookieadmin_law_container .cookieadmin_notice_con,
.cookieadmin_law_container .cookieadmin_consent_btns,
.cookieadmin_law_container .cookieadmin-poweredby {
  background: transparent !important;
  background-color: transparent !important;
  box-shadow: none !important;
}

/* No inner outlines. Every wrapper the plugin paints a border on sits inside
   the panel the theme already draws one around. */
.cookieadmin_law_container .cookieadmin_consent_inside,
.cookieadmin_law_container .cookieadmin_notice_con,
.cookieadmin_law_container .cookieadmin_consent_btns,
.cookieadmin_law_container .cookieadmin-poweredby,
.cookieadmin_law_container #cookieadmin_notice {
  border: 0 !important;
  outline: 0 !important;
}

.cookieadmin_law_container .cookieadmin_consent_inside {
  display: flex !important;
  flex-wrap: wrap !important;
  align-items: center !important;
  justify-content: space-between !important;
  gap: 0.6rem 1.5rem !important;
  max-width: none !important;
  width: 100% !important;
  padding: 0 !important;
  margin: 0 !important;
  text-align: left !important;
}
.cookieadmin_law_container #cookieadmin_notice_title {
  flex: 0 0 100% !important;
  margin: 0 !important;
  font-family: var(--font-heading) !important;
  font-size: var(--step--1) !important;
  font-weight: 700 !important;
  letter-spacing: -0.01em !important;
  color: var(--c-panel-text) !important;
}
.cookieadmin_law_container #cookieadmin_notice_title:empty { display: none !important; }
.cookieadmin_law_container .cookieadmin_notice_con {
  flex: 1 1 22rem !important;
  min-width: 0 !important;
  margin: 0 !important;
  padding: 0 !important;
}
.cookieadmin_law_container #cookieadmin_notice,
.cookieadmin_law_container .cookieadmin_desc {
  margin: 0 !important;
  font-size: var(--step--1) !important;
  line-height: 1.45 !important;
  color: var(--c-panel-soft) !important;
}
.cookieadmin_law_container #cookieadmin_notice a {
  color: var(--c-panel-text) !important;
  text-decoration: underline !important;
}

.cookieadmin_law_container .cookieadmin_consent_btns {
  display: flex !important;
  flex: 0 0 auto !important;
  flex-wrap: wrap !important;
  align-items: center !important;
  gap: 0.5rem !important;
  margin: 0 !important;
  padding: 0 !important;
}
.cookieadmin_law_container .cookieadmin_btn {
  margin: 0 !important;
  padding: 0.5rem 1.05rem !important;
  font-family: var(--font-body) !important;
  font-size: var(--step--1) !important;
  font-weight: 600 !important;
  line-height: 1.2 !important;
  border-radius: var(--radius-sm) !important;
  border: 1px solid transparent !important;
  cursor: pointer !important;
  width: auto !important;
  min-width: 0 !important;
}
.cookieadmin_law_container .cookieadmin_accept_btn {
  background: #ffffff !important;
  border-color: #ffffff !important;
  color: var(--c-panel) !important;
}
.cookieadmin_law_container .cookieadmin_accept_btn:hover {
  background: #e9eef4 !important;
  border-color: #e9eef4 !important;
}
.cookieadmin_law_container .cookieadmin_reject_btn,
.cookieadmin_law_container .cookieadmin_customize_btn {
  background: transparent !important;
  border-color: var(--c-panel-border) !important;
  color: var(--c-panel-text) !important;
}
.cookieadmin_law_container .cookieadmin_reject_btn:hover,
.cookieadmin_law_container .cookieadmin_customize_btn:hover {
  border-color: #ffffff !important;
  color: #ffffff !important;
}

.cookieadmin_law_container .cookieadmin-poweredby {
  flex: 0 0 100% !important;
  margin: 0 !important;
  text-align: right !important;
}

/* The vendor credit is a wordmark drawn in dark navy on transparent, so on the
   panel's own dark ground it was a smudge - and dimming the whole block to 40%
   to keep it quiet only made that worse. It gets a white pill instead: the
   artwork is on the ground it was drawn for, it reads as an attribution badge
   rather than site furniture, and it stays small. */
.cookieadmin_law_container .cookieadmin-poweredby a {
  display: inline-flex !important;
  align-items: center !important;
  gap: 0.35rem !important;
  padding: 0.2rem 0.6rem !important;
  border-radius: 999px !important;
  background: #ffffff !important;
  color: #0c1821 !important;
  font-size: 0.66rem !important;
  line-height: 1.4 !important;
  text-decoration: none !important;
  opacity: 0.62 !important;
  transition: opacity 0.2s ease !important;
}
.cookieadmin_law_container .cookieadmin-poweredby a:hover { opacity: 1 !important; }
.cookieadmin_law_container .cookieadmin-poweredby a span { color: inherit !important; }
.cookieadmin_law_container .cookieadmin-poweredby svg { display: block !important; }

/* Dismissal.
   The plugin hides the banner the instant a button is clicked, with an inline
   display:none, so there is nothing to animate by default. theme.js adds
   .iaf-consent-fading on click; the display rule here outranks the plugin's
   inline style long enough for the fade to play, then the class is removed and
   the plugin's own hiding takes over. */
.cookieadmin_law_container {
  transition: opacity 0.42s ease, transform 0.42s ease !important;
}
.cookieadmin_law_container.iaf-consent-fading {
  display: block !important;
  opacity: 0 !important;
  transform: translateX(-50%) translateY(12px) !important;
  pointer-events: none !important;
}

@media (prefers-reduced-motion: reduce) {
  .cookieadmin_law_container { transition: none !important; }
}

@media (max-width: 719px) {
  .cookieadmin_law_container .cookieadmin_consent_inside { justify-content: stretch !important; }
  .cookieadmin_law_container .cookieadmin_consent_btns { flex: 1 1 100% !important; }
  .cookieadmin_law_container .cookieadmin_btn { flex: 1 1 auto !important; text-align: center !important; }
}

/* ==========================================================================
   25. Healing timeline planner  (template-healing-timeline.php)
   The first interactive tool on the site. Everything here is theme tokens, so
   dark mode needs no second copy of these rules - the two exceptions at the
   bottom are the stage rail's tints, which have to be readable against both
   grounds and cannot be derived from an existing token.
   ========================================================================== */

/* The prose under the tool runs the same --w-page measure as the tool itself
   (Jordan, 2026-09-10). It used to drop to --w-prose, which made the page read
   as a wide panel sitting on top of a narrow article. .wrap already sets the
   measure, so there is nothing to add here - this note is what stops someone
   putting the cap back. */

.ht-tool {
  margin-block: 2rem 3rem;
  padding: clamp(1rem, 0.6rem + 1.6vw, 1.9rem);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
}

/* --- the top: the form on the left, the photo on the right --- */

/* Half and half from 56rem. Below that there is no room for a second column
   that does not truncate the options, so the photo is not shown at all (and
   the <picture> in the template stops narrow screens downloading it). */
.ht-top {
  display: grid;
  gap: clamp(1rem, 0.6rem + 1.6vw, 1.9rem);
  grid-template-columns: minmax(0, 1fr);
}

.ht-photo { display: none; }

@media (min-width: 56rem) {
  .ht-top { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* The photo has no height of its own. The grid row is exactly as tall as
     the form, and the image is absolutely placed inside its cell so its
     intrinsic 900px can never make the row taller - it covers whatever the
     form comes to, at every width. */
  .ht-photo {
    display: block;
    position: relative;
    min-height: 0;
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--c-surface-alt);
  }
  .ht-photo img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

/* --- the form --- */

/* One column: it is the left half of the panel on a desktop and the whole
   panel below 56rem, and a single column is the only layout in which the
   longest option ("Anywhere, or I would rather not say", ~300px of text)
   never truncates. */
.ht-form {
  display: grid;
  gap: 1rem;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
}

.ht-field { display: flex; flex-direction: column; gap: 0.45rem; min-width: 0; }

.ht-field label {
  font-family: var(--font-heading);
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--c-text);
}

/* One box for every control, native or custom. A date input and a select have
   different intrinsic heights in every browser (51px against 45px in
   Chromium), and a <button> does not inherit the page font at all. A fixed
   height and `font: inherit` settle both. */
.ht-field input,
.ht-field select,
.ht-trigger,
.ht-field--action .button {
  width: 100%;
  height: 3.5rem;
  margin: 0;
  font: inherit;
  font-size: var(--step-0);
  line-height: 1.2;
}

.ht-field input,
.ht-field select,
.ht-trigger {
  padding: 0 1rem;
  font-weight: 600;
  color: var(--c-text);
  background-color: var(--c-bg);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  -webkit-appearance: none;
  appearance: none;
}
.ht-field input:hover,
.ht-field select:hover,
.ht-trigger:hover { border-color: var(--c-text-faint); }
.ht-field input:focus,
.ht-field select:focus { border-color: var(--c-accent); }

/* The native select still needs an arrow for the no-JavaScript page. */
.ht-field select {
  padding-right: 2.9rem;
  text-overflow: ellipsis;
  cursor: pointer;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    right 1.425rem center,
    right 1.05rem center;
  background-size: 0.375rem 0.375rem;
  background-repeat: no-repeat;
}

.ht-field input::-webkit-date-and-time-value { text-align: left; }

.ht-field input:focus-visible,
.ht-field select:focus-visible,
.ht-trigger:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 1px;
}

.ht-field--action .button {
  padding: 0 1.25rem;
  font-family: var(--font-heading);
  font-weight: 700;
  cursor: pointer;
}
/* The hover colour is the wash's job, exactly as on the header search button.
   .button:hover swaps the background instantly, which would paint the whole
   control before the wash had moved. */
.ht-field--action .button.btn-wash:hover {
  background: var(--c-accent);
  border-color: var(--c-accent);
}

/* --- the custom controls (v0.9.8) ---
   html.ht-js is set in the head before first paint, so the swap from native
   to custom happens before anything is drawn. No class, no swap: the native
   controls are the page. See iaf_ht_field_select() for why the native control
   stays in the DOM either way. */
.ht-custom { display: none; }
.ht-js .ht-custom { display: block; }
.ht-js .ht-field .ht-native { display: none; }

.ht-control { position: relative; }

.ht-trigger {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  text-align: left;
  cursor: pointer;
}
/* Up to two lines, then an ellipsis. Two lines at this size are ~43px and the
   control is 56px, so a long option on a phone ("Anywhere, or I would rather
   not say" in a 309px field) wraps instead of losing its end, and the control
   stays the same height as its neighbours. */
.ht-trigger__value {
  flex: 1 1 auto;
  min-width: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  line-height: 1.2;
}
.ht-trigger__value.is-empty { font-weight: 500; color: var(--c-text-faint); }

.ht-icon { width: 1.25rem; height: 1.25rem; flex: none; }
.ht-trigger .ht-icon { color: var(--c-text-soft); }

.ht-control.is-open .ht-trigger {
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px var(--c-accent-soft);
}
.ht-control.is-open .ht-icon--chevron { transform: rotate(180deg); }

/* The panel. Anchored to both edges of the control, so it is exactly the
   field's width at every size the field can be, and it sits 0.5rem clear of
   it. It reveals downward by clip-path rather than by height, which would
   reflow the page on every frame.

   ⛔ Never `hidden` or display:none for the closed state - display:none skips
   the transition entirely (the TOC panel learned this first). Closed is
   visibility:hidden, which also takes it out of the tab order and the
   accessibility tree, with visibility's own transition delayed until the
   fade has finished.

   The clip runs 40px wide of the box and 60px past its foot when open, or it
   would shave the shadow off. z-index 15 keeps it under the sticky header
   (20) when the page scrolls. */
.ht-pop {
  position: absolute;
  z-index: 15;
  top: calc(100% + 0.5rem);
  left: 0;
  right: 0;
  margin: 0;
  padding: 0.4rem;
  list-style: none;
  color: var(--c-text);
  background: var(--c-surface);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  box-shadow:
    0 22px 44px -18px rgb(12 24 33 / 0.38),
    0 3px 8px rgb(12 24 33 / 0.10);
  visibility: hidden;
  opacity: 0;
  transform: translateY(-0.35rem);
  clip-path: inset(0 -40px 100% -40px);
}
.ht-control.is-open .ht-pop {
  visibility: visible;
  opacity: 1;
  transform: none;
  clip-path: inset(0 -40px -60px -40px);
}

@media (prefers-reduced-motion: no-preference) {
  .ht-pop {
    transition:
      opacity 0.16s ease,
      transform 0.28s cubic-bezier(0.2, 0, 0, 1),
      clip-path 0.28s cubic-bezier(0.2, 0, 0, 1),
      visibility 0s linear 0.28s;
  }
  .ht-control.is-open .ht-pop {
    transition:
      opacity 0.18s ease,
      transform 0.32s cubic-bezier(0.2, 0, 0, 1),
      clip-path 0.32s cubic-bezier(0.2, 0, 0, 1),
      visibility 0s linear 0s;
  }
  .ht-icon--chevron { transition: transform 0.25s cubic-bezier(0.2, 0, 0, 1); }
  .ht-trigger { transition: border-color 0.15s ease, box-shadow 0.2s ease; }
}

/* The listbox. */
.ht-list {
  max-height: min(24rem, 62vh);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Focus sits on the list while it is open, but the highlighted option is the
   focus indicator (aria-activedescendant), so the list draws no ring of its own
   - it doubled up with the panel's border. */
.ht-list:focus { outline: none; }

.ht-list__opt {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.75rem 0.85rem;
  border-radius: var(--radius-sm);
  font-weight: 500;
  cursor: pointer;
  user-select: none;
}
.ht-list__opt .ht-icon { width: 1.1rem; height: 1.1rem; color: var(--c-accent); opacity: 0; }
.ht-list__opt.is-active { background: var(--c-accent-soft); box-shadow: inset 0 0 0 1px var(--c-border-strong); }
.ht-list__opt[aria-selected="true"] { font-weight: 700; color: var(--c-accent-hover); }
.ht-list__opt[aria-selected="true"] .ht-icon { opacity: 1; }

/* The calendar. */
.ht-cal { padding: 0.8rem; }

.ht-cal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}
.ht-cal__month {
  margin: 0;
  font-family: var(--font-heading);
  font-size: var(--step-0);
  font-weight: 700;
}
.ht-cal__nav {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  color: var(--c-text);
  background: transparent;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.ht-cal__nav:hover:not(:disabled) { background: var(--c-accent-soft); border-color: var(--c-border-strong); }
.ht-cal__nav:disabled { opacity: 0.35; cursor: default; }

.ht-cal__grid {
  width: 100%;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0.2rem;
}
.ht-cal__grid th {
  padding: 0.3rem 0;
  font-family: var(--font-heading);
  font-size: var(--step--1);
  font-weight: 600;
  text-align: center;
  color: var(--c-text-faint);
}
.ht-cal__grid td { padding: 0; }

.ht-cal__day {
  display: block;
  width: 100%;
  height: 2.6rem;
  padding: 0;
  font: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--c-text);
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.ht-cal__day:hover:not(:disabled) { background: var(--c-accent-soft); }
.ht-cal__day.is-outside { font-weight: 500; color: var(--c-text-faint); }
.ht-cal__day:disabled { opacity: 0.35; cursor: default; }
/* Today gets the one touch of the brand's second colour in the control. */
.ht-cal__day.is-today { box-shadow: inset 0 0 0 1.5px var(--c-accent-2); }
.ht-cal__day[aria-pressed="true"],
.ht-cal__day[aria-pressed="true"]:hover {
  color: var(--c-on-accent);
  background: var(--c-accent);
}
.ht-cal__day:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 1px; }

.ht-cal__foot {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.4rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--c-border);
}
.ht-cal__today {
  padding: 0.4rem 0.7rem;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 700;
  color: var(--c-accent);
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.ht-cal__today:hover { background: var(--c-accent-soft); }

/* --- the answer: where you are right now --- */

/* Dark in both themes, like the newsletter panel, with the brand's flash-sheet
   gold as the frame: a hairline outside, a second fainter rule 5px inside it.
   --ht-gold is the dark-theme --c-accent-2, pinned, because the light-theme
   gold (#b8860b) goes muddy on a navy ground. */
.ht-status {
  --ht-gold: #e0b04a;
  margin: clamp(1.25rem, 1rem + 1vw, 1.75rem) 0 0;
  padding: clamp(1.4rem, 1rem + 2vw, 2.4rem) 1.25rem;
  text-align: center;
  color: var(--c-panel-text);
  background:
    radial-gradient(120% 140% at 50% 0%, rgb(224 176 74 / 0.16), transparent 60%),
    radial-gradient(90% 120% at 50% 120%, rgb(50 74 95 / 0.6), transparent 70%),
    var(--c-panel);
  border: 1px solid var(--ht-gold);
  border-radius: calc(var(--radius) + 4px);
  box-shadow:
    inset 0 0 0 5px var(--c-panel),
    inset 0 0 0 6px rgb(224 176 74 / 0.35),
    0 22px 44px -26px rgb(12 24 33 / 0.7);
}
.ht-status p { margin: 0; }

.ht-status__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-heading);
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ht-gold);
}
/* One line between its stars at every width: on a phone the spaced capitals
   broke onto two lines with a star stranded either side. */
@media (max-width: 30rem) {
  .ht-status__eyebrow { gap: 0.4rem; font-size: 0.75rem; letter-spacing: 0.08em; white-space: nowrap; }
}
.ht-status__eyebrow::before,
.ht-status__eyebrow::after {
  content: "\2726";
  content: "\2726" / "";
  font-size: 0.8em;
  letter-spacing: 0;
  opacity: 0.85;
}

.ht-status .ht-status__day {
  margin-top: 0.4rem;
  font-family: var(--font-heading);
  font-size: clamp(2.8rem, 2rem + 3.5vw, 4.6rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--c-panel-text);
}
.ht-status__day span { color: var(--ht-gold); }

.ht-status .ht-status__stage {
  margin-top: 0.8rem;
  font-size: var(--step-0);
  color: var(--c-panel-soft);
}
.ht-status__stage strong { font-weight: 700; color: var(--c-panel-text); }

.ht-status .ht-status__prompt {
  max-width: 36rem;
  margin-inline: auto;
  font-size: var(--step-0);
  color: var(--c-panel-soft);
}

/* --- the stages --- */

/* A rail down the left with a dot per stage. Drawn with a border and a pseudo
   element rather than a background image, so it survives a font-size change and
   costs no request. */
.ht-lead {
  max-width: var(--w-prose);
  margin: 0 0 0.5rem;
  color: var(--c-text-soft);
}

.ht-stages {
  list-style: none;
  margin: 1.8rem 0 0;
  padding: 0 0 0 1.75rem;
  border-left: 2px solid var(--c-border);
}

/* scroll-margin clears the sticky header when Build My Timeline jumps here.
   The jump also moves focus to the stage (tabindex -1), which should only
   draw a ring for a keyboard user. */
.ht-stage { position: relative; padding-bottom: 1.9rem; scroll-margin-top: 6.5rem; }
.ht-stage:focus { outline: none; }
.ht-stage:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 0.5rem; border-radius: var(--radius-sm); }
.ht-stage:last-child { padding-bottom: 0; }

.ht-stage::before {
  content: "";
  position: absolute;
  left: -2.3rem;
  top: 0.25rem;
  width: 0.85rem; height: 0.85rem;
  border-radius: 50%;
  background: var(--c-bg);
  border: 2px solid var(--c-border-strong);
}

.ht-stage__when {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
  margin: 0 0 0.15rem;
  font-family: var(--font-heading);
  font-size: var(--step--1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-faint);
}

/* Empty until a date is entered, and an empty span with a gap on either side
   leaves a visible hole in the row. */
.ht-stage__dates:empty { display: none; }
.ht-stage__dates { color: var(--c-accent); letter-spacing: 0; text-transform: none; }

.ht-stage__title {
  margin: 0 0 0.5rem;
  font-size: var(--step-1);
}

.ht-stage__lines { margin: 0; padding-left: 1.1rem; }
.ht-stage__lines li { margin-bottom: 0.45rem; }
.ht-stage__lines li:last-child { margin-bottom: 0; }

/* Past stages are dimmed, not hidden: someone on day 9 still wants to know
   whether what happened on day 4 was normal, and that is half of why they are
   on this page. */
.ht-stage.is-past { opacity: 0.55; }
.ht-stage.is-past::before { background: var(--c-border-strong); border-color: var(--c-border-strong); }

.ht-stage.is-now::before {
  background: var(--c-accent-2);
  border-color: var(--c-accent-2);
  box-shadow: 0 0 0 4px var(--c-accent-2-soft);
}
.ht-stage.is-now .ht-stage__title { color: var(--c-accent-hover); }
.ht-stage.is-now .ht-stage__when { color: var(--c-accent-2-hover); }

/* A stage moving between past, now and future is a class swap, so it is
   CSS's to animate. Text and height changes are the script's job - see
   morph() in assets/healing-timeline.js. */
@media (prefers-reduced-motion: no-preference) {
  .ht-stage { transition: opacity 0.4s ease; }
  .ht-stage::before {
    transition: background-color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease;
  }
  .ht-stage__title,
  .ht-stage__when { transition: color 0.35s ease; }
  .ht-field input,
  .ht-field select { transition: border-color 0.15s ease; }
}

/* flow-root so the box the script measures contains its children's margins.
   Without it a margin collapses out through the edge, and the height the
   script animates to is short by exactly that margin. */
.ht-stage__lines,
.ht-place-notes { display: flow-root; }

/* --- placement note --- */

.ht-place-note {
  margin: 1.5rem 0 0;
  padding: 0.8rem 1rem;
  font-size: var(--step--1);
  color: var(--c-text-soft);
  background: var(--c-surface-alt);
  border-radius: var(--radius-sm);
}

/* --- milestones --- */

.ht-sub {
  margin: 2.4rem 0 0.9rem;
  font-size: var(--step-1);
}

.ht-milestones {
  display: grid;
  gap: 0.9rem;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  list-style: none;
  margin: 0;
  padding: 0;
}

.ht-milestone {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.9rem 1rem;
  background: var(--c-surface-alt);
  border-radius: var(--radius-sm);
}

.ht-milestone__label {
  font-family: var(--font-heading);
  font-size: var(--step--1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-faint);
}

.ht-milestone__value {
  font-family: var(--font-heading);
  font-size: var(--step-1);
  font-weight: 700;
  color: var(--c-accent-hover);
}

.ht-milestone__detail { font-size: var(--step--1); color: var(--c-text-soft); }

/* --- the safety block, and the citations --- */

/* It is the last thing in the tool and it should read as the heaviest thing in
   it, so it loses the component's usual top margin and gains weight instead. */
.ht-safety { margin-top: 2.4rem; }

/* Inline source links. Small, quiet, and unmistakably a link - the whole point
   of them is that a reader can check a claim without leaving the sentence, so
   they must not read as decoration. */
.ht-cite {
  font-size: 0.72em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
  color: var(--c-text-faint);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.ht-cite:hover, .ht-cite:focus-visible { color: var(--c-accent); }

.ht-sources { margin-block: 3rem 0; font-size: var(--step--1); }
.ht-sources h2 { font-size: var(--step-1); }
.ht-sources ul { margin: 0; padding-left: 1.1rem; }
.ht-sources li { margin-bottom: 0.7rem; }
.ht-sources__checked { display: block; color: var(--c-text-faint); }

/* Dark mode. The rail and the dots are the only things here that do not fall
   out of the tokens: --c-bg is the dot's fill, and against the darker panel
   ground it reads as a hole rather than a marker. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ht-stage::before { background: var(--c-surface); }
  :root:not([data-theme="light"]) .ht-field input,
  :root:not([data-theme="light"]) .ht-field select,
  :root:not([data-theme="light"]) .ht-trigger { background-color: var(--c-surface-alt); }
  :root:not([data-theme="light"]) .ht-pop { box-shadow: 0 22px 44px -18px rgb(0 0 0 / 0.7), 0 3px 8px rgb(0 0 0 / 0.35); }
}
:root[data-theme="dark"] .ht-stage::before { background: var(--c-surface); }
:root[data-theme="dark"] .ht-field input,
:root[data-theme="dark"] .ht-field select,
:root[data-theme="dark"] .ht-trigger { background-color: var(--c-surface-alt); }
:root[data-theme="dark"] .ht-pop { box-shadow: 0 22px 44px -18px rgb(0 0 0 / 0.7), 0 3px 8px rgb(0 0 0 / 0.35); }
