/* ==========================================================================
   Fonts
   ========================================================================== */
/* anotherSerif is licensed as a hosted webfont and served by the foundry
   (Not Your Type), not from assets/fonts/. Their delivery URL is keyed to the
   Synonym account; it is the whole of the licence, so it is not mirrored or
   re-hosted locally. Note the family name it declares carries the style:
   "anotherSerif notRegular", which is why --font-accent below reads that way.

   An @import rather than a <link> in the head: this section is lifted verbatim
   into the Shopify chrome by bin/build-shopify-chrome.sh, so declaring the font
   here is the only way both surfaces get it from one edit. It costs a serial
   round-trip, which is affordable — the face sets one word, and font-display:
   swap means it never blocks render.

   Must stay the first rule in the file; @import is invalid after any other. */
@import url("https://www.notyourtype.nl/68c03757536f6/p/sotGd1ZCSh.css");

@font-face {
  font-family: "Matter Mono";
  src:
    url("../assets/fonts/MatterMonoRegular.woff2") format("woff2"),
    url("../assets/fonts/MatterMonoRegular.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Matter";
  src:
    url("../assets/fonts/MatterRegular.woff2") format("woff2"),
    url("../assets/fonts/MatterRegular.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* The drawn italic, declared into the same "Matter" family as the roman above
   so <em> picks it up on its own — no class, no template change. Without this
   the browser synthesises italics by slanting the roman, which it will do
   silently: the faux and the real face have identical widths, so nothing looks
   broken, it just isn't the typeface. */
@font-face {
  font-family: "Matter";
  src:
    url("../assets/fonts/MatterRegularItalic.woff2") format("woff2"),
    url("../assets/fonts/MatterRegularItalic.woff") format("woff");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "HAL Timezone";
  src:
    url("../assets/fonts/HALTimezone-Regular.woff2") format("woff2"),
    url("../assets/fonts/HALTimezone-Regular.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Matter Heavy";
  src:
    url("../assets/fonts/Matter-Heavy.woff2") format("woff2"),
    url("../assets/fonts/Matter-Heavy.woff") format("woff");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* The drawn heavy italic, into the same family as the roman above — same
   arrangement as the Matter regular italic, and for the same reason: without
   it an <em> inside a heavy title gets a mechanical slant that looks close
   enough to pass unnoticed. */
@font-face {
  font-family: "Matter Heavy";
  src:
    url("../assets/fonts/Matter-HeavyItalic.woff2") format("woff2"),
    url("../assets/fonts/Matter-HeavyItalic.woff") format("woff");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

/* ==========================================================================
   Design tokens
   ========================================================================== */
:root {
  --color-bg: #f4f3f3;
  --color-text: #1d1c1b;
  /* Per-story theming — both overridden per post via inline style on
     <body>, and both will come from post meta once this is a real
     WordPress template.

     --story-color is the accent: the hero wash, the pullquote, the card tag
     and the end mark. Deliberately *not* captions — accent-on-background is
     around 2.7:1 at the default orange, well under the 4.5:1 AA needs, and
     captions carry information rather than decorate. They take --color-text.
     --story-gradient is the hero wash. It's optional — left unset, the wash
     falls back to a simple two-stop ramp from the accent down to the page
     background (see .story-hero__gradient, which is where that fallback has
     to live: a default declared up here would substitute *this* element's
     --story-color, i.e. the one below, and so ignore whatever the post set
     on <body>). It's a whole gradient value rather than just a color so a
     story can supply as many stops as its art direction needs (see
     story-soup.html's three-stop version) without the stylesheet having to
     know how many to expect. */
  --story-color: #e37826;

  /* The brand accent. Unlike --story-color this doesn't change per post — it's the
     one accent the site uses for a primary action, and the wash behind the
     print panel on the subscribe page. */
  --color-accent: #6ae3de;

  /* The accent as a wash rather than a fill: the brand teal with a pink either
     side of it and a coral between, read left to right across the panel. It
     ends on the colour it starts on so that the band has no seam — the two ends
     meet at the same pink wherever the panel is cut off.

     Only ever seen through a 60px blur, which is what turns four stops into one
     drift of colour rather than three visible joins. Anything sharp-edged wants
     --color-accent instead. */
  --accent-wash: linear-gradient(90deg, #e9a2c7, #6ce2db, #faa993, #e9a2c7);

  /* The floor for text dialled back with opacity. Opacity blends the ink toward
     the page background, so quiet type fails contrast long before it looks
     faint: --color-text on --color-bg is 15.9:1 at full strength, 3.2:1 at 0.5,
     and 4.33:1 at 0.6 — still under the 4.5:1 AA asks for. 0.7 clears it at
     6:1. Reach for this rather than picking a number per rule. */
  --quiet: 0.7;

  --font-mono: "Matter Mono", "IBM Plex Mono", "Courier New", monospace;
  --font-serif: "HAL Timezone", serif;
  --font-display: "Matter", serif;
  --font-display-heavy: "Matter Heavy", serif;
  --font-accent: "anotherSerif notRegular", serif;

  --nav-font-size: 16px;
  --nav-line-height: 24px;
  --nav-tracking: 0.05em; /* ~0.8px at 16px */

  /* 8px spacing scale — use these instead of raw px everywhere */
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 32px;
  --space-5: 40px;
  --space-6: 48px;
  --space-7: 56px;
  --space-8: 64px;
  --space-9: 72px;
  --space-10: 80px;
  --space-11: 88px;
  --space-12: 96px;
  --space-14: 112px;
  --space-15: 120px;
  --space-18: 144px;

  /* 12-col grid, spec'd against the 1440px artboard */
  --grid-columns: 12;
  --grid-gutter: var(--space-2);  /* 16px, fixed across breakpoints */
  --grid-margin: var(--space-5);  /* 40px at desktop, scales down below */
  --container-max: 1440px;
  --footer-max: 984px;

  /* How tall a back-catalogue cover stands. A token because the current issue
     borrows it on phones — see .current-issue__stage in the Responsive block,
     where the flipping cover drops to exactly the size of the cards below it. */
  --issue-cover-height: 304px;

  /* The wordmark is a fluid-width SVG: its height is the width of the box it sits
     in times the artwork's own 144/1346 ratio, so it changes with every viewport.
     Hoisted here because three things need it — the menu's close cross, the nav's
     mobile margin, and --chrome-block below.

     The `min()` is the whole of what makes this true above 1440: .site-header
     carries .container, so the wordmark stops widening at --container-max while
     the window keeps going. Measured off 100vw alone this over-reported by 60px
     on a 2000px-wide screen, and --chrome-block with it — which put the About
     headline 60px below where every other page starts its heading, on exactly
     the wide monitors where the two are easiest to compare. */
  --wordmark-height: calc((min(100vw, var(--container-max)) - var(--grid-margin) * 2) * 0.10698);

  /* How far down the page the chrome finishes: the height .site-header occupies
     plus the nav's bottom margin. Nothing on an ordinary page uses it — there the
     header is in normal flow and pushes the content down by exactly this much on
     its own. It exists for the pages whose header is taken out of flow to sit over
     a hero (.overlay-hero), where a heading that wants to start where every other
     page's heading starts has to be told the number.

     Kept as the sum of the parts rather than a measured constant so it stays true
     when any of them changes:
       .site-header padding-top + wordmark + .primary-nav margin-top (negative,
       creeping up into the wordmark) + .primary-nav min-height + its margin-bottom. */
  --chrome-block: calc(
    var(--space-2) + var(--wordmark-height) - var(--space-3) + var(--space-14) + var(--space-6)
  );
}

/* Margin steps down in 8px increments as the viewport narrows.
   Column count and gutter stay fixed at 12 / 16px throughout —
   adjust here if you want fewer columns on smaller screens. */
@media (max-width: 1023px) {
  :root { --grid-margin: var(--space-4); } /* 32px */
}
@media (max-width: 767px) {
  :root { --grid-margin: var(--space-3); } /* 24px */
}
@media (max-width: 479px) {
  :root { --grid-margin: var(--space-2); } /* 16px */
}

/* ==========================================================================
   Reset (minimal)
   ========================================================================== */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-mono);
  -webkit-font-smoothing: antialiased;
}

img {
  display: block;
  max-width: 100%;
}

a {
  color: inherit;
  text-decoration: none;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 0;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* The first thing in the tab order on every page: out of sight until it takes
   focus, then a real target sitting over the chrome. Its own class rather than
   a :focus rule on .visually-hidden above, because most of what wears that
   class — form labels, the loading status, the "opens in a new tab" notes —
   is meant to stay hidden unconditionally.

   z-index clears the pinned nav (1000) and the mobile menu panel (1100). */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1200;
  transform: translateY(-120%);
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--color-text);
}

.skip-link:focus {
  transform: translateY(0);
}

/* <main> carries tabindex="-1" so Safari actually moves focus there when the
   skip link is followed rather than only scrolling. It can never be focused by
   any other route, so the ring it would otherwise draw is noise. */
main:focus {
  outline: none;
}

/* ==========================================================================
   Grid system
   12 columns / 16px gutter / margin scales in 8px steps (see tokens above)
   ========================================================================== */
.container {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--grid-margin);
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
}

/* Any listing that runs to more than one row — the issue archive behind
   "Magazine", section and search results (index.php). The bare .grid has no
   row-gap, which is right for the single-row grids on the homepage but leaves
   a second row of cards sitting directly on the first one here.

   120px matches .issues-row, so a page of back issues keeps the same rhythm as
   the row of them on the homepage. Story cards carry their own 48px bottom
   margin for that homepage grid; it's cleared here so both card types answer to
   the row-gap alone and a mixed listing doesn't step unevenly. */
.archive-grid {
  row-gap: var(--space-15);
}

.archive-grid .story-card {
  margin-bottom: 0;
}

/* The magazine page: three covers to a row rather than the homepage row's four,
   each row centred under the one above it, and the cards and the gutter between
   them left exactly as they are everywhere else. Only the count per row changes.

   Only this grid. The class is on the homepage's back-catalogue row too, where
   four across is right — .archive-grid is what tells the two apart.

   Flex rather than the twelve-column grid the page otherwise uses, because only
   a flex line centres what it holds. A grid puts a last row of two in the first
   two tracks, and no whole number of columns will move them under the middle of
   the three above: that offset is half a card plus half a gutter, which is a
   length and not a column.

   The cards keep the width a three-column card has, so the space around each
   cover is what it was. The row is capped at exactly three of them, which is
   what holds it to three on a wide window — four of these fit the full content
   width precisely, so without the cap flex would take four and leave one. */
@media (min-width: 768px) {
  .archive-grid.issues-grid {
    --issues-card: calc(
      (min(100vw, var(--container-max)) - 2 * var(--grid-margin) - 11 * var(--grid-gutter)) / 12 * 3
      + 2 * var(--grid-gutter)
    );

    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    column-gap: var(--grid-gutter);
    /* Border-box, so .container's own padding is added back here rather than
       eaten out of the three cards this is sized to hold. */
    max-width: calc(3 * var(--issues-card) + 2 * var(--grid-gutter) + 2 * var(--grid-margin));
  }

  .archive-grid.issues-grid .issue-card {
    flex: 0 0 var(--issues-card);
  }
}

.col-span-1  { grid-column: span 1; }
.col-span-2  { grid-column: span 2; }
.col-span-3  { grid-column: span 3; }
.col-span-4  { grid-column: span 4; }
.col-span-5  { grid-column: span 5; }
.col-span-6  { grid-column: span 6; }
.col-span-7  { grid-column: span 7; }
.col-span-8  { grid-column: span 8; }
.col-span-9  { grid-column: span 9; }
.col-span-10 { grid-column: span 10; }
.col-span-11 { grid-column: span 11; }
.col-span-12 { grid-column: span 12; }

@media (max-width: 767px) {
  /* Default to full-width stacking on mobile unless overridden */
  [class*="col-span-"] { grid-column: span var(--grid-columns); }
}

/* ==========================================================================
   Shared nav link styling
   ========================================================================== */
.nav-list a,
.footer-links a,
.footer-meta li,
.subscribe-form button,
.back-to-top,
.footer-copyright {
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  white-space: nowrap;
}

.footer-copyright {
  margin: 0;
}

/* Underline grows left-to-right on hover/focus. */
.nav-list a,
.footer-links a,
.footer-meta a,
.story-byline__name a,
.story-subject__credit-name a,
.contributor-intro__links a,
.subscribe-form button {
  position: relative;
  text-decoration: none;
  color: inherit;
}

.nav-list a::after,
.footer-links a::after,
.footer-meta a::after,
.story-byline__name a::after,
.story-subject__credit-name a::after,
.contributor-intro__links a::after,
.subscribe-form button::after,
/* Deliberately only in the two ::after groups and not in the one above: that
   rule sets position: relative, and the hero tag is placed absolutely against
   the headline. It's already a containing block for its own ::after, and it
   sets its own text-decoration and colour. */
a.story-hero__tag::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -4px;
  width: 0;
  height: 1px;
  background: currentColor;
  transition: width 0.3s ease;
}

.nav-list a:hover::after,
.nav-list a:focus-visible::after,
.footer-links a:hover::after,
.footer-links a:focus-visible::after,
.footer-meta a:hover::after,
.footer-meta a:focus-visible::after,
.story-byline__name a:hover::after,
.story-byline__name a:focus-visible::after,
.story-subject__credit-name a:hover::after,
.story-subject__credit-name a:focus-visible::after,
.contributor-intro__links a:hover::after,
.contributor-intro__links a:focus-visible::after,
.subscribe-form button:hover::after,
.subscribe-form button:focus-visible::after,
a.story-hero__tag:hover::after,
a.story-hero__tag:focus-visible::after {
  width: 100%;
}

/* ==========================================================================
   Header
   ========================================================================== */
.site-header {
  padding-top: var(--space-2);
}

.wordmark {
  position: relative;
  /* Left over from a blur that used to sit behind the nav in flow. Harmless,
     and still the right answer: the wordmark is only ever on screen while the
     header is in flow, and .nav-veil is faded out in every one of those
     states. */
  z-index: 1;
  will-change: transform;
}

.wordmark img,
.wordmark-svg {
  width: 100%;
  height: auto;
  display: block;
}

/* Each letter's dot-matrix is split into individual <path> elements at
   runtime (see main.js) so nearby dots can visually fuse with the cursor
   trail. Everything stays ink-colored — the liveliness comes from the
   goo filter, not a color change. */
.wordmark-dot {
  fill: #1D1C1B;
}

/* A little secret: a chain of invisible circles (added in JS) trails the
   cursor near the logo. Sharing this blur+contrast filter with the dots
   is the classic metaball/goo technique — blur merges nearby shapes,
   then the alpha contrast sharpens the fused silhouette back up. See
   initMetaballTrail() in main.js. */
.wordmark-goo-group {
  filter: url(#wordmark-goo);
}

.wordmark-blob {
  fill: #1D1C1B;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .wordmark-goo-group {
    filter: none;
  }
}

.nav-spacer {
  height: 0;
}

.primary-nav {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--space-14);
  margin-top: calc(var(--space-3) * -1); /* creep -24px into the wordmark graphic above */
  margin-bottom: var(--space-6);
  flex-wrap: wrap;
  will-change: transform;
}

/* Pinned state: taken out of flow and fixed to the viewport once the icon
   would otherwise scroll to -80px. Defaults to the "peek" position — only
   the bottom of the menu icon (which has its own drawn border) shows above
   the fold; the nav links sit higher up in the row so they stay off-screen
   until the bar is fully revealed. No background — page content stays
   visible scrolling behind it in both states. */
.primary-nav.is-pinned {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--grid-margin);
  margin-top: 0;
  margin-bottom: 0;
  z-index: 1000;
  pointer-events: none;
  transform: translateY(-80px);
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

.primary-nav.is-pinned .menu-toggle {
  pointer-events: auto;
}

.primary-nav.is-pinned.is-revealed {
  pointer-events: auto;
  transform: translateY(24px);
}

/* While closing back to the natural header (the last stretch of scroll
   back to the very top), the transform is driven directly by scroll
   position in JS rather than eased on a timer — this is what makes it
   read as the wordmark "pushing" the bar down rather than the bar
   independently animating on its own clock. */
.primary-nav.is-pinned.is-closing {
  transition: none;
}

/* ---- The band under the pinned bar (.nav-veil, printed after <nav> in
   template-parts/nav.php) -------------------------------------------------

   The Stories filter's veil, mirrored to the top of the window: the page blurred
   and washed into the background colour, strongest at the very top and gone
   before the band ends, with no edge anywhere. Same reasoning as down there —
   the bar is small type and a drawn icon over whatever photograph happens to be
   scrolling underneath, and it needs ground under it only once it's floating
   over the page rather than sitting in the header where it belongs.

   So: only while pinned *and* revealed. Landing on a page shows nothing; the
   band arrives with the bar when the reader scrolls back up for it and leaves
   with it. While peeking there's nothing to protect but the bottom edge of the
   icon, which carries its own drawn border.

   This is not the old .nav-blur. That was a permanent stack of masked
   backdrop-filter layers that dimmed the picture at every scroll position, and
   it is not coming back — nor is the About page's trick of measuring the
   photograph under the bar and flipping to the light ink over its dark parts.
   The chrome is one ink everywhere, and now it gets a ground under it only in
   the one state where it is genuinely adrift. (The `.nav-scrolled` body class
   went with .nav-blur and is still gone; the reveal classes say all of this.) */
.nav-veil {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Deep enough that the fade is finished well below whatever the bar is
     holding. --nav-veil-reach is the bottom of an open dropdown, set by
     initNavDropdowns() when one opens and dropped when it closes; the fallback
     is the revealed bar's own 180px, which with the tail below makes the 300px
     band that matches the filter dock's, so the two edges of the window wash out
     over the same distance.

     It has to follow the panel because the panel has no fill of its own any
     more (see .nav-list .sub-menu above) — this band is its ground, and the
     menu is editable, so a fourth or fifth child would otherwise hang off the
     bottom of the wash onto the bare photograph. The tail stays a constant
     120px whatever the menu's length, so the fade always finishes the same
     distance past the last row. */
  height: calc(var(--nav-veil-reach, 180px) + var(--space-15));
  z-index: 999; /* under the bar itself (1000), over the filter dock (900) */
  pointer-events: none;
  opacity: 0;
  /* Height as well as opacity: opening a dropdown while the bar is already
     revealed deepens the band under it, and that should read as the wash making
     room rather than jumping. */
  transition: opacity 0.3s ease, height 0.3s ease;
  /* The dock's eased stops, flipped to run downward. A straight ramp between
     two stops has a visible shoulder where it leaves full strength and another
     where it reaches zero; these approximate an ease-out, so it holds near-solid
     behind the bar and then gives way slowly, with nothing in it that reads as a
     line — and enough stops that it doesn't band on a wide screen. */
  background: linear-gradient(
    to bottom,
    var(--color-bg) 0%,
    color-mix(in srgb, var(--color-bg) 97%, transparent) 16%,
    color-mix(in srgb, var(--color-bg) 88%, transparent) 30%,
    color-mix(in srgb, var(--color-bg) 74%, transparent) 43%,
    color-mix(in srgb, var(--color-bg) 55%, transparent) 56%,
    color-mix(in srgb, var(--color-bg) 36%, transparent) 69%,
    color-mix(in srgb, var(--color-bg) 19%, transparent) 81%,
    color-mix(in srgb, var(--color-bg) 7%, transparent) 91%,
    transparent 100%
  );
}

/* Driven off the bar's own state, which is why the veil has to be a *later*
   sibling of it — there is no selector for "the element before this one". */
.primary-nav.is-pinned.is-revealed ~ .nav-veil {
  opacity: 1;
}

/* Except on the way home. Inside the closing zone the bar is being pushed back
   down into the header by the wordmark scrolling up behind it, and it is on its
   way to being in flow again — so the ground goes out from under it as it lands
   rather than snapping away at scrollY 0. */
.primary-nav.is-pinned.is-closing ~ .nav-veil {
  opacity: 0;
}

@supports (backdrop-filter: blur(16px)) {
  .nav-veil {
    backdrop-filter: blur(16px);
    /* The same eased curve as the tint above it, so the blur and the colour let
       go of the page together instead of one outliving the other. */
    -webkit-mask-image: linear-gradient(
      to bottom,
      #000 0%,
      rgba(0, 0, 0, 0.98) 16%,
      rgba(0, 0, 0, 0.9) 30%,
      rgba(0, 0, 0, 0.76) 43%,
      rgba(0, 0, 0, 0.57) 56%,
      rgba(0, 0, 0, 0.38) 69%,
      rgba(0, 0, 0, 0.2) 81%,
      rgba(0, 0, 0, 0.07) 91%,
      transparent 100%
    );
    mask-image: linear-gradient(
      to bottom,
      #000 0%,
      rgba(0, 0, 0, 0.98) 16%,
      rgba(0, 0, 0, 0.9) 30%,
      rgba(0, 0, 0, 0.76) 43%,
      rgba(0, 0, 0, 0.57) 56%,
      rgba(0, 0, 0, 0.38) 69%,
      rgba(0, 0, 0, 0.2) 81%,
      rgba(0, 0, 0, 0.07) 91%,
      transparent 100%
    );
  }
}

/* Below 900px the nav links are gone and the bar is just the menu icon, so
   there is less to cover and a 300px wash over a phone screen is most of the
   picture. */
@media (max-width: 900px) {
  /* Flat, and no --nav-veil-reach in it: at this width the bar's links aren't
     rendered and there is no dropdown to make room for. */
  .nav-veil {
    height: 220px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav-veil {
    transition-duration: 0.01s;
  }
}

.menu-toggle {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: var(--space-11); /* 88px */
  height: var(--space-14); /* 112px */
  display: flex;
  align-items: center;
  justify-content: center;
}

.menu-toggle img,
.menu-toggle .menu-svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* The two dots are swapped for real circles at runtime (see
   initMenuLoader() in main.js) so they can run into each other while the
   homepage slideshow preloads. Same goo filter as the wordmark trail:
   blur fuses the shapes as they close in, then the alpha contrast sharpens
   the merged silhouette back up. Switched off while they sit at rest so
   the idle icon is pixel-for-pixel the shape the SVG ships with. */
.menu-goo-group {
  filter: url(#menu-goo);
}

.menu-goo-group.is-idle {
  filter: none;
}

.menu-dot {
  fill: #1d1c1b;
}

.nav-list {
  display: flex;
  flex: 1 1 0;
  justify-content: center;
  gap: var(--space-10);
  flex-wrap: wrap;
}

/* ---- Dropdowns -----------------------------------------------------------
   A nav item with children below it in Appearance → Menus. Click-to-open
   rather than hover: the bar spends most of its life pinned and peeking at the
   top of a story, and a menu that opens because the pointer passed over it on
   the way somewhere else is a menu that opens by accident.

   Everything the panel is made of — the caret, the panel, the options in it —
   is the Stories filter's issue dropdown, shared by selector rather than copied
   (see .story-filter__chevron, .story-filter__menu, .story-filter__option).
   Only what is particular to sitting in the nav is set here.
   -------------------------------------------------------------------------- */
.nav-list .menu-item-has-children {
  position: relative;
}

/* A pseudo-element rather than a span, so the caret needs a box before the
   shared rule can draw it. On the <li> and not on the link's ::after, which is
   already the underline that wipes in on hover. */
.nav-list .menu-item-has-children::after {
  content: "";
  display: inline-block;
  margin-left: var(--space-1);
}

/* Closed until asked. visibility is what keeps it out of the tab order;
   opacity is only there to give the open state something to fade from. */
.nav-list .sub-menu {
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.2s ease, visibility 0.2s;
}

/* Over the filter, not under it. Both panels are the shared rule's z-index: 3,
   and on the Stories page they can overlap — equal values would hand it to the
   one later in the document, which is the filter. Qualified by .primary-nav so
   it outweighs the shared rule wherever that ends up in the file. */
.primary-nav .nav-list .sub-menu {
  z-index: 10;
}

.nav-list .menu-item-has-children.is-open > .sub-menu {
  visibility: visible;
  opacity: 1;
}

/* The no-JS way in: tabbing to the parent opens the panel, so the children are
   reachable without a script.

   Only without a script, though. Clicking the parent leaves focus sitting on
   it, so with the toggle running as well this would hold the panel open against
   the very click that just closed it — the second click on Community would
   appear to do nothing. initNavDropdowns() adds .is-scripted once it has taken
   the click, which switches this off. */
.nav-list:not(.is-scripted) .menu-item-has-children:focus-within > .sub-menu {
  visibility: visible;
  opacity: 1;
}

/* The options carry their own stroke on hover; the nav's underline would be a
   second answer to the same pointer. */
.nav-list .sub-menu a::after {
  content: none;
}

/* ==========================================================================
   Mobile menu — template-parts/nav.php, opened by the menu icon below 900px.

   Plain opaque background rather than a blurred veil: the panel is the page
   while it's open, and the pages it opens over are photographic, so anything
   translucent leaves the destinations sitting on someone's dinner.

   [hidden] does the hiding, so it stays out of the tab order and off the
   accessibility tree when closed without a display:none of our own to fight
   the fade — see initSiteMenu(), which drops [hidden] first and adds .is-open
   on the next frame so the transition has a start state to run from.
   ========================================================================== */
.site-menu {
  position: fixed;
  inset: 0;
  z-index: 1100;
  background: var(--color-bg);
  overflow-y: auto;
  overscroll-behavior: contain;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.site-menu[hidden] {
  display: none;
}

.site-menu.is-open {
  opacity: 1;
}

.site-menu__inner {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-6);
  padding-block: var(--space-14) var(--space-10);
}

/* A cross standing exactly where the menu icon was — same centre line, same
   place on the page — so the panel closes where it opened. The offset repeats
   the header's own arithmetic (its mobile top padding plus half the wordmark;
   see the Responsive block) because that's what the icon is centred on, and
   the wordmark's height is a ratio of the viewport rather than a fixed number.
   The hit area is the icon's own 88px, not the 24px the cross is drawn at. */
.site-menu__close {
  position: absolute;
  top: calc(var(--space-7) + var(--wordmark-height) / 2);
  left: 50%;
  width: var(--space-11);
  height: var(--space-11);
  transform: translate(-50%, -50%);
}

/* No grey flash under the finger on iOS: the cross is two hairlines on an
   otherwise empty box, and the highlight lands as a square behind it. */
.site-menu__close {
  -webkit-tap-highlight-color: transparent;
}

/* The cross takes focus the moment the panel opens, because the panel is modal.
   Opened by a tap or a click there's nothing to tell the reader — they know
   where they just pressed — so the ring is suppressed until the first Tab, at
   which point initSiteMenu() drops the class and it comes back. */
.site-menu.is-pointer-open .site-menu__close:focus {
  outline: none;
}

.site-menu__close::before,
.site-menu__close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 1.5px;
  background: var(--color-text);
}

.site-menu__close::before {
  transform: translate(-50%, -50%) rotate(45deg);
}

.site-menu__close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.site-menu__list {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
  text-align: center;
}

.site-menu__list a {
  font-family: var(--font-display);
  font-size: clamp(2rem, 9vw, 3rem);
  line-height: 1.1;
}

/* No dropdowns in the panel — there is room to simply print the children, so
   they sit under their parent as a smaller list, open. The caret isn't a
   promise of something hidden here, it's the mark that says the two small
   words below belong to the big one above; it turns over and folds them away
   for a reader who would rather see the top level on its own.

   The caret the <li> carries on desktop stays off, because in here the mark is
   a control rather than a decoration and has to be something you can press —
   initSiteMenuSubmenus() puts a real button in beside the link. */
.site-menu__list .menu-item-has-children::after {
  display: none;
}

/* The parent of a submenu isn't a link in the panel — Community is the name of
   the pair under it rather than a destination of its own, so in here it is
   simply the control that folds them away, and initSiteMenuSubmenus() prints it
   as one. Set in the list's own display face at the list's own size: it is a
   line in the menu and has to read as one, whatever element it is made of.

   The whole word is the target, which is a far better one than the 7px mark
   beside it ever was. */
.site-menu__sub-toggle {
  position: relative;
  display: block;
  padding: 0;
  border: 0;
  background: none;
  -webkit-appearance: none;
  appearance: none;
  color: inherit;
  cursor: pointer;
  font-family: var(--font-display);
  font-size: clamp(2rem, 9vw, 3rem);
  line-height: 1.1;
  /* Same reason as the close cross: a box of plain type takes the iOS tap
     highlight as a grey slab behind the word. */
  -webkit-tap-highlight-color: transparent;
}

/* Hung off the word's right edge rather than set beside it in a row. What is
   centred on the page is the word — every other line in the panel is a word
   and they all share one centre — and a caret taking part in the centring
   pushes Community off that line by half a caret, which is exactly as visible
   as it sounds.

   Centred against the word with auto block margins rather than the usual
   top: 50% and a translate, because the shared caret rule opposite needs
   `transform` for the rotation and can only have one. */
.site-menu__sub-toggle::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 0;
  bottom: 0;
  margin: auto 0 auto var(--space-2);
}

/* Open is the resting state and the state with no script, so it's the fold
   that's written rather than the reveal — the children are printed and stay
   printed unless someone asks for them to go. */
.site-menu__list .menu-item-has-children.is-collapsed > .sub-menu {
  display: none;
}

.site-menu__list .sub-menu {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}

.site-menu__list .sub-menu a {
  font-size: clamp(1.25rem, 5vw, 1.75rem);
  opacity: var(--quiet);
}

/* The membership pair and the shop are actions rather than places, so they
   drop back to the same mono caps they wear in the bar on desktop. */
.site-menu__list--account {
  gap: var(--space-2);
}

.site-menu__list--account a {
  font-family: inherit;
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

/* Locks the page behind the panel — without it a scroll that runs past the
   menu's own end carries on scrolling the story underneath. */
body.menu-open {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .site-menu {
    transition: none;
  }
}

/* ==========================================================================
   Main content
   ========================================================================== */
.site-content {
  padding-block: var(--space-10);
  min-height: 40vh;
}

/* Temporary — just gives enough scroll distance to test the header's
   pin/peek/reveal behavior. Remove once real page content lands. */
.site-content--scroll-test {
  min-height: 220vh;
}

/* ==========================================================================
   Stories index — home.php, template-parts/story-filter.php

   Two rows of pills: the issue a story ran in (Digital Only among them), then the
   section. Each pill is a link, so the state is in the URL and the back button
   walks back through it. The second row is built from what the first leaves.
   ========================================================================== */
/* Both levels centred on the page, with no labels — the closed dropdown says
   which issue, and "All Stories" says what the row below it is. The filter is
   the top of the page, so it carries the space a headline would have.

   One line: the issue dropdown and the section pills read as a single sentence
   of controls rather than as two stacked rows, and the row wraps on its own once
   there are more sections than fit. The wider column gap is what still separates
   the two levels inside the one line. */
.story-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-1) var(--space-2);
  padding-block: var(--space-10) var(--space-8);
}

.story-filter__level {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
}

.story-filter__options {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ---- Issue: a dropdown -------------------------------------------------
   A <details>, so it opens, closes and takes the keyboard without any script.
   The menu is absolutely positioned so opening it doesn't shove the section
   row down the page. */
.story-filter__dropdown {
  position: relative;
}

/* Filled in the accent — the same one behind the print panel on the
   subscribe page. It's the only control on the page that sets the whole view,
   and the fill is what separates it from the section toggles below it. */
.story-filter__summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 6px 14px;
  border: 1px solid var(--color-accent);
  border-radius: 99px;
  background: var(--color-accent);
  color: var(--color-text);
  font-family: var(--font-mono);
  font-size: 14px;
  /* 24px, matching the count circle, so this control stands exactly as tall as
     the section pills below it — see .story-filter__pill-label. */
  line-height: 24px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  cursor: pointer;
  list-style: none; /* Firefox */
  transition: opacity 0.3s ease;
}

/* The default disclosure triangle, removed in WebKit. */
.story-filter__summary::-webkit-details-marker {
  display: none;
}

/* The same hover as the Subscribe page's CTA pill — it's the same kind of
   control, so it should answer the same way. */
.story-filter__summary:hover,
.story-filter__summary:focus-visible,
.story-filter__dropdown[open] .story-filter__summary {
  opacity: 0.7;
}

/* Shared with the primary nav's dropdown, so the two carets are the same mark
   drawn by the same rule rather than two that happen to look alike. */
.story-filter__chevron,
.nav-list .menu-item-has-children::after,
.site-menu__sub-toggle::after {
  width: 7px;
  height: 7px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  /* Rotated on its own centre, then lifted: a square turned 45° hangs low,
     because its visual weight is the corner rather than the box. */
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.3s ease;
}

.story-filter__dropdown[open] .story-filter__chevron,
.nav-list .menu-item-has-children.is-open::after,
.site-menu__sub-toggle[aria-expanded="true"]::after {
  transform: translateY(1px) rotate(-135deg);
}

/* Centred under the summary rather than left-aligned to it, now that the
   control itself sits in the middle of the page. */
.story-filter__menu,
.nav-list .sub-menu {
  position: absolute;
  z-index: 3;
  top: calc(100% + var(--space-1));
  left: 50%;
  transform: translateX(-50%);
  min-width: 100%;
  margin: 0;
  padding: var(--space-1);
  list-style: none;
  white-space: nowrap;
  background: var(--color-bg);
  border: 1px solid color-mix(in srgb, var(--color-text) 25%, transparent);
  border-radius: 16px;
}

/* The panel is the same card in every state, pinned or in flow.

   It used to drop its fill once the bar pinned, on the reasoning that .nav-veil
   is already on screen behind it — a panel only opens while the bar is pinned
   *and* revealed, which is exactly when the band is there, so a card as well
   stacked a second opaque shape on a wash that had already done the job.

   That held while this was the only dropdown in the bar. The shop's nav puts a
   currency picker beside Community, and it carries a fill of its own, so the
   outline version left two dropdowns in one bar disagreeing about what a
   dropdown is. One card, both states, both sites — the veil behind it does no
   harm, it just is not the thing providing the ground any more.

   The filter's own dropdown was already like this: it keeps its card in every
   state, opening over the bare story grid with nothing behind it at all. */

/* Built exactly as the section pills outside the menu are — same padding, same
   border box, same 38px overall — so the options don't read as a smaller,
   softer control than the ones they sit under. The border is carried at rest as
   transparent rather than added on hover, which is what keeps the row from
   shifting by a pixel when the stroke appears. */
.story-filter__option,
.nav-list .sub-menu a {
  display: block;
  padding: 6px 14px;
  border: 1px solid transparent;
  border-radius: 99px;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 24px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  color: var(--color-text);
  text-decoration: none;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

/* The same stroke the section pills answer a pointer with — one hover language
   across the whole filter, rather than a wash here and an outline there. */
.story-filter__option:hover,
.story-filter__option:focus-visible,
.nav-list .sub-menu a:hover,
.nav-list .sub-menu a:focus-visible {
  border-color: var(--color-text);
}

.story-filter__option.is-active {
  background: var(--color-accent);
  color: var(--color-text);
}

/* ---- Section: toggles ---------------------------------------------------
   Outlined at rest at a quarter strength — a row of full-ink outlines reads as
   a wall of boxes and competes with the grid underneath. */
.story-filter__pill {
  display: inline-flex;
  align-items: center;
  padding: 6px 14px;
  border: 1px solid color-mix(in srgb, var(--color-text) 25%, transparent);
  border-radius: 99px;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 20px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  color: var(--color-text);
  text-decoration: none;
  /* Beat one of the reveal: the fill lands first, on its own, before anything
     moves. The width follows it — see the delay below and on the circle. */
  transition: background-color 0.25s ease, color 0.25s ease, border-color 0.25s ease,
    padding-right 0.4s cubic-bezier(0.32, 0.72, 0, 1) 0.12s;
}

/* Every pill stands 24px tall inside its padding, whether or not it has a
   circle. On the counted pills the circle sets that height — it keeps its 24px
   even while collapsed, so the row doesn't jump when one opens — and this gives
   "All Stories", which has no circle, the same line box to sit in. Without it
   that pill is 4px shorter than the rest of the row. */
.story-filter__pill-label {
  line-height: 24px;
}

.story-filter__pill:hover,
.story-filter__pill:focus-visible {
  border-color: var(--color-text);
}

.story-filter__pill.is-active {
  background: var(--color-text);
  border-color: var(--color-text);
  color: var(--color-bg);
}

/* The count circle. Collapsed to nothing at rest and opened by the active
   state, so selecting a section grows the pill to the right rather than
   swapping one shape for another.

   Width and margin are what animate, not `display` — a hidden element can't
   transition. The circle keeps its 24px height throughout so the pill's own
   height never changes and the row doesn't jump.

   It runs as three beats rather than all at once, which is what stops it
   reading as one mechanical slide:

     0.00s  the pill fills — colour only, nothing moves
     0.12s  it widens, the circle opening with it
     0.34s  the number fades up inside the circle

   The digit is a child element so it can be timed separately from the circle
   that holds it; fading the circle itself would fade its accent along with the
   number. Everything is opacity and width, so there's almost nothing to
   repaint. */
.story-filter__count {
  display: grid;
  place-items: center;
  overflow: hidden;
  width: 0;
  height: 24px;
  margin-left: 0;
  border-radius: 99px;
  background: var(--color-accent);
  color: var(--color-text);
  font-size: 12px;
  letter-spacing: 0;
  transition: width 0.4s cubic-bezier(0.32, 0.72, 0, 1) 0.12s,
    margin-left 0.4s cubic-bezier(0.32, 0.72, 0, 1) 0.12s;
}

/* Beat three. Held back until the circle it sits in has finished opening —
   a number sliding out of a widening box is the thing that read as jerky. */
.story-filter__count-value {
  opacity: 0;
  transition: opacity 0.22s ease;
}

/* Only the pills that carry a circle: "All Stories" has no count, and this
   rule applied to every active pill was cropping its right padding. */
.story-filter__pill--count.is-active {
  /* Trimmed to keep the gap either side of the circle even — the circle brings
     its own 8px on the left. */
  padding-right: 5px;
}

.story-filter__pill--count.is-active .story-filter__count {
  width: 24px;
  margin-left: var(--space-1);
}

.story-filter__pill--count.is-active .story-filter__count-value {
  opacity: 1;
  transition-delay: 0.34s;
}

@media (prefers-reduced-motion: reduce) {
  .story-filter__count,
  .story-filter__count-value {
    transition-duration: 0.01s;
    transition-delay: 0s;
  }
}

/* "Issue 5" sits quieter than the issue's name next to it. */
.story-filter__number {
  margin-right: 0.4em;
  opacity: var(--quiet);
}

/* ---- The docked filter --------------------------------------------------
   A pill locked near the bottom of the window that opens the same filter in a
   sheet above itself. It takes over on a wide screen once the real filter has
   scrolled up near the top, and below 900px it is the filter — the row at the
   top of the page is hidden there, because two rows of pills above the first
   card push the stories themselves off the first screen.

   Everything about it needs JavaScript, so it is display: none until
   initStoryFilter() marks the document. With no script the ordinary filter
   stays at the top of the page at every width, which is the honest fallback.
   ------------------------------------------------------------------------- */
.story-filter-dock {
  display: none;
}

/* Full width, anchored to the bottom of the window, with its contents in flow:
   the pill first, the sheet stacked above it (column-reverse, so the DOM order
   stays pill-then-sheet for the keyboard). Laying them out rather than floating
   the sheet means the band behind them grows when the sheet opens.

   Under the pinned nav (1000) and the mobile menu panel (1100): the menu covers
   the page, and this is part of the page.

   pointer-events sit on the controls, not the container — the band spans the
   whole width and would otherwise swallow clicks meant for the grid behind it. */
.has-filter-dock .story-filter-dock {
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: var(--space-2);
  position: fixed;
  z-index: 900;
  left: 0;
  right: 0;
  bottom: 0;
  padding-block: var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  transform: translateY(calc(100% + var(--space-6)));
  opacity: 0;
  pointer-events: none;
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.25s ease;
}

.has-filter-dock .story-filter-dock.is-visible {
  transform: translateY(0);
  opacity: 1;
}

/* Above the band. The veil is positioned and these two are not, and a positioned
   sibling paints over its in-flow ones whatever the source order says — which
   left the controls behind the blur and the pill behind the solid bottom of it,
   so pressing Filter appeared to delete the button. */
.story-filter-dock__toggle,
.story-filter-dock__sheet {
  position: relative;
  z-index: 1;
  pointer-events: auto;
}

/* Over the sheet, not under it: full screen the sheet covers the window, and
   the pill has to stay on top of it because it's what closes it. */
.story-filter-dock__toggle {
  z-index: 2;
}

/* The band itself: the page blurred and faded into the background colour,
   strongest at the bottom edge and gone by the top of the run. The mask is what
   graduates it — it fades the element *and* the backdrop-filter behind it, so
   there is no line where the blur stops. It reaches above the controls by 80px
   so the fade has somewhere to happen.

   Only while the filters are open. The pill on its own is a small thing floating
   over the grid and needs no ground under it; the band exists to get the
   controls off the photographs once they're on screen, so it arrives with them
   and leaves with them. */
.story-filter-dock__veil {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* A long tail, because the whole point is that it never has an edge: the band
     is the height of the controls plus this much again above them to fade out
     over. */
  top: calc(var(--space-15) * -2);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
  /* Eased rather than linear. A straight ramp between two stops has a visible
     shoulder where it leaves full strength and another where it reaches zero;
     these stops approximate an ease-out, so it holds near-solid behind the pill
     and then gives way slowly, with nothing in it that reads as a line. The
     extra stops also keep it from banding on a wide screen. */
  background: linear-gradient(
    to top,
    var(--color-bg) 0%,
    color-mix(in srgb, var(--color-bg) 97%, transparent) 16%,
    color-mix(in srgb, var(--color-bg) 88%, transparent) 30%,
    color-mix(in srgb, var(--color-bg) 74%, transparent) 43%,
    color-mix(in srgb, var(--color-bg) 55%, transparent) 56%,
    color-mix(in srgb, var(--color-bg) 36%, transparent) 69%,
    color-mix(in srgb, var(--color-bg) 19%, transparent) 81%,
    color-mix(in srgb, var(--color-bg) 7%, transparent) 91%,
    transparent 100%
  );
}

.story-filter-dock.is-open .story-filter-dock__veil {
  opacity: 1;
}

@supports (backdrop-filter: blur(16px)) {
  .story-filter-dock__veil {
    backdrop-filter: blur(16px);
    /* The same eased curve as the tint above it, so the blur and the colour let
       go of the page together instead of one outliving the other. */
    -webkit-mask-image: linear-gradient(
      to top,
      #000 0%,
      rgba(0, 0, 0, 0.98) 16%,
      rgba(0, 0, 0, 0.9) 30%,
      rgba(0, 0, 0, 0.76) 43%,
      rgba(0, 0, 0, 0.57) 56%,
      rgba(0, 0, 0, 0.38) 69%,
      rgba(0, 0, 0, 0.2) 81%,
      rgba(0, 0, 0, 0.07) 91%,
      transparent 100%
    );
    mask-image: linear-gradient(
      to top,
      #000 0%,
      rgba(0, 0, 0, 0.98) 16%,
      rgba(0, 0, 0, 0.9) 30%,
      rgba(0, 0, 0, 0.76) 43%,
      rgba(0, 0, 0, 0.57) 56%,
      rgba(0, 0, 0, 0.38) 69%,
      rgba(0, 0, 0, 0.2) 81%,
      rgba(0, 0, 0, 0.07) 91%,
      transparent 100%
    );
  }
}

/* Below the breakpoint the pill doesn't wait to be revealed — it is the filter,
   so it is simply there. Said in CSS rather than left to the script's media
   query listener: this is a question about the viewport, and the answer
   shouldn't depend on an event having fired. */
@media (max-width: 900px) {
  .has-filter-dock .story-filter-dock {
    transform: translateY(0);
    opacity: 1;
  }
}

/* Gone once the footer is on screen — the stories have run out and the pill is
   standing over the newsletter form. Placed after the width rule above and
   carrying one more class than it, so it overrules the resting state at every
   width rather than only where the pill waits to be revealed. */
.has-filter-dock .story-filter-dock.is-hidden {
  transform: translateY(calc(100% + var(--space-6)));
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .has-filter-dock .story-filter-dock,
  .story-filter-dock__veil {
    transition-duration: 0.01s;
  }
}

/* Accent-filled like the issue dropdown's summary — it is the same kind of
   control, the one that sets the whole view — but at the size of the site's
   primary button (.cta-pill, the Subscribe page's "Create your account"): this
   one is the only control on screen once it takes over, and the small type of a
   filter pill isn't enough to carry that on its own.

   The padding is a pixel short of .cta-pill's each way because this has a
   border and that doesn't, so the two stand exactly the same 56px tall. */
.story-filter-dock__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The floor the open state lands on: with the word collapsed the button has
     almost no content left, and this is what keeps it a 56px circle rather than
     a stub. Inert while the label is there — "Filter" makes it 110 wide. */
  min-width: var(--space-7);
  padding: calc(var(--space-2) - 1px) calc(var(--space-3) - 1px);
  border: 1px solid var(--color-accent);
  border-radius: 99px;
  background: var(--color-accent);
  color: var(--color-text);
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity 0.3s ease,
    padding 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    padding-right 0.4s cubic-bezier(0.32, 0.72, 0, 1) 0.12s;
}

.story-filter-dock__toggle:hover,
.story-filter-dock__toggle:focus-visible {
  opacity: 0.7;
}

/* The count circle again, saying how many facets are on — the same shape and
   the same three-beat opening as the section pills'. Inverted, because here it
   sits on the accent rather than beside it. */
.story-filter-dock__toggle .story-filter__count {
  background: var(--color-bg);
}

/* Trimmed to keep the gap either side of the circle even — the circle brings
   its own 8px on the left. Same trim as the section pills', measured from this
   pill's larger padding. */
.story-filter-dock__toggle.is-active {
  padding-right: calc(var(--space-1) + 1px);
}

.story-filter-dock__toggle.is-active .story-filter__count {
  width: 24px;
  margin-left: var(--space-1);
}

.story-filter-dock__toggle.is-active .story-filter__count-value {
  opacity: 1;
  transition-delay: 0.34s;
}

/* ---- Filter → close ------------------------------------------------------
   Open, the pill becomes a cross in a circle — at every width. On a phone that
   cross *is* the modal's close button, sitting where a thumb already is, which
   is why the panel has no cross of its own in the corner.

   Nothing here animates the button's width directly — an auto width can't be
   transitioned — it animates the things the width is made of: the word
   collapses, the count circle closes, the side padding comes in, and the box
   follows them down to the 56px floor above. */
.story-filter-dock__label {
  display: block;
  /* A shade over the word's own 62px, so the collapse starts almost at once —
     the distance between this and the text is dead time in the animation. */
  max-width: 4.5em;
  overflow: hidden;
  white-space: nowrap;
  transition: max-width 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    opacity 0.2s ease;
}

.story-filter-dock.is-open .story-filter-dock__label {
  max-width: 0;
  opacity: 0;
}

/* The tally has nothing to say while the thing it counts is open. */
.story-filter-dock.is-open .story-filter-dock__toggle.is-active {
  padding-right: calc(var(--space-2) - 1px);
}

.story-filter-dock.is-open .story-filter-dock__toggle .story-filter__count {
  width: 0;
  margin-left: 0;
}

.story-filter-dock.is-open .story-filter-dock__toggle .story-filter__count-value {
  opacity: 0;
  transition-delay: 0s;
}

.story-filter-dock.is-open .story-filter-dock__toggle {
  padding-inline: calc(var(--space-2) - 1px);
}

/* The cross itself, drawn the way the site menu draws its own. It grows out
   from the middle rather than fading in — with the pill closing around it at
   the same time, the two read as one movement. */
.story-filter-dock__toggle::before,
.story-filter-dock__toggle::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 1.5px;
  background: var(--color-text);
  transition: width 0.4s cubic-bezier(0.32, 0.72, 0, 1) 0.1s;
}

.story-filter-dock__toggle::before {
  transform: translate(-50%, -50%) rotate(45deg);
}

.story-filter-dock__toggle::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.story-filter-dock.is-open .story-filter-dock__toggle::before,
.story-filter-dock.is-open .story-filter-dock__toggle::after {
  width: 20px;
}

@media (prefers-reduced-motion: reduce) {
  .story-filter-dock__label,
  .story-filter-dock__toggle::before,
  .story-filter-dock__toggle::after {
    transition-duration: 0.01s;
  }
}

/* No panel drawn around it any more: the controls sit straight on the band, in
   flow above the pill. */
.story-filter-dock__sheet {
  max-width: 100%;
  padding-inline: var(--grid-margin);
}

.story-filter-dock__sheet[hidden] {
  display: none;
}

/* Stacked by default: at a phone's width one line of controls would wrap into
   an unreadable block anyway. */
.story-filter--dock {
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding-block: 0;
}

/* With room for it, the sheet carries the same single line as the filter at the
   top of the page — the issue dropdown and every section beside it. */
@media (min-width: 901px) {
  /* The same width the filter at the top of the page gets, which is what makes
     the two copies of it behave alike. At 960 they didn't: the top filter had a
     1440px container and set the dropdown and all four sections on one line,
     while the sheet had 880px of content box for a line that needs 915 and
     wrapped into two rows.

     The sheet is a shrink-to-fit flex item, so this is only ever a ceiling — it
     takes the width of its own controls and no more, whatever the screen. That
     also means the old cap could only ever force a wrap, never prevent a
     too-wide line. Enough sections to overrun 1440 still wrap, exactly as they
     would at the top of the page. */
  .story-filter-dock__sheet {
    max-width: min(92vw, var(--container-max));
  }

  .story-filter--dock {
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-2);
  }
}

/* The issue menu opens up rather than down — there is nothing below the sheet
   but the pill and the bottom of the window. */
.story-filter--dock .story-filter__menu {
  top: auto;
  bottom: calc(100% + var(--space-1));
}

/* ---- The full-screen version --------------------------------------------
   Below the breakpoint the sheet is not a popover on a band, it's a modal over
   the whole page: the stories blurred away behind it, the filters stacked down
   the middle at the size of the pill that opened them, and that pill — now a
   cross — sitting at the bottom to close it.
   ------------------------------------------------------------------------- */
@media (max-width: 900px) {
  /* The dock is fixed *and* transformed, and a transform makes an element the
     containing block for anything fixed inside it — which sizes the modal to the
     pill's own strip at the bottom of the window rather than to the window.

     Two things this has to get right, and it used to get both wrong.

     `transition` first: `none` is a different value from the identity matrix the
     dock rests at, so the change was transitioned like any other, and for the
     0.35s that took, the computed transform was still a matrix. The panel spent
     that long drawn into a 208px strip along the bottom of the screen and then
     snapped to full height — the flash at the bottom of the window on every
     open. Naming a transition here without transform in it is what makes the
     change instant: a transition starts on the strength of the property list in
     the style being changed *to*.

     Then the state it keys off. .is-open goes at the top of the close, while the
     sheet is still fading and still on screen, which would run the same 350ms in
     reverse — the modal shrinking into the strip as it faded. .is-showing spans
     the whole time the panel is displayed, hidden to hidden, so the dock is only
     a containing block again once there is nothing fixed inside it to size. */
  .has-filter-dock .story-filter-dock.is-showing {
    transform: none;
    transition: opacity 0.25s ease;
  }

  .story-filter-dock__sheet {
    position: fixed;
    inset: 0;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* From the top, not the middle: the first button lands in the same place
       whether there are three filters or twelve, and a long list simply runs on
       down the screen instead of the whole stack sliding up to stay centred.
       The bottom padding clears the pill — 24px of dock padding, its 56px, and
       room to breathe over it. */
    justify-content: flex-start;
    padding: var(--space-12) var(--grid-margin) var(--space-14);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: color-mix(in srgb, var(--color-bg) 86%, transparent);
    /* The blur arrives on its own, before anything appears on top of it — the
       controls' entrance below is timed to start as this finishes. */
    opacity: 0;
    transition: opacity 0.22s ease;
  }

  .story-filter-dock.is-open .story-filter-dock__sheet {
    opacity: 1;
  }

  @supports (backdrop-filter: blur(24px)) {
    .story-filter-dock__sheet {
      backdrop-filter: blur(24px);
    }
  }

  /* Opaque where the blur can't be drawn: a page reading through an 86% panel
     with nothing softening it is worse than one that's simply covered. */
  @supports not (backdrop-filter: blur(24px)) {
    .story-filter-dock__sheet {
      background: var(--color-bg);
    }
  }

  /* The band has nothing to do under a panel that covers the window. */
  .story-filter-dock.is-open .story-filter-dock__veil {
    opacity: 0;
  }

  /* One column of controls, held to a comfortable width rather than run to the
     edges of the screen.

     They fade as one thing, on the panel's own clock: the blur takes the page
     and the options arrive on it a moment later, and on the way out both go
     together. This used to be a cascade, each control rising into place behind
     the one before it — which reads well on a desktop popover and doesn't on a
     phone, where a stack of eight buttons arriving one at a time is movement
     everywhere at once. */
  .story-filter--dock {
    width: min(20rem, 100%);
    gap: var(--space-2);
    opacity: 0;
    transition: opacity 0.2s ease;
  }

  /* The delay is the hair of a start the blur gets, and it's only on the way in:
     closing, the base rule above has no delay, so the options let go at the same
     moment the panel does. */
  .story-filter-dock.is-open .story-filter--dock {
    opacity: 1;
    transition-delay: 0.06s;
  }

  .story-filter--dock .story-filter__level,
  .story-filter--dock .story-filter__options {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    gap: var(--space-2);
  }

  .story-filter--dock .story-filter__dropdown {
    width: 100%;
  }

  /* Every control the size of the pill that opened them — the same 16/24 type
     in the same 56px box, so the stack reads as one set of buttons rather than
     as small filter chips inside a big panel. */
  .story-filter--dock .story-filter__summary,
  .story-filter--dock .story-filter__pill {
    justify-content: center;
    width: 100%;
    padding: calc(var(--space-2) - 1px) calc(var(--space-3) - 1px);
    font-size: var(--nav-font-size);
    line-height: var(--nav-line-height);
  }

  .story-filter--dock .story-filter__pill-label {
    line-height: var(--nav-line-height);
  }

  /* The issue list opens into the flow rather than over it, and drops the card
     it was drawn in: no panel, no border, no shadow — just more buttons, the
     same size as the ones above and below, pushing the sections down as an
     accordion does. Absolutely positioned it hung off the stack and out of the
     window; in flow the panel simply scrolls. */
  .story-filter--dock .story-filter__menu {
    position: static;
    transform: none;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    width: 100%;
    min-width: 0;
    margin-top: var(--space-2);
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    white-space: normal;
  }

  /* Drawn as a section pill is: outlined at a quarter strength, filling with the
     accent when it's the issue being shown. */
  .story-filter--dock .story-filter__option {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: calc(var(--space-2) - 1px) calc(var(--space-3) - 1px);
    border: 1px solid color-mix(in srgb, var(--color-text) 25%, transparent);
    border-radius: 99px;
    font-size: var(--nav-font-size);
    line-height: var(--nav-line-height);
    text-align: center;
  }

  .story-filter--dock .story-filter__option:hover,
  .story-filter--dock .story-filter__option:focus-visible {
    border-color: var(--color-text);
  }

  .story-filter--dock .story-filter__option.is-active {
    background: var(--color-accent);
    border-color: var(--color-accent);
  }

  /* The one cascade left in the panel, and it earns it: this is a short list
     appearing inside a panel that is already up, on its own tap, so nothing else
     is moving while it runs. What it can no longer do is collide with the
     panel's own entrance — the sheet's <details> is collapsed once it's out of
     sight (closeSheet in main.js), so the list is always closed when the panel
     opens. */
  .story-filter--dock .story-filter__dropdown[open] .story-filter__menu li {
    animation: filter-control-in 0.2s cubic-bezier(0.32, 0.72, 0, 1) backwards;
  }

  .story-filter--dock .story-filter__dropdown[open] .story-filter__menu li:nth-child(1) { animation-delay: 0.02s; }
  .story-filter--dock .story-filter__dropdown[open] .story-filter__menu li:nth-child(2) { animation-delay: 0.06s; }
  .story-filter--dock .story-filter__dropdown[open] .story-filter__menu li:nth-child(3) { animation-delay: 0.1s; }
  .story-filter--dock .story-filter__dropdown[open] .story-filter__menu li:nth-child(4) { animation-delay: 0.14s; }
  .story-filter--dock .story-filter__dropdown[open] .story-filter__menu li:nth-child(n + 5) { animation-delay: 0.18s; }
}

@keyframes filter-control-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .story-filter--dock .story-filter__dropdown[open] .story-filter__menu li {
    animation: none;
  }

  .story-filter-dock__sheet,
  .story-filter--dock {
    transition-duration: 0.01s;
  }
}

/* Behind the modal, the page underneath doesn't scroll — same lock the site
   menu uses. */
body.filters-open {
  overflow: hidden;
}


/* ---- Centering a short row ----------------------------------------------
   The stories grid is the one listing whose length is up to the reader: a
   filter can leave three stories, or seven. Left-aligned, the leftovers sit
   under the first cards with an empty half-row beside them. Laid out as a
   centered wrap instead, a row that isn't full sits in the middle of the page.

   The widths are the same numbers the 12-column grid produces — a col-span-3
   card is (100% - 3 gutters) / 4 across, a col-span-6 card (100% - 1 gutter)
   / 2 — so a full row is identical to what the grid gave and only the short
   row moves. The hair off each width keeps four cards from rounding past 100%
   and wrapping the last one down a line. */
.stories-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.stories-grid .story-card {
  width: calc((100% - 3 * var(--grid-gutter)) / 4 - 0.02px);
}

/* Two across, matching the col-span-6 the grid hands cards at this width. */
@media (min-width: 768px) and (max-width: 1023px) {
  .stories-grid .story-card {
    width: calc((100% - var(--grid-gutter)) / 2 - 0.02px);
  }
}

@media (max-width: 767px) {
  .stories-grid .story-card {
    width: 100%;
  }
}

/* The empty-result line is not a card, and a flex row would size it to its
   own text and centre that instead. */
.stories-grid .placeholder-note {
  width: 100%;
}

/* ---- The grid's entrance ------------------------------------------------
   Thumbnails blur and fade in whenever the filter changes. Under JS that's the
   swap in main.js adding .is-swapping and removing it on the next frame; with
   no JS it's the ordinary page load, and the same animation runs off
   .stories-grid's own load state. Either way the cards arrive the way the
   in-story images do — same blur-in language as .reveal-image. */
.stories-grid .story-card {
  animation: stories-card-in 0.6s cubic-bezier(0.4, 0, 0.2, 1) backwards;
}

/* Staggered just enough to read as a sequence rather than a single flash.
   Capped at the sixth card: past that the wait is longer than the animation. */
.stories-grid .story-card:nth-child(2) { animation-delay: 0.05s; }
.stories-grid .story-card:nth-child(3) { animation-delay: 0.1s; }
.stories-grid .story-card:nth-child(4) { animation-delay: 0.15s; }
.stories-grid .story-card:nth-child(5) { animation-delay: 0.2s; }
.stories-grid .story-card:nth-child(n + 6) { animation-delay: 0.25s; }

@keyframes stories-card-in {
  from {
    opacity: 0;
    filter: blur(12px);
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    filter: blur(0);
    transform: none;
  }
}

/* ---- Loading the rest as you go -----------------------------------------
   The pagination is still rendered and still works — it is the whole navigation
   with no JavaScript — but once main.js is appending pages as the reader nears
   the end of the grid, the numbered links are a second way to do the same thing
   and are taken out of the page. It comes back if a fetch ever fails.

   The sentinel is what the observer watches. It needs a box to intersect with,
   so it has a height, and 1px of it sits in the 120px gap under the grid. */
.has-infinite-stories .pagination {
  display: none;
}

.stories-sentinel {
  height: 1px;
}

/* Held blank between the click and the new cards arriving, so the outgoing set
   doesn't sit there looking like nothing happened. */
.stories-grid.is-swapping {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .stories-grid .story-card {
    animation: none;
  }

  .stories-grid.is-swapping {
    opacity: 1;
  }
}

.pagination {
  display: flex;
  justify-content: center;
  gap: var(--space-2);
  padding-bottom: var(--space-10);
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.pagination .page-numbers {
  color: var(--color-text);
  text-decoration: none;
  opacity: var(--quiet);
  transition: opacity 0.3s ease;
}

.pagination .page-numbers:hover,
.pagination .page-numbers:focus-visible,
.pagination .page-numbers.current {
  opacity: 1;
}


.placeholder-note {
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  opacity: var(--quiet);
  text-align: center;
}

.story-card {
  display: block;
  margin-bottom: var(--space-6);
  color: inherit;
  text-decoration: none;
}

/* Four across is too tight to carry a 24px title once the row is under
   ~1000px — a quarter column lands around 180px there. Two across between
   the tablet breakpoint and the full-width stacking below it. */
@media (min-width: 768px) and (max-width: 1023px) {
  .story-card.col-span-3 {
    grid-column: span 6;
  }
}

/* True to the file: no aspect-ratio, no crop. Cards in a row therefore have
   images of different heights and their titles don't line up — that's intended,
   not a regression to fix later. */
/* Grows on hover exactly as the issue covers do — same 4%, same curve, same
   duration (see .issue-card__cover img). Scaled on the image rather than on
   the card so the tag and title underneath stay put; the card is what's
   hovered, so pointing at the words grows the picture too.

   Nothing clips it, which is the point: these thumbnails are never cropped, so
   the image grows past its box rather than zooming inside a window. The spill
   is 2% per side — about 6px at a four-across column, inside the 16px gutter,
   and under the 16px margin above the tag. */
.story-card__image {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: var(--space-2);
  /* Named so the scroll reveal can put it back — see .story-card__image
     .is-revealed, which has to restate this transition because the reveal's
     own one replaces it. */
  --card-image-transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  transition: var(--card-image-transition);
}

.story-card:hover .story-card__image,
.story-card:focus-visible .story-card__image {
  transform: scale(1.04);
}

@media (prefers-reduced-motion: reduce) {
  .story-card__image {
    transition: none;
  }

  .story-card:hover .story-card__image,
  .story-card:focus-visible .story-card__image {
    transform: none;
  }
}

.story-card__tag {
  margin: 0 0 var(--space-1);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  color: var(--story-color);
}

.story-card__title {
  margin: 0;
  /* Now an h2 — the display face has no bold cut, so left alone the browser
     would synthesise one over the heading default. */
  font-weight: 400;
  font-family: var(--font-display);
  font-size: 24px;
  line-height: 1.2;
  color: var(--color-text);
}

/* The standfirst under the title. The reading face rather than the display
   one — it is a sentence to be read, not a name to be recognised, and it is
   set at the same 16/24 the story pages read at. */
.story-card__dek {
  margin: var(--space-1) 0 0;
  font-family: var(--font-serif);
  font-size: 1rem;      /* 16px */
  line-height: 1.5;     /* 24px */
  color: var(--color-text);
}

/* ==========================================================================
   Homepage hero
   Same pin-and-dissolve idea as the story hero, but here what's underneath
   is a real panel rather than a sharpened copy of the same picture: the
   stage sticks to the top of the viewport and the slideshow blurs/fades
   off it, uncovering the current-issue panel that was behind it the whole
   time. Scroll math and the wrapper's height live in initHomeReveal().

   The frames are deliberately taller than the viewport (--home-hero-bleed)
   so .home-hero__fade can wash the image into the page background entirely
   below the fold — there's never a visible bottom edge on the photo.
   ========================================================================== */
:root {
  --home-hero-bleed: 320px;
}

/* The wordmark and nav sit *over* the hero rather than above it in flow, so the
   header comes out of the document flow entirely and the nav's normal-flow
   spacer has nothing to reserve.

   Keyed off .overlay-hero (set in header.php) rather than .home-page: the About
   page opens the same way, and this is the whole of what "opens on a full-bleed
   picture" costs. The .home-page selectors stay for the hero machinery below,
   which is genuinely homepage-only. */
.overlay-hero .site-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
}

.overlay-hero .nav-spacer {
  display: none;
}

.home-hero-wrapper {
  position: relative;
  /* JS overrides this with 100vh + the reveal distance; the plain 100vh
     here is the no-JS resting state. */
  height: 100vh;
}

.home-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  /* The hero is scaled up as it dissolves (HOME_HERO_ZOOM in main.js), which
     pushes it past both sides of the viewport — its own overflow:hidden is no
     help, since the scale applies to that box and takes the clip with it. So
     the clip has to sit out here, on the unscaled stage.

     `clip` rather than `hidden` on purpose, twice over: hidden would make this
     a scroll container and kill the sticky positioning right above, and only
     clip is allowed to pair with an unclipped other axis — which is what lets
     the frames keep bleeding --home-hero-bleed past the bottom edge, the whole
     point of that bleed. */
  overflow-x: clip;
  overflow-y: visible;
}

.home-hero {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: calc(100vh + var(--home-hero-bleed));
  z-index: 2;
  overflow: hidden;
  /* Purely decorative once it starts dissolving — never let it swallow
     clicks meant for the panel revealed underneath it. */
  pointer-events: none;
  will-change: opacity, filter, transform;
}

.home-hero__frames {
  position: absolute;
  inset: 0;
  /* Intro: the whole stack fades and un-blurs in on load, matching the
     story hero's entrance. The scroll-driven blur is applied to
     .home-hero itself so the two never fight over one filter value. */
  opacity: 0;
  filter: blur(48px);
  transition: opacity 1.2s ease, filter 1.4s ease;
}

.home-hero.is-loaded .home-hero__frames {
  opacity: 1;
  filter: blur(0);
}

/* Every frame is stacked in the same box; the play-through just switches
   each one on in turn, so each fades in over the top of the one before it
   (later in the DOM = higher in the stack). */
.home-hero__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  /* Keep in step with SLIDESHOW_FRAME_FADE in main.js. */
  transition: opacity 400ms linear;
}

.home-hero__frame.is-visible {
  opacity: 1;
}

.home-hero__fade {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--home-hero-bleed);
  background: linear-gradient(to bottom, transparent, var(--color-bg));
  opacity: 0;
  transition: opacity 1.2s ease;
}

.home-hero.is-loaded .home-hero__fade {
  opacity: 1;
}

/* ==========================================================================
   Current issue
   Sits behind the hero inside the pinned stage and is uncovered as the
   slideshow dissolves. Its opacity/blur are driven by the same scroll
   progress — see initHomeReveal().
   ========================================================================== */
.current-issue {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: var(--space-6) var(--grid-margin);
  /* Opaque rather than transparent: the slideshow above it fades and blurs
     away to uncover this panel, and anything showing through mid-fade would
     be a third layer in a crossfade meant to read as two. */
  background: var(--color-bg);
  will-change: opacity, transform, filter;
}

/* The band of the panel the cover stands in, and the thing the spreads are
   centred on. It is the cover's own size — the spreads are twice as wide and
   deliberately overflow it, because what they share with the cover is the top
   and bottom edge, not the column. */
.current-issue__stage {
  position: relative;
  /* Fills the viewport height minus everything stacked with it: the 48px
     panel padding top and bottom, the 32px gap, and the ~104px meta block.
     Derived rather than a vh fraction so it stays as large as it can be
     without the title or CTAs ever being pushed off-screen. */
  height: min(880px, calc(100vh - 240px));
  /* Set per issue from the cover file's own dimensions — see issue-card.php.
     The fallback is the trim every issue has used so far. */
  aspect-ratio: var(--cover-ratio, 1800 / 2363);
  max-width: 100%;
}

/* The cover art is a full-bleed photograph trimmed to the printed page, so
   there's nothing to knock out of it and the box only has to match the file's
   own shape to keep `contain` from letterboxing it. With a back cover set,
   this is also the 3D box the two faces turn inside — see Cover flip below. */
.current-issue__cover {
  display: block;
  width: 100%;
  height: 100%;
  /* Deep enough that the turning cover reads as a solid object rotating
     rather than a card lunging at the reader. Roughly twice the box. */
  perspective: 1800px;
}

.current-issue__cover img,
.issue-card__cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Named so the scroll reveal can put it back — see the .is-revealed rule
     alongside .story-card__image's, which restates it. */
  --card-image-transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  transition: var(--card-image-transition);
}

/* Only the covers that can't turn over grow on hover: a cover mid-flip is
   already saying "there's another side to this", and scaling it as well
   would be two answers to the same gesture. */
.current-issue__cover:not(.has-back):hover img,
.current-issue__cover:not(.has-back):focus-visible img,
.issue-card__cover:hover img,
.issue-card__cover:focus-visible img {
  transform: scale(1.04);
}

/* ==========================================================================
   Cover flip
   The front and back of the printed issue as two faces of one box, turned by
   the reader's own scroll rather than by hover — see initHomeReveal(). Which
   is why there's no transition here: the rotation is written from JS on every
   frame, and easing it as well would put the cover a beat behind the hand
   turning it. It also means the gesture belongs to every reader, phone
   included, and that scrolling back up turns the cover back.
   ========================================================================== */
.cover-flip {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  /* Rewritten every frame the reader scrolls through the flip, same reason
     the panel around it carries one. */
  will-change: transform;
}

.cover-flip__face {
  display: block;
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
}

/* Pre-turned, so that the flipper's own half-turn lands it face on. */
.cover-flip__face--back {
  transform: rotateY(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .current-issue__cover img,
  .issue-card__cover img {
    transition: none;
  }

  .current-issue__cover:not(.has-back):hover img,
  .current-issue__cover:not(.has-back):focus-visible img,
  .issue-card__cover:hover img,
  .issue-card__cover:focus-visible img {
    transform: none;
  }
}

/* Label / title / CTAs are identical between the full-bleed current-issue
   panel and the smaller cards in the issues row below it, so the type
   lives here once and only the cover size differs between the two. */
.issue-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  text-align: center;
}

.issue-meta__label {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.issue-meta__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 40px;
  line-height: 1.2;
}

/* An issue lettered in a face of its own. The heading is a picture rather than
   type — see issue-card.php — and the rules above stop applying to it, which is
   why there is nothing here undoing them: a block-level image leaves the
   heading no line box to have a size or a leading.

   Sized by height, never by width. The marks are all cut to one canvas, so one
   height stands them all the same whatever the word is; keying off width would
   make Pantry twice the size of Rice & Beans. The knob is declared here rather
   than at :root because this is the only thing that has one — the same reason
   --about-title-max is declared on the headline it belongs to. */
.issue-meta__title--mark {
  --issue-mark-height: 48px;
}

.issue-meta__title--mark img {
  display: block;
  margin-inline: auto;
  width: auto;
  height: auto;
  max-height: var(--issue-mark-height);
  /* Held inside its column: a mark wider than the card would push the grid
     rather than the grid holding it. Height follows the cap, which is the one
     way a row of them comes out uneven — and only if the files aren't all cut
     to the same canvas, which the field asks for. */
  max-width: 100%;
}

/* Two outlined pills rather than two text links with a pipe between them.
   Deliberately not .cta-pill--outline: that is the site's primary action at
   56px tall, and these are a pair of quiet choices under a cover — small
   enough that four of them across a row of back issues don't read as four
   demands. Same face and tracking as the chrome, which is what keeps them
   related to it. */
.issue-ctas {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.issue-ctas__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1) var(--space-2);
  /* Drawn and answering the same way as .story-filter__pill: these are the same
     kind of control — a quiet choice offered in a row of them — so a reader who
     has used the filters on the stories index already knows what these do. The
     border is the only thing that moves, and it moves from faint to full ink
     rather than filling: a filled pill is what .story-filter__pill.is-active
     means, and nothing here is ever in that state.

     Half strength rather than the filter's quarter, per the Figma — these carry
     16px type against the filter's 14px and sit under a cover rather than in a
     row of their own, where a quarter-strength edge disappears. */
  border: 1px solid color-mix(in srgb, var(--color-text) 50%, transparent);
  border-radius: 99px;
  color: var(--color-text);
  text-decoration: none;
  transition: border-color 0.25s ease;
}

.issue-ctas__link:hover,
.issue-ctas__link:focus-visible {
  border-color: var(--color-text);
}

@media (prefers-reduced-motion: reduce) {
  .issue-ctas__link {
    transition: none;
  }
}

/* ==========================================================================
   Spreads
   Pages from inside the issue, reached by turning its cover over. The layer
   lies on the stage and does nothing until the flip has finished — see
   initIssueSpreads(), which is what takes the disabled attributes off the two
   halves. That is also what keeps the cover an ordinary link until then: these
   halves cover it, so before the flip the click falls through to the link and
   afterwards it doesn't, with no handler having to cancel anything.

   Centred on the stage rather than laid out in it, because a spread is two
   pages and the cover is one: what they share is the top and bottom edge, and
   the spread is allowed to overhang the cover's column on both sides. The two
   halves the reader clicks overhang further still, out to the window.
   ========================================================================== */
.issue-spreads {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  /* Two pages wide, always — the frame doesn't resize when a spread opens. It
     used to grow from the cover's width to this, which read as the picture
     zooming rather than as a page arriving. Now the frame is simply the size a
     spread is, standing empty over the back cover until one flies into it. */
  width: 200%;
  max-width: calc(100vw - var(--grid-margin) * 2);
  transform: translateX(-50%);
  /* Inert while the cover is still turning, which is the whole of the link's
     protection — see the block comment above. */
  pointer-events: none;
  /* Down the page is the browser's, across the page is ours: the swipe that
     pages the issue is read here (see initIssueSpreads), and without this a
     horizontal drag is left to the browser to interpret — which on a phone
     means a half-started scroll or the back gesture rather than a page turn.
     pinch-zoom is named explicitly because pan-y alone would take it away, and
     a reader looking closely at a spread is exactly who wants it. */
  touch-action: pan-y pinch-zoom;
}

.current-issue.is-flipped .issue-spreads {
  pointer-events: auto;
}

.issue-spreads__slides {
  position: absolute;
  inset: 0;
}

/* One page arrives as the last one leaves, both travelling the same way and
   the whole width of the window to do it: the reader asked for the next
   spread, and the one they were reading gets out of its way. Parked off the
   side it will come in from, so adding .is-current is the whole of the
   animation.

   Nothing fades. A page is opaque and it either is in front of you or it is
   somewhere else, and a page that dissolves on its way past is a slide in a
   deck rather than a thing being moved. What keeps the parked ones out of
   sight is the distance alone — 50vw carries the frame's own centre to the
   edge of the window and the further 50% carries its edge there — and what
   keeps them from putting a scrollbar on the page is .home-stage's
   overflow-x: clip, which the hero's zoom already depends on.

   Which side a page is parked on depends on which way the reader is going, and
   that is what data-dir on the frame says. initIssueSpreads() sets it before it
   moves anything, so a slide is always already parked on the correct side by
   the time it is asked to come in. */
.issue-spreads__slide {
  position: absolute;
  inset: 0;
  margin: 0;
  transform: translateX(calc(50vw + 50%));
}

.issue-spreads[data-dir="prev"] .issue-spreads__slide {
  transform: translateX(calc(-50vw - 50%));
}

/* Written long-hand so it outranks the parked rule directly above, which
   carries an attribute as well as its two classes. Left as
   `.issue-spreads__slide.is-current` it loses to that rule and the page the
   reader has just asked for sits parked off the left of the frame and never
   arrives — which happens going back and not going forward, because forward is
   the direction with no attribute in its selector.

   Equal specificity with the rule above, so it also has to stay below it: at
   equal specificity the last word is source order. */
.issue-spreads .issue-spreads__slide.is-current {
  transform: translateX(0);
}

/* The one on its way out. It leaves in the direction of travel rather than
   back the way it came, so the two pages move together instead of passing
   each other. */
.issue-spreads[data-dir="next"] .issue-spreads__slide.is-leaving {
  transform: translateX(calc(-50vw - 50%));
}

.issue-spreads[data-dir="prev"] .issue-spreads__slide.is-leaving {
  transform: translateX(calc(50vw + 50%));
}

/* The two that are actually moving, and only those. Every other page is parked,
   and a parked page swaps sides the moment the reader changes direction — with
   a transition on it that swap is the page travelling right across the window
   to get there, every page in the issue at once, the first time anyone goes
   back. Parked pages teleport; the two in the move animate. */
.issue-spreads__slide.is-current,
.issue-spreads__slide.is-leaving {
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Hidden until the flip has finished, and the flip's own cover hidden from then
   on. The carousel opens on the same back cover at the same size in the same
   place, so the handover is invisible — and from that moment there is one thing
   on the stage rather than a carousel with a cover showing through the gap
   between two travelling pages. */
.issue-spreads {
  visibility: hidden;
}

.current-issue.is-flipped .issue-spreads {
  visibility: visible;
}

.current-issue.is-flipped .current-issue__cover {
  visibility: hidden;
}

/* A cover is one page where a spread is two, and they share a frame built for
   two. Held to half of it, which is exactly the stage's own width — that is
   what lets a cover in the carousel stand precisely where the cover on the
   stage stood. */
.issue-spreads__slide--cover .issue-spreads__image {
  width: 50%;
  margin-inline: auto;
}

.issue-spreads__image {
  display: block;
  width: 100%;
  height: 100%;
  /* Fitted, never cropped: these are pages, and a page with its edge cut off
     is a mistake rather than a crop. A file that isn't exactly twice the
     cover's shape sits inside the frame with the page colour either side. */
  object-fit: contain;
}

/* The two halves, and they are halves of the window rather than of the
   picture: the reader's pointer is somewhere in this band of the page, and
   asking them to bring it back onto the spread before it will admit there is
   anything to click makes the invitation smaller than it needs to be. Anywhere
   right of the middle goes forward, anywhere left of it goes back.

   The frame is centred on the window, so its own 50% mark is the window's
   middle and 50vw from there reaches the edge exactly. .home-stage clips the
   overflow, so nothing here can put a scrollbar on the page. */
.issue-spreads__nav {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50vw;
  padding: 0;
  border: 0;
  background: none;
  -webkit-appearance: none;
  appearance: none;
}

/* A plain ink arrow. The keyword after it is what a browser that refuses the
   image falls back to, which is why they are the resize cursors: the only pair
   in the standard set that point left and right. */
.issue-spreads__nav--prev {
  right: 50%;
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cpath d='M31 20H9M17 12l-8 8 8 8' fill='none' stroke='%231d1c1b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") 20 20, w-resize;
}

.issue-spreads__nav--next {
  left: 50%;
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cpath d='M9 20h22M23 12l8 8-8 8' fill='none' stroke='%231d1c1b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") 20 20, e-resize;
}

.issue-spreads__nav:focus-visible {
  outline: 2px solid var(--color-bg);
  outline-offset: -6px;
}

@media (prefers-reduced-motion: reduce) {
  .issue-spreads__slide.is-current,
  .issue-spreads__slide.is-leaving {
    transition: none;
  }
}

/* ==========================================================================
   Issues row
   The back-catalogue strip that follows the current issue — four covers on
   the 12-col grid (3 columns each, matching the Figma ISSUES frame).
   ========================================================================== */
.issues-row {
  /* Tighter above than below: the row belongs with the current issue it follows,
     and wants clear air before the About blurb starts a new thought. */
  padding-top: var(--space-10);
  padding-bottom: var(--space-15);
  row-gap: var(--space-15);
}

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

.issue-card__cover {
  display: block;
  height: var(--issue-cover-height);
  /* Set per issue from the cover file, same as the panel above: matching the
     box to the file keeps `contain` from letterboxing the cover inside its
     column. */
  aspect-ratio: var(--cover-ratio, 1800 / 2363);
  max-width: 100%;
  margin-inline: auto;
}

/* --------------------------------------------------------------------------
   The current issue on a phone

   On a desktop the panel is the whole window and the cover fills it, which is
   right — it is the object the page is about. On a phone that same rule made
   it a full-screen cover standing over a row of thumbnails, and the issue you
   are being sold looked like a different kind of thing from the four below it.
   So down here it is simply the first of them: the same height as a back
   issue, at the top of the row it belongs to.

   The spreads keep the stage's top and bottom edge, as they do everywhere —
   what changes is that they stop being held inside the window. A spread is two
   pages against a cover's one, and on a screen this narrow honouring both the
   window's width and the cover's height would shrink the pages to a stamp
   floating in an empty frame. They run off both sides instead and are read at
   the size the cover set. .home-stage clips it, so nothing reaches the
   scrollbar.
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  .current-issue__stage {
    height: var(--issue-cover-height);
  }

  /* The desktop cap is the window; here the stage's own 200% is the whole
     answer and the overhang is the point. */
  .issue-spreads {
    max-width: none;
  }
}

/* ==========================================================================
   Home: the About blurb

   A photograph the width of the window with the magazine's statement read out
   of the bottom of it. The picture is the only cropped one on this page — it is
   asked to fill the width, which no file does at every window, so it covers.

   Both edges are washed out in the page colour rather than cut: a full-bleed
   photograph on a page that has no other hard horizontal line would draw two
   of them. The bottom wash is doing a second job as well — it is the ground the
   headline is read against, which is why the words overlap the picture instead
   of following it.

   The words are opted into the site's ordinary scroll reveal (.reveal-flow), so
   they rise in on contact like every other run of type. What isn't ordinary is
   the picture, which initHomeAbout() takes down as the reader scrolls past it —
   the statement doesn't so much arrive as get uncovered.
   ========================================================================== */
.home-about {
  /* Deliberately outside .container: this section is the width of the window,
     not of the 1440 column the rows above and below it sit in. */
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.home-about__frame {
  position: relative;
  width: 100%;
  /* 952px at the Figma's 1440 artboard. Tied to the viewport below that so a
     narrow window gets a band of photograph rather than the same 952px eating
     three screens of a phone. */
  height: clamp(320px, 66vw, 952px);
  /* The washes are painted in the page colour and have to have the page colour
     to fade *to*; without this they would resolve against whatever the browser
     is painting behind the section. */
  background: var(--color-bg);
}

.home-about__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Written by initHomeAbout() on every frame of the scroll past. */
  will-change: opacity;
}

/* The two washes. 320px in the Figma at both ends; kept proportional to the
   picture so the top of a short one isn't entirely gradient. */
.home-about__fade {
  position: absolute;
  left: 0;
  width: 100%;
  height: min(320px, 34%);
  pointer-events: none;
}

.home-about__fade--top {
  top: 0;
  background: linear-gradient(to bottom, var(--color-bg), transparent);
}

.home-about__fade--bottom {
  bottom: 0;
  background: linear-gradient(to top, var(--color-bg), transparent);
}

.home-about__content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  /* Lifted into the bottom wash rather than placed under the picture. Half the
     wash's height, per the Figma: enough that the headline is unmistakably part
     of the photograph, not so much that it is read over anything still busy. */
  margin-top: max(-160px, -11.11vw);
  padding-inline: var(--grid-margin);
  /* Ten of the twelve columns, the same measure the About page's headline
     takes — this is the same sentence in the same voice, and it should break
     the same way. */
  width: calc(
    (min(100vw, var(--container-max)) - 2 * var(--grid-margin) - 11 * var(--grid-gutter)) / 12 * 10
    + 9 * var(--grid-gutter)
    + 2 * var(--grid-margin)
  );
  max-width: 100%;
  text-align: center;
}

/* The same pill as the one that names a photograph on the About page. Not
   sharing that class: this one sits in the flow and is read as part of the
   section, where .about-credit is fixed to the window and fades with whatever
   picture is being held. Same face, same colours, different job. */
.home-about__credit {
  margin: 0;
  padding: var(--space-1) var(--space-2);
  border-radius: 99px;
  background: var(--color-text);
  color: var(--color-bg);
  font-family: var(--font-mono);
  font-size: 0.75rem;   /* 12px */
  line-height: 1.333;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.home-about__headline {
  margin: 0;
  font-family: var(--font-display);
  /* 64px at 1440 in the Figma, against the About page's 72px — the homepage
     gives this sentence less room, and the type steps down with it rather than
     taking more lines. */
  font-size: clamp(1.75rem, 4.44vw, 4rem);
  line-height: 0.875;   /* 56px leading at 64px, per the Figma */
  font-weight: 400;
}

.home-about__cta {
  margin: 0;
}

/* ==========================================================================
   Home: the rows under the About blurb

   "Latest Stories" and "New in the Shop" are the same object twice — a heading,
   a row of four on the 12-col grid, and one way through to the rest. Only the
   cards inside differ, so the frame is written once here.
   ========================================================================== */
.home-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-8);
  padding-block: var(--space-15);
}

/* The two rows sit directly on each other, so the second one's own top padding
   would double the gap the Figma draws between them. */
.home-row + .home-row {
  padding-top: 0;
}

.home-row__heading {
  margin: 0;
  font-family: var(--font-display);
  font-size: 2.5rem;   /* 40px */
  line-height: 1.2;
  font-weight: 400;
  text-align: center;
}

/* .container puts the margins back on: the row is a flex column so the grid
   inside it can't inherit them from a wrapper the way the issues row does.

   The row-gap is here rather than on the cards because .home-row's own 64px is
   measuring from the *edge* of the grid: a bottom margin on the last row of
   cards would be added to it, and the gap under the row would quietly become
   112px. Cards drop their margin inside this grid and the grid spaces its own
   rows, which it only has more than one of below 1024px. */
.home-row__grid {
  width: 100%;
  row-gap: var(--space-6);
}

.home-row__grid .story-card,
.home-row__grid .shop-card {
  margin-bottom: 0;
}

.home-row__cta {
  margin: 0;
}

/* --------------------------------------------------------------------------
   A product in the shop row

   Shaped like a story card and deliberately not one: it carries a price where
   that carries a section, and it leaves the site when clicked.
   -------------------------------------------------------------------------- */
.shop-card {
  display: block;
  margin-bottom: var(--space-6);
  color: inherit;
  text-decoration: none;
}

/* Same two-across step the story cards take: four 24px titles in a row under
   ~1000px lands each one in about 180px, which is not a column. */
@media (min-width: 768px) and (max-width: 1023px) {
  .shop-card.col-span-3 {
    grid-column: span 6;
  }
}

.shop-card__image {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: var(--space-4);
  /* Named for the same reason as .story-card__image's — see the .is-revealed
     rule that restates it. */
  --card-image-transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  transition: var(--card-image-transition);
}

a.shop-card:hover .shop-card__image,
a.shop-card:focus-visible .shop-card__image {
  transform: scale(1.04);
}

@media (prefers-reduced-motion: reduce) {
  .shop-card__image {
    transition: none;
  }

  a.shop-card:hover .shop-card__image,
  a.shop-card:focus-visible .shop-card__image {
    transform: none;
  }
}

.shop-card__price {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.shop-card__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.5rem;   /* 24px */
  line-height: 1.333;
  /* The display face has no bold cut, so left alone the browser would
     synthesise one over the heading default. */
  font-weight: 400;
}

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer {
  padding-bottom: var(--space-15);
}

.site-footer .footer-inner {
  max-width: var(--footer-max);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-10);
  text-align: center;
}

/* Block so the anchor wraps the mark exactly, rather than picking up inline
   line-height and sitting on a few pixels of dead space. */
.footer-logo-link {
  display: block;
}

.footer-logo {
  width: 219px;
  height: auto;
}

/* The wordmark's secret, on the mark at the foot of the page: a chain of circles
   trails the cursor across the logotype and fuses with it. See initFooterLogo() in
   main.js for why this one filters the letters themselves while the header filters
   only an overlay — up there the drawing is dots the trail can pick up, down here
   it is solid letterforms with nothing to lift. The blur is kept small for the same
   reason: a goo filter can't tell a blob near a stroke from two letters near each
   other, and at any real strength this logotype closes up into one shape. */
.footer-logo-svg {
  display: block;
}

.footer-logo-goo-group {
  filter: url(#footer-logo-goo);
}

.footer-logo-blob {
  fill: #1D1C1B;
  pointer-events: none;
}

/* No reduced-motion rule to match the wordmark's: there, the mark is inlined either
   way and only the trail is skipped, so the filter has to be turned off in CSS. Here
   initFooterLogo() returns before it inlines anything, and the logo stays the plain
   <img> that is in the markup — nothing to unfilter. */

.subscribe-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  max-width: 450px;
}

.subscribe-form input {
  flex: 1 0 0;
  min-width: 0;
  border: 0;
  border-bottom: 1px solid var(--color-text);
  background: transparent;
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  color: var(--color-text);
  padding: 0;
}

.subscribe-form input::placeholder {
  color: var(--color-text);
  /* The visible label for this field — the real <label> is visually hidden —
     so it has the least room of anything here to be quiet. */
  opacity: var(--quiet);
}

.subscribe-form button {
  flex-shrink: 0;
}

.footer-links,
.footer-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-10);
}

/* Keep the meta row (copyright + legal links) on one line down to the
   point where it genuinely can't fit — only "Back to top" is meant to
   sit on its own line below it. */
.footer-meta {
  flex-wrap: nowrap;
}

.back-to-top {
  display: inline-block;
}

/* ==========================================================================
   Story hero
   Sticky within a taller wrapper: the hero pins at the top of the viewport
   for an extra stretch of scroll (see initStoryHero() in main.js), during
   which the text fades/drifts up and out and the hazy image sharpens.
   Once that scroll distance is used up, the wrapper ends and the hero
   releases to scroll away normally with the rest of the page. Percentages
   on the text match the Figma frame's 1440x960 proportions.
   ========================================================================== */
.story-hero-wrapper {
  position: relative;
}

.story-hero {
  position: sticky;
  top: 0;
  width: 100%;
  height: 100vh;
  overflow: hidden;
}

.story-hero__bg {
  position: absolute;
  inset: 0;
}

.story-hero__gradient {
  position: absolute;
  inset: 0;
  background: var(--story-gradient, linear-gradient(to bottom, var(--story-color), var(--color-bg)));
}

.story-hero__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The "lighten" blend can't be smoothly transitioned on its own
   (mix-blend-mode isn't animatable), so this crossfades to a plain copy of
   the same image underneath — but both layers share the same 40px->0px
   blur ease (blur, unlike blend mode, *is* animatable), so the whole
   crossfade reads as one continuous dreamy-to-sharp blur rather than a
   crisp layer just fading in underneath a blurry one. See initStoryHero(). */
.story-hero__image--hazy {
  filter: blur(40px);
  mix-blend-mode: lighten;
  will-change: opacity, filter;
}

.story-hero__image--sharp {
  filter: blur(40px);
  opacity: 0;
  will-change: opacity, filter;
}

.story-hero__content {
  will-change: opacity, transform, filter;
  position: absolute;
  inset: 0;
  /* Per-story: sets where the title breaks across lines, which is an art
     direction call rather than something to derive from the text length.
     Override via --story-title-width on <body> (a unitless number of px at the
     1440px baseline — not a length — so it can be scaled below). Below 1440px
     this holds flat at that px value; above it, it grows with the viewport at
     the same rate as the title's font-size, so the line breaks it was tuned for
     don't shift on wide screens.

     Declared here rather than at :root because --story-title-width is set on
     <body>, and a :root declaration would resolve it against :root — where it
     isn't set — and silently freeze every story at the 447 default. Two boxes
     need it: the headline, and the plain hero's title, which is handed back to
     the flex column and so has to carry its own measure. */
  --hero-title-measure: min(
    max(
      calc(var(--story-title-width, 447) * 1px),
      calc(var(--story-title-width, 447) * 100vw / 1440)
    ),
    85vw
  );
}

/* Sits on the title, not on the frame: bottom: 100% is the top edge of
   .story-hero__headline, which is the title's own box, so however many lines the
   title runs to the tag stays the same distance above its first one. It used to
   be pinned near the top of the hero at 7.5%, a long way from the thing it
   labels, and needed a rule of its own to dodge the revealed nav bar — which is
   why that rule has gone with it. */
.story-hero__tag {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin: 0 0 var(--space-3);
  font-family: var(--font-mono);
  /* The small mono-caps register — 12/16, the same as .story-card__tag, which
     is this very tag on the homepage cards, and as the caption and issue
     labels. It was on the nav's 16/24, which is the size for things you click,
     not for a label sitting on a photograph. */
  font-size: 12px;
  line-height: 16px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  color: var(--color-text);
  white-space: nowrap;
}

/* The tag names the section, so it goes to it: the Stories page with that
   section already filtered (synonym_section_url()). Undecorated at rest — it's
   a label first — and on hover it takes the same underline that grows
   left-to-right as the nav, the footer and the bylines, which is written once
   up at the top of this file. */
a.story-hero__tag {
  text-decoration: none;
}

/* The title's box, and the tag's anchor. It carries the placement the title used
   to do itself; the title inside it is back in normal flow, and the tag hangs off
   the top of it. Nothing is centred here but the box, so the title lands exactly
   where it always did — the tag is out of flow and adds no height. */
.story-hero__headline {
  position: absolute;
  /* top marks the vertical center, not the top edge (see transform below)
     — offset by one line's height at the 1440px baseline so the anchor
     matches where the old top-edge positioning landed for the default
     2-line title. Needed because line count and font-size both now vary
     with viewport width; anchoring from the top edge let the box's
     center drift whenever it grew taller or wrapped to more lines. */
  top: calc(39.2% + 88px);
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  width: var(--hero-title-measure);
  /* A floor at the longest word. A line box wider than its container is laid
     out from the start edge and hangs off the end — text-align: center can't
     pull it back — so a title measure narrower than one of its own words
     leaves that line sitting visibly right of the others. Whatever the
     measure is set to, the box is never allowed under the width the text
     needs to not overflow. min-content only ever grows to the longest
     unbreakable run, so the editor's line breaks stand. */
  min-width: min-content;
}

.story-hero__title {
  margin: 0;
  font-family: "Matter", serif;
  font-weight: 400;
  font-size: clamp(2.5rem, 6.67vw, 9rem); /* 96px at the 1440px baseline, keeps growing to ~144px by ~2160px wide */
  line-height: 0.917; /* 88px at 96px */
  text-align: center;
  color: var(--color-text);
}

/* Reused wherever a word gets the decorative dot-glyph treatment: hero
   title, up-next title, homepage story cards. */
.title-connector {
  font-family: var(--font-accent);
  font-weight: 400;
}

/* Browsers happily break a line after a hyphen, which turns a hyphenated
   title word into two — wrap that word in this to keep it whole and let the
   break fall between words instead. Art direction, same as
   --story-title-width: where a title breaks is a decision, not an outcome. */
.title-nobreak {
  white-space: nowrap;
}

/* Anchored from the bottom rather than the top, so the space under it is the
   thing that stays fixed and a long dek grows upward into the frame instead of
   downward out of it.

   Off a top anchor the gap below was whatever was left after the wrapping, and
   for a dek of any length there wasn't any: the In-betweenness standfirst runs
   to five lines and finished 55px past the bottom edge of a 900px hero, before
   the window is narrowed at all. Narrower windows only wrap it further.

   8.3% is the split hero's number, which was taken off the Figma frame — it sat
   the same distance off the bottom as the tag does off the top. The two heroes
   now anchor their dek the same way, which is why the split's own copy of this
   rule has gone.

   It also picks up the svh correction on .story-hero__content below: on a phone
   the content box stops short of the browser's toolbar, so a bottom-anchored
   dek clears the chrome where a top-anchored one never knew about it. */
.story-hero__dek {
  position: absolute;
  bottom: 8.3%;
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  /* Font-size is frozen past 1440px (see below), but the box itself keeps
     growing with the viewport so it doesn't start wrapping unnecessarily
     as the hero gets wider — same technique as the title's width. */
  width: min(max(431px, calc(431 * 100vw / 1440)), 85vw);
  font-family: "HAL Timezone", serif;
  font-size: clamp(1rem, 1.67vw, 1.5rem); /* 24px at the 1440px baseline */
  line-height: 1.333; /* 32px at 24px */
  text-align: center;
  color: var(--color-text);
}

@media (prefers-reduced-motion: reduce) {
  .story-hero {
    position: relative;
  }
}

/* --------------------------------------------------------------------------
   Split hero
   The exact same three layers as above — wash, hazy/blended copy, sharp copy
   — but arranged as a diptych instead of a crossfade: each half of the frame
   is a pane framing its own copy of the picture, dreamy on the left with the
   wash and the text over it, sharp on the right. Both states are on screen
   at once, so there is nothing left to dissolve between.

   Which is why nothing here moves relative to anything else on scroll. The
   diptych *is* the composition, so it leaves as one: the text goes first,
   then the whole picture behind it. See initSplitHero() in main.js.
   -------------------------------------------------------------------------- */
.story-hero--split .story-hero__pane {
  position: absolute;
  top: 0;
  bottom: 0;
  /* Each half clips its own picture, which is what keeps the diptych two
     pictures rather than one smeared one.

     A CSS blur doesn't stay inside the box it's drawn in — it samples outwards,
     and the 40px the hazy copy rests at carries something like 120px past its
     own edge. Unclipped, that means the left pane's blur lies across the right
     pane and, during the load-in, the right pane's 64px starting blur lies back
     across the left: two spreads overlapping in a band down the centre of the
     frame, which is the smear on the seam. The clip stops each side at the
     middle. */
  overflow: hidden;
}

.story-hero--split .story-hero__pane--hazy {
  left: 0;
  right: 50%;
}

.story-hero--split .story-hero__pane--sharp {
  left: 50%;
  right: 0;
}

.story-hero--split .story-hero__image--sharp {
  opacity: 1;
  filter: none;
}

/* Text moves off the frame's center onto the left pane's center; the width
   clamps come down from 85vw to roughly that pane's share of the viewport
   so nothing can spill across the seam. */
.story-hero--split .story-hero__headline,
.story-hero--split .story-hero__dek {
  left: 25%;
}

.story-hero--split .story-hero__headline,
.story-hero--split .story-hero__dek {
  max-width: 40vw;
}

/* Too narrow to read as a diptych — both panes go full-bleed and what's left
   is the full-screen hero's layout: the hazy image over the wash with the text
   centred on it. The sharp copy starts hidden rather than dropped, because
   scroll resolves the hazy one into it here just as it does on the full-screen
   hero — initSplitHero() reads the same breakpoint and drives the two copies
   directly in this mode. Left at 0 the whole way with no JS, which is the right
   resting state: the picture simply stays dreamy. */
@media (max-width: 900px) {
  .story-hero--split .story-hero__pane {
    left: 0;
    right: 0;
  }

  .story-hero--split .story-hero__image--sharp {
    opacity: 0;
  }

  .story-hero--split .story-hero__headline,
  .story-hero--split .story-hero__dek {
    left: 50%;
  }

  .story-hero--split .story-hero__headline,
  .story-hero--split .story-hero__dek {
    max-width: 85vw;
  }
}

/* --------------------------------------------------------------------------
   Plain hero
   No picture at all — the tag/title/dek sit straight on the page background.
   With nothing to fill the frame there is also nothing to pin: unlike the
   other two heroes this one is an ordinary block in the flow, sized to its
   own text, scrolling away with the page. So all the absolute positioning
   the others use to hit percentage marks on a 100vh frame gets unwound here
   and replaced with a plain centred column on the Figma frame's 72px rhythm.

   The block that follows it starts 72px below rather than the flow's usual
   120px — see the rule under .story-content-flow. The dek and the first block
   are the same thought here ("check out our video interview", and then the
   film), not two things separated by a chapter break, and the tighter gap is
   also what lifts the block far enough up the first screen to reveal itself
   without the reader scrolling.
   -------------------------------------------------------------------------- */
.story-hero--plain {
  position: static;
  height: auto;
  overflow: visible;
  padding-top: var(--space-10);
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

.story-hero--plain .story-hero__content {
  position: static;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* The tag's distance from the title, and the image heroes' — where the tag
     hangs off the top of the title's box on a 24px margin (.story-hero__tag).
     It was the 72px rhythm, the same as everything else in this column, which
     left the section floating three times as far from the title it labels as
     it does on every other hero. The rhythm itself is unchanged: the dek puts
     the rest of it back below. */
  gap: var(--space-3);
}

/* No box: display: contents drops it out of the layout and hands the tag and the
   title straight back to the flex column, which is where the plain hero already
   put them and already spaces them. Nothing else in this variant has to know the
   wrapper exists. */
.story-hero--plain .story-hero__headline {
  display: contents;
}

/* A box set to display: contents has no width, so the measure it was carrying
   has to be re-applied to the title that is now a flex item in its own right.
   Same custom property, so there's still only one copy of the formula. */
.story-hero--plain .story-hero__title {
  width: var(--hero-title-measure);
  min-width: min-content;
}

.story-hero--plain .story-hero__tag,
.story-hero--plain .story-hero__title,
.story-hero--plain .story-hero__dek {
  position: static;
  top: auto;
  left: auto;
  transform: none;
  /* The column's own gap and the dek's margin below do the spacing here — the
     tag's own margin is for holding it off the title when it's hanging above
     one. */
  margin: 0;
}

/* The gap above holds the tag close to the title, so the 72px the column is
   drawn on has to be made up here: what the gap has already given, plus this,
   is the rhythm the block below it also answers to. */
.story-hero--plain .story-hero__dek {
  margin-top: calc(var(--space-9) - var(--space-3));
}

/* The tag sits far higher here than on the image heroes — high enough that
   the revealed nav bar would land right on it. Shifting the whole section
   with a transform rather than nudging the tag on its own keeps the 72px
   rhythm intact, and a transform doesn't reflow the page to do it.

   The flow below has to travel with it. It used to be left where it was, on
   the reasoning that not moving is cheaper than moving — which held only
   while the gap under the dek was wide enough to swallow 80px of hero. At
   the 72px this hero now uses it isn't: the dek came down onto the top edge
   of the video every time the nav revealed on a scroll up. Moving both by
   the same amount keeps every gap between them exactly as drawn, and still
   reflows nothing. What the flow's tail runs into is its own 120px of
   bottom padding, which is empty, so the footer is untouched. */
body.nav-revealed .story-hero--plain,
body.nav-revealed .story-hero--plain + .story-content-flow {
  transform: translateY(80px);
}

/* --------------------------------------------------------------------------
   Scroll reveal
   The site's ordinary load-in: type fades and drifts up, pictures blur in, as
   each comes into view. Any container that says .reveal-flow gets it, which is
   how a page opts in — see initScrollReveal() in main.js.

   Given its own sub-banner because the shop lifts this block by name into its
   own stylesheet (bin/build-shopify-chrome.sh). It sits under Story hero for
   historical reasons — it started there — but nothing in it is a story's.
   -------------------------------------------------------------------------- */

/* Both entrances below — the hero intro and the scroll reveal — start by
   hiding things from JavaScript, which means they can only hide them once
   main.js has arrived and run. Paint doesn't wait for that: on a cold cache
   the finished page gets shown for the length of the script fetch and is
   then pulled back to nothing, which reads as a flash of the whole story
   before the intro starts.

   So the hidden state has to be in the stylesheet, switched on by an inline
   script in each story page's <head> — that runs before any of the body
   below it is parsed, so the very first paint is already primed. main.js
   drops the flag once it has set the real starting states (and the inline
   script arms a timeout to drop it anyway, so a script that never arrives
   can't strand the page). No JS at all and the flag is never set, which
   leaves the page plainly visible — the right fallback. */
.is-priming .story-hero__content,
.is-priming .story-content-flow,
.is-priming .reveal-flow {
  visibility: hidden;
}

/* Scroll-triggered reveal: text fades + drifts up, images blur in. Classes
   are added dynamically to content within .story-content-flow by
   initScrollReveal() in main.js (via IntersectionObserver), not written
   into the markup by hand — see the note there for what's excluded and
   why.

   The transition lives on the revealed state rather than the hidden one, so
   it only ever runs in the arriving direction. Going the other way — the
   moment the script adds the hidden class to content that is currently
   painted — is instantaneous instead of a half-second fade to nothing. That
   never showed on the image heroes, where every reveal target starts below
   the fold, but the plain hero puts the first block on screen at load, where
   a transition here reads as the picture leaving before it arrives. */
.reveal-text {
  opacity: 0;
  transform: translateY(16px);
}

.reveal-text.is-revealed {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.reveal-image {
  opacity: 0;
  filter: blur(24px);
  /* Named for the same reason as --card-image-transition: so the rule below
     can add to it without a second copy of these numbers. */
  --reveal-transition: opacity 0.6s ease, filter 0.9s ease;
}

.reveal-image.is-revealed {
  opacity: 1;
  filter: blur(0);
  transition: var(--reveal-transition);
}

/* A card image that is also a reveal target — every story card on a
   contributor's page, and on the homepage every issue cover in the back
   catalogue and every product in the shop row, all of which sit in .reveal-flow.

   `transition` is one property, so the rule above doesn't extend the card's
   transition, it replaces it: transform drops out of the list the moment the
   card has revealed, and the hover scale stops animating and simply snaps to
   1.04. Both transitions are named above so this can be the two of them
   together rather than a third copy of either.

   Before the reduced-motion block below on purpose — that has to keep the last
   word, and at equal specificity the last word is source order. */
.story-card__image.is-revealed,
.shop-card__image.is-revealed,
.issue-card__cover img.is-revealed {
  transition: var(--reveal-transition), var(--card-image-transition);
}

@media (prefers-reduced-motion: reduce) {
  .reveal-text.is-revealed,
  .reveal-image.is-revealed {
    transition: none;
  }
}

/* ==========================================================================
   Story content blocks
   A reusable set of block types matching the Figma content rows — this is
   the layout vocabulary the eventual WordPress blocks will map onto.
   Vertical rhythm (the 120px gap between every block) lives once on the
   flow container rather than being repeated per block.
   ========================================================================== */
.story-content-flow {
  display: flex;
  flex-direction: column;
  gap: var(--space-15);
  margin-top: var(--space-15);
  padding-bottom: var(--space-15);
}

/* After a plain hero the 120px above closes to 72px — the same step the plain
   hero uses between its own tag, title and dek, so the first block reads as the
   next line of that column rather than as the start of a separate section. It
   also brings the block up the page, which is what gets it far enough into view
   at load for initScrollReveal() to fade it in where the reader can see it. */
.story-hero--plain + .story-content-flow {
  margin-top: var(--space-9);
  /* Same curve and duration as the hero's own — the two move as one thing when
     the nav reveals. See the rule that shifts them both. */
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

/* A run of text blocks is one piece of writing, not several.

   The 120px above is the gap between *things* — a picture, a pullquote, a
   recipe. Copy and Interview blocks get broken into several for reasons that
   have nothing to do with the writing: each margin image needs its own block to
   hang off, so an interview with four pictures beside it is four blocks, and at
   120px apart it reads as four separate pieces rather than one conversation.

   So when a text block follows a text block, the gap comes down to 48px. Still
   a break — it's 1.5x the space between paragraphs inside the copy — but a
   break within something, rather than between things. Anything that isn't a
   text block (a picture, a pullquote) is a different class and doesn't match,
   so the full 120px still opens up around it.

   Expressed as a negative margin because the 120 is a flex `gap`, which can't
   be overridden per pair; margins add to it, so this subtracts the difference.
   The paywall teaser carries --content too, which is right: it's the copy
   fading out, not a new thing. */
.story-block--content + .story-block--content,
.story-block--content + .story-block--final {
  margin-top: calc(var(--space-6) - var(--space-15));
}

/* Added by initSplitHero(), which also sets the negative top margin that
   pulls the flow up into the hero's pinned range, so the page rises over
   the picture while that fades out instead of waiting below for it to
   finish. Deliberately left transparent — the story reads straight over
   the picture for the length of the overlap, and the picture fading out
   underneath is what resolves it. All this needs is to paint above the
   hero, which is sticky and therefore positioned, and so would otherwise
   sit on top of in-flow content. Well below the pinned nav's z-index. */
.story-content-flow--overlaps {
  position: relative;
  z-index: 1;
}

/* Unlike the sitewide .container (capped at --container-max), content
   rows inside a story scale fluidly with the viewport — fixed side
   margins, but the 12-col grid tracks grow with the screen so both text
   and images keep getting bigger past 1440px, matching kinfolk.com. */
.story-content-flow .container {
  max-width: none;
  /* And full width, explicitly.
  
     .container carries margin-inline: auto to centre itself once it hits
     --container-max. Inside the flow that cap is lifted, so the auto margins have
     nothing left to centre — but they are still auto, and the flow is a column
     flexbox. A flex item with an auto margin on the cross axis does not stretch:
     it sizes to fit-content and the margins centre what's left over.
  
     Which is invisible until the window is wide enough that a block's content
     stops filling it. Below about 2000px every block's longest line is wider than
     the screen, so fit-content clamps to the full width and the shrinking never
     shows. On a 27-inch display it does, and only for the blocks whose copy
     happens to be short — one entry in a round-up sitting indented and narrower
     than the eight around it, for no reason a reader could work out.
  
     An explicit width takes the sizing away from fit-content, and the auto margins
     resolve to zero on their own. */
  width: 100%;
}

/* Rail grid: on the real 12-column grid (same tokens as the sitewide
   .grid utility) — rail = 2 cols, gap = 1 col, article = 6 cols, gap =
   1 col, rail = 2 cols. Confirmed against the Figma frame: the rail/gap/
   article pixel widths at the 1360px content width match this exactly. */
.story-block {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
  align-items: start;
}

.story-rail--left {
  grid-column: 1 / span 2;
}

/* An opening with no opening paragraph: byline in the left rail, maybe an issue
   card in the right, and nothing in the measure between them. Left alone, that
   empty measure still costs the block its rails' height plus the flow's 120px
   gap, so the first real block starts a long way under a credit sitting on its
   own.

   Given no height and a negative margin cancelling the gap, the block below
   starts exactly where this one did — level with the byline — and the rails hang
   down beside it. Which is where they'd have been all along if the lead had been
   written: this is what the block looks like with its middle column filled in.

   Two things it stands down for, both because they'd collide rather than sit
   alongside:

   - a next block carrying its own rail content (a margin picture on the first
     block, say), which would land in the same two columns as the byline;
   - the paywall, which is already pulled up by its own fade — the two negative
     margins would compound and drag it over the hero.

   Both guards are :has(), so a browser without it simply keeps today's layout.
   Below 901px nothing applies: the rails are full-width rows there, stacked in
   order, and there is no margin to sit in. */
@media (min-width: 901px) {
  .story-block--rails-only:not(:has(+ .story-paywall)):not(:has(+ * .story-rail > *)) {
    height: 0;
    margin-bottom: calc(var(--space-15) * -1);
  }
}


.story-copy,
.story-lead {
  grid-column: 4 / span 6;
}

.story-rail--right {
  grid-column: 11 / span 2;
}

/* A margin picture shouldn't set the pace of the writing.

   .story-block sizes its row to its tallest item, so a rail picture taller than
   the paragraph beside it leaves the block running on past the end of its own
   text — and the reader, who is in the middle column, sees that as gap. It
   doesn't show at laptop widths, where the measure is narrow enough that the
   copy is the taller of the two. Past about 1440 the grid keeps growing while
   .story-copy stays at 20/32, so the measure widens, the paragraphs get shorter,
   and the picture wins. On a 2560 screen this story ran 296px between two
   paragraphs of one interview where the gap is meant to be 48.

   So inside a run of text blocks the rail stops contributing height and the
   picture hangs below, beside the copy that follows — which is what a margin
   figure does on paper. The copy alone sets the rhythm and the 48px above is
   the gap the reader actually sees.

   :has() is doing something specific: only a block with another text block
   after it lets its rail overflow, because only then is there a column of copy
   for the picture to sit beside. The last block of a run keeps its rail sizing
   it, so an overflowing picture can't drop into the full-width photograph or
   the pullquote that follows. Where :has() isn't supported nothing matches and
   the rail sizes the block as it did before.

   Above the stacking breakpoint only. Below it the rails are in the flow at
   grid-column 1, and a zero-height rail would put the picture through the copy.

   The one arrangement this can't help: two blocks in a row with margin pictures
   on the *same* side, where the first picture is taller than the whole block
   after it. Then it reaches the second picture. The side is the editor's
   choice, so it can be arranged, and alternating sides — which is what the
   stories do — can't collide at all. */
@media (min-width: 901px) {
  .story-block--content:has(+ .story-block--content) .story-rail,
  .story-block--content:has(+ .story-block--final) .story-rail {
    height: 0;
  }
}

/* Section heading inside a copy block: the body face at 32/40, on the same
   measure as the copy under it and set clear of it by one body line — the
   Figma frame leaves an empty 32px line between the two. Plain weight rather
   than the browser's bold h2, which is the only thing that would set it apart
   in Matter Regular. */
/* In the story's accent, like the pullquote — the two are the same size and the
   same job, a mark in the flow of the copy rather than something to read on its
   own. Note that an accent this pale carries less contrast than body copy: see
   the --story-color token for the numbers. */
.story-heading {
  margin: 0 0 var(--space-4);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 32px;
  line-height: 40px;
  color: var(--story-color);
}

.story-copy p {
  margin: 0 0 var(--space-4);
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 32px;
  color: var(--color-text);
}

.story-copy p:last-child {
  margin-bottom: 0;
}

/* ---- Paragraph styles inside a Copy block --------------------------------
   Set from the Formats menu on the block's own field rather than from separate
   CMS fields, so a block can carry as many of these as the copy needs, in any
   order — see synonym_copy_formats(). The classes land on the paragraphs
   themselves and everything below is ordinary body copy.
   -------------------------------------------------------------------------- */

/* A name at the head of a passage. The body face two steps up, in the page's own
   ink — not .story-heading, which is smaller and set in the story's accent
   because it labels a section rather than naming somebody. */
/* Qualified with the element, because the rule these override — .story-copy p —
   carries a type selector and would otherwise win on specificity alone. */
.story-copy p.story-copy__title {
  font-size: 40px;
  line-height: 48px;
  /* Tight to the label under it: the two are one unit, and the 32px that
     separates paragraphs would break them apart. */
  margin-bottom: var(--space-1);
}

/* The line under a name — where somebody is from, what they do. The site's label
   type, at the smallest size it is set anywhere. */
.story-copy p.story-copy__label {
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 16px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

/* A name arriving after a passage of copy opens a new one, so it gets the gap
   between passages rather than the gap between paragraphs. Adjacent margins
   collapse, so this replaces the paragraph's own 32px rather than adding to it. */
.story-copy p + p.story-copy__title {
  margin-top: var(--space-10);
}

/* Two uses of one recipe: the opening few words of a section, and the
   speaker label that opens each turn in an interview-style story (see
   .story-copy p in story-shave.html). Both are set inline ahead of the body
   text, both keep the paragraph's own line-height so the baseline doesn't
   shift under them. */
.story-copy__lead-in,
.story-speaker {
  font-family: var(--font-display-heavy);
  font-size: 16px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

/* Source note / aside hanging off the end of a content block — same column
   as the body copy, but set back at the small size. The extra top margin
   stacks on .story-copy p's own 32px bottom margin for the 80px gap the
   Figma frame has. Doubled up with the .story-copy prefix so it outranks
   .story-copy p's body size when it sits inside one. */
.story-footnote,
.story-copy .story-footnote {
  margin-top: var(--space-6);
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
}

.story-lead {
  margin: 0;
  font-family: var(--font-display);
  font-size: 32px;
  line-height: 40px;
  color: var(--color-text);
}

/* The lead can be a single <p class="story-lead"> or, where it needs to
   break across paragraphs, a wrapping <div> holding several — the type is
   set on the container either way. */
.story-lead p {
  margin: 0 0 var(--space-4);
}

.story-lead p:last-child {
  margin-bottom: 0;
}

/* The heavy-caps opening. Usually the first few words of a lead, but it
   also takes a whole paragraph where the story opens on a quotation (see
   story-shave.html). */
.story-lead__intro {
  font-family: var(--font-display-heavy);
  font-size: 24px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

/* Source credit closing a quoted opening — breaks onto its own line and
   drops out of the heavy caps back to plain body type, so it reads as
   attribution rather than as more of the quote. Both the casing and the
   tracking have to be undone explicitly, since they inherit from the
   paragraph above. */
.story-lead__attribution {
  display: block;
  font-family: var(--font-display);
  font-size: 20px;
  letter-spacing: normal;
  text-transform: none;
}

/* No rules for the quotation opening (.story-lead--quote), on purpose.
   It used to take the story's accent, with the attribution explicitly pulled back
   to ink to undo the inherit. Both are gone: the opening is set in ink like the
   rest of the lead, and the accent is carried by the pullquote, the captions and
   the end mark instead.

   The modifier is still on the markup and now styles nothing — it's a hook, and
   the layout comes from .story-lead. Any paragraph following the quote needs no
   rule either: .story-lead sets 32/40 on the container, so a plain <p> inherits
   the lead size already. */

.story-byline p {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
  color: var(--color-text);
}

.story-byline p:last-child {
  margin-bottom: 0;
}

.story-byline__label {
  display: block;
}

/* Set like a primary nav link — same face, size and tracking. The hover
   underline comes from the shared nav-link rules above, which already list
   .story-byline__name a; it is the link that carries it, not the name, so an
   unlinked contributor is not underlined for decoration. */
.story-byline__name {
  display: block;
  font-family: var(--font-mono);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.story-buy-issue {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  text-decoration: none;
  color: inherit;
}

.story-buy-issue img {
  flex: 1 0 0;
  min-width: 0;
  aspect-ratio: 900 / 1181;
  object-fit: cover;
  filter: blur(5px);
  transition: filter 0.4s ease;
}

.story-buy-issue__text {
  flex: 1 0 0;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
  color: var(--color-text);
  opacity: var(--quiet);
  transition: opacity 0.4s ease;
}

/* Each part of "from / Issue #: / issue name" gets its own line
   regardless of available width — not just wherever the text happens to
   wrap. */
.story-buy-issue__line {
  display: block;
}

.story-buy-issue:hover img,
.story-buy-issue:focus-visible img {
  filter: blur(0);
}

.story-buy-issue:hover .story-buy-issue__text,
.story-buy-issue:focus-visible .story-buy-issue__text {
  opacity: 1;
}

.story-buy-issue__cta {
  display: block;
  text-decoration: underline;
}

.story-caption {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.story-caption__label {
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 16px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  color: var(--color-text);
}

.story-caption__text,
.story-caption--plain {
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
  color: var(--color-text);
  margin: 0;
}

/* The margin: a picture in either rail of a copy or interview block, with its
   caption under it in the same type as every other caption. Two columns wide, and
   no crop — the image keeps whatever shape it was uploaded at. */
.story-margin {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
}

.story-margin img {
  display: block;
  width: 100%;
  height: auto;
}

/* On the left the picture shares its column with the caption inherited from the
   photograph above the block. Set clear of it, so the two read as two things in
   one margin rather than as a caption belonging to the picture under it. */
.story-rail .story-margin:not(:first-child) {
  margin-top: var(--space-4);
}

/* --------------------------------------------------------------------------
   Round Up: an entry's picture, floating in the margin.

   A round-up is a run of short answers — a name, where they are, a sentence or
   two — and the pictures that go with them belong in the margins rather than in
   the column. Not in a rail, though: a rail picture is a grid item two columns
   wide, it holds the row open to its own height, and a dozen of them down a page
   would set the pace of the reading. This is taken out of the flow altogether.

   So the figure is absolutely positioned against its entry and the entry's height
   is the copy's alone. It hangs off the vertical centre of the answer, runs past
   the top and bottom of it, and laps a little way over the edge of the text —
   which is the whole point of a margin picture and is why it is *behind* the
   words (see the z-index below): where the two meet, the type wins.

   The horizontal anchor is the edge of the text column rather than the edge of
   the page, so the lap is an exact number of pixels at every width. There is no
   token for that edge — the copy is columns 4–9 of the block's own 12-column grid
   — so it is reckoned here from the same two grid tokens the grid is built from.
   One catch worth knowing: an absolutely positioned child resolves its
   percentages against the *padding* box, and .story-round-up is also .container,
   so 100% here is the content width plus both grid margins. Hence subtracting
   them back off to get to the track width.

   Sides alternate down the page and the sizes come off short ladders, both dealt
   in round_up.php. Each one then drifts against the scroll — see
   initRoundUpMargins() in main.js, which writes --round-up-drift and nothing
   else, so the resting position stays entirely in this file.
   -------------------------------------------------------------------------- */
.story-round-up {
  position: relative;

  /* The 12-column grid, reckoned in the coordinate space an absolutely
     positioned child sees. */
  --round-up-track: calc(
    (100% - 2 * var(--grid-margin) - 11 * var(--grid-gutter)) / var(--grid-columns)
  );
  /* The left edge of the copy: the grid margin, then three tracks and three
     gutters. The right edge is the same distance in from the other side, which is
     why one number places both margins. */
  --round-up-inset: calc(
    var(--grid-margin) + 3 * (var(--round-up-track) + var(--grid-gutter))
  );
  /* What a picture measures at --round-up-scale: 1. Fluid, like everything else
     in the story flow — the margin keeps growing past 1440px and the pictures
     grow with it — and capped so that a very wide window doesn't turn a margin
     note into a plate. */
  --round-up-base: clamp(150px, 19vw, 340px);
  --round-up-width: calc(var(--round-up-base) * var(--round-up-scale, 1));

  /* How far off its entry's centre line a picture may hang, either way. PHP deals
     each one a share of this between -1 and 1 (--round-up-drop); the number of
     pixels lives here because the entry's own floor has to allow for it too — see
     .story-round-up--pictured. */
  --round-up-swing: 36px;

  /* Anchored by its inner edge to the edge of the text and lapping over it by
     --round-up-lap, the picture then slides back out into the margin by a share
     (--round-up-out) of whatever room is left between it and the edge of the
     window. At 0 it hugs the column; at 1 its outer edge is at the edge of the
     page and the text is clear of it. That range is what stops a margin full of
     photographs from reading as a second column. */
  --round-up-shift: calc(
    max(0px, var(--round-up-inset) + var(--round-up-lap, 24px) - var(--round-up-width))
      * var(--round-up-out, 0)
  );
}

/* Above the pictures, and the only thing in the block that is. Everything the
   reader has to read stays legible where a photograph passes under it. */
.story-round-up .story-copy {
  position: relative;
  z-index: 1;
}

/* One answer to the next. A run of copy blocks closes to 48px because it is one
   piece of writing broken into blocks (see .story-block--content + …); a round-up
   is the other thing — a dozen separate people answering the same question, each
   with a photograph hanging off it. At 48 the pictures crowd the answer below and
   the entries read as paragraphs of one text. Not the full 120 either: that is the
   gap between a story's *sections*, and these are still a list.

   After the 48px rule in the file, and at the same specificity, so this is the one
   that lands. Same negative-margin arithmetic: the 120 is the flow's own flex gap
   and margins add to it. */
.story-round-up + .story-round-up {
  margin-top: calc(var(--space-12) - var(--space-15));
}

/* The picture hangs off the centre of its entry, and the entry has a floor that
   can make it taller than the answer inside it (see .story-round-up--pictured) —
   so left at the top, the words sat above the middle of their own photograph. This
   puts the answer back on the picture's centre line, which is where it was always
   meant to be, and splits the floor's spare height evenly above and below. */
.story-round-up--pictured .story-copy {
  align-self: center;
}

.story-round-up__figure {
  position: absolute;
  top: 50%;
  z-index: 0;
  margin: 0;
  width: var(--round-up-width);
  /* Never further out than the edge of the window. The margin is wide enough for
     the base size on any laptop, but a narrow window plus a wide ladder step
     would otherwise put a picture half off the page — and unlike the About
     scatter, which may overhang deliberately, these sit beside type that is
     still being read across from them. */
  max-width: calc(var(--round-up-inset) + var(--round-up-lap, 24px));

  /* Three things in one transform, in the order they were decided: the half of
     its own height that turns `top: 50%` into a centre line, the ladder step that
     lifts or drops it off that line, and the parallax, which is the only one of
     the three that moves. Written as a custom property by main.js precisely so
     the other two don't have to be — a transform written from JS would have to
     carry all three and would own the resting position as well as the motion. */
  transform: translate3d(
    0,
    calc(
      -50% + var(--round-up-drop, 0) * var(--round-up-swing) + var(--round-up-drift, 0px)
    ),
    0
  );
  /* Recomputed every frame while scrolling; easing toward a moving target only
     adds lag. Same reasoning as .about-scatter__thumb. */
  will-change: transform;
}

/* Anchored by its inner edge to the edge of the text, then allowed to run
   outward — so the lap over the column is the same on both sides whatever the
   picture measures. */
.story-round-up__figure--left {
  right: calc(100% - var(--round-up-inset) - var(--round-up-lap, 24px) + var(--round-up-shift));
}

.story-round-up__figure--right {
  left: calc(100% - var(--round-up-inset) - var(--round-up-lap, 24px) + var(--round-up-shift));
}

/* An entry holds itself open to most of the height of its own picture.

   A round-up is a run of two-line answers and the photographs are the tall thing
   on the page: left to themselves, three short entries in a row put three
   pictures through each other. So an entry with a picture is given a floor. It
   only ever *adds* height — an entry with more to say than its picture needs is
   already past this and nothing here touches it.

   Where the number comes from. Sides alternate, so two pictures in one margin
   belong to entries two apart, and each is centred on its own entry: between
   their centres is half of each entry, the entry in between, and two 48px gaps,
   and half of each picture has to fit in there. Three things then eat into that
   distance: the parallax takes 8–14% of it as the pair comes up the window (see
   the note on depth in round_up.php), the entry in between may be a short answer
   with no picture at all, and — the largest of the three — the upper picture may
   have been dealt a drop toward the lower one at the same time as the lower one
   was lifted toward it, which is the full swing twice over. 0.85 of the picture's
   height plus that swing, less half a gap, clears all three with room to spare at
   every shape a photograph is normally uploaded at. A picture more than about
   twice as tall as it is wide is the case it can't answer for; there the editor's
   own sense of how much to say is the guard.

   The picture's shape comes from PHP as --round-up-ratio — CSS can draw an image
   but it can't ask what shape it is. */
.story-round-up--pictured {
  --round-up-height: calc(
    var(--round-up-base) * var(--round-up-scale, 1) / var(--round-up-ratio, 1)
  );

  min-height: max(
    0px,
    calc(var(--round-up-height) * 0.85 + 2 * var(--round-up-swing) - var(--space-4))
  );
}

.story-round-up__image {
  display: block;
  width: 100%;
  height: auto;
}

.story-round-up__figure .story-caption--plain {
  margin-top: var(--space-2);
}

/* --------------------------------------------------------------------------
   Round Up, centred: the one entry worth stopping on.

   The picture leaves the margin, comes back into the flow above the answer and is
   set eight columns wide — a third wider than the measure, so it reads as a plate
   rather than as a note. Which makes it the one arrangement in this block that
   interrupts the run instead of decorating it, and the reason it is a setting on
   the Margin field rather than a block of its own: it is still one person's answer,
   and it still carries their name and where they are underneath.

   It keeps the drift and nothing else. No lap, no swing, no floor — those all place
   a picture in a margin, and this one has a place of its own on the page.
   -------------------------------------------------------------------------- */
.story-round-up--centred {
  row-gap: var(--space-6);
}

.story-round-up--centred .story-round-up__figure {
  position: static;
  /* Above the answer, which is after it in the markup — the words are what the
     entry is, and a reader on a screen reader should meet them first. */
  grid-row: 1;
  grid-column: 3 / span 8;
  width: auto;
  /* Eight columns is the slot; the picture is then held a little inside it. The
     grid has no symmetrical span between eight columns and the six of the measure
     — seven can't be centred on twelve — so this last bit of sizing is a
     percentage of the slot rather than a column count. It stays centred on the
     page either way. */
  max-width: 90%;
  margin-inline: auto;

  /* The drift alone. Nothing here is being centred on anything or held off a
     centre line, so the -50% and the swing that the margin pictures carry would
     simply be an offset from the slot the picture is sitting in. */
  transform: translate3d(0, var(--round-up-drift, 0px), 0);
}

.story-round-up--centred .story-copy {
  grid-row: 2;
}

/* Two ceilings, and the picture is fitted inside whichever it meets first.

   The width one is the guard the centred image block carries, for its reason:
   eight columns is wider than the measure and keeps growing past 1440px, and
   nothing on this site is ever drawn larger than the file it came from.

   The height one is what eight columns costs a portrait photograph. At 1440 the
   slot is 810px, so an upright picture would be drawn well past 1000px tall —
   taller than the window, which means the reader meets a wall of photograph and
   has to scroll to find out whose answer it belongs to. Capped at three quarters
   of the window, the picture is unmistakably the largest thing on the page and the
   name under it is still on screen with it.

   width: auto is load-bearing rather than tidying: the base rule sets width: 100%,
   and a definite width with a capped height is how you squash a photograph. Left
   auto, both ceilings are constraints on a replaced element and the browser fits
   the picture inside them at its own shape. */
.story-round-up--centred .story-round-up__image {
  width: auto;
  max-width: min(100%, var(--image-natural-width, 100%));
  max-height: 75vh;
  margin-inline: auto;
}

/* Stacked, there is no margin to float in: the picture rejoins the flow under
   the answer it belongs to, at the width of the column, and everything the layer
   was doing — the lap, the ladder, the drift — comes off with it. */
@media (max-width: 900px) {
  .story-round-up__figure {
    position: static;
    grid-column: 1;
    width: 100%;
    max-width: none;
    transform: none;
  }

  /* And the entry goes back to being as tall as its own words. */
  .story-round-up--pictured {
    min-height: 0;
  }

  /* The centred one keeps its order, though: it is above the answer at every
     width, which is the whole of what "centred" means once there are no margins
     left to be in. The drift comes off with everything else. */
  .story-round-up--centred .story-round-up__figure {
    grid-row: 1;
    /* Both of these are only repeated because the desktop rules above set them
       with a class in front and would otherwise outrank the plain
       .story-round-up__figure rule that stacks everything else. */
    grid-column: 1;
    transform: none;
  }

  .story-round-up--centred .story-copy {
    grid-row: 2;
  }
}

/* Caption for the hero photograph, under the byline in the opening block's left
   rail (see story-intro.php for why it sits there and not on the picture). Same
   small type as the captions on the in-flow images, set clear of the last byline
   line so it reads as a separate note rather than a credit. */
.story-hero-caption {
  margin: var(--space-3) 0 0;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
  color: var(--color-text);
}

/* Full-bleed image — deliberately outside .container, spans the viewport. */
.story-image-full {
  margin: 0;
  width: 100%;
}

.story-image-full img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
}

/* Half-width image: image = 6 grid columns (the same width as the
   article column), flush to one side; caption sits in the opposite
   2-column rail, vertically bottom-aligned with the image. */
.story-image-half {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
  align-items: end;
  margin: 0;
}

.story-image-half__image {
  display: block;
  grid-column: 7 / span 6;
  width: 100%;
  aspect-ratio: 672 / 1008;
  object-fit: cover;
}

.story-image-half__caption {
  grid-column: 1 / span 2;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
  color: var(--color-text);
  margin: 0;
}

.story-image-half--left .story-image-half__image {
  grid-column: 1 / span 6;
}

.story-image-half--left .story-image-half__caption {
  grid-column: 11 / span 2;
}

.story-pullquote {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
}

.story-pullquote p {
  grid-column: 3 / span 8;
  margin: 0;
  font-family: var(--font-display);
  /* Flat 32px up to the 1440px baseline (2.222vw already matches it there
     exactly), then keeps growing with the viewport up to 48px by ~2160px
     wide — same freeze point as the hero title. */
  font-size: clamp(2rem, 2.222vw, 3rem);
  line-height: 1.75; /* 56px at 32px, scales with font-size above */
  color: var(--story-color);
  text-align: center;
  /* Same as the recipe and section titles: a pullquote is a mark in the flow
     rather than something read as a sentence, and the caps set it apart from
     the copy either side without changing its size or its colour. */
  text-transform: uppercase;
}

/* Two images side by side across the full 12 columns, inside the same page
   margins as every other block. Nothing is cropped: each image keeps its own
   aspect ratio and grows in proportion to it (--ratio, set per image by the
   block template), so the row is one shared height and a pair of matching
   images still lands on the 6 + gutter + 6 split that lines up with
   .story-subject and the article column. */
.story-image-two-col {
  width: 100%;
}

.story-image-two-col__row {
  display: flex;
  gap: var(--grid-gutter);
  align-items: flex-start;
}

.story-image-two-col img {
  display: block;
  /* Width proportional to shape, from a zero basis, is what puts two different
     ratios on the same height. */
  flex: var(--ratio, 1) 1 0;
  min-width: 0;
  width: auto;
  height: auto;
}

/* --------------------------------------------------------------------------
   Subject split
   Section opener for a story that runs through several subjects: a centered
   title panel in the left six columns and a full-bleed portrait in the
   right six. Same 6 + gutter + 6 split as the two-column images above, so
   the image edge lines up with everything else on the page.
   -------------------------------------------------------------------------- */
.story-subject {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
  align-items: center;
}

.story-subject__text {
  grid-column: 1 / span 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Generous enough that the three lines read as three separate marks
     rather than one stacked block — it's the whole point of the layout. */
  gap: var(--space-15);
  /* 442 of the pane's 672 in the Figma frame; kept as a ratio so it tracks
     the pane as the page scales past 1440. */
  width: 66%;
  margin-inline: auto;
  text-align: center;
}

.story-subject__label {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 16px;
  line-height: 24px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.story-subject__name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  /* 64px at the 1440px baseline, and keeps growing with the viewport past
     it — same treatment as the hero title and pullquote. */
  font-size: clamp(2rem, 4.44vw, 6rem);
  line-height: 0.875; /* 56px at 64px */
}

.story-subject__credit {
  margin: 0;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
}

/* The underline belongs to the link, not to the name — an unlinked credit
   underlined for decoration reads as a link that doesn't work. */
.story-subject__credit-name {
  font-family: var(--font-mono);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.story-subject__image {
  grid-column: 7 / span 6;
  display: block;
  width: 100%;
  aspect-ratio: 672 / 895;
  object-fit: cover;
}

/* --------------------------------------------------------------------------
   Recipe
   A recipe pulled out of the article and set on a soft wash of the post's
   accent: photo in columns 2-4, title across 6-11 with the serving-size
   badge floating up over its right end, then ingredients (6-7) and method
   (8-11) below.
   -------------------------------------------------------------------------- */
.story-recipe {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  /* The photograph spans both rows, and these tracks grow with the viewport
     rather than capping at 1440 (see .story-content-flow .container). Left as
     two auto rows, the picture's height gets distributed across both, so past
     ~1600px it inflates the title's row and strands the ingredients well below
     the title — 103px of dead space at 1800px, where the title needs only 70.

     Row 2 has to be 1fr, not auto. Grid distributes a spanning item's leftover
     height into *any* track it spans when no track can absorb it, so a
     min-content row 1 still gets pushed open whenever the ingredients and
     method are shorter than the picture — which is most recipes. A flexible
     track takes that slack first, so row 1 stays at the title's own height and
     the gap below it is the row-gap, nothing more. */
  grid-template-rows: min-content 1fr;
  column-gap: var(--grid-gutter);
  row-gap: var(--space-5);
  align-items: start;
  padding-block: var(--space-10);
  /* The one block where the accent is the ground rather than the mark, so it's
     the one block whose ink can't be assumed. --story-ink is worked out per
     story from the accent's luminance (see synonym_ink_on() in
     inc/template-tags.php) and knocks out to the page off-white where the
     accent is dark enough that the page's ink would disappear into it — the
     Preservation feature's forest green puts near-black body copy at about
     1.8:1. Set once on the container: everything inside inherits, and the
     recipe's own links and the serving-size ring are drawn in currentColor so
     they come with it. */
  color: var(--story-ink, var(--color-text));
}

/* The wash is just a rect of the accent pushed through a big blur, so it
   has no hard edge at all and bleeds a little way into the 120px gaps above
   and below — the same soft-color language as the hero gradient. Inset by
   the page margin so its solid middle lines up with the content width. */
.story-recipe__wash {
  position: absolute;
  /* Held off the sides by more than the page margin, so the panel floats rather
     than reading as a full-bleed band: inset by the margin alone, a 60px blur
     throws its haze 20px past the edge of the window and there is no edge left
     to see.

     The extra is fluid because the recipe's own columns are. It has to come out
     of the space between the window's edge and the photograph, and that space
     only exists on a wide screen — at 1024 there is none, and a fixed inset
     would slide the fade in under the picture. So: nothing at 1024, growing to
     the full 48px by about 1400, where the wash's solid middle still starts
     clear of the content column. */
  inset: 0 calc(var(--grid-margin) + clamp(0px, (100vw - 1024px) / 8, var(--space-6)));
  background: var(--story-color);
  filter: blur(60px);
}

/* Being absolutely positioned, the wash would otherwise paint over the
   in-flow content; positioning the content too puts it back on top, in
   tree order. */
.story-recipe > *:not(.story-recipe__wash) {
  position: relative;
}

.story-recipe__image {
  grid-column: 2 / span 3;
  /* To the last row line, whichever that is — the block is two rows without an
     introduction and three with one (see .story-recipe--intro). */
  grid-row: 1 / -1;
  width: 100%;
  aspect-ratio: 328 / 437;
  object-fit: cover;
}

/* Title and yield in one run of text, so the ring lands on the end of the name
   however long the name is.

   As two grid items in fixed columns there was nothing holding them together:
   the stamp sat in columns 10-11 whatever the title did, and reached the name
   only when the name happened to run long enough to meet it — "Qidreh" left it
   floating 228px clear. Neither a flex row nor a float fixes that. A flex row
   pins the ring to the end of the title's *box*, which a wrapped title doesn't
   fill — "Marzipan Sorbet" is already two lines, so most titles would still
   miss. A float does hold the first line out to the ring, but only if the
   wrapper shrink-wraps, and `width: max-content` on a grid item hands the two
   1fr tracks below a max-content contribution and blows the column grid open —
   the same trap the ring's own comment warns about further down.

   The one box that always sits at the end of a wrapped run is an inline one in
   the run itself. So the title goes inline and the ring follows it as an
   inline-grid, and the overlap below is then simply a negative margin between
   two words. The type moves up here because this is the block that establishes
   the line boxes now — an inline h2 with its own font-size leaves the strut on
   the wrapper, and the leading comes out of whichever is taller.

   The ring stays *outside* the h2, so the heading's accessible name is the
   recipe's name and nothing else. `display: inline` doesn't touch the heading
   role. */
.story-recipe__head {
  grid-column: 6 / span 6;
  grid-row: 1;
  font-family: var(--font-display);
  font-weight: 400;
  /* 64px at the 1440px baseline, then keeps growing with the viewport —
     same treatment as the hero title and .story-subject__name. */
  font-size: clamp(2rem, 4.44vw, 6rem);
  line-height: 0.875; /* 56px at 64px */
  text-transform: uppercase;
}

.story-recipe__title {
  display: inline;
  margin: 0;
  /* The size and weight live on .story-recipe__head now, and there is no
     heading reset in this sheet — so without these the browser's own
     h2 { font-size: 1.5em; font-weight: bold } lands on top of them and the
     name comes out half again as big as the block was drawn at, and bold. */
  font-size: inherit;
  font-weight: inherit;
}

/* The yield, ringed like a stamp. Sized by what's in it rather than drawn at a
   fixed 160x88 with the long ones spilling out through the sides: "Serves 4"
   still gets the 160x88 of the Figma frame, a line or two more widens the ring
   as far as its own two columns, and past that — "Makes one 9-inch deep-dish pie
   or 8 mini pies" — the words wrap and the ring grows downwards instead.

   The padding is what keeps the words off the curve. An ellipse only contains a
   rectangle of text when it is about 1.4x that rectangle in both directions, so
   the inline padding is deliberately far larger than the block padding: the text
   box is much wider than it is tall, and the sides are where the curve closes in
   fastest. */
.story-recipe__serves {
  /* Inline, so it sits after the title's last word wherever that word ends —
     see .story-recipe__head. Grid on the inside still, to centre the yield in
     the ring. */
  display: inline-grid;
  place-items: center;
  /* Measured from the top of the line, which is the one edge that doesn't move
     with the font's metrics — so the three margins below all mean what they
     say. */
  vertical-align: top;
  /*
   * Top: the lift, half the title's own size — so it tracks the type as the
   * clamp grows it, which a fixed pixel lift did not.
   *
   * Half an em is the largest lift that still leaves the ring on the title's
   * *last* line. It was -80px, tuned on a title that fits one line, where
   * everything above the line is the block's top padding and there is nothing
   * to hit. A title that wraps — "Spicy Bagoóng Caramels" — puts a line of
   * capitals there instead, and 80px is more than the 56px leading, so the
   * whole ring, its own words included, landed on the line above and read as
   * a printing error. At -0.5em the ring's box ends level with the bottom of
   * the line it belongs to and overhangs only upwards, by the 32px that its
   * 88px height exceeds that line: a thin arc crosses the line above at worst,
   * never the ring's text. A line carrying the ring is still no taller than
   * one that isn't, which is what keeps the leading closed up.
   *
   * Left: the overlap itself, 40px. It has to be this big because of the
   * curve — the ring's leftmost point is at its own vertical middle, and by
   * the time it drops to the letters it has already turned back in, so 40px of
   * boxes is a good deal less than 40px of ink.
   *
   * Right: the remaining 120px of the ring's 160px width, so that left and
   * right together cancel it exactly and the ring contributes nothing to the
   * line. Without this the ring is 120px of inline content the title has to
   * make room for, and it pushes titles the design sets on one line onto two —
   * "Marzipan Sorbet" among them. The ring can then overhang the measure by
   * whatever it needs (31px for that title), which is the page margin's
   * problem and not the grid's.
   */
  margin: -0.5em calc(var(--space-5) * -3) 0 calc(var(--space-5) * -1);
  /* Hugs its content and floors at the drawn size. Deliberately *not*
     width: max-content — an inline-grid already shrink-wraps to its content,
     while max-content would hand the two 1fr tracks under it a 417px content
     contribution and blow the recipe's whole column grid open. So the ring
     stays at its drawn width and grows downwards. */
  min-width: 160px;
  min-height: 88px;
  padding: var(--space-2) var(--space-4);
  /* currentColor, not --color-text: the ring sits on the accent wash like the
     rest of the block and has to knock out with it (see .story-recipe). */
  border: 1px solid currentColor;
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 16px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  text-align: center;
  /* Even lines rather than a long one and a short one — inside a ring the
     ragged edge is what reads as overflow. */
  text-wrap: balance;
}

/* Optional standfirst across the full text width, between the title and the
   two columns below it. HAL rather than Matter — the same standfirst voice the
   section intros use elsewhere — at the method's size and leading.

   The extra row it needs lives with the mirrored layout below, scoped to the
   two-column breakpoint — see .story-recipe--intro there. */
.story-recipe__intro {
  grid-column: 6 / span 6;
  grid-row: 2;
}

.story-recipe__intro p {
  margin: 0 0 var(--space-4);
  font-family: var(--font-serif);
  font-size: 20px;
  line-height: 32px;
}

.story-recipe__intro p:last-child {
  margin-bottom: 0;
}


.story-recipe__ingredients {
  grid-column: 6 / span 2;
  grid-row: 2;
  margin: 0;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 32px;
}

/* The ingredients column is either a bare <ul> or, where a recipe has more
   than one component to shop for, a stack of labelled groups. */
.story-recipe__ingredient-group + .story-recipe__ingredient-group {
  margin-top: var(--space-4);
}

/* Ingredients hang: the first line of an item sits flush and every
   continuation line steps in, so a long ingredient stays readable as one
   item in a two-column-wide measure. Applies whether the line wraps on its
   own or is broken deliberately with a __qualifier below. */
.story-recipe__ingredients li {
  padding-left: var(--space-4);
  text-indent: calc(var(--space-4) * -1);
}

/* Forces the break where the rest of an ingredient is a distinct aside — a
   substitution, an "(optional)". Its own first line starts from the padding
   edge rather than the item's outdented one, so it lands on the hang. */
.story-recipe__qualifier {
  display: block;
  text-indent: 0;
}

/* Aside sitting under a group's list: a prep note that isn't a step. */
.story-recipe__note {
  margin: var(--space-4) 0 0;
}

/* Label for a group of ingredients, or for a distinct phase of the method
   ("To serve"). Same mono-caps register as the nav and the eyebrows. */
.story-recipe__subhead {
  margin: 0 0 var(--space-1);
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 400;
  line-height: 16px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.story-recipe__method {
  grid-column: 8 / span 4;
  grid-row: 2;
}

.story-recipe__method p {
  margin: 0 0 var(--space-4);
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 32px;
}

.story-recipe__method p:last-child {
  margin-bottom: 0;
}

/* Fine print closing the block: a source, a substitution, where to buy the one
   ingredient nobody has. Under the method, in the method's own four columns,
   so it reads as the end of that column rather than as a second full-width
   measure opening up under a block that has finished. Set off by the grid's
   own row gap, nothing drawn.

   The method's face at the ingredients' size: smaller carries "secondary" on
   its own, and it's the one move available. Not dialled back with --quiet —
   that token is calibrated for ink on the page background, and here the ground
   is the story's accent, where 0.7 opacity puts this at 3.5:1 on the default
   orange, under AA. */
.story-recipe__footnote {
  grid-column: 8 / span 4;
  /* The track appended by .story-recipe--footnote below — last either way,
     whether that's the third row or the fourth with an introduction above. */
  grid-row: -2;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
}

.story-recipe__footnote p {
  margin: 0 0 var(--space-3);
}

.story-recipe__footnote p:last-child {
  margin-bottom: 0;
}

/* The two modifiers, both scoped to the two-column layout: below the
   breakpoint everything stacks in a single column, where neither has anything
   to say and both would out-specify the stacking rules. */
@media (min-width: 901px) {
  /* A third row, and only when there's an introduction to put in it: an
     always-declared third track would be empty on most recipes and grid gaps
     don't collapse, so every recipe without one would carry 40px of dead
     space above the ingredients. Hence the modifier, added by the template
     only when the field is filled in.

     The row assignments in particular have to stay inside this query: they
     would otherwise beat the single-column `grid-row: auto` and stack the
     ingredients back on top of the method. */
  .story-recipe--intro {
    grid-template-rows: min-content min-content 1fr;
  }

  .story-recipe--intro .story-recipe__ingredients,
  .story-recipe--intro .story-recipe__method {
    grid-row: 3;
  }

  /* A footnote appends a track the same way, and for the same reason it isn't
     simply always declared. Spelled out for both cases rather than composed,
     because grid-template-rows is one property and there is no adding to it —
     two combinations is the whole set, since the footnote is always last and
     the introduction always second. */
  .story-recipe--footnote {
    grid-template-rows: min-content 1fr min-content;
  }

  .story-recipe--intro.story-recipe--footnote {
    grid-template-rows: min-content min-content 1fr min-content;
  }

  /* Mirrored: image over on the right, the whole text side swung to the
     left half. */
  .story-recipe--flip .story-recipe__image {
    grid-column: 9 / span 3;
  }

  .story-recipe--flip .story-recipe__head {
    grid-column: 2 / span 6;
  }

  .story-recipe--flip .story-recipe__intro {
    grid-column: 2 / span 6;
  }

  .story-recipe--flip .story-recipe__ingredients {
    grid-column: 2 / span 2;
  }

  .story-recipe--flip .story-recipe__method {
    grid-column: 4 / span 4;
  }

  /* Under the method here too — which the mirror puts in columns 4-7. */
  .story-recipe--flip .story-recipe__footnote {
    grid-column: 4 / span 4;
  }

  /* No photograph: the same six-column run of type, moved to the middle of the
     grid. Columns 6-11 are where they are because a picture holds 2-4, so with
     nothing there the type has no reason to stay on one side — left where it
     was it reads as a photograph that failed to load, and pushed to the left
     edge it reads as a block waiting for one. Centred, it reads as what it is.
     The measures don't change, only which columns they sit in.

     The two modifiers never meet: the template drops the mirror switch when
     there's no photograph, since there is then nothing to mirror. */
  .story-recipe--no-image .story-recipe__head,
  .story-recipe--no-image .story-recipe__intro {
    grid-column: 4 / span 6;
  }

  .story-recipe--no-image .story-recipe__ingredients {
    grid-column: 4 / span 2;
  }

  .story-recipe--no-image .story-recipe__method,
  .story-recipe--no-image .story-recipe__footnote {
    grid-column: 6 / span 4;
  }

  /* And the wash comes in with it, rather than staying a full-width band with
     the recipe floating in the middle of it.

     Positioning it by grid area instead of by inset: an absolutely positioned
     grid child that names its own rows and columns takes that area as its
     containing block, so `inset: 0` on the sides lands it exactly on columns
     3-10 — a column of wash either side of the type — without any of the
     column arithmetic the full-width version has to do in calc.

     Vertically the grid area is the content rows alone, so the negative top
     and bottom put back the block's own padding: the wash has to reach past
     the type for the blur to have anywhere to fade out, which is what it gets
     for free from `inset: 0` when it's positioned against the padding box. */
  .story-recipe--no-image .story-recipe__wash {
    grid-column: 3 / span 8;
    grid-row: 1 / -1;
    inset: calc(var(--space-10) * -1) 0;
  }
}

.story-image-single {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
}

/* Sized by the file, not by the slot. The image sits in a 6-column well and is
   centred in it, but takes its own proportions: a landscape photograph fills the
   width and stays short, a portrait one runs taller and narrower. There's no
   aspect-ratio and no object-fit, so nothing is ever cropped — the earlier 441/564
   was recomposing every landscape upload into a portrait.

   The picture fills the measure — `width: 100%`, not auto. Auto sounds right and
   isn't: once an <img> carries a srcset the browser derives its width from the
   chosen source's descriptor and the sizes attribute rather than from the well,
   so a 2000px file in a 672px measure was arriving at 576px, narrower than the
   copy above it for no reason a reader could see.

   The one cap left is the file's own resolution, and it has to be given the real
   number by PHP for the same srcset reason: a 400px file in the well reports
   itself as 672 and would be drawn at 672, soft and obviously wrong.
   --image-natural-width is measured off the file in
   synonym_image_natural_width(), where nothing in the markup can inflate it.
   Falling back to 100% leaves an image with no measurement filling the measure.

   There was a max-height of 80vh here, to stop a very tall portrait taking the
   whole screen. It's gone, because it contradicted the rule above for exactly
   the pictures it applied to: at the measure a 4:5 portrait is 840px tall and a
   2:3 one 1008px, so on any normal window the cap bound first and pulled every
   portrait back under the copy's width — the 576px this was meant to fix. It
   also can't simply be left in place alongside a definite width: a replaced box
   with both axes constrained stops honouring its intrinsic ratio and stretches,
   which is a 4:5 photograph drawn at 672x720.

   With the width below the well, margin-inline: auto has free space to absorb and
   the picture centres itself. */
.story-image-single img {
  grid-column: 4 / span 6;
  display: block;
  width: 100%;
  max-width: min(100%, var(--image-natural-width, 100%));
  height: auto;
  margin-inline: auto;
}

/* A caption set under the picture it belongs to, centred on it. The centred
   image is the one block where the caption has nowhere else to go: there is no
   rail beside it and no copy it belongs to, so it hangs under the picture.

   Same small type as every other caption in a story. Measure-capped rather than
   running the full 6 columns — a two-line caption centred under a photograph
   reads as a caption; the same words set to the full width read as a paragraph
   somebody forgot to style. */
.story-image-caption {
  margin: var(--space-2) auto 0;
  max-width: 46ch;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
  color: var(--color-text);
  text-align: center;
  text-wrap: balance;
}

.story-image-single .story-image-caption {
  grid-column: 4 / span 6;
}

/* The fallback position for a caption that had nowhere to go: a full-bleed
   photograph or a pair with no copy block under it to carry the caption into its
   rail (see synonym_story_plan()). Sits on the measure under the picture rather
   than being dropped. */
.story-image-caption--measure {
  margin-top: var(--space-3);
}

/* The second copy of a caption that was sent down to a rail — printed by the
   block it came from as well as by the rail that received it. The rails only
   exist on the wide layout, so exactly one of the two is ever on screen: the
   rail's above 900px, this one below it (see the stacked caption rules in the
   breakpoint). */
.story-image-caption--stacked {
  display: none;
}

/* ---- Promo ---------------------------------------------------------------
   A note from the magazine in the middle of a story — on the copy's own six
   columns, in the type of the Subscribe page's "Prefer it in print?" panel.
   Nothing is drawn around it: what sets it apart from the story either side is
   that it is centred, and set in the display face at a size the copy never
   reaches. */
.story-promo {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
  /* Only ever spends itself between a picture and the words under it — a promo
     without one is a single row and grid gaps need two. */
  row-gap: var(--space-6);
}

.story-promo__panel {
  grid-column: 4 / span 6;
  text-align: center;
}

.story-promo__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 4.444vw, 4rem);  /* 64px */
  line-height: 0.875;
  font-weight: 400;
  text-wrap: balance;
}

/* The full six columns, the same measure the copy runs on. */
.story-promo__body p {
  margin: var(--space-5) 0 0;
  font-family: var(--font-display);
  font-size: 1.25rem;   /* 20px */
  line-height: 1.6;
  text-wrap: pretty;
}

/* No headline, or the paragraph is the first thing in the panel: the padding is
   already the space above it. */
.story-promo__body:first-child p:first-child {
  margin-top: 0;
}

/* The picture sits above the words, small, on the middle two of the panel's six
   columns — 6 and 7 of the twelve, which is dead centre of a block running 4 to
   9, so it lines up with the centred type under it without anything having to be
   nudged.

   Two columns rather than the panel's six because this is an aside: at the full
   measure the picture was the largest thing in the block and read as the point
   of it rather than as an illustration of it.

   Sized like the centred image block otherwise, and for the same reasons: it
   fills the two columns rather than taking the width its srcset reports, and
   --image-natural-width stops a small file being drawn bigger than it is. No
   aspect-ratio and no object-fit, so a landscape promo picture stays short and
   wide and nothing is cropped — the recipe's 328/437 portrait crop came off with
   the arrangement it belonged to.

   margin-inline: auto centres a file too small to fill even those two. */
.story-promo__image {
  grid-column: 6 / span 2;
  display: block;
  width: 100%;
  max-width: min(100%, var(--image-natural-width, 100%));
  height: auto;
  margin-inline: auto;
}

/* Video: the copy block's own six columns, at 16:9 — a film in the middle of a
   story sits on the measure the writing does. The poster carries the block until
   someone plays it, and preload="none" in the markup keeps the file itself off
   the wire until then, so a story can open on a video without paying to download
   one. */
.story-video {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
  margin: 0;
}

.story-video__player {
  grid-column: 4 / span 6;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--color-text);
  /* The provider's player arrives as an iframe in this same slot when someone
     presses play — see initStoryVideo(). */
  border: 0;
}

/* What a Vimeo or YouTube block shows until then: the poster frame, painted as
   a background because the element is the control itself, and a ring with a
   triangle in it. One element rather than a picture with a button over it, so
   the whole 16:9 area is the target and the scroll reveal has one thing to
   fade in rather than two nested ones. */
.story-video__facade {
  position: relative;
  display: grid;
  place-items: center;
  background-size: cover;
  background-position: center;
  text-decoration: none;
  /* The mark sits on a photograph that could be any brightness, so it takes the
     page's off-white and the scrim below guarantees the contrast. */
  color: var(--color-bg);
}

/* A wash over the poster, so the ring reads on a pale frame as surely as on a
   dark one, deepening a little under the cursor. */
.story-video__facade::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(29 28 27 / 0.2);
  transition: background 0.3s ease;
}

.story-video__facade:hover::before,
.story-video__facade:focus-visible::before {
  background: rgb(29 28 27 / 0.35);
}

/* A filled circle with the triangle taken out of it — what shows through the
   triangle is the poster underneath.

   112px rather than the 88px of the recipe's yield stamp, which is what this
   borrowed at first: measured off Vimeo's own loading spinner, the circle the
   reader sees a second later when the film starts, this lands within a few
   pixels of it. The two reading as one size is the point — the mark doesn't
   jump when the player takes over. It's the 8px scale's 112, not the 115 the
   measurement gave, and nobody can see the 3px.

   The ring-and-triangle below is the fallback, and is what any browser without
   mask-image still gets. */
.story-video__play {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--space-14);
  height: var(--space-14);
  border: 1px solid currentColor;
  border-radius: 50%;
  /* Grows under the cursor like the story cards' images do — same curve, same
     duration (see --card-image-transition), a little more of it because this is
     a control rather than a picture. The scrim behind it deepens at the same
     time, so the two read as one hover. */
  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Hovering anywhere on the 16:9 area, not just on the mark: the whole facade is
   the button, so the whole facade is what answers. */
.story-video__facade:hover .story-video__play,
.story-video__facade:focus-visible .story-video__play {
  transform: scale(1.08);
}

@media (prefers-reduced-motion: reduce) {
  .story-video__play {
    transition: none;
  }

  .story-video__facade:hover .story-video__play,
  .story-video__facade:focus-visible .story-video__play {
    transform: none;
  }
}

.story-video__play::before {
  content: "";
  width: 0;
  height: 0;
  border-style: solid;
  /* Scaled with the circle — the same proportions the mask's triangle keeps. */
  border-width: 13px 0 13px 22px;
  border-color: transparent transparent transparent currentColor;
  /* A triangle's visual centre isn't its bounding box's — without this it sits
     noticeably left of the middle of the ring. */
  margin-left: 6px;
}

/* One shape, not a disc with a triangle drawn on it: the disc and the triangle
   are a single path with an even-odd fill, so the triangle is a hole in the
   circle rather than ink over it. As a mask on a solid fill, that hole is
   genuinely transparent — the photograph and its scrim read through the play
   mark, which is what makes it a cut-out rather than a triangle in the scrim's
   colour, and it holds on any poster.

   The triangle's points are set off-centre on purpose, for the same reason the
   bordered one carries a margin: a triangle centred by its bounding box reads
   left of the middle. These put its centroid on the circle's centre instead. */
@supports (mask-image: linear-gradient(#000, #000)) or (-webkit-mask-image: linear-gradient(#000, #000)) {
  .story-video__play {
    --play-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 88 88'%3E%3Cpath fill-rule='evenodd' d='M44 0a44 44 0 1 0 0 88 44 44 0 1 0 0-88Zm-6 34 17 10-17 10Z'/%3E%3C/svg%3E");
    border: 0;
    /* currentColor is the page off-white here — set once on the facade, so the
       fill and the fallback's ring stay the same one colour. */
    background: currentColor;
    -webkit-mask-image: var(--play-mark);
    mask-image: var(--play-mark);
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
  }

  /* The drawn triangle is the fallback's, and would sit as a solid mark in the
     middle of the hole it was replaced by. */
  .story-video__play::before {
    display: none;
  }
}

/* The end mark — appended inline after the last sentence of the final
   paragraph in a story (see story.html), not a standalone element. */
.story-end-mark {
  color: var(--story-color);
  margin-left: var(--space-1);
}

.story-up-next {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
  color: var(--color-text);
  text-align: center;
  text-decoration: none;
  padding-block: var(--space-10);
}

/* Hidden until hover — the next story's hero image, permanently blurred
   (not animated from sharp like the hero itself) so it reads as an
   atmospheric preview rather than a photo you're meant to look at
   directly. Centred on the block and unclipped, so the blur spreads into a
   soft cloud rather than reading as a sharp-edged photo.

   Two sizes, and only two: a small landscape, or the same footprint stood on
   end. Sizing it from the picture's own proportions — the earlier square slot
   with object-fit: contain — meant a 16:9 hero and a 4:5 one arrived at
   noticeably different weights, and every story's up-next looked like a
   different component. A fixed box per orientation costs a crop, which is
   nothing on something blurred to 16px. */
.story-up-next__image {
  position: absolute;
  grid-column: 5 / span 4;
  /* Centred in that column span rather than filling it — the box is a fixed
     size now, so it has to be placed rather than stretched. */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 240px;
  height: 160px;
  object-fit: cover;
  filter: blur(16px);
  opacity: 0;
  transition: opacity 0.4s ease;
}

.story-up-next__image--portrait {
  width: 160px;
  height: 240px;
}

/* Hover trigger is scoped to this wrapper (label + title only), not the
   full-width row — the image itself is much bigger than the text, so
   hovering the whole block would make the reveal trigger far outside
   where the title visually is. */
.story-up-next__hover {
  position: relative;
  z-index: 1;
  grid-column: 5 / span 4;
}

/* Half strength at rest of the eye: it's a hint of where the link goes, and
   the title is sitting on top of it. */
.story-up-next__hover:hover ~ .story-up-next__image,
.story-up-next:focus-visible .story-up-next__image {
  opacity: 0.5;
}

.story-up-next__label {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 16px;
  line-height: 24px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.story-up-next__title {
  margin: var(--space-2) 0 0;
  font-family: var(--font-display);
  font-size: 48px;
  line-height: 48px;
}


@media (max-width: 900px) {
  .story-block,
  .story-image-half,
  .story-pullquote,
  .story-image-single,
  .story-promo,
  .story-subject,
  .story-recipe,
  .story-video,
  .story-up-next {
    grid-template-columns: 1fr;
    row-gap: var(--space-6);
  }

  .story-rail--left,
  .story-rail--right,
  .story-copy,
  .story-lead,
  .story-image-half__image,
  .story-image-half--left .story-image-half__image,
  .story-image-half__caption,
  .story-image-half--left .story-image-half__caption,
  .story-pullquote p,
  .story-image-single img,
  .story-image-single .story-image-caption,
  .story-promo__image,
  .story-promo__panel,
  .story-video__player,
  .story-subject__text,
  .story-subject__image,
  .story-recipe__image,
  .story-recipe__head,
  .story-recipe__intro,
  .story-recipe__ingredients,
  .story-recipe__method,
  .story-recipe__footnote,
  .story-up-next__hover,
  .story-up-next__image {
    grid-column: 1;
  }

  /* The stacked single column opens every gap to var(--space-6); a caption is
     not a separate item from the picture above it, so it keeps its own 16px. */
  .story-image-single {
    row-gap: 0;
  }

  /* ---- Captions, stacked -------------------------------------------------
     Every caption reads the same way at this width, whatever it does on the
     wide layout: centred on the measure, directly under the picture it belongs
     to — the centred image block's treatment, applied to all of them. There are
     no rails down here to set one beside a photograph or to send one down to,
     and a caption left-aligned across the full width of a phone reads as a
     paragraph somebody forgot to style rather than a note about the picture
     above it. */
  .story-image-half__caption,
  .story-rail .story-margin > .story-caption--plain,
  .story-image-caption--stacked {
    display: block;
    max-width: 46ch;
    margin-inline: auto;
    text-align: center;
    text-wrap: balance;
  }

  /* The half-width block's caption is written before the image when the image
     is on the right, so that the wide layout reads caption-then-picture across
     the row. Stacked it belongs under the picture either way. */
  .story-image-half {
    row-gap: 0;
  }

  .story-image-half__image {
    order: 1;
  }

  .story-image-half__caption {
    order: 2;
    margin-top: var(--space-2);
  }

  /* Same 16px under the picture as the centred block's caption, in place of the
     24px it takes when it's standing in for a rail on the wide layout. */
  .story-image-caption--measure {
    margin-top: var(--space-2);
  }

  /* A caption that travelled down into a rail: the block it came from is
     printing its own copy under itself now, so this one goes. Only the rail's
     own inherited caption — never the margin figure's, which is already sitting
     under its own picture. */
  .story-rail > .story-caption--plain {
    display: none;
  }

  .story-recipe__image,
  .story-recipe__head,
  .story-recipe__intro,
  .story-recipe__ingredients,
  .story-recipe__method,
  .story-recipe__footnote {
    grid-row: auto;
  }

  .story-recipe {
    /* Everything above stacks into one column with grid-row: auto, so the
       two-row template the wide layout needs no longer describes this grid. */
    grid-template-rows: none;
    row-gap: var(--space-4);
    padding-block: var(--space-8);
    /* Stacked, every part of the recipe runs the full content width and lands
       hard against the wash's edges. This steps the whole column in — picture
       and text together, so the picture comes down in size by the same amount
       the text gains as margin — and it's padding rather than a narrower grid
       so the wash, which is positioned against this block's padding box and
       inset by the page margin, stays exactly where it is. */
    padding-inline: calc(var(--grid-margin) + var(--space-3));
  }

  /* No blur at this width, and the fade is painted rather than filtered.
     Stacked, a recipe runs to three or four thousand pixels tall — five on a
     long one — and at a phone's 3x that is a layer 12,000-15,000 device pixels
     high. WebKit can't rasterise a filtered layer that big in one piece: it
     tiles it, and each tile blurs on its own, so the wash comes out in bands
     with a hard seam across the middle and the fade spreading to the edges of
     the screen below it. Which recipes broke depended on how long they were.

     A gradient in the background paints straight into the block with no layer
     of its own, so there is nothing to run out of. The element is pulled out
     past the top and bottom edges and fades across 96px, which puts the solid
     middle where the blur used to put it and keeps the bleed into the gaps
     above and below. Full width rather than inset by the page margin: at a
     phone's width the blur reached the edges anyway, and a painted fade on the
     sides would land under the copy, which is only 40px in. */
  .story-recipe__wash {
    inset: calc(var(--space-6) * -1) 0;
    filter: none;
    background: linear-gradient(
      to bottom,
      transparent 0,
      var(--story-color) var(--space-12),
      var(--story-color) calc(100% - var(--space-12)),
      transparent 100%
    );
  }

  /* Back into the flow rather than floating up over the title. */
  /* Stacked, the stamp goes back into the flow under the title — there is no
     room beside it at this width. A block title ends its line, so the ring
     drops onto the next one on its own; it stays inline-grid, which is what
     keeps it hugging its own words rather than stretching across the screen. */
  .story-recipe__title {
    display: block;
  }

  .story-recipe__serves {
    vertical-align: baseline;
    margin: 0;
  }

  .story-recipe__head {
    font-size: 40px;
  }

  .story-image-two-col__row {
    flex-direction: column;
  }

  /* Stacked, the row's main axis is the height — a proportional grow from a
     zero basis would collapse it, so each image goes back to its own size on
     the full width. */
  .story-image-two-col img {
    flex: 0 0 auto;
    width: 100%;
  }

  .story-subject__text {
    width: 100%;
    gap: var(--space-6);
  }

  .story-subject__name {
    font-size: 40px;
    line-height: 40px;
  }

  .story-pullquote p {
    font-size: 24px;
    line-height: 40px;
  }

  .story-lead {
    font-size: 24px;
    line-height: 32px;
  }

  /* The rhythm tightens to 48px here; the tag stays the same 24px off the
     title, exactly as it does on the image heroes at every width. */
  .story-hero--plain .story-hero__dek {
    margin-top: calc(var(--space-6) - var(--space-3));
  }

  .story-lead__intro {
    font-size: 18px;
  }

  .story-up-next__title {
    font-size: 32px;
    line-height: 36px;
  }

  /* The hero's marks are placed as percentages of its frame, and the frame is
     100vh — which on a phone is the viewport with the browser's toolbars hidden.
     The dek sits at 88.3%, so with the toolbars actually showing it lands
     underneath them and can't be read at all. Shrinking just the content layer
     by the difference between the two viewport heights (the toolbars' own
     height) puts every mark back inside what's really on screen, while the
     picture behind it keeps filling the full frame. Browsers without svh drop
     the whole declaration as invalid, which leaves them exactly as they were. */
  .story-hero:not(.story-hero--plain) .story-hero__content {
    bottom: calc(100vh - 100svh);
  }

  /* The rails aren't rails at this width — they're full-width rows stacked
     above and below the text — so their left edge isn't a margin any more and
     centring reads as deliberate. */
  .story-byline,
  .story-hero-caption {
    text-align: center;
  }

  /* Puts the issue card straight after the byline, ahead of the story it's
     selling, rather than stranding it below the whole lead. */
  .story-block--intro .story-rail--left {
    order: 1;
  }

  /* Stacked, the issue card is a full row of its own, and the 48px row-gap
     under it read as part of the lead rather than as a break before it. This
     brings the space below the card up to the 120px that separates the hero
     from the byline at the top of the same block. */
  .story-block--intro .story-rail--right {
    order: 2;
    padding-bottom: calc(var(--space-15) - var(--space-6));
  }

  .story-block--intro .story-lead {
    order: 3;
  }

  /* Both halves shrink to their content and the pair is centred as one, rather
     than each taking half the row — at full width the cover would run to half
     the screen. Small, with the credit alongside it, it reads as a stamp on the
     byline instead of a second hero. */
  .story-buy-issue {
    justify-content: center;
  }

  .story-buy-issue img {
    flex: none;
    width: 96px;
  }

  /* Left-aligned against the cover's edge: this is the one thing in the stacked
     rail that has something to line up with. */
  .story-buy-issue__text {
    flex: none;
    text-align: left;
  }
}

/* Two treatments that only exist on hover, on devices that have no hover: the
   cover would sit permanently blurred behind half-opacity type, and the up-next
   image would only ever appear as a flash under the tap that's already leaving
   the page. So the cover rests at what hovering it would have shown, and the
   up-next image doesn't render at all. */
@media (hover: none) {
  .story-buy-issue img {
    filter: none;
  }

  .story-buy-issue__text {
    opacity: 1;
  }

  .story-up-next__image {
    display: none;
  }
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 900px) {
  /* The docked pill is the filter at this width — see the dock block above. */
  .has-filter-dock .story-filter--page {
    display: none;
  }

  /* No room for two rows of links at this width, and stacking them pushed the
     bar down over the page. The icon carries the whole nav instead: the links
     are gone from the bar entirely and live in .site-menu, which it opens. */
  .nav-list {
    display: none;
  }

  /* With the rows gone the icon is the nav, so it comes off the right edge and
     centres on the page — and the wordmark drops to meet it, so the two share a
     centre line rather than sitting in two stacked rows. The negative margin is
     half the wordmark plus half the icon, which is exactly the distance from the
     nav's natural top to the icon being centred on the wordmark. It has to be
     computed rather than eyeballed because the wordmark is a fluid-width SVG:
     its height is the container width times the artwork's own 144/1346 ratio,
     so it changes with every viewport. Height is left at the icon's own 112px,
     which means the header still reserves the space the icon actually occupies
     and .nav-spacer still gets an honest number when the bar pins. */
  .primary-nav {
    margin-top: calc((var(--wordmark-height) + var(--space-14)) / -2);
  }

  /* Pushes the wordmark (and so the icon centred on it) clear of the top edge —
     the icon reaches 56px above that shared centre line. */
  .site-header {
    padding-top: var(--space-7);
  }

  /* Sharing a centre line means the wordmark's box lies right across the icon,
     and it paints above (z-index: 1), so every tap on the icon was landing on
     the logo instead and the menu could only be opened at its very top and
     bottom edges. The wordmark is a picture with nothing to press, so it stops
     taking the pointer altogether and the icon's whole 88x112 is live again.
     The cursor trail is driven off window pointermove, not off this element,
     so the secret up there is untouched. */
  .wordmark {
    pointer-events: none;
  }

  /* The two rules above are both terms in --chrome-block, so it has to be
     restated here or a heading positioned from it would sit where the desktop
     header used to finish. Same sum, with this breakpoint's padding and margin:
     the wordmark and the nav now overlap by half of each, which is what collapses
     the two rows onto one line. */
  :root {
    --chrome-block: calc(
      var(--space-7) + var(--wordmark-height) / 2 + var(--space-14) / 2 + var(--space-6)
    );
  }

  .footer-links,
  .footer-meta {
    flex-wrap: wrap;
    gap: var(--space-3) var(--space-4);
  }
}

/* ==========================================================================
   Paywall
   Figma: GATE (319:431). template-parts/paywall.php + paywall-teaser.php.

   The story fades out into the page background and the message sits on a blurred
   accent wash. No blur on the story itself — it dissolves by alpha alone.

   --paywall-fade is one dial on purpose: the block is pulled up by exactly the
   height being faded, so the region faded is always the region covered. Two
   independent values (how far to pull up, where to put the copy) can disagree,
   and when they do the teased fragment ends up UNDER the message instead of
   above it.

   What that region *is* differs in the two cases below, and the difference is the
   whole point. With a teased fragment, the fade covers the fragment: withheld
   content, dissolving, which is what it's for. Without one there is nothing
   withheld to dissolve — the block above the cut is the last thing the reader is
   allowed, in full — so the fade covers the flow's own 120px gap instead and
   stops at that block's bottom edge. It used to run 360px deep here and take a
   couple of hundred pixels of that block with it, which read as the story being
   cut off early rather than as the paywall starting.
   ========================================================================== */
.story-paywall {
  /* Exactly the flow's gap, so the dissolve happens entirely in the empty space
     above and the last free block is untouched. */
  --paywall-fade: var(--space-15);

  position: relative;
  /* Horizontal only: the wash is wider than the measure and blurs past it, and
     that shouldn't put a scrollbar on the page. Vertically it must stay visible
     or the wash gets a hard edge along the top. */
  overflow-x: clip;
  /* Up by the fade alone — not by the flow gap as well. Pulling up both is what
     puts the gradient over real content; this way the message still lands where
     the gap would have put it. */
  margin-top: calc(var(--paywall-fade) * -1);
  padding-top: var(--paywall-fade);
  padding-bottom: var(--space-18);
  /* Solid by the time the copy starts, so the message never sits on a
     half-transparent photograph. This gradient IS the fade — there's no separate
     veil element doing it. */
  background: linear-gradient(
    to bottom,
    rgba(244, 243, 243, 0) 0,
    var(--color-bg) var(--paywall-fade)
  );
}

/* The teased fragment of the next block dissolves over its OWN height, so it reads
   the same whether it ran to two lines or five — a fade measured in fixed pixels
   can't know how tall the text turned out and washes it out completely at 45 words
   while barely touching it at 15. */
.story-paywall-teaser {
  mask-image: linear-gradient(to bottom, #000 0, transparent 92%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0, transparent 92%);
  user-select: none;
}

/* With a fragment above it the dissolve is shorter, because the fragment is doing
   most of the work on its own mask. Left at the full height the wash would swallow
   the fragment's first line before anyone could read it.

   This is the one case that *should* reach up past the gap: what it reaches into
   is the fragment, which the reader isn't allowed anyway. 160px is under the
   fragment's own height plus that 120px gap even where the fragment runs to two
   lines, so it never gets as far as the last free block. */
.story-paywall-teaser + .story-paywall {
  --paywall-fade: 160px;
  margin-top: calc((var(--space-15) + var(--paywall-fade)) * -1);
}

.story-paywall__inner {
  position: relative;
  text-align: center;
}

/* Both this and .story-paywall__copy are positioned, so paint order follows the
   markup and the copy lands on top of the wash. Giving the wash a negative z-index
   instead would put it behind .story-paywall's own background gradient, which is
   opaque by this point — the accent would simply disappear. */
.story-paywall__wash {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(1360px, 100vw);
  height: 505px;
  background: var(--color-accent);
  filter: blur(60px);
  pointer-events: none;
}

.story-paywall__copy {
  position: relative;
}

.story-paywall__heading {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 4.444vw, 4rem);  /* 64px at 1440 */
  line-height: 0.875;
  font-weight: 400;
}

.story-paywall__body {
  margin: var(--space-5) auto 0;
  font-family: var(--font-display);
  font-size: 1.25rem;   /* 20px */
  line-height: 1.6;
  max-width: 58ch;
}

.story-paywall__ctas {
  margin: var(--space-3) 0 0;
  display: flex;
  justify-content: center;
}

.story-paywall__signin {
  margin: var(--space-3) 0 0;
  font-family: var(--font-display);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
}

.story-paywall__signin a {
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: opacity 0.3s ease;
}

.story-paywall__signin a:hover,
.story-paywall__signin a:focus-visible {
  opacity: 0.6;
}

@media (max-width: 767px) {
  /* Tall enough to sit behind the message once the heading wraps to two or three
     lines, and no wider than the screen. */
  .story-paywall__wash {
    width: 100vw;
    height: 420px;
    filter: blur(40px);
  }
}

/* ==========================================================================
   WordPress image compatibility

   WordPress emits width and height attributes on every image it renders. Those
   attributes are presentational hints that set the CSS width and height
   properties — and aspect-ratio only takes effect when one axis is auto. So
   every rule in this file that sizes an image by aspect-ratio was being
   silently ignored once the markup came from the CMS instead of being written
   by hand: the buy-issue thumbnail rendered at its attribute height of 300px,
   the half-width figures at 1024px, both squeezed to whatever width was left.

   The attributes are worth keeping — they're what lets the browser reserve
   space before the image loads, which is the whole point of them. Setting
   height back to auto keeps that benefit and hands sizing back to aspect-ratio.

   A type selector on purpose: every component rule here is class-scoped, so
   anything that genuinely wants a fixed height (.story-hero__image and its
   height: 100%) still wins on specificity.
   ========================================================================== */
img {
  height: auto;
}

/* ==========================================================================
   Links in body copy

   Distinct from the chrome links above, which carry no underline at rest and
   wipe one in on hover. A link buried mid-paragraph needs to be findable
   without hovering, so it's drawn as an outlined pill in the story's accent —
   the text stays in ink so the sentence still reads as one colour — and fills
   with the accent on hover.

   box-decoration-break is what makes this survive a link that wraps: without
   it a break mid-link leaves the first fragment with no right edge and the
   second with no left, so the pill reads as two broken boxes. `clone` gives
   each line its own complete rounded outline.

   Vertical padding is in em and kept small on purpose. Padding on an inline
   element doesn't push the line box open, it overflows it, so a larger value
   would have the pill of one line touching the text of the line above. At
   0.1em it sits inside the 32px leading of body copy.
   ========================================================================== */
.story-copy a,
.story-lead a,
.story-promo__body a {
  padding: 0.1em 0.4em;
  border: 2px solid var(--story-color);
  border-radius: 99px;
  color: var(--color-text);
  text-decoration: none;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-color 0.3s ease, color 0.3s ease;
}

.story-copy a:hover,
.story-copy a:focus-visible,
.story-lead a:hover,
.story-lead a:focus-visible,
.story-promo__body a:hover,
.story-promo__body a:focus-visible {
  background-color: var(--story-color);
  /* The page's off-white rather than pure white — same knock-out as the text on
     the dark CTA pills, so nothing on the page is brighter than the paper. */
  color: var(--color-bg);
}

/* --------------------------------------------------------------------------
   Recipe links

   The same pill as the running copy of a story, with its two colours
   exchanged.

   Out on the page the pill is outlined in --story-color, fills with
   --story-color, and the word knocks out to the page background. In here the
   accent has stopped being the mark and become the ground — the wash is
   --story-color — so an outline drawn in it reads as a gap in the sentence
   rather than a link. Every colour therefore swaps places: outlined in the
   block's ink, filling with the ink, and the word knocking back to the wash it
   was sitting on. Same shape, same two moves, same 0.3s.

   This replaces an underline-sweep treatment that existed only because the
   pill's outline was invisible on the wash. Taking the ink instead of the
   accent solves that, and one link pattern across the site beats a bespoke one
   in the single block that paints its own ground.

   The vertical padding stays at the pill's 0.1em for the reason given up
   there: padding on an inline element overflows its line box instead of
   opening it, so anything larger would have a pill on one line touching the
   words on the line above. At 0.1em it sits inside the 32px leading the method
   and the introduction are both set in.
   -------------------------------------------------------------------------- */
.story-recipe__intro a,
.story-recipe__method a,
.story-recipe__footnote a {
  padding: 0.1em 0.4em;
  /* The token, not currentColor. The outline has to hold still while the word
     inside it changes colour, so that on hover it is the same ink as the fill
     and disappears into it — on the page pill the ring and the fill are both
     --story-color and all you ever see is a solid lozenge. Left as currentColor
     it would follow the word down to the ground colour and draw a ring around
     the fill that the page version doesn't have. */
  border: 2px solid var(--story-ink, var(--color-text));
  border-radius: 99px;
  /* Inherited, not --color-text: on a dark accent the block knocks out to the
     page off-white (see .story-recipe) and the link has to come with it —
     otherwise the one word in a paragraph that's a link is the one word that
     disappears. */
  color: inherit;
  text-decoration: none;
  /* A link broken across two lines gets a complete rounded outline on each
     fragment rather than one box sliced between them — same reason the page
     pill uses it. */
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-color 0.3s ease, color 0.3s ease;
}

.story-recipe__intro a:hover,
.story-recipe__intro a:focus-visible,
.story-recipe__method a:hover,
.story-recipe__method a:focus-visible,
.story-recipe__footnote a:hover,
.story-recipe__footnote a:focus-visible {
  /* Spelled out rather than currentColor: currentColor in a background resolves
     against this rule's own `color` below, so the fill would come out in the
     ground colour and the pill would disappear at the moment it's hovered. */
  background-color: var(--story-ink, var(--color-text));
  color: var(--story-color);
}

/* ==========================================================================
   Widow control

   Two different problems, so two different properties.

   Running text gets `pretty`: the browser looks ahead at the last few lines
   and shuffles the breaks to avoid leaving a single word stranded on the last
   line. It only touches the end of a paragraph, so the rest of the rag is
   unchanged and long copy doesn't get re-flowed.

   Short display text — a dek, a pullquote, a card title — gets `balance`,
   which evens the line lengths across the whole block instead. That's the
   right answer for two or three lines and the wrong one for a paragraph,
   which is why the two lists are separate. (Browsers cap `balance` at a few
   lines and ignore it beyond that, so a long block silently falls back.)

   Both degrade to normal wrapping where they aren't supported — nothing here
   is load-bearing, it just stops looking as good.

   .story-hero__title is deliberately absent. Where that title breaks is set
   by the Title measure field (see acf-fields.php, "a decision, not an
   outcome") and balancing it would quietly overrule the editor.
   ========================================================================== */
.story-copy p,
.story-lead p,
.story-recipe__intro p,
.story-recipe__method p,
.story-recipe__footnote p,
.story-recipe__note,
.story-subject__text {
  text-wrap: pretty;
}

.story-hero__dek,
.story-pullquote p,
.story-caption__text,
.story-caption--plain,
.story-image-half__caption,
.story-hero-caption,
.story-card__title,
.story-heading,
.story-up-next__title,
.issue-meta__title,
.story-recipe__head,
.story-subject__name,
.block-gate__heading,
.block-gate-recipe__message,
.tier__name,
/* The standfirst under a page title, on all five pages that have one. Two or
   three lines of 24px serif on a centred 46ch measure is exactly the case
   `balance` is for, and exactly the case that strands a word: the natural rag
   put "print." alone under two full lines on Stockists. On the paragraph rather
   than its wrapper — the property is inherited by block containers but only acts
   on the box that holds the lines, and on four of the five the copy comes out of
   the editor already wrapped in a <p>. */
.subscribe-intro__standfirst p,
.stockists-intro__standfirst p,
.contact-intro__standfirst p,
.support-intro__standfirst p,
.about-standfirst p,
.legal-masthead__standfirst {
  text-wrap: balance;
}

/* ==========================================================================
   Subscribe page
   Figma: SUBSCRIBE (319:2). page-subscribe.php + template-parts/tier-card.php.

   Everything is centred and the page is a single column of 80px-separated
   blocks: intro, The Drop, the three paid tiers, the print panel. The Drop is
   deliberately out of the row — it's free, so there's nothing to compare it
   against, and it reads as the first step rather than the cheapest option.
   ========================================================================== */
.subscribe-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-10);
  padding-top: var(--space-10);
  padding-bottom: var(--space-15);
  text-align: center;
}

.subscribe-intro {
  display: flex;
  flex-direction: column;
  gap: var(--space-9);
  align-items: center;
}

.subscribe-intro__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 6.667vw, 6rem);  /* 96px at 1440 */
  line-height: 0.92;                        /* 88/96 */
  font-weight: 400;
}

.subscribe-intro__standfirst {
  font-family: var(--font-serif);
  font-size: clamp(1.125rem, 1.667vw, 1.5rem);  /* 24px */
  line-height: 1.333;
  max-width: 46ch;
}

.subscribe-intro__standfirst p {
  margin: 0;
}

/* --------------------------------------------------------------------------
   The pill CTA — dark by default, accent for the one primary action on the
   page (The Drop). Shared by the tiers and the print panel.
   -------------------------------------------------------------------------- */
.cta-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-2) var(--space-3);
  border-radius: 99px;
  background: var(--color-text);
  color: var(--color-bg);
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
  transition: opacity 0.3s ease;
}

.cta-pill--accent {
  background: var(--color-accent);
  color: var(--color-text);
}

/* The secondary action: the same pill drawn rather than filled. The border eats
   2px of the padding on each side rather than adding to the box, so an outlined
   pill and a filled one stand exactly the same height side by side. */
.cta-pill--outline {
  padding: calc(var(--space-2) - 2px) calc(var(--space-3) - 2px);
  border: 2px solid var(--color-text);
  background: none;
  color: var(--color-text);
}

a.cta-pill:hover,
a.cta-pill:focus-visible {
  opacity: 0.7;
}

/* Not a link: a tier with no checkout URL set. */
.cta-pill.is-unset {
  background: none;
  color: var(--color-text);
  opacity: var(--quiet);
}

/* --------------------------------------------------------------------------
   Tiers
   -------------------------------------------------------------------------- */
.tier {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}

.tier__details {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}

/* Pushes the CTA to a common baseline across the row, whose feature lists
   differ in length. Keyed to the row rather than to the 'plan' variant: The Drop
   sits in it too, and has to stretch with the rest.

   The basis has to stay `auto`: at 0 the details contribute no height, the grid
   row sizes itself to the CTA alone, and the whole card collapses behind it. */
.tier-plans .tier__details {
  flex: 1 0 auto;
}

/* In the row a pill stands where a 40px price would — The Drop's "Free", and
   "Subscribed" on whichever card is the reader's. The pill keeps its own size:
   the difference between the two boxes is split above and below as margin, so it
   sits centred on the line the price would have occupied and the names, features
   and CTAs below stay level across the cards.
   .tier__price is 2.5rem at 1.2; the pill is 0.75rem at 1.333 plus its padding. */
.tier-plans .tier__pill {
  margin-block: calc( ( 2.5rem * 1.2 - ( 0.75rem * 1.333 + var(--space-1) * 2 ) ) / 2 );
}

/* The Drop's price is a pill, not a number — there's no figure to set. */
.tier__pill {
  margin: 0;
  padding: var(--space-1) var(--space-2);
  border-radius: 99px;
  background: var(--color-text);
  color: var(--color-bg);
  font-family: var(--font-mono);
  font-size: 0.75rem;   /* 12px */
  line-height: 1.333;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.tier__price {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-family: var(--font-display);
  font-size: 2.5rem;    /* 40px */
  line-height: 1.2;
  /* No tracking here: at this size it opened a gap between the currency
     symbol and the numeral that the optical sizing below handles far better. */
  letter-spacing: 0;
}

/* Symbol and numeral travel together as one flex item, so the row's gap only
   ever separates the amount from the "/ mo". Their own spacing is set in em
   below so it holds at any size. */
.tier__amount {
  display: inline-flex;
  align-items: center;
  gap: 0.2em;
}

/* Set smaller than the numeral and centred against it — the full-size symbol
   overpowers a two-character price. */
.tier__currency {
  font-size: 0.62em;
}

.tier__period {
  font-size: 1.25rem;   /* 20px */
  line-height: 1.6;
  letter-spacing: 0.05em;
  opacity: var(--quiet);
}

/* "The" is set in anotherSerif against the rest in Matter — see tier-card.php,
   where the two halves are stored separately for exactly this reason. */
.tier__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: 2.5rem;    /* 40px */
  line-height: 1.2;
  font-weight: 400;
}

.tier__name-lead {
  font-family: var(--font-accent);
}

.tier__includes {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding-block: var(--space-3);
}

.tier__inherits {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.75rem;   /* 12px */
  line-height: 1.333;
  letter-spacing: 0.067em;
  text-transform: uppercase;
}

.tier__inherits-name {
  font-family: var(--font-mono);
}

/* align-items: center shrinks each row to its own text, so the dot stays beside
   the label instead of stranded at the left edge of a full-width row. */
.tier__features {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  font-family: var(--font-display);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
}

/* Drawn here rather than loaded as the exported Figma asset. The asset baked the
   accent into its own fill, so it couldn't read --color-accent: every change to
   the brand colour needed the file edited alongside the token, and because a CSS
   asset URL carries no ?ver= a missed upload went stale silently rather than
   failing. An 8px flat circle is the one shape where reproducing it costs
   nothing, so the dot now tracks the token like everything else the accent
   touches, and assets/icons/bullet.svg is unreferenced.

   top/margin rather than centring by flex: this reproduces the old
   `left center` background position exactly, which sits the dot on the middle of
   the whole row — so a feature that wraps to two lines keeps the dot where it
   has always been rather than jumping to the first line. */
.tier__features li {
  position: relative;
  padding-left: var(--space-2);
}

.tier__features li::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 8px;
  height: 8px;
  margin-top: -4px;
  border-radius: 50%;
  background: var(--color-accent);
}

/* --------------------------------------------------------------------------
   The tier row — three across at desktop, matching the 443px cards in Figma
   (3 × 443 + 2 × 16 gutters = the 1361px content measure).

   auto-fit rather than a fixed three: a reader on a paid tier doesn't see The
   Drop, and the two remaining cards should fill the measure rather than leave a
   column-shaped hole where it was. The 280px floor is only there to give auto-fit
   a track width to count with — at this breakpoint four would fit, so three cards
   make three columns and two make two. (minmax(0, 1fr) can't do this: with no
   definite minimum the count collapses to one.) */
.tier-plans {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--grid-gutter);
  align-items: stretch;
}

.tier-plans .tier__cta {
  margin: 0;
}

/* Two pills abreast where a tier can be gifted, one where it can't. They wrap
   rather than squeeze: each is sized by its own label, and three cards across
   leaves them no room to shrink into at the narrower desktop widths. */
.tier__cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

/* --------------------------------------------------------------------------
   Print panel
   An accent wash bled behind the whole block, the two most recent covers
   overlapping at the left, copy at the right. The wash is a blurred block
   rather than a gradient so it keeps the soft, uneven edge from the design.
   -------------------------------------------------------------------------- */
.print-panel {
  position: relative;
  /* Own stacking context, so the wash's z-index: -1 sits behind the panel's
     contents rather than behind the page background entirely. */
  isolation: isolate;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: var(--space-6);
  padding-block: var(--space-10);
  text-align: left;
}

.print-panel__wash {
  position: absolute;
  inset: var(--space-10) 0;
  background: var(--color-accent);
  filter: blur(60px);
  pointer-events: none;
  z-index: -1;
}

.print-panel__covers {
  position: relative;
  justify-self: center;
  display: flex;
  align-items: flex-start;
  padding-bottom: var(--space-8);
}

.print-panel__cover {
  width: 213px;
  height: auto;
  box-shadow: 0 4px 80px rgba(29, 28, 27, 0.4);
}

/* The nearer cover sits lower and overlaps the one behind by 99px, per Figma
   (covers at x=115 and x=229, both 213px wide). */
.print-panel__cover + .print-panel__cover {
  margin-top: var(--space-8);
  margin-left: -99px;
}

.print-panel__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 4.444vw, 4rem);  /* 64px */
  line-height: 0.875;
  font-weight: 400;
}

.print-panel__body {
  margin: var(--space-5) 0 0;
  font-family: var(--font-display);
  font-size: 1.25rem;   /* 20px */
  line-height: 1.6;
  max-width: 40ch;
}

.print-panel__cta {
  margin: var(--space-4) 0 0;
}

@media (max-width: 1023px) {
  .tier-plans {
    grid-template-columns: 1fr;
    gap: var(--space-10);
  }

  .print-panel {
    grid-template-columns: 1fr;
    text-align: center;
    gap: var(--space-8);
  }

  .print-panel__body {
    margin-inline: auto;
  }
}

@media (max-width: 767px) {
  .subscribe-page {
    gap: var(--space-8);
  }

  .subscribe-intro {
    gap: var(--space-5);
  }

  .print-panel__cover {
    width: 150px;
  }
}

/* ==========================================================================
   Single gated block
   template-parts/block-gate.php — stands in for one members-only block where
   the rest of the story is readable.

   Sits on the story's own measure and stays small: the reader hasn't been
   stopped, so this shouldn't read like the end of the piece. Same vocabulary as
   the paywall (accent wash, dark pill) at about a third the scale.

   Not in the Figma file yet.
   ========================================================================== */
/* width: 100% is load-bearing. .container sets margin-inline: auto, and an auto
   margin on the cross axis of a flex column stops the item stretching — without
   this the gate collapses to shrink-to-fit and the wash is clipped to a narrow
   band with hard vertical edges. Clipping belongs out here at the full measure,
   far from the wash, not on the inner. */
.block-gate {
  width: 100%;
  overflow-x: clip;
}

.block-gate__inner {
  position: relative;
  /* Same trick as the paywall wash: both layers are positioned so paint order
     follows the markup. A negative z-index would drop the wash behind the
     page background instead of behind the copy. */
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-block: var(--space-8);
  text-align: center;
}

.block-gate__wash {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* Kept well inside the measure so the blur has room to fall off on its own
     rather than being cut by the clip above. */
  width: min(680px, 80%);
  height: 260px;
  background: var(--color-accent);
  filter: blur(50px);
  pointer-events: none;
}

.block-gate__copy {
  position: relative;
}

.block-gate__heading {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.222vw, 2rem);  /* 32px */
  line-height: 1.25;
}

.block-gate__cta {
  margin: var(--space-3) 0 0;
}

.block-gate__signin {
  margin: var(--space-2) 0 0;
  font-family: var(--font-display);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
}

.block-gate__signin a {
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: opacity 0.3s ease;
}

.block-gate__signin a:hover,
.block-gate__signin a:focus-visible {
  opacity: 0.6;
}

@media (max-width: 767px) {
  .block-gate__wash {
    height: 220px;
    filter: blur(36px);
  }
}

/* --------------------------------------------------------------------------
   Gated recipe — block-gate-recipe.php
   Figma: RECIPE (321:474).

   Photograph, title and serving size on an accent wash, then the ask. None of
   the recipe is rendered at all, so unlike the story paywall there's nothing
   here doing any concealing — no blur, no mask, no fade. The layout is simply
   what a reader without access gets.
   -------------------------------------------------------------------------- */
.block-gate--recipe {
  position: relative;
  overflow-x: clip;
  padding-block: var(--space-10);
}

.block-gate-recipe__wash {
  position: absolute;
  inset: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(1360px, 100vw);
  background: var(--color-accent);
  filter: blur(60px);
  pointer-events: none;
}

/* Photograph and body as one centred pair rather than on the 12-column grid —
   the Figma composition doesn't sit on the columns, and forcing it there moves
   the picture without making anything easier to maintain. */
.block-gate-recipe__inner {
  position: relative;
  display: grid;
  grid-template-columns: 328px minmax(0, 671px);
  gap: 132px;
  justify-content: center;
  align-items: start;
  padding-inline: var(--grid-margin);
}

/* Mirrored — the recipe block's "Mirrored" switch, honoured here so a gated
   recipe keeps the photograph on the side the story puts it on. The columns
   swap width and the figure is ordered second; the text side stays left-aligned
   either way, since it's still the title's own edge it lines up with.

   Scoped to the two-column layout for the same reason .story-recipe--flip is:
   below 1024px the pair stacks and there is no side to flip to. */
@media (min-width: 1024px) {
  .block-gate-recipe--flip .block-gate-recipe__inner {
    grid-template-columns: minmax(0, 671px) 328px;
  }

  .block-gate-recipe--flip .block-gate-recipe__figure {
    order: 2;
  }
}

/* No photograph: the two-column template would leave the text in the 328px
   picture column with the wide one standing empty beside it, so the grid drops
   to the single text column instead. Sized to the text column rather than to
   the pair, since the message and the CTA want the same measure they have when
   a picture is there — and `justify-content: center` on the inner then centres
   the one column in the block. */
.block-gate-recipe--no-image .block-gate-recipe__inner {
  grid-template-columns: minmax(0, 671px);
}

/* And the wash comes in with the column, the same way it does on the ungated
   recipe. 900px is the text column with the surround the full-width wash gives
   the pair kept intact: 1360 around 328 + 132 + 671 leaves 114 either side, so
   671 + 114 + 114. Stated as a number because this block is deliberately off
   the 12-column grid (see .block-gate-recipe__inner) and has none of its
   arithmetic to borrow. */
.block-gate-recipe--no-image .block-gate-recipe__wash {
  width: min(900px, 100vw);
}

.block-gate-recipe__figure {
  margin: 0;
}

.block-gate-recipe__image {
  width: 100%;
  height: auto;
}

/* Relative so the serves stamp can hang off the top-right corner.
   Everything below the title — message, CTA, sign-in — hangs off the same left
   edge as the title rather than being centred under it, so the whole text side
   reads as one column. */
.block-gate-recipe__body {
  position: relative;
  text-align: left;
}

.block-gate-recipe__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 4.444vw, 4rem);  /* 64px at 1440 */
  line-height: 0.875;
  font-weight: 400;
  text-transform: uppercase;
  /* No right padding to clear the stamp, deliberately: reserving its width forces
     "Marzipan Sorbet" onto two lines, and the design has it on one. The stamp is
     an oval lifted above the baseline, so a short overlap with the title's tail
     reads fine — that's how the Figma frame sits. A very long title will run
     under it; if that shows up in real content, wrap the title sooner rather than
     padding it, or the common case pays for the rare one. */
}

/* Body takes the full width when the recipe has no photograph, instead of being
   squeezed into the picture's column. */
.block-gate-recipe__body:only-child {
  grid-column: 1 / -1;
}

/* Same stamp as the real recipe block: a CSS oval. In flow by default, and lifted
   to hang off the title's corner only when there IS a title — with the title
   missing there's no corner to hang from and it would land on the message. */
.block-gate-recipe__serves {
  /* Sits on the body's left edge with everything else when there's no title to
     hang from; centred again once the layout stacks (see the media query). */
  margin: var(--space-4) 0 0;
  display: grid;
  place-items: center;
  width: 160px;
  height: 88px;
  border: 1px solid var(--color-text);
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 16px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.block-gate-recipe__title + .block-gate-recipe__serves {
  position: absolute;
  top: -60px;
  right: 0;
  margin: 0;
}

.block-gate-recipe__message {
  margin: var(--space-7) 0 0;
  font-family: var(--font-display);
  font-size: 1.25rem;   /* 20px */
  line-height: 1.6;
}

.block-gate--recipe .block-gate__cta {
  margin: var(--space-3) 0 0;
}

.block-gate--recipe .block-gate__signin {
  margin: var(--space-3) 0 0;
}

@media (max-width: 1023px) {
  .block-gate-recipe__inner {
    grid-template-columns: minmax(0, 480px);
    gap: var(--space-8);
  }

  .block-gate-recipe__figure {
    justify-self: center;
  }

  /* Stacked under the photograph, the text has no title edge to line up with —
     it's a single centred column, as it was before. */
  .block-gate-recipe__body,
  .block-gate-recipe__title {
    text-align: center;
    padding-right: 0;
  }

  /* Stacked there's no title corner to hang off, so it goes back in the flow.
     Matches the adjacent-sibling selector above or it loses on specificity. */
  .block-gate-recipe__serves,
  .block-gate-recipe__title + .block-gate-recipe__serves {
    position: static;
    margin: var(--space-4) auto 0;
  }
}

/* The same step in as the recipe block it stands in for — the two sit on the
   same wash and should hold the same margin against it. One value does both
   here, since stacked this is a single column with the photograph and the text
   in it. */
@media (max-width: 900px) {
  .block-gate-recipe__inner {
    padding-inline: calc(var(--grid-margin) + var(--space-3));
  }
}

/* ==========================================================================
   Stockists
   page-stockists.php + inc/stockists.php.

   A directory, not an article: the masthead is centred like the subscribe page,
   and everything below it goes left-aligned into columns so someone looking for
   their own city finds it by scanning rather than scrolling. Two levels of
   heading do the work — the region in the display face, the state or country in
   small mono caps — which leaves the shop names themselves as the only editorial
   type in the column and the thing the eye lands on.
   ========================================================================== */
.stockists-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  padding-top: var(--space-10);
  padding-bottom: var(--space-15);
}

.stockists-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
  text-align: center;
}

.stockists-intro__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 6.667vw, 6rem);  /* 96px at 1440 */
  line-height: 0.92;                        /* 88/96 */
  font-weight: 400;
}

.stockists-intro__standfirst {
  font-family: var(--font-serif);
  font-size: clamp(1.125rem, 1.667vw, 1.5rem);  /* 24px */
  line-height: 1.333;
  max-width: 46ch;
}

.stockists-intro__standfirst p {
  margin: 0;
}

/* --------------------------------------------------------------------------
   Wholesale panel — trade enquiries rather than readers, so it's set apart in
   the same accent the subscribe page reserves for the one thing on a page that
   isn't the main path.

   Built as the subscribe page's print panel: the wash is a blurred block rather
   than a filled rectangle, which is what gives it the soft uneven edge from the
   design, and the type matches it exactly. The one difference is that the print
   panel is a two-column grid pairing its copy with a pair of covers — this has
   no picture to show, so the copy runs the full width on its own.
   -------------------------------------------------------------------------- */
.wholesale-panel {
  position: relative;
  /* Own stacking context, so the wash's z-index: -1 sits behind the panel's
     contents rather than behind the page background entirely. */
  isolation: isolate;
  /* Deeper than the print panel's 80px. There the copy is balanced by a pair of
     covers beside it; here it's alone in the middle of the wash, and it needs
     accent above and below it to sit in rather than float on. */
  padding-block: var(--space-15);
  text-align: center;
}

/* Inset less than the padding, so the solid part of the wash reaches past the
   copy on both sides before the blur takes over. */
.wholesale-panel__wash {
  position: absolute;
  inset: var(--space-6) 0;
  background: var(--color-accent);
  filter: blur(60px);
  pointer-events: none;
  z-index: -1;
}

.wholesale-panel__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 4.444vw, 4rem);  /* 64px */
  line-height: 0.875;
  font-weight: 400;
}

.wholesale-panel__body {
  /* No measure cap, unlike the print panel's 40ch. There are only two sentences
     here and each is meant to land on one line — the container's own 1440px is
     the limit, and a 60ch cap was breaking both of them in half for no reason. */
  margin: var(--space-5) auto 0;
  font-family: var(--font-display);
  font-size: 1.25rem;   /* 20px */
  line-height: 1.6;
}

.wholesale-panel__body p {
  margin: 0;
}

.wholesale-panel__body p + p {
  margin-top: var(--space-1);
}

/* The two addresses are the point of the panel, so they take the same outlined
   pill a link in the middle of a story takes: findable without hovering, which an
   underline in a block of centred display type is not, and filling on hover.

   Outlined in ink rather than in either accent. --story-color would be the story
   pill's orange on a page that has no story; --color-accent is the wash the panel
   is sitting on, and a cyan outline on cyan is no outline at all. Ink filling to
   ink with the type knocked out to the page colour is the same pair the CTA pills
   run on, and it's the only ink on the wash either way.

   The em padding and box-decoration-break are the body pill's, for the reasons
   given in *Links in body copy*: the panel's 20/32 leading is the body's, so 0.1em
   sits inside it, and a wrapped address needs each line closed off on its own. */
.wholesale-panel__body a {
  padding: 0.1em 0.4em;
  border: 2px solid var(--color-text);
  border-radius: 99px;
  color: var(--color-text);
  text-decoration: none;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-color 0.3s ease, color 0.3s ease;
}

.wholesale-panel__body a:hover,
.wholesale-panel__body a:focus-visible {
  background-color: var(--color-text);
  color: var(--color-bg);
}

/* --------------------------------------------------------------------------
   A region and its columns
   -------------------------------------------------------------------------- */
.stockist-region + .stockist-region {
  margin-top: calc(var(--space-4) * -1);  /* the page's 96px gap is too much between two lists */
}

/* Centred, with nothing under it. The rule and the tally that used to sit on it
   were doing a job the columns below already do — a heading in the display face
   with clear air around it is enough of a break between two lists. */
.stockist-region__title {
  margin: 0 0 var(--space-8);
  font-family: var(--font-display);
  font-size: clamp(2rem, 3.333vw, 3rem);  /* 48px at 1440 */
  line-height: 1.1;
  font-weight: 400;
  text-align: center;
}

/* Columns rather than a grid: the places run to wildly different lengths —
   California has eight shops, Georgia one — and a grid would leave a row of
   holes as tall as the longest cell in it. Columns pack them. The reading order
   goes down each column and back up, which is right for an index of headed
   groups in a way it wouldn't be for continuous prose. */
.stockist-region__places {
  column-count: 3;
  column-gap: var(--space-6);
}

.stockist-place {
  break-inside: avoid;
  margin-bottom: var(--space-6);
}

/* Nothing after the last group in a column, or the region's own gap doubles up
   on whichever column happens to be tallest. */
.stockist-place:last-child {
  margin-bottom: 0;
}

.stockist-place__name {
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 20px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 400;
  opacity: var(--quiet);
}

/* An entry can be two lines, so the gap between two of them has to beat the gap
   inside one — at 8px a name sat as close to the shop above it as to its own
   address. */
.stockist-place__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Two lines, one face: the name carries the size, and the address sits at half
   strength under it — present when you want it, out of the way while you're
   scanning down a column of fifty names. */
.stockist__name {
  display: block;
  font-family: var(--font-display);
  font-size: 32px;
  line-height: 1.15;
}

/* The name gets the site's underline, wiped in left to right on hover with the
   link mark and the address — the shop is a link, and this is how a link says so
   everywhere else on the site.

   Drawn as a background gradient rather than the shared ::after bar up in *Shared
   nav link styling*. That bar is absolutely positioned against the link's box,
   which works for a nav item that never wraps and fails here: a name like
   Ampersand Vintage Modern takes two lines in a column, and one bar at the bottom
   of the box would sit under the second line at the width of the first. A
   background is painted per line fragment, so `clone` gives every line its own,
   and all of them grow together.

   Only where the entry is actually a link. A shop with no website underlines
   nothing — there is nowhere for it to go, and the entry has no hit area beyond
   the row it sits in, so a hover anywhere along the column would otherwise light
   up a name that does nothing. :has() is the guard; without it nothing draws,
   which is exactly today's page. */
.stockist__text {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-size 0.3s ease;
}

.stockist:has(.stockist__link):hover .stockist__text,
.stockist:has(.stockist__link):focus-within .stockist__text {
  background-size: 100% 1px;
}

.stockist__address {
  display: block;
  margin-top: var(--space-1);
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
  opacity: var(--quiet);
  transition: opacity 0.25s ease;
  /* The address is a later sibling than the link, so it paints over the link's
     click panel and would swallow a click aimed straight at the street name.
     Waving pointer events through puts the whole entry back in play — it costs
     the ability to select the address as text, which is a fair trade for the
     line being part of a link in the first place. */
  pointer-events: none;
}

/* Hovering the entry brings the address up to full strength alongside the link
   mark appearing — the whole entry lights up as one thing, which is also what
   it now behaves as. */
.stockist:hover .stockist__address,
.stockist:focus-within .stockist__address {
  opacity: 1;
}

/* The website, as a mark on the end of the name. Inline rather than a block so
   it follows the last word when a long name wraps.

   Hidden until the entry is hovered — anywhere on it, name or address, which is
   why the rule hangs off the <li> rather than the name. A column of these
   showing at once reads as a row of ticks down the page and pulls the eye off
   the names; revealed one at a time it answers a question the reader is already
   asking about that shop. Opacity rather than display, so it fades rather than
   snapping, and so it never changes the line's width. */
.stockist__link {
  display: inline-block;
  margin-left: 6px;
  /* The glyph's box is 14px against a 37px line, so it centres high on the
     name's baseline. */
  vertical-align: 1px;
  opacity: 0;
  color: color-mix(in srgb, var(--color-text) 45%, transparent);
  transition: opacity 0.25s ease, color 0.25s ease;
}

.stockist__link svg {
  display: block;
}

/* The click target is the whole entry, not the 14px glyph. Rather than wrap the
   name and address in the anchor — block content inside an inline link, and a
   screen reader then reading the address as part of the link's name — the
   anchor throws a transparent panel over the entry it belongs to. The cursor,
   the hit area and the hover all follow from that one rule.

   It works while the mark itself is invisible because an element at opacity 0
   is still hit-tested; only display/visibility take it out of play. */
.stockist {
  position: relative;
}

.stockist__link::after {
  content: '';
  position: absolute;
  inset: 0;
  cursor: pointer;
}

.stockist:hover .stockist__link,
/* Keyboard users never trigger the hover, and a link that can be tabbed to but
   not seen is worse than one that's always visible. */
.stockist__link:focus-visible {
  opacity: 1;
}

.stockist__link:hover,
.stockist__link:focus-visible {
  color: var(--color-text);
}

/* Nothing hovers on a touchscreen, so there the mark simply stays. */
@media (hover: none) {
  .stockist__link {
    opacity: 1;
  }
}

@media (max-width: 1199px) {
  .stockist-region__places { column-count: 2; }
}

@media (max-width: 767px) {
  .stockists-page {
    gap: var(--space-8);
    padding-bottom: var(--space-12);
  }

  /* One column. Two of ~160px each was fine while an entry was just a name, but
     a 32px name over an address has no room in half a phone. The page gets long;
     scrolling a long list beats reading a broken one.

     The name keeps its full 32px here — one column is wide enough for it, and
     it's the size that makes the list scannable in the first place. */
  .stockist-region__places {
    column-count: 1;
  }

  /* Shallower than the desktop 120px, but the wash stays inset well inside it so
     there's still accent above and below the copy rather than a band cropped to
     it. padding-block rather than padding: any inline padding would pull the
     accent in off the page margin. */
  .wholesale-panel {
    padding-block: var(--space-12);
  }

  .wholesale-panel__wash {
    inset: var(--space-4) 0;
  }
}

/* ==========================================================================
   About
   page-about.php.

   One centred column the whole way down, opened by a full-bleed hero and broken
   by full-bleed photographs. The copy sits on a narrow measure in the middle of
   a wide page on purpose — it's a statement of intent, not an article, and it
   should read like one voice speaking rather than a column of body text.
   ========================================================================== */
/* The hero and the flow are siblings rather than items in one stack: the flow
   carries its own rhythm and gets pulled up over the hero on scroll, which a
   shared flex gap would fight. */
.about-flow {
  display: flex;
  flex-direction: column;
  gap: var(--space-15);
  padding-bottom: var(--space-15);
}

/* Every section spans the flow, and any narrower measure is set on the element
   itself.

   Needed because these all carry .container, whose `margin-inline: auto` is an
   auto margin on a flex item's cross axis — which suppresses the stretch a
   column flex container would otherwise apply, leaving each section shrunk to
   its own content width and centred. A max-width declared on the element still
   wins over this, so the standfirst and the copy keep their measures. */
.about-flow > * {
  width: 100%;
}

/* Set by initAboutHero() along with the negative margin — the flow has to be
   above the hero it's arriving over. Matches .story-content-flow--overlaps. */
.about-flow--overlaps {
  position: relative;
  z-index: 1;
}

/* --------------------------------------------------------------------------
   Hero — outside .container, headline sitting on the picture
   -------------------------------------------------------------------------- */
/* The pinned pair, same shape as .story-hero-wrapper / .story-hero: the wrapper
   is cut taller than the viewport by initAboutHero() and the hero sticks to the
   top of it while that extra height scrolls past. Left alone — no JS, or reduced
   motion — the wrapper is exactly as tall as the hero and nothing pins. */
.about-hero-wrapper {
  position: relative;
}

/* Starts at the very top of the document with the wordmark and nav over it —
   see .overlay-hero above, and the body class in header.php. Nothing to offset
   here as a result: the header reserves no space to clear.

   Full viewport height rather than a fraction of it, which is what puts the
   picture's bottom edge off-screen and retired the gradient that used to hide
   it. See the note on initAboutHero(). */
.about-hero {
  position: sticky;
  top: 0;
  height: 100vh;
  /* Not clipped. The clip it used to carry was for the backdrops, which are
     scaled up a touch and would otherwise show their edges — so it has moved onto
     those (see .about-hero__backdrops) and the headline is free to run past the
     bottom of a short window instead of being cut off mid-line there. Now that
     the headline starts at a fixed offset rather than centring itself in the
     frame, that's a case that can actually happen: a long headline on a 1440×700
     window finishes about 10px below the fold. It scrolls into view as the hero
     leaves, which is what the same headline would do on any other page. */
}

/* The layer the scroll-fade drives, so the headline's own centring transform
   survives it. Same split as .story-hero__content, same reason. */
.about-hero__content {
  position: absolute;
  inset: 0;
  will-change: opacity, filter, transform;
}

/* The stack of photographs the thumbnails switch between. Its opacity and blur
   are what initAboutHero() drives on scroll; the layers inside are what the
   thumbnails drive on hover, so the two never write to the same property. */
/* Carries the clip the hero used to — the pictures inside are scaled to 1.04 so
   their blurred edges stay off-screen, and that only works against a boundary. */
.about-hero__backdrops {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.about-hero__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* A blurred layer samples from outside its own box, so its edges would go
     translucent mid-fade and show the page through them. Scaling it a touch
     keeps those edges off-screen for the whole of the dissolve. */
  transform: scale(1.04);
  /* Every layer rests hidden and slightly out of focus; the active one resolves.
     Blur as well as opacity so a swap reads as one picture coming into focus as
     another leaves, rather than a straight dissolve. */
  opacity: 0;
  filter: blur(16px);
  transition: opacity 0.6s ease, filter 0.6s ease;
}

.about-hero__image.is-active {
  opacity: 1;
  filter: blur(0px);
}

/* .title-connector inside it picks up the accent face from the shared rule —
   the same treatment a story title gets on "Paths *to* Pan Chino".

   The scroll exit — opacity, blur and drift — is written by initAboutHero() onto
   .about-hero__content, the layer around this one, so the transform below stays
   the stylesheet's to own. */
/* Started where every other page's heading starts.

   `top` marks the top edge, not the centre — this is the one page whose header
   is out of flow, so the heading has to be told the offset the flow would
   otherwise have given it: the chrome's own height plus the same --space-10 that
   .subscribe-page and .stockists-page put above their titles. Both come out at
   377px on a 1440-wide window, and they track together at every breakpoint
   because --chrome-block is built from the header's own terms rather than
   measured once.

   It used to be centred on 39.2% + 88px, a story hero's arrangement. That holds
   a long headline in the middle of the frame whatever it wraps to, which is
   right for a two-word story title and wrong here: it left the About headline
   starting a good 145px higher than the heading on any other page, so the set of
   pages didn't read as one set.

   The horizontal centre is still the window's, not a 1440px column's. */
.about-hero__title {
  position: absolute;
  top: calc(var(--chrome-block) + var(--space-10));
  left: 50%;
  transform: translateX(-50%);
  /* Sized to its own text, not to a measure. A story title takes an explicit
     --story-title-width because where it breaks is art direction; here the
     editor types the breaks into the field itself, so a width would only
     override them — the cap just stops the longest line reaching the edges.

     An explicit width rather than shrink-to-fit: an absolutely positioned box
     with `left: 50%` and no `right` gets an available width of the containing
     block *minus* that offset, so left to itself it settles at half the hero —
     a measure nobody chose.

     Ten of the page's twelve columns, per the Figma — 1131px at a 1440 artboard,
     which is what the arithmetic below comes to. Derived from the grid tokens
     rather than hard-coded so it stays ten columns when the page margin steps
     down at each breakpoint, instead of drifting into some other fraction.

     Ten columns of --about-title-max rather than of --container-max: this is the
     one thing on the page that is centred on the *window* rather than sitting in
     the 1440 column, and stopping it dead at 1440 left a small headline adrift in
     the middle of a 25" monitor. The rest of the page keeps its 1440 grid; only
     the headline is allowed the extra room, which is why the ceiling is declared
     here rather than at :root. */
  --about-title-max: 1920px;

  width: calc(
    (min(100vw, var(--about-title-max)) - 2 * var(--grid-margin) - 11 * var(--grid-gutter)) / 12 * 10
    + 9 * var(--grid-gutter)
  );
  margin: 0;
  font-family: var(--font-display);
  /* 72px at 1440, growing with the measure to 96px at 1920 and holding there.

     The two have to grow together or the headline changes shape as the window
     widens: the column and the type both scale linearly off the viewport, so the
     line holds the same ~15.5 characters and the headline keeps its line count
     from a laptop to a large desktop. Raising the type alone would have wrapped
     it from six lines to eight and run it off the bottom of the hero. */
  font-size: clamp(2.5rem, 5vw, 6rem);
  line-height: 0.889;                     /* 64px leading at 72px, per the Figma */
  font-weight: 400;
  text-align: center;
  /* No text-wrap: balance, deliberately — and the story title doesn't use it
     either. Balancing redistributes words to even the lines out, which quietly
     overrides the breaks the editor typed into the headline field. */
}

/* The pictures set into the headline, in two parts.
   -------------------------------------------------------------------------- */

/* The slot: an empty inline box in the headline that reserves the space and
   nothing else. 108×72 at the Figma's 72px type, expressed in em so it scales
   with the clamp instead of stranding at one size — 1.5em wide, 1em tall, the
   same 3:2 at any viewport.

   The negative block margin is what keeps it out of the leading. An inline-block
   contributes its *margin* box to the line, and this stands 1em against a
   0.889em line, so without it every line carrying a picture would push its
   neighbours apart and the headline's rhythm would depend on where the editor
   put the tokens. */
.about-hero__slot {
  display: inline-block;
  width: 1.5em;
  height: 1em;
  margin-block: calc((1em - 0.889em) / -2);
  margin-inline: 0.08em;
  padding: 0;
  border: 0;
  vertical-align: baseline;
  background: none;
  cursor: pointer;
  /* Empty, and the only thing here that takes pointer events: the picture it
     covers sits in a layer below the headline, so the words paint over it and
     this catches the hover on its behalf. */
}

.about-hero__slot:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* The layer: the actual photographs, outside the headline so they can hold still
   while it drifts away. Positioned onto the slots by initAboutThumbs(), which is
   why there are no coordinates here. */
.about-hero__thumbs {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Presentational. The control is the slot in the headline directly above it —
   nothing in this layer takes a pointer event, which is what lets the words sit
   over the pictures without stealing the hover from them. */
.about-hero__thumb {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  /* No overflow: hidden. A blurred child samples from outside its own box, and
     clipping it here would cut that spill off at the rectangle — the picture
     would go soft in the middle and stay hard-edged, which reads as a blurred
     photograph behind a sharp window rather than a photograph dissolving. Let it
     out and the edges fade with everything else.

     Nothing overflows at rest anyway: the image is sized to the box exactly, and
     its corners are square. */
  /* Driven by initAboutHero() on scroll — they hold still, then leave late. */
  will-change: opacity, filter, transform;
}

.about-hero__thumb-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: filter 0.6s ease;
}


/* --------------------------------------------------------------------------
   Standfirst and passages
   -------------------------------------------------------------------------- */
/* Standfirst and passages in one box, and a positioned one: it is the containing
   block for .about-scatter, and it has to be the element initAboutScatter()
   measures against. If the layer resolved to a positioned ancestor further up
   instead — .about-flow did, once — the two coordinate spaces would differ by
   whatever sits between them and every picture would land that far down the page.
   Invisible in testing, because a picture in the wrong margin is still in a
   margin.

   Flexed with the same 120px gap .about-flow runs on, so pulling these two out of
   the flow and into a wrapper changes nothing about the rhythm. */
.about-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-15);
  position: relative;
}

.about-standfirst {
  /* Above the scatter layer, for the reason given on .about-passage__copy. */
  position: relative;
  z-index: 1;
  max-width: 44ch;
  margin-inline: auto;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.5vw, 2.25rem);  /* 36px at 1440 */
  line-height: 1.28;
  text-align: center;
}

.about-standfirst p {
  margin: 0;
}

/* One rhythm the whole way down the body: the space under a paragraph, the space
   under the photograph beneath it, and the space before the next paragraph are
   all the same 120px the rest of the page runs on (--space-15, as .about-flow
   uses). At 96px the gap inside a passage read tighter than the gap between
   them, which made a picture look attached to the paragraph above it rather
   than sitting in its own air. */
.about-passages {
  display: flex;
  flex-direction: column;
  gap: var(--space-15);
  position: relative;
}

.about-passage {
  display: flex;
  flex-direction: column;
  gap: var(--space-15);
  /* Containing block for the scattered thumbnails — see .about-scatter. */
  position: relative;
}

.about-passage__copy {
  max-width: 62ch;
  /* .container already sets padding-inline; the auto margins are what centre the
     measure inside it. */
  margin-inline: auto;
  font-family: var(--font-display);
  font-size: 1.25rem;   /* 20px */
  line-height: 1.6;
  text-align: center;
}

.about-passage__copy p {
  margin: 0;
}

.about-passage__copy p + p {
  margin-top: var(--space-3);
}

/* Full-bleed, edge to edge, like the story flow's image_full block. Nothing is
   cropped here — see the note in the template — so a portrait photograph run to
   the full width of the window stands taller than the window itself. That's the
   trade the width buys, and it's a reason to feed this field landscape. */
.about-passage__figure {
  margin: 0;
}

.about-passage__image {
  width: 100%;
  height: auto;
}

/* The caption keeps the page margins the picture above it has given up. */
.about-passage__caption {
  margin: var(--space-2) 0 0;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 18px;
  color: var(--color-text);
  text-align: center;
}

/* --------------------------------------------------------------------------
   Scattered thumbnails

   Small pictures out in the margins beside the passages. Hovering one fades its
   photograph in behind the whole page and letting go fades it out again — the
   opposite of the headline thumbnails, which stay where you put them. These are
   passed rather than arrived at: the reader is reading, and the picture is a
   glance, not a destination.
   -------------------------------------------------------------------------- */

/* Fixed, full-screen, and *behind* the copy — above the page's background and
   below .about-flow, which already carries a z-index for riding over the hero.
   Not z-index: -1, which would put it behind the body's background colour and
   make it invisible. */
.about-scatter-backdrops {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.about-scatter-backdrops__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  filter: blur(16px);
  transition: opacity 0.6s ease, filter 0.6s ease;
}

.about-scatter-backdrops__image.is-active {
  opacity: 1;
  filter: blur(0px);
}

/* No wash over the photograph, deliberately. One went in here so the paragraphs
   stayed readable across it, and it made the picture look like a backdrop behind
   a page rather than a picture. The copy is hard to read while a thumbnail is
   held — that's the trade, and it only lasts as long as the pointer does. */
.about-flow {
  position: relative;
  z-index: 1;
}

/* Positioned by initAboutScatter(), which is why there are no coordinates here.
   The layer spans the standfirst and the passages together; the pictures land in
   the margins either side of the text column, alternating left and right,
   starting beside the standfirst and working down. They drift on scroll — see
   .about-scatter__thumb.

   Below the copy rather than above it. They are placed clear of the text and so
   should never meet it — but "should never" is doing a lot of work in a layout
   that depends on a measurement, and if one ever does land wide the words are
   the thing that has to survive it. */
.about-scatter {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.about-passage__copy {
  position: relative;
  z-index: 1;
}

/* Width and height are written by initAboutScatter(), which measures one of the
   headline thumbnails and matches it — so these are the same object as those,
   met again further down the page, rather than a second set at their own size. */
.about-scatter__thumb {
  position: absolute;
  top: 0;
  left: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  pointer-events: auto;
  /* Unclipped and square-cornered — see .about-hero__thumb for why: the hover
     blur has to be allowed past the rectangle or it stops at a hard edge. */
  /* transform is written by initAboutScatter() on scroll — each picture hangs
     back a little as the copy beside it moves, so the margins read as a plane
     behind the words. No transition on it: the value is recomputed every frame,
     and easing toward a moving target only adds lag. */
  will-change: transform;
}

/* Until initAboutScatter() has found room for one it has no coordinates, and
   would sit in the corner of the layer. Out of the document until placed. */
.about-scatter__thumb:not(.is-placed) {
  display: none;
}

.about-scatter__thumb:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
}

.about-scatter__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: filter 0.6s ease;
}


@media (max-width: 767px) {
  /* They stay on a phone, and they land differently: there is no margin to put
     them in — the text column is the page — so they sit inside the two edges of
     the window instead and are allowed to overlap the words. Where they go is
     decided in initAboutScatter(); this is only what changes about them here.

     Over the copy rather than under it, which is the reverse of the desktop
     order. On a wide screen they are placed clear of the text and the words win
     any argument; here an overlap is the arrangement rather than a failure, and
     a picture the reader can see half of but not tap is worse than a line of
     type behind a photograph. */
  .about-scatter {
    z-index: 2;
  }
}

/* --------------------------------------------------------------------------
   Holding a photograph

   While a thumbnail is held, its photograph is the page — and the page gets out
   of its way entirely. The words go: the headline, the copy, the masthead, the
   support panel, the wordmark and the nav all fade to nothing over the same 0.6s
   the picture arrives on. The pictures stay. All the thumbnails hold, not only
   the one being held, and so does the full-screen photograph further down: they
   are the same kind of thing as the picture filling the window, and clearing
   them would have the effect hide *images* to show an image. It is the reading
   that leaves, not the pictures.

   Opacity rather than a blur, which is what this was first. A blur says "still
   here, out of focus" and leaves a grey rhythm of lines over the photograph; at
   zero there is only the photograph, which is the whole point of holding one.

   The class goes on <body> because the two sets of thumbnails are hundreds of
   pixels apart and the effect reaches the whole document from either of them —
   see setAboutHold().

   Why each selector is spelled out rather than swept up by a parent:

   - .about-hero__backdrops and .about-scatter-backdrops are the photograph,
     .about-hero__thumbs is the small copies of it, and .about-break is the
     full-screen picture further down — every image on the page stays;
   - .about-scatter is the layer the thumbnails in the margins live in, so its
     parent .about-body can't be faded wholesale — hence the two `>` lists below,
     which take every sibling *around* those two layers instead;
   - .site-header is left alone: .site-menu is a fixed-position panel inside it,
     and it is opened from a control that would be invisible at the moment it was
     pressed. Its three visible parts are faded one by one instead;
   - .primary-nav likewise carries the transform that pins and reveals it, and
     the transition that goes with it. Taking the lists and the icon inside it
     leaves that alone.

   Nothing here is display: none or visibility: hidden, and that is deliberate:
   an element at zero opacity still takes pointer events, so the slot under the
   reader's pointer keeps receiving them and can still fire the leave that brings
   the page back. Hiding the headline outright would strand the hover on. */
.wordmark,
.menu-toggle,
.nav-list,
.about-hero__title,
.about-flow > *,
.about-body > * {
  transition: opacity 0.6s ease;
}

.is-holding-photo .wordmark,
.is-holding-photo .menu-toggle,
.is-holding-photo .nav-list,
.is-holding-photo .about-hero__title,
.is-holding-photo .about-flow > :not(.about-body):not(.about-break),
.is-holding-photo .about-body > :not(.about-scatter) {
  opacity: 0;
}

/* Invisible text is still selectable and still a tab stop, and a caret landing
   in type nobody can see is worse than the type being gone. Only for the moment
   it lasts. */
.is-holding-photo .about-hero__title,
.is-holding-photo .about-flow > :not(.about-body):not(.about-break),
.is-holding-photo .about-body > :not(.about-scatter) {
  user-select: none;
}

@media (prefers-reduced-motion: reduce) {
  /* The fade is the whole of the effect, so there is nothing to keep once the
     transition is gone — the entire page's type blinking out on every hover is
     far worse than not doing it. The photograph still fades in behind the words;
     that one is the point, and it is legible either way. */
  .is-holding-photo .wordmark,
  .is-holding-photo .menu-toggle,
  .is-holding-photo .nav-list,
  .is-holding-photo .about-hero__title,
  .is-holding-photo .about-flow > :not(.about-body):not(.about-break),
  .is-holding-photo .about-body > :not(.about-scatter) {
    opacity: 1;
  }
}

/* --------------------------------------------------------------------------
   Photograph credit

   The same pill as The Drop's price on the subscribe page (.tier__pill) — ink
   ground, page-coloured mono, fully rounded — because it is the same object: a
   short label that has to read as a label rather than as a line of the page.

   Fixed to the bottom of the window, centred, and only there while a photograph
   is. It sits above everything, including the copy, because at that moment the
   copy is blurred and this is one of two things on the page in focus.
   -------------------------------------------------------------------------- */
.about-credit {
  position: fixed;
  bottom: var(--space-5);
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  margin: 0;
  max-width: calc(100% - var(--grid-margin) * 2);
  padding: var(--space-1) var(--space-2);
  border-radius: 99px;
  background: var(--color-text);
  color: var(--color-bg);
  font-family: var(--font-mono);
  font-size: 0.75rem;   /* 12px */
  line-height: 1.333;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-align: center;
  /* Never in the way of the photograph it names, or of a thumbnail it happens to
     land over. */
  pointer-events: none;
  opacity: 0;
  /* Arrives a little after the photograph and leaves with it: the picture is
     what the reader asked for, and a label appearing at the same instant reads
     as chrome that was already there. */
  transition: opacity 0.4s ease 0.2s;
}

.about-credit.is-visible {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .about-credit {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   The full-screen photograph between the reading and the masthead

   The one picture on this page that is cropped. Everywhere else the rule is that
   what's uploaded is what shows — the passage photographs, the story blocks and
   the issue cards all run to the width of the window and take whatever height
   that gives them. This one is asked to fill the window instead, which no single
   file can do at every aspect ratio, so it covers and centres. That is the whole
   difference between it and .about-passage__figure, and it's why the field says
   so in as many words.
   -------------------------------------------------------------------------- */
.about-break {
  margin: 0;
  width: 100%;
  height: 100vh;
}

.about-break__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --------------------------------------------------------------------------
   Masthead
   -------------------------------------------------------------------------- */
.about-team__heading {
  margin: 0 0 var(--space-8);
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 4.444vw, 4rem);  /* 64px — the same as "Support us" */
  line-height: 0.875;
  font-weight: 400;
  text-align: center;
}

/* Three across at full width, and the roles run to wildly different lengths, so
   this is a grid rather than columns: a masthead should line up in rows. */
.about-team__list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-8) var(--grid-gutter);
  margin: 0;
  text-align: center;
}

.about-team__member {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.about-team__role {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 20px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: var(--quiet);
}

.about-team__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: 24px;
  line-height: 1.25;
}

/* --------------------------------------------------------------------------
   Support panel — the wholesale panel's wash and shape, one page over
   -------------------------------------------------------------------------- */
.support-panel {
  position: relative;
  isolation: isolate;
  /* Grows with the viewport rather than capping at 1440, the way a recipe block
     does inside a story — it carries the page's margins and nothing else. */
  max-width: none;
  padding-block: var(--space-15);
  text-align: center;
}

/* Sized like the two other accent washes on the site: vertically it covers the panel's
   content box and no more, the way .print-panel__wash does against its own
   padding, and horizontally it holds the recipe gate's measure rather than
   running the full width of a wide window (.block-gate-recipe__wash).

   Set as four sides rather than `inset` plus overrides: left, right and width
   together over-constrain a positioned box, and the one the browser drops is not
   the one you meant. */
.support-panel__wash {
  position: absolute;
  /* Inset less than the panel's own 120px padding, so the accent carries on past
     the copy at both ends instead of stopping level with it. The width still
     holds the recipe gate's measure. */
  top: var(--space-8);
  bottom: var(--space-8);
  left: 50%;
  transform: translateX(-50%);
  width: min(1360px, 100vw);
  background: var(--accent-wash);
  filter: blur(60px);
  pointer-events: none;
  z-index: -1;
}

/* Back to the flat accent, for a panel that is asked to stay as it was. The
   gradient is the default here because the panels are one element and most of
   them want it; this is the opt-out rather than the other way round, so a panel
   added later joins the many rather than the one. */
.support-panel--flat .support-panel__wash {
  background: var(--color-accent);
}

.support-panel__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 4.444vw, 4rem);  /* 64px */
  line-height: 0.875;
  font-weight: 400;
}

.support-panel__body {
  max-width: 62ch;
  margin: var(--space-5) auto 0;
  font-family: var(--font-display);
  font-size: 1.25rem;   /* 20px */
  line-height: 1.6;
}

.support-panel__body p {
  margin: 0;
}

.support-panel__cta {
  margin: var(--space-5) 0 0;
}

/* The homepage's closing panel, and the last thing before the footer — which
   has a padding-bottom and no padding-top, so the wordmark below would sit
   120px under the button where the shop row above sits 240px over the title.
   This is the other 120.

   Margin rather than more padding: the wash is positioned against the panel's
   own box and sits symmetrically 56px clear of the content at both ends, so
   growing the box would push its bottom edge down without moving its top and
   leave the accent trailing off under the button. */
.support-panel--closing {
  margin-bottom: var(--space-15);
}

/* --------------------------------------------------------------------------
   Fiscal sponsor — the quietest thing on the page, and the last
   -------------------------------------------------------------------------- */
/* Label, mark, label. The outer cells are equal fractions so the logo sits on
   the panel's centre line rather than the midpoint of two labels of unequal
   length. */
.about-sponsor {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  /* 112px, so each label stands 80px further off the mark than the 32px it
     started at. The gap is the whole of the spacing here — the 1fr tracks
     absorb everything outside it — so this number is the one to move. */
  gap: var(--space-14);
  margin-top: var(--space-10);
  /* The wash behind the panel holds min(1360px, 100vw) — see
     .support-panel__wash — and the 1fr tracks would otherwise carry the two
     labels out to the page margins, which on a 2000px window strands them on
     the off-white either side of the blue. Capped a little inside the wash so
     the three cells stay on it at any width. */
  max-width: 1100px;
  margin-inline: auto;
}

.about-sponsor__note,
.about-sponsor__status {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

/* Each label is centred in its 1fr track and then drawn 80px in toward the
   logo, so the three read as one line of type with a mark in it rather than two
   labels stranded at opposite edges of a very wide block. Centred rather than
   ranged toward the logo because the status breaks onto two lines after "(3)"
   (see the templates), and two lines ranged left read ragged. Mobile stacks and
   drops the shift; see the queries below. */
.about-sponsor__note,
.about-sponsor__status {
  text-align: center;
}

@media (min-width: 768px) {
  .about-sponsor__note {
    transform: translateX(80px);
  }

  .about-sponsor__status {
    transform: translateX(-80px);
  }
}

.about-sponsor__mark {
  display: flex;
  justify-content: center;
}

.about-sponsor__logo {
  width: auto;
  max-height: 56px;
}

@media (max-width: 1023px) {
  .about-team__list {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 767px) {
  /* 70% of a phone is a 260px column, and a headline of any length towers in it
     — twelve lines of 40px type for the current one. The proportion that reads
     as generous margin on a desktop reads as a chimney here, so the measure
     opens up and the page margin does the spacing instead.

     It has to live in *this* block rather than beside the other mobile rules
     further up: the About section was appended after Stockists, so its base
     `width: 70vw` comes later in the file and beats an earlier media query at
     the same specificity. */
  .about-hero__title {
    width: 88vw;
  }

  .about-flow {
    gap: var(--space-10);
    padding-bottom: var(--space-12);
  }

  .about-passages,
  .about-passage {
    gap: var(--space-8);
  }

  .about-team__list {
    grid-template-columns: 1fr;
    gap: var(--space-6);
  }

  .support-panel {
    padding-block: var(--space-12);
  }

  /* Three cells across a phone leaves each label two words wide. Stacked, and
     centred, since there's no longer a left and a right to hang off. */
  .about-sponsor {
    grid-template-columns: 1fr;
    gap: var(--space-3);
    text-align: center;
  }

  .about-sponsor__note,
  .about-sponsor__status {
    text-align: center;
  }

  .support-panel__wash {
    top: var(--space-6);
    bottom: var(--space-6);
  }
}

/* ==========================================================================
   Memberful overlay
   ==========================================================================
   Log in, Account and checkout all open in Memberful's own overlay: a
   full-viewport <div id="memberful-overlay"> that embed.js drops at the end of
   <body>, holding a transparent, full-viewport iframe served from
   synonym-mag.memberful.com.

   That split is the whole constraint here. Everything *around* the form is
   ours; everything *inside* it is a cross-origin document we cannot reach with
   a stylesheet — its card, type and button colour come from Memberful's own
   branding settings (Website → Branding in their dashboard), not from this
   file. So this section does the one thing it can do properly: throw out the
   plugin's black 65% scrim and blur the page instead, so the form has
   something quiet to sit on and the page underneath is still recognisably
   there behind it.

   embed.js writes background-color inline on the overlay when it opens, which
   is why that one declaration has to be !important. Nothing else is inlined,
   so the rest is plain CSS and no script is needed to hold it.
   -------------------------------------------------------------------------- */
#memberful-overlay {
  background-color: transparent !important;
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

/* Without backdrop-filter the page behind stays sharp and the form has to fight
   the photography for attention — and with the scrim gone there is nothing else
   holding the two apart. A flat veil of the page background is the trade: less
   atmospheric, still readable. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  #memberful-overlay {
    background-color: rgba(244, 243, 243, 0.72) !important;
  }
}

/* The note under the sign-in form. Memberful's iframe fills the viewport and
   carries the maximum z-index, so this can only be a later sibling of it —
   painted on top by tree order, and transparent to the pointer everywhere
   except the link, so a click anywhere around it still reaches the form.

   Positioned by a fixed offset from the top rather than measured against the
   card: the card is in a cross-origin document, so there is nothing to measure.
   Memberful lays it out from the top of the frame, which makes its *top* stable
   — only its height moves, growing when the form goes from the email step to the
   password step.

   So the offset clears the *tallest* step rather than hugging the first one, and
   the gap under the email form is the price of that. There is no way to do
   better from out here: the later steps are Turbo renders inside the frame, so
   there is no navigation, no iframe load event, and no page.loaded to react to
   (see revealMemberfulAside() in main.js). An offset that hugged the first step
   would be sat on by the second.

   It arrives with the form rather than with the overlay: the overlay opens at
   once, but the iframe behind it has a page to fetch, and a sentence sitting
   alone under an empty backdrop for half a second reads as a mistake.
   initMemberfulAside() adds .is-in on the iframe's load event — see main.js.

   None of this runs on a phone. embed.js gates the overlay on
   Memberful.isSupportedDevice(), which returns false for any phone user agent,
   and the Log in link is followed as an ordinary link to Memberful's own hosted
   sign-in page instead. There is no page of ours left to hang a note on, so the
   narrow-window rules at the foot of this section are for a desktop browser
   resized small — the one narrow case that does still get the overlay. */
.memberful-aside {
  position: fixed;
  /* Three numbers, in the order clamp() reads them:
       420px  the floor — where the note sits under the *email* step, close
              enough to belong to it. Never higher, or it lands on that card.
       100vh  minus the note's own height and some air, so on a short window it
              rides up off the bottom edge instead of falling off it.
       560px  the ceiling — clear of the *password* step, the tallest this card
              gets. Where there's height to spare, this is what applies.
     The gap this leaves under the email form is the cost of clearing a step
     that can't be detected from outside the frame. Moving the note only once
     the reader engages with the form was tried and abandoned — see the git
     history if it's ever tempting again. */
  top: clamp(420px, calc(100vh - 160px), 560px);
  left: 50%;
  /* Both halves of the transform in one declaration: the X is what centres the
     paragraph, the Y is the small drift the fade rides in on, and .is-in has to
     restate the centring because it can only replace the property whole. */
  transform: translate(-50%, 8px);
  z-index: 2147483647;
  /* Set by the break count rather than by a comfortable line length: the
     sentence is meant to sit on two lines here, and it turns three somewhere
     below 500px, so this leaves a margin over that. 86vw only takes over below
     a ~650px viewport, by which point the phone rule is in charge anyway. */
  width: min(560px, 86vw);
  margin: 0;
  text-align: center;
  color: var(--color-text);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.memberful-aside.is-in {
  opacity: 1;
  transform: translate(-50%, 0);
}

.memberful-aside__note {
  margin: 0;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
}

/* The sentence turns after "newsletter" rather than wherever the measure runs
   out, which at 560px is mid-clause after "access to". Neither half fits the
   other's length, so no width wraps it here on its own — it takes a real break.
   Dropped on a narrow window below, where the two halves wrap anyway and a
   forced turn only strands a short line. */
.memberful-aside__break {
  display: inline;
}

/* The same mono label the fiscal sponsor wears on the About page — see
   .about-sponsor__note. Attribution rather than copy, so it takes the meta
   voice, and the mark sits on the line with it rather than under it.

   The gap is the flex gap alone: the <img> and the text are separated by real
   whitespace in the markup, which would add a space character's width on top of
   it. flex makes each child its own item and that whitespace is dropped. */
.memberful-aside__powered {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  margin: var(--space-3) 0 0;
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

/* Height rather than width, so the mark is set to the type beside it rather
   than to a box. 18px because the wordmark's own box is taller than its
   letters: the caps occupy about 64% of that 23-unit viewBox and the swash on
   the M takes the rest, so 18px puts the E through the L at the same cap height
   as POWERED BY and lets the flourish run past it, which is what it is for.
   Matching the box heights instead would set the whole logotype too small. */
.memberful-aside__logo {
  width: auto;
  height: 18px;
}

/* The same outlined pill the in-story links wear (see .story-copy a), drawn in
   the brand accent rather than a post's own colour — there's no story here to take
   a colour from. It keeps the dark text when it fills rather than knocking out
   to the page off-white the way those links do: off-white on the accent is two
   pale colours against each other, and the word stops reading. */
/* The tier's name, underlined. Same underline offset as the link below so the
   two rules read as one hand, even though only one of them is clickable. */
.memberful-aside__name {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.memberful-aside a {
  padding: 0.1em 0.4em;
  border: 2px solid var(--color-accent);
  border-radius: 99px;
  color: inherit;
  text-decoration: none;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  pointer-events: auto;
  transition: background-color 0.3s ease, color 0.3s ease;
}

.memberful-aside a:hover,
.memberful-aside a:focus-visible {
  background-color: var(--color-accent);
  color: var(--color-text);
}

@media (max-width: 767px) {
  /* A narrow *window*, not a phone — see the note on isSupportedDevice above.
     Memberful's card goes full-bleed here and starts at the very top of the
     frame rather than a fifth of the way down it, so it finishes higher and the
     note comes up to meet it. Only the offset changes; 16px holds its own on a
     narrow measure. */
  /* Same three-part offset, lower throughout: the card goes full-bleed here and
     starts at the very top of the frame, so every step of it finishes higher —
     and the note runs to three lines without its break, so it needs more air. */
  .memberful-aside {
    top: clamp(380px, calc(100vh - 180px), 500px);
  }

  .memberful-aside__break {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .memberful-aside {
    transform: translate(-50%, 0);
    transition: none;
  }
}

/* ==========================================================================
   Contributors — taxonomy-contributor.php, page-contributors.php
   A page per credited person, and a grid of all of them. Both are built out
   of type and one circle; there is no panel, no card and no rule anywhere in
   here, which is what keeps them of a piece with the story pages they hang
   off rather than reading as a section of the site with its own furniture.
   ========================================================================== */

/* Centred on the measure, the way the stockists page opens: a name, a line of
   links and a paragraph is a masthead, not a column of copy. */
.contributor-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* One gap throughout, per the Figma frame: picture, name, paragraph and links
     are four things at equal distance rather than a headshot with a caption. */
  gap: var(--space-4);
  /* The same 80px under the chrome that Subscribe and Stockists open on. The
     circle stands in for the headline here, so it's the circle that starts where
     every other page's title does. */
  padding-block: var(--space-10) 0;
  text-align: center;
}

/* The one circle on the site, and small — 104px from the frame. Fixed rather
   than fluid: a headshot is a face at a readable size, and a portrait that grew
   with the viewport would be a different picture on a laptop than on a phone.
   Small enough that it reads as a mark above the name rather than as the
   photograph the page opens on. */
.contributor-intro__photo {
  width: 104px;
  height: 104px;
  border-radius: 50%;
  overflow: hidden;
}

.contributor-intro__photo img,
.contributor-card__photo img {
  display: block;
  width: 100%;
  height: 100%;
  /* Cropped to the circle rather than fitted inside it — a headshot is the one
     picture on the site that is a shape before it is a photograph. */
  object-fit: cover;
}

/* The page-title size — the same 96/88 the Stockists and About pages open on.
   A contributor's name is the title of their page, not a heading inside it. */
.contributor-intro__name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(3.5rem, 6.667vw, 6rem); /* 96px at 1440 */
  line-height: 0.92;                       /* 88/96 */
}

/* The site's label type, for the same reason the stockist entries use it: these
   are addresses, not sentences. Spaced apart rather than separated by a
   character, so a contributor with one link doesn't need a different rule. */
/* One line of mono caps divided by upright rules, per the frame. 8px either
   side of each rule, which is why the gap is on the row and not on the link.
   Wraps rather than truncating: the frame shows two links, the field takes as
   many as somebody has. */
.contributor-intro__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-1);
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--nav-font-size);
  line-height: var(--nav-line-height);
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

/* The rule between two of them. Not tracked, so it sits centred in its own gap
   rather than pushed right by the letter-spacing meant for words. */
.contributor-intro__links-divider {
  letter-spacing: normal;
}

/* 900px in the frame, which is the eight-column measure at the 1440 content
   width — wider than the 46ch the rest of the site's centred copy is held to,
   because this is three lines under a 96px name rather than a standfirst. */
.contributor-intro__bio {
  max-width: 900px;
  font-family: var(--font-serif);
  font-size: clamp(1.125rem, 1.667vw, 1.5rem); /* 24px */
  line-height: 1.333;
}

.contributor-intro__bio p {
  margin: 0 0 var(--space-3);
}

.contributor-intro__bio p:last-child {
  margin-bottom: 0;
}

/* The gap between the introduction and the grid of stories, which nothing labels
   — the pictures say what they are. */
.contributor-page .site-content {
  padding-top: var(--space-15);
}

/* Centred rather than set on the 12 columns, because most contributors have one
   or two stories and a grid would leave those hard against the left margin,
   under a page that is centred all the way down to the links.

   Flex, with each card given the width its column span works out to, so a full
   row still lands exactly where the grid would have put it: four cards of three
   columns plus the three gutters between them come to 100% on the nose. What
   changes is only the last row, which now sits in the middle instead of
   trailing off to the right. */
.contributor-page .archive-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  column-gap: var(--grid-gutter);
}

/* Three of twelve columns: three tracks and the two gutters they span. */
.contributor-page .archive-grid .story-card {
  width: calc(
    (100% - 11 * var(--grid-gutter)) / 12 * 3 + 2 * var(--grid-gutter)
  );
}

/* Two across between the tablet breakpoint and full-width stacking, matching
   what .story-card.col-span-3 does in the ordinary grid: six columns and the
   five gutters they span. */
@media (min-width: 768px) and (max-width: 1023px) {
  .contributor-page .archive-grid .story-card {
    width: calc(
      (100% - 11 * var(--grid-gutter)) / 12 * 6 + 5 * var(--grid-gutter)
    );
  }
}

@media (max-width: 767px) {
  .contributor-page .archive-grid .story-card {
    width: 100%;
  }
}

/* ---- The index ---------------------------------------------------------- */

/* The page shell Subscribe and Stockists use, so the title starts at the same
   height here as it does there. */
.contributors-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  padding-top: var(--space-10);
  padding-bottom: var(--space-15);
}

.contributors-grid {
  row-gap: var(--space-10);
}

.contributor-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  color: inherit;
  text-decoration: none;
  text-align: center;
}

/* The same 104px the contributor's own page opens on — a headshot is a face at
   a readable size, and it shouldn't be a different size depending on which page
   you meet it. Six to a row at that size rather than four, so the index reads as
   a masthead of everyone rather than a gallery.

   Still width: 100% under the cap, so on a narrow column the circle comes down
   with it rather than overflowing. */
.contributor-card__photo {
  width: 100%;
  max-width: 104px;
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
}

/* No headshot yet: the circle is filled with the brand accent and carries the
   contributor's initials. Not a placeholder waiting to be replaced — a page of
   these is a page of coloured discs with letters in them, which is a look, where
   a page of empty grey circles is a page that hasn't loaded.

   The accent is the site's one fixed colour rather than the story's, which
   changes per post and would make the same person a different colour depending
   on which grid they turned up in. Ink on it, not the page colour: #1d1c1b on
   #6ae3de is 11:1, and the reverse is 1.4:1. */
.contributor-card__photo--initials,
.contributor-intro__photo--initials {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-accent);
  color: var(--color-text);
  font-family: var(--font-display);
  line-height: 1;
  /* These are already caps, and the tracking stops a pair like "II" reading as
     one mark. */
  letter-spacing: 0.02em;
  user-select: none;
}

/* Sized per circle rather than in proportion to it: there are two sizes of
   circle and they're both known, and a proportional unit here would have to be
   a container query on an element that is its own container. */
.contributor-intro__photo--initials {
  font-size: 36px;
}

.contributor-card__photo--initials {
  font-size: 36px;
}

.contributor-card__name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 24px;
  line-height: 1.2;
  opacity: var(--quiet);
  transition: opacity 0.3s ease;
}

.contributor-card:hover .contributor-card__name,
.contributor-card:focus-visible .contributor-card__name {
  opacity: 1;
}

@media (max-width: 900px) {
  .contributor-page .site-content {
    padding-top: var(--space-10);
  }

  .contributors-grid {
    row-gap: var(--space-6);
  }
}

/* Two across on a phone rather than one: these are small circles with a name
   under them, and stacked one to a row a page of thirty would never end. */
@media (max-width: 767px) {
  .contributor-card.col-span-2 {
    grid-column: span 6;
  }
}

/* ==========================================================================
   Legal pages — privacy policy, terms of use
   --------------------------------------------------------------------------
   The one part of the site that is read rather than looked at, and the type is
   set for that: a centred masthead in the display face, so the page belongs to
   the same set as Subscribe, Stockists and About, and then a single ranged-left
   column of prose from there down. Centred body copy is right for the three
   paragraphs on the About page and wrong for fifteen numbered sections — a
   reader working down a policy needs one left edge to come back to on every
   line.

   The whole page is one measure wide and it stays there. There is no picture, no
   rail and no full-bleed anything in a legal document, so the column sits on the
   page's own margins rather than in the 12-column grid, and the contents list at
   the top is what does the work a layout would otherwise be asked to do.
   ========================================================================== */
/* Following a link in the contents travels to the section rather than cutting to
   it, so the reader keeps their bearings: a document of fifteen numbered sections
   is one long column, and a jump gives no sense of whether Section 12 was just
   below or most of a page away.

   CSS rather than a click handler on purpose. Left to the browser, the anchor
   still does everything an anchor does — the hash lands in the address bar so the
   section can be linked to, a history entry is made so Back returns to where the
   reader was reading, and focus moves to the heading for anyone on a keyboard or a
   screen reader. Every one of those is a thing a preventDefault() would have had
   to rebuild, and rebuild correctly.

   Declared on :root because the scrolling element is <html>, not this page's box,
   and scoped with :has() so it stays this page's behaviour — the story pages drive
   scroll positions of their own and shouldn't have them eased underneath them.
   Where :has() isn't supported the selector doesn't match and the links go back to
   jumping, which is what they did before.

   Inside no-preference rather than being undone in a reduce block: a reader who
   has asked for less motion never has this switched on in the first place. */
@media (prefers-reduced-motion: no-preference) {
  :root:has(.legal-page) {
    scroll-behavior: smooth;
  }
}

.legal-page {
  display: flex;
  flex-direction: column;
  /* The same 80px above the title that .subscribe-page and .stockists-page use,
     so the three pages start on one line. */
  padding-top: var(--space-10);
  padding-bottom: var(--space-15);

  /* How far down the viewport the pinned nav finishes, and so how far down
     anything that parks against the top of the window has to start.

     Scrolling up the page reveals that bar (see updateHeaderOnScroll), and it
     carries no backdrop — it is the site's ink straight over whatever is behind
     it, which is fine for content scrolling past underneath and not fine for
     anything that stops there. Two things here do stop there: a heading arriving
     from the contents list, and the sticky rail itself.

     Its own terms rather than a measured constant — the bar's revealed offset
     (24px) plus its min-height (112px) plus 16px of air — so it stays true if
     either of them moves. */
  --legal-nav-clearance: calc(var(--space-3) + var(--space-14) + var(--space-2));
}

/* --------------------------------------------------------------------------
   Masthead
   -------------------------------------------------------------------------- */
.legal-masthead {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
  text-align: center;
  margin-bottom: var(--space-12);
}

/* Exactly the Stockists title: same face, same clamp, same 0.92 leading. These
   are peer pages and they should meet the reader identically. */
.legal-masthead__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 6.667vw, 6rem);  /* 96px at 1440 */
  line-height: 0.92;                        /* 88/96 */
  font-weight: 400;
}

.legal-masthead__standfirst {
  margin: 0;
  max-width: 46ch;
  font-family: var(--font-serif);
  font-size: clamp(1.125rem, 1.667vw, 1.5rem);  /* 24px */
  line-height: 1.333;
}

/* Label over fact, set exactly as the About page's masthead sets a role over the
   name that holds it: the label in the site's small mono caps at reduced strength,
   the thing itself in the display face under it. It is the same kind of pair, and
   it should look like one.

   Below the rail breakpoint there is no margin to stand in, so it falls back to a
   block at the head of the document, centred under the masthead — see the stacked
   block further down. It used to be ruled off underneath, and the contents list
   above it ruled off on top. Both rules are gone: stacked, the page is one centred
   column from the title down, and white space separates its parts the way it
   separates every other centred masthead on the site. Two hairlines across the
   measure were drawing boxes around apparatus that needed no boxes. */
.legal-effective {
  grid-column: 1 / span var(--grid-columns);
  width: 100%;
  max-width: 68ch;
  margin: 0 auto var(--space-8);
}

.legal-effective__label {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 20px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: var(--quiet);
}

.legal-effective__date {
  margin: 0;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
}

/* --------------------------------------------------------------------------
   The document and its contents

   One grid on the theme's own 12 columns. Above 900px — the breakpoint the story
   flow stacks its rails at, so both pages fold at the same width — the contents
   is a sticky rail in columns 1-2 and the document sits in 4-9, which is exactly
   where .story-rail--left and .story-copy sit. That geometry was set against the
   Figma frame and it is reused rather than reinvented, so the reading column on
   a policy lands on the same centre line as the reading column on a story, and
   the index sits in the margin where a caption would.

   Below 900px the two are back to blocks stacked in the ordinary way: the
   contents first, ruled off, then the document. There is no margin on a phone to
   put a rail in.
   -------------------------------------------------------------------------- */
.legal-flow {
  row-gap: 0;
}

/* --------------------------------------------------------------------------
   Contents

   A two-column grid per row so the titles keep one left edge once the numbering
   runs past 9 — `auto` is the width of the widest number on the page, and every
   title starts after it.
   -------------------------------------------------------------------------- */
.legal-contents {
  grid-column: 1 / span var(--grid-columns);
  width: 100%;
  max-width: 68ch;
  margin: 0 auto var(--space-12);
}

.legal-contents__list {
  display: grid;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
  /* Numbers in their own column, titles in the second. */
  grid-template-columns: auto minmax(0, 1fr);
}

/* The <li> is not a row box — its two spans are the grid's real cells, so the
   list item spans the full width and lays its own contents out on the parent's
   columns. */
.legal-contents__item {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
}

.legal-contents__item > a {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  font-family: var(--font-display);
  font-size: 18px;
  line-height: 28px;
  transition: opacity 0.3s ease;
}

.legal-contents__item > a:hover,
.legal-contents__item > a:focus-visible {
  opacity: 0.6;
}

/* Tabular figures so 1 and 11 occupy the same box and the numbers stack into a
   straight column. */
.legal-contents__number {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 28px;
  letter-spacing: 0.05em;
  font-variant-numeric: tabular-nums;
  opacity: var(--quiet);
}

/* Underlines the title alone on hover, not the number beside it — the number is
   a marker, the words are the link. */
.legal-contents__item > a:hover .legal-contents__title,
.legal-contents__item > a:focus-visible .legal-contents__title {
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

/* Without subgrid the two spans fall back to sitting inline; the numbers stop
   aligning into a column but nothing breaks. */
@supports not (grid-template-columns: subgrid) {
  .legal-contents__list { display: flex; flex-direction: column; }
  .legal-contents__item,
  .legal-contents__item > a { display: block; }
  .legal-contents__number { margin-right: var(--space-2); }
}

/* --------------------------------------------------------------------------
   The document

   68ch rather than the story flow's grid column: a policy is read in long
   uninterrupted runs and it wants a measure sized to the reading, not to a
   layout that also has to hold photographs.
   -------------------------------------------------------------------------- */
.legal-body {
  grid-column: 1 / span var(--grid-columns);
  width: 100%;
  max-width: 68ch;
  margin-inline: auto;
}

/* Half a step down from the story flow's 20/32. The face and the rhythm are the
   same; the size says what this is — the reference at the back of the magazine
   rather than the writing at the front. */
.legal-body p,
.legal-body li {
  font-family: var(--font-display);
  font-size: 18px;
  line-height: 30px;
  color: var(--color-text);
}

.legal-body p {
  margin: 0 0 var(--space-3);
}

.legal-body > *:last-child {
  margin-bottom: 0;
}

/* ---- Headings ------------------------------------------------------------
   Both levels are built the same way by synonym_legal_body(): the section
   number in its own span, then the title. The number is set in the label face
   and hung in the margin beside the title where there is room for it, so the
   titles keep the same left edge as the paragraphs under them and the numbering
   reads as an index down the side of the page rather than as part of the words.
   -------------------------------------------------------------------------- */
.legal-heading {
  position: relative;
  font-weight: 400;
  /* Where a heading comes to rest when the contents sends the reader to it.
     Clicking Section 2 from Section 14 scrolls up, which is exactly what reveals
     the pinned nav — so at the 48px this used to be, the heading arrived
     underneath it. See --legal-nav-clearance. */
  scroll-margin-top: var(--legal-nav-clearance);
}

.legal-heading__number {
  font-family: var(--font-mono);
  letter-spacing: 0.05em;
  font-variant-numeric: tabular-nums;
  opacity: var(--quiet);
}

/* A section. The display face at the story flow's own heading size — this is
   the same job .story-heading does, marking a break in a column of copy, and it
   is set to match. In the page's ink rather than an accent, though: the accent
   is a story's art direction and a policy has none. */
.legal-heading--2 {
  margin: var(--space-10) 0 var(--space-3);
  font-family: var(--font-display);
  font-size: 32px;
  line-height: 40px;
}

.legal-heading--2 .legal-heading__number {
  font-size: 13px;
  line-height: 20px;
}

/* A subsection — 2.1, 9.4. The medium display cut at body size and a good deal
   less air above it than a section gets, so the two levels are told apart by
   the space around them as much as by the size. */
.legal-heading--3 {
  margin: var(--space-6) 0 var(--space-2);
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 28px;
}

.legal-heading--3 .legal-heading__number {
  font-size: 12px;
  line-height: 16px;
}

/* Nothing above the first heading in the document — the contents list already
   set the gap. */
.legal-body > .legal-heading:first-child {
  margin-top: 0;
}

/* Hung out to the left of the measure where the margin can take it, which needs
   room the page only has above 1100px or so. Below that the numbers come back
   inline ahead of the title, which is where they were typed. */
.legal-heading__number {
  display: inline-block;
  margin-right: var(--space-1);
}

@media (min-width: 1200px) {
  .legal-heading__number {
    position: absolute;
    right: 100%;
    /* The number sits on the cap line of the title beside it, not on the top of
       its box — the two faces are set at very different sizes and aligning the
       boxes would float it. */
    top: 0.55em;
    margin-right: var(--space-3);
    text-align: right;
    white-space: nowrap;
  }

  .legal-heading--3 .legal-heading__number {
    top: 0.5em;
  }
}

/* ---- Lists ---------------------------------------------------------------
   Both documents lean on bulleted lists hard — what we collect, what you may
   ask for, what you agree not to do — and several of the items run to three or
   four lines. So the dot is the site's own accent circle (the same 8px mark the
   membership tiers use, drawn rather than loaded for the reason given there),
   sitting on the first line and holding the text off one hanging indent.
   -------------------------------------------------------------------------- */
.legal-body ul,
.legal-body ol {
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
}

.legal-body li {
  position: relative;
  padding-left: var(--space-3);
  margin-bottom: var(--space-2);
}

.legal-body li:last-child {
  margin-bottom: 0;
}

/* On the first line rather than centred on the whole item — unlike a tier's
   one-line feature, a bullet here is often a sentence of its own with a bolded
   lead-in, and a dot halfway down four lines of it reads as unattached. */
.legal-body ul > li::before {
  content: "";
  position: absolute;
  top: 11px;              /* (30px line-height - 8px dot) / 2, rounded to the line */
  left: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-accent);
}

/* A numbered list keeps its numbers — set in the label face, in the same
   hanging indent the dot uses. */
.legal-body ol {
  counter-reset: legal-item;
}

.legal-body ol > li {
  counter-increment: legal-item;
  padding-left: var(--space-4);
}

.legal-body ol > li::before {
  content: counter(legal-item) ".";
  position: absolute;
  left: 0;
  top: 0;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 30px;
  letter-spacing: 0.05em;
  font-variant-numeric: tabular-nums;
  opacity: var(--quiet);
}

/* A nested list — the run of service providers under "Service providers" in the
   privacy policy. Quieter dot, tighter items: it is a list inside a bullet, not
   a second list of equals. */
.legal-body li > ul {
  margin: var(--space-2) 0 0;
}

.legal-body li > ul > li {
  margin-bottom: var(--space-1);
}

.legal-body li > ul > li::before {
  width: 5px;
  height: 5px;
  top: 13px;
  background: var(--color-text);
  opacity: 0.4;
}

/* ---- Everything else in the prose --------------------------------------- */

/* The bolded lead-in that opens most bullets — "Recurring billing.",
   "Service providers." The heavy cut rather than the browser's bold, which is
   what the rest of the site does everywhere it needs emphasis in Matter.

   This asked for the medium cut until Matter Medium was dropped from the site,
   at which point it was asking for a face that no longer loads: the family fell
   back to Matter Regular at weight 400, which is exactly the body copy around
   it, and every bolded lead-in in both legal documents quietly stopped being
   bold. Heavy is the only emphatic cut the site still ships, so it is the one
   that has to do this job.

   Weight 700 named explicitly. "Matter Heavy" is declared at 700 and at no other
   weight, so a request for 400 would be answered with the 700 face anyway — but
   only after the browser has decided to substitute, and a browser that decides
   instead to synthesise gets a faux-bold Heavy. Asking for what is there removes
   the question.

   Sentence case here, and caps in the list rule below. Emphasis buried in a
   paragraph is a word being leant on inside a sentence, and it has to stay in the
   sentence's own case to read as one; a bullet's lead-in is a different animal,
   and is set as one. */
.legal-body strong,
.legal-body b {
  font-family: var(--font-display-heavy);
  font-weight: 700;
}

/* The run-in sidehead that opens a bullet — "Recurring billing.", "Service
   providers.", "Strictly necessary cookies". Heavy caps, which is .story-speaker
   and .story-copy__lead-in: the recipe the site already uses for the name that
   opens a turn in an interview. That is what these are. Each bullet is a named
   topic followed by what it says about it, set exactly the way a named speaker is
   followed by what they said.

   Caps are doing a job here that the weight alone was not. Every one of these
   lists is scanned before it is read — someone in the terms is looking for
   Cancellation, someone in the privacy policy for who their data is handed to —
   and Heavy sentence-case at body size is a difference you notice only once you
   are already reading the line. In caps the labels separate from the prose at a
   glance and the list becomes a set of headed entries, which is what it is.

   Scoped to list items, so it does not reach emphasis inside a paragraph. In the
   two legal documents as they stand that distinction is theoretical — all thirty
   of their <strong>s open an <li> — but the rule should say what it means rather
   than rely on that staying true of the next draft counsel sends.

   16px against the 18px body, the same step down .story-speaker takes against the
   story flow's 20px. Caps at the body's own size sit visually larger than the
   lowercase around them and would turn every lead-in into a heading. No
   line-height: the paragraph's 30px is inherited, so a label that runs onto a
   line with body text doesn't shift the baseline under it.

   The trailing full stops inside about two-thirds of these ("Recurring billing.")
   are left alone. They are counsel's copy, they live in files that get replaced
   wholesale by the next draft, and a run-in sidehead set in caps with a period
   after it is an ordinary typographic form rather than a mistake. */
.legal-body li strong,
.legal-body li b {
  font-size: 16px;
  letter-spacing: var(--nav-tracking);
  text-transform: uppercase;
}

.legal-body em,
.legal-body i {
  font-style: italic;
}

/* The Copy block's outlined pill (see .story-copy a), which is the site's
   answer for a link buried mid-paragraph: findable without hovering, with the
   words left in ink so the sentence still reads as one colour.

   Drawn in the brand accent rather than --story-color, and keeping the dark text
   when it fills rather than knocking out to the page off-white — both for the
   reasons the membership note gives (.memberful-aside a): there is no story here
   to take a colour from, and off-white on the accent is two pale colours against
   each other with the word lost between them.

   box-decoration-break: clone for the same reason the story links carry it —
   without it a link that breaks across two lines loses the right edge of the
   first fragment and the left edge of the second, and there are several long
   mailto links here that will break. Vertical padding stays in the story rule's
   0.1em: padding on an inline box overflows the line rather than opening it, and
   this copy is set on a tighter 30px leading than the story flow's 32px. */
.legal-body a {
  padding: 0.1em 0.4em;
  border: 2px solid var(--color-accent);
  border-radius: 99px;
  color: var(--color-text);
  text-decoration: none;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-color 0.3s ease, color 0.3s ease;
}

.legal-body a:hover,
.legal-body a:focus-visible {
  background-color: var(--color-accent);
  color: var(--color-text);
}

/* The address block used to be the one exception to the pill: its email took a
   plain underline instead, on the reasoning that four of the block's five lines
   are a postal address and a pill around the fifth turns a stamp into a button.
   The exception is gone, because what it actually produced was three addresses in
   the two documents drawn one way and four elsewhere in the same prose drawn
   another, with nothing on the page to explain the difference to a reader. One
   link style for the whole document is worth more than the stamp was.

   The address block still gets its own leading below — that part was never about
   the link. */

/* The all-caps blocks — the warranty disclaimer and the liability cap. Counsel
   sets these in capitals because the law in several states asks for them to be
   conspicuous, so they have to stay capitals; what the type can do is stop them
   shouting. The mono face at 15px with the line opened up turns a wall of caps
   into something a reader can actually get through, and reads as fine print
   rather than as the loudest thing on the page. */
.legal-body p.legal-body__caps {
  font-family: var(--font-mono);
  font-size: 15px;
  line-height: 26px;
  letter-spacing: 0.01em;
}

/* The address at the foot of both documents: several short lines that are one
   block, so they lose the gap between paragraphs and keep their own line
   breaks. */
.legal-body .legal-body__address {
  /* Leading only. The size comes from the paragraph rule above, so the address
     steps down with the rest of the copy at narrow widths instead of being left
     a point larger than everything around it. What it does change is the
     line-height: 30px is set for a measure of running prose and pulls a
     five-line address apart into five separate things. */
  line-height: 1.5;
}

/* A bracketed note left in the draft for counsel to replace — see the returns
   policy in the terms and the arbitration note under governing law. Marked so
   it is impossible to publish one by accident and not notice: it is only ever
   visible to whoever is reading the page, so this is a flag for us rather than
   a design element, and it should be gone before either page goes live. */
.legal-body .legal-body__todo {
  padding: var(--space-2) var(--space-3);
  border: 1px dashed currentColor;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 24px;
  opacity: var(--quiet);
}

/* --------------------------------------------------------------------------
   Stacked: contents and date centred under the masthead

   Below the rail breakpoint neither of these is in a margin — there is no margin
   — so they are stacked under the title as blocks, and the page they are stacked
   under is centred: a 96px title and a standfirst, both on the middle. Ranged
   left on a 68ch measure they read as a different document's apparatus that has
   been left at the top of this one. Centred, the whole page above the first
   paragraph is one column, which is what it looks like.

   What gets centred is the titles, and only the titles. A number set beside its
   title inside the centred box pushes the words off the page's centre line by
   half the number's width — by a different half for 1 than for 11, so the
   titles don't even line up with each other. Hung outside instead: the anchor's
   box is the title alone and centres like every other line on the page, and the
   numbers keep the left, marking where each title starts.

   Which is why the list drops its subgrid here — the rail's two aligned tracks
   are what a number column costs, and stacked there are never enough entries on
   screen for that alignment to be worth the centre line.
   -------------------------------------------------------------------------- */
@media (max-width: 1199px) {
  .legal-effective {
    text-align: center;
  }

  /* Room either side for the hanging numbers. On both edges rather than the left
     alone, because padding on one side moves the centre the titles are being
     centred on — which is the whole point of the block. */
  .legal-contents__list {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    padding-inline: var(--space-4);
  }

  .legal-contents__item {
    display: block;
    text-align: center;
  }

  /* Shrunk to the title and centred on the measure. Block rather than the grid
     it is in the rail: there is no second track to put anything in any more. */
  .legal-contents__item > a {
    position: relative;
    display: block;
    width: fit-content;
    margin-inline: auto;
  }

  /* Out of flow and hung off the title's left edge, which is what keeps it out
     of the centring. Top-aligned rather than vertically centred so a title that
     wraps to two lines still has its number beside the first of them — the
     number marks where the entry begins, not where its middle is. */
  .legal-contents__number {
    position: absolute;
    top: 0;
    right: 100%;
    margin-right: var(--space-2);
  }

  /* The pitch address on Submissions, which is two blocks each shrunk to its own
     words so that each carries its own hover bar (see .submissions-pitch__line).
     Shrunk, they range left inside a box that is otherwise centred — the auto
     margins are what put them back on the middle, and they keep the bars the
     width of the words rather than the width of the column. */
  .submissions-pitch__line {
    margin-inline: auto;
  }
}

/* --------------------------------------------------------------------------
   The sticky rail

   Everything that turns the contents from a block above the document into an
   index beside it. Only the placement and the type change — the markup is the
   same list either way.

   1200px rather than the 901px the story flow folds its own rails at, and the
   difference is what the rail is being asked to hold. A story's left rail
   carries one caption of a line or two; this carries fifteen section titles,
   several of them eight words long, and two columns of a 1024px window is 144px
   — every title breaking into four lines and the list running half again as
   long as the window. Below 1200 the contents is better as what it is under the
   masthead: a block, ruled off, on the full measure.
   -------------------------------------------------------------------------- */
@media (min-width: 1200px) {
  /* All three pinned to row 1. Auto-placement walks forward through the columns
     and never goes back, so whichever of them is written last — here the
     document, because the date reads before it — would be dropped into a row of
     its own, out from beside the rail. Naming the row removes the question. */
  .legal-contents,
  .legal-body,
  .legal-effective {
    grid-row: 1;
  }

  .legal-contents {
    grid-column: 1 / span 2;
    /* align-self: start is what makes sticky work at all here. A grid item
       stretches to the row's height by default, and an element already as tall
       as its container has no distance to travel — it would simply never move.
       Shrunk to its own height, the document's height beside it becomes the
       range it sticks over. */
    align-self: start;
    position: sticky;
    /* Parked clear of the pinned nav, for the reason given on
       --legal-nav-clearance: the bar's menu icon sits at the far left of the
       window, which is exactly where this rail is, and a sticky element that
       stops under it stays under it. */
    top: var(--legal-nav-clearance);
    max-width: none;
    margin: 0;
    /* Fifteen entries at two lines each will outrun a laptop window, and a
       sticky element taller than the viewport pins its *top* and hides its own
       tail permanently. Capped to what is on screen and allowed to scroll
       inside itself, the last section stays reachable. */
    max-height: calc(100vh - var(--legal-nav-clearance) - var(--space-6));
    overflow-y: auto;
    /* Its own scroll shouldn't drag the page along once it hits the end. */
    overscroll-behavior: contain;
  }

  .legal-body {
    grid-column: 4 / span 6;
    max-width: none;
    margin-inline: 0;
  }

  /* The right rail — columns 11-12, where a story hangs its issue card. Level
     with the top of the document and staying there: unlike the contents it is one
     fact read once, not an index you come back to, so it goes with the page. */
  .legal-effective {
    grid-column: 11 / span 2;
    align-self: start;
    max-width: none;
    margin: 0;
    /* Ranged left again: out here it is a fact hanging in the margin beside the
       document's first line, not a line in the centred column the stacked layout
       makes of the whole page. */
    text-align: left;
  }

  /* Down from 18/28, and set tight. The rail is two columns — 213px at the 1440
     artboard, 173px at 1200 — and the list is apparatus rather than reading, so
     it sits between the rail caption's 16px and the label face's 13px.

     The size is doing one specific job: keeping all fifteen entries on screen at
     once on a laptop. At 15/22 the list measured 796px in a 1280 window, which
     is taller than the 704px a 800px-high window leaves it, so the last four
     sections were behind an internal scrollbar — an index you have to scroll to
     read the end of is most of the way back to no index at all. */
  .legal-contents__item > a {
    font-size: 14px;
    line-height: 20px;
  }

  .legal-contents__number {
    font-size: 12px;
    line-height: 20px;
  }

  .legal-contents__list {
    gap: var(--space-1);
  }

}

/* --------------------------------------------------------------------------
   Narrower
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  .legal-page {
    padding-top: var(--space-8);
    padding-bottom: var(--space-12);
  }

  .legal-masthead {
    gap: var(--space-3);
    margin-bottom: var(--space-8);
  }

  .legal-contents {
    margin-bottom: var(--space-8);
  }

  /* The section is the break the reader is scrolling for, and at a phone's
     width 80px of empty above each one is most of a screen. */
  .legal-heading--2 {
    margin-top: var(--space-8);
    font-size: 26px;
    line-height: 32px;
  }

  .legal-heading--3 {
    margin-top: var(--space-5);
    font-size: 18px;
    line-height: 26px;
  }

  .legal-body p,
  .legal-body li {
    font-size: 17px;
    line-height: 28px;
  }

  .legal-body ul > li::before {
    top: 10px;
  }

  /* The caps blocks don't inherit the step-down above — they carry their own
     size, on a rule specific enough to win — so they take their own. Mono caps
     at 15px wrap two or three times per sentence on a phone. */
  .legal-body p.legal-body__caps {
    font-size: 14px;
    line-height: 24px;
  }
}

/* ==========================================================================
   Contact
   page-contact.php.

   Three addresses set as a directory rather than a page of prose: the same centred
   masthead as Stockists and Subscribe, then the routes side by side, so the reader
   picks one by scanning rather than by reading a paragraph that mentions all three.

   Two things per entry and no third: the address, set large, and one line saying what
   to send to it. The mono-caps label that stood over the address is gone — the part
   before the @ was already saying the same word, and a heading that repeats the thing
   under it is a heading the eye learns to skip. Nothing is drawn around them either;
   the white space between the columns does what a rule would.

   No form and so no submit button, which means the page has no primary action and
   wants no accent — the addresses themselves are the only links on it, and they are
   set large enough to be the thing the eye lands on.
   ========================================================================== */
.contact-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  padding-top: var(--space-10);
  padding-bottom: var(--space-15);
}

.contact-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
  text-align: center;
}

.contact-intro__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 6.667vw, 6rem);  /* 96px at 1440 */
  line-height: 0.92;                        /* 88/96 */
  font-weight: 400;
}

.contact-intro__standfirst {
  font-family: var(--font-serif);
  font-size: clamp(1.125rem, 1.667vw, 1.5rem);  /* 24px */
  line-height: 1.333;
  max-width: 46ch;
}

.contact-intro__standfirst p {
  margin: 0;
}

/* --------------------------------------------------------------------------
   The addresses
   -------------------------------------------------------------------------- */
/* An explicit three-track grid rather than the site's 12-column one: there are
   three entries by construction, and auto-fit on a minimum width would drop the
   third onto a row of its own the moment the window came in — which reads as two
   important routes and an afterthought, exactly the wrong thing to say about
   wholesale. Below the breakpoint they all stack, together.

   A fourth address added in the CMS wraps onto a second row and lines up under the
   first three, which is the arrangement that survives the field being a repeater. */
.contact-channels {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-8) var(--space-6);
}

/* The address is the entry. Fluid down to 24px because these are long unbroken
   strings — @synonym-mag.com alone is 16 characters with nowhere to break that isn't
   a hyphen — and a fixed 32px would have the domain overhanging its column in a
   third of a laptop window.

   `break-word` is the backstop for the narrowest phone, where even 24px doesn't fit:
   a wrapped address is ugly, an address running out past the page margin is broken.

   Nothing at rest, so the three addresses read as the list they are rather than as a
   row of buttons — the size is what says they're the point of the page. The underline
   is the site's, growing left to right on hover, which is the same gesture the nav
   and the footer links make. */
.contact-channel__email {
  display: inline-block;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.222vw, 2rem);  /* 32px at 1440 */
  line-height: 1.1;
  text-decoration: none;
  color: inherit;
}

/* The two halves of the address, split at the @ in the markup. Blocks, so each takes
   its own line and its own underline — the shared rule up in *Shared nav link
   styling* hangs one ::after off the anchor, which on a two-line link would draw a
   single bar under the second line as wide as the widest.

   `fit-content` is what keeps each bar the width of its own words rather than of the
   column: a plain block would stretch, and the underline under "info" would run out
   to wherever the domain below it ends. */
.contact-channel__email-line {
  position: relative;
  display: block;
  width: fit-content;  /* the bar is the line's width, not the block's */
  overflow-wrap: break-word;
}

.contact-channel__email-line::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 0;
  height: 1px;
  background: currentColor;
  transition: width 0.3s ease;
}

/* Both lines at once, off the anchor rather than the span: the link is one target,
   and underlining only the half the cursor happens to be over would read as two. */
.contact-channel__email:hover .contact-channel__email-line::after,
.contact-channel__email:focus-visible .contact-channel__email-line::after {
  width: 100%;
}

.contact-channel__note {
  margin: var(--space-2) 0 0;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 24px;
  opacity: var(--quiet);
  max-width: 34ch;
}

/* One column well before the phone breakpoint. Three tracks of a 900px window are
   about 280px each, which is under the measure the note wants and barely over the
   address itself — stacked, each entry gets the full width and the labels still read
   down the page as a list of three choices. */
@media (max-width: 899px) {
  .contact-channels {
    grid-template-columns: 1fr;
    gap: var(--space-6);
  }
}

@media (max-width: 767px) {
  .contact-page {
    gap: var(--space-8);
    padding-bottom: var(--space-12);
  }
}

/* ==========================================================================
   Submissions page

   Everything the pitching guidelines do differently from the legal pages, and
   nothing else. The two share a template shape and a stylesheet — see the note at
   the top of page-submissions.php — because .legal-page is the theme's setting for
   a plain document rather than a setting for privacy policies, and the guidelines
   are a document of the same shape: centred masthead, one reading column on the
   story flow's centre line, an index in the left margin, a fact in the right.

   Two things differ, and they are the two rules' worth below. The sections are
   not numbered, so the contents list is one column rather than two. And the fact
   in the right margin is an address to write to rather than a date, which wants
   the display face and a link, not a label and a line of 16px.
   ========================================================================== */

/* One column, because there is no number to put in the other. The legal list sets
   auto | 1fr and hangs the section number in the first track; here the title is
   the whole entry, and left on that grid it would sit in an auto track sized to
   its longest line — which in a 173px rail is most of the way to overflowing it.
   The subgrid on the item follows the parent, so this is the only rule needed. */
.submissions-page .legal-contents__list {
  grid-template-columns: minmax(0, 1fr);
}

/* The column names that open the bullets under "Columns in Every Issue" —
   Community Board, Shelf Life, Chewing the Fat — have no rule of their own here.
   They are set by .legal-body li strong up in the document rules, along with every
   bulleted lead-in in the privacy policy and the terms: a name opening a bullet is
   set in heavy caps wherever the theme sets a document, and this page is one.

   Which is also what keeps the editor field honest. Whoever adds a fourth column
   types the name and hits bold, and it comes out right without their having had to
   know any of this exists. */

/* The address in the right margin. .legal-effective gives it the box, the label
   and the rule under it when the layout is stacked; this gives the address itself
   the treatment the Contact page gives its three — the display face, big enough
   to read as the thing you act on rather than as a caption. */
.submissions-pitch__address {
  margin: 0;
}

/* Not pilled. The pill is how a link is drawn inside running prose
   (.legal-body a), where it has to be picked out of a paragraph; standing alone
   in the margin under its own label there is nothing to pick it out of, and a
   24px pill in a two-column rail reads as a button someone has left there.
   Undrawn until hover instead, with the site's growing underline — see *Shared
   nav link styling*, where the same bar is hung off the nav, the footer links and
   the byline names. */
.submissions-pitch__address a {
  display: inline-block;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1.667vw, 1.5rem);  /* 24px at 1440 */
  line-height: 1.2;
  color: inherit;
  text-decoration: none;
}

/* The two halves of the address, split at the @ in the markup for the reason
   given in the template: the rail is 213px at the 1440 artboard and 173px at
   1200, and no submissions address fits that on one line. Blocks here rather than
   in the markup so the address stays one inline string to copy and one string to
   read out.

   Each line carries its own bar rather than the anchor carrying one, which is the
   arrangement .contact-channel__email-line uses and for the same reason: a single
   ::after on a two-line link draws one rule under the second line, as wide as the
   wider of the two. `fit-content` is what keeps each bar the width of its own
   words — a plain block would stretch, and the bar under the local part would run
   out to wherever the domain below it ends.

   position: relative so the bar is placed against the line rather than against
   the nearest positioned ancestor further up the page. */
.submissions-pitch__line {
  position: relative;
  display: block;
  width: fit-content;
  overflow-wrap: break-word;
}

/* The site's default: zero-width, grown to full on hover over 0.3s. -2px rather
   than the shared rule's -4px, which is set for a single line of 16px nav type —
   at 24px on 1.2 leading the two lines are close enough together that a 4px drop
   puts the first line's bar into the second line's caps. */
.submissions-pitch__line::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 0;
  height: 1px;
  background: currentColor;
  transition: width 0.3s ease;
}

/* Both bars at once, off the anchor rather than the span: the link is one target,
   and underlining only the half the cursor happens to be over would read as two
   links. focus-visible alongside hover so it is reachable from the keyboard,
   which is the whole reason the shared rule pairs them everywhere else. */
.submissions-pitch__address a:hover .submissions-pitch__line::after,
.submissions-pitch__address a:focus-visible .submissions-pitch__line::after {
  width: 100%;
}

@media (min-width: 1200px) {
  /* Down in the rail, where 24px would break the local part onto two lines of its
     own before the @ ever got a chance to. 18/24 is the size the contents list
     steps down to out here, and this sits a shade above it — it is the one thing
     in either margin a reader is meant to act on. */
  .submissions-pitch__address a {
    font-size: 20px;
    line-height: 26px;
  }
}

/* ==========================================================================
   Support page
   page-support.php. The Contact page's masthead, then three asks top to bottom:
   buy (two pills), give (the About page's accent support panel, reused as-is),
   and the slower routes to a gift, set quietly side by side.
   ========================================================================== */
.support-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  padding-top: var(--space-10);
  padding-bottom: var(--space-15);
  /* Matter's contextual alternates turn "(3)" into a circled ③, and this page
     says 501(c)(3) three times. */
  font-feature-settings: "calt" 0;
}

.support-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
  text-align: center;
}

.support-intro__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 6.667vw, 6rem);  /* 96px at 1440 */
  line-height: 0.92;
  font-weight: 400;
}

/* A little wider than the other standfirsts' 46ch: this one is a full paragraph
   rather than a line or two, and at 46ch it stood six lines tall. */
.support-intro__standfirst {
  max-width: 54ch;
  font-family: var(--font-serif);
  font-size: clamp(1.125rem, 1.667vw, 1.5rem);  /* 24px */
  line-height: 1.333;
}

.support-intro__standfirst p {
  margin: 0;
}

.support-intro__standfirst p + p {
  margin-top: 1em;
}

.support-intro__ctas {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
  margin: 0;
}

/* The panel carries its own 120px of padding for the wash to bleed into, which
   on top of the page's gap would open a gulf either side of it. */
.support-donate {
  margin-block: calc(var(--space-12) * -0.5);
}

/* Inside the donation panel, under the sponsor line. */
.support-other {
  margin-top: var(--space-10);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
  gap: var(--space-8) var(--space-10);
  width: 100%;
  max-width: 960px;
  margin-inline: auto;
  text-align: center;
}

/* The Subscribe page's small mono line (.tier__inherits), a step below the nav
   size, so the two labels sit quieter than the sponsor line above them. */
.support-other__title {
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: 0.75rem;   /* 12px */
  line-height: 1.333;
  letter-spacing: 0.067em;
  text-transform: uppercase;
  font-weight: 400;
}

.support-other__copy {
  font-family: var(--font-display);
  font-size: 1.125rem;
  line-height: 1.5;
}

.support-other__copy p {
  margin: 0;
}

.support-other__copy p + p {
  margin-top: var(--space-3);
}

/* The outlined pill every link in running copy wears, filling on hover — in ink,
   as on the Stockists wholesale panel (.wholesale-panel__body a), because these
   sit on the accent wash and an accent outline on it is no outline at all. */
.support-other__copy a {
  padding: 0.1em 0.4em;
  border: 2px solid var(--color-text);
  border-radius: 99px;
  color: var(--color-text);
  text-decoration: none;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-color 0.3s ease, color 0.3s ease;
}

.support-other__copy a:hover,
.support-other__copy a:focus-visible {
  background-color: var(--color-text);
  color: var(--color-bg);
}

/* Otherwise the address breaks at its hyphen, "synonym-" over "mag.com". */
.support-other__copy a[href^="mailto:"] {
  white-space: nowrap;
}

@media (max-width: 767px) {
  .support-page {
    gap: var(--space-8);
    padding-bottom: var(--space-12);
  }

  .support-donate {
    margin-block: calc(var(--space-8) * -0.5);
  }
}
