/* ==========================================================================
   Erin Hoover — poet
   Implemented from the Claude Design source (design-source/Erin Hoover.dc.html)
   ========================================================================== */

@font-face {
  font-family: 'Bantayog';
  src: url('fonts/bantayog-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
/* The Light cut, for the review quote only. A browser will not synthesise a
   lighter weight — without this face, font-weight:300 silently renders as
   Regular. Not preloaded: it sits below the fold and would compete with the
   Regular face the hero needs immediately. */
@font-face {
  font-family: 'Bantayog';
  src: url('fonts/bantayog-light.woff2') format('woff2');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Surfaces */
  --cream:        #FCFBF4;
  --cream-2:      #F3F1E7;
  --charcoal:     #272728;
  --bone:         #E8E3D2;   /* deeper than --cream-2, which sits only 1.09
                                off --cream and would read as plain white */
  --black:        #0A0A0A;

  /* Ink on cream */
  --ink:          #202020;
  --ink-soft:     #4A4844;
  --ink-label:    #5E5B54;
  --ink-muted:    #6E6C66;

  /* Ink on charcoal */
  --paper:        #F5F2E9;
  --paper-80:     rgba(245, 242, 233, 0.9);
  --paper-70:     rgba(245, 242, 233, 0.72);
  --paper-60:     rgba(245, 242, 233, 0.66);
  --paper-50:     rgba(245, 242, 233, 0.62);

  /* Rules */
  --rule:         rgba(32, 32, 32, 0.16);
  --rule-strong:  rgba(32, 32, 32, 0.35);
  --rule-dark:    rgba(245, 242, 233, 0.18);
  --rule-dark-40: rgba(245, 242, 233, 0.4);

  --quote:        #C9C5B4;

  /* Type */
  --serif: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  --sans:  'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
  /* Display face for the hero subheadline. Subset to Latin + digits +
     punctuation; the Light/Semilight weights are in ../Fonts if wanted. */
  --display: 'Bantayog', 'Archivo', system-ui, sans-serif;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --wrap: 1120px;
  --nav-h: 52px;
}

/* --------------------------------------------------------------- reset -- */

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

html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scroll-behavior: smooth;
}
/* NB: do not put overflow-x on html or body. Any non-visible value computes
   the other axis to `auto`, which moves the scroll container off the viewport
   and silently breaks window.scrollTo() — and with it every anchor link.
   Nothing overflows horizontally here; keep it that way instead of clipping. */

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--sans);
}

img { max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--ink);
  color: var(--cream);
  padding: 12px 18px;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.skip-link:focus { left: 8px; top: 8px; }

/* ----------------------------------------------------------- utilities -- */

.wrap { max-width: var(--wrap); margin: 0 auto; }

.eyebrow {
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-label);
}
.eyebrow--dark { color: var(--paper-60); }

.label {
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-label);
}
.label--dark { color: var(--paper-50); }

/* Underlined text link — cream surfaces */
.ulink {
  display: inline-block;
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: 3px;
  transition: border-color 200ms ease;
}
.ulink:hover { border-bottom-color: var(--ink); }

/* Underlined text link — charcoal surfaces */
.ulink--dark {
  color: var(--paper);
  border-bottom-color: var(--rule-dark-40);
}
.ulink--dark:hover { border-bottom-color: var(--paper); }

.ulink--quiet {
  color: rgba(245, 242, 233, 0.6);
  border-bottom-color: transparent;
}
.ulink--quiet:hover {
  color: var(--paper);
  border-bottom-color: var(--rule-dark-40);
}

.rule      { height: 1px; background: var(--rule); }
.rule--dark { height: 1px; background: var(--rule-dark); }

.figure__caption {
  display: flex;
  justify-content: space-between;
  /* Wrap as whole parts, never mid-phrase. Without these the two spans shrink
     and break inside themselves, so a narrow tile turns "Sawmill Poetry 2024"
     into a ragged "Sawmill Poetry" / "2024". Wrapping instead drops the second
     part to its own line, which reads as deliberate. Bites the 2x2 Sawmill
     tiles, which sit at ~217-248px — either side of the ~245px these need. */
  flex-wrap: wrap;
  gap: 2px 12px;
  margin-top: 12px;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-label);
}
.figure__caption > span { white-space: nowrap; }
.figure__caption--dark { color: var(--paper-50); margin-top: 11px; }

/* ----------------------------------------------------------------- nav -- */

.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  min-height: var(--nav-h);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  column-gap: clamp(8px, 2vw, 20px);
  padding: 8px clamp(12px, 3vw, 24px);
  background: rgba(252, 251, 244, 0.94);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid rgba(32, 32, 32, 0.14);
}

/* Scroll progress. The page runs to ~13 screens and the nav is identical at
   every one of them. This fills the hairline the nav already draws rather than
   adding a second bar — the border is the track, so no new chrome and no
   layout change. --scroll-progress is written by script.js and defaults to 0,
   so with no JS the line just stays empty and nothing looks broken. */
.nav::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: 1px;
  background: var(--ink);
  opacity: 0.55;
  transform: scaleX(var(--scroll-progress, 0));
  transform-origin: 0 50%;
  pointer-events: none;
}

.nav__group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  row-gap: 2px;
  column-gap: clamp(9px, 1.6vw, 22px);
  min-width: 0;
}
.nav__group--end { justify-content: flex-end; }

.nav__link {
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 4px 0;
  border-bottom: 1px solid transparent;
  transition: color 180ms ease, border-color 180ms ease;
}
.nav__link:hover { color: var(--ink); border-bottom-color: var(--ink); }

.nav__brand {
  font-family: var(--serif);
  font-size: clamp(11px, 2.4vw, 15px);
  letter-spacing: clamp(0.14em, 0.6vw, 0.3em);
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
  min-width: 0;
}

.nav__cta {
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  border: 1px solid var(--rule-strong);
  padding: 6px 12px;
  transition: background-color 200ms ease, color 200ms ease,
              transform 160ms var(--ease);
}
.nav__cta:hover { background: var(--ink); color: var(--cream); }
.nav__cta:active { transform: scale(0.97); transition-duration: 120ms; }

/* Menu button — mobile only */
.nav__toggle {
  display: none;
  appearance: none;
  background: none;
  border: 1px solid var(--rule-strong);
  color: var(--ink);
  font-family: inherit;
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 7px 12px;
  cursor: pointer;
  transition: transform 160ms var(--ease);
}
.nav__toggle:active { transform: scale(0.97); transition-duration: 120ms; }
.nav__toggle-close { display: none; }
.nav.is-open .nav__toggle-open  { display: none; }
.nav.is-open .nav__toggle-close { display: inline; }

/* ---------------------------------------------------------------- hero -- */

/* The hero photo is a high-key portrait on a blown-out white plate, and the
   subject sits dead centre with white only at the edges — so the type cannot
   clear her as an overlay at any width. It gets its own band above the photo
   instead, which also lets the hero run light: cream ground, ink type. */

.hero {
  position: relative;
  background: var(--cream);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.hero__type {
  padding: calc(var(--nav-h) + clamp(40px, 5.4vw, 82px)) 6.5% clamp(6px, 1vw, 14px);
  text-align: center;
}

.hero__meta {
  margin-top: clamp(16px, 2.4vw, 30px);
  animation: ehFade 500ms 420ms both;
}

/* Back to the original 11px / 0.22em the laureate lines carried. NB the
   pipes rely on U+007C having been added to the Bantayog subset — without
   it they fall back to Archivo mid-line. */
.hero__role {
  margin: 0;
  font-family: var(--display);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.hero__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(38px, 11.4vw, 190px);
  line-height: 0.78;
  letter-spacing: 0.012em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink);
  animation: ehRise 900ms 120ms var(--ease) both;
}

/* Photo band. The source is 1920x900, so at this ratio nothing is cropped;
   narrow screens crop inward to keep her a usable size. */
.hero__figure {
  position: relative;
  width: 100%;
  margin: 0;
  /* The source carries ~27% empty white above her head. This ratio plus the
     bottom-anchored crop below cuts almost all of it, so she sits tucked up
     under the headline instead of floating below a band of white. */
  aspect-ratio: 1920 / 690;
  overflow: hidden;   /* clips the scaled-up image below */
  /* The title rises and the role line fades; without this the portrait — the
     one thing the page is actually about — was simply already there. Fades
     under the tail of the title's rise so the opening reads as one moment.
     Opacity only: a rise would fight the crop this frame is tuned to. */
  animation: ehFade 700ms 260ms both;
}

.hero__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;   /* overrides the img{height:auto} reset */
  object-fit: cover;
  object-position: 50% 100%;
  /* Zoomed 8%. Origin is the TOP edge, not the centre: her head sits ~4% down
     the frame, so scaling about the centre would push it out of shot. At the
     previous 1.3 the crop landed mid-chest, part way through the neckline; 1.08
     brings the cut to just below the bust so the V reads whole. Each 0.01 here
     moves the bottom edge ~6 source px, so small changes are visible. */
  transform: scale(1.08);
  transform-origin: 50% 0;
}

/* No darkening pass — a soft cream fade so the plate dissolves into the page
   at the edges instead of ending on a hard rectangle. */
.hero__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Bottom dissolve only, so her torso doesn't end on a hard cut. No other
     fade is needed: the encode maps the photo's white point onto the page
     cream, so the plate and the band around it are already the same value. */
  background: linear-gradient(180deg,
    rgba(252, 251, 244, 0) 62%,
    rgba(252, 251, 244, 1) 97%,
    rgba(252, 251, 244, 1) 100%);
}

/* Replaces the tapered hairline that used to divide hero from About: a short
   review, set in Bantayog — the same display face as the hero's laureate
   lines, so the two bookend the photo. Sized up slightly because Bantayog
   runs smaller than Archivo at the same nominal size.
   NB the shipped subset has no opening single quote (U+2018) — use the curly
   doubles and ’ only, or it falls back mid-sentence.
   The tapered separator it replaced has been removed entirely. */
.hero-review {
  /* Bracketed above and below by hairlines at the quote's own measure. Uses
     --rule, the same weight as the site's other section rules (.pullquote's
     border-top), rather than the heavier --rule-strong the taper carried —
     two straight lines read stronger than one that tapers to nothing.
     The width keeps a gutter at small sizes so the rules never touch the
     screen edge. */
  width: min(640px, calc(100% - clamp(32px, 8vw, 48px)));
  margin: clamp(44px, 6vw, 82px) auto 0;
  padding: clamp(28px, 3.6vw, 44px) 0;
  text-align: center;
  position: relative;
}
/* The hairlines draw outward from the centre as the quote fades — the gesture
   the tapered separator used to make before it was replaced. Pseudo-elements
   rather than borders, because a border cannot be scaled on its own. */
.hero-review::before,
.hero-review::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--rule);
  transform: scaleX(0);
  transform-origin: 50% 50%;
  transition: transform 900ms var(--ease);
}
.hero-review::before { top: 0; }
.hero-review::after  { bottom: 0; }
.hero-review.is-in::before,
.hero-review.is-in::after { transform: scaleX(1); }
/* Let the lines carry the movement; the quote itself only fades, so the two
   gestures stay legible instead of the whole block sliding as the rules grow. */
.hero-review.reveal { transform: none; }
.no-js .hero-review::before,
.no-js .hero-review::after { transform: scaleX(1); }
.hero-review__text {
  margin: 0 auto;
  max-width: 100%;
  font-family: var(--display);
  font-weight: 300;   /* Light — see the second @font-face at the top */
  font-size: clamp(16px, 2vw, 22px);
  line-height: 1.55;
  letter-spacing: 0.03em;
  color: var(--ink);
  text-wrap: pretty;
}
.hero-review__cite {
  display: block;
  margin-top: 20px;
  font-family: var(--display);
  font-style: normal;
  font-size: 9.5px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-label);
}

/* Below 900px the figure is taller than the source, so object-fit crops only
   horizontally and nothing trims the white above her head. The zoom has to do
   it instead: anchor the scale to the bottom so it eats the headroom rather
   than her torso, keeping her tucked the way she is on desktop. */
@media (max-width: 900px) {
  .hero__figure { aspect-ratio: 4 / 3; }
  .hero__img {
    transform-origin: 50% 100%;
    /* Narrow screens anchor the scale at the BOTTOM, so here the zoom controls
       headroom rather than where her torso is cut — the bottom edge is already
       the foot of the source. Desktop's 1.08 would leave ~25% white above her
       head, so these keep the tighter framing they had. */
    transform: scale(1.3);
  }
}
@media (max-width: 600px) {
  .hero__figure { aspect-ratio: 1 / 1; }
}

/* ------------------------------------------------------------ sections -- */

.section {
  padding: clamp(84px, 10vw, 150px) clamp(16px, 4vw, 24px) clamp(80px, 9vw, 140px);
  scroll-margin-top: var(--nav-h);
}
.section--dark { background: var(--charcoal); }
.section--top-rule { border-top: 1px solid var(--rule); }

.section-head { text-align: center; }
.section-title {
  margin: 20px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(34px, 5.4vw, 66px);
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink);
}

.band { background: var(--charcoal); height: 74px; }

/* --------------------------------------------------------------- about -- */

.about {
  padding: clamp(84px, 10vw, 150px) clamp(16px, 4vw, 24px) clamp(80px, 9vw, 140px);
  scroll-margin-top: var(--nav-h);
}
/* Ornament behind the About section, anchored to the lower LEFT. Every knob
   is a custom property so tune.html can drive them live; the values below are
   the committed defaults. Re-encoded from the supplied art with alpha derived
   from the ink and the transparent margin trimmed, so these offsets map to the
   artwork rather than to empty pixels.
   isolation:isolate keeps z-index:-1 inside this section — without it the art
   drops behind the page background and vanishes. */
.about {
  position: relative;
  isolation: isolate;
  /* Clips the ornament at the section boundary — which is exactly where the
     dark band starts. --orn-y can stay negative to control how much of the
     artwork is cropped away, without it bleeding on into Books below.
     Safe here: .about is not the scroll container, so this does not touch the
     window.scrollTo trap that overflow on html/body causes. */
  overflow: hidden;
  --orn-src: url('illustrations/flower-corner.webp');
  --orn-aspect: 672 / 1030;
  --orn-base: 510px;   /* width at 100% scale; x1.37 below = 699px rendered */
  --orn-scale: 1.37;
  --orn-x: 18px;       /* offset from the left edge (+ pushes right) */
  --orn-crop: 43%;     /* share of the artwork cropped off at the bottom */
  --orn-opacity: 0.17;
}
.about::after {
  content: "";
  position: absolute;
  left: var(--orn-x);
  bottom: 0;
  /* The crop is a share of the artwork's OWN height — translateY percentages
     resolve against the element, unlike bottom:, which resolves against the
     container. A fixed -462px left only 21px of a 483px-tall phone rendering
     visible; as a percentage the same value crops correctly at every width. */
  transform: translateY(var(--orn-crop));
  z-index: -1;
  width: calc(var(--orn-base) * var(--orn-scale));
  aspect-ratio: var(--orn-aspect);
  background: var(--orn-src) no-repeat center / contain;
  opacity: var(--orn-opacity);
  pointer-events: none;
}
@media (max-width: 720px) {
  /* On a phone the ornament comes off the section floor and anchors to the
     honors block instead, sitting directly above it. Two reasons: anchored to
     the floor, the desktop crop of 43% cut at exactly the point where the
     bouquet's stems gather, landing as a severed bunch on the dark band; and
     uncropped it ran the full height of the honors, where the .honor panels'
     opaque cream backgrounds sliced it into two disconnected pieces.
     bottom:100% resolves against the honors block itself, so this holds
     wherever the copy reflows — no fixed offset to go stale. */
  .about { --orn-base: 230px; --orn-opacity: 0.14; }
  .about::after { display: none; }
  .honors { position: relative; }
  .honors::before {
    content: "";
    position: absolute;
    left: 0;
    bottom: calc(100% + 22px);
    /* Same stacking trick as .about::after: negative z-index inside .about's
       isolated context paints over the section's ground but under its copy. */
    z-index: -1;
    width: calc(var(--orn-base) * var(--orn-scale));
    aspect-ratio: var(--orn-aspect);
    background: var(--orn-src) no-repeat center / contain;
    opacity: var(--orn-opacity);
    pointer-events: none;
  }
}

.about__grid {
  max-width: var(--wrap);
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: clamp(40px, 6vw, 96px);
  align-items: start;
}
.about__portrait { width: 100%; display: block; filter: saturate(0.92); }

/* Top dissolve only. Held to the top edge rather than mirroring the hero's
   full 35% span: her hair starts ~19% down this crop, and a fade that long
   washed into her face. Gone by 16%, so it softens the edge and nothing else. */
.about__portrait-wrap { position: relative; }
.about__portrait-wrap::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(252, 251, 244, 1) 0%,
    rgba(252, 251, 244, 1) 2%,
    rgba(252, 251, 244, 0) 16%);
}

.about__divider { height: 1px; background: var(--rule); margin: 22px 0 30px; }


.about__body {
  margin: 34px 0 0;
  max-width: 46ch;
  font-size: 13px;
  line-height: 1.72;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.about__body + .about__body { margin-top: 18px; }

.honors {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 1px;
  margin-top: 48px;
  background: var(--rule);
}
.honor { background: var(--cream); padding: 22px 20px 20px 0; }
.honor + .honor { padding: 22px 0 20px 24px; }
.honor__name {
  margin-top: 10px;
  font-family: var(--serif);
  font-size: 19px;
  line-height: 1.3;
  color: var(--ink);
}
.honor__note { margin-top: 6px; font-size: 11px; color: var(--ink-muted); }

/* --------------------------------------------------------------- books -- */

.books { padding-bottom: clamp(76px, 8.5vw, 130px); }

.books__covers {
  display: flex;
  gap: clamp(36px, 6vw, 84px);
  align-items: flex-end;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 96px;
}

.cover {
  display: block;
  transition: transform 260ms var(--ease);
}
.cover:active { transform: scale(0.98); transition-duration: 120ms; }
.cover img {
  width: 100%;
  display: block;
  box-shadow: 0 1px 0 rgba(32, 32, 32, 0.2);
  transition: transform 400ms var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .cover:hover { transform: translateY(-3px); }
  .cover:hover img { transform: scale(1.03); }
  .cover:active { transform: translateY(-3px) scale(0.98); }
}
/* One width for all three so the row reads as a set. The two real covers
   differ in ratio by half a percent (400x618 vs 500x776) — under 1.5px of
   height here — so they are left uncropped rather than forced to match
   exactly. The placeholder below takes the same ratio to end flush. */
.cover--barnburner,
.cover--nsp,
.cover--forthcoming { width: clamp(150px, 17vw, 205px); }

.cover--forthcoming {
  aspect-ratio: 500 / 776;
  border: 1px solid var(--rule-strong);
  background: var(--cream-2);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 18px 16px;
}
.cover--forthcoming .label { font-size: 8.5px; letter-spacing: 0.16em; }
.cover__title {
  font-family: var(--serif);
  font-size: 27px;
  line-height: 1;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
}
.cover__year {
  margin-top: 8px;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.book-notes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  margin-top: 74px;
}
.book-note {
  padding: 26px;
  border-top: 1px solid var(--rule);
  border-left: 1px solid var(--rule);
}
.book-note:first-child { padding: 26px 26px 26px 0; border-left: 0; }
.book-note:last-child  { padding: 26px 0 26px 26px; }
.book-note__title {
  font-family: var(--serif);
  font-size: 25px;
  line-height: 1.1;
  color: var(--ink);
}
.book-note__imprint {
  margin-top: 9px;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-label);
}
.book-note__body {
  margin: 16px 0 0;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.book-note__praise {
  margin: 14px 0 0;
  font-family: var(--serif);
  font-size: 14.5px;
  font-style: italic;
  line-height: 1.5;
  color: var(--ink-label);
  text-wrap: pretty;
}
.book-note__praise span {
  display: block;
  margin-top: 6px;
  font-family: var(--sans);
  font-style: normal;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.book-note .ulink { margin-top: 20px; }

/* ----------------------------------------------------------- pullquote -- */

.pullquote {
  padding: clamp(74px, 8vw, 120px) clamp(16px, 4vw, 24px) clamp(76px, 8.5vw, 130px);
  background: var(--cream);
  border-top: 1px solid var(--rule);
}
.pullquote__grid {
  max-width: var(--wrap);
  margin: 0 auto;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: clamp(16px, 4vw, 56px);
  align-items: center;
}
.pullquote__mark {
  font-family: var(--display);
  font-size: clamp(48px, 7vw, 96px);
  line-height: 0.6;
  color: var(--quote);
  transition: transform 250ms var(--ease);
  user-select: none;
}
@media (hover: hover) and (pointer: fine) {
  .pullquote__grid:hover .pullquote__mark--open  { transform: translateX(-4px); }
  .pullquote__grid:hover .pullquote__mark--close { transform: translateX(4px); }
}

/* Sized to the longest verse line, not to a comfortable measure — the five
   lines (see .pq-l below) must each hold on one line or the lineation reads
   as broken. That line measures 598px at the 22px top of the font-size clamp;
   620px clears it, and the grid has ~950px to give at --wrap. */
.pullquote__body { text-align: center; max-width: 620px; margin: 0 auto; }
.pullquote__text {
  margin: 0;
  /* Bantayog, matching the hero review so the two between-section quotes are
     set in one voice. NB no italic cut ships — font-style:italic here would be
     a synthesised oblique, so the face carries the emphasis instead. */
  font-family: var(--display);
  font-weight: 300;
  /* Same ramp as .hero-review__text, so the two between-section quotes match
     in size as well as in face. */
  font-size: clamp(16px, 2vw, 22px);
  line-height: 1.4;
  color: var(--ink);
  text-wrap: pretty;
}
/* One verse line each. Above 600px they sit on their own lines, as in the
   poem itself. Below that the longest line (435px at the 16px floor of the
   font-size clamp) no longer fits the grid column, and block lineation
   collapses into a long/short stagger that reads as damaged verse rather than
   as verse. So under 600px the lines run together in the standard inline form
   with slash separators. Both are correct ways to quote verse; this uses
   whichever the available width can actually carry.
   This threshold tracks the type size and has moved twice: 640px in Cormorant
   italic, 780px when the face became Bantayog (which sets lowercase as small
   capitals, a third wider), and 600px once the size dropped to match the hero
   review. Measured failure is ~560px. No allowance needed for the Archivo
   fallback — it sets true lowercase, so it is narrower than the face it
   stands in for, not wider. */
.pq-l { display: block; }
@media (max-width: 600px) {
  .pq-l { display: inline; }
  /* Leading space comes from the newline between the spans in the markup,
     which is also what keeps the words apart when the quote is copied. */
  .pq-l + .pq-l::before { content: "/ "; color: var(--ink-label); }
}

.pullquote__line {
  height: 1px;
  width: 64px;
  background: rgba(32, 32, 32, 0.25);
  margin: 30px auto 0;
}
.pullquote__source {
  margin-top: 18px;
  font-family: var(--display);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-label);
}

/* --------------------------------------------------------------- poems -- */

.poems { padding-bottom: clamp(84px, 10vw, 150px); }

.poems__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 24px;
  flex-wrap: wrap;
}
.poems__title {
  margin: 20px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(32px, 4.6vw, 58px);
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--paper);
}
/* A list of poems is a list. <ol> gives screen readers item counts and list
   navigation that a <div> of anchors cannot; the markers are removed so the
   rendering is unchanged. */
.poems__list {
  margin-top: 74px;
  border-top: 1px solid var(--rule-dark);
  list-style: none;
  padding: 0;
}
.poems__list > li { display: block; }
.events__group { list-style: none; margin: 0; padding: 0; }
.events__group > li { display: block; }

.poem { border-bottom: 1px solid var(--rule-dark); }

.poem__summary {
  cursor: pointer;
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr) auto;
  gap: 20px;
  align-items: baseline;
  padding: 26px 0;
  color: var(--paper);
  transition: color 180ms ease;
  list-style: none;
}
.poem__summary:hover { color: #FFFFFF; }

.poem__num {
  font-size: 9px;
  letter-spacing: 0.14em;
  color: var(--paper-50);
  font-variant-numeric: tabular-nums;
}
.poem__name {
  font-family: var(--serif);
  font-size: clamp(21px, 2.4vw, 29px);
  line-height: 1.2;
  font-weight: 300;
}
.poem__cue {
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}

.poem__body {
  padding: 8px 0 62px;
  max-width: 620px;
  margin-left: clamp(0px, 6vw, 72px);
}
.poem__body--wide  { max-width: 680px; }
.poem__body--wider { max-width: 720px; }

.poem__body p {
  margin: 0 0 22px;
  font-family: var(--serif);
  font-size: 17px;
  line-height: 1.62;
  color: var(--paper-80);
}
.poem__body p:last-child { margin-bottom: 0; }

.poem__body--airy p  { line-height: 2.05; }

/* Poem 07 is measured verse whose line endings are authorial and fixed, so it
   is set line-for-line with <br>. Its lines run long (up to ~57 characters),
   which means narrow screens will still turn a line over — the hanging indent
   makes a turned-over line read as a continuation of the line above rather
   than as a new one, which is how verse is set in print when it will not fit
   the measure. No justification and no `hyphens: auto` here: automatic
   hyphenation would introduce breaks the poet did not write. */
/* Each authored line is its own block. It has to be: `text-indent` applies
   only to the first line of a block, so with <br>-separated lines a hanging
   indent lands once at the top and then pushes every following line in —
   flush first line, thirty indented ones, and no way to tell an authored line
   from a turned-over one. Per-line blocks give each line a flush start and
   indent only what genuinely turns over. */
.poem__body--measured .pv-l {
  display: block;
  padding-left: 2.4em;
  text-indent: -2.4em;
}
.poem__body--list p  { font-size: 16.5px; line-height: 1.85; }

/* Rows 07-10 are not reproduced on the page — they link out to the magazines
   that first published them, which hold those rights. They sit in the same
   list as the accordions and read identically, cue included: an <a> rather
   than a <summary>, opening in a new tab. No extra rules needed for the
   anchor — the base reset is `color: inherit; text-decoration: none`, and
   .poem__summary supplies the grid and the hover. */

.poem__epigraph {
  margin: 0 0 30px;
  padding-left: 32px;
  font-family: var(--sans);
  font-size: 11.5px;
  line-height: 1.7;
  letter-spacing: 0.02em;
  color: var(--paper-60);
}
.poem__hang { padding-left: 2.4em; display: inline-block; }

/* <details> gives no transition of its own, and it cannot be given a height
   one here: Chrome collapses the content with content-visibility on
   ::details-content, so while closed the browser skips its layout entirely —
   nothing can measure it and block-size:auto will not interpolate. So the
   height still snaps; only the verse itself is animated, which is the part
   you actually read. Runs on open because the element starts rendering then. */

/* -------------------------------------------------------------- events -- */

.events__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: clamp(40px, 7vw, 110px);
  align-items: start;
}
.events__title {
  margin: 20px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(30px, 4.2vw, 52px);
  line-height: 1.02;
  letter-spacing: 0.01em;
  color: var(--ink);
  text-wrap: pretty;
}
.events__body {
  margin: 26px 0 0;
  max-width: 34ch;
  font-size: 13px;
  line-height: 1.72;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.events__topics {
  margin: 22px 0 0;
  max-width: 38ch;
  font-size: 11.5px;
  line-height: 1.85;
  color: var(--ink-muted);
  text-wrap: pretty;
}
.events__topics .label { display: block; margin-bottom: 6px; }

.events__notelink {
  color: var(--ink);
  border-bottom: 1px solid var(--rule-strong);
  transition: border-color 200ms ease;
}
.events__notelink:hover { border-bottom-color: var(--ink); }

.events__cta { margin-top: 28px; }
/* The width moved to the <figure> so the caption's space-between rule spans
   the picture rather than the whole column — same as the About portrait. */
.events__figure {
  width: 74%;
  margin: 56px 0 0;
}
.events__portrait {
  width: 100%;
  display: block;
  filter: saturate(0.9);
}

.events__list { border-top: 1px solid var(--rule); }
/* Group labels inside the appearances list. Every dated entry is past, so the
   list is split rather than left under a heading that promised upcoming ones. */
.press__more {
  margin-top: clamp(34px, 4.5vw, 56px);
  padding-top: clamp(22px, 2.8vw, 32px);
  border-top: 1px solid var(--rule);
}

.events__grouphead {
  margin: 0;
  padding: 26px 0 10px;
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-label);
}
.events__grouphead:first-child { padding-top: 18px; }

.event {
  display: grid;
  grid-template-columns: 112px minmax(0, 1fr) auto;
  gap: 24px;
  align-items: baseline;
  padding: 24px 0;
  border-bottom: 1px solid var(--rule);
}
a.event { transition: background-color 200ms ease; }
a.event:hover { background: rgba(32, 32, 32, 0.03); }

.event__when {
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-label);
}
.event__name {
  display: block;
  font-family: var(--serif);
  font-size: 23px;
  line-height: 1.2;
  color: var(--ink);
}
.event__where {
  display: block;
  margin-top: 6px;
  font-size: 11px;
  color: var(--ink-muted);
}
.event__tag {
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-label);
}
.events__note {
  margin-top: 26px;
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--ink-label);
}

/* ------------------------------------------------------------- sawmill -- */

.sawmill { padding-bottom: clamp(80px, 9vw, 140px); }

.sawmill__grid {
  display: grid;
  /* Two explicit columns, deliberately not auto-fit. At --wrap the auto-fit
     version resolved to THREE 306px tracks and the copy sat in the first one
     alone. auto-fit only collapses tracks that are empty, and the figure block
     spanning 1/-1 kept all three occupied, so none ever collapsed. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(40px, 7vw, 104px);
  align-items: start;
}
/* Collapses at 1020px, not at the site's usual 860 or 720. The breakpoint is
   set by the .facts row inside the copy column: three items at minmax(150px)
   need ~450px to stay on one line, and the column only clears that above
   ~1020px. Below it the facts break to 2+1 and leave a ragged empty cell in a
   bordered row, which looks like a mistake rather than a wrap. Stacking early
   gives the copy the full width and keeps the facts intact. */
@media (max-width: 1020px) {
  .sawmill__grid { grid-template-columns: 1fr; }
}
.sawmill__title {
  margin: 22px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(38px, 6.2vw, 86px);
  line-height: 0.94;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--paper);
}
.sawmill__divider { height: 1px; background: var(--rule-dark); margin: 38px 0 30px; }
.sawmill__body {
  margin: 0;
  max-width: 42ch;
  font-size: 13px;
  line-height: 1.74;
  color: var(--paper-70);
  text-wrap: pretty;
}
.sawmill__body + .sawmill__body { margin-top: 18px; color: rgba(245, 242, 233, 0.7); }

/* Inline prose link on the charcoal ground. .ulink is 9.5px uppercase — a
   micro-link, wrong inside 13px body copy — and .events__notelink is ink on
   cream. The body sits at --paper-70, so the link takes full --paper: it
   reads as emphasis without needing a second colour. */
.sawmill__body a {
  color: var(--paper);
  border-bottom: 1px solid var(--rule-dark-40);
  transition: border-color 200ms ease;
}
.sawmill__body a:hover { border-bottom-color: var(--paper); }

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-top: 46px;
  border-bottom: 1px solid var(--rule-dark);
}
.fact {
  padding: 20px 16px;
  border-top: 1px solid var(--rule-dark);
  border-left: 1px solid var(--rule-dark);
}
.fact:first-child { padding: 20px 16px 20px 0; border-left: 0; }
.fact:last-child  { padding: 20px 0 20px 16px; }
.fact__value {
  margin-top: 9px;
  font-family: var(--serif);
  font-size: 17px;
  color: var(--paper);
}

/* Second line inside a fact value — the venue's city. Mirrors .event__name +
   .event__where: primary in serif, location beneath, smaller and muted. Uses
   --paper-50 rather than --ink-muted because these facts sit on the dark
   sawmill ground. The source newline before the span is deliberate: a block
   span contributes no whitespace of its own, so without it the flattened text
   a crawler reads is "BookshopCookeville". */
.fact__where {
  display: block;
  margin-top: 6px;
  font-size: 11px;
  color: var(--paper-50);
}
.sawmill__links {
  display: flex;
  gap: 26px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 36px;
}

/* A tier below the two actions above it, not a fourth peer in their row: these
   are wayfinding, not things to do. Four .ulinks side by side would read as a
   flat list of equals and cost "Follow the series" its weight.
   Set in the middot-separated voice .bb__talklist already uses for a run of
   outbound links, and named in plain text — the page carries no icons at all
   (zero <svg>), and the footer lists Instagram and Bluesky the same way. */
.sawmill__social {
  margin: 18px 0 0;
  font-size: 12.5px;
  line-height: 2;
  color: var(--paper-50);
}
.sawmill__social a {
  color: var(--paper-70);
  border-bottom: 1px solid transparent;
  transition: color 180ms ease, border-color 180ms ease;
}
.sawmill__social a:hover {
  color: var(--paper);
  border-bottom-color: var(--rule-dark-40);
}
.sawmill__social span { padding: 0 8px; }

/* A 2x2 plate of photographs beside the copy — a contact sheet on the wall of
   the section, which is the arrangement this page's other sections already
   use: a column of words against a column of pictures.
   Four across the full width was the previous arrangement. It left the copy
   stranded in a single narrow track with 73% of the row empty beside it,
   because .sawmill__grid's auto-fit resolved to three columns and the
   full-width span stopped the empty ones collapsing. Two explicit columns on
   the grid and a 2x2 here fill the row and put the pictures back at the
   reader's eye rather than below it.
   The UA default margin on <figure> is reset here and not inline: each figure
   used to carry style="margin:0", which outranks any stylesheet rule and was
   silently killing the layout declared for it. */
.sawmill__figures {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(12px, 1.6vw, 22px);
  align-items: start;
}
.sawmill__fig--1,
.sawmill__fig--2,
.sawmill__fig--3,
.sawmill__fig--4 { margin: 0; width: auto; }
/* The four assets are 4:3, 3:4 and two 1:1. A common square frame holds the
   grid true and keeps the captions on one line; square is the least
   destructive shared shape, taking the sides off the landscape one and the top
   and bottom off the portrait one rather than forcing either into the other. */
.sawmill__figures img {
  width: 100%;
  display: block;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  filter: saturate(0.86) brightness(0.96);
  transition: transform 400ms var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .sawmill__figures img:hover { transform: scale(1.03); }
}

/* --------------------------------------------------------------- press -- */

.press { padding: clamp(76px, 8.5vw, 130px) clamp(16px, 4vw, 24px) clamp(80px, 9vw, 140px); }
.press__title {
  margin: 20px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(30px, 4.4vw, 54px);
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink);
}
.press__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  margin-top: 80px;
  border-bottom: 1px solid var(--rule);
}
.press-card {
  padding: 34px 30px;
  border-top: 1px solid var(--rule);
  border-left: 1px solid var(--rule);
}
.press-card:first-child { padding: 34px 30px 34px 0; border-left: 0; }
.press-card:last-child  { padding: 34px 0 34px 30px; }
.press-card__body {
  margin: 18px 0 0;
  font-size: 12.5px;
  line-height: 1.72;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.press-card__shots { display: flex; gap: 10px; margin-top: 18px; }
.press-card__shots img {
  width: 50%;
  display: block;
  filter: saturate(0.9);
  transition: transform 400ms var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .press-card__shots img:hover { transform: scale(1.03); }
}
.press-card__note {
  margin-top: 12px;
  font-size: 10.5px;
  line-height: 1.6;
  color: var(--ink-muted);
}
.press-card__list {
  margin-top: 18px;
  font-size: 12.5px;
  line-height: 1.9;
  color: var(--ink-soft);
}

/* ------------------------------------------------------------- contact -- */

.contact {
  background: var(--black);
  padding: clamp(84px, 10vw, 150px) clamp(16px, 4vw, 24px);
  position: relative;
  overflow: hidden;
  scroll-margin-top: var(--nav-h);
}
.contact__inner {
  max-width: 640px;
  margin: 0 auto;
  text-align: center;
  position: relative;
}
.contact__title {
  margin: 26px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(28px, 3.6vw, 46px);
  line-height: 1.14;
  letter-spacing: 0.01em;
  color: var(--paper);
  text-wrap: pretty;
}
.contact__body {
  margin: 26px auto 0;
  max-width: 44ch;
  font-size: 12.5px;
  line-height: 1.72;
  color: rgba(245, 242, 233, 0.6);
  text-wrap: pretty;
}

.contact__btnwrap { position: relative; display: inline-block; margin-top: 44px; }
.contact__btnshadow {
  position: absolute;
  left: 7px; top: 7px;
  width: 100%; height: 100%;
  border: 1px solid rgba(245, 242, 233, 0.22);
  pointer-events: none;
}
.contact__btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 46px;
  padding: 0 34px;
  background: var(--paper);
  color: var(--black);
  font-size: 9.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  transition: transform 250ms var(--ease), box-shadow 250ms var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .contact__btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
  }
}
.contact__btn:active {
  transform: translateY(-1px) scale(0.97);
  transition-duration: 120ms;
}

/* ------------------------------------------------------- invitation form --
   The first form on the site, so it sets the conventions: no border-radius
   anywhere (the stylesheet has none), micro-labels in the same 9px tracked
   uppercase as .label, and a single hairline under each field rather than a
   box — a boxed input on this near-black ground reads as a UI panel dropped
   into what is otherwise a composed page ending. */

.invite { margin-top: 44px; }
.invite__summary {
  list-style: none;
  cursor: pointer;
  display: inline-block;
}
.invite__summary::-webkit-details-marker { display: none; }
/* The button visual lives inside the summary, so reset the wrap's own margin. */
.invite__summary .contact__btnwrap { margin-top: 0; }
.invite__label-open { display: none; }
.invite[open] .invite__label-closed { display: none; }
.invite[open] .invite__label-open { display: inline; }

.invite__form {
  max-width: 520px;
  margin: 40px auto 0;
  text-align: left;
}
.invite__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0 24px;
}
.invite__field { margin: 0 0 22px; }
.invite__hint {
  display: block;
  margin: -2px 0 8px;
  font-size: 11px;
  line-height: 1.55;
  color: var(--paper-50);
}
.invite__label {
  display: block;
  margin-bottom: 7px;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper-50);
}
.invite__input {
  width: 100%;
  appearance: none;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule-dark);
  padding: 7px 0;
  font-family: inherit;
  font-size: 13px;
  line-height: 1.6;
  color: var(--paper);
  transition: border-color 200ms ease;
}
/* 0.44, not the 0.3 this started at — at 0.3 a placeholder sits near 2.4:1 on
   this ground, which is unreadable rather than merely quiet. Placeholders here
   carry only examples; anything a sender actually needs is in .invite__hint,
   which does not vanish on first keystroke. */
.invite__input::placeholder { color: rgba(245, 242, 233, 0.44); }
.invite__input:hover { border-bottom-color: var(--rule-dark-40); }
/* Required vs optional stated on the label rather than only enforced at submit,
   so someone can see what the form actually needs before filling it in. */
.invite__req, .invite__opt {
  font-size: 8.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.invite__req { color: var(--paper-70); }
.invite__opt { color: var(--paper-50); }
.invite__input:focus {
  outline: none;
  border-bottom-color: var(--paper);
}
/* The shared :focus-visible ring is a 2px outline offset 3px — on a field that
   is only a hairline it draws a box around nothing. The border carries focus
   instead, and the ring is kept for keyboard users on the controls. */
.invite__textarea { resize: vertical; min-height: 92px; }
.invite__select {
  /* A native select cannot inherit the transparent treatment on every engine;
     give the dropdown itself a dark ground so the options are legible. */
  color: var(--paper);
}
.invite__select option { background: var(--charcoal); color: var(--paper); }

/* Honeypot: off-screen rather than display:none, because some bots skip
   fields that are not rendered at all. */
.invite__pot {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

.invite__actions {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  margin-top: 30px;
}
.invite__submit {
  appearance: none;
  height: 46px;
  padding: 0 30px;
  background: var(--paper);
  color: var(--black);
  border: 0;
  font-family: inherit;
  font-size: 9.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform 160ms var(--ease), opacity 200ms ease;
}
.invite__submit:active { transform: scale(0.97); transition-duration: 120ms; }
.invite__submit[disabled] { opacity: 0.5; cursor: default; }
.invite__status {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--paper-60);
}
.invite__status--error { color: #E8A08A; }

@media (max-width: 520px) {
  .invite__row { gap: 0; }
}

.contact__email {
  margin-top: 26px;
  display: flex;
  gap: 10px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}
.contact__emailval {
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paper-60);
  border-bottom: 1px solid var(--rule-dark-40);
  padding-bottom: 2px;
  transition: color 180ms ease, border-color 180ms ease;
}
a.contact__emailval:hover { color: var(--paper); border-bottom-color: var(--paper); }

.contact__email + .contact__email { margin-top: 14px; }
.contact__emailnote {
  font-size: 10px;
  letter-spacing: 0.08em;
  color: rgba(245, 242, 233, 0.45);
}

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

.footer {
  background: var(--charcoal);
  padding: clamp(64px, 7vw, 96px) clamp(16px, 4vw, 24px) 0;
}
.footer__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 40px;
  padding-bottom: 72px;
}
.footer__brand {
  font-family: var(--serif);
  font-size: 17px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--paper);
}
.footer__meta {
  margin-top: 16px;
  font-size: 11px;
  line-height: 1.8;
  color: var(--paper-60);
}
/* Newsletter signup. Posts to Erin's EXISTING MailerLite list — the same form
   her WordPress site already collects on (account 502025), so the two sites feed
   one list rather than splitting her subscribers.

   Deliberately not MailerLite's embed widget. The site loads no third-party
   JAVASCRIPT — the only external requests in the whole build are Google Fonts
   stylesheets — so their widget would be the first foreign script here, and it
   arrives with its own styling to fight. Instead this takes the shape the
   invitation form already documents: a real POST that works with no JavaScript,
   upgraded by script.js to submit in place.

   Its own band above the grid, because the grid's columns are minmax(160px) and
   a field plus a button does not fit one. */
.footer__signup {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: clamp(24px, 4vw, 64px);
  align-items: start;
  padding-bottom: clamp(40px, 5vw, 62px);
}
.footer__signupnote {
  margin: 10px 0 0;
  max-width: 34ch;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--paper-60);
}
.nl__label {
  display: block;
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}
.nl__row { display: flex; gap: 14px; align-items: flex-end; margin-top: 10px; }
.nl__input {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule-dark);
  padding: 7px 0;
  font-family: inherit;
  font-size: 13px;
  line-height: 1.6;
  color: var(--paper);
  transition: border-color 200ms ease;
}
.nl__input:focus { outline: none; border-bottom-color: var(--paper); }
.nl__input::placeholder { color: var(--paper-50); }
.nl__submit {
  flex: 0 0 auto;
  appearance: none;
  height: 40px;
  padding: 0 22px;
  background: var(--paper);
  color: var(--black);
  border: 0;
  font-family: inherit;
  font-size: 9.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform 160ms var(--ease), opacity 200ms ease;
}
.nl__submit:hover { transform: translateY(-1px); }
.nl__submit:disabled { opacity: 0.5; cursor: default; transform: none; }
/* Honeypot. Off-screen rather than display:none — a bot that skips hidden
   inputs would not fill it, and then it catches nothing. Matches .invite__pot. */
.nl__pot {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

.nl__status { margin: 12px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--paper-60); }
.nl__status--error { color: var(--paper); }
@media (max-width: 720px) {
  .footer__signup { grid-template-columns: 1fr; }
}

.footer__head {
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}
.footer__links {
  margin-top: 16px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  align-items: flex-start;
}
.footer__link {
  font-size: 11.5px;
  color: rgba(245, 242, 233, 0.78);
  border-bottom: 1px solid transparent;
  transition: color 180ms ease, border-color 180ms ease;
}
.footer__link:hover {
  color: var(--paper);
  border-bottom-color: rgba(245, 242, 233, 0.6);
}
.footer__list {
  margin-top: 16px;
  font-size: 11.5px;
  line-height: 1.9;
  color: var(--paper-50);
}
.footer__bottom {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding: 22px 0 26px;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper-50);
}

/* ------------------------------------------------------------ keyframes -- */

@keyframes ehFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ehRise {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Scroll reveal — JS adds .is-in */
.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 600ms var(--ease), transform 600ms var(--ease);
  transition-delay: calc(var(--reveal-i, 0) * 60ms);
}
.reveal.is-in { opacity: 1; transform: translateY(0); }

/* No JS: never leave content hidden */
.no-js .reveal { opacity: 1; transform: none; }

/* For a container whose children should cascade rather than the whole block
   arriving at once. Goes on the element that receives .is-in. */
.stagger-kids > * {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 520ms var(--ease), transform 520ms var(--ease);
}
/* Two triggers, one cascade. A .stagger-kids that is itself a .reveal fires on
   its own .is-in; one nested inside a .reveal fires on the ancestor's, so a
   grid inside an already-revealing block cascades without a second observer
   and without the two arrivals fighting. Safe because no .reveal on this page
   is nested inside another — an ancestor going .is-in can never reveal a
   descendant block that has not had its own turn yet. */
.stagger-kids.is-in > *,
.reveal.is-in .stagger-kids > * { opacity: 1; transform: none; }
.stagger-kids.is-in > *:nth-child(2),
.reveal.is-in .stagger-kids > *:nth-child(2)    { transition-delay: 70ms; }
.stagger-kids.is-in > *:nth-child(3),
.reveal.is-in .stagger-kids > *:nth-child(3)    { transition-delay: 140ms; }
.stagger-kids.is-in > *:nth-child(4),
.reveal.is-in .stagger-kids > *:nth-child(4)    { transition-delay: 210ms; }
.stagger-kids.is-in > *:nth-child(n+5),
.reveal.is-in .stagger-kids > *:nth-child(n+5)  { transition-delay: 280ms; }
/* a.event declares its own hover transition, and at (0,1,1) that beats the
   (0,1,0) cascade rule above — so the one linked row in the events list kept
   its background-color transition, never got an opacity one, and popped in at
   full strength while its seven siblings were still waiting their turn.
   Restated here at (0,2,1) so the whole list animates as one run. */
.stagger-kids > a.event {
  transition: background-color 200ms ease,
              opacity 520ms var(--ease),
              transform 520ms var(--ease);
}

.no-js .stagger-kids > * { opacity: 1; transform: none; }

/* --------------------------------------------------------------- small -- */

/* The nav collapses earlier than everything else below, and needs its own
   query for it. Its 8 items stop fitting one row at ~810px — "Sawmill Poetry"
   is the widest, and past that it wraps the left group to a second line and
   grows the bar from 52px to 58px. Content-sized columns would not save it
   either: at 725px the three groups need 765px whatever the grid does. 860px
   leaves ~22px of headroom, enough to absorb an Archivo fallback.
   Kept separate from the 720px block on purpose — the component rules there
   (.event, .poem__summary, .sawmill__fig, .book-note, .press-card, .fact) are
   phone layouts and should NOT start at 860. */
@media (max-width: 860px) {
  /* 8 links will not fit one row at these widths — collapse them behind
     a Menu button so the bar stays one line and never covers the hero. */
  .nav {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "brand toggle"
      "start start"
      "end   end";
    row-gap: 0;
  }
  .nav__brand   { grid-area: brand; justify-self: start; }
  .nav__toggle  { grid-area: toggle; display: block; }
  .nav__group--start { grid-area: start; }
  .nav__group--end   { grid-area: end; justify-content: flex-start; }
  .nav__group {
    display: flex;
    column-gap: 18px;
    row-gap: 6px;
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    padding-top: 0;
    visibility: hidden;
    transition: max-height 260ms var(--ease), padding-top 260ms var(--ease),
                opacity 180ms ease, visibility 0s linear 260ms;
  }
  .nav.is-open .nav__group {
    max-height: 120px;
    opacity: 1;
    padding-top: 12px;
    visibility: visible;
    transition: max-height 260ms var(--ease), padding-top 260ms var(--ease),
                opacity 180ms ease, visibility 0s;
  }
  .nav.is-open .nav__group--end { padding-bottom: 4px; }
  .nav__link { padding: 6px 0; }
}

@media (max-width: 720px) {
  .event { grid-template-columns: 92px minmax(0, 1fr); }
  .event__tag { grid-column: 2; }
  .poem__summary { grid-template-columns: 32px minmax(0, 1fr) auto; gap: 14px; }
  /* Stacked since 1020; the plate simply tightens its gap on a phone. It stays
     2x2 rather than going single-file — four squares in one column is a very
     long scroll for what is one idea. */
  .sawmill__figures { gap: 16px; }
  .events__portrait { width: 100%; }

  /* stacked cards lose their side borders */
  .book-note, .press-card, .fact { border-left: 0; }
  .book-note:first-child, .book-note:last-child { padding: 26px 0; }
  .press-card:first-child, .press-card:last-child { padding: 34px 0; }
  .fact:first-child, .fact:last-child { padding: 20px 0; }
  .book-note, .press-card { padding-left: 0; padding-right: 0; }
  .fact { padding-left: 0; padding-right: 0; }
  .honor, .honor + .honor { padding: 22px 0 20px; }
}

@media (max-width: 420px) {
  .pullquote__grid { grid-template-columns: 1fr; gap: 10px; }
  .pullquote__mark { display: none; }
}

/* ------------------------------------------------------------- motion --- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
  .reveal { opacity: 1; transform: none; }
}

/* --------------------------------------------------- standalone pages -- */
/* Poem and index pages reuse the section, wrap, eyebrow, poem__body and ulink
   vocabulary already on the homepage. Only what is genuinely new lives here:
   a breadcrumb, a page title at h1 weight, and the between-poems nav. */

.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  margin-bottom: clamp(28px, 4vw, 48px);
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.crumbs a { border-bottom: 1px solid transparent; transition: border-color 180ms ease; }
.crumbs a:hover { border-bottom-color: currentColor; }
.crumbs span[aria-hidden] { opacity: 0.4; }
.crumbs--dark { color: var(--paper-50); }
.crumbs--dark a { color: var(--paper-70); }
.crumbs--dark a:hover { color: var(--paper); }
.crumbs--dark [aria-current] { color: var(--paper); }

/* Matches .poems__title, but as the page's h1 rather than a section h2. */
.poem-page__title {
  margin: 20px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(34px, 5.6vw, 68px);
  line-height: 1.04;
  letter-spacing: 0.01em;
  color: var(--paper);
  max-width: 20ch;
}
.poem-page__rule {
  width: 54px;
  height: 1px;
  margin: clamp(26px, 3.4vw, 40px) 0 clamp(30px, 4vw, 46px);
  background: var(--rule-dark);
}

/* The poem body on the homepage sits inside a <details> that supplies its own
   left gutter. Standing alone it needs that measure set here instead. */
.poem-page__body { max-width: 62ch; }

/* First-publication credit, set after the poem rather than in the header: it is
   an endnote about the poem, not a fact about the page. Sentence case, not the
   uppercase of .poem-page__from directly above — journal names carry italics and
   issue numbers, and uppercasing both would flatten them.

   The left margin REPEATS .poem__body's, rather than sitting at the wrap edge.
   Every poem is indented by clamp(0px, 6vw, 72px) — the base class carries it and
   none of the five body modifiers override it — so a credit at the wrap edge hangs
   72px to the left of the poem it credits at desktop widths. Repeating the clamp
   keeps them aligned at every width, including where it collapses to 0. */
.poem-page__credit {
  margin: clamp(40px, 5vw, 64px) 0 0 clamp(0px, 6vw, 72px);
  max-width: 62ch;
  font-family: var(--sans);
  font-size: 11.5px;
  line-height: 1.7;
  color: var(--paper-50);
}

.poem-page__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 26px;
  align-items: baseline;
  margin-top: clamp(48px, 6vw, 82px);
  padding-top: clamp(24px, 3vw, 36px);
  border-top: 1px solid var(--rule-dark);
}

.poems__more {
  margin-top: clamp(34px, 4.5vw, 56px);
  padding-top: clamp(22px, 2.8vw, 32px);
  border-top: 1px solid var(--rule-dark);
}

/* Book pages reuse the homepage's own .bb__* section markup below the head,
   so the collections look the same standing alone as they do in context.
   Only the head strip and the index list are new. */
.book-head { padding-bottom: 0; }
.book-head__imprint {
  margin: 0 0 22px;
  font-family: var(--sans);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}
.book-head__blurb {
  margin: 0 0 20px;
  max-width: 54ch;
  font-size: 14px;
  line-height: 1.75;
  color: var(--paper-70);
}
.book-head__note {
  margin: 0;
  max-width: 54ch;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--paper-50);
}
.poem-page__nav--flush { margin-top: 0; }

.book-list { display: grid; gap: 0; }
.book-list__item {
  padding: clamp(28px, 3.6vw, 44px) 0;
  border-top: 1px solid var(--rule-dark);
}
.book-list__item:last-child { border-bottom: 1px solid var(--rule-dark); }
.book-list__title {
  margin: 0 0 10px;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(24px, 3.2vw, 38px);
  line-height: 1.1;
  color: var(--paper);
}
.book-list__title a { color: inherit; border-bottom: 1px solid transparent; transition: border-color 180ms ease; }
.book-list__title a:hover { border-bottom-color: var(--rule-dark-40); }
.book-list__imprint {
  margin: 0 0 16px;
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}
.book-list__blurb {
  margin: 0 0 20px;
  max-width: 58ch;
  font-size: 13.5px;
  line-height: 1.75;
  color: var(--paper-70);
}

.poem-page__from {
  margin: 14px 0 0;
  font-family: var(--sans);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}
.poem-page__from a { color: var(--paper-70); border-bottom: 1px solid var(--rule-dark-40); transition: color 180ms ease; }
.poem-page__from a:hover { color: var(--paper); }

/* The collection sits under the title as a quiet second line rather than a
   third column — a row already carries a number and a cue, and a fourth
   element would flatten the hierarchy the title depends on. */
.poem-index__book {
  display: block;
  margin-top: 6px;
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}

.prose { max-width: 62ch; }
.prose p { margin: 0 0 1.15em; font-size: 14px; line-height: 1.78; }
.prose__lead { font-family: var(--serif); font-size: clamp(19px, 2.2vw, 25px) !important; line-height: 1.5 !important; font-weight: 300; margin-bottom: 1.4em !important; }
.prose--dark { color: var(--paper-70); }
.prose--dark .prose__lead { color: var(--paper); }
.prose--dark a { color: var(--paper); border-bottom: 1px solid var(--rule-dark-40); transition: border-color 180ms ease; }
.prose--dark a:hover { border-bottom-color: var(--paper); }
.facts--page { margin-top: clamp(38px, 5vw, 60px); }
/* SECTION HEADS ARE A REAL TIER, not a label. They were 9px uppercase Archivo in
   --paper-50 — smaller AND fainter than the 14px body they introduced, with a
   2.4em top margin that, computed against a 9px font, came to 21.6px. On /press/,
   which is eight stacked sections and nothing else, that left the page with a
   68px title, a 14px wall of text, and no middle: the only markers of structure
   were the least visible thing on it.

   So they move to the site's own display face at roughly 26px, giving a
   68 / 26 / 14 rhythm. The small-caps sans is kept for what it is good at —
   .eyebrow, .label, cites, captions — so meta and structure stop sharing one
   voice and become distinguishable at a glance.

   Separation is space, not a rule: a rule would span 62ch inside .prose and the
   full wrap outside it (see #portraits-h), and the changing width would read as
   an error. The margin is in px, not em, so it no longer scales off the heading's
   own size.

   Used on /press/ (8) and /about/ (4) only — check both when touching this. */
.prose__head {
  margin: clamp(44px, 5.5vw, 66px) 0 0.5em;
  font-family: var(--serif);
  font-size: clamp(21px, 2.1vw, 26px);
  line-height: 1.2;
  letter-spacing: 0.01em;
  text-transform: none;
  font-weight: 300;
  color: var(--paper);
}
/* No :first-child exception. It looks right until you notice /press/ has THREE
   .prose blocks — the exception zeroed the top margin on "Media contact" too,
   because it is first inside its own wrapper, and collapsed it into the portraits
   above. The uniform margin is correct everywhere, including after the title
   rule, where it simply gives the h1 room. */
.prose--dark em { font-style: italic; }
/* A pull quote set inside .prose. Both .prose p and .bb__epigraph p are (0,1,1),
   so source order alone decides — the epigraph rule sits later and wins, which
   also strips the paragraph margin .prose would have given it. Hence the spacing
   here, on the blockquote itself. */
.prose .bb__epigraph { margin: 1.7em 0 1.9em; }
.press-list { margin: 0 0 1.2em; padding: 0; list-style: none; max-width: 62ch; }
/* Titles in here are plain <a>, NOT .ulink. .ulink is a 9.5px uppercase
   inline-block built for short labels ("Order from Black Lawrence Press"); on a
   long article title it shrinks the title below the 13.5px text around it,
   uppercases it, and breaks its wrapping. It also made the one item without a
   URL impossible to match. A bare <a> inside .prose--dark inherits the list's
   size and case and takes only the hairline underline, so linked and unlinked
   citations sit in the same format. */
.press-list li {
  padding: 11px 0;
  border-bottom: 1px solid var(--rule-dark);
  font-size: 13.5px;
  line-height: 1.6;
}
.press-list li:first-child { border-top: 1px solid var(--rule-dark); }
/* /press/ is the live press page and had no way to actually obtain the photographs
   — the downloads only ever existed in media-kit.html, which nothing links to. */
.press-dl {
  margin: 0 0 30px;
  font-size: 12.5px;
  line-height: 1.9;
  color: var(--paper-50);
}

.press-shots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: clamp(14px, 2vw, 24px);
  max-width: 62ch;
  margin: 0 0 14px;
}
.press-shots figure { margin: 0; }
.press-shots img { width: 100%; display: block; aspect-ratio: 4 / 5; object-fit: cover; filter: saturate(0.9); }
.press-credit {
  margin: 0 0 8px;
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}

/* Same tier as .prose__head, and for the same reason. This is an <h2> opening
   the poem index on every book page, and it was 9px uppercase Archivo in
   --paper-50 with margin-top ZERO — so on /books/consent/ it sat flush against
   the bottom of the praise columns and read as a caption belonging to the last
   blurb rather than as the heading of the list beneath it.

   The top margin is what separates it; the size is what stops it being mistaken
   for a label. .bb__praise above it opens with clamp(64px, 8vw, 110px), so this
   is set a little wider to make the break read as larger than the one inside the
   praise block itself.

   book.njk renders this TWICE — once after the praise block (books without
   pageHtml, e.g. Consent) and once at the top of its own section (books with
   pageHtml, e.g. No Spare People, Barnburner). In the second case the section's
   own padding already provides the space and this margin would double it, hence
   the :first-child reset. That reset is scoped to `.wrap >` deliberately: an
   unscoped one caused exactly this bug on /press/, where a section had three
   .prose blocks and the exception fired on the wrong one. */
.book-poems__head {
  margin: clamp(72px, 9vw, 118px) 0 clamp(20px, 2.6vw, 30px);
  font-family: var(--serif);
  font-size: clamp(21px, 2.1vw, 26px);
  line-height: 1.2;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--paper);
  font-weight: 300;
}
.wrap > .book-poems__head:first-child { margin-top: 0; }

.poem-index { list-style: none; margin: 0; padding: 0; }
.poem-index__item { border-top: 1px solid var(--rule-dark); }
.poem-index__item:last-child { border-bottom: 1px solid var(--rule-dark); }
.poem-index__link {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr) auto;
  gap: 20px;
  align-items: baseline;
  padding: 22px 0;
  color: var(--paper-70);
  transition: color 180ms ease;
}
.poem-index__link:hover { color: var(--paper); }
.poem-index__name {
  font-family: var(--serif);
  font-size: clamp(19px, 2.4vw, 27px);
  font-weight: 300;
  line-height: 1.2;
}
@media (max-width: 720px) {
  .poem-index__link { grid-template-columns: 32px minmax(0, 1fr) auto; gap: 14px; }
}

/* --------------------------------------------------------------- print -- */

@media print {
  .nav, .hero__scrim, .contact__btnshadow { display: none; }
  body { background: #fff; color: #000; }
  .section--dark, .contact, .footer, .band { background: #fff !important; }
  .poems__title, .sawmill__title, .contact__title, .footer__brand,
  .poem__name, .poem__body p, .sawmill__body { color: #000 !important; }
  .poem__body { display: block !important; }
  .reveal { opacity: 1 !important; transform: none !important; }
}


/* ==========================================================================
   Media kit  (media-kit.html)
   Uses the same tokens, type scale and rules as the main page.
   ========================================================================== */

.nav__link--current { color: var(--ink); border-bottom-color: var(--rule-strong); }

/* ------------------------------------------------------------- masthead -- */

.mk-head {
  background: var(--charcoal);
  padding: calc(var(--nav-h) + clamp(64px, 8vw, 120px)) clamp(16px, 4vw, 24px) clamp(56px, 7vw, 96px);
}
.mk-head__title {
  margin: 20px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(44px, 8.4vw, 108px);
  line-height: 0.92;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--paper);
}
.mk-head__sub {
  margin: 22px 0 0;
  font-family: var(--serif);
  font-size: clamp(18px, 2.2vw, 26px);
  color: var(--paper-70);
}
.mk-head__intro {
  margin: 30px 0 0;
  max-width: 52ch;
  font-size: 13px;
  line-height: 1.74;
  color: var(--paper-60);
  text-wrap: pretty;
}

/* ------------------------------------------------------------ at a glance -- */

.mk-glance {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: clamp(40px, 7vw, 96px);
  align-items: start;
}

.mk-facts { margin: 0; }
.mk-facts > div {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr);
  gap: 20px;
  padding: 15px 0;
  border-bottom: 1px solid var(--rule);
}
.mk-facts dt {
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-label);
  padding-top: 3px;
}
.mk-facts dd {
  margin: 0;
  font-family: var(--serif);
  font-size: 17px;
  line-height: 1.35;
  color: var(--ink);
}
.mk-facts--dark > div { border-bottom-color: var(--rule-dark); }
.mk-facts--dark dt { color: var(--paper-50); }
.mk-facts--dark dd { color: var(--paper); }

.mk-link {
  border-bottom: 1px solid var(--rule-strong);
  transition: border-color 200ms ease;
}
.mk-link:hover { border-bottom-color: var(--ink); }
.mk-link--dark { border-bottom-color: var(--rule-dark-40); }
.mk-link--dark:hover { border-bottom-color: var(--paper); }

.mk-photo { margin: 0; }
.mk-photo img { width: 100%; display: block; filter: saturate(0.92); }

.mk-dl {
  display: block;
  margin-top: 16px;
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: 8px;
  transition: border-color 200ms ease;
}
.mk-dl:hover { border-bottom-color: var(--ink); }
.mk-dl span {
  display: block;
  margin-top: 6px;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: none;
  color: var(--ink-muted);
}
.mk-dl--dark { color: var(--paper); border-bottom-color: var(--rule-dark-40); }
.mk-dl--dark:hover { border-bottom-color: var(--paper); }
.mk-dl--dark span { color: var(--paper-50); }

/* ----------------------------------------------------------------- bios -- */

.mk-bios {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  margin-top: 74px;
  border-bottom: 1px solid var(--rule);
}
.bio {
  padding: 30px 30px 34px;
  border-top: 1px solid var(--rule);
  border-left: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
}
.bio:first-child { padding-left: 0; border-left: 0; }
.bio:last-child  { padding-right: 0; }

.bio__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 14px;
}
.bio__count {
  font-size: 9px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.bio__text { margin-top: 18px; flex: 1; }
.bio__text p {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.75;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.bio__text p + p { margin-top: 13px; }

.bio__copy {
  align-self: flex-start;
  margin-top: 24px;
  appearance: none;
  background: none;
  border: 1px solid var(--rule-strong);
  color: var(--ink);
  font-family: inherit;
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 8px 14px;
  cursor: pointer;
  text-align: center;
  /* filter/opacity match the 90ms the script holds the swap, so the blur is
     at full strength exactly as the word changes, not still on its way. */
  transition: background-color 200ms ease, color 200ms ease,
              filter 90ms ease, opacity 90ms ease,
              transform 160ms var(--ease);
}
.bio__copy:hover { background: var(--ink); color: var(--cream); }
.bio__copy:active { transform: scale(0.97); transition-duration: 120ms; }
/* Blur bridges the two labels so you read one word changing, not two words
   overlapping. JS holds this class for ~90ms either side of the swap. */
.bio__copy.is-swapping { filter: blur(2px); opacity: 0.72; }
.bio__copy.is-copied { background: var(--ink); color: var(--cream); border-color: var(--ink); }

.mk-note {
  margin: 26px 0 0;
  font-size: 11px;
  line-height: 1.7;
  color: var(--ink-label);
}
.mk-note--dark { color: var(--paper-50); margin-top: 34px; }

/* ----------------------------------------------------------------- book -- */

.mk-book {
  display: grid;
  grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr);
  gap: clamp(40px, 7vw, 90px);
  align-items: start;
}
.mk-book__cover { margin: 0; }
.mk-book__cover img { width: 100%; display: block; }
.mk-book__title {
  margin: 20px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(34px, 5vw, 60px);
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--paper);
}
.mk-book__blurb {
  margin: 28px 0 34px;
  max-width: 60ch;
  font-size: 13px;
  line-height: 1.76;
  color: var(--paper-70);
  text-wrap: pretty;
}
.mk-booklinks { display: flex; gap: 26px; flex-wrap: wrap; margin-top: 34px; }

/* The book block was built for the dark No Spare People section only. Barnburner
   now uses the same component on cream, so its title and blurb need ink values. */
.mk-book--light .mk-book__title { color: var(--ink); }
.mk-book--light .mk-book__blurb { color: var(--ink-soft); }
/* .ulink--quiet is defined as paper-on-dark, so on a light ground it renders
   at ~1:1 — invisible. Third place this has had to be corrected; the class is
   dark-surface-only and would be better renamed. */
.mk-book--light .ulink--quiet       { color: var(--ink-label); }
.mk-book--light .ulink--quiet:hover { color: var(--ink); border-bottom-color: var(--rule-strong); }

/* Interview strip: light by default, dark variant for the No Spare People side.
   Entries are links now rather than plain text, on both books. */
.mk-inline__list a {
  border-bottom: 1px solid transparent;
  transition: color 180ms ease, border-color 180ms ease;
}
.mk-inline__list a:hover { border-bottom-color: var(--rule-strong); }
.mk-inline--dark { border-top-color: var(--rule-dark); }
.mk-inline--dark .mk-inline__list { color: var(--paper); }
.mk-inline--dark .mk-inline__list a { color: var(--paper-70); }
.mk-inline--dark .mk-inline__list a:hover { color: var(--paper); border-bottom-color: var(--rule-dark-40); }

/* --------------------------------------------------------------- quotes -- */

.mk-quotes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(28px, 4vw, 56px);
  margin-top: 44px;
}
.mk-quote { margin: 0; }
.mk-quote p {
  margin: 0;
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.6;
  color: var(--paper-80);
  text-wrap: pretty;
}
.mk-quote cite {
  display: block;
  margin-top: 14px;
  font-family: var(--sans);
  font-style: normal;
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}
.mk-quotes--light { margin-top: 74px; }
.mk-quotes--light .mk-quote p   { color: var(--ink-soft); }
.mk-quotes--light .mk-quote cite { color: var(--ink-label); }

.mk-inline {
  margin-top: 60px;
  padding-top: 26px;
  border-top: 1px solid var(--rule);
  display: flex;
  gap: 26px;
  align-items: baseline;
  flex-wrap: wrap;
}
.mk-inline__list { margin: 0; font-family: var(--serif); font-size: 17px; color: var(--ink); }

/* --------------------------------------------------------------- record -- */

.mk-record {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: clamp(40px, 7vw, 96px);
  margin-top: 74px;
  align-items: start;
}
.mk-awards { margin-top: 20px; border-top: 1px solid var(--rule-dark); }
.mk-award {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 20px;
  align-items: baseline;
  padding: 16px 0;
  border-bottom: 1px solid var(--rule-dark);
}
.mk-award__year {
  font-size: 9.5px;
  letter-spacing: 0.12em;
  color: var(--paper-50);
  font-variant-numeric: tabular-nums;
}
.mk-award__kind {
  display: block;
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1.25;
  color: var(--paper);
}
.mk-award__who { display: block; margin-top: 4px; font-size: 11px; color: var(--paper-50); }

.mk-tags {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.mk-tags li {
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paper-70);
  border: 1px solid var(--rule-dark);
  padding: 6px 11px;
}

.mk-list {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  font-size: 12.5px;
  line-height: 1.95;
  color: var(--paper-70);
}


/* ------------------------------------------------------------ downloads -- */

.mk-downloads { margin-top: 74px; border-top: 1px solid var(--rule); }
.mk-file {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) auto;
  gap: 24px;
  align-items: center;
  padding: 22px 0;
  border-bottom: 1px solid var(--rule);
  transition: background-color 200ms ease;
}
.mk-file:hover { background: rgba(32, 32, 32, 0.03); }
.mk-file__kind {
  font-size: 9px;
  letter-spacing: 0.14em;
  color: var(--ink-label);
  border: 1px solid var(--rule);
  padding: 5px 0;
  text-align: center;
}
.mk-file__name {
  display: block;
  font-family: var(--serif);
  font-size: 21px;
  line-height: 1.25;
  color: var(--ink);
}
.mk-file__meta { display: block; margin-top: 5px; font-size: 11px; color: var(--ink-muted); }
.mk-file__go {
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-label);
}
.mk-file:hover .mk-file__go { color: var(--ink); }

/* ---------------------------------------------------------------- small -- */

@media (max-width: 860px) {
  .mk-glance, .mk-book { grid-template-columns: 1fr; }
  .mk-book__cover { max-width: 260px; }
  .mk-photo { max-width: 340px; }
}

@media (max-width: 720px) {
  .bio { padding: 30px 0 34px; border-left: 0; }
  .mk-facts > div { grid-template-columns: 1fr; gap: 5px; }
  .mk-file { grid-template-columns: 52px minmax(0, 1fr); }
  .mk-file__go { grid-column: 2; }
  .mk-award { grid-template-columns: 74px minmax(0, 1fr); gap: 14px; }
}

.press__lead {
  margin: 22px auto 0;
  max-width: 46ch;
  font-size: 12.5px;
  line-height: 1.72;
  color: var(--ink-soft);
  text-wrap: pretty;
}

/* ==========================================================================
   Barnburner feature section
   ========================================================================== */

/* Shared layout for both book features. Deliberately nothing visual here —
   hanging treatment off .bb once gave No Spare People a burning house and a
   2168px height, because both sections share the class. Scope anything visual
   to #barnburner, as the backdrop below does. */
.bb {
  padding-bottom: clamp(84px, 10vw, 150px);
}

/* Duncannon, Pennsylvania — a distressed print of the main street, with ink
   flames drawn along the rooftops. Sits above the section's charcoal ground and below its content:
   z-index:-1 inside an isolated stacking context paints over the element's own
   background but under its children. The stacking context lives here and not
   on .bb, for the reason above.
   No aspect-ratio: the section sizes to its content and `cover` crops the
   print's sky and foreground. Shaping the section to the picture is what left
   an empty half when that rule reached the wrong section.
   GROUND: --black, not the --charcoal that .section--dark gives the other two
   dark sections. The print's own border is nearly black, so on charcoal the
   section read as a grey surround with a darker picture sitting on it; against
   --black the two meet and the burnt edge becomes the edge of the section.
   Scoped here rather than changed on .section--dark, which Poems and Sawmill
   also use.
   OPACITY: 0.22, not the 0.3 the burning-house image carried. This print is
   far lighter (mean #ADA59E, brightest band #CDC5BD), so it lifts the ground
   rather than darkening it — at 0.3 over charcoal the body copy fell to
   4.35:1, under AA. On black the darker ground buys that back and more: at
   0.30 body copy holds 5.5:1 and headings 8.6:1, both past AA. 0.30 is the
   practical ceiling — 0.35 puts body copy at 4.9:1 and 0.40 breaks it. */
#barnburner {
  background: var(--black);
  position: relative;
  isolation: isolate;
  /* NB no overflow:hidden here. It was needed only while ::before was 124%
     tall for the parallax and overhung the sections either side; ::before is
     inset:0 now, so there is nothing to clip. Re-adding it would silently
     break the aspect-ratio below: a clipped box no longer grows to its
     content, and at phone widths the ratio asks for 667px against ~1970px of
     copy — the section would swallow two thirds of its own text. */
  /* Shaped close to the picture (the asset is 1400x2489), so nearly all of the
     print shows — sky, street and flames — rather than the ~400px that was
     cropped when the section kept its own content-driven proportions.
     Deliberately 2240 and not 2489: at the exact ratio `cover` fills with zero
     slack, and a background with no slack has nothing to pan through. That 10%
     of give IS the parallax travel. The two cannot both be maximised — one
     wants no overhang, the other needs it.
     aspect-ratio yields to content, so this only ever ADDS height: on narrow
     screens the copy is already taller than the ratio asks for and the rule
     does nothing. Scoped to the id — this is the rule that, on the shared .bb
     class, once gave No Spare People a 2168px empty section. */
  aspect-ratio: 1400 / 2240;
  --bb-backdrop: 0.3;
}
#barnburner::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Panned by script.js via --bb-shift. `cover` guarantees the box stays
     filled, so the shift can only slide through the slack the section's 10% of
     give provides — it can never open a gap.
     Moving the BACKGROUND rather than the element is what makes this safe: a
     background is clipped to its own box, so there is no overhang to land on
     the cream pull-quote below, and no overflow:hidden is needed. That matters
     — clipping here would stop the section growing to its content, which at
     phone widths hid two thirds of the copy. */
  background: url('illustrations/duncannon-flames.webp') no-repeat;
  background-size: cover;
  background-position: center calc(50% + var(--bb-shift, 0px));
  opacity: var(--bb-backdrop);
  /* No edge mask: the picture runs full bleed to the section's edges. There
     used to be two intersected linear-gradients here fading all four edges to
     transparent, which read as a vignette. Removed deliberately.
     The comment that justified them claimed the print's paper margin was light
     (~#E5D7C8) and would show as a grey frame. Re-measured off the asset, the
     edges are #BEB0A2 to #D0C0B2, luminance 178-194 — actually DARKER than the
     interior's 200, because the brightest part of this print is its sky, not
     its border. There was no light frame to hide. If a soft edge is ever wanted
     back, it was that gradient pair with mask-composite: intersect. */
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  #barnburner::before { background-position: center 50%; }
}

/* No Spare People — a swingset at dusk, the child seen from behind. Scoped to
   the id and never to .bb: that class is shared with Barnburner, and hanging
   anything visual off it once gave this section a burning house.

   WHY THIS SECTION IS NOW DARK. It ran bone (#E8E3D2) with ink copy until this
   picture arrived. The picture is low-key — mean #2F3B4A, median luminance 59 —
   and a backdrop DARKENS the ground under dark text. On bone the dimmest tone,
   ink-label, sits at 5.27:1, leaving sixteen levels of headroom, which capped
   this picture at 0.08 opacity: an invisible smudge. Washed high-key to fit that
   ceiling it lost the sky entirely and the swingset became a ghost. Inverting
   the section is what makes the picture usable at all, and it gives the two book
   features matching treatment. The page still alternates — books (light), this
   (dark), pull-quote (cream), Barnburner (black) — so no two dark grounds meet.

   GROUND: --charcoal, straight from .section--dark, with no override. #barnburner
   needs --black so its print's light paper border meets the section edge; this
   picture is full-bleed and its bottom edge composites to luminance 41 against
   charcoal's 39, so the two meet with no seam. THIS source has no border — an
   earlier one was a print photographed with its cream surround, which had to be
   kept whole and drove a very different geometry. Check before assuming.

   OPACITY 0.54, measured, and 0.54 is the ceiling. Note the direction: the
   picture (mean luminance 57) is LIGHTER than the charcoal ground (39), so
   raising opacity lightens this section and lowering it darkens — the opposite
   of the intuition, and the reason 0.40 read too dark. Worst case is the
   picture's bright tail (p99 = 91) behind the dimmest light tone: --paper-50
   holds 4.53:1 there, 5.51:1 over the picture's mean, and body copy 8.83:1.
   Measured over the band the copy ACTUALLY covers in the tall layout below
   (31%-76% of the print at 1254px, and 20%-76% at 960px, since the copy is
   pinned to the child's head) it is 4.50:1, 5.02:1 and 8.74:1 — the same line,
   at every width. 4.5 is the AA line, which is why nothing above 0.54 is
   available — 0.58 drops --paper-50 to 4.43 — and why no white overlay is
   either.

   THE ASSET IS TONED, and that is what buys the budget. The sunset original is
   far too bright to use raw: mean luminance 109, p99 223, and --paper-50 fails
   AA at EVERY opacity at that brightness. A
   luminance curve (out = 0.47 * 255 * (1 - e^-1.62L), applied as a proportional
   RGB scale so hue and saturation are untouched) maps it onto the envelope above
   and hard-limits the highlights at 120. Do not swap in an untoned frame and
   expect the numbers here to hold — re-measure, or the quote text goes under AA
   across the whole section. originals/derive-nsp-backdrop.py regenerates it.

   The rolloff also fixes what the previous backdrop could not: its brightest
   pixels reached luminance 162 and put --paper-50 at 2.94:1 over the child's
   shirt. This asset's maximum is 95, so the darkest light tone holds 4.42:1 at
   the single brightest pixel and 4.53:1 across the bright tail. No hot patch.

   POSITION bottom AND RIGHT, not centre. The frame is 1600x2727 and the swing set
   runs diagonally from the middle of the frame out to its RIGHT EDGE — measured,
   it occupies x 51-100%. Any centred crop therefore shaves the near leg off. The
   left of the frame is open field and sky, which is what can afford to be lost,
   so the crop is taken from there instead. At 960px that is 26% off the left; at
   1254px, 8%; at 1920px the section matches the picture's aspect exactly and
   nothing is cropped at all.
   From a bottom anchor only a DOWNWARD shift would be safe, since moving the
   picture up lifts its bottom edge off the section's and opens a gap, which is
   why script.js pans --nsp-shift over 0..range rather than ±range and clamps
   range each frame to the slack `cover` actually leaves.

   AT 960px AND UP THAT PAN IS INERT, by arithmetic rather than by a rule: the
   min-height below forces the section to be at least as tall as the picture, so
   `cover` scales by height, the vertical slack is zero and the clamp returns
   zero. The dust keeps its 150s drift but loses its scroll parallax. The pan
   still runs in the 721-959px band, where the section is sized by its copy.

   THAT CLAMP NEEDS THE ASSET'S ASPECT RATIO, and script.js carries its own
   hardcoded copy of it — search `--nsp-shift` there and update it whenever the
   asset here changes. It is 1792/2400 today.

   NO aspect-ratio: that rule, on the shared class, once gave this section 2168px
   of empty height. Height comes from min-height and content, never from it. */
#no-spare-people { position: relative; isolation: isolate; }
#no-spare-people::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: url('illustrations/no-spare-people-backdrop-7.webp') no-repeat;
  background-size: cover;
  background-position: right calc(100% + var(--nsp-shift, 0px));
  opacity: 0.54;
  /* Fades the top edge only, so the sky does not end in a hard line against the
     books section above. The bottom is left alone — the picture's own dark field
     already matches the ground there. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 12%, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 12%, #000 100%);
  pointer-events: none;
}

/* ---- THE TALL LAYOUT, 960px and up ----------------------------------------
   Above this width the WHOLE PRINT IS SHOWN, border and all, and it is the
   picture — not the copy — that sets the section's height. The copy sits on the
   empty sky in the upper half and the swingset reads below it, clear of the book
   cover. That was the point: at `cover` the swingset sat *behind* the copy and
   the cover plate hid most of the frame.

   960px IS MEASURED, not chosen for tidiness. The picture's height scales with
   the viewport, but the copy gets TALLER as the column narrows, so the two cross
   over. Where the copy ends, as a fraction of the picture (the swingset starts
   at 67%): 721px -> 109%, 768px -> 101%, 900px -> 65%, 960px -> 60%, 1254px ->
   50%, 1440px -> 45%. Below ~900 the copy overruns the whole frame and the
   section outgrows the picture, leaving a charcoal band under it — 239px of one
   at 721px. So this layout starts at 960, where there is real margin, and
   everything narrower keeps the `cover` treatment above.

   `100% auto`, not `cover`: it always scales by WIDTH, so the left and right
   borders can never be cropped however the height lands. Any mismatch is
   absorbed at the bottom against the section's own ground instead. `cover` would
   scale by height whenever the section ran tall and shave the side borders off —
   the one edge that must not move.

   min-height is the asset's own aspect, written as its pixel dimensions rather
   than a rounded 240.93vw so it is greppable against the file and against the
   copy of the same ratio in script.js. With classic (space-taking) scrollbars
   100vw runs slightly wider than the section and this overshoots by a few px,
   which lands as charcoal below the picture, never as a crop.

   The pan goes quiet here by arithmetic, not by a rule: script.js clamps the
   shift to the slack `cover` leaves, and at this height there is none. The dust
   keeps its 150s drift but loses its scroll parallax. */
@media (min-width: 960px) {
  #no-spare-people {
    /* THERE IS NO SUBJECT TO PIN TO IN THIS FRAME. The previous backdrop had a
       child on a swing and the copy was pinned so the last line landed just above
       her head. This one is an empty swing set, shot at an angle, with no single
       point that has to clear the text — so the rule is now simply that the
       bottom THIRD of the picture stays clear of the copy. That is where the
       seats and the worn ground under them read; above it, the copy crosses the
       frame and the tree line, which is intended.

       Two regimes, as before, and max() takes whichever binds. Narrow: the copy
       is taller than the picture, the section is sized by content, and 35% of it
       works out near 800px because the copy's own height barely varies. Wide:
       min-height takes over and the reserve has to grow with the viewport.

       min-height is the asset's aspect, which keeps the section at least as tall
       as the picture so `cover` never scales by width and leaves a gap. */
    min-height: calc(100vw * 2727 / 1600);
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding-bottom: max(800px, calc(100vw * 2727 / 1600 * 0.35));
  }
  #no-spare-people::before {
    /* THE PARALLAX LIVES HERE, and it needs slack that `cover` no longer leaves.
       min-height above forces the section at least as tall as the picture, so
       `cover` scales by HEIGHT and the vertical slack goes to zero — measured
       -163px, which clamped --nsp-shift to 0.0px and killed the pan outright.

       So the picture is deliberately oversized instead: `auto 112%` makes it 12%
       taller than its box, which IS the travel. 12% of a 2300px section is 276px
       of slack against a 180px peak, so the bottom edge can never be bared.

       Anchored bottom, panning DOWNWARD only — from a bottom anchor an upward
       shift lifts the picture's bottom edge off the section's and opens a gap.
       At rest the shift is 0 and the bottom sits flush, so the composition is
       exactly what it was before the pan was reinstated.

       The oversize crops more from the left: 33% at 960px, 17% at 1254, 11% at
       1920. The swing structure starts at x 51%, so it survives all three. */
    background-size: auto 112%;
    background-position: right calc(100% + var(--nsp-shift, 0px));
  }
}
/* Dropped below the same breakpoint as the Barnburner backdrop and for the same
   reason: the section is sized by its copy there, so `cover` scales by height and
   shows a narrow vertical slice — here, mostly empty sky. display:none, not
   opacity:0, so phones never fetch the asset. Placed after the base rule — a
   media query adds no specificity, so source order is what carries it. */

/* Vignette. A second layer rather than a background on ::before, because that
   one carries opacity 0.54 and anything folded into it would be dimmed by the
   same factor — this needs its own strength. Same z-index as ::before, so
   source order alone puts it above the picture and below the copy.

   It darkens TOWARDS --charcoal, the section's own ground, not towards black.
   That matters at the top: ::before is masked to transparent over the first
   12%, so the picture is already pure ground there, and a vignette reaching for
   black would paint a dark band across it — a visible seam against the light
   books section above. Charcoal over charcoal is invisible, so the gradient
   simply stops mattering where the picture stops.

   Centred at 55% rather than 50%: the swingset occupies roughly 47-87% of the
   box's height, so biasing the clear zone downwards keeps the subject out of
   the falloff and puts the darkening where the frame is brightest — the sky and
   the upper corners.

   Contrast: a vignette can only darken, and every tone over it is light, so
   this can only raise the ratios measured above. It is the one layer here that
   needs no contrast budget. */
#no-spare-people::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse at 50% 55%,
              transparent 30%, rgba(39, 39, 40, 0.65) 100%);
  pointer-events: none;
}

/* Dust. Three depth layers of motes sitting between the photograph and the
   vignette, so the corners dim the dust the same way they dim the picture —
   which is what happens in air. Paint order is document order at the shared
   z-index: ::before, then this element, then ::after.

   This is the ONLY looping animation on the site. Everything else animates once
   on arrival and stops, so it is kept deliberately subliminal: 1-3px motes at
   0.05-0.14 alpha, 150s a pass, no twinkle. Dust in still air, not sparkle —
   this section carries a book about coercive control, and glinting particles
   would read as whimsy.

   MOTION IS SPLIT, and each half is cheap. The parent carries the drift as one
   transform animation the compositor can run without repainting; air moves as a
   mass, so a single shared drift is also what is physically true. The three
   children carry the parallax, each at a different multiple of --nsp-shift, and
   nested transforms compose — so a child's real motion is the drift plus its own
   depth. That difference is the entire depth cue.

   THE DRIFT IS VERTICAL ONLY, and that is not an aesthetic choice. A horizontal
   drift needs horizontal overflow to translate into, and the note at the top of
   this file forbids overflow-x on html/body (it computes the other axis to auto
   and breaks window.scrollTo, and with it every anchor link). Clipping locally
   is out too — #barnburner documents why overflow:hidden must stay off these
   sections. So the layer overhangs upward only, where its own mask has already
   faded it to nothing, and drifts down. Zero horizontal overflow.

   COLOUR is keyed to the picture rather than invented: the fog measures #29303B
   to #323C46 once composited at 0.54 over charcoal, so the motes are a lighter,
   desaturated cast of that same blue-grey instead of white specks.

   MASKED to the upper region, for two reasons that agree. The sky is where the
   frame is empty enough for dust to read at all, and it keeps motes off the
   quote columns below, where --paper-50 sits at 4.50:1 with no margin to spend. */
.nsp-air {
  /* OFF since 2026-09-05, deliberately. Everything below still describes a real
     effect and it still works — but its COLOUR was sampled off the backdrop this
     section used to carry, the blue-grey night photograph whose sky composited to
     #272F3A-#313A45. Against that, rgb(169,183,199) read as pale dust in air.

     The backdrop is now a golden-hour frame compositing to #2C2B2C-#3A3533, so
     those same motes read as a cold sheen over a warm picture and the navy fog
     masses muddy it rather than deepening it. Josh called it "a glow and haze"
     and asked for it gone.

     Kept rather than deleted because re-keying is the obvious alternative and it
     is six colour values: the four rgba(169,183,199,*) mote sets and the three
     rgba(16,22,32,*) fog masses want warm equivalents. If this is still off when
     someone next touches the section, delete this rule block, the four
     .nsp-air > span rules, @keyframes nspDrift, and the .nsp-air markup — which
     lives in BOTH src/index.njk and src/_data/books.json -> pageHtml.

     Contrast is unaffected: every ratio measured for this section was taken on
     the photograph over charcoal without the dust, so removing it changes none
     of them. It does lighten the section very slightly, since the fog masses
     were the darker half of the effect. */
  display: none;

  position: absolute;
  inset: -90px 0 auto;
  height: 76%;
  z-index: -1;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 14%, #000 56%, transparent 94%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 14%, #000 56%, transparent 94%);
  /* no-repeat matters now that the layers overhang this box: a tiled mask would
     stamp a second fade across the overhang instead of hiding it. */
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  animation: nspDrift 150s linear infinite alternate;
}
/* The overhang is headroom for the pan. The near layer now travels 1.2x
   --nsp-shift, and without room above it a downward pan would bare its own top
   edge inside the frame. Top only — horizontal overhang is forbidden here. */
.nsp-air > span { position: absolute; inset: -300px 0 0; background-repeat: repeat; }

/* 1. HAZE. Drifting fog masses, and the one layer that can be generous: it
   DARKENS. Measured across the section, every band that carries text has between
   -0.02 and +0.13 of contrast headroom at --paper-50 — effectively none — so
   nothing here may brighten a large area. Darkening spends nothing and buys
   contrast back, and denser fog over a dusk field is what atmosphere actually
   looks like. Slowest parallax, so it reads as the furthest thing in the frame. */
.nsp-air > span:nth-child(1) {
  background-image:
    radial-gradient(60% 34% at 22% 26%, rgba(16, 22, 32, 0.20) 0, transparent 70%),
    radial-gradient(52% 28% at 74% 44%, rgba(16, 22, 32, 0.17) 0, transparent 72%),
    radial-gradient(70% 30% at 46% 70%, rgba(16, 22, 32, 0.14) 0, transparent 74%);
  background-size: 1600px 1100px;
  transform: translate3d(0, calc(var(--nsp-shift, 0px) * 0.10), 0);
}
/* 2-4. DUST, brighter than before so the parallax is legible at all. Motes are
   1-5px and sparse, so they are point features rather than background — the
   large-area rule above still holds. The rates are spread much wider than
   before (0.38 / 0.75 / 1.20 against the old 0.18 / 0.42 / 0.75) because the
   depth cue is the DIFFERENCE between layers, and at the old spread the layers
   moved too nearly together to separate. */
.nsp-air > span:nth-child(2) {
  background-image:
    radial-gradient(circle 1px   at 7%  23%, rgba(169,183,199,0.09) 0, transparent 100%),
    radial-gradient(circle 1px   at 31% 61%, rgba(169,183,199,0.08) 0, transparent 100%),
    radial-gradient(circle 1.5px at 54% 12%, rgba(169,183,199,0.10) 0, transparent 100%),
    radial-gradient(circle 1px   at 78% 44%, rgba(169,183,199,0.08) 0, transparent 100%),
    radial-gradient(circle 1px   at 19% 86%, rgba(169,183,199,0.08) 0, transparent 100%),
    radial-gradient(circle 1.5px at 66% 77%, rgba(169,183,199,0.09) 0, transparent 100%),
    radial-gradient(circle 1px   at 92% 9%,  rgba(169,183,199,0.10) 0, transparent 100%);
  background-size: 830px 710px;
  transform: translate3d(0, calc(var(--nsp-shift, 0px) * 0.38), 0);
}
.nsp-air > span:nth-child(3) {
  background-image:
    radial-gradient(circle 2px   at 14% 38%, rgba(169,183,199,0.13) 0, transparent 100%),
    radial-gradient(circle 2.5px at 43% 7%,  rgba(169,183,199,0.11) 0, transparent 100%),
    radial-gradient(circle 2px   at 69% 55%, rgba(169,183,199,0.14) 0, transparent 100%),
    radial-gradient(circle 2.5px at 27% 81%, rgba(169,183,199,0.11) 0, transparent 100%),
    radial-gradient(circle 2px   at 88% 27%, rgba(169,183,199,0.13) 0, transparent 100%),
    radial-gradient(circle 2.5px at 56% 93%, rgba(169,183,199,0.12) 0, transparent 100%);
  background-size: 610px 530px;
  background-position: 130px 90px;
  transform: translate3d(0, calc(var(--nsp-shift, 0px) * 0.75), 0);
}
.nsp-air > span:nth-child(4) {
  background-image:
    radial-gradient(circle 3.5px at 22% 19%, rgba(169,183,199,0.16) 0, transparent 100%),
    radial-gradient(circle 3px   at 61% 41%, rgba(169,183,199,0.14) 0, transparent 100%),
    radial-gradient(circle 4.5px at 38% 72%, rgba(169,183,199,0.13) 0, transparent 100%),
    radial-gradient(circle 3px   at 84% 63%, rgba(169,183,199,0.15) 0, transparent 100%);
  background-size: 520px 450px;
  background-position: 270px 210px;
  transform: translate3d(0, calc(var(--nsp-shift, 0px) * 1.20), 0);
}
@keyframes nspDrift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, 26px, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .nsp-air { animation: none; }
}
/* Only where the backdrop is still bottom-anchored. Above 960px it is anchored
   top and shows whole, so resetting it to `bottom` here would crop the print's
   top border straight back off. */
@media (prefers-reduced-motion: reduce) and (max-width: 959px) {
  #no-spare-people::before { background-position: center bottom; }
}
@media (max-width: 720px) {
  #no-spare-people::before,
  #no-spare-people::after,
  .nsp-air { display: none; }
}


.bb__head { text-align: center; }
.bb__title {
  margin: 20px 0 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(40px, 7.4vw, 96px);
  line-height: 0.96;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--paper);
}
.bb__imprint {
  margin: 18px 0 0;
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--paper-50);
}

.bb__main {
  display: grid;
  grid-template-columns: minmax(0, 0.66fr) minmax(0, 1.34fr);
  gap: clamp(40px, 7vw, 92px);
  align-items: start;
  margin-top: clamp(56px, 7vw, 92px);
}
/* Barnburner runs cover-right; No Spare People below it keeps cover-left, so
   the two book features alternate rather than repeat. Scoped to the id because
   .bb__main is shared — flipping it on the class would flip both, which is the
   same trap that once gave No Spare People a burning house.
   `order` rather than explicit grid-column placement: the cover stays first in
   the DOM, so the single-column stack below 860px still leads with the book
   without needing the columns re-declared. No focusable elements live in the
   cover, so visual order and tab order do not diverge. */
#barnburner .bb__main {
  grid-template-columns: minmax(0, 1.34fr) minmax(0, 0.66fr);
}
#barnburner .bb__cover { order: 2; }
#barnburner .bb__body  { order: 1; }

.bb__cover { margin: 0; }
/* The homepage's two large book covers are links to their own pages. The block
   display keeps the anchor flush to the image so the shadow below still reads as
   the cover's own, not a box around a link. Only the homepage copy is wrapped:
   the identical markup in books.json renders ON each book page, where a cover
   linking to itself would be a dead end. */
.bb__coverlink { display: block; }
.bb__coverlink:focus-visible { outline: 2px solid var(--paper); outline-offset: 6px; }

.bb__cover img {
  width: 100%;
  display: block;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.38);
}

.bb__epigraph {
  margin: 0;
  padding-left: clamp(16px, 2.4vw, 26px);
  border-left: 1px solid var(--rule-dark);
}
.bb__epigraph p {
  margin: 0;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(17px, 1.9vw, 22px);
  line-height: 1.5;
  color: var(--paper-80);
  text-wrap: pretty;
}
/* The description paragraph is a quotation too — by Kathryn Nuernberger, who
   judged the award. It takes the same attribution style as the epigraph it
   sits directly beneath. */
.bb__descq { margin: 0; }
.bb__epigraph cite,
.bb__descq cite {
  display: block;
  margin-top: 14px;
  font-family: var(--sans);
  font-style: normal;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper-50);
}

.bb__desc {
  margin: 34px 0 0;
  max-width: 58ch;
  font-size: 13px;
  line-height: 1.76;
  color: var(--paper-70);
  text-wrap: pretty;
}

/* accolades */
.bb-acc {
  list-style: none;
  margin: 40px 0 0;
  padding: 0;
  border-top: 1px solid var(--rule-dark);
}
.bb-acc li { border-bottom: 1px solid var(--rule-dark); }
/* .bb-acc__static carries an accolade that has no working URL yet, so the row
   still lays out identically without being a link. */
.bb-acc a,
.bb-acc .bb-acc__static {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 20px;
  align-items: baseline;
  padding: 13px 0;
  transition: opacity 200ms ease;
}
.bb-acc a:hover { opacity: 0.72; }
.bb-acc__what {
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1.3;
  color: var(--paper);
}
.bb-acc__who {
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paper-50);
  text-align: right;
}

.bb__links {
  display: flex;
  gap: 26px;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 38px;
}

/* praise */
.bb__praise { margin-top: clamp(64px, 8vw, 110px); }
.bb-quotes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: clamp(30px, 4vw, 56px);
  margin-top: 34px;
}
.bb-quote { margin: 0; }
.bb-quote p {
  margin: 0;
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.6;
  color: var(--paper-80);
  text-wrap: pretty;
}
.bb-quote cite {
  display: block;
  margin-top: 13px;
  font-family: var(--sans);
  font-style: normal;
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-50);
}
.bb-quote cite a {
  border-bottom: 1px solid transparent;
  transition: color 180ms ease, border-color 180ms ease;
}
.bb-quote cite a:hover { color: var(--paper); border-bottom-color: var(--rule-dark-40); }

/* interviews */
.bb__talk {
  margin-top: clamp(56px, 7vw, 92px);
  padding-top: 30px;
  border-top: 1px solid var(--rule-dark);
}
/* No Spare People runs on bone: it sits between the cream pull-quote and the
   dark #poems and has to separate from both. --cream-2 was too close to the
   pull-quote's ground to show a seam, so this is a step deeper. Ink text, not
   paper — every tone used here clears AA (the dimmest, --ink-label, is 5.3).
   Only colour is overridden; all layout comes from the shared .bb rules. */
.bb--bone { background: var(--bone); }
.bb--bone .bb__title           { color: var(--ink); }
.bb--bone .bb__imprint         { color: var(--ink-label); }
.bb--bone .bb__desc            { color: var(--ink-soft); }
.bb--bone .bb-acc              { border-top-color: var(--rule); }
.bb--bone .bb-acc li           { border-bottom-color: var(--rule); }
.bb--bone .bb-acc__what        { color: var(--ink); }
.bb--bone .bb-acc__who         { color: var(--ink-label); }
.bb--bone .bb-quote p          { color: var(--ink-soft); }
.bb--bone .bb-quote cite       { color: var(--ink-label); }
/* .ulink--quiet is paper-on-dark by definition — it renders near-invisible on
   a light ground, so it needs its own values here rather than the shared ones. */
.bb--bone .ulink--quiet        { color: var(--ink-label); border-bottom-color: transparent; }
.bb--bone .ulink--quiet:hover  { color: var(--ink); border-bottom-color: var(--rule-strong); }
.bb--bone .bb__talk            { border-top-color: var(--rule); }
.bb--bone .bb__talklist        { color: var(--ink-label); }
.bb--bone .bb__talklist a      { color: var(--ink-soft); }
.bb--bone .bb__talklist a:hover { color: var(--ink); border-bottom-color: var(--rule-strong); }

.bb__talklist {
  margin: 16px 0 0;
  font-size: 12.5px;
  line-height: 2.1;
  color: var(--paper-50);
}
.bb__talklist a {
  color: var(--paper-70);
  border-bottom: 1px solid transparent;
  transition: color 180ms ease, border-color 180ms ease;
}
.bb__talklist a:hover { color: var(--paper); border-bottom-color: var(--rule-dark-40); }

/* No backdrop below the site's mobile breakpoint. The section there is sized
   by its copy rather than by the ratio above — at 375px it is ~2150px tall
   against 375 wide — so `cover` scaled the asset by HEIGHT and showed a 31%
   vertical slice of it, mostly empty sky with the flames crushed into the last
   quarter. Neither a crop nor a `contain` plate read well at that size, so the
   picture is simply dropped and the section falls back to flat colour.
   --charcoal, not the --black the base rule sets: that black exists only so
   the print's burnt edge meets the section edge with no grey seam. With no
   print there is nothing to meet, and charcoal is what the other two dark
   sections (Poems, Sawmill) use.
   display:none rather than opacity:0 — it also stops phones downloading the
   161KB asset they would never show.
   Placed after the base rule rather than given extra weight: a media query
   adds no specificity, so order is what carries it. */
@media (max-width: 720px) {
  #barnburner { background: var(--charcoal); }
  #barnburner::before { display: none; }
}

@media (max-width: 860px) {
  .bb__main { grid-template-columns: 1fr; }
  .bb__cover { max-width: 260px; margin: 0 auto; }
  /* One column from here down, so drop the flip — the cover should lead the
     stack on a phone rather than arrive after the description.
     The columns have to be re-declared at the same id specificity: a media
     query adds none, so `#barnburner .bb__main` above (0,1,1,0) outranks the
     plain `.bb__main` rule in this block and Barnburner would otherwise stay
     two-column on a phone while No Spare People collapsed. */
  #barnburner .bb__main { grid-template-columns: 1fr; }
  #barnburner .bb__cover,
  #barnburner .bb__body { order: 0; }
}
@media (max-width: 560px) {
  .bb-acc a,
  .bb-acc .bb-acc__static { grid-template-columns: 1fr; gap: 4px; }
  .bb-acc__who { text-align: left; }
}
