/* ==========================================================================
   michaelkutcher.com — design system
   Palette: Navy & Brass. Every colour is a token; nothing is inlined.
   Every text element declares its own colour rather than inheriting, because
   inherited colour is the first thing a canvas builder's section themes break.

   Motion system, and its Framer mapping:
     entrance choreography  -> Framer Appear effects with per-layer delays
     [data-parallax]        -> Framer Scroll Speed (value = speed %)
     .triad scroll sequence -> Framer Sticky + Scroll Transform
     .ticker                -> Framer Ticker component
     .clip-reveal           -> Framer Appear (mask reveal)
   ========================================================================== */

/* ------------------------------------------------------------------ tokens */

:root {
  --paper:        #F2EEE6;
  --bone:         #E3DCCE;
  --accent:       #B08D57;
  --accent-link:  #755E3A;
  --accent-soft:  #B99A69;
  --ground:       #152A45;
  --ground-2:     #1E3757;
  --ink:          #111D2E;
  --ink-soft:     #4A5568;
  --on-accent:    #141414;
  --on-ground:    #F2EEE6;

  --hairline:      rgba(17, 29, 46, 0.18);
  --hairline-dark: rgba(242, 238, 230, 0.20);

  --font-display: Newsreader, Georgia, "Times New Roman", serif;
  --font-body: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Spaces around the operators are mandatory. clamp(3rem,1rem+8vw,8rem) is
     invalid and the whole declaration is dropped without warning. */
  --text-giant:   clamp(4rem, 0.5rem + 13.8vw, 16.5rem);
  --text-hero:    clamp(2.4rem, 1.1rem + 3.8vw, 5.6rem);
  --text-display: clamp(2rem, 1rem + 2.9vw, 4.4rem);
  --text-title:   clamp(1.4rem, 1.05rem + 1.3vw, 2.3rem);
  --text-lead:    clamp(1.1rem, 0.98rem + 0.55vw, 1.55rem);
  --text-body:    clamp(1.0625rem, 0.98rem + 0.3vw, 1.25rem);
  --text-micro:   clamp(0.75rem, 0.7rem + 0.15vw, 0.875rem);

  /* The old curve resolved to ~154px a side at 1920, so ~308px of nothing sat
     between every section and the page ran to 17,000px. Airy is the intent;
     stranded is not. This lands near 120px a side on a large display. */
  --space-section: clamp(4rem, 2.75rem + 4vw, 9rem);
  --gutter:        clamp(1.5rem, 0.9rem + 2.6vw, 5rem);
  --page-max:      1760px;
  /* Fallback only. The masthead's real height depends on the fluid logo, so
     the script overwrites this with the measured value on load and resize. */
  --masthead-h:    107px;

  --duration:      480ms;
  --duration-slow: 640ms;
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --rule: 1px;
}

/* -------------------------------------------------------------------- base */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: 1.6;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
  caret-color: var(--accent);
  accent-color: var(--accent);
}

/* The browser surfaces we did not draw still carry the design. */
::selection { background: var(--accent); color: var(--on-accent); }
::-webkit-scrollbar { width: 12px; }
::-webkit-scrollbar-track { background: var(--bone); }
::-webkit-scrollbar-thumb {
  background: var(--ink-soft); border: 3px solid var(--bone); border-radius: 6px;
}
html { scrollbar-color: var(--ink-soft) var(--bone); scrollbar-width: thin; }

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

a {
  color: var(--accent-link);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  text-decoration-color: color-mix(in srgb, var(--accent-link) 45%, transparent);
  transition: text-decoration-color var(--duration) var(--ease-out-expo);
}
a:hover { text-decoration-color: var(--accent-link); }

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

.skip {
  position: absolute; left: 0; top: 0; z-index: 100;
  transform: translateY(-120%);
  background: var(--accent); color: var(--on-accent);
  padding: 0.75rem 1.25rem; text-decoration: none;
  transition: transform var(--duration) var(--ease-out-expo);
}
.skip:focus { transform: translateY(0); }

.vh {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* ---------------------------------------------------------------- typography */

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-wrap: balance;
  margin: 0;
}

h1 { font-size: var(--text-hero); line-height: 1.04; }
h2 { font-size: var(--text-display); line-height: 1.08; }
h3 { font-size: var(--text-title); line-height: 1.14; }

/* A small heading that labels a list it owns. Not an eyebrow: it never
   floats above another heading. */
.listhead {
  font-family: var(--font-body);
  font-size: var(--text-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--accent-link);
  margin: 0 0 0.85rem;
}
.listhead--dark { color: var(--accent-soft); }

p { margin: 0; }
.prose p + p { margin-top: 1.1em; }
.prose { max-width: 65ch; color: var(--ink-soft); }
.prose strong { color: var(--ink); font-weight: 600; }

.lead {
  font-size: var(--text-lead);
  line-height: 1.55;
  color: var(--ink-soft);
  max-width: 46ch;
}

/* His line, set as speech rather than as a heading. */
.aside-voice {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.25rem, 1rem + 1.1vw, 1.8rem);
  line-height: 1.35;
  color: var(--ink);
  max-width: 30ch;
}
.aside-voice cite { font-style: normal; display: block; margin-top: 0.75rem;
  font-family: var(--font-body); font-size: 0.8125rem; color: var(--ink-soft); }

/* ------------------------------------------------------------------ layout */

.shell {
  padding-inline: var(--gutter);
  max-width: var(--page-max);
  margin-inline: auto;
}

.section { padding-block: var(--space-section); }
.section--tight { padding-block: clamp(3rem, 2rem + 3vw, 5.5rem); }
.section--bone   { background: var(--bone); }
.section--ground { background: var(--ground); }
.section--ground h2, .section--ground h3 { color: var(--on-ground); }
.section--ground .prose, .section--ground p { color: rgba(242, 238, 230, 0.80); }

.split {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 5rem);
  /* Bottom-aligning hung the supporting sentence off the heading's last
     baseline, which read as the text having slipped rather than been placed.
     Aligning to the top lets both columns start on the same line. */
  align-items: start;
}
.split--middle { align-items: center; }

/* ------------------------------------------------------ the proof headline
   Was a .split: heading in a 735px column on the left, the supporting line in
   a 544px column starting at x=844. Jarryd, 18 Sept: it "hangs in open space."
   It did, and the split was the cause. The two columns shared no edge with
   each other or with the logo wall directly beneath, and the shorter right
   column left a void the height of the heading. Centred and measured, this
   sits on the same axis as the wall and the quote below it, so the section
   reads as one stack. The measure is capped at 20ch for the heading so it
   breaks into two strong lines rather than one thin ribbon across 1440px. */
.proof__head {
  display: grid; justify-items: center; text-align: center;
  gap: clamp(0.9rem, 0.7rem + 0.8vw, 1.4rem);
  margin-bottom: clamp(2rem, 1.6rem + 1.6vw, 3.25rem);
}
.proof__head h2 { max-width: 20ch; }
.proof__head .prose { max-width: 52ch; }

/* The band is a divider, so it should not be preceded and followed by the
   section's full rhythm as well as its own. */
.proof .logos + * { margin-top: 0; }
.split--media img { max-height: min(80vh, 820px); width: 100%; height: auto; object-fit: cover; object-position: 50% 18%; }

@media (max-width: 860px) {
  .split, .split--middle { grid-template-columns: 1fr; align-items: start; }
}

.stack { display: flex; flex-direction: column; gap: 1.5rem; }

/* ------------------------------------------------------------------ button */

/* One button, three grounds. This replaced four generations of overrides that
   had accumulated on top of each other: an early solid brass pill, a wash that
   swept up on hover, a dot that flooded outward, and then this token version.
   Each generation left its rules behind, and the survivors contradicted one
   another. On the navy footer the last block still set a brass background
   while an earlier one set a cream label, so the CTA rendered as a brass pill
   with an invisible label. Rewritten as a single system rather than patched
   again.

   The shape: a label with a brass token holding the arrow. Hover widens the
   token and the arrow travels across it. The ground never changes colour, so
   nothing can ever paint the label out. */

.btn {
  display: inline-flex; align-items: center;
  /* Jarryd: "the little arrow is way close to availability". The token needs
     to read as a separate object, not as punctuation on the last word. */
  gap: 1.4rem;
  min-height: 46px;
  padding: 0.5rem 0.5rem 0.5rem 1.4rem;
  background: transparent;
  color: var(--ink);
  font-size: 0.9375rem; font-weight: 500; letter-spacing: 0.01em;
  font-family: var(--font-body);
  text-decoration: none; border: 0; border-radius: 999px; cursor: pointer;
  box-shadow: inset 0 0 0 1px var(--hairline);
  transition: box-shadow var(--duration) var(--ease-out-expo);
}
/* ⚠ The label is nowrap, so if the button is ever allowed to shrink below its
   content width the label overruns the token and the gap visually collapses
   to nothing. Measured: every button on the homepage had a real 22px gap
   except the footer CTA, which sits in a narrow grid column and was being
   compressed from 293px to 270px. `flex: none` plus an explicit max-content
   width stops a flex or grid parent from ever squeezing it. */
.btn { flex: none; width: max-content; max-width: 100%; }
.btn__label { white-space: nowrap; }
.btn__token {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.3rem; height: 2.3rem; flex: 0 0 auto;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  overflow: hidden;
  transition: width 480ms cubic-bezier(0.22, 1, 0.36, 1);
}
.btn__label, .btn__arrow, .btn__icon {
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
}
.btn__icon { display: inline-flex; align-items: center; }
.btn:hover .btn__token, .btn:focus-visible .btn__token { width: 3.1rem; }
.btn:hover .btn__arrow, .btn:focus-visible .btn__arrow { transform: translateX(4px); }
.btn:hover, .btn:focus-visible { box-shadow: inset 0 0 0 1px var(--accent); }

/* On any dark ground: the footer, a ground section, the enquiries card.
   Only the ring and the label colour change. The token stays brass. */
.btn--onDark,
.footer .btn,
.section--ground .btn,
.enquiries .btn {
  background: transparent;
  color: var(--on-ground);
  box-shadow: inset 0 0 0 1px rgba(242, 238, 230, 0.30);
}
.btn--onDark:hover, .btn--onDark:focus-visible,
.footer .btn:hover, .footer .btn:focus-visible,
.section--ground .btn:hover, .section--ground .btn:focus-visible,
.enquiries .btn:hover, .enquiries .btn:focus-visible {
  box-shadow: inset 0 0 0 1px var(--accent-soft);
  /* Pinned. The footer's generic link-hover was recolouring this label to
     brass, which is the same class of accident as the bug that made it vanish
     in the first place: a rule written for links reaching a button. Only the
     ring and the token move on hover, on every ground. */
  color: var(--on-ground);
}
.footer .btn:hover .btn__label, .footer .btn:focus-visible .btn__label,
.btn:hover .btn__label, .btn:focus-visible .btn__label { color: inherit; }

/* Solid, for the one or two places a CTA has to carry on its own. */
.btn--solid { background: var(--accent); color: var(--on-accent); box-shadow: none; }
.btn--solid .btn__token { background: var(--on-accent); color: var(--accent); }
.btn--solid:hover, .btn--solid:focus-visible {
  background: var(--accent-link); box-shadow: none;
}

/* ⚠ A .btn with no brass token has to pay its own padding back.
   `.btn` is padded 22.4px on the left and 8px on the right, because the token
   is a 2.3rem disc that fills the right-hand side and supplies the optical
   balance. Every Copy button on the media kit is a label with NO token, so it
   inherited that asymmetry and sat 15px right of centre in a 69px pill.
   Jarryd, 18 Sept: the words "are not centred correctly and [sit] to the right
   hand side of that button." Measured: label at x=1170 in a button spanning
   1147 to 1216, so 23px of air on the left and 8px on the right.
   Structural rather than a class, because the condition IS "has no token" and
   a class would be one more thing to remember on the next button. */
.btn:not(:has(.btn__token)) {
  justify-content: center;
  padding-inline: 1.4rem;
  gap: 0.5rem;
}

/* Ghost, for the secondary action beside a primary one. */
.btn--ghost { background: transparent; color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--hairline); }
.btn--ghost:hover, .btn--ghost:focus-visible { box-shadow: inset 0 0 0 1px var(--ink); }
.section--bone .btn--ghost { box-shadow: inset 0 0 0 1px rgba(17, 29, 46, 0.28); }
.section--bone .btn--ghost:hover { box-shadow: inset 0 0 0 1px var(--ink); }

@media (max-width: 480px) {
  .btn { gap: 1rem; padding-left: 1.1rem; font-size: 0.875rem; }
}


.cta-row { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
.cta-note { font-size: 0.8125rem; color: var(--ink-soft); }

/* The masthead is sticky, so an anchor jump parks its target underneath the
   bar. Reserve the bar's height plus a little breathing room on anything that
   can be linked to. */
:target,
[id] { scroll-margin-top: calc(var(--masthead-h) + 2rem); }

/* Every cutout of Michael comes from a frame cropped mid-torso, so each one
   ends in a razor-straight horizontal edge. Both places we use one, that edge
   lands exactly on the boundary of the next section and reads as the section
   having sliced him in half. Dissolving the last stretch lets him settle into
   what follows instead.

   Kept shallow deliberately: fade any further up and the crossed arms, which
   are the most recognisable part of the pose, go translucent.

   mask-image and drop-shadow must NOT sit on the same element. Together they
   paint the element's whole bounding box as a pale rectangle behind the
   subject, which is glaring on a narrow viewport where the figure is small.
   Same shape of problem as animation-fill versus inline transform: the
   wrapper carries one, the image carries the other. */
.stagehero__figure,
.stakes__cutout {
  mask-image: linear-gradient(to bottom, #000 89%, transparent 100%);
}

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

.masthead {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--gutter);
  padding: 1.4rem var(--gutter);
  border-bottom: var(--rule) solid var(--hairline);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  position: sticky; top: 0; z-index: 20;
  transition: box-shadow var(--duration) var(--ease-out-expo);
}
.masthead.is-scrolled { box-shadow: 0 10px 30px -22px rgba(17, 29, 46, 0.5); }

/* Over a navy section the paper tint reads as a grey slab, so the bar inverts.
   Colours are declared rather than inherited so the logo, links and CTA all
   move together instead of half the bar going dark. */
.masthead.is-dark {
  background: color-mix(in srgb, var(--ground) 82%, transparent);
  border-bottom-color: var(--hairline-dark);
}
.masthead.is-dark .masthead__nav a { color: var(--on-ground); }
.masthead.is-dark .masthead__nav a[aria-current="page"] { color: var(--accent-soft); }
.masthead.is-dark .masthead__nav a::after { background: var(--accent-soft); }
.masthead.is-dark .menu-toggle { color: var(--on-ground); }

/* ⚠ The comment above claims the CTA moves with the rest of the bar. It did
   not. Every other element in the masthead had an is-dark rule and the button
   had none, so it kept `.btn`'s `color: var(--ink)`: navy label on a navy bar,
   on the one control the whole page exists to get people to press. The bar
   inverts over several sections of the homepage, so for most of a scroll the
   primary call to action was effectively unreadable. The brass token beside it
   never changed, which is why it still read as "designed" rather than broken
   and survived three rounds of review. */
.masthead.is-dark .btn {
  color: var(--on-ground);
  box-shadow: inset 0 0 0 1px var(--hairline-dark);
}
.masthead.is-dark .btn:hover,
.masthead.is-dark .btn:focus-visible {
  box-shadow: inset 0 0 0 1px var(--accent-soft);
}

.masthead__logo { width: clamp(132px, 12vw, 178px); height: auto; }
.masthead__nav { display: flex; align-items: center; gap: clamp(1rem, 1.6vw, 2rem); }
.masthead__nav a {
  color: var(--ink); text-decoration: none;
  font-size: 0.875rem; padding: 0.4rem 0; position: relative;
}
.masthead__nav a[aria-current="page"] { color: var(--accent-link); }
.masthead__nav a::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 100%; height: 1px; background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--duration) var(--ease-out-expo);
}
.masthead__nav a:hover::after, .masthead__nav a:focus-visible::after,
.masthead__nav a[aria-current="page"]::after { transform: scaleX(1); }

.menu-toggle { display: none; background: none; border: 0; cursor: pointer;
  min-height: 44px; min-width: 44px; color: var(--ink); font-size: 0.875rem; }

@media (max-width: 900px) {
  .masthead__nav { display: none; }
  .menu-toggle { display: inline-flex; align-items: center; justify-content: center; }
  .masthead__nav.is-open {
    display: flex; flex-direction: column; align-items: flex-start;
    position: absolute; inset: 100% 0 auto 0;
    background: var(--paper); padding: 1rem var(--gutter) 1.5rem;
    border-bottom: var(--rule) solid var(--hairline);
  }
}

/* --------------------------------------------------------------- hero stage
   The Malcom-style sandwich: his name set enormous behind, the cutout figure
   standing in front of it, the argument in front of him. Three depths, three
   scroll speeds. The entrance is the site's one authored moment. */

.stagehero {
  position: relative;
  min-height: clamp(680px, 96vh, 1200px);
  display: grid;
  grid-template-rows: 1fr auto;
  align-items: end;
  overflow: clip;
  background:
    radial-gradient(120% 90% at 78% 30%, var(--bone) 0%, transparent 55%),
    var(--paper);
  border-bottom: var(--rule) solid var(--hairline);
}

/* Depth 1: the name. */
.stagehero__name {
  position: absolute;
  inset: clamp(3.5rem, 7vh, 6rem) 0 auto 0;
  z-index: 1;
  text-align: center;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-giant);
  line-height: 0.86;
  letter-spacing: -0.03em;
  color: var(--ink);
  user-select: none;
  pointer-events: none;
}
.stagehero__name span { display: block; }
.stagehero__name .row2 { color: var(--accent); }

/* Depth 2: the figure. Real alpha matte, never a geometric mask.
   The wrapper owns position and the entrance animation; the img owns the
   parallax transform. They must stay separate: an active animation fill
   overrides inline transforms, so an element cannot carry both. */
.stagehero__figwrap {
  position: absolute;
  z-index: 2;
  left: 50%;
  bottom: 0;
  /* ⚠ The resting X lives in a variable because the entrance keyframe has to
     land on exactly this value. When they were written separately the mobile
     override moved to -50% and the keyframe stayed at -46%, so the figure
     snapped sideways at the end of its entrance, and the fix at the time was
     to switch the animation off on mobile entirely. One value, two users. */
  --fig-x: -42%;
  transform: translateX(var(--fig-x));
  height: min(80vh, 940px);
  pointer-events: none;
  /* The shadow lives here, not on the image, because the image carries the
     bottom fade mask and the two cannot share an element. See the cutout
     fade rule above. */
  filter: drop-shadow(0 34px 44px rgba(17, 29, 46, 0.40));
}
.stagehero__figure {
  height: 100%;
  width: auto;
  /* ⚠ max-width: none is load bearing, and its absence is what squashed him.
     The global `img { max-width: 100% }` at the top of this file resolves
     against the wrapper, but the wrapper is absolutely positioned with no
     width of its own, so its width comes from THIS image. That circularity is
     resolved by shrink-to-fit, which clamped the width while `height: 100%`
     held the height, and the cutout rendered up to 50% narrower than life:
     148px wide where his own proportions want 292px.
     Releasing the clamp lets width follow from the aspect ratio. The hero is
     `overflow: clip`, so a figure wider than its wrapper is contained. */
  max-width: none;
}

/* Depth 3: the argument. */
.stagehero__copy {
  position: relative;
  z-index: 3;
  grid-row: 2;
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding: clamp(1.5rem, 3vh, 2.5rem) var(--gutter) clamp(2.25rem, 5vh, 4rem);
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: end;
  gap: clamp(1.5rem, 4vw, 5rem);
}
.stagehero__copy h1 {
  font-size: clamp(1.6rem, 1.1rem + 1.9vw, 3.1rem);
  line-height: 1.16;
  max-width: 21ch;
}
.stagehero__copy h1 em { font-style: italic; color: var(--accent-link); }
.stagehero__aside {
  justify-self: end;
  text-align: right;
  display: flex; flex-direction: column; align-items: flex-end; gap: 1.1rem;
}
.stagehero__aside .cta-note { max-width: 26ch; }

@media (max-width: 860px) {
  .stagehero { min-height: 88vh; }
  /* Figure steps right so the copy column stays clear of his torso. */
  .stagehero__figwrap { height: min(58vh, 520px); left: 62%; --fig-x: -50%; }
  .stagehero__copy {
    grid-template-columns: 1fr;
    /* Paper rises under the copy so the headline is readable over the figure. */
    background: linear-gradient(to top, var(--paper) 72%, transparent);
    padding-top: clamp(2.5rem, 6vh, 4rem);
  }
  .stagehero__aside { justify-self: start; text-align: left; align-items: flex-start; }
  .stagehero__copy h1 { max-width: 22ch; }
}

/* Entrance choreography. Gated behind .js so nothing depends on script, and
   removed entirely under reduced motion. Framer mapping: Appear effects with
   the same delays. */
@keyframes rise { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
@keyframes figure-in {
  from { opacity: 0; transform: translateX(var(--fig-x)) translateY(3%); }
  to   { opacity: 1; transform: translateX(var(--fig-x)) translateY(0); }
}

.stagehero__name .ltr { display: inline-block; }
.js .stagehero__name .ltr {
  animation: rise 640ms var(--ease-out-expo) both;
  animation-delay: calc(var(--i) * 38ms);
}
.js .stagehero__name .row2 .ltr {
  animation-delay: calc(120ms + var(--i) * 38ms);
}
.js .stagehero__figwrap { animation: figure-in 820ms var(--ease-out-expo) 180ms both; }
.js .stagehero__copy > * { animation: rise 640ms var(--ease-out-expo) both; }
.js .stagehero__copy > :first-child { animation-delay: 340ms; }
.js .stagehero__copy > :last-child { animation-delay: 440ms; }
/* The entrance now runs at every width. It used to be switched off below 860
   because the keyframe and the mobile resting transform disagreed; both read
   --fig-x now, so there is nothing left to snap. */

/* ----------------------------------------------------------------- pillars */

.pillars { display: grid; grid-template-columns: repeat(3, 1fr); background: var(--ground); }
.pillars div {
  padding: clamp(1.4rem, 1rem + 1.4vw, 2.4rem) var(--gutter);
  border-left: var(--rule) solid var(--hairline-dark);
}
.pillars div:first-child { border-left: none; }
.pillars span {
  display: block; font-family: var(--font-display);
  font-size: clamp(1.25rem, 0.9rem + 1.2vw, 2.1rem); line-height: 1.15;
  color: var(--on-ground);
}
.pillars small { display: block; margin-top: 0.5rem; font-size: clamp(0.8125rem, 0.75rem + 0.25vw, 1rem);
  line-height: 1.5; color: rgba(242, 238, 230, 0.74); }
@media (max-width: 720px) {
  .pillars { grid-template-columns: 1fr; }
  .pillars div { border-left: 0; border-top: var(--rule) solid var(--hairline-dark); }
  .pillars div:first-child { border-top: none; }
}

/* ------------------------------------------------------------------- logos */

/* Client marks as a slow marquee lane, large. Framer mapping: Ticker. */
.logos {
  /* Jarryd, 18 Sept: "the padding around the logo bar is way too high, it
     needs to be tighter." Measured, the band was 312px tall around a 202px
     lane: 54px of its own padding at each end on top of the lane's, so a row
     of small grey marks sat in a slab taller than the marks themselves. The
     band is a divider between the headline and the quote, not a section, and
     a divider that occupies a third of the viewport stops dividing. */
  padding-block: clamp(1.1rem, 0.8rem + 1vw, 1.9rem);
  /* The floor is 20px rather than 26px so a long wordmark still fits inside
     its max-width on a 320px screen without needing to be squeezed. */
  --logo-h: clamp(20px, 2.4vw, 38px);
  border-top: var(--rule) solid var(--hairline);
  border-bottom: var(--rule) solid var(--hairline);
  overflow: clip;
  /* Without this the lane guillotines a logo at each edge, which reads as a
     layout fault rather than a loop. Fading the ends lets marks arrive and
     leave. The fade is a touch wider than the gutter so a wide mark is fully
     dissolved before it reaches the page edge. */
  --logo-fade: clamp(3rem, 7vw, 9rem);
  mask-image: linear-gradient(to right,
    transparent 0, #000 var(--logo-fade),
    #000 calc(100% - var(--logo-fade)), transparent 100%);
}
/* The static fallback is centred and wrapped, so it has no edges to dissolve
   and a mask would only dim the outer marks. */
.no-motion .logos, .logos:has(.logos__lane.is-static) { mask-image: none; }
.logos__lane {
  --lane-gap: clamp(3.5rem, 5vw, 7rem);
  display: flex;
  align-items: center;
  width: max-content;
  animation: ticker-scroll 55s linear infinite;
}
.logos__lane .lane__set {
  align-items: center;
  gap: var(--lane-gap);
  padding-right: var(--lane-gap);
}

/* ⚠ Height, not width. The marks are cropped to their own ink by
   design/trim_logos.py, so the element IS the logo and a row of them can be
   balanced the way a designer balances one: same optical weight, not same
   pixel width. --logo-scale comes from each mark's aspect ratio and keeps a
   square stacked lockup from towering over a long wordmark. */
.logos img {
  height: calc(var(--logo-h) * var(--logo-scale, 1));
  width: auto; max-width: 56vw;
  /* ⚠ `contain`, not the default `fill`. With a set height and a max-width,
     the moment max-width bites the box stops matching the file and `fill`
     stretches the mark to the new shape. The audit measured Progressive at
     320px squashed 33%, Janus Henderson 8%, Transamerica 6%: a client's
     wordmark visibly distorted on a phone, which is the one thing a logo may
     never be. `contain` letterboxes instead of lying about the proportions. */
  object-fit: contain; object-position: center;
  mix-blend-mode: multiply; filter: grayscale(1); opacity: 0.8;
  transition: opacity var(--duration) var(--ease-out-expo),
              filter var(--duration) var(--ease-out-expo);
}
.logos img:hover { opacity: 1; filter: grayscale(0); }
.no-motion .logos__lane, .logos__lane.is-static {
  animation: none; width: auto;
  justify-content: center;
  padding-inline: var(--gutter);
}
.logos__lane.is-static .lane__set, .no-motion .logos__lane .lane__set {
  flex-wrap: wrap; justify-content: center; row-gap: 2rem; padding-right: 0;
}

/* ------------------------------------------------------------------- quote */

.quote {
  margin: 0; display: grid;
  padding-block: clamp(2rem, 1.5rem + 2vw, 4rem);
  /* The attribution needs a rail, not a third of the page. Giving it an equal
     share left a void beside a quote that was itself capped narrow, so the
     whole figure read as crammed into the middle of an empty band. */
  grid-template-columns: minmax(200px, 0.55fr) minmax(0, 2.2fr);
  gap: clamp(1.5rem, 4vw, 4.5rem); align-items: start;
}
.quote blockquote {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.5rem, 1rem + 2.2vw, 3.4rem); line-height: 1.24;
  letter-spacing: -0.015em; color: var(--ink); max-width: 30ch; text-wrap: pretty;
}
.quote blockquote::before { content: "\201C"; color: var(--accent); margin-left: -0.42em; }
.quote figcaption {
  border-top: 2px solid var(--ink); padding-top: 0.9rem;
  font-size: 0.875rem; line-height: 1.5; color: var(--ink); align-self: start;
}
.quote figcaption strong { display: block; font-weight: 600; color: var(--ink); }
.quote figcaption span { color: var(--ink-soft); }
@media (max-width: 860px) {
  .quote { grid-template-columns: 1fr; }
  .quote figcaption { order: 2; }
}

/* ------------------------------------------------------------------ ticker
   One slow lane of what rooms say afterwards. Framer mapping: the native
   Ticker component at low speed, pause on hover. Reduced motion gets a
   static wrapped list instead of a frozen half-sentence. */

.ticker {
  border-top: var(--rule) solid var(--hairline);
  border-bottom: var(--rule) solid var(--hairline);
  overflow: hidden;
  padding-block: clamp(1.4rem, 1rem + 1.2vw, 2.5rem);
}
/* Marquees run in perpetuity. Nothing the pointer does may slow, pause or
   otherwise touch them: a strip that stops when the cursor crosses it reads
   as broken, and these sit under content people move across constantly.
   pointer-events is off so hover cannot be triggered at all. */
.ticker__lane, .logos__lane, .covstrip__lane {
  pointer-events: none;
  animation-play-state: running !important;
}
.ticker__lane {
  --lane-gap: 4.5rem;
  display: flex; width: max-content;
  animation: ticker-scroll 90s linear infinite;
}
.lane__set {
  display: flex; align-items: baseline;
  gap: var(--lane-gap);
  padding-right: var(--lane-gap);
}
/* Pausing outright made it look broken whenever the pointer happened to rest
   over the strip. Slowing it keeps quotes readable without ever appearing
   stuck. */

.ticker__item {
  display: flex; align-items: baseline; gap: 1rem; white-space: nowrap;
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 1rem + 0.7vw, 1.9rem);
  color: var(--ink);
}
.ticker__item cite {
  font-style: normal; font-family: var(--font-body);
  font-size: clamp(0.78rem, 0.7rem + 0.2vw, 0.95rem); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-soft);
}
/* Opening quote mark: these items are testimonials. The coverage strip
   reuses the same ticker machinery but carries publication names, which
   are not quotations, so it opts out. */
.ticker__item::before { content: "\201C"; color: var(--accent); }
.covstrip__item::before { content: none; }
@keyframes ticker-scroll { to { transform: translateX(-50%); } }

.no-motion .ticker__lane, .ticker__lane.is-static {
  animation: none; width: auto;
  padding-inline: var(--gutter);
}
.is-static .lane__set, .no-motion .lane__set {
  flex-wrap: wrap; gap: 1rem 3rem; padding-right: 0;
}
.no-motion .ticker__item, .is-static .ticker__item { white-space: normal; }

/* --------------------------------------------------------------- questions
   The planner's three doubts, as an interrogation column rather than a card
   grid. Hairline rows, the question set large. */

.doubts { border-top: var(--rule) solid var(--hairline); }
.doubts > div {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
  gap: clamp(1rem, 3vw, 3.5rem); align-items: baseline;
  padding-block: clamp(1.4rem, 1rem + 1.4vw, 2.2rem);
  border-bottom: var(--rule) solid var(--hairline);
}
.doubts h3 { font-size: clamp(1.4rem, 1rem + 1.6vw, 2.7rem); font-style: italic; }
/* At 46ch and 0.99rem the answer filled less than half its column, so the
   right third of the section sat empty next to a 43px question. Widening the
   measure and lifting the size closes the gap and the hierarchy jump. */
.doubts p {
  color: var(--ink-soft);
  font-size: clamp(0.9875rem, 0.9rem + 0.3vw, 1.1875rem);
  line-height: 1.6;
  max-width: 58ch;
}
@media (max-width: 720px) { .doubts > div { grid-template-columns: 1fr; gap: 0.5rem; } }

/* Statement list. Hairline rows of display serif, no bullet glyphs:
   dash-like marks read as punctuation errors, so the type carries it. */
.ticks { list-style: none; margin: 0; padding: 0; }
.ticks li {
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 1rem + 0.9vw, 1.9rem);
  line-height: 1.3;
  color: var(--ink);
  padding-block: clamp(0.9rem, 0.7rem + 0.6vw, 1.4rem);
  border-top: var(--rule) solid var(--hairline);
}
.ticks li:last-child { border-bottom: var(--rule) solid var(--hairline); }
.section--ground .ticks li { color: var(--on-ground); border-color: var(--hairline-dark); }

/* ------------------------------------------------------------------- triad
   The client's own crescendo, played as a scroll sequence. The section is
   tall; the lines are sticky; each ignites as the reader passes its third.
   Framer mapping: Sticky section + Scroll Transform on each line. */

.triad { background: var(--ground); }
.triad__track { min-height: 230vh; position: relative; }
.triad__stage {
  position: sticky; top: 0; min-height: 100vh;
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  text-align: center;
  gap: clamp(1.4rem, 3.5vh, 3rem);
  padding: 0 var(--gutter);
}
.triad__line {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 1rem + 3.1vw, 4.6rem);
  line-height: 1.12; letter-spacing: -0.02em;
  color: rgba(242, 238, 230, 0.24);
  max-width: 26ch;
  transition: color 600ms var(--ease-out-expo), transform 600ms var(--ease-out-expo);
  transform: translateY(8px);
}
.triad__line em { font-style: italic; color: inherit; }
.triad__line.is-lit { color: var(--on-ground); transform: none; }
.triad__line.is-lit em { color: var(--accent-soft); }
.triad__source {
  margin-top: clamp(1.5rem, 4vh, 3rem);
  font-size: 0.8125rem; color: rgba(242, 238, 230, 0.60);
  opacity: 0; transition: opacity 600ms var(--ease-out-expo);
}
.triad__source.is-lit { opacity: 1; }
/* Was micro type lost under three lines of display. It is the attribution for
   the whole crescendo and should be readable. */
.triad__source {
  font-size: clamp(0.9375rem, 0.85rem + 0.45vw, 1.25rem);
  letter-spacing: 0.02em;
  color: var(--accent-soft);
  margin-top: clamp(1.5rem, 3vw, 2.75rem);
}

.no-motion .triad__track { min-height: 0; }
.no-motion .triad__stage { position: static; min-height: 0; padding-block: var(--space-section); }
.no-motion .triad__line { color: var(--on-ground); transform: none; }
.no-motion .triad__line em { color: var(--accent-soft); }
.no-motion .triad__source { opacity: 1; }

/* Numbered plan steps. The numeral carries the sequence, which is real. */
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(1rem, 2.4vw, 2.5rem); counter-reset: step; }
@media (max-width: 860px) { .steps { grid-template-columns: 1fr; } }
.step { display: flex; flex-direction: column; gap: 0.65rem; counter-increment: step; }
.step::before {
  content: counter(step, decimal-leading-zero);
  font-family: var(--font-display); font-size: clamp(2.8rem, 1.6rem + 3vw, 5.2rem);
  line-height: 1; color: var(--accent); opacity: 0.9;
}
.step h3 { color: var(--ink); }
.section--ground .step h3 { color: var(--on-ground); }
.section--ground .step::before { color: var(--accent-soft); }
.step p { color: var(--ink-soft); font-size: var(--text-body); max-width: 34ch; }
.section--ground .step p { color: rgba(242, 238, 230, 0.76); }

/* ------------------------------------------------------------------- video */

.video {
  position: relative; margin: 0; background: var(--ground);
  aspect-ratio: 16 / 9; overflow: hidden;
}
.video img { width: 100%; height: 100%; object-fit: cover; opacity: 0.72; }
.video__play {
  position: absolute; inset: 0; margin: auto;
  width: 88px; height: 88px; border-radius: 50%;
  background: var(--accent); color: var(--on-accent);
  border: 0; cursor: pointer; display: grid; place-items: center;
  transition: transform var(--duration) var(--ease-out-expo);
}
.video__play:hover, .video__play:focus-visible { transform: scale(1.06); }
.video__missing {
  position: absolute; inset: auto 0 0 0; padding: 0.9rem var(--gutter);
  background: rgba(21, 42, 69, 0.84); color: var(--on-ground); font-size: 0.8125rem;
}

/* ------------------------------------------------------- listings and grids */

.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(1.25rem, 2.6vw, 2.5rem); }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: clamp(1.5rem, 3vw, 3rem); }
@media (max-width: 900px) { .grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 660px) { .grid-3, .grid-2 { grid-template-columns: 1fr; } }

/* Keynote rows. The italic subtitle is the differentiator; no fake numbering. */
.keynote {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(1.5rem, 4vw, 5rem);
  padding-block: clamp(3rem, 2rem + 3vw, 5.5rem);
  border-top: var(--rule) solid var(--hairline);
  align-items: center;
}
/* Alternate which side the photograph sits on. */
.keynote:nth-of-type(even) .keynote__media { order: 2; }
.keynote__media { margin: 0; overflow: clip; }
.keynote__media img {
  width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover;
  /* Same reason as the homepage panels: centred cover on a wide stage frame
     crops to whatever is in the middle, which here was a curtain. */
  object-position: var(--pos, 50% 22%);
  transition: transform 900ms var(--ease-out-expo);
}
/* The first block sits directly under the page intro, which already ends in a
   rule. A second one right beneath it read as a stray line across the page. */
.keynote:first-of-type { border-top: 0; }
.keynote:hover .keynote__media img { transform: scale(1.03); }
.keynote__body { display: flex; flex-direction: column; gap: 1.25rem; }
.keynote:last-of-type { border-bottom: var(--rule) solid var(--hairline); }
.keynote h2 { font-size: var(--text-title); }
.keynote__sub { font-family: var(--font-display); font-style: italic;
  font-size: clamp(1.05rem, 0.95rem + 0.5vw, 1.35rem); color: var(--accent-link); margin-top: 0.4rem; }
.keynote__aud { margin-top: 1.1rem; font-size: var(--text-body); color: var(--ink-soft); max-width: 38ch; }
@media (max-width: 860px) {
  .keynote { grid-template-columns: 1fr; }
  .keynote:nth-of-type(even) .keynote__media { order: 0; }
  .keynote__media img { aspect-ratio: 3 / 2; }
}

/* Case studies as alternating editorial rows, not a card grid. */
.case {
  display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 4.5rem);
  padding-block: clamp(2rem, 1.5rem + 2vw, 3.5rem);
  border-top: var(--rule) solid var(--hairline);
  align-items: start;
}
.case:nth-child(even) > .case__pull { order: -1; }
.case h3 { max-width: 22ch; }
.case__who { font-size: 0.875rem; color: var(--ink-soft); margin-top: 0.4rem; }
.case__body { margin-top: 1rem; color: var(--ink-soft); font-size: var(--text-body); max-width: 58ch; }
.case__pull {
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(1.3rem, 1rem + 1.3vw, 2.2rem); line-height: 1.35;
  color: var(--ink); align-self: center; max-width: 30ch;
  background: var(--paper);
  padding: clamp(1.5rem, 2.5vw, 2.75rem);
  position: relative;
}
.section--bone .case__pull { background: var(--paper); }
.case__pull::before {
  content: "\201C";
  display: block;
  font-size: 2.2em; line-height: 0.6;
  margin-bottom: 0.25em;
  color: var(--accent);
}
@media (max-width: 860px) {
  .case { grid-template-columns: 1fr; }
  .case:nth-child(even) > .case__pull { order: 0; }
}

/* Press and events rows read as a table without being one. */
.rows { display: flex; flex-direction: column; }
.row {
  display: grid; grid-template-columns: 8rem minmax(0, 1fr) 10rem;
  gap: clamp(1rem, 2.5vw, 2.5rem); align-items: baseline;
  padding-block: 1.35rem;
  border-top: var(--rule) solid var(--hairline);
  color: var(--ink); text-decoration: none;
}
.rows .row:last-child { border-bottom: var(--rule) solid var(--hairline); }
.row__meta { font-size: 0.8125rem; color: var(--ink-soft); letter-spacing: 0.02em; }
.row__title { font-family: var(--font-display); font-size: clamp(1.15rem, 1rem + 0.5vw, 1.6rem); color: var(--ink); }
a.row:hover .row__title { color: var(--accent-link); }
.row__tag {
  justify-self: end; font-size: var(--text-micro); letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--accent-link);
}
@media (max-width: 720px) {
  .row { grid-template-columns: 1fr; gap: 0.35rem; }
  .row__tag { justify-self: start; }
}

/* Empty state. Says what is missing and who fills it. */
.empty {
  padding: clamp(2rem, 1.5rem + 2vw, 3.5rem);
  background: var(--bone); border-left: 1px solid var(--accent);
}
.empty h3 { color: var(--ink); margin-bottom: 0.5rem; }
.empty p { color: var(--ink-soft); max-width: 52ch; }


/* ------------------------------------------------------------------ stakes
   The cost of the wrong choice, with Michael present rather than abstract. */

.section--flush { padding-bottom: 0; overflow: clip; }
.stakes {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 5rem);
  align-items: end;
}
.stakes__copy { padding-bottom: var(--space-section); }
.section--tight .stakes__copy { padding-bottom: clamp(3rem, 2rem + 3vw, 5.5rem); }
.stakes__cutout {
  justify-self: center;
  align-self: end;
  height: clamp(300px, 38vw, 560px);
  width: auto;
  display: block;
  margin-bottom: 0;
  /* No drop-shadow here: this cutout carries the bottom fade mask, and the
     two together paint a pale rectangle behind the subject. It sits on navy
     where a navy shadow read as nothing anyway. */
}
@media (max-width: 860px) {
  .stakes { grid-template-columns: 1fr; }
  .stakes__cutout { justify-self: start; height: 300px; }
}

/* ----------------------------------------------------------------- book 3d
   A physical object for a book with no cover yet: palette-bound boards, a
   blind-embossed name, and the honest line "Title to be announced". Tilts
   toward the cursor; idles on a slow float for touch and reduced motion.
   Framer mapping: code component (Book3D). */

.book3d {
  perspective: 2000px;
  display: grid;
  place-items: center;
  min-height: clamp(360px, 46vw, 560px);
}
.book3d__obj {
  position: relative;
  --thick: 66px;
  width: clamp(250px, 28vw, 380px);
  aspect-ratio: 1 / 1.47;
  transform-style: preserve-3d;
  transform: rotateY(var(--ry, 7deg)) rotateX(var(--rx, 1deg));
  will-change: transform;
}
.js .book3d__obj:not(.is-tracking) { animation: book-idle 9s ease-in-out infinite alternate; }
/* Bookland's book sits essentially face on. The idle drift is a breath,
   not a turn. */
@keyframes book-idle {
  from { transform: rotateY(8deg) rotateX(1.5deg); }
  to   { transform: rotateY(4deg) rotateX(0.5deg); }
}
.book3d__face {
  position: absolute; inset: 0;
  backface-visibility: hidden;
}
.book3d__front {
  background:
    linear-gradient(135deg, rgba(239, 229, 213, 0.06), transparent 40%),
    url("../../assets/pattern-ground.png") center / 420px auto,
    var(--ground-2);
  border-radius: 6px 2px 2px 6px;
  box-shadow: 0 40px 70px -30px rgba(0, 0, 0, 0.55);
  display: grid;
  align-content: end;
  gap: 0.4rem;
  padding: clamp(1.25rem, 2vw, 2rem);
  transform: translateZ(calc(var(--thick, 34px) / 2));
}
.book3d__front .name {
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 1.4vw, 1.8rem);
  line-height: 1.1;
  color: var(--on-ground);
}
.book3d__front .tba {
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-soft);
}

/* The real jacket, once uploaded and live. Same object, same 3D transform,
   the front face just wears art instead of a typographic placeholder.
   background-image itself comes from an inline style per page (the cover URL
   is content, not a fixed asset), this only sets how that image sits. */
.book3d__front--cover {
  background-size: cover;
  background-position: center;
  display: block;
}

/* ---------- Tour calendar ----------
   Jarryd, 21 Sept: "use the calendar in a motion animated way". A real month
   grid, server-rendered from the events collection; the motion is the detail
   panel below it, not the grid itself, because the grid IS the content and
   should read instantly, not perform for a reader trying to find a date. */
.cal {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: clamp(2rem, 4vw, 3.5rem);
  margin-top: 2.5rem;
}
.cal__month { max-width: 30rem; }
.cal__title {
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(1.3rem, 1vw + 1rem, 1.7rem);
  margin: 0 0 1rem;
  color: var(--on-ground);
}
.cal__grid { width: 100%; border-collapse: collapse; table-layout: fixed; }
.cal__grid th {
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em;
  color: var(--accent-soft); padding-block: 0.5rem;
}
.cal__cell {
  text-align: center; padding: 0.15rem;
  height: 2.75rem;
  color: rgba(242, 238, 230, 0.42);
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}
.cal__cell--pad { color: transparent; }

/* The sweep. Each cell carries its own --i (one counter per month, set in
   calendar_view) and rides the .reveal observer that .cal__month already
   has, so there is no second IntersectionObserver for this.

   ⚠ 14ms per cell, not the 60-90ms used elsewhere on the site. A month is
   ~35 cells: at 60ms the last cell lands two full seconds after the first,
   which stops being a reveal and becomes a loading screen. 14ms sweeps the
   whole grid in under half a second and reads as one gesture.

   This supersedes the earlier "the grid should read instantly, never
   perform" note. Jarryd asked for the calendar to carry real motion. The
   compromise is that the motion is fast and the type never moves far enough
   to be unreadable mid-flight. */
.js-reveal .cal__month .cal__cell > * {
  opacity: 0; transform: translateY(6px) scale(0.94);
}
.js-reveal .cal__month.is-in .cal__cell > * {
  opacity: 1; transform: none;
  transition: opacity 320ms var(--ease-out-expo, ease-out),
              transform 320ms var(--ease-out-expo, ease-out);
  transition-delay: calc(var(--i, 0) * 14ms);
}
.cal__day {
  all: unset; box-sizing: border-box;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.15rem; width: 100%; height: 100%; border-radius: 10px;
  cursor: pointer;
  color: var(--on-ground);
  /* A marked day has to read as marked BEFORE anyone hovers it. On a dark
     ground a bare dot is not enough, so the cell carries a faint brass wash
     and hairline of its own at rest. */
  background: rgba(176, 141, 87, 0.10);
  box-shadow: inset 0 0 0 1px rgba(176, 141, 87, 0.28);
  transition: background-color var(--duration-fast, 150ms) var(--ease-out-expo, ease-out),
              box-shadow var(--duration-fast, 150ms) var(--ease-out-expo, ease-out),
              transform var(--duration-fast, 150ms) var(--ease-out-expo, ease-out);
}
.cal__day:hover, .cal__day:focus-visible {
  background: rgba(176, 141, 87, 0.22);
  box-shadow: inset 0 0 0 1px var(--accent);
  transform: translateY(-2px) scale(1.04);
}
.cal__day[aria-expanded="true"] {
  background: var(--accent); color: var(--on-accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  transform: translateY(-2px) scale(1.04);
}
.cal__day[aria-expanded="true"] .cal__dot { background: var(--on-accent); }
.cal__num { font-weight: 600; }
.cal__dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }

/* The detail panel. It animates in from a hidden, translated resting state
   the moment a marked day is chosen, and never sits mid-transition on load:
   [hidden] wins over the transition every time, satisfying "show the page at
   rest" while still giving the reveal real motion once triggered. */
/* ⚠ A RAISED DARK SURFACE, not the bone card this used to be. The section
   went to --ground when the calendar moved into the tour slot, and
   `.section--ground p` / `.section--ground .btn` both reach inside this
   panel: on a cream card that paints the venue line and the Tickets label
   near-white on cream, which is invisible rather than merely ugly. Matching
   the panel to the ground costs three colour values and removes the whole
   class of override. Do not put this back on --bone without also pinning
   every text and button colour inside it. */
.cal__panel {
  margin-top: 2rem; max-width: 24rem;
  background: rgba(255, 255, 255, 0.05);
  box-shadow: inset 0 0 0 1px rgba(176, 141, 87, 0.30);
  border-radius: 10px;
  padding: 1.5rem 1.75rem;
  position: relative;
  opacity: 0; transform: translateY(10px);
  transition: opacity var(--duration-normal, 300ms) var(--ease-out-expo, ease-out),
              transform var(--duration-normal, 300ms) var(--ease-out-expo, ease-out);
}
.cal__panel.is-shown { opacity: 1; transform: translateY(0); }
.cal__panel[hidden] { display: none; }
.cal__close {
  all: unset; position: absolute; top: 0.75rem; right: 0.9rem;
  font-size: 1.25rem; line-height: 1;
  color: rgba(242, 238, 230, 0.55); cursor: pointer;
}
.cal__close:hover, .cal__close:focus-visible { color: var(--on-ground); }
.cal__panel-when {
  font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--accent-soft); margin: 0 0 0.35rem;
}
/* Both pinned against `.section--ground p`, which would otherwise win. */
.section--ground .cal__panel-where {
  font-family: var(--font-display); font-size: 1.4rem; margin: 0 0 0.15rem;
  color: var(--on-ground);
}
.section--ground .cal__panel-venue {
  color: rgba(242, 238, 230, 0.72); font-size: 0.9rem; margin: 0 0 1rem;
}
.cal__panel-link[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .cal__day, .cal__panel { transition: none; }
  /* The sweep is motion for its own sake, so it goes entirely: the cells
     land in place rather than arriving late with no animation. */
  .js-reveal .cal__month .cal__cell > *,
  .js-reveal .cal__month.is-in .cal__cell > * {
    opacity: 1; transform: none; transition: none; transition-delay: 0s;
  }
}
.book3d__back {
  background: var(--ground-2);
  border-radius: 6px 2px 2px 6px;
  transform: rotateY(180deg) translateZ(calc(var(--thick, 34px) / 2));
}
.book3d__spine {
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: var(--thick, 34px);
  background: var(--accent);
  /* Spine on the right, because the book now turns the other way. */
  transform: rotateY(90deg) translateZ(calc(var(--thick, 34px) / 2));
  display: grid; place-items: center;
}
.book3d__spine span {
  writing-mode: vertical-rl;
  font-family: var(--font-display);
  font-size: 0.8rem;
  letter-spacing: 0.12em;
  color: var(--on-accent);
  white-space: nowrap;
}
.book3d__pages {
  position: absolute;
  top: 1.5%; bottom: 1.5%; left: 0;
  width: var(--thick, 34px);
  background: repeating-linear-gradient(to right,
    #DDD0BA 0, #DDD0BA 1px, #E8DCC8 1px, #E8DCC8 3px);
  transform: rotateY(-90deg) translateZ(calc(100% - var(--thick, 34px) / 2));
}
.book3d__shadow {
  position: absolute;
  left: 50%; bottom: clamp(-2rem, -3vh, -1rem);
  width: 68%; height: 26px;
  margin-left: -34%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(0, 0, 0, 0.5), transparent 70%);
  filter: blur(6px);
  opacity: 0.35;
  pointer-events: none;
}
.no-motion .book3d__obj { animation: none; transform: rotateY(-24deg) rotateX(4deg); }

/* -------------------------------------------------------- timeline, animated
   The spine draws itself as the reader descends; each entry ignites as its
   dot is passed. Framer mapping: Scroll Transform on the line, Appear on
   entries. */

/* ---------------------------------------------------------------- timeline
   Verified biography only. Each entry reveals as it enters. */

.timeline { position: relative; margin-left: 0.4rem; }
.timeline::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: var(--rule); background: var(--hairline);
}
.timeline::after {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: 2px; margin-left: -0.5px; background: var(--accent);
  transform: scaleY(var(--drawn, 0));
  transform-origin: top;
}
.no-motion .timeline::after { transform: none; }
.timeline > div {
  position: relative;
  padding: 0 0 clamp(2rem, 1.4rem + 2vw, 3.5rem) clamp(1.75rem, 3.5vw, 3.5rem);
}
.timeline > div::before {
  content: ""; position: absolute; left: -5px; top: 0.5em;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--bone);
  box-shadow: inset 0 0 0 2px var(--ink-soft);
  transition: background var(--duration) var(--ease-out-expo),
              box-shadow var(--duration) var(--ease-out-expo);
}
.timeline > div.is-past::before {
  background: var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}
.timeline > div > * { transition: opacity var(--duration-slow) var(--ease-out-expo); }
.js .timeline > div:not(.is-past) > * { opacity: 0.35; }
.no-motion .timeline > div > * { opacity: 1 !important; }
.timeline time {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 1.1rem + 1.7vw, 3rem);
  color: var(--ink); display: block; line-height: 1.1;
}
.timeline h3 { font-family: var(--font-body); font-size: 1rem; font-weight: 600;
  letter-spacing: 0; margin-top: 0.35rem; color: var(--ink); }
.timeline p { margin-top: 0.45rem; color: var(--ink-soft); font-size: 0.9875rem; max-width: 52ch; }

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

.book {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  gap: clamp(1.5rem, 4vw, 4.5rem); align-items: center;
}
@media (max-width: 860px) { .book { grid-template-columns: 1fr; } }
.book__cover {
  aspect-ratio: 2 / 3; background: var(--ground-2);
  display: grid; place-items: center; padding: 2rem; text-align: center;
  box-shadow: 0 30px 60px -30px rgba(17, 29, 46, 0.6);
}
.book__cover p { color: rgba(242, 238, 230, 0.62); font-size: 0.8125rem; line-height: 1.5; }

/* -------------------------------------------------------------------- faq */

.faq { border-top: var(--rule) solid var(--hairline); }
.faq details { border-bottom: var(--rule) solid var(--hairline); }
.faq summary {
  list-style: none; cursor: pointer;
  display: grid; grid-template-columns: minmax(0, 1fr) 2rem;
  align-items: center; gap: 1rem;
  padding-block: 1.3rem;
  font-family: var(--font-display); font-size: clamp(1.1rem, 1rem + 0.6vw, 1.45rem);
  color: var(--ink);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: ""; justify-self: end;
  width: 12px; height: 12px;
  border-right: 2px solid var(--accent-link); border-bottom: 2px solid var(--accent-link);
  transform: rotate(45deg);
  transition: transform var(--duration) var(--ease-out-expo);
}
.faq details[open] summary::after { transform: rotate(225deg); }
.faq summary:hover { color: var(--accent-link); }
.faq .faq__a { padding: 0 0 1.4rem; color: var(--ink-soft); max-width: 60ch; }
.faq .faq__a a { color: var(--accent-link); }

/* ------------------------------------------------------------------- forms */

.form { display: grid; gap: 1.15rem; max-width: 34rem; }
.field { display: flex; flex-direction: column; gap: 0.4rem; }
.field label { font-size: 0.875rem; font-weight: 500; color: var(--ink); }
.field input, .field select, .field textarea {
  min-height: 44px; min-width: 0; width: 100%; padding: 0.7rem 0.85rem;
  border: 1px solid var(--hairline); border-radius: 2px;
  background: var(--paper); color: var(--ink);
  font-family: var(--font-body); font-size: 1rem;
  transition: border-color var(--duration) var(--ease-out-expo);
}
.field input::placeholder, .field textarea::placeholder { color: var(--ink-soft); opacity: 0.75; }
.field textarea { min-height: 120px; resize: vertical; }
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent;
}
.field__hint { font-size: 0.8125rem; color: var(--ink-soft); }
.field--pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.15rem; }
@media (max-width: 560px) { .field--pair { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------- media kit */

.gate { max-width: 26rem; display: grid; gap: 1rem; }
.gate__error { color: #9B2C1B; font-size: 0.875rem; min-height: 1.3em; }

.kit { display: grid; gap: clamp(2rem, 1.5rem + 2vw, 3.5rem); }
.kit__group h2 { font-size: var(--text-title); margin-bottom: 1rem; }
.asset {
  display: grid; grid-template-columns: 4.5rem minmax(0, 1fr) auto;
  gap: 1.25rem; align-items: center;
  padding-block: 1.1rem; border-top: var(--rule) solid var(--hairline);
}
.asset:last-child { border-bottom: var(--rule) solid var(--hairline); }
.asset__thumb {
  aspect-ratio: 1; background: var(--bone); display: grid; place-items: center;
  font-size: var(--text-micro); color: var(--ink-soft); overflow: hidden;
}
.asset__thumb img { width: 100%; height: 100%; object-fit: cover; }
.asset__name { font-family: var(--font-display); font-size: 1.05rem; color: var(--ink); }
.asset__meta { font-size: 0.8125rem; color: var(--ink-soft); margin-top: 0.15rem; }
@media (max-width: 640px) {
  .asset { grid-template-columns: 3.25rem minmax(0, 1fr); }
  .asset > .btn { grid-column: 2; justify-self: start; }
}

/* ------------------------------------------------------------------ pagehead */

.pagehead {
  padding: clamp(3rem, 2rem + 4vw, 6rem) var(--gutter) clamp(2rem, 1.5rem + 2vw, 3.5rem);
  border-bottom: var(--rule) solid var(--hairline);
  display: flex; flex-direction: column; gap: 1.25rem;
}
.pagehead h1 { font-size: var(--text-display); max-width: 18ch; }
.pagehead .lead { max-width: 52ch; }

/* ⚠ The legal pages' header has to sit in the SAME box as their body, and it
   did not. `.pagehead` supplies a gutter and the `.shell` inside it supplies
   another, so the header was inset twice; the body below is a single `.shell`
   capped to 68ch. Measured, the two disagreed by 24px on a handset and 10px
   on desktop, which is exactly the misalignment Jarryd reported on both.
   Dropping the outer gutter and matching the measure makes one edge. */
.pagehead--legal { padding-inline: 0; border-bottom: 0; }
/* ⚠ The rule moves onto the inner shell.
   Dropping the outer gutter fixed the header/body alignment, but it left
   `.pagehead`'s own border-bottom running the full width of the viewport
   underneath content that stops at 68ch. A hairline that outruns the text it
   sits under reads as a stray line rather than a divider. On the shell it
   ends exactly where the words do. */
.pagehead--legal > .shell {
  max-width: 68ch;
  padding-bottom: clamp(2rem, 1.5rem + 2vw, 3.5rem);
  border-bottom: var(--rule) solid var(--hairline);
}


/* --------------------------------------------------------------- interlude
   Full-bleed graded stage photography with one line of display type. The
   image carries 116% height and drifts inside its frame, so depth comes
   from the crop rather than from gaps. Framer mapping: image layer with
   Scroll Speed inside a clipping frame. */

.interlude {
  position: relative;
  overflow: clip;
  background: var(--ground);
  min-height: clamp(440px, 78vh, 900px);
  display: grid;
  align-items: end;
}
.interlude__img {
  position: absolute;
  inset: calc(var(--over, 16%) * -1) 0;
  width: 100%;
  height: calc(100% + var(--over, 16%) * 2);
  object-fit: cover;
  object-position: var(--pos, 50% 30%);
}
/* ⚠ Handsets get their own crop. The band keeps its height while the viewport
   loses most of its width, so `cover` discards the sides rather than the top
   and bottom, and the desktop crop framed an empty stage on all three bands.
   --pos-m is set per band in build.py from the actual frame. */
@media (max-width: 860px) {
  .interlude__img { object-position: var(--pos-m, var(--pos, 50% 30%)); }
}
/* ⚠ A band whose subject is a PERSON gets NO overhang and NO parallax.
   
   The overhang exists so the parallax has somewhere to travel, and that is
   right for a room or a crowd where every crop is a good crop. With one
   figure it is actively harmful: at 16% the travel is ±112px on a 700px
   window, which drifted Michael's head clean out of frame, and even at 6% it
   ate the headroom the crop was cut to give him. The source has him near the
   top of the frame, so there is no more headroom to find.
   
   With --over: 0 the image box is the band exactly, the crop lands where
   design/ingest_media.py put it, and it never moves. Losing the parallax on
   one band is a much smaller cost than a decapitated subject. */
.interlude--subject .interlude__img { --over: 0%; }
.interlude::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(21, 42, 69, 0.74), rgba(21, 42, 69, 0.10) 55%);
}
.interlude__line {
  position: relative;
  z-index: 1;
  padding: 0 var(--gutter) clamp(2rem, 5vh, 3.5rem);
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1rem + 3.3vw, 4.8rem);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--on-ground);
  max-width: 22ch;
  text-wrap: balance;
}
.interlude__line small {
  display: block;
  margin-top: 0.8rem;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(242, 238, 230, 0.76);
}

/* ------------------------------------------------------------ pattern band
   His own monogram, recoloured into the palette, drifting sideways as the
   page scrolls. Texture from the subject's world, not decoration invented
   for it. Framer mapping: Ticker at near-zero speed, or Scroll Transform X. */

.patternband {
  overflow: clip;
  border-top: var(--rule) solid var(--hairline);
  border-bottom: var(--rule) solid var(--hairline);
}
.patternband--ground { background: var(--ground); border-color: var(--hairline-dark); }
.patternband__inner {
  height: clamp(72px, 10vw, 130px);
  width: 200%;
  background-image: url("../../assets/pattern-bone.png");
  background-size: auto 200%;
  background-position: center;
  background-repeat: repeat-x;
}
.patternband--ground .patternband__inner { background-image: url("../../assets/pattern-ground.png"); }
.no-motion .patternband__inner { transform: none !important; }

/* ---------------------------------------------------------------- showcase
   The three keynotes as a sticky horizontal shelf. The tall track drives a
   translateX across the panels, so scrolling down walks the shelf left.
   Framer mapping: the sticky + horizontal Scroll Transform pattern.
   On mobile and reduced motion it is a plain vertical stack. */

.showcase { background: var(--ground); }
/* Track height is set by JS to (viewport + shelf travel), so scroll distance
   equals shelf distance exactly. When the shelf already fits the viewport
   (very wide screens) .is-static renders it as a centred row instead of a
   dead pinned section. */
.showcase__track { position: relative; }
.showcase__stage {
  position: sticky; top: 0;
  /* ⚠ Capped, not a flat 100vh. The heading plus the card shelf come to about
     800px; in a 1000px viewport a full-height stage left 200px of empty navy
     around them, and on a tall screen far more. Jarryd read that as too much
     space between the shelf and the band below it. Capping the stage means the
     pin still works but the dead ground goes. */
  /* ⚠ 100vh, and the cap is gone.
     The cap was 54rem (864px), which fixed the ORIGINAL complaint (a flat
     100vh left dead ground around the content on a 1000px screen) by making
     the stage shorter than the window. That works until the window is tall:
     on a 1609px display the sticky stage was 923px and the remaining 686px of
     the pinned window was empty navy under the cards, which is what Jarryd
     photographed.

     The real fix for dead ground is to CENTRE the content in a full-height
     stage, not to shrink the stage. `justify-content: center` was already
     here doing exactly that; it just had nothing to centre within. Now the
     stage always fills the pin, and the content always sits in the middle of
     it, at every screen height. */
  min-height: 100vh;
  display: flex; flex-direction: column; justify-content: center;
  overflow: clip;
  gap: clamp(1.1rem, 2.6vh, 2rem);
  /* ⚠ The masthead is sticky at top 0 and so is this stage, so they occupy
     the same space while pinned: the heading sat at y=41 beneath a bar whose
     bottom edge is 105 and was cut in half. The pin has to start below it. */
  padding-block: calc(var(--masthead-h, 76px) + clamp(1rem, 2vh, 2rem))
                 clamp(1.5rem, 3vh, 2.75rem);
}
.showcase__head { padding: 0 var(--gutter); text-align: center; }
.showcase__head h2 { color: var(--on-ground); }
.showcase__row {
  display: flex;
  gap: clamp(1.5rem, 3vw, 3rem);
  padding: 0 var(--gutter);
  width: max-content;
  will-change: transform;
}
.panel {
  /* ⚠ Width is capped by viewport HEIGHT as well as width, and that second
     cap is what keeps the pinned stage inside the screen.

     A panel is a 3:2 image plus a fixed-height body, so its height follows
     from its width. At 1440x900 the panel came out 662 wide and therefore
     696 tall, which pushed the whole stage to 932px inside a 900px window;
     the stage then scrolled under the masthead at the end of the pin and cut
     the heading in half.

     The height term only bites on SHORT viewports. From about 1080px tall it
     exceeds the 700px cap and has no effect, so wide desktop displays are
     completely unchanged. The 300px floor keeps it sane on a phone, where the
     shelf is a vertical column anyway. */
  width: min(clamp(300px, 46vw, 700px), max(300px, calc(135vh - 620px)));
  background: var(--ground-2);
  color: var(--on-ground);
  display: grid;
  grid-template-rows: auto 1fr;
  text-decoration: none;
}
.panel__img { aspect-ratio: 3 / 2; overflow: clip; }
.panel__img img {
  width: 100%; height: 100%; object-fit: cover;
  /* Cropping a 2:3 portrait into a 3:2 frame with the default centred
     object-position keeps the middle band, which on a photograph of a person
     is the torso: it cut his head off entirely on the Diffability frame.
     Faces sit in the upper third, so that is the default, and --pos lets any
     individual image override it. */
  object-position: var(--pos, 50% 26%);
  transition: transform var(--duration-slow) var(--ease-out-expo);
}
.panel:hover .panel__img img { transform: scale(1.04); }
.panel__body { padding: clamp(1.25rem, 1rem + 1vw, 2rem); display: flex; flex-direction: column; gap: 0.6rem; }
.panel__body h3 { color: var(--on-ground); }
.panel__sub { font-family: var(--font-display); font-style: italic;
  font-size: clamp(1.05rem, 1rem + 0.4vw, 1.4rem); color: var(--accent-soft); }
.panel__aud { font-size: clamp(0.875rem, 0.8rem + 0.3vw, 1.0625rem); line-height: 1.55; color: rgba(242, 238, 230, 0.74); }
.panel__more {
  margin-top: auto; padding-top: 0.9rem;
  font-size: 0.875rem; font-weight: 500; color: var(--accent-soft);
}
.panel:hover .panel__more { text-decoration: underline; text-underline-offset: 0.2em; }

@media (max-width: 860px) {
  .showcase__track { height: auto !important; }
  .showcase__stage { position: static; min-height: 0; padding-block: var(--space-section); }
  .showcase__row { flex-direction: column; width: auto; transform: none !important; }
  /* The cards arrive rather than appear, matching the desktop shelf's entry.
     Resting state is fully visible, so a failed observer leaves a readable
     page rather than an empty one. */
  .showcase__row .panel {
    opacity: 0.001; transform: translateY(22px);
    transition: opacity 700ms var(--ease-out-expo),
                transform 700ms var(--ease-out-expo);
  }
  .showcase__row .panel.is-lit { opacity: 1; transform: none; }
  /* ⚠ Three ways this reveal can never run, and all three must leave the
     cards readable. Without them a reduced-motion visitor gets a column of
     blank rectangles, which is far worse than no animation.
       1. no JS at all          -> html:not(.js)
       2. reduced motion        -> .no-motion (initMobileMotion returns early)
       3. observer never fires  -> the failsafe in site.js adds is-lit */
  html:not(.js) .showcase__row .panel,
  .no-motion .showcase__row .panel { opacity: 1; transform: none; }
  .panel { width: auto; }
}
.no-motion .showcase__track, .showcase__track.is-static { height: auto !important; }
.no-motion .showcase__stage, .is-static .showcase__stage {
  position: static; min-height: 0; padding-block: var(--space-section);
}
.no-motion .showcase__row { flex-direction: column; width: auto; transform: none !important; }
.is-static .showcase__row {
  width: auto; transform: none !important;
  justify-content: center; flex-wrap: wrap;
}
.no-motion .panel { width: auto; }

/* ------------------------------------------------------------- photo strip
   Three frames, deliberately off-grid: varied top offsets so the row reads
   editorial rather than templated. */

.strip {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1rem, 2.4vw, 2.25rem);
  align-items: start;
}
.strip figure { margin: 0; }
.strip figure:nth-child(2) { margin-top: clamp(1.5rem, 4vw, 4rem); }
.strip figure:nth-child(3) { margin-top: clamp(0.75rem, 2vw, 2rem); }
.strip img { width: 100%; height: auto; }
@media (max-width: 660px) {
  .strip { grid-template-columns: 1fr; }
  .strip figure:nth-child(n) { margin-top: 0; }
}

/* -------------------------------------------------------------- megafooter
   The site closes the way it opens: his name, set enormous, this time
   reversed on the ground. */



/* --------------------------------------------------------------- plan steps
   The booking plan as a drawn line with numbered nodes. The rule fills as
   the section enters; the numbers sit on it like stations. */

.steps-wrap { position: relative; }
.steps__line {
  position: absolute;
  left: 0; right: 0;
  top: calc(clamp(2.8rem, 1.6rem + 3vw, 5.2rem) / 2);
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
}
.steps-wrap::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: calc(clamp(2.8rem, 1.6rem + 3vw, 5.2rem) / 2);
  height: var(--rule);
  background: var(--hairline);
}
.no-motion .steps__line { transform: none; }
.steps-wrap .step::before {
  background: var(--bone);
  color: var(--accent);
  position: relative;
  display: inline-grid;
  place-items: center;
  padding-inline: 0.35em;
}
.section--bone .steps-wrap .step::before { background: var(--bone); }

/* ---------------------------------------------------------------- journey
   Life as a scroll journey. Sticky stage: giant year left, chapter right.
   Chapters crossfade; the year swaps with a rise; a thin bar charts
   progress. Mobile and reduced motion get the stacked static story. */

.journey { background: var(--bone); }
/* Scroll runway. Sized per chapter: at 380vh across seven chapters each
   year got barely half a screen and the reel raced. */
.journey__track { position: relative; min-height: 640vh; }
.journey__stage {
  position: sticky; top: 0; min-height: 100vh;
  display: grid;
  /* years+chapters in one column, the photo reel in its own. */
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  align-items: center;
  gap: clamp(1.5rem, 4vw, 5rem);
  padding: 0 var(--gutter);
  max-width: var(--page-max);
  margin-inline: auto;
  overflow: clip;
}
.journey__rail {
  position: absolute; left: 0; top: 12vh; bottom: 12vh; width: 2px;
  background: var(--hairline);
}
.journey__bar {
  position: absolute; inset: 0;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: top;
}
.journey__year { position: relative; height: clamp(8rem, 16vw, 19rem); }
.journey__year span {
  position: absolute; inset: 0;
  font-family: var(--font-display);
  font-size: clamp(6rem, 14vw, 17rem);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--ink);
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 500ms var(--ease-out-expo), transform 500ms var(--ease-out-expo);
}
.journey__year span.is-active { opacity: 1; transform: none; }
.journey__year span em { font-style: italic; color: var(--accent); }
.journey__panels { position: relative; min-height: clamp(16rem, 30vh, 24rem); }
.journey__chapter {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; gap: 0.9rem; justify-content: center;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 500ms var(--ease-out-expo), transform 500ms var(--ease-out-expo);
  pointer-events: none;
}
.journey__chapter.is-active { opacity: 1; transform: none; pointer-events: auto; }
.journey__chapter h3 {
  font-size: var(--text-title);
  color: var(--ink);
}
.journey__chapter p { color: var(--ink-soft); max-width: 46ch; font-size: var(--text-body); }

/* Static story for reduced motion.
   ⚠ This used to swap in below 860px as well, hiding the pinned track
   outright, which is why the phone had a plain list of dates and a wrapped
   grid of Polaroids instead of the scroll. The static twin now serves ONLY
   reduced motion, where replacing the effect is the correct answer rather
   than a shortcut. The pinned version is rebuilt for one column further
   down this file. */
.journey__static { display: none; }
.no-motion .journey__track { display: none; }
.no-motion .journey__static { display: block; padding-block: var(--space-section); }
.journey__static article {
  padding-block: clamp(1.5rem, 4vw, 2.5rem);
  border-top: var(--rule) solid var(--hairline);
}
.journey__static time {
  font-family: var(--font-display);
  font-size: clamp(2rem, 5vw, 3rem);
  color: var(--accent-link);
  display: block;
}
.journey__static h3 { margin-top: 0.4rem; color: var(--ink); }
.journey__static p { margin-top: 0.5rem; color: var(--ink-soft); max-width: 52ch; }

/* -------------------------------------------------------------- film strip
   The About photos as one coherent full-bleed row that drifts sideways with
   the scroll, film-strip style, instead of three floating offsets. */

/* ------------------------------------------------------------- gallery band

   A strip of photographs that runs edge to edge, in perpetuity.

   Replaced a scroll-driven glide. Three complaints, all correct: it only
   moved while you were scrolling so it read as inert, the frames were mixed
   aspect ratios so the row was ragged along the bottom, and it stopped short
   of the screen edge, which makes a full-bleed band look like a component
   that failed to fit.

   Same machinery as the testimonial ticker: the set is emitted twice and the
   lane translates exactly -50%, so the seam is invisible and the loop never
   ends. */

.gal {
  overflow: clip;
  /* Feathered ends. A hard edge against the paper announces where the strip
     stops; a fade says it continues past the window, which is what a band of
     photography is meant to imply. */
  -webkit-mask-image: linear-gradient(to right,
    transparent 0, #000 5%, #000 95%, transparent 100%);
  mask-image: linear-gradient(to right,
    transparent 0, #000 5%, #000 95%, transparent 100%);
}
.gal__lane {
  display: flex; width: max-content;
  animation: gal-scroll var(--gal-speed, 68s) linear infinite;
  /* Nothing the pointer does may touch it. Same rule as every other marquee
     on this site: a strip that reacts to the cursor reads as broken. */
  pointer-events: none;
  animation-play-state: running !important;
  will-change: transform;
}
@keyframes gal-scroll { to { transform: translateX(-50%); } }

.gal__set { display: flex; }
.gal__cell {
  margin: 0;
  /* One height for every frame. The ragged bottom edge was the single thing
     that made this read as photographs pasted onto the page rather than as a
     designed band. */
  height: clamp(230px, 26vw, 400px);
  flex: none;
}
.gal__cell img {
  display: block; height: 100%; width: auto;
  object-fit: cover;
  background: var(--bone);
}

/* No gaps. A continuous ribbon of image reads as film; evenly spaced cards
   read as a carousel, which is the pattern this is deliberately not. The
   frames are separated by a hairline of the page ground instead. */
.gal__cell + .gal__cell { border-left: 2px solid var(--paper); }

@media (prefers-reduced-motion: reduce) {
  .gal { mask-image: none; -webkit-mask-image: none; overflow-x: auto; }
  .gal__lane { animation: none; }
  .gal__set:last-child { display: none; }
}

/* ---------------------------------------------------------------- articles
   Kutch's Kommunications, with his real writing in it. */

.article-list { display: flex; flex-direction: column; }
.article-row {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr) minmax(0, 1.1fr) 2.5rem;
  gap: clamp(1rem, 2.5vw, 3rem);
  align-items: baseline;
  padding-block: clamp(1.5rem, 1rem + 1.5vw, 2.5rem);
  border-top: var(--rule) solid var(--hairline);
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration) var(--ease-out-expo);
}
.article-list .article-row:last-child { border-bottom: var(--rule) solid var(--hairline); }
.article-row:hover { background: color-mix(in srgb, var(--bone) 55%, transparent); }
.article-row time { font-size: var(--text-micro); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-soft); }
.article-row h3 {
  font-size: clamp(1.3rem, 1rem + 1vw, 2rem);
  transition: color var(--duration) var(--ease-out-expo);
}
.article-row:hover h3 { color: var(--accent-link); }
.article-row .excerpt {
  font-size: var(--text-body); color: var(--ink-soft);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.article-row .arrow {
  justify-self: end; color: var(--accent-link);
  transition: transform var(--duration) var(--ease-out-expo);
}
.article-row:hover .arrow { transform: translateX(5px); }
@media (max-width: 860px) {
  .article-row { grid-template-columns: 1fr; gap: 0.4rem; }
  .article-row .arrow { display: none; }
}

/* Article page */
/* ⚠ .article deliberately has NO max-width. It used to cap at 46rem, from the
   version where the whole piece was one centred column. That cap survived the
   rebuild and silently strangled the new two-column layout: the grid inherited
   a 736px box, so the rail and the reading measure each ended up under 200px
   wide and every paragraph wrapped after four words. The measure is now capped
   on .article__measure, where it belongs, and the shells inside handle width. */
.article {
  padding: clamp(3rem, 6vw, 6rem) 0 var(--space-section);
}
.article time { font-size: var(--text-micro); letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--accent-link); }
.article h1 { margin-top: 0.75rem; font-size: clamp(2rem, 1rem + 3vw, 3.8rem); }
.article__hero { margin: clamp(1.5rem, 3vw, 2.5rem) 0; }
.article__hero img { width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.article .prose { margin-top: 1.5rem; font-size: var(--text-lead); line-height: 1.7; }
.article .prose p + p { margin-top: 1.35em; }
.article__back { margin-top: 3rem; }

/* --------------------------------------------------------------- megafooter
   redesigned: baseline finale, not a centred float. */

.megafooter {
  background: var(--ground);
  padding: clamp(3rem, 8vh, 6rem) 0 0;
  overflow: clip;
}
.megafooter__inner {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: 0 var(--gutter) clamp(2rem, 4vh, 3.5rem);
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  align-items: end;
  gap: clamp(1.5rem, 4vw, 5rem);
  border-bottom: var(--rule) solid var(--hairline-dark);
}
.megafooter__name {
  margin: 0;
  text-align: left;
  font-family: var(--font-display);
  font-size: clamp(3.4rem, 0.5rem + 9.5vw, 11.5rem);
  line-height: 0.9;
  letter-spacing: -0.03em;
  /* Outlined, not filled. Filling it with --ground-2 put navy on navy with a
     hairline round it, which read as a rendering fault rather than a device.
     A transparent fill with a real stroke is unambiguously deliberate. */
  /* Solid brass. The outline read as a ghost of the wordmark rather than the
     wordmark. */
  color: var(--accent);
  -webkit-text-stroke: 0;
  user-select: none;
}
.megafooter__name span { display: block; }
.megafooter__name strong { font-weight: 400; color: rgba(242, 238, 230, 0.92); -webkit-text-stroke: 0; }
.megafooter__cta {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 1.1rem;
  padding-bottom: clamp(0.5rem, 1.5vh, 1.25rem);
}
.megafooter__cta p {
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 1rem + 0.8vw, 1.8rem);
  line-height: 1.35;
  color: rgba(239, 229, 213, 0.8);
  max-width: 22ch;
}
@media (max-width: 860px) {
  .megafooter__inner { grid-template-columns: 1fr; align-items: start; }
}

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

/* The megafooter already carries its own generous top space, so a full
   section's padding on top of it left a dead band above the wordmark. */
.footer { background: var(--ground); color: var(--on-ground);
  padding-block: clamp(2rem, 1.5rem + 1.5vw, 3.25rem) 2rem; }
.footer__grid {
  display: grid; grid-template-columns: 1.4fr repeat(3, 1fr);
  gap: clamp(1.5rem, 3vw, 3rem);
}
@media (max-width: 860px) { .footer__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer__grid { grid-template-columns: 1fr; } }
/* ⚠ h2, NOT h3. These column labels were h3 and were promoted to h2 to close
   a heading-level skip on article pages (h1 title straight to h3 footer). The
   promotion silently handed them the h2 SIZE, and the footer suddenly had
   three enormous display headings in it.
   The level is semantic and the size is visual; they are set independently
   here so the outline stays valid without changing how the footer looks. */
.footer h2, .footer h3 {
  font-size: 0.8125rem; letter-spacing: 0.12em; text-transform: uppercase;
  font-family: var(--font-body); font-weight: 500;
  color: var(--accent-soft);        /* brass, as originally designed */
  margin-bottom: 0.85rem;
}
.footer ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.7rem; }
/* ⚠ `:not(.btn)` is load-bearing, do not simplify it away.
   `.footer a` scores 0,1,1 and beats the bare `.btn` at 0,1,0, so without the
   exclusion this rule made every footer button `display: inline-block`. A
   button that is not a flex container silently ignores `gap`, so the label
   sat hard against the brass token with zero space and the CTA read as
   squashed. The nudge-on-hover below is for nav links only; on a pill button
   it slides the whole control sideways. */
.footer a:not(.btn) {
  color: rgba(242, 238, 230, 0.82); text-decoration: none; font-size: 0.9375rem;
  line-height: 1.4; display: inline-block;
  transition: color var(--duration) var(--ease-out-expo),
              transform var(--duration) var(--ease-out-expo);
}
.footer a:not(.btn):hover, .footer a:not(.btn):focus-visible {
  color: var(--accent-soft); transform: translateX(3px);
}
/* The lead column is display type; the others are eyebrow-led lists. Without
   this the paragraph sits on the eyebrows' baseline and the row looks skewed. */
/* The base rule sat almost on top of the column headings. */
.footer__grid { padding-top: clamp(2rem, 1.5rem + 1.6vw, 3.25rem); }
.footer__grid > :first-child { padding-top: 0.15rem; }
.footer__grid > * + * { padding-top: 0.15rem; }
.footer__base {
  margin-top: clamp(2.5rem, 2rem + 2vw, 4rem); padding-top: 1.5rem;
  border-top: var(--rule) solid var(--hairline-dark);
  display: flex; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap;
  font-size: 0.8125rem; color: rgba(242, 238, 230, 0.70);
}
.footer__base a { font-size: 0.8125rem; color: rgba(242, 238, 230, 0.70); }
.footer__base { align-items: center; }
/* Read left to right: the wordmark follows the words, not the other way. */
.footer__by { flex-direction: row; }
.footer__by {
  display: inline-flex; align-items: center; gap: 0.7rem;
  text-decoration: none; opacity: 0.72;
  transition: opacity var(--duration) var(--ease-out-expo);
}
.footer__by:hover, .footer__by:focus-visible { opacity: 1; transform: none; }
/* One line, mark to the right of the words, never stacked. */
.footer__by { flex-wrap: nowrap; white-space: nowrap; flex-direction: row; align-items: center; }
.footer__by span { font-size: 0.8125rem; color: rgba(242, 238, 230, 0.70); }
/* The mark is a wordmark, so it needs enough height to actually be read. */
.footer__by img { display: block; height: 2.1rem; width: auto; flex: 0 0 auto; }

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

/* Scroll parallax. Values are Framer Scroll Speed percentages; the JS engine
   converts (speed - 100) into a translate offset. Never applied without JS. */
[data-parallax], [data-glide] { will-change: transform; }
.no-motion [data-glide] { transform: none !important; }
.no-motion [data-parallax] { transform: none !important; will-change: auto; }

/* Image reveal: the frame opens from the bottom as it enters.
   Framer mapping: Appear effect, mask reveal. */
.js-reveal .clip-reveal {
  clip-path: inset(14% 0 0 0);
  opacity: 0.001;
  transition: clip-path 900ms var(--ease-out-expo), opacity 900ms var(--ease-out-expo);
}
.js-reveal .clip-reveal.is-in { clip-path: inset(0 0 0 0); opacity: 1; }

/* Rise reveal. Additive only: content ships visible, the class arrives with
   the script, and the failsafe removes the whole layer after 1.2s. */
.js-reveal .reveal {
  opacity: 0; transform: translateY(14px);
  transition: opacity var(--duration-slow) var(--ease-out-expo),
              transform var(--duration-slow) var(--ease-out-expo);
}
.js-reveal .reveal.is-in { opacity: 1; transform: none; }

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

/* ----------------------------------------------------------------- consent */

/* Only rendered when a GA4 property actually exists, so the site never asks
   for consent to tracking that is not happening. Bottom sheet rather than a
   full-screen interstitial: it must not block the page it is asking about. */

.consent {
  position: fixed;
  left: var(--gutter);
  right: var(--gutter);
  bottom: clamp(1rem, 2vw, 1.75rem);
  z-index: 60;
  margin-inline: auto;
  max-width: 44rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem clamp(1rem, 3vw, 2.5rem);
  padding: clamp(1rem, 2vw, 1.4rem) clamp(1.1rem, 2.4vw, 1.75rem);
  background: var(--ground);
  color: var(--on-ground);
  border: var(--rule) solid var(--hairline-dark);
  box-shadow: 0 26px 60px -30px rgba(17, 29, 46, 0.7);
  animation: consent-in 460ms var(--ease-out-expo) both;
}
.consent p {
  margin: 0;
  flex: 1 1 20rem;
  font-size: 0.875rem;
  line-height: 1.55;
  color: rgba(242, 238, 230, 0.82);
}
.consent__actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.consent .btn { padding: 0.6rem 1.1rem; font-size: 0.8125rem; }
/* Decline must be as reachable as Accept. A ghost button on navy still needs a
   real border and full-strength label, otherwise the "no" is a dark pattern. */
.consent .btn--ghost {
  background: transparent;
  color: var(--on-ground);
  border: var(--rule) solid rgba(242, 238, 230, 0.45);
}
.consent .btn--ghost:hover { border-color: var(--on-ground); background: rgba(242, 238, 230, 0.08); }

@keyframes consent-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .consent { animation: none; }
}

/* Honeypot. A booking form is high intent and low volume, so a CAPTCHA on it
   costs real enquiries; a field only a bot fills costs nothing. Moved off
   screen rather than display:none, because some bots skip hidden inputs and
   the point is that they fill it in. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---------------------------------------------------------------- coverage */

/* A ledger, not a card grid. Equal cards are the most AI-looking pattern
   there is, and they flatten hierarchy, which defeats the purpose: the House
   of Representatives and a podcast must not carry the same visual weight.
   Closer to an index page in a book: numbered, ruled, outlet set large. */

.cov__strip {
  display: flex; flex-wrap: wrap;
  gap: 0.6rem clamp(1.25rem, 3vw, 2.75rem);
  margin-top: clamp(1.75rem, 3vw, 3rem);
  padding-top: clamp(1.25rem, 2vw, 2rem);
  border-top: var(--rule) solid var(--hairline);
  font-family: var(--font-display);
  font-size: clamp(0.9375rem, 0.85rem + 0.4vw, 1.25rem);
  color: var(--ink-soft);
}
.cov__strip span { white-space: nowrap; }
/* A hairline between names rather than a bullet or a dash glyph. */
.cov__strip span + span { padding-left: clamp(1.25rem, 3vw, 2.75rem); border-left: var(--rule) solid var(--hairline); }

.cov__list { list-style: none; margin: 0; padding: 0; border-top: 2px solid var(--ink); }

.cov { border-bottom: var(--rule) solid var(--hairline); }
.cov__link {
  display: grid;
  /* ⚠ The description column was an uncapped 1fr, so on a 2560px monitor it
     grew to about 120 characters a line. A measure is a reading constraint,
     not a leftover: it caps here and the row keeps its alignment because the
     final date column still tracks the right edge. */
  grid-template-columns: 3.5rem minmax(0, 17.5rem) minmax(0, 34rem) minmax(0, 10rem);
  gap: clamp(1rem, 2.5vw, 3rem);
  align-items: start;
  padding-block: clamp(1.6rem, 1.2rem + 1.6vw, 2.9rem);
  text-decoration: none; color: inherit;
  transition: background var(--duration) var(--ease-out-expo);
}
/* The whole row is the target, so the hover has to read across all of it. */
.cov__link:hover { background: color-mix(in srgb, var(--bone) 55%, transparent); }
.cov__link:focus-visible { outline: 2px solid var(--accent-link); outline-offset: 4px; }

.cov__no {
  font-family: var(--font-display); font-size: 0.9375rem;
  color: var(--accent-link); padding-top: 0.35rem;
  font-variant-numeric: tabular-nums;
}
.cov__mark { display: block; padding-top: 0.1rem; }
/* Where no clean single-colour logo exists, the outlet gets a designed
   typographic plate rather than a line of fallback text. Set small, wide and
   uppercase inside a ruled box, it reads as a mark in a row of marks instead
   of as a logo that failed to load, which is how the plain wordmark read.
   This is also the honest answer: substituting a near-miss logo to fill the
   gap would put the wrong brand next to a story. */
.cov__wordmark {
  display: inline-flex; align-items: center;
  min-height: 2.4rem; padding: 0.45rem 0.8rem;
  box-shadow: inset 0 0 0 1px var(--hairline);
  border-left: 2px solid var(--accent);
  font-family: var(--font-body);
  font-size: clamp(0.6875rem, 0.62rem + 0.22vw, 0.8125rem);
  font-weight: 500;
  letter-spacing: 0.13em; text-transform: uppercase;
  line-height: 1.25; color: var(--ink);
  text-wrap: balance;
}
/* Marks are pre-normalised by design/build_outlet_logos.py, including a 1.5x
   boost for round seals so a badge and a wordmark read as the same weight.
   The cap here is therefore generous: it is a safety rail, not the sizing. */
.cov__logo { display: block; width: auto; max-height: 3.2rem; max-width: 100%; }
/* A round seal needs a taller ceiling than a wordmark to read at the same
   weight; the shared cap was clipping it back down to wordmark height. */
.cov__logo[src*="house"] { max-height: 4.6rem; }

.cov__body { display: block; }
.cov__kind {
  display: block; font-size: 0.75rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--accent-link); margin-bottom: 0.5rem;
}
.cov__head {
  display: block; font-size: clamp(1.0625rem, 1rem + 0.35vw, 1.3125rem);
  line-height: 1.35; color: var(--ink); text-wrap: pretty;
}
.cov__note {
  display: block; margin-top: 0.6rem; max-width: 54ch;
  font-size: 0.9375rem; line-height: 1.6; color: var(--ink-soft);
}

.cov__meta { display: block; text-align: right; padding-top: 0.2rem; }
.cov__meta time { display: block; font-size: 0.875rem; color: var(--ink-soft); }
.cov__go {
  display: inline-block; margin-top: 0.7rem;
  font-size: 0.875rem; color: var(--accent-link);
  opacity: 0; transform: translateX(-6px);
  transition: opacity var(--duration) var(--ease-out-expo),
              transform var(--duration) var(--ease-out-expo);
}
.cov__link:hover .cov__go, .cov__link:focus-visible .cov__go { opacity: 1; transform: none; }
/* Touch and reduced motion never get the hover, so the affordance must show. */
@media (hover: none) { .cov__go { opacity: 1; transform: none; } }
.no-motion .cov__go { opacity: 1; transform: none; }

.cov__foot {
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
  font-size: 0.8125rem; color: var(--ink-soft); max-width: 60ch;
}

/* The first entry is the strongest credential on the page, so it gets the
   editorial lead treatment rather than sitting in the queue. */
/* The lead entry's plate is a shade larger, so the ledger still opens with
   a heavier note without the plates drifting out of family. */
.cov:first-child .cov__wordmark {
  font-size: clamp(0.75rem, 0.68rem + 0.26vw, 0.9375rem);
  padding: 0.6rem 1rem;
}
.cov:first-child .cov__head { font-size: clamp(1.1875rem, 1rem + 0.7vw, 1.625rem); }

@media (max-width: 1100px) {
  .cov__link { grid-template-columns: 2.5rem minmax(0, 12rem) minmax(0, 1fr); }
  .cov__meta { grid-column: 2 / -1; text-align: left; padding-top: 0.9rem; }
  .cov__meta time { display: inline; }
  .cov__go { margin-top: 0; margin-left: 1rem; }
}
@media (max-width: 700px) {
  .cov__link { grid-template-columns: 2rem minmax(0, 1fr); gap: 0.75rem 1rem; }
  .cov__body { grid-column: 2; }
  .cov__meta { grid-column: 2; }
}

/* The intro block and the ledger are one idea, so their section paddings
   should not stack into a gap the reader reads as a mistake. */
.section--pull { padding-top: 0; }

/* ------------------------------------------------- journey polaroid reel */

/* Family photographs that travel upward through the years beside the
   journey. The frame, the shadow and the tilt are all CSS so nothing has to
   be re-exported to retune them, and the image assets stay reusable.

   The stage was two columns (years, chapters). It is now three, with the
   copy paired into one column so the reel gets a rail of its own. */

.journey__copy { display: block; }
/* The reel is 3,000px of photographs. Left in normal flow it stretched the
   sticky stage to its own height, and a sticky element as tall as its track
   barely sticks: the whole section scrolled past instead of pinning. The
   viewport is a fixed-height window in the grid; the strip inside it is taken
   out of flow so it can be any length without touching the stage. */
.journey__viewport {
  position: relative;
  height: 100vh;
  overflow: clip;
  /* ⚠ A hard clip cut the last print in half at the foot of the window, which
     is what Jarryd was seeing as "cut off at the bottom". Feathering both ends
     means a frame leaving the window fades rather than being guillotined.
     Kept narrow (7%) so it reads as depth, not as a vignette. */
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 7%, #000 93%, transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 7%, #000 93%, transparent 100%);
}
.journey__reel {
  position: absolute;
  top: 0; left: 0; right: 0;
  display: flex; flex-direction: column;
  align-items: center;
  gap: clamp(1.4rem, 2.4vw, 2.5rem);
  /* No end padding needed: the script centres frames from their measured
     positions, so it does not care where the strip starts. */
  will-change: transform;
}

.pola {
  --tilt: 0deg;
  --scale: 1;
  position: relative;
  margin: 0;
  /* Sizes up 35% on Jarryd's note, and each frame carries its own --scale so
     the strip has a few larger and a few smaller prints in it. A stack of
     identically sized rectangles is what makes a set of photographs read as a
     gallery widget rather than as a shoebox someone actually kept. */
  width: calc(clamp(290px, 26.3vw, 466px) * var(--scale));
  flex: 0 0 auto;

  /* A real Polaroid print is warm, not white, and the emulsion pack gives it
     a faint gradient rather than a flat field. */
  background:
    linear-gradient(168deg, #FDFBF4 0%, #F7F1E3 62%, #F2EBDA 100%);
  /* Even on three sides, deep at the foot. That single asymmetry is most of
     what makes the shape recognisable. */
  padding: 4.6% 4.6% 0;
  border-radius: 3px;

  /* Two shadows: a tight contact shadow so it sits ON something, and a wide
     soft one so it lifts. One shadow alone always reads as a CSS card. */
  box-shadow:
    0 1px 2px rgba(38, 30, 18, 0.20),
    0 22px 42px -16px rgba(38, 30, 18, 0.45);
  transform: rotate(var(--tilt));
  opacity: 0.52;
  filter: saturate(0.86);
  transition: opacity 620ms var(--ease-out-expo),
              filter 620ms var(--ease-out-expo);
}

/* The print's own edge. A hairline of slightly darker card stops the frame
   dissolving into a light background. */
.pola::after {
  content: ""; position: absolute; inset: 0; border-radius: 3px;
  box-shadow: inset 0 0 0 1px rgba(38, 30, 18, 0.10);
  pointer-events: none;
}

/* Alternating tilt and scale, with a couple of odd ones so neither scans as a
   repeating pattern. Uniformity is what makes a stack look generated. */
.pola:nth-child(odd)   { --tilt: -1.7deg; }
.pola:nth-child(even)  { --tilt:  2.1deg; }
.pola:nth-child(3n)    { --tilt:  1.1deg; }
.pola:nth-child(5n)    { --tilt: -2.6deg; }
.pola:nth-child(4n)    { --scale: 1.12; }
.pola:nth-child(7n)    { --scale: 0.88; }
.pola:nth-child(5n+2)  { --scale: 0.94; }

.pola__print { position: relative; display: block; }
.pola img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 1 / 1; object-fit: cover;
  background: var(--bone);
}
/* Chemical vignette. Every real print of this era is darker and slightly
   warmer at the corners than in the middle. */
.pola__print::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(120% 100% at 50% 45%,
      rgba(0, 0, 0, 0) 52%, rgba(60, 42, 20, 0.16) 100%);
  mix-blend-mode: multiply;
}
.pola figcaption {
  /* The deep bottom lip is where a Polaroid gets written on, so the caption
     is set in the hand the rest of the site uses for display type. */
  padding: 0.55rem 0.35rem 1.5rem;
  font-family: var(--font-display);
  font-size: calc(0.9375rem * var(--scale));
  line-height: 1.2;
  color: #4A4238;
  text-align: center;
}
.pola.is-lit { opacity: 1; filter: none; }


.no-motion .pola, .journey__static ~ * .pola { opacity: 1; filter: none; }

@media (max-width: 1180px) {
  .pola { width: calc(clamp(200px, 20vw, 300px) * var(--scale)); }
}
/* Below the journey's own breakpoint the sticky scroll is off entirely, so
   the reel becomes a plain wrapped set under the static chapter list. */
/* ⚠ The journey KEEPS its pinned scroll on a handset.
   It used to be flattened here into a static wrapped grid, which threw away
   the one effect the section exists for: the year counting up, 1978 to today,
   while the Polaroid for that year rides into the middle of the screen.
   Jarryd called it the most important scroll effect on the site, and it is
   the only place the biography is told rather than listed.

   The MECHANIC is kept and the LAYOUT is rebuilt for one column: the year and
   its chapter sit above, the reel runs beneath them, and the whole stage is
   still sticky against a 640vh runway. Nothing here is horizontal, so none of
   the touch-scroll objections to the other pinned section apply. */
@media (max-width: 860px) {
  .journey__stage {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0, 1fr);
    align-content: center;
    gap: clamp(0.75rem, 3vw, 1.5rem);
    padding-block: clamp(1rem, 4vh, 2rem);
  }
  /* The year is the headline of the section, so it stays large, but not so
     large that it and the photograph cannot share one screen. */
  .journey__year { height: clamp(4.5rem, 19vw, 7rem); }
  .journey__year span { font-size: clamp(3.4rem, 17vw, 6rem); }

  /* ⚠ The reel is `position: absolute`, so the viewport has NO in-flow content
     and `height: auto` collapses it to zero, taking the reel off screen with
     it. It has to be given a real box: the remaining grid row, with a floor so
     a short handset still shows a whole Polaroid. */
  .journey__viewport { height: 100%; min-height: 46vh; }
  .pola { width: calc(clamp(150px, 40vw, 230px) * var(--scale)); }
}

/* The reel's plain-grid twin, shown wherever the pinned scroll is not.
   Same Polaroid frames, no motion, nothing dimmed.
   ⚠ Hidden by default now. It is the reduced-motion twin only; leaving it
   visible on a handset would put nine Polaroids on the page a second time,
   directly under the reel that is already showing them. */
.journey__grid {
  /* ⚠ none, not flex. This is the reduced-motion twin only. */
  display: none;
  flex-wrap: wrap; justify-content: center;
  gap: clamp(1rem, 2.5vw, 2rem);
  margin-top: clamp(2.5rem, 5vw, 4.5rem);
}
.no-motion .journey__grid { display: flex; }
.journey__grid .pola { opacity: 1; filter: none;
  width: calc(clamp(190px, 28vw, 310px) * var(--scale)); }


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

/* The intro was a headline and a paragraph in the left half with nothing at
   all in the right, which at 1920 is half a screen of empty paper. */
.topichero { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(1.5rem, 4vw, 5rem); align-items: center; }
.topichero__media { position: relative; margin: 0; }
.topichero__media img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 4 / 5; object-fit: cover; object-position: 50% 18%;
}
.pagehead:has(.topichero) { border-bottom: 0; }

/* ⚠ Do not reintroduce a rule, tick or bar near this image. A 2px brass line
   used to hang off the picture's left edge as an anchoring device. It was
   reported as a bug three separate times, because at every width it reads as
   a stray line across the photograph rather than as a deliberate mark. */

/* The hero column was heading, lead and button stacked with no rhythm, which
   is what made it read as compact. Each step down the hierarchy now gets air
   proportional to what it introduces. */
.topichero > div > h1 { margin: 0; }
.topichero > div > .lead { margin: clamp(1.25rem, 1rem + 1vw, 2rem) 0 0; }
.topichero > div > div { margin-top: clamp(1.75rem, 1.25rem + 1.6vw, 2.75rem); }
@media (max-width: 860px) {
  .topichero { grid-template-columns: 1fr; }
  .topichero__media { order: -1; }
  .topichero__media img { aspect-ratio: 3 / 2; }
}

/* A dark section that is the last thing on the page runs straight into the
   footer, which is the same navy. Two section paddings then stack inside what
   reads as a single block, which is what made those closes feel like a slab
   of blue with the content floating in it. Collapse the seam deliberately. */
main > .section--ground:last-child,
main > .section--ground.section--flush:last-child {
  padding-bottom: 0;
}
main > .section--ground:last-child + * .megafooter,
.footer .megafooter { padding-top: clamp(2.5rem, 2rem + 2vw, 4.5rem); }

/* Publication marks arrive in their own brand colours: Forbes black, CNN red,
   Paramount blue. Dropped in raw they read as a link farm rather than a
   credibility wall, which is the whole reason the page exists. Rendering them
   in ink puts them inside the design system, the same treatment the client
   logo wall already gets. */
.cov__logo {
  max-width: 100%; max-height: 3.2rem; height: auto;
  /* ⚠ Desaturate only. NEVER brightness(0).
     brightness(0) flattens a mark to a solid silhouette, which reads as the
     lettering on a wordmark but turns a round seal into a black disc: the
     House Appropriations seal rendered as a plain black dot. Greyscale keeps
     the internal drawing, which is the whole point of a seal. */
  filter: grayscale(1) contrast(1.05);
  opacity: 0.78;
  transition: opacity var(--duration) var(--ease-out-expo),
              filter var(--duration) var(--ease-out-expo);
}
/* Colour returns on hover, so the reader can confirm the mark. */
.cov__link:hover .cov__logo { filter: none; }
.cov__link:hover .cov__logo { opacity: 1; }
/* Marks and wordmarks share a row, so they need a common optical baseline. */
.cov__mark { display: flex; align-items: center; min-height: 2.6rem; }

/* The coverage strip: same marquee machinery as the quote ticker, set as
   wordmarks rather than sentences. */
.covstrip { border-top: 0; background: var(--bone); }
.covstrip__lane { --lane-gap: clamp(2.5rem, 5vw, 5rem); animation-duration: 70s; }
.covstrip__item {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 0.9rem + 0.6vw, 1.6rem);
  color: var(--ink); opacity: 0.75;
}
/* A brass dot between names reads as a separator without using a dash glyph. */
.covstrip__item::after {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent); margin-left: clamp(2.5rem, 5vw, 5rem);
  display: inline-block; vertical-align: middle;
}


/* The boilerplate copy control was the only button on the site with no icon,
   which is why it read as unstyled next to everything else. It takes a copy
   glyph rather than the arrow, because it does not navigate anywhere, and the
   glyph swaps to a tick on success. */
.btn__icon { position: relative; z-index: 1; display: inline-flex; align-items: center; }
.btn--copy .btn__icon svg { transition: transform var(--duration) var(--ease-out-expo); }
.btn--copy:hover .btn__icon svg { transform: translateY(-1px); }
/* Ghost on a bone ground needs a real border, not a hairline that disappears. */
.section--bone .btn--ghost { box-shadow: inset 0 0 0 1px rgba(17, 29, 46, 0.28); }
.section--bone .btn--ghost:hover { box-shadow: inset 0 0 0 1px var(--ink); }

/* Press: media enquiries.
   Was a run of <br>-separated lines with inline colours inside a navy section
   that ran straight into the navy footer, which is what made it read as blue
   on blue with a lot of nothing at the end. Now a defined card with the
   agency's own mark closing it. */
.enquiries {
  position: relative;
  background: var(--ground-2);
  padding: clamp(1.5rem, 1.2rem + 1.4vw, 2.5rem);
  border-left: 2px solid var(--accent);
  overflow: hidden;
  /* It is a business card, so it behaves like one: it has a face that catches
     the light and it tips toward whoever is looking at it. --rx/--ry are
     written by initEnquiryCard() and both default to zero, so with no JS this
     is simply a card. */
  transform:
    perspective(1100px)
    rotateX(calc(var(--rx, 0deg) + var(--enter-rx, 0deg)))
    rotateY(var(--ry, 0deg))
    translateY(var(--enter-y, 0px));
  transform-style: preserve-3d;
  box-shadow: 0 18px 40px -24px rgba(0, 0, 0, 0.6);
  transition: box-shadow 600ms var(--ease-out-expo);
  will-change: transform;
}
.enquiries:hover { box-shadow: 0 34px 64px -28px rgba(0, 0, 0, 0.72); }

/* The sheen. A narrow band of light that tracks the pointer across the face,
   which is the thing that actually sells a card as a physical object. Sits
   above the ground and below the type, and is inert to the mouse. */
.enquiries::before {
  content: ""; position: absolute; inset: -1px; pointer-events: none;
  background: radial-gradient(
    38rem 28rem at var(--mx, 50%) var(--my, 0%),
    rgba(242, 238, 230, 0.16) 0%,
    rgba(242, 238, 230, 0.05) 32%,
    transparent 62%);
  opacity: 0;
  transition: opacity 480ms var(--ease-out-expo);
}
.enquiries:hover::before, .enquiries:focus-within::before { opacity: 1; }

/* A brass edge that brightens with the tilt, so the card has a lit side. */
.enquiries::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(184, 146, 90, calc(0.10 + var(--lit, 0) * 0.34));
  transition: box-shadow 480ms var(--ease-out-expo);
}
.enquiries > * { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .enquiries { transform: none; transition: none; }
  .enquiries::before { display: none; }
}
.enquiries__label {
  margin: 0; font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--accent-soft);
}
.enquiries__who {
  margin: 0.7rem 0 0; font-family: var(--font-display);
  font-size: clamp(1.15rem, 1rem + 0.6vw, 1.6rem); line-height: 1.25;
  color: var(--on-ground);
}
.enquiries__who span {
  display: block; margin-top: 0.35rem; font-family: var(--font-body);
  font-size: 0.875rem; color: rgba(242, 238, 230, 0.72);
}
.enquiries__links { list-style: none; margin: 1.1rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.45rem; }
.enquiries__links a { color: var(--accent-soft); text-decoration: none; font-size: 0.9375rem; }
.enquiries__links a:hover { color: var(--on-ground); }
.enquiries__ta {
  display: block; margin-top: clamp(1.4rem, 2.5vw, 2.2rem);
  padding-top: clamp(1.1rem, 2vw, 1.6rem);
  border-top: var(--rule) solid var(--hairline-dark);
  opacity: 0.8; transition: opacity var(--duration) var(--ease-out-expo);
}
.enquiries__ta:hover { opacity: 1; }
.enquiries__ta img { display: block; width: clamp(140px, 13vw, 190px); height: auto; }

/* Word reveal, ported from 21st.dev "Blur Out Up". Only armed once the script
   has split the text, so a no-JS page renders the sentence normally. */
.words-armed .word {
  display: inline-block; white-space: pre;
  opacity: 0; filter: blur(6px); transform: translateY(10px);
  transition: opacity 860ms cubic-bezier(0.22, 1, 0.36, 1),
              filter 860ms cubic-bezier(0.22, 1, 0.36, 1),
              transform 860ms cubic-bezier(0.22, 1, 0.36, 1);
}
.words-in .word { opacity: 1; filter: blur(0); transform: none; }
@media (prefers-reduced-motion: reduce) {
  .words-armed .word { opacity: 1; filter: none; transform: none; }
}

/* -------------------------------------------------------------------- plan */

/* The 1-2-3. Was three identical bordered boxes in a row, which is the
   template pattern this project is explicitly trying to avoid, and it gave a
   planner no sense of sequence. Now a brass rule draws across the three as
   you scroll and each card lifts as the line reaches it, so the steps read as
   a process rather than a list. */

.plan { background: var(--bone); position: relative; overflow: clip; }
.plan__head { max-width: 44ch; }
.plan__eyebrow {
  margin: 0 0 0.9rem; font-size: 0.75rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--accent-link);
}
.plan__head h2 { margin: 0; }
.plan__lead {
  margin-top: 1rem; color: var(--ink-soft);
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.1875rem);
}

.plan__steps {
  list-style: none; margin: clamp(3rem, 2rem + 3vw, 5.5rem) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vw, 3rem);
  position: relative;
}
/* The rail sits on the numerals' optical centre. */
.plan__rail {
  position: absolute; left: 0; right: 0; top: 1.55rem; height: 2px;
  background: rgba(17, 29, 46, 0.12);
}
.plan__rail-fill {
  position: absolute; inset: 0; background: var(--accent);
  transform: scaleX(0); transform-origin: left;
}
.no-motion .plan__rail-fill { transform: scaleX(1); }

.stepcard {
  position: relative; padding-top: 3.6rem;
  opacity: 0.35;
  transform: translateY(10px);
  transition: opacity 1100ms var(--ease-out-expo),
              transform 1100ms var(--ease-out-expo);
}
.stepcard.is-on { opacity: 1; transform: none; }
.no-motion .stepcard { opacity: 1; transform: none; }

.stepcard__no {
  position: absolute; top: 0; left: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 3.1rem; height: 3.1rem; border-radius: 50%;
  background: var(--bone);
  box-shadow: inset 0 0 0 2px rgba(17, 29, 46, 0.14);
  font-family: var(--font-display); font-size: 1.05rem; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  transition: background 900ms var(--ease-out-expo),
              color 900ms var(--ease-out-expo),
              box-shadow 900ms var(--ease-out-expo);
}
.stepcard.is-on .stepcard__no {
  background: var(--accent); color: var(--on-accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}
.stepcard__meta {
  display: block; font-size: 0.75rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--accent-link); margin-bottom: 0.55rem;
}
.stepcard h3 {
  margin: 0; font-size: clamp(1.25rem, 1.05rem + 0.8vw, 1.85rem);
  color: var(--ink); line-height: 1.2;
}
.stepcard p {
  margin: 0.7rem 0 0; color: var(--ink-soft);
  font-size: clamp(0.9375rem, 0.9rem + 0.25vw, 1.0625rem);
  line-height: 1.6; max-width: 34ch;
}
.stepcard__bar {
  display: block; margin-top: 1.4rem; height: 2px; width: 0;
  background: var(--accent);
  transition: width 1200ms var(--ease-out-expo);
}
.stepcard.is-on .stepcard__bar { width: clamp(2.5rem, 5vw, 4.5rem); }

.plan__cta { margin-top: clamp(2.5rem, 2rem + 2vw, 4rem); }

@media (max-width: 860px) {
  .plan__steps { grid-template-columns: 1fr; gap: 2.5rem; }
  /* Vertical rail on a stacked layout. */
  .plan__rail { top: 0; bottom: 0; left: 1.55rem; right: auto; width: 2px; height: auto; }
  .plan__rail-fill { transform-origin: top; }
  .no-motion .plan__rail-fill { transform: scaleY(1); }
  .stepcard { padding-top: 0; padding-left: 4.6rem; min-height: 3.1rem; }
}

/* A filled box while a strip image decodes, so nothing shows through the gap. */
.filmstrip__row img { background: var(--bone); }

/* ------------------------------------------------- stakes figure, anchored */

/* The cutout sat on flat navy with a soft fade at its feet and nothing behind
   it, so it read as pasted onto the page rather than standing in it. Two
   things fix that, and they are the same two that make any cutout sit: a
   surface behind it to overlap, and a shadow under it to stand on. */

.stakes { position: relative; }

/* A tonal panel the figure overlaps. Overlap is what creates the depth cue;
   without it he is a sticker. */
/* The tonal panel is gone. Its masked edge rendered as a pale vertical line
   beside the figure, which read as a stray shadow. The contact shadow alone
   grounds him without introducing an edge. */
.stakes::before {
  content: none;
  position: absolute;
  right: 0;
  bottom: 0;
  width: clamp(16rem, 34%, 30rem);
  height: 62%;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--ground-2) 0%, transparent),
    var(--ground-2) 78%);
  /* Fade the left edge too, so the panel does not draw a hard vertical line
     down the middle of the section. */
  mask-image: linear-gradient(to right, transparent, #000 22%);
  pointer-events: none;
}
.stakes__copy, .stakes__cutout { position: relative; z-index: 1; }

/* The contact shadow. Elliptical and tight to the base, so he reads as
   standing on the panel rather than hovering over it. */
.stakes__cutout::after { content: none; }
.stakes__shadow {
  position: absolute;
  bottom: 0;
  /* Anchored to the figure's own column, not the section's centre. The grid is
     1.5fr copy / 1fr figure, so the shadow lives in the right-hand third. */
  right: 0;
  left: auto;
  width: 40%;
  height: clamp(2rem, 4vw, 3.25rem);
  transform: none;
  background: radial-gradient(50% 50% at 50% 50%,
    rgba(6, 12, 22, 0.55), rgba(6, 12, 22, 0) 70%);
  pointer-events: none;
  z-index: 0;
}

@media (max-width: 860px) {
  .stakes::before { width: 70%; height: 46%; }
}

/* Logos in the scrolling bar sit optically with the wordmarks beside them. */
.covstrip__item--mark { display: inline-flex; align-items: center; }
.covstrip__item--mark img {
  height: clamp(1.15rem, 1vw + 0.75rem, 1.75rem); width: auto; display: block;
  /* Same reason as .cov__logo: greyscale, never brightness(0). */
  filter: grayscale(1) contrast(1.05);
  opacity: 0.72;
}

/* -------------------------------------------------------------------- legal */

/* Long-form policy text. Measure capped for reading, headings given real
   separation, so it does not read as a wall of small print. */
.legal { max-width: 68ch; }
/* h3 exists now, and the h2 rhythm needed real air: the sections ran into
   each other and the page read as one undifferentiated block of small print. */
.legal h3 {
  margin: clamp(1.75rem, 2.5vw, 2.25rem) 0 0.7rem;
  font-family: var(--font-body);
  font-size: clamp(0.9375rem, 0.9rem + 0.2vw, 1.0625rem);
  font-weight: 600; letter-spacing: 0.01em; color: var(--ink);
}
.legal__intro {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 0.95rem + 0.5vw, 1.35rem);
  line-height: 1.5; color: var(--ink);
  padding-bottom: clamp(1.5rem, 3vw, 2.5rem);
  margin-bottom: clamp(0.5rem, 1vw, 1rem);
  border-bottom: var(--rule) solid var(--hairline);
  max-width: 56ch;
}
.legal h2 {
  padding-top: clamp(2rem, 3vw, 3rem);
  border-top: var(--rule) solid var(--hairline);
  margin: clamp(2.5rem, 4vw, 3.75rem) 0 0.9rem;
  font-size: clamp(1.15rem, 1rem + 0.6vw, 1.55rem);
  color: var(--ink);
}
/* ⚠ The intro closes with a border-bottom and every h2 opens with a
   border-top, so the FIRST h2 drew a second rule a few pixels under the
   first. Two parallel hairlines with a gap between them read as a mistake.
   The intro's rule is the one that belongs there, so the first h2 drops its
   own. */
.legal h2:first-of-type {
  margin-top: clamp(1.5rem, 2.5vw, 2.25rem);
  border-top: 0;
  padding-top: 0;
}
.legal li { padding-left: 0.15rem; }
.legal ul { margin: 0 0 1.25rem; padding-left: 1.35rem; }
.legal li::marker { color: var(--accent); }
.legal p, .legal li {
  color: var(--ink-soft); line-height: 1.7;
  font-size: clamp(0.9375rem, 0.9rem + 0.25vw, 1.0625rem);
}
.legal p { margin: 0 0 1rem; }
.legal ul { margin: 0 0 1rem; padding-left: 1.15rem; }
.legal li { margin-bottom: 0.6rem; }
.legal li strong { color: var(--ink); }
.legal a { color: var(--accent-link); text-underline-offset: 3px; }
.legal__stamp {
  margin-top: 1.25rem; font-size: 0.8125rem;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft);
}
/* Open decisions, deliberately visible rather than buried in a comment. These
   must be resolved before launch, not guessed. */
.legal__todo {
  border-left: 2px solid var(--accent);
  padding: 0.9rem 0 0.9rem 1.1rem;
  background: color-mix(in srgb, var(--bone) 55%, transparent);
  color: var(--ink);
}

.footer__legal a {
  margin-left: 1rem; color: rgba(242, 238, 230, 0.70);
  text-decoration: underline; text-underline-offset: 3px;
}
.footer__legal a:hover { color: var(--accent-soft); }

/* --------------------------------------------------------------- casefiles */

/* Was three alternating text-and-pull-quote pairs that flipped sides, which
   read as six competing columns with no order. Now each study is a single
   record with a clear reading path: index, outcome, who, story, evidence. */

.casefiles__head { max-width: 46ch; }
.casefiles__head h2 { margin: 0; }
.casefiles__list {
  margin-top: clamp(2.5rem, 2rem + 2.5vw, 4.5rem);
  border-top: 2px solid var(--ink);
}

.casefile {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(1.25rem, 3vw, 4rem);
  padding-block: clamp(2rem, 1.5rem + 2vw, 3.5rem);
  border-bottom: var(--rule) solid var(--hairline);
}
.casefile__head { display: flex; gap: clamp(1rem, 2vw, 1.75rem); align-items: start; }
.casefile__no {
  /* ⚠ Same fix as .kn__no: brass as TEXT needs --accent-link. At 2.27:1 this
     was the lowest-contrast type on the site. */
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.5rem);
  line-height: 1; color: var(--accent-link);
  font-variant-numeric: tabular-nums;
  padding-top: 0.1rem;
}
.casefile h3 {
  margin: 0; max-width: 20ch; line-height: 1.18;
  font-size: clamp(1.2rem, 1rem + 0.9vw, 1.9rem); color: var(--ink);
}
.casefile__who {
  margin: 0.6rem 0 0; font-size: 0.8125rem;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft);
}
.casefile__body p {
  margin: 0; color: var(--ink-soft); line-height: 1.7;
  font-size: clamp(0.9375rem, 0.9rem + 0.3vw, 1.0625rem);
}
/* The quote is evidence, so it closes the record rather than competing with
   the story for the reader's eye. */
.casefile__quote {
  margin: clamp(1.25rem, 2vw, 1.75rem) 0 0;
  padding-left: clamp(1rem, 1.6vw, 1.5rem);
  border-left: 2px solid var(--accent);
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(1.05rem, 0.95rem + 0.55vw, 1.45rem);
  line-height: 1.4; color: var(--ink);
}
/* Each record slides in a beat after the one above it. */
.js-reveal .casefile { transition-delay: calc(var(--i) * 90ms); }

@media (max-width: 860px) {
  .casefile { grid-template-columns: 1fr; gap: 1.25rem; }
}

/* The step you are standing on. "is-on" accumulates as the rail passes; this
   marks the one at the centre of the viewport, so the third lights when you
   have actually scrolled to it. */
.stepcard.is-current .stepcard__no {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: inset 0 0 0 2px var(--accent),
              0 0 0 6px color-mix(in srgb, var(--accent) 22%, transparent);
}
.stepcard.is-current h3 { color: var(--ink); }
.stepcard.is-current .stepcard__meta { color: var(--accent); }
.stepcard.is-current .stepcard__bar { width: clamp(3.5rem, 7vw, 6rem); }

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

/* Reworked against the Bookland reference: nearly face on, a deep page block
   on the left, only enough rotation to read as an object. The previous pose
   turned it far enough to become a study in perspective rather than a cover
   you could actually read. */
/* Thicker again now the inline override is gone. Bookland runs ~21% of
   width; a memoir reads heavier than that, so this sits nearer 28%. */
.book3d__obj { --thick: 104px; }

/* --------------------------------------------------------------- the close */

/* Bone, not navy. A dark section immediately above the dark footer merges
   into one slab however carefully the seam is handled, and the cutout that
   used to sit here never stopped looking pasted on. A real photograph in a
   frame solves both: it occupies space on its own terms, and the lighter
   ground gives the footer an edge to start from. */
.closer { background: var(--bone); }
.closer__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(1.75rem, 5vw, 5.5rem);
  align-items: center;
}
.closer__media { margin: 0; position: relative; }
.closer__media img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 4 / 5; object-fit: cover; object-position: 50% 18%;
}
/* ⚠ The brass rule that used to hang off this picture is GONE, and must not
   come back. It was the same device as the topics hero, and it was reported
   as a stray line across Michael's photograph on both pages. A 2px bar that
   overlaps a portrait reads as a rendering fault, not as a mark, no matter
   how deliberate it is in the stylesheet. */
.closer__copy h2 { margin: 0; max-width: 20ch; }
.closer__lead {
  margin-top: 1.25rem; max-width: 40ch;
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 1rem + 0.75vw, 1.7rem);
  line-height: 1.35; color: var(--ink-soft);
}
@media (max-width: 860px) {
  .closer__grid { grid-template-columns: 1fr; }
  .closer__media img { aspect-ratio: 3 / 2; }
}

/* ------------------------------------------------------------- the journal */



/* The archive, as a numbered index rather than a stack of equal rows. */
.idx__list { margin-top: clamp(2rem, 3vw, 3.5rem); border-top: 2px solid var(--ink); }
.idx {
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr) 9rem 2rem;
  gap: clamp(1rem, 2.5vw, 2.5rem);
  align-items: baseline;
  padding-block: clamp(1.4rem, 1.1rem + 1.2vw, 2.1rem);
  border-bottom: var(--rule) solid var(--hairline);
  text-decoration: none; color: inherit;
  transition: background var(--duration) var(--ease-out-expo);
}
.idx:hover { background: color-mix(in srgb, var(--bone) 50%, transparent); }
.idx__no {
  font-family: var(--font-display); font-size: 0.9375rem;
  color: var(--accent-link); font-variant-numeric: tabular-nums;
}
.idx__title {
  display: block; font-family: var(--font-display);
  font-size: clamp(1.1rem, 1rem + 0.65vw, 1.65rem);
  line-height: 1.2; color: var(--ink);
}
.idx__excerpt {
  display: block; margin-top: 0.5rem; max-width: 62ch;
  font-size: 0.9375rem; line-height: 1.6; color: var(--ink-soft);
}
.idx__meta { text-align: right; font-size: 0.8125rem; color: var(--ink-soft); }
.idx__read { display: block; margin-top: 0.3rem; color: var(--accent-link); }
.idx__arrow {
  color: var(--accent-link); opacity: 0; transform: translateX(-6px);
  transition: opacity var(--duration) var(--ease-out-expo),
              transform var(--duration) var(--ease-out-expo);
}
.idx:hover .idx__arrow { opacity: 1; transform: none; }
@media (hover: none) { .idx__arrow { opacity: 1; transform: none; } }
@media (max-width: 860px) {
  .idx { grid-template-columns: 2.25rem minmax(0, 1fr); }
  .idx__meta { grid-column: 2; text-align: left; margin-top: 0.6rem; }
  .idx__read { display: inline; margin-left: 0.75rem; }
  .idx__arrow { display: none; }
}

/* --------------------------------------------------------------- an article */

/* ------------------------------------------------------------ one article */

/* An editorial page rather than a centred blog column.

   The column version left roughly 450px of empty paper down each side at
   1440 and put nothing in it, buried the narration player below the fold
   inside the text, and gave no sense of a person having written the piece.
   The rail is what that empty margin becomes: byline, listen and share, all
   sticky, so the player travels with the reader instead of scrolling away.

   At narrow widths the rail collapses above the text and the reading order
   is unchanged, so nothing here depends on the two-column form. */

.article__head {
  padding-block: clamp(2.5rem, 2rem + 3vw, 5.5rem) clamp(1.5rem, 2vw, 2.5rem);
}
.article__head .shell { max-width: min(var(--page-max), 1180px); }
.article__kicker {
  margin: 0 0 1.25rem; font-size: 0.75rem;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.article__kicker a { color: var(--accent-link); text-decoration: none; }
.article__kicker a:hover { text-decoration: underline; }
.article__title {
  margin: 0; max-width: 20ch;
  font-size: clamp(2.1rem, 1.4rem + 3.1vw, 4.6rem);
  line-height: 1.02; letter-spacing: -0.028em; text-wrap: balance;
}
/* The standfirst belongs in the header, set large. Michael often opens on a
   short line for rhythm, and giving that line its own air is the difference
   between a deliberate opening and a stray fragment above the body. */
.article__standfirst {
  margin: clamp(1.5rem, 2.5vw, 2.25rem) 0 0; max-width: 34ch;
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(1.2rem, 1.05rem + 0.85vw, 1.8rem);
  line-height: 1.4; color: var(--accent-link);
}

.article__hero {
  margin: 0 auto clamp(2.5rem, 4vw, 4.5rem);
  max-width: min(var(--page-max), 1180px);
  padding-inline: var(--gutter);
}
.article__hero img {
  display: block; width: 100%; height: auto;
  max-height: min(64vh, 620px); object-fit: cover; object-position: 50% 30%;
}

.article__grid {
  display: grid;
  grid-template-columns: minmax(0, 17rem) minmax(0, 40rem);
  gap: clamp(2rem, 5vw, 6rem);
  justify-content: center;
  align-items: start;
}
.article__railin { position: sticky; top: 7rem; }
.article__measure { max-width: 40rem; min-width: 0; }

/* --------------------------------------------------------------- byline */

.byline {
  display: flex; align-items: center; gap: 0.9rem; margin: 0 0 1.75rem;
  padding-bottom: 1.5rem; border-bottom: 1px solid var(--hairline);
}
.byline__face {
  width: 3rem; height: 3rem; flex: 0 0 auto; border-radius: 999px;
  object-fit: cover; object-position: 50% 22%;
}
.byline__text { display: block; min-width: 0; }
.byline__by {
  display: block; font-size: 0.6875rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-soft);
}
.byline__who {
  display: block; font-family: var(--font-display);
  font-size: 1.0625rem; line-height: 1.2; margin-top: 0.15rem;
}
.byline__when {
  display: block; margin-top: 0.25rem;
  font-size: 0.8125rem; color: var(--ink-soft);
}

/* ----------------------------------------------------------------- body */

.article__body p {
  margin: 0 0 1.4rem; line-height: 1.78;
  font-size: clamp(1.0625rem, 1rem + 0.35vw, 1.1875rem); color: var(--ink-soft);
}
.article__body p:first-of-type::first-letter {
  float: left; font-family: var(--font-display);
  font-size: 3.6em; line-height: 0.82;
  padding: 0.06em 0.12em 0 0; color: var(--accent-link);
}

/* A line lifted from the piece, set large. Chosen mechanically in build.py
   from the middle third, never authored, so the page cannot put words in his
   mouth. The rule sits left rather than around it: a boxed quote reads as a
   component, a ruled one reads as a magazine. */
.article__pull {
  margin: clamp(2rem, 3.5vw, 3rem) 0;
  padding-left: clamp(1.25rem, 2.5vw, 2rem);
  border-left: 2px solid var(--accent);
}
.article__pull p {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.375rem, 1.1rem + 1.2vw, 2.1rem);
  line-height: 1.28; letter-spacing: -0.015em; color: var(--ink);
  text-wrap: pretty;
}

/* -------------------------------------------------------------- onward */

/* Somewhere to go that is not the back button. */
.onward {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
  margin-top: clamp(3rem, 5vw, 4.5rem);
  padding-top: clamp(1.75rem, 3vw, 2.5rem);
  border-top: 2px solid var(--ink);
}
.onward__link {
  display: block; text-decoration: none; color: inherit;
  padding: 1.1rem 1.25rem; border-radius: 3px;
  box-shadow: inset 0 0 0 1px var(--hairline);
  transition: box-shadow var(--duration) var(--ease-out-expo);
}
.onward__link:hover { box-shadow: inset 0 0 0 1px var(--accent); }
.onward__label {
  display: block; font-size: 0.6875rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--accent-link);
}
.onward__title {
  display: block; margin-top: 0.5rem; font-family: var(--font-display);
  font-size: 1.125rem; line-height: 1.25; text-wrap: pretty;
}
.onward__meta {
  display: block; margin-top: 0.45rem; font-size: 0.8125rem;
  color: var(--ink-soft);
}

.article__back { margin-top: clamp(2rem, 3vw, 3rem); }

@media (max-width: 1080px) {
  .article__grid { grid-template-columns: minmax(0, 40rem); }
  /* Collapsed, the rail is a header strip above the piece rather than a
     column, so sticky would pin it over the text it introduces. */
  .article__railin { position: static; }
  .article__rail { max-width: 40rem; }
}

/* --------------------------------------------------------------- narration */

.listen {
  display: grid; grid-template-columns: auto 1fr; gap: 0 1rem;
  align-items: center;
  padding: clamp(0.9rem, 1.4vw, 1.25rem);
  margin-bottom: clamp(1.75rem, 3vw, 2.5rem);
  background: var(--bone);
  border-left: 2px solid var(--accent);
}
.listen__btn {
  width: 2.9rem; height: 2.9rem; border: 0; border-radius: 50%;
  background: var(--accent); cursor: pointer;
  display: grid; place-items: center; grid-row: span 2;
  transition: background var(--duration) var(--ease-out-expo);
}
.listen__btn:hover { background: var(--accent-link); }
.listen__icon {
  width: 0; height: 0; margin-left: 3px;
  border-left: 11px solid var(--on-accent);
  border-top: 7px solid transparent; border-bottom: 7px solid transparent;
}
.listen.is-playing .listen__icon {
  margin-left: 0; width: 11px; height: 13px; border: 0;
  background: linear-gradient(to right,
    var(--on-accent) 0 4px, transparent 4px 7px, var(--on-accent) 7px 11px);
}
.listen__label { margin: 0; font-size: 0.875rem; color: var(--ink); font-weight: 500; }
.listen__time { margin: 0.15rem 0 0; font-size: 0.8125rem; color: var(--ink-soft);
  font-variant-numeric: tabular-nums; }
.listen__track { grid-column: 2; height: 3px; background: rgba(17,29,46,0.14); margin-top: 0.6rem; }
.listen__fill { display: block; height: 100%; width: 0; background: var(--accent); }

/* ------------------------------------------------------------------- share */

/* Not a row of social icons chasing reach. One person passing a piece to
   another, so email leads and the networks come last and quietly. */
.share {
  margin-top: clamp(2.5rem, 4vw, 4rem);
  padding-top: clamp(1.5rem, 2.5vw, 2.25rem);
  border-top: var(--rule) solid var(--hairline);
}
.share__label {
  margin: 0 0 1.1rem; font-family: var(--font-display);
  font-size: clamp(1.05rem, 0.95rem + 0.5vw, 1.4rem); color: var(--ink);
}
.share__row { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; align-items: center; }
.share__net {
  font-size: 0.8125rem; color: var(--ink-soft); text-decoration: none;
  border-bottom: 1px solid var(--hairline); padding-bottom: 2px;
}
.share__net:hover { color: var(--accent-link); border-bottom-color: var(--accent); }

/* The enquiries card now sits on paper rather than navy, so it keeps its own
   navy ground and reads as a card instead of merging with the section. */
.section:not(.section--ground) .enquiries h2,
.section:not(.section--ground) .enquiries { color: var(--on-ground); }
.section:not(.section--ground) .enquiries { box-shadow: 0 24px 48px -28px rgba(17, 29, 46, 0.45); }

/* -------------------------------------------------- typographic variations */

/* The word reveal is the house effect, but every heading behaving identically
   is its own kind of monotony. These are variants of the same idea, sharing
   the blur-and-rise but differing in direction and character, so the site has
   a family of effects rather than one on repeat.

   All of them are additive on top of .words-armed, so a heading without a
   variant simply gets the base treatment, and reduced motion still bypasses
   the lot. */

/* Rise from further, for the big page-opening statements. */
.words-armed[data-fx="lift"] .word { transform: translateY(20px); filter: blur(8px); }

/* Settle from the side, for section heads that sit beside body copy. */
.words-armed[data-fx="drift"] .word { transform: translate(-14px, 6px); filter: blur(5px); }

/* Come forward, for anything on a dark ground where a rise gets lost. */
.words-armed[data-fx="near"] .word { transform: scale(0.94); filter: blur(7px); transform-origin: left bottom; }

/* Resolve in place. Jarryd asked for the two turning-point headings to
   "dissolve in", and none of the three above actually does: each one moves,
   so the word arrives from somewhere. This one does not travel at all, it
   simply comes into focus, which is what a dissolve is. Keeping it motionless
   is the whole point, so there is deliberately no transform here to inherit
   from the base rule. */
.words-armed[data-fx="dissolve"] .word {
  transform: none; filter: blur(12px); opacity: 0;
}
.words-in[data-fx="dissolve"] .word { filter: blur(0); opacity: 1; }

/* Every variant lands in the same place, so the resting state is shared. */
.words-in[data-fx] .word { transform: none; filter: blur(0); }

/* ------------------------------------------------------ more coverage */

.cov__more { margin-top: clamp(2rem, 3vw, 3rem); }

/* A real <dialog>, so focus trapping, Escape and inertness are the browser's
   job rather than a hand-rolled approximation that gets them wrong. */
.covdlg {
  border: 0; padding: 0; max-width: min(72rem, 92vw); width: 100%;
  max-height: 86vh; background: var(--paper); color: var(--ink);
  box-shadow: 0 40px 90px -30px rgba(17, 29, 46, 0.55);
}
.covdlg::backdrop { background: rgba(10, 18, 32, 0.62); backdrop-filter: blur(3px); }
.covdlg__inner { padding: clamp(1.5rem, 3vw, 3rem); overflow-y: auto; max-height: 86vh; }
.covdlg__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1.5rem; margin-bottom: clamp(1.25rem, 2.5vw, 2rem);
}
.covdlg__head h2 { margin: 0; font-size: clamp(1.4rem, 1.1rem + 1.2vw, 2.2rem); }
.covdlg__close {
  border: 0; background: none; cursor: pointer; line-height: 1;
  font-size: 2rem; color: var(--ink-soft); padding: 0 0.25rem;
  transition: color var(--duration) var(--ease-out-expo);
}
.covdlg__close:hover { color: var(--ink); }
/* The dialog opens with the same easing as the rest of the site. */
.covdlg[open] { animation: covdlg-in 420ms cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes covdlg-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .covdlg[open] { animation: none; } }

/* -------------------------------------------------- the business card */

/* Stephanie's card. It lifts and the brass edge grows as it arrives, so it
   reads as something handed over rather than a panel that was always there.

   ⚠ The entrance and the pointer tilt MUST NOT both declare `transform`.
   They did, and the entrance rule won on specificity, so the card silently
   stopped tipping even though the script was updating --rx and --ry every
   frame. Both now write only custom properties, and the single transform
   declaration on .enquiries composes them. */
.js-reveal .enquiries.reveal, .enquiries {
  --enter-rx: 4deg;
  --enter-y: 18px;
  opacity: 0;
  transition: opacity 900ms cubic-bezier(0.22, 1, 0.36, 1);
  border-left-width: 0;
}
.js-reveal .enquiries.reveal.is-in, .enquiries.is-in {
  --enter-rx: 0deg;
  --enter-y: 0px;
  opacity: 1;
  border-left-width: 2px;
  transition: opacity 900ms cubic-bezier(0.22, 1, 0.36, 1),
              border-left-width 500ms 400ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* The entrance easing lives on the two properties themselves, so the arrival
   is still eased while the per-frame tilt stays untransitioned. */
@supports (transition-property: --enter-y) {
  .enquiries {
    transition-property: opacity, --enter-rx, --enter-y;
    transition-duration: 900ms;
    transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  }
}
@property --enter-rx { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
@property --enter-y  { syntax: "<length>"; inherits: false; initial-value: 0px; }
.enquiries__ta img { transition: opacity 600ms 500ms var(--ease-out-expo); }
.no-motion .enquiries {
  --enter-rx: 0deg; --enter-y: 0px;
  transform: none; opacity: 1; border-left-width: 2px;
}

/* -------------------------------------------------------- journal grid */

/* Every post has an image and the old list used none of them. The grid gives
   the page a shape: two wide cards break the rhythm so it never reads as a
   uniform block of squares, which is the card-grid failure mode. */
/* The journal's card grid lived here. Replaced by the .jmast / .jlead /
   .jlisten / .jx system below: seventeen two-minute pieces are better served
   by a masthead, one lead and a typographic index than by seventeen equal
   rectangles with a photograph on each. */

}

/* ------------------------------------------------------ optical centring */

/* ⚠ REVERTED. Do not reintroduce a global shell narrowing here.

   An attempt to fix two sections Jarryd named ("Trusted in rooms", "A
   forgettable keynote") by capping .section .shell at 1360px squashed every
   page on the site into a narrow centred column: the topics hero, the case
   studies, the about page and the book all lost their intended width, and
   the result read as one compressed template rather than as a set of pages
   with their own shape. His words: roll back on the centring.

   The lesson, recorded so it is not repeated: those two sections read as
   off-centre because of what is INSIDE them (a short heading beside a long
   paragraph, copy beside a cutout), not because the container was too wide.
   Fix the individual composition, never the global container. */

/* The interlude line's two parts animate as a pair rather than one block. */
.interlude__line small { display: block; }

/* =========================================================== the journal ===
   Kutch's Kommunications. Built as a publication, not a feed.

   The two rejected versions were a link list and then a card grid. The grid
   is the specific thing the anti-AI directive rules out, and it also fought
   the material: a card gives a 130 character excerpt the same footprint as a
   photograph, so seventeen two-minute pieces ran to sixteen thousand pixels.

   What replaces it is a masthead, one lead, a listening shelf and an index
   set as type. The index is the centre of gravity: the whole archive can be
   scanned in about a screen and a half, and the opening line of each piece
   arrives on hover rather than occupying permanent space.
   ------------------------------------------------------------------------ */

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

/* Centred. A masthead is centred, and the previous left-aligned hero left
   two thirds of a 1440 screen empty beside a four word title. */
.jmast .shell { text-align: center; max-width: min(var(--page-max), 940px); }
.jmast__eyebrow {
  font-size: 0.75rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--accent-link); margin: 0 0 clamp(1rem, 2vw, 1.75rem);
}
.jmast__title {
  font-size: clamp(2.6rem, 1rem + 5.6vw, 6.4rem);
  line-height: 0.94; letter-spacing: -0.03em; margin: 0;
  text-wrap: balance;
}
.jmast__lead {
  max-width: 54ch; margin: clamp(1.25rem, 2vw, 2rem) auto 0;
  font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.3125rem);
  line-height: 1.55; color: var(--ink-soft); text-wrap: pretty;
}

/* Three facts, set as a ledger. This is the one place the page states what it
   is holding, and "5 recorded" is the thing no other speaker's journal says. */
.jmast__stats {
  display: flex; justify-content: center; flex-wrap: wrap;
  gap: clamp(2rem, 5vw, 5rem);
  margin: clamp(2.25rem, 4vw, 3.5rem) 0 0;
  padding-top: clamp(1.5rem, 3vw, 2.25rem);
  border-top: 1px solid var(--hairline);
}
.jmast__stats div { margin: 0; }
.jmast__stats dt {
  font-size: 0.6875rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-soft);
}
.jmast__stats dd {
  margin: 0.35rem 0 0; font-family: var(--font-display);
  font-size: clamp(1.75rem, 1.2rem + 1.6vw, 2.75rem); line-height: 1;
  color: var(--accent-link);
}

/* -------------------------------------------------------------- lead piece */

.jlead {
  display: grid; grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
  gap: clamp(1.75rem, 4vw, 4.5rem); align-items: center;
  text-decoration: none; color: inherit;
}
.jlead__media { margin: 0; overflow: clip; background: var(--bone); }
.jlead__media img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 3 / 2; object-fit: cover;
  transition: transform 900ms var(--ease-out-expo);
}
.jlead:hover .jlead__media img { transform: scale(1.03); }
.jlead__kicker {
  font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--accent-link); margin: 0 0 0.9rem;
}
.jlead__title {
  font-size: clamp(1.9rem, 1.2rem + 2.4vw, 3.5rem); line-height: 1.02;
  letter-spacing: -0.025em; margin: 0; text-wrap: balance;
}
.jlead__excerpt {
  margin: clamp(1rem, 1.6vw, 1.5rem) 0 0; max-width: 46ch;
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.1875rem);
  line-height: 1.6; color: var(--ink-soft); text-wrap: pretty;
}
.jlead__meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: 1.25rem;
  margin: clamp(1.25rem, 2vw, 1.9rem) 0 0;
  font-size: 0.875rem; color: var(--ink-soft);
}
.jlead__read { color: var(--accent-link); }
.jlead__audio {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.3rem 0.75rem; border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--hairline);
  color: var(--accent-link); font-size: 0.8125rem;
}
@media (max-width: 860px) { .jlead { grid-template-columns: 1fr; } }

/* --------------------------------------------------------- listening shelf */

/* The narration is the thing this page has that a speaker's blog normally
   does not, and it was previously buried as an inline control inside the
   article pages where nobody browsing would ever meet it. */
.jlisten__head { max-width: 46ch; }
.jlisten__lead { margin: 1rem 0 0; color: var(--on-ground-soft, rgba(242,238,230,0.72)); }
.jlisten__rail {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: clamp(0.75rem, 1.5vw, 1.25rem);
  margin-top: clamp(2rem, 3.5vw, 3rem);
}
.jlisten__card {
  display: flex; align-items: center; gap: 1rem;
  padding: 1.1rem 1.25rem; border-radius: 4px;
  text-decoration: none; color: var(--on-ground);
  box-shadow: inset 0 0 0 1px rgba(242, 238, 230, 0.18);
  transition: box-shadow var(--duration) var(--ease-out-expo),
              background var(--duration) var(--ease-out-expo);
}
.jlisten__card:hover {
  box-shadow: inset 0 0 0 1px var(--accent-soft);
  background: rgba(242, 238, 230, 0.05);
}
.jlisten__play {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.4rem; height: 2.4rem; flex: 0 0 auto; border-radius: 999px;
  background: var(--accent); color: var(--on-accent);
  transition: transform var(--duration) var(--ease-out-expo);
}
.jlisten__play svg { margin-left: 2px; }
.jlisten__card:hover .jlisten__play { transform: scale(1.08); }
.jlisten__text { display: block; min-width: 0; }
.jlisten__title {
  display: block; font-family: var(--font-display); font-size: 1.0625rem;
  line-height: 1.25; text-wrap: pretty;
}
.jlisten__len {
  display: block; margin-top: 0.3rem; font-size: 0.75rem;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--accent-soft);
}
.js-reveal .jlisten__card { transition-delay: calc(var(--i) * 60ms); }

/* ------------------------------------------------------------------- index */

/* The head is a PAIR now: the heading on the left, the audio key on the
   right. Previously it was a two-line heading alone in a 44ch column with an
   empty right half, which is what made the section look unfinished. */
/* ============================================================ the index ===

   Same width discipline as the article page, and for the same reason. The
   rows previously ran the full 1250px container with the date and read time
   pushed to the far right edge, so the eye crossed an empty gulf between a
   title and its own metadata. Nothing on a list should require that journey.

   Widths here:
     head   ~wide, so the section heading has presence
     rows   ~54rem, a comfortable scan width where the meta stays attached
   ------------------------------------------------------------------------ */

.jx__head {
  margin-inline: auto;
  display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(1.5rem, 4vw, 4rem); align-items: end;
  max-width: min(var(--page-max), 54rem);
  margin: 0 auto clamp(1.75rem, 3vw, 2.75rem);
}
.jx__head > div { max-width: 26ch; }
.jx__lead { margin: 0.9rem 0 0; color: var(--ink-soft); max-width: 40ch; }
.jx__listen {
  margin: 0; font-size: 0.875rem; line-height: 1.6; color: var(--ink-soft);
  max-width: 30ch; justify-self: end; text-align: right;
}
.jx__audio--key {
  width: 1.15rem; height: 1.15rem; margin: 0 0.15rem;
  display: inline-flex; vertical-align: -0.2em;
}

.jx__jump {
  margin-inline: auto;
  display: flex; flex-wrap: wrap; gap: 0.6rem;
  max-width: min(var(--page-max), 54rem);
  margin: 0 auto clamp(2rem, 3.5vw, 3rem);
}
.jx__jump a {
  display: inline-flex; align-items: baseline; gap: 0.6rem;
  padding: 0.6rem 1.1rem; border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--hairline);
  text-decoration: none; color: var(--ink); font-size: 0.875rem;
  transition: box-shadow var(--duration) var(--ease-out-expo),
              background var(--duration) var(--ease-out-expo);
}
.jx__jump a:hover, .jx__jump a:focus-visible {
  box-shadow: inset 0 0 0 1px var(--accent); background: var(--bone);
}
.jx__jump span {
  font-size: 0.6875rem; letter-spacing: 0.08em;
  color: var(--accent-link); font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------- the groups */

.jx__group {
  max-width: min(var(--page-max), 54rem);
  margin-inline: auto;
  scroll-margin-top: 7rem;
}
.jx__group + .jx__group { margin-top: clamp(2.5rem, 4vw, 4rem); }
.jx__grouphead {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1.5rem; flex-wrap: wrap;
  margin: 0 0 0.5rem; padding-bottom: 0.7rem;
  border-bottom: 2px solid var(--ink);
}
.jx__theme {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.375rem, 1.1rem + 1vw, 2rem);
  line-height: 1.1; letter-spacing: -0.02em; color: var(--ink);
}
.jx__themenote {
  margin: 0; font-size: 0.875rem; color: var(--ink-soft);
  display: flex; align-items: baseline; gap: 1rem;
}
.jx__count {
  font-size: 0.6875rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--accent-link); white-space: nowrap;
}

/* --------------------------------------------------------------- the rows */

.jx__list { list-style: none; margin: 0; padding: 0; }
.jx__item + .jx__item { border-top: 1px solid var(--hairline); }

/* Two columns only: the number, and everything about the piece. The meta
   now lives INSIDE the main column directly under the title, so the date is
   beside the thing it dates instead of a screen-width away from it. */
.jx__link {
  display: grid; grid-template-columns: 3rem minmax(0, 1fr);
  gap: 0 clamp(0.75rem, 1.5vw, 1.25rem); align-items: baseline;
  padding: clamp(1.1rem, 1.8vw, 1.5rem) 0;
  text-decoration: none; color: inherit;
  transition: padding-left var(--duration) var(--ease-out-expo);
}
.jx__link:hover { padding-left: 0.75rem; }
.jx__no {
  font-size: 0.75rem; letter-spacing: 0.08em; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.jx__main { display: block; min-width: 0; }
.jx__title {
  display: block; font-family: var(--font-display);
  font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.7rem);
  line-height: 1.22; letter-spacing: -0.015em; text-wrap: pretty;
  transition: color var(--duration) var(--ease-out-expo);
}
.jx__link:hover .jx__title { color: var(--accent-link); }
.jx__audio {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.35rem; height: 1.35rem; margin-left: 0.55rem; border-radius: 999px;
  vertical-align: middle; background: var(--accent); color: var(--on-accent);
}
.jx__audio svg { margin-left: 1px; }

/* The meta sits under its own title, always visible. Hiding the date behind a
   hover made the list unscannable for the one thing people sort by. */
.jx__meta {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.45rem;
  margin-top: 0.5rem;
  font-size: 0.8125rem; color: var(--ink-soft);
}
.jx__dot { opacity: 0.5; }
.jx__mins { color: var(--accent-link); }

/* The opening line arrives on hover. Permanent excerpts on seventeen rows
   would treble the height of the index and undo the point of it. */
.jx__excerpt {
  display: block; max-width: 60ch;
  font-size: 0.9375rem; line-height: 1.55; color: var(--ink-soft);
  overflow: hidden; max-height: 0; opacity: 0;
  transition: max-height 460ms var(--ease-out-expo),
              opacity 320ms var(--ease-out-expo),
              margin-top 460ms var(--ease-out-expo);
}
.jx__link:hover .jx__excerpt,
.jx__link:focus-visible .jx__excerpt {
  max-height: 6rem; opacity: 1; margin-top: 0.6rem;
}

@media (max-width: 860px) {
  .jx__head { grid-template-columns: 1fr; align-items: start; }
  .jx__listen { justify-self: start; text-align: left; }
}
@media (max-width: 640px) {
  .jx__link { grid-template-columns: 2.25rem minmax(0, 1fr); }
  /* Hover does not exist on touch, so the excerpt is simply always there. */
  .jx__excerpt { max-height: none; opacity: 1; margin-top: 0.5rem; }
}


@media (prefers-reduced-motion: reduce) {
  .jx__excerpt, .jx__link, .jlead__media img, .jlisten__play {
    transition: none;
  }
}

/* ============================================================= press desk ===
   Rebuilt from six consecutive heading-left / content-right splits, which is
   what made the page read as a template with photographs dropped into it.
   The grounds now alternate paper, navy, bone, band, paper, and each block is
   shaped by what it holds rather than by one repeated grid.
   ------------------------------------------------------------------------ */

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

/* The contact sits IN the masthead. A producer on deadline wants a name and a
   number before anything else, and it used to be the last thing on the page. */
.presshead__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.65fr);
  gap: clamp(2rem, 5vw, 5rem);
  /* Top-aligned, not bottom. Bottom alignment let the taller contact card
     push the headline down the page and opened a band of dead paper above
     the first thing anyone reads. */
  align-items: start;
}
.presshead__copy h1 { max-width: 16ch; }
.presshead__copy .lead { max-width: 46ch; }

.presscard {
  background: var(--ground);
  color: var(--on-ground);
  padding: clamp(1.5rem, 1.2rem + 1.2vw, 2.25rem);
  border-top: 3px solid var(--accent);
  box-shadow: 0 26px 52px -30px rgba(17, 29, 46, 0.55);
}
.presscard__label {
  margin: 0; font-size: 0.6875rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--accent-soft);
}
.presscard__who {
  margin: 0.85rem 0 0; font-family: var(--font-display);
  font-size: clamp(1.25rem, 1.05rem + 0.7vw, 1.7rem); line-height: 1.2;
}
.presscard__who span {
  display: block; margin-top: 0.4rem; font-family: var(--font-body);
  font-size: 0.8125rem; color: rgba(242, 238, 230, 0.66);
}
.presscard__links {
  list-style: none; margin: 1.25rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.5rem;
}
.presscard__links a {
  color: var(--accent-soft); text-decoration: none; font-size: 0.9375rem;
  transition: color var(--duration) var(--ease-out-expo);
  /* stephanie@taspeakersmanagement.com is 34 unbreakable characters. On a
     375px phone it set the grid's minimum content width wider than the
     viewport and gave the whole page a horizontal scrollbar. */
  overflow-wrap: anywhere;
}
.presscard { min-width: 0; }
.presshead__grid > * { min-width: 0; }
.presscard__links a:hover { color: var(--on-ground); }
.presscard__ta {
  display: block; margin-top: clamp(1.25rem, 2.2vw, 1.9rem);
  padding-top: clamp(1rem, 1.8vw, 1.5rem);
  border-top: 1px solid rgba(242, 238, 230, 0.16);
  opacity: 0.8; transition: opacity var(--duration) var(--ease-out-expo);
}
.presscard__ta:hover { opacity: 1; }
.presscard__ta img { display: block; width: clamp(140px, 12vw, 180px); height: auto; }

@media (max-width: 900px) {
  .presshead__grid { grid-template-columns: 1fr; align-items: start; }
}

/* --------------------------------------------------------------- kit block */

/* The page's one loud moment. The photograph is a layer INSIDE the block, not
   a rectangle floating beside a heading, which is the difference between a
   designed section and two things that happen to be adjacent. */
.kitblock {
  position: relative; isolation: isolate;
  padding-block: clamp(4rem, 3rem + 6vw, 9rem);
  background: var(--ground);
  color: var(--on-ground);
  overflow: clip;
}
.kitblock__bg {
  position: absolute; inset: 0; z-index: -2;
  width: 100%; height: 100%; object-fit: cover; object-position: 62% 46%;
  /* Was 0.62. With a lighter scrim and a frame worth seeing, the photograph
     can carry its own weight. */
  opacity: 0.86;
}
/* The scrim is a gradient rather than a flat wash so the image survives on
   one side while the type stays at full contrast on the other. */
.kitblock::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  /* Lightened. At 88% across the middle the navy sat on the photograph like a
     lid; the image is meant to be visible, not merely implied. */
  /* Lightened again on 16 Sept. The band now carries a frame Michael is
     actually in, standing right of centre, so the scrim only has to protect
     the type on the LEFT. It clears entirely past the midpoint. */
  background: linear-gradient(100deg,
    color-mix(in oklab, var(--ground) 90%, transparent) 0%,
    color-mix(in oklab, var(--ground) 52%, transparent) 42%,
    color-mix(in oklab, var(--ground) 6%, transparent) 72%,
    transparent 100%);
}
.kitblock__copy { max-width: 44ch; }
.kitblock__eyebrow {
  margin: 0; font-size: 0.6875rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--accent-soft);
}
.kitblock__h {
  margin: clamp(0.9rem, 1.6vw, 1.4rem) 0 0;
  font-size: clamp(2.2rem, 1.5rem + 3.2vw, 4.6rem);
  line-height: 1.0; letter-spacing: -0.03em;
}
.kitblock__lead {
  margin: clamp(1rem, 1.8vw, 1.6rem) 0 clamp(1.75rem, 2.8vw, 2.5rem);
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.1875rem); line-height: 1.6;
  color: rgba(242, 238, 230, 0.78);
}
.kitblock__note { color: rgba(242, 238, 230, 0.55); }

/* ------------------------------------------------------------- spec sheet */

/* Six verified facts. A bullet list beside a heading gave a birth date the
   same weight as a sentence about a book deal; a numbered sheet with a key
   column lets the eye jump straight to the one it needs. */
.spec__head { max-width: 46ch; margin-bottom: clamp(2rem, 3.5vw, 3rem); }
.spec__lead { margin: 0.9rem 0 0; color: var(--ink-soft); }
/* ⚠ ONE column, not three.
   The facts are numbered 01 to 06, and in a three-column grid they filled
   left-to-right so the numbers ran 01 02 03 across the top and 04 05 06
   underneath, while the columns read as three independent lists that the eye
   wants to scan DOWNWARD. The numbering fought the layout, and each cell was
   its own little three-column grid, which made six noisy sub-tables.
   A single column reads in the order it is numbered, and looking up one fact
   is a straight scan down the key column. That is what a spec sheet is. */
.spec {
  margin: 0;
  border-top: 2px solid var(--ink);
}
.spec__row {
  display: grid;
  grid-template-columns: 3rem minmax(0, 11rem) minmax(0, 1fr);
  gap: 0 clamp(1rem, 2.5vw, 2.5rem); align-items: baseline;
  padding: clamp(1.05rem, 1.7vw, 1.6rem) 0;
  border-bottom: 1px solid var(--hairline);
  transition: padding-left var(--duration) var(--ease-out-expo);
}
.spec__row:hover { padding-left: 0.5rem; }
.js-reveal .spec__row { transition-delay: calc(var(--i) * 60ms); }
.spec__no {
  font-size: 0.75rem; letter-spacing: 0.08em; color: var(--accent-link);
  font-variant-numeric: tabular-nums;
}
.spec__key {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.0625rem, 1rem + 0.5vw, 1.375rem); color: var(--ink);
}
.spec__val {
  margin: 0; max-width: 62ch;
  font-size: clamp(0.9375rem, 0.9rem + 0.2vw, 1.0625rem);
  line-height: 1.6; color: var(--ink-soft); text-wrap: pretty;
}
@media (max-width: 720px) {
  .spec__row { grid-template-columns: 2.25rem minmax(0, 1fr); }
  .spec__val { grid-column: 2; margin-top: 0.4rem; }
}

/* ------------------------------------------------------------- boilerplate */

/* Set as the document it is: reading size, in the display face, with a rule
   down its edge. The old version made an approved paragraph look like body
   copy in a column, so nothing said "this is the thing you paste". */
/* ⚠ A PLATE, not a floating column. Jarryd, 18 Sept: "the boilerplate is just
   hovering in space ... position it better on the page and make it a little
   tighter because it's just there in empty space."

   Measured, the block was capped at 940px inside a 1440px section, so it sat
   with 302px of nothing on each side and the quote itself ran to about 626px:
   a small object adrift in a large field with no edge to relate to. The cause
   was the absence of a ground, not the amount of margin, so widening or
   narrowing it would only have moved the problem.

   It now sits ON something. One bone plate spanning the full shell, a brass
   rule down its left edge, and the label, the paragraph and the action all
   aligned to the plate's own padding rather than to the page. Reading order is
   unchanged, because the previous fix to this block was to stop splitting the
   control away from the text it copies, and that was right. */
.boiler {
  background: var(--bone);
  border-left: 3px solid var(--accent);
  padding: clamp(1.6rem, 1.3rem + 1.8vw, 3rem) clamp(1.4rem, 1.1rem + 2vw, 3.25rem);
  display: grid;
  gap: clamp(1.1rem, 0.9rem + 1vw, 1.75rem);
}
.boiler__title { margin: 0; }
.boiler__note { margin: 0; color: var(--ink-soft); max-width: 46ch; }
.boiler__rail {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 1rem clamp(1.25rem, 3vw, 2rem);
}
@media (min-width: 900px) {
  .boiler {
    grid-template-columns: minmax(0, 1.45fr) minmax(16rem, 1fr);
    column-gap: clamp(2rem, 4vw, 4rem);
    align-items: start;
  }
  .boiler__title { grid-column: 1 / -1; }
  .boiler__rail {
    display: grid; justify-items: start; align-content: start;
    gap: clamp(1rem, 0.8rem + 0.8vw, 1.6rem);
    /* Optically aligned to the first line of the paragraph beside it rather
       than to the top of its box, which sits higher than the text does. */
    padding-top: 0.35rem;
  }
}
.boiler__count {
  margin: 0; font-size: 0.75rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
/* The paragraph no longer needs its own ground or rule: the plate carries
   both, and doubling them made a box inside a box. */
.boiler__text {
  margin: 0; padding: 0;
  max-width: 54ch;
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 1rem + 0.8vw, 1.6rem);
  line-height: 1.45; color: var(--ink); text-wrap: pretty;
}


/* ------------------------------------------------------------ talks index */

.talks {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}
.talks__head { position: sticky; top: 7rem; }
.talks__lead { margin: 1rem 0 0; color: var(--ink-soft); max-width: 36ch; }
.talks__lead a { color: var(--accent-link); }
.talks__list {
  list-style: none; margin: 0; padding: 0; border-top: 2px solid var(--ink);
}
.talks__item {
  display: grid; grid-template-columns: 2.75rem minmax(0, 1fr);
  gap: 0 1rem; align-items: baseline;
  padding: clamp(1.1rem, 1.8vw, 1.6rem) 0;
  border-bottom: 1px solid var(--hairline);
}
.js-reveal .talks__item { transition-delay: calc(var(--i) * 70ms); }
.talks__no {
  font-size: 0.75rem; letter-spacing: 0.08em; color: var(--accent-link);
  font-variant-numeric: tabular-nums;
}
.talks__title {
  display: block; font-family: var(--font-display);
  font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.9rem);
  line-height: 1.15; letter-spacing: -0.015em;
}
/* ⚠ Two of the three keynotes share the title "Listen to the Music" and
   differ only by subtitle. That is the client's naming, not ours, and on a
   list it reads as a duplication bug. The subtitle therefore carries real
   weight here rather than being set as small print. */
.talks__sub {
  display: block; margin-top: 0.45rem;
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(0.95rem, 0.9rem + 0.3vw, 1.125rem);
  color: var(--accent-link); text-wrap: pretty;
}
@media (max-width: 860px) {
  .talks { grid-template-columns: 1fr; }
  .talks__head { position: static; }
}

.press__figure img { display: block; width: 100%; height: auto; }

/* ============================================================ topics page ===
   Three keynotes as a selection board, not a stack of three posts.
   ------------------------------------------------------------------------ */

/* ----------------------------------------------------------- the chooser */

.picks__head { max-width: 42ch; margin-bottom: clamp(2rem, 3.5vw, 3rem); }
.picks__lead { margin: 1rem 0 0; color: var(--ink-soft); }
.picks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  gap: clamp(1rem, 2vw, 1.75rem);
}
.pick {
  display: flex; flex-direction: column;
  padding: clamp(1.4rem, 1.2rem + 1vw, 2rem);
  background: var(--paper);
  text-decoration: none; color: inherit;
  border-top: 2px solid var(--ink);
  transition: transform 520ms var(--ease-out-expo),
              box-shadow 520ms var(--ease-out-expo);
}
.js-reveal .pick { transition-delay: calc(var(--i) * 80ms); }
.pick:hover {
  transform: translateY(-4px);
  box-shadow: 0 22px 40px -26px rgba(17, 29, 46, 0.5);
}
.pick__no {
  font-size: 0.75rem; letter-spacing: 0.1em; color: var(--accent-link);
  font-variant-numeric: tabular-nums;
}
.pick__title {
  display: block; margin-top: 0.9rem;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1.1rem + 0.6vw, 1.6rem);
  line-height: 1.15; letter-spacing: -0.015em;
}
/* ⚠ Two of the three keynotes share a title. The subtitle is what actually
   tells them apart, so it carries the weight here rather than being set as
   small print underneath. */
.pick__sub {
  display: block; margin-top: 0.55rem;
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(0.95rem, 0.9rem + 0.3vw, 1.1rem);
  line-height: 1.3; color: var(--accent-link); text-wrap: pretty;
}
.pick__aud {
  display: block; margin-top: 1rem; padding-top: 1rem;
  border-top: 1px solid var(--hairline);
  font-size: 0.875rem; line-height: 1.55; color: var(--ink-soft);
  text-wrap: pretty; flex: 1;
}
.pick__go {
  display: block; margin-top: 1.25rem;
  font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--accent-link);
  transition: transform var(--duration) var(--ease-out-expo);
}
.pick:hover .pick__go { transform: translateY(2px); }

/* --------------------------------------------------- keynote detail block */

/* The structural move that stops these reading as blog posts: the photograph
   bleeds off the edge of the page instead of sitting in a contained rectangle
   beside the text. A picture that runs out of the frame cannot be mistaken
   for an article illustration. Sides alternate so the page has a rhythm. */
.kn {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  gap: clamp(2rem, 5vw, 6rem);
  align-items: center;
  padding-block: clamp(3rem, 2rem + 4vw, 7rem);
  scroll-margin-top: 6rem;
}
.kn + .kn { border-top: 1px solid var(--hairline); }
.kn__media { margin: 0; overflow: clip; }
.kn__media img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 4 / 5; object-fit: cover;
  transition: transform 900ms var(--ease-out-expo);
}
.kn:hover .kn__media img { transform: scale(1.02); }

/* Odd blocks bleed left, even blocks bleed right. */
.kn:nth-of-type(odd) .kn__media {
  margin-left: calc(var(--gutter) * -1);
  border-radius: 0 3px 3px 0;
}
.kn:nth-of-type(even) .kn__media {
  order: 2;
  margin-right: calc(var(--gutter) * -1);
  border-radius: 3px 0 0 3px;
}
.kn:nth-of-type(even) .kn__body { order: 1; }

.kn__body { max-width: 52ch; }
.kn__no {
  /* ⚠ --accent-link, not --accent. The design system is explicit that brass
     is a FILL token and fails as text at 2.67:1; --accent-link is the same
     brass darkened until it passes. Compounded with 0.28 opacity these
     numerals measured 2.67:1, below even the 3:1 large-text threshold. The
     opacity lifts a little so they still read as quiet sequence markers
     rather than as headings. */
  margin: 0; font-family: var(--font-display);
  font-size: clamp(2.5rem, 1.5rem + 3.5vw, 5rem);
  line-height: 0.9; color: var(--accent-link);
  opacity: 0.5;
}
.kn__title {
  margin: clamp(0.5rem, 1vw, 1rem) 0 0;
  font-size: clamp(1.9rem, 1.4rem + 2.2vw, 3.4rem);
  line-height: 1.03; letter-spacing: -0.025em;
}
.kn__sub {
  margin: 0.7rem 0 0;
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(1.05rem, 0.95rem + 0.55vw, 1.45rem);
  line-height: 1.3; color: var(--accent-link); text-wrap: pretty;
}
.kn__aud {
  margin: 1.1rem 0 0; padding-left: 1rem;
  border-left: 2px solid var(--accent);
  font-size: 0.9375rem; line-height: 1.55; color: var(--ink-soft);
}
.kn__prose { margin-top: clamp(1.5rem, 2.5vw, 2.25rem); }
.kn__prose p {
  margin: 0 0 1.1rem; line-height: 1.7;
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.0625rem); color: var(--ink-soft);
}

/* Outcomes as a numbered list rather than ticks. A planner is buying a
   result, so the results are counted. */
.kn__out { margin-top: clamp(1.5rem, 2.5vw, 2.25rem); }
.kn__outhead {
  margin: 0 0 1rem; font-size: 0.6875rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-soft); font-weight: 500;
}
.kn__outlist { list-style: none; margin: 0; padding: 0; }
.kn__outlist li {
  display: grid; grid-template-columns: 1.75rem minmax(0, 1fr);
  gap: 0 0.75rem; align-items: baseline;
  padding: 0.7rem 0; border-top: 1px solid var(--hairline);
  font-family: var(--font-display);
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.1875rem);
  line-height: 1.35; color: var(--ink); text-wrap: pretty;
}
.kn__outlist li:last-child { border-bottom: 1px solid var(--hairline); }
.kn__onum {
  font-family: var(--font-body); font-size: 0.75rem;
  color: var(--accent-link); font-variant-numeric: tabular-nums;
}
.kn .cta-row { margin-top: clamp(1.75rem, 2.5vw, 2.5rem); }

@media (max-width: 900px) {
  .kn { grid-template-columns: 1fr; gap: 1.75rem; }
  .kn:nth-of-type(even) .kn__media { order: 0; }
  .kn:nth-of-type(even) .kn__body { order: 0; }
  .kn__media { margin-inline: calc(var(--gutter) * -1) !important; border-radius: 0 !important; }
  /* ⚠ 4:5 portrait source into a 3:2 landscape box crops the TOP and BOTTOM,
     and the default 50% 50% then centres on the chest. Michael's face sits at
     roughly 23% down every one of these frames, so a centred crop clipped his
     head on all three keynote cards. Faces live in the upper third; the crop
     has to be told that. */
  .kn__media img { aspect-ratio: 3 / 2; object-position: 50% 20%; }
  .kn__body { max-width: none; }
}

/* ================================================ one article, part two ===
   Jarryd: "extremely dry... be like a web designer with 50 years of creative
   direction experience."

   The bones were right, the page just had nothing on it. What was missing was
   the things a magazine does that a blog template does not: an opening that
   is set rather than stacked, artwork that belongs to the brand, a quotation
   that is actually large, and a sign-off so the piece ends instead of
   stopping.
   ------------------------------------------------------------------------ */

/* A rule under the opening. Small thing, but it is what separates a masthead
   from a paragraph that happens to be big. */
.article__rule {
  height: 0; margin: clamp(2rem, 3.5vw, 3.25rem) 0 0;
  border-top: 2px solid var(--ink);
  max-width: 8rem;
}

/* The opening paragraph carries the drop cap AND a larger measure, so the
   piece starts at a different weight from the body and settles into it. */
.article__body > p:first-of-type {
  font-size: clamp(1.1875rem, 1.05rem + 0.6vw, 1.4375rem);
  line-height: 1.62;
  color: var(--ink);
}

/* --------------------------------------------------------------- artwork */

/* The duotoned band. It sits inside the reading column but breaks its
   measure, which is what makes it read as a breath rather than an inserted
   picture. Fifteen of these images are generic Unsplash stock from the old
   site; duotoned into the navy ink they stop looking borrowed. */
.artband {
  /* Breaks the measure decisively. A band that oversteps the column by only a
     few pixels reads as a misaligned picture; one that clearly oversteps reads
     as a deliberate breath. It runs left into the rail gutter and right past
     the text edge, so the column visibly opens up and closes again. */
  margin: clamp(2.75rem, 4.5vw, 4.25rem) 0;
  width: calc(100% + clamp(3rem, 9vw, 11rem));
  margin-left: calc(clamp(2rem, 6vw, 7rem) * -1);
  overflow: clip;
}
@media (max-width: 1080px) {
  .artband { width: 100%; margin-left: 0; }
}
.artband img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 2 / 1; object-fit: cover;
  background: var(--ground);
}

/* ----------------------------------------------------------- pull quote */

/* Actually large, with the mark set as a mark. The previous version was a
   ruled paragraph at 2rem, which is a slightly bigger sentence, not a pull
   quote. */
.article__pull {
  position: relative;
  margin: clamp(2.5rem, 4.5vw, 4rem) 0;
  padding: 0 0 0 clamp(2.5rem, 5vw, 4.5rem);
  border-left: 0;
}
.article__pull::before {
  content: "\201C";
  position: absolute; left: -0.06em; top: -0.28em;
  font-family: var(--font-display);
  font-size: clamp(5rem, 3rem + 7vw, 10rem);
  line-height: 1; color: var(--accent);
  opacity: 0.28; pointer-events: none;
}
.article__pull p {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.6rem, 1.2rem + 1.9vw, 2.9rem);
  line-height: 1.2; letter-spacing: -0.022em; color: var(--ink);
  text-wrap: pretty;
}

/* ------------------------------------------------------------- sign-off */

/* A column ends with the writer's mark. Without it the piece just stopped and
   the share bar became the ending, which put the call to action where the
   last thought should be. */
.signoff {
  display: flex; align-items: center; gap: clamp(1rem, 2vw, 1.75rem);
  margin-top: clamp(3rem, 5vw, 4.5rem);
  padding-top: clamp(1.75rem, 3vw, 2.5rem);
  border-top: 1px solid var(--hairline);
}
.signoff__mark {
  width: clamp(84px, 8vw, 116px); height: auto; flex: 0 0 auto;
  opacity: 0.85;
}
.signoff__text {
  margin: 0; font-size: 0.9375rem; line-height: 1.6; color: var(--ink-soft);
  max-width: 42ch;
}
.signoff__text strong { color: var(--ink); font-weight: 500; }

/* --------------------------------------------------- reading progress */

/* A hairline of brass across the top of the masthead, filling as the piece is
   read. On a page whose whole promise is "this is a two minute read", showing
   the two minutes running down is worth one line of chrome. */
.readbar {
  position: fixed; inset: 0 auto auto 0; z-index: 60;
  height: 2px; width: 100%;
  transform: scaleX(var(--read, 0)); transform-origin: 0 50%;
  background: var(--accent);
  pointer-events: none;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) { .readbar { display: none; } }

/* ================================================== video: reel and loops ===
   Two kinds of video, and they are not the same component.
   ------------------------------------------------------------------------ */

/* ------------------------------------------------------------- the reel */

/* User-initiated, with controls and sound. It replaced a placeholder box with
   a disabled play triangle, which promised a reel the page could not deliver. */
.reel { margin: 0; }
.reel__v {
  display: block; width: 100%; height: auto;
  aspect-ratio: 16 / 9; object-fit: cover;
  background: var(--ground);
  border-radius: 2px;
}
.reel__cap {
  margin-top: 0.9rem; font-size: 0.8125rem; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-soft);
}

/* --------------------------------------------------- the full-bleed reel
   Jarryd asked for the reel to run full width and silent, and to open with
   sound when pressed. The frame IS the control, so it is a real <button>:
   keyboard reachable, announced, and focusable, which a click handler bolted
   onto a <video> would not be. */
.reel--bleed { margin-top: clamp(1.75rem, 1.4rem + 1.5vw, 3rem); }
.reel__open {
  display: block; position: relative; isolation: isolate;
  width: 100%; padding: 0; border: 0; background: var(--ground);
  cursor: pointer; overflow: clip;
}
.reel--bleed .reel__v {
  border-radius: 0;
  /* Tall enough to read as a scene rather than a strip, capped so it never
     pushes the page below it off a laptop screen. */
  aspect-ratio: 16 / 9; max-height: 78vh;
}
/* A scrim only under the cue, so the label stays legible over any frame
   without dimming the picture as a whole. */
.reel__cue {
  position: absolute; inset: auto 0 0 0; z-index: 1;
  display: flex; align-items: center; gap: 0.9rem;
  padding: clamp(2.5rem, 2rem + 3vw, 5rem) clamp(1.25rem, 3vw, 3.5rem)
           clamp(1.1rem, 0.9rem + 1vw, 2rem);
  color: var(--on-ground);
  background: linear-gradient(to top,
    rgba(10, 21, 35, 0.72), rgba(10, 21, 35, 0.28) 55%, transparent);
  pointer-events: none;
}
.reel__play {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.9rem; height: 2.9rem; flex: 0 0 auto;
  border-radius: 999px; background: var(--accent); color: var(--on-accent);
  transition: transform 420ms var(--ease-out-expo);
}
.reel__tri { font-size: 0.8rem; margin-left: 0.15em; }
.reel__cue-text {
  font-size: 0.9375rem; font-weight: 500; letter-spacing: 0.01em;
}
.reel__open:hover .reel__play,
.reel__open:focus-visible .reel__play,
.reel__open.is-hinted .reel__play { transform: scale(1.12); }
.reel__open:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }

/* The dialog. A real <dialog>, so Escape, focus trapping and inertness are the
   browser's job rather than a hand-rolled approximation that gets them wrong,
   matching the coverage dialog on the Media page. */
.reeldlg {
  border: 0; padding: 0; background: transparent;
  width: min(92vw, 1180px); max-width: 92vw;
  color: var(--on-ground);
}
.reeldlg::backdrop { background: rgba(10, 21, 35, 0.86); }
.reeldlg__v {
  display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;
  background: #000; border-radius: 2px;
}
.reeldlg__v iframe { display: block; width: 100%; height: 100%; border: 0; }

.reeldlg__x {
  position: absolute; top: -3rem; right: 0;
  width: 44px; height: 44px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.5rem; border: 0; border-radius: 999px;
  background: transparent; color: var(--on-ground); cursor: pointer;
}
.reeldlg__x:hover, .reeldlg__x:focus-visible { color: var(--accent-soft); }

/* ----------------------------------------------------------- ambient loops */

/* Texture, not content. No controls, no sound, no way to interact: if a
   visitor can pause it, it is a player and it demands attention. These are
   meant to be barely noticed. */
.loopfig { margin: 0; position: relative; overflow: clip; background: var(--bone); }
.loop {
  display: block; width: 100%; height: 100%;
  /* 16:9, matching how the montage is cut. The earlier 4:5 window threw away
     a third of every wide shot in it. */
  aspect-ratio: 16 / 9; object-fit: cover;
  /* The masters are lit for a room, and a slightly cooler, slightly softer
     grade seats them beside the duotoned journal artwork instead of jumping
     out as raw camera footage. */
  filter: saturate(0.92) contrast(1.03);
}
.loopfig--tall .loop,
.loopfig--tall img { aspect-ratio: 3 / 4; }
.loopfig img { display: block; width: 100%; height: auto; object-fit: cover; }
.loop::-webkit-media-controls { display: none !important; }

/* A loop that plays edge to edge with a hard border reads as an embed. A
   whisper of vignette at the foot lets it sit on the paper. */
.loopfig::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 22%;
  background: linear-gradient(to top, rgba(17, 29, 46, 0.14), transparent);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  /* Honour the preference properly: the JS pauses the element, and the
     poster stays visible underneath. */
  .loop { filter: none; }
}

/* ------------------------------------------------------- about story block */

/* Was two plain 50/50 splits stacked: heading beside a floating rectangle,
   then a paragraph beside another floating rectangle. Nothing tied the
   picture to the words, and at 1440 the second one left a column of empty
   paper down the middle.
   
   Now one composition: the prose runs in a real reading measure, and the
   aside carries the loop with the line that the whole page is about set
   directly underneath it, so the image and the type are a single object. */
.storyblock {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: clamp(2rem, 5vw, 6rem);
  align-items: start;
}
.storyblock__prose { max-width: 62ch; }
/* ⚠ Uses the shared body token, NOT its own clamp. This block had a bespoke
   font-size that drifted from the rest of the site, so the About page's main
   paragraph read at a different size to every other paragraph. */
.storyblock__prose p {
  margin: 0 0 1.25rem; line-height: 1.7;
  font-size: var(--text-body); color: var(--ink-soft);
}
.storyblock__prose strong { color: var(--ink); font-weight: 500; }
.storyblock__aside { position: sticky; top: 7rem; }
.storyblock__pull {
  margin: clamp(1.25rem, 2vw, 1.75rem) 0 0;
  padding-left: clamp(1rem, 1.6vw, 1.4rem);
  border-left: 2px solid var(--accent);
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 1rem + 0.7vw, 1.55rem);
  line-height: 1.32; color: var(--ink); text-wrap: pretty;
}
.storyblock__pull cite {
  display: block; margin-top: 0.7rem;
  font-family: var(--font-body); font-style: normal;
  font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-soft);
}
@media (max-width: 900px) {
  .storyblock { grid-template-columns: 1fr; }
  .storyblock__aside { position: static; }
}

/* The client's own mark on a case study. Sits at the end of the header row so
   the eye lands on the outcome first and the proof second, which is the order
   the study is written in. Height-matched across the three (see
   design/build_case_logos.py) so a wide wordmark and a round badge read as
   the same weight. */
.casefile__head { position: relative; }
.casefile__mark {
  display: block;
  margin-top: clamp(0.85rem, 1.3vw, 1.15rem);
  padding-top: clamp(0.75rem, 1.1vw, 1rem);
  border-top: 1px solid var(--hairline);
  max-width: 15rem;
}
.casefile__mark img {
  /* ⚠ Height-capped AND width-capped. HSPC's mark is a bilingual two-line
     wordmark with a badge, so at a height that suits the Tremblant oval its
     type became unreadable. Letting it run taller and capping the width
     instead keeps all three optically equal without shrinking the wordiest
     one into nothing. */
  display: block; width: auto; height: clamp(40px, 4.6vw, 62px);
  max-width: 100%; object-fit: contain; object-position: left center;
  /* Client logos arrive in their own brand colours, which fight both each
     other and the page. Held back slightly so they read as evidence rather
     than as three competing adverts, and restored on hover. */
  opacity: 0.78;
  transition: opacity var(--duration) var(--ease-out-expo);
}
.casefile:hover .casefile__mark img { opacity: 1; }

/* ------------------------------------------------------------ signup field

   One field, one action, one control.

   It was a full-width labelled input with a pill button floating 46px
   underneath it, which is the layout for a multi-field form. On a single
   email capture it reads as two unrelated objects and the button looked
   stranded, which is what Jarryd saw. Joining them states the whole
   transaction in one line: type here, press that. */

.signup {
  display: flex; align-items: stretch;
  max-width: 30rem;
  background: var(--paper);
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--hairline);
  transition: box-shadow var(--duration) var(--ease-out-expo);
}
.signup:focus-within { box-shadow: inset 0 0 0 2px var(--accent); }
.signup input {
  flex: 1 1 auto; min-width: 0;
  padding: 0.95rem 0.5rem 0.95rem 1.4rem;
  border: 0; background: transparent; color: var(--ink);
  font-family: var(--font-body); font-size: 0.9375rem;
}
/* The ring belongs to the group, not the input inside it, or you get two
   rounded rectangles fighting. */
.signup input:focus, .signup input:focus-visible { outline: 0; box-shadow: none; }
.signup input::placeholder { color: var(--ink-soft); opacity: 0.7; }

.signup__go {
  display: inline-flex; align-items: center; gap: 0.85rem;
  flex: none; margin: 4px 4px 4px 0;
  padding: 0 0.5rem 0 1.1rem;
  border: 0; border-radius: 999px; cursor: pointer;
  background: var(--accent); color: var(--on-accent);
  font-family: var(--font-body); font-size: 0.875rem; font-weight: 500;
  white-space: nowrap;
  transition: background var(--duration) var(--ease-out-expo);
}
.signup__go:hover, .signup__go:focus-visible { background: var(--accent-link); }
.signup__token {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
  transition: transform var(--duration) var(--ease-out-expo);
}
.signup__go:hover .signup__token { transform: translateX(3px); }

.signup + .field__hint { margin-top: 0.75rem; }

/* On a narrow screen the joined pill would squeeze the label to nothing, so
   it stacks and the button goes full width. */
@media (max-width: 560px) {
  .signup { flex-direction: column; border-radius: 14px; }
  .signup input { padding: 0.9rem 1.1rem; }
  .signup__go { margin: 0 4px 4px; padding: 0.75rem 0.5rem 0.75rem 1.1rem;
    justify-content: space-between; border-radius: 10px; }
}

/* ------------------------------------------------------------ touch targets

   Measured, not guessed: sixteen links per page rendered between 19px and
   21px tall. WCAG 2.2 asks for 24px minimum and every practical guideline
   asks for more; on a phone a 19px phone-number link is a genuinely hard
   thing to hit, and it was the ONE link on the press page a producer most
   needs to press.

   Padding rather than min-height, because these sit in flowing text and
   columns where a min-height would break the line rhythm. The padding is
   vertical only, so nothing moves horizontally. */

.footer a,
.nav a,
.presscard__links a {
  padding-block: 0.42rem;
  /* inline-block so vertical padding actually applies to an inline link */
  display: inline-block;
}

/* The nav is a flex row, so restore its alignment after the padding. */
.nav { align-items: center; }

/* On a phone the target is the whole row, not just the words. */
@media (max-width: 640px) {
  .footer a, .presscard__links a { padding-block: 0.55rem; width: 100%; }
}

/* The last two holdouts: the coverage dialog's close control and the contact
   form's inline links, both still under 30px after the pass above. */
.covdlg__close { min-width: 44px; min-height: 44px; }
.contact a, .legal a, .cov__foot a { padding-block: 0.35rem; display: inline-block; }

/* Contact page form links and the article share row were the last holdouts:
   they sit inside their own components and were not caught by the selectors
   above. Same treatment, same reason. */
.enquiries a, .contactblock a, .form a,
.share a, .share button, .article__back a { padding-block: 0.4rem; }
.share a, .share button { display: inline-flex; align-items: center; min-height: 30px; }

/* Inline links inside running prose: the contact page's email and phone, and
   the article kicker. These have no class of their own, so they are reached
   through their container. An inline element ignores vertical padding for
   layout, so inline-block is what makes the target actually grow. */
.prose a, .contact__col a, .article__kicker a, .lead a {
  display: inline-block; padding-block: 0.3rem;
}

/* Contact-page inline links live in their own blocks; reached by container
   because the markup gives them no class. */
.contactcard a, .contact__meta a, .faq a, dd a {
  display: inline-block; padding-block: 0.3rem;
}

/* ⚠ .footer__by is excluded from the `.footer a:not(.btn)` padding rule by
   being matched here more specifically, because it is a LAYOUT element (the
   "Site by" lockup) rather than a text link. It measured 12px tall, so it
   gets a real target without the padding disturbing the lockup. */
.footer a.footer__by { min-height: 32px; padding-block: 0.3rem; }

/* The masthead home link wraps only an image. An inline anchor around an
   inline-level image collapses to the line box (12px measured), so the whole
   logo was a 12px target even though the mark looks much bigger. Making the
   anchor a flex box hands it the image's real height. */
/* ⚠ Targeted by structure, not with :has(). The masthead's home link is its
   first child anchor; :has() was matching but losing to an earlier rule, and
   a structural selector is both better supported and unambiguous here. */
.masthead > a:first-child { display: inline-flex; align-items: center; min-height: 30px; }
.masthead__logo { display: block; }

/* The "Topics page" link inside the talks lead. Last inline link on the site
   without a real target; it wraps to two lines at narrow widths so it needs
   inline-block rather than a min-height. */
.talks__lead a { display: inline-block; padding-block: 0.3rem; }

/* ============================================================ video hero ===
   The About page opens on the video itself rather than on a video sitting
   beside a photograph. Two media objects competing at the top of a page is
   what made that section fail twice; one full-bleed ground with the type over
   it is unambiguous about what the page is.
   ------------------------------------------------------------------------ */

.vhero {
  position: relative; isolation: isolate;
  display: flex; align-items: flex-end;
  /* Taller, so the type has ground of its own below the picture rather than
     sitting across whatever the montage is showing. The extra height is what
     lets the gradient finish before it reaches a face. */
  min-height: min(94vh, 56rem);
  padding-block: clamp(3.5rem, 2rem + 7vw, 8.5rem);
  background: var(--ground);
  overflow: clip;
}
.vhero__v {
  position: absolute; inset: 0; z-index: -2;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 32%;
  /* Overrides .loop's own aspect-ratio: here the video fills the section
     rather than holding a shape of its own. */
  aspect-ratio: auto;
}
/* The scrim. A gradient from the foot, not a flat wash, so the picture stays
   a picture at the top while the type sits on near-solid ground at the
   bottom. Contrast measured against --on-ground at the text's own position. */
.vhero::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to top,
    /* Dropped back so more of the picture survives. The type sits on the
       lower left, so the scrim is strongest there and clears quickly. */
    color-mix(in oklab, var(--ground) 92%, transparent) 0%,
    color-mix(in oklab, var(--ground) 66%, transparent) 34%,
    color-mix(in oklab, var(--ground) 26%, transparent) 66%,
    color-mix(in oklab, var(--ground) 6%, transparent) 100%);
}
.vhero__copy {
  color: var(--on-ground);
  /* Held to the left half so the video is not hidden behind the type. */
  max-width: min(var(--page-max), 1180px);
  /* ⚠ Round one, planner item P7: "move the About hero text to the left".
     It was already capped at 40ch and still landed 403px in on a 1440 screen,
     directly across Michael's face. The cap was never the problem. .vhero is
     a FLEX container and .shell carries margin-inline:auto, so the block was
     being centred as a flex item; the comment above described an intent the
     layout quietly overrode. Width plus a zero inline margin puts the type
     back on the gutter where the note asked for it. */
  width: 100%;
  margin-inline: 0;
}
.vhero__copy > * { max-width: 40ch; }
.vhero__eyebrow {
  margin: 0 0 clamp(0.9rem, 1.6vw, 1.4rem);
  font-size: 0.6875rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--accent-soft);
}
/* ⚠ The colour has to be stated on the h1 AND on its word spans.
   A global `h1 { color: var(--ink) }` beats inheritance from .vhero__copy, so
   the headline rendered in dark navy ON the dark video and was very nearly
   invisible. The .word spans are created at runtime by the reveal script and
   inherit from the h1, so fixing the h1 fixes both, but both are named here
   so a future change to either cannot silently reintroduce it. */
.vhero__copy h1,
.vhero__copy h1 .word {
  color: var(--on-ground);
}
.vhero__copy h1 {
  margin: 0; max-width: 13ch;
  /* Smaller and narrower on Jarryd's note, so the type occupies the left of
     the frame and leaves the picture visible rather than covering it. */
  font-size: clamp(1.7rem, 1rem + 2.3vw, 2.9rem);
  line-height: 0.98; letter-spacing: -0.03em;
}
.vhero__lead {
  margin: clamp(1rem, 1.6vw, 1.4rem) 0 0; max-width: 42ch;
  font-size: var(--text-body);
  line-height: 1.6; color: rgba(242, 238, 230, 0.86);
}
.vhero__cta { margin-top: clamp(1.4rem, 2.2vw, 2rem); }

@media (max-width: 720px) {
  .vhero { min-height: 74vh; }
  .vhero__v { object-position: 50% 26%; }
}

/* ------------------------------------------------------- polaroid bottom */

/* ⚠ The deep bottom lip is what makes a Polaroid a Polaroid, and it was
   MISSING: it came entirely from the figcaption's padding, and these frames
   carry no caption, so the print ended flush at the base of the photograph.
   Jarryd: "the Polaroids don't have bottoms."
   The lip now belongs to the frame itself, so it exists whether or not a
   caption does. Roughly 1.6x the side border, which is the real proportion. */
/* A real SX-70 print has a chin roughly TWICE the side border, and that
   proportion is most of what makes the shape recognisable. 7.4% was still
   reading as a slightly generous margin rather than as a Polaroid.
   Side border is 4.6%, so the chin is set at 11% -- a shade under 2.4x. */
.pola { padding-bottom: 11%; }
.pola figcaption { padding-bottom: 0; }

/* ------------------------------------------------------- press sticky bar

   A press page is used by somebody on a deadline, and the action they take
   most is "get hold of a human". Once the masthead contact card scrolls away
   there was no route to Stephanie short of returning to the top or scrolling
   to the footer. This brings the contact back and keeps it there.

   Below the masthead, not over it, so it never covers the nav. */
.pressbar {
  position: fixed; z-index: 40;
  inset: var(--masthead-h, 92px) 0 auto 0;
  background: var(--ground);
  color: var(--on-ground);
  border-bottom: 1px solid rgba(242, 238, 230, 0.14);
  box-shadow: 0 12px 26px -18px rgba(17, 29, 46, 0.7);
  transform: translateY(-102%);
  transition: transform 460ms var(--ease-out-expo);
}
.pressbar.is-up { transform: none; }
.pressbar__inner {
  display: flex; align-items: center; gap: clamp(0.75rem, 2vw, 1.75rem);
  padding-block: 0.7rem;
  flex-wrap: wrap;
}
.pressbar__label {
  margin: 0; font-size: 0.625rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--accent-soft);
}
.pressbar__who {
  margin: 0; font-family: var(--font-display); font-size: 1.0625rem;
}
.pressbar__link {
  color: rgba(242, 238, 230, 0.86); text-decoration: none; font-size: 0.875rem;
  transition: color var(--duration) var(--ease-out-expo);
}
.pressbar__link:hover { color: var(--accent-soft); }
.pressbar__kit { margin-left: auto; }

@media (max-width: 760px) {
  /* On a phone the bar would eat a third of the screen. Keep only the two
     things worth interrupting for. */
  .pressbar__label, .pressbar__who { display: none; }
  .pressbar__inner { gap: 0.75rem; padding-block: 0.5rem; }
  .pressbar__kit { margin-left: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .pressbar { transition: none; }
}

/* The two links in the sticky press bar were the last undersized targets. */
.pressbar__link { display: inline-flex; align-items: center; min-height: 30px; }

/* --------------------------------------------------- about story heading */

/* The prose opened with no heading of any kind, directly under a full-bleed
   video hero. Jarryd: "the copy just looks like it's hanging there... really
   empty without any form of heading". A section that carries the substance of
   the page needs a way in. */
.storyblock__eyebrow {
  margin: 0 0 clamp(0.8rem, 1.4vw, 1.2rem);
  font-size: 0.6875rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--accent-link);
}
.storyblock__h {
  margin: 0 0 clamp(1.5rem, 2.4vw, 2.25rem);
  font-size: clamp(1.9rem, 1.4rem + 2.1vw, 3.2rem);
  line-height: 1.04; letter-spacing: -0.025em; max-width: 18ch;
}

/* ================================================ headings on dark ground ===

   ⚠ SYSTEMIC FIX. `h1, h2, h3, h4 { color: var(--ink) }` beats inheritance,
   so ANY heading placed on a dark section renders in dark navy on dark navy
   and is effectively invisible. This has now bitten twice: the About video
   hero, and "The media kit" on the press page.

   Rather than remember to set the colour on every new dark component, every
   dark ground on the site claims its headings here. `.word` is included
   because the reveal script wraps heading text in spans at runtime.

   Add a new dark section? Add its class to this list. */
.section--ground h1, .section--ground h2, .section--ground h3, .section--ground h4,
.footer h1, .footer h4,
.triad h1, .triad h2, .showcase h1, .showcase h2,
.pillars h2, .pillars h3,
.interlude h2, .interlude p,
.stakes h2,
.kitblock h1, .kitblock h2, .kitblock h3,
.vhero h1, .vhero h2,
.jlisten h2, .jlisten h3,
.presscard h2, .presscard h3,
.megafooter h1, .megafooter h2, .megafooter h3,
.section--ground .word, .footer .word, .kitblock .word,
.vhero .word, .jlisten .word, .megafooter .word {
  color: var(--on-ground);
}

/* ========================================================== press, rebuilt ===
   Full overhaul. The previous version put a navy contact CARD in the top
   right, which fought the headline for the same optical space and left the
   whole opening unbalanced, and it opened straight into prose with no way for
   a producer to jump to the one thing they came for.
   ------------------------------------------------------------------------ */

.presshead {
  padding-block: clamp(2.5rem, 2rem + 4vw, 5.5rem) 0;
  border-bottom: 0;
}
.presshead__eyebrow {
  margin: 0 0 clamp(0.9rem, 1.6vw, 1.4rem);
  font-size: 0.6875rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--accent-link);
}
.presshead h1 { max-width: 18ch; }
.presshead__lead {
  margin: clamp(1.1rem, 2vw, 1.75rem) 0 0; max-width: 52ch;
  font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.25rem);
  line-height: 1.55; color: var(--ink-soft);
}

/* The contact as a rule across the page. Four items on one line at desk
   width, stacked on a phone. It states the same three facts the card did
   without taking the headline's room. */
.presshead__contact {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: 0.6rem clamp(1.25rem, 3vw, 3rem);
  margin-top: clamp(2rem, 3.5vw, 3.25rem);
  padding-top: clamp(1.25rem, 2vw, 1.75rem);
  border-top: 2px solid var(--ink);
}
.presshead__label {
  margin: 0; font-size: 0.6875rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-soft);
  flex-basis: 100%;
}
.presshead__who {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.15rem, 1rem + 0.6vw, 1.5rem); line-height: 1.2;
}
.presshead__who span {
  display: block; margin-top: 0.3rem; font-family: var(--font-body);
  font-size: 0.8125rem; color: var(--ink-soft);
}
.presshead__contact a {
  color: var(--accent-link); text-decoration: none; font-size: 0.9375rem;
  display: inline-block; padding-block: 0.3rem;
  border-bottom: 1px solid transparent;
  transition: border-color var(--duration) var(--ease-out-expo);
}
.presshead__contact a:hover { border-bottom-color: var(--accent); }

/* ------------------------------------------------------------- the router */

/* A press page is not read, it is raided. Somebody arrives needing one
   specific thing, so the three things they come for are stated once, before
   any prose, as the page's actual navigation. */
.grab {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
  border-top: 1px solid var(--hairline);
}
.grab__item {
  display: flex; flex-direction: column;
  padding: clamp(1.4rem, 1.2rem + 1vw, 2rem) 0 clamp(1.4rem, 1.2rem + 1vw, 2rem);
  text-decoration: none; color: inherit;
  border-top: 2px solid transparent;
  margin-top: -1px;
  transition: border-color 380ms var(--ease-out-expo);
}
.grab__item + .grab__item { border-left: 1px solid var(--hairline); padding-left: clamp(1rem, 2vw, 2rem); }
.grab__item:hover { border-top-color: var(--accent); }
.grab__no {
  font-size: 0.6875rem; letter-spacing: 0.12em; color: var(--accent-link);
  font-variant-numeric: tabular-nums;
}
.grab__title {
  display: block; margin-top: 0.85rem;
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 1.15rem + 0.9vw, 1.9rem);
  line-height: 1.12; letter-spacing: -0.02em;
}
.grab__note {
  display: block; margin-top: 0.8rem; flex: 1;
  font-size: 0.9375rem; line-height: 1.55; color: var(--ink-soft);
  text-wrap: pretty;
}
.grab__go {
  display: block; margin-top: 1.25rem;
  font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--accent-link);
  transition: transform var(--duration) var(--ease-out-expo);
}
.grab__item:hover .grab__go { transform: translateX(3px); }

@media (max-width: 720px) {
  .grab__item + .grab__item { border-left: 0; padding-left: 0; }
}


/* ---------------------------------------------------- footer, two fixes */

/* ⚠ The megafooter's CTA was still `display: block`, so `gap` did nothing and
   the label sat hard against the brass token. Same class of bug as the
   `.footer a` override, in a component the earlier fix did not reach. */
.megafooter a.btn, .footer a.btn, .megafooter__cta a.btn {
  display: inline-flex !important;
  align-items: center;
}

/* "Site by" belongs beside the Culture Cartel mark, not stacked above it.
   The anchor was display:block, so the label and the logo fell onto separate
   lines. */
.footer a.footer__by {
  display: inline-flex; align-items: center; gap: 0.6rem;
  min-height: 32px; padding-block: 0.3rem;
}
.footer__by span { white-space: nowrap; }
.footer__by img { display: block; }


/* The agency mark in the press contact rule. Sits at the end of the line, at
   the same optical weight as the phone number rather than as a second logo. */
.presshead__ta {
  display: inline-flex; align-items: center;
  margin-left: auto; padding-block: 0.3rem;
  opacity: 0.62;
  transition: opacity var(--duration) var(--ease-out-expo);
}
.presshead__ta:hover { opacity: 1; }
.presshead__ta img {
  display: block; width: clamp(104px, 9vw, 138px); height: auto;
  /* The supplied file is white artwork for dark grounds; this page is paper,
     so it is inverted to ink rather than shipping a second file. */
  filter: invert(1) brightness(0.35);
}
@media (max-width: 860px) { .presshead__ta { margin-left: 0; } }

/* ============================================================ the article ===

   A long-form reading page.

   Two rounds of failure before this, and the second is the instructive one.
   Round one ran the prose in a right-hand column with a six-item sidebar
   permanently alongside it. Round two removed the sidebar, which was right,
   but then centred EVERY element on the same 34rem measure: the header, the
   meta, the artwork, the sign-off, the share row, the onward cards and the
   back button. On a wide screen that is a thin ribbon of content down the
   middle of a mostly empty page, and nothing has any hierarchy because
   nothing differs in width from anything else.

   What a good long-form page actually does is vary the width deliberately:

     text      ~68 characters, the only width tuned for reading
     header    wider, so the title can be set with confidence
     artwork   wider still, and a pull quote breaks out of the measure
     footer    wider again, because cards and controls are not prose

   Those four widths are the whole layout system here.
   ------------------------------------------------------------------------ */

.art {
  --measure: 38rem;              /* ~68 characters at the body size below */
  --wide:    50rem;              /* header, sign-off, share */
  --broad:   62rem;              /* artwork, onward cards */
  padding-bottom: clamp(3rem, 2rem + 4vw, 6rem);
}

.art > * {
  width: min(var(--measure), calc(100% - var(--gutter) * 2));
  margin-inline: auto;
}

/* ------------------------------------------------------------------ header */

.art__head {
  width: min(var(--wide), calc(100% - var(--gutter) * 2));
  padding-top: clamp(2.75rem, 2rem + 3.5vw, 5.5rem);
}
.art__kicker {
  margin: 0 0 clamp(1rem, 2vw, 1.5rem);
  font-size: 0.6875rem; letter-spacing: 0.18em; text-transform: uppercase;
}
.art__kicker a {
  color: var(--accent-link); text-decoration: none;
  display: inline-block; padding-block: 0.45rem;
}
.art__kicker a:hover { text-decoration: underline; }

.art__title {
  margin: 0; max-width: 17ch;
  font-size: clamp(2.4rem, 1.5rem + 3.6vw, 4.4rem);
  line-height: 1.0; letter-spacing: -0.03em;
}
/* The standfirst is the writer's own first line, so it gets real presence:
   near reading size, in ink rather than grey, on a comfortable measure. */
.art__standfirst {
  margin: clamp(1.25rem, 2.2vw, 1.9rem) 0 0; max-width: 46ch;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1.1rem + 0.85vw, 1.7rem);
  line-height: 1.4; color: var(--ink); text-wrap: pretty;
}

.art__meta {
  display: flex; align-items: center; gap: 0.9rem;
  margin-top: clamp(1.75rem, 3vw, 2.5rem);
  padding-top: clamp(1.25rem, 2vw, 1.75rem);
  border-top: 1px solid var(--hairline);
}
.art__face {
  width: 2.9rem; height: 2.9rem; border-radius: 999px;
  object-fit: cover; flex: none;
}
.art__by {
  margin: 0; font-family: var(--font-display); font-size: 1.125rem;
  line-height: 1.25;
}
.art__by span {
  display: block; margin-top: 0.25rem; font-family: var(--font-body);
  font-size: 0.8125rem; color: var(--ink-soft);
}

/* --------------------------------------------------------------- the audio */

/* Five of the seventeen pieces are recorded, and that is the thing this
   journal has that a speaker's blog normally does not. So the control is a
   full bar directly under the byline, where a reader decides whether to read
   or to listen, rather than a small box tucked beside the author's name. */
.hear {
  width: min(var(--wide), calc(100% - var(--gutter) * 2));
  display: flex; align-items: center; gap: clamp(0.9rem, 2vw, 1.4rem);
  margin: clamp(1.5rem, 2.5vw, 2rem) auto 0;
  padding: clamp(0.9rem, 1.6vw, 1.25rem) clamp(1.1rem, 2vw, 1.5rem);
  background: var(--bone);
  border-left: 3px solid var(--accent);
  border-radius: 2px;
}
.hear__play {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3rem; height: 3rem; flex: none;
  border: 0; border-radius: 999px; cursor: pointer;
  background: var(--accent); color: var(--on-accent);
  transition: transform var(--duration) var(--ease-out-expo),
              background var(--duration) var(--ease-out-expo);
}
.hear__play:hover { transform: scale(1.05); background: var(--accent-link); }
/* The glyph is drawn in CSS so play and pause can swap without a second
   asset or an icon font. */
.hear__icon {
  width: 0; height: 0; margin-left: 3px;
  border-left: 12px solid currentColor;
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
}
.hear.is-playing .hear__icon {
  width: 12px; height: 14px; margin-left: 0;
  border: 0; border-left: 4px solid currentColor;
  border-right: 4px solid currentColor;
  background: transparent;
}
.hear__main { flex: 1; min-width: 0; }
.hear__label {
  margin: 0 0 0.5rem; font-size: 0.875rem; color: var(--ink);
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline;
}
/* Stated plainly: these are read by a narrator, not by Michael. A listener
   who assumes it is his voice and then realises it is not has been misled by
   the page, so the page says so up front. */
.hear__read {
  font-size: 0.75rem; color: var(--ink-soft);
}
.hear__track {
  position: relative; height: 5px; border-radius: 999px;
  background: rgba(17, 29, 46, 0.12); cursor: pointer;
}
.hear__track:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.hear__fill {
  position: absolute; inset: 0 auto 0 0; width: 0;
  border-radius: 999px; background: var(--accent);
}
.hear__time {
  margin: 0; flex: none; font-size: 0.8125rem; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.hear__sep { opacity: 0.5; }

@media (max-width: 560px) {
  .hear { flex-wrap: wrap; }
  .hear__time { margin-left: auto; }
}

/* ---------------------------------------------------------------- artwork */

.art__hero {
  width: min(var(--broad), calc(100% - var(--gutter) * 2));
  margin: clamp(2.5rem, 4.5vw, 4rem) auto;
  overflow: clip;
}
.art__hero img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 2 / 1; object-fit: cover; background: var(--ground);
}

/* ------------------------------------------------------------------- body */

.art__body {
  font-size: clamp(1.125rem, 1.05rem + 0.4vw, 1.3125rem);
  color: var(--ink);
}
.art__body p {
  margin: 0 0 1.35em; line-height: 1.75; text-wrap: pretty;
}
.art__body > p:first-of-type { font-size: 1.1em; line-height: 1.65; }

/* ⚠ The drop cap is scoped to the FIRST PARAGRAPH. It was previously
   inherited by the pull quote too, which put an enormous letter in the middle
   of a quotation and read as a rendering fault. */
.art__body > p:first-of-type::first-letter {
  float: left; font-family: var(--font-display);
  font-size: 3.6em; line-height: 0.8;
  padding: 0.04em 0.1em 0 0; color: var(--accent-link);
}

/* The key line, set large IN PLACE. It breaks out of the reading measure so
   the eye registers a change of gear, and because it is the paragraph itself
   rather than a copy of it, the reader never meets the same sentence twice. */
.art__beat {
  margin: clamp(2.25rem, 4vw, 3.25rem) 0 !important;
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 1.15rem + 1.2vw, 2rem) !important;
  line-height: 1.3 !important; letter-spacing: -0.018em;
  color: var(--ink);
  padding-left: clamp(1.1rem, 2.2vw, 1.75rem);
  border-left: 3px solid var(--accent);
}
.art__beat::first-letter { font-size: inherit; float: none; padding: 0; }

.art__pull {
  width: min(var(--wide), calc(100vw - var(--gutter) * 2));
  margin: clamp(2.5rem, 4.5vw, 3.75rem) 0;
  margin-left: calc((min(var(--wide), 100vw - var(--gutter) * 2) - 100%) / -2);
  padding-left: clamp(1.25rem, 2.5vw, 2rem);
  border-left: 3px solid var(--accent);
}
.art__pull p {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.45rem, 1.2rem + 1.3vw, 2.15rem);
  line-height: 1.28; letter-spacing: -0.018em; color: var(--ink);
  text-wrap: pretty;
}
.art__pull p::first-letter { font-size: inherit; float: none; padding: 0; }

/* -------------------------------------------------------------------- end */

.art__end {
  width: min(var(--broad), calc(100% - var(--gutter) * 2));
  margin-top: clamp(3rem, 5vw, 5rem);
  padding-top: clamp(2rem, 3vw, 3rem);
  border-top: 1px solid var(--hairline);
}
.art__end .signoff { margin-top: 0; padding-top: 0; border-top: 0; }
.art__end .share { margin-top: clamp(2rem, 3vw, 3rem); }
/* The newsletter at the foot of every piece: the moment a reader has just
   finished one is the moment they are most likely to want the next. */
.art__signup {
  margin-top: clamp(2rem, 3vw, 3rem);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  background: var(--bone);
  border-radius: 18px;
}
.art__signuplead {
  margin: 0 0 1rem;
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 1.1rem + 1vw, 1.75rem);
  line-height: 1.2;
}
.art__end .onward { margin-top: clamp(2rem, 3vw, 3rem); }
.art__back { margin: clamp(2rem, 3vw, 3rem) 0 0; }

/* The article kicker link ("Kutch's Kommunications") lost its target when the
   header was rebuilt: the old `.article__kicker a` selector no longer matches. */
/* 27px at 0.3rem padding, one pixel short of the 28px floor. The kicker is
   set in 11px uppercase, so the padding has to do all the work. */
.art__kicker a { display: inline-block; padding-block: 0.45rem; }

/* ------------------------------------------------- fast facts, copyable */

/* A producer is not reading this section, they are lifting things off it. The
   difference between a fact they can take and one they retype by hand is the
   whole usefulness of the block. */
.spec__val { position: relative; }
.spec__copy {
  display: inline-flex; align-items: center; gap: 0.35rem;
  margin-left: 0.7rem; padding: 0.45rem 0.7rem; min-height: 28px;
  border: 0; border-radius: 999px; cursor: pointer;
  background: transparent; color: var(--accent-link);
  font-family: var(--font-body); font-size: 0.6875rem;
  letter-spacing: 0.08em; text-transform: uppercase;
  box-shadow: inset 0 0 0 1px var(--hairline);
  /* Present but quiet until the row is engaged. Hidden entirely would make it
     undiscoverable; always loud would make six of them shout down the facts. */
  opacity: 0; transform: translateY(2px);
  transition: opacity var(--duration) var(--ease-out-expo),
              transform var(--duration) var(--ease-out-expo),
              box-shadow var(--duration) var(--ease-out-expo);
}
.spec__row:hover .spec__copy,
.spec__copy:focus-visible { opacity: 1; transform: none; }
.spec__copy:hover { box-shadow: inset 0 0 0 1px var(--accent); }
.spec__copy.is-done { opacity: 1; transform: none; color: var(--ink); }
/* Keyboard users never hover, so the control is always reachable for them. */
@media (hover: none) { .spec__copy { opacity: 1; transform: none; } }

.spec__foot {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 1rem clamp(1.25rem, 3vw, 2rem);
  margin-top: clamp(1.5rem, 2.5vw, 2.25rem);
}
.spec__note {
  margin: 0; font-size: 0.875rem; color: var(--ink-soft); max-width: 44ch;
}

/* ------------------------------------------------------- agency credit */

/* Says who a producer is actually dealing with, rather than parking a logo at
   the end of a phone number. */
.agency {
  display: flex; align-items: center; gap: clamp(1.25rem, 3vw, 2.25rem);
  flex-wrap: wrap;
  margin-top: clamp(1.5rem, 2.5vw, 2.25rem);
  padding: clamp(1.1rem, 1.8vw, 1.5rem) clamp(1.25rem, 2vw, 1.75rem);
  background: var(--bone);
  border-left: 2px solid var(--accent);
}
.agency__mark {
  display: inline-flex; align-items: center; flex: none;
  padding-block: 0.3rem; opacity: 0.72;
  transition: opacity var(--duration) var(--ease-out-expo);
}
.agency__mark:hover { opacity: 1; }
.agency__mark img {
  display: block; width: clamp(112px, 10vw, 150px); height: auto;
  /* Supplied as white artwork for dark grounds; this sits on bone. */
  filter: invert(1) brightness(0.3);
}
.agency__note {
  /* ⚠ max-width, not just a flex basis. `flex: 1 1 22rem` sets where the item
     STARTS, not where it stops, so on a wide display this ran to 155
     characters a line. */
  margin: 0; flex: 1 1 22rem; min-width: 0; max-width: 42rem;
  font-size: 0.9375rem; line-height: 1.6; color: var(--ink-soft);
}
.agency__note strong { color: var(--ink); font-weight: 500; }

/* ====================================================================
   MOBILE TYPE FLOOR

   Measured, not guessed: design/mobile_audit.mjs walks every page at
   320/360/375/390/430 and reports any element carrying a sentence that
   computes below 15px. This block answers those reports.

   ⚠ It deliberately does NOT touch eyebrows, labels or the uppercase
   micro type. Small uppercase with wide tracking is a design device
   here and it reads fine at 12px; inflating it would flatten the
   hierarchy the whole site is built on. Only running prose is raised.

   ⚠ Must stay LAST in this file. Every rule it overrides has the same
   specificity (0,1,0 or 0,1,1), so source order is what decides the
   winner. Adding anything below this re-breaks it silently.
   ==================================================================== */

@media (max-width: 860px) {
  /* Sentences. 15px is the comfortable floor on a handset held at arm's
     length; below it people zoom, and zooming breaks every fixed and
     sticky element on the page. */
  .cta-note,
  .field__hint,
  .asset__meta,
  .row__meta,
  .case__who,
  .cov__note,
  .book__cover p,
  .video__missing,
  .panel__more,
  .agency__note,
  figcaption {
    font-size: 0.9375rem;
    line-height: 1.55;
  }

  /* The keynote panel's audience line is a clamp whose lower bound was
     tuned for desktop. Raise the floor only; the upper bound is fine. */
  .panel__aud {
    font-size: clamp(0.9375rem, 0.86rem + 0.3vw, 1.0625rem);
  }

  /* Form controls: iOS zooms the whole page when a focused input is
     under 16px, which then leaves the layout scrolled sideways with no
     way back. This is the one place 16px is a functional requirement
     rather than a comfort one. */
  input, select, textarea {
    font-size: 1rem;
  }

  /* The 3D book's cover type scales with the book, so it goes tiny on a
     phone where the book itself is small. Give it a floor. */
  .book3d__front .tba {
    font-size: 0.8125rem;
  }

  /* Footer legal ran wider than its column at 320 and clipped. */
  .footer__legal {
    font-size: 0.8125rem;
    line-height: 1.5;
    overflow-wrap: anywhere;
  }
}

/* ====================================================================
   MOBILE TOUCH TARGETS AND NARROW-WIDTH OVERFLOW

   Second half of the mobile pass. Same ordering rule as the block above:
   this must stay at the end of the file.

   The numbers come from design/mobile_audit.mjs, which measures every
   interactive element on all 28 pages at 320/360/375/390/430. A finger
   pad is about 9mm, which is 44 CSS px. Anything smaller is a control
   people miss, and missing a control in a footer feels like a broken
   site rather than a small target.

   ⚠ Height is bought with PADDING, never with `height`. These controls
   sit in flex rows next to type, and a fixed height would either clip
   a wrapped label or push the row's baseline off.
   ==================================================================== */

@media (max-width: 860px) {

  /* --- footer navigation ------------------------------------------
     Measured 21px tall. The list gap is removed as the padding takes
     over the spacing job, so the footer grows by far less than
     doubling each row would suggest. */
  .footer ul { gap: 0; }
  .footer a:not(.btn) {
    padding: 0.72rem 0;
    /* The hover nudge slides the box, so the tappable area has to be
       the thing that moves, not just the text inside it. */
    display: block;
  }

  /* --- footer base row --------------------------------------------
     At 320 the legal line and the Culture Cartel credit sat on one
     flex row with `white-space: nowrap` on the credit, so the row was
     277px of content in a 272px column and clipped. Stacking them is
     the honest fix; squeezing the credit would shrink the wordmark
     below the point where it can be read. */
  /* ⚠ `align-items: stretch`, NOT `flex-start`. flex-start shrink-wraps
     each child to its content, which collapsed this column to 154px and
     then the legal line overflowed its own container by 16px. Stretch
     keeps every child the full width of the footer, which is what a
     stacked base row wants anyway. */
  .footer__base {
    flex-direction: column;
    align-items: stretch;
    gap: 1.25rem;
  }
  /* ⚠ The legal line's links are PROSE, not nav rows.
     An earlier rule gives `.footer a` `width: 100%` below 640px, which is
     right for a stacked column of navigation links and wrong here: it made
     each inline link fill the paragraph, forced a line break around it, and
     pushed the paragraph 16px past its own container on every page.
     `:not(.btn)` matches the specificity of the rule being overridden
     (0,2,1) so source order decides, and this block is later. */
  .footer__legal a:not(.btn) {
    display: inline;
    width: auto;
    padding-block: 0;
  }
  .footer__by { min-height: 44px; }

  /* --- raising the earlier 24px floor to 44px on handsets -----------
     ⚠ These three selectors are copied EXACTLY from the "touch targets"
     block earlier in this file, and that is deliberate rather than
     sloppy. That block was written to WCAG 2.2's 24px minimum and set
     hard floors of 30px and 32px. `.share a, .share button` scores
     0,1,1 and beat every 0,1,0 class selector I could write here, and
     its `min-height: 30px` was also overriding `.btn`'s own 46px.

     Matching the selector exactly makes specificity a tie, which hands
     the decision to source order, which this block wins. Scoped to the
     handset so the desktop composition is untouched. */
  .share a, .share button { min-height: 44px; }
  .footer a.footer__by { min-height: 44px; }
  .masthead > a:first-child { min-height: 44px; }
  .btn { min-height: 46px; }

  .share__net { padding: 0.7rem 0; }
  .share__row { gap: 0.5rem 1.25rem; }

  /* --- the copy control on the press fact list --------------------- */
  .spec__copy { min-height: 44px; padding: 0.6rem 0.9rem; }

  /* --- buttons ------------------------------------------------------
     .btn already declares min-height: 46px and holds it. What fails at
     320 is width: `.btn__label` is `white-space: nowrap`, so a long
     label like "Check Michael's availability" measured 277px inside a
     272px column and pushed out of its container.
     Releasing the nowrap only at the narrowest widths keeps the label
     on one line everywhere it fits, which is what the nowrap was for. */
  .btn { max-width: 100%; }
}

@media (max-width: 380px) {
  .btn__label { white-space: normal; }
  .btn {
    padding-left: 1rem;
    gap: 0.9rem;
    /* A wrapped label needs its arrow token to stay vertically centred
       against two lines, not sit against the first. */
    align-items: center;
  }
}

/* ====================================================================
   MOBILE, THIRD PASS
   Everything below answers a specific line in out/mob4.txt.
   ==================================================================== */

@media (max-width: 860px) {

  /* The masthead home link wraps only the logo, so its width is the
     logo's width: 39px at 375. Height was already fixed; a target has
     to clear 44 on BOTH axes. */
  .masthead > a:first-child { min-width: 44px; justify-content: flex-start; }

  /* Footer legal and the audio player's own microcopy were the last
     prose under 15px. Legal small print at 13px is a convention worth
     breaking on a handset: it carries the copyright line and the two
     policy links, which are exactly what someone reaches for when they
     are already struggling to read. */
  .footer__legal { font-size: 0.9375rem; }
  .hear__label   { font-size: 0.9375rem; }
  .hear__time    { font-size: 0.9375rem; }

  /* iOS zooms the page when a focused field is under 16px. One field
     was still slipping through at 15px because its own selector is
     more specific than the bare element rule above. */
  .signup__input, .field input, .field textarea, .field select {
    font-size: 1rem;
  }
}

/* --- the audio player, below 560 ------------------------------------
   `.hear` is a flex row: a 48px play button, then everything else in a
   `flex: 1; min-width: 0` column. min-width: 0 is what lets a flex item
   shrink below its content, and on a narrow article column that column
   collapsed to 40px holding 48px of label, so the duration overlapped
   the title.

   Stacking is the right answer rather than shrinking: an audio player
   is a control, and the scrubber wants the full width of the column to
   be draggable with a thumb. */
@media (max-width: 560px) {
  .hear {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas:
      "play main"
      "track track";
    gap: 0.75rem 1rem;
    align-items: center;
  }
  .hear__play  { grid-area: play; }
  .hear__main  { grid-area: main; min-width: 0; }
  .hear__track { grid-area: track; width: 100%; }
  .hear__label { margin-bottom: 0.2rem; }
}

/* ------------------------------------------------- masthead on small phones
   Measured at 320: logotype + menu button + 195px CTA came to 355px of
   content in a 320px header. Flex resolved it by shrinking the logotype to
   39px, so the mark was both illegible AND still overflowing.

   The CTA moves into the menu panel instead. Two copies exist in the markup
   and exactly one is ever displayed, so there is only ever one control. */

.nav__cta { display: none; }

/* ⚠ 520, not 420. The first attempt used 420 and simply moved the overflow
   to the next size up: at 430 the header still needed 451px for 430px of
   room. The real constraint is arithmetic, so it should be solved with
   arithmetic. Widest handset in the test set is 430; the header needs
   132 (wordmark) + 44 (menu) + 195 (CTA) + 48 (two gaps) + 48 (padding)
   = 467px to hold all three, so anything narrower than about 500 cannot.
   520 clears every phone with margin for font rendering differences. */
@media (max-width: 520px) {
  /* Header: wordmark and menu only. */
  .masthead > .btn { display: none; }
  .masthead { gap: 0.75rem; padding-inline: 1rem; }

  /* The panel copy takes over, full width so it reads as the primary action
     rather than an afterthought at the bottom of a list. */
  .masthead__nav.is-open .nav__cta { display: block; width: 100%; margin-top: 1.4rem; }
  /* ⚠ centre, NOT space-between. Stretched to the full panel width, the
     label sat hard against the left edge and the arrow token hard against
     the right, with 200px of empty pill between them: it read as a broken
     layout rather than a button. Full width is kept for the tap target; the
     label and its token travel together in the middle. */
  .masthead__nav.is-open .nav__cta .btn {
    width: 100%; justify-content: center; padding-inline: 1.2rem;
  }
  /* The panel's own edge has to line up with the masthead's, or the links sit
     8px inboard of the wordmark directly above them. */
  .masthead__nav.is-open { padding-inline: 1rem; }
}

/* ⚠ The logotype must never be flex-shrunk. It is a wordmark: below about
   110px the letterforms stop resolving and it reads as a smudge. Locking
   flex-shrink means a future layout change overflows visibly instead of
   quietly degrading the identity, which is the failure mode you want. */
.masthead > a:first-child { flex: 0 0 auto; }
.masthead__logo { flex: 0 0 auto; min-width: 110px; }

/* ====================================================================
   MOBILE, FOURTH PASS
   Long strings, remaining targets, remaining prose sizes.
   ==================================================================== */

@media (max-width: 860px) {

  /* --- unbreakable strings ------------------------------------------
     Every remaining horizontal overflow traced to one cause: an email
     address or a URL is a single word with no break opportunity in it,
     and "stephanie@taspeakersmanagement.com" is 36 characters that
     cannot wrap. At 320 that is wider than the column, so it ran out
     of its container on the contact, press, privacy and terms pages.

     `anywhere` rather than `break-word`: break-word will not break a
     word that is alone on its line, which is exactly this case. */
  .faq__a, .faq__a p,
  .legal p, .legal li,
  .presshead__contact, .presshead__contact a,
  .contactcard, .contactcard a,
  .contact__col, .contact__col a {
    overflow-wrap: anywhere;
  }

  /* --- the last prose under 15px ------------------------------------ */
  .cov__foot,
  .spec__note,
  .jx__themenote,
  .jx__listen,
  .jlead__meta,
  figcaption {
    font-size: 0.9375rem;
  }

  /* --- the last small targets ---------------------------------------
     Press page contact details measured 35px. These are the two things
     a producer on a deadline actually reaches for, so they should be
     the easiest things on the page to hit, not the hardest. */
  .presshead__contact a { min-height: 44px; display: inline-flex; align-items: center; }

  /* Journal theme filters measured 42px, two short of the mark. */
  .jx__themelink, .jx__theme a, .jx__themes a { min-height: 44px; }

  /* Every form field, everywhere. 24px tall was a real control on the
     book and contact pages. */
  input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]),
  select, textarea {
    min-height: 44px;
    font-size: 1rem;      /* under 16px and iOS zooms the whole page */
  }
}

/* ====================================================================
   MOBILE, FINAL PASS
   ==================================================================== */

@media (max-width: 860px) {
  /* The journal theme jump chips measured 42px, two short. Reached by
     `.jx__jump a` because the anchors carry no class of their own; the
     earlier attempt guessed at class names that do not exist. */
  .jx__jump a { min-height: 44px; align-items: center; }

  /* The pull-quote attribution. `.quote figcaption` scores 0,1,1 and
     beat the bare `figcaption` in the earlier block, so the selector
     has to match to win on source order. */
  .quote figcaption { font-size: 0.9375rem; }
}

/* ------------------------------------------------ grid min-content overflow
   The last two overflows at 320 were both CSS grids, and both had the same
   cause. A grid item's default `min-width` is `auto`, not 0, which means it
   refuses to shrink below its own min-content width. One long unbreakable
   word inside the item therefore widens the TRACK, which widens the grid,
   which overflows the container: the contact form measured 328px inside a
   272px column, and the topics hero 313px.

   `minmax(0, ...)` on the track declarations already guards the columns that
   were written with it. These are the items that were not. */

@media (max-width: 860px) {
  .form, .form > *, .form .field,
  .topichero, .topichero > * {
    min-width: 0;
  }

  /* And the actual pressure: a heading long enough to set the min-content
     width of its own track. Breaking it is preferable to overflowing. */
  .topichero h1, .form label, .field label {
    overflow-wrap: break-word;
  }
}

/* --------------------------------------------- the last 320px overflow
   `.split` and `.field--pair` both collapse to `grid-template-columns: 1fr`
   on mobile, and `1fr` is shorthand for `minmax(auto, 1fr)`. That `auto`
   minimum is the trap: the track refuses to go below the min-content width
   of its contents, so the contact form sat at 328px inside a 272px column
   and pushed the section, the shell and main out with it.

   `minmax(0, 1fr)` says the track may shrink to nothing, which lets the
   contents wrap instead of the container overflowing. The two-column
   declarations in this file already use minmax(0, ...) for exactly this
   reason; the single-column collapses were written as bare `1fr` and
   inherited the bug. */

@media (max-width: 860px) {
  .split, .split--middle { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 560px) {
  .field--pair { grid-template-columns: minmax(0, 1fr); }
}

/* The contact page's aside holds the email address as running prose rather
   than inside one of the card components covered earlier, so it needed
   naming too. A 36-character address has no break opportunity in it and is
   wider than a 272px column on its own. */
@media (max-width: 860px) {
  .stack, .stack p, .stack a,
  .prose, .prose p, .prose a, .prose li {
    overflow-wrap: anywhere;
  }
}



/* ------------------------------------------------------------ book: the tour
   Values lifted verbatim from the inline styles they replace, so desktop is
   pixel-identical. Only the handset composition below is new. */
.tour__soon { justify-self: center; }
.tour__soonword {
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(3.2rem, 6.5vw, 7.5rem); line-height: 0.95;
  color: var(--ground-2); -webkit-text-stroke: 1px rgba(239, 229, 213, 0.3);
}
.tour__soonnote {
  margin-top: 0.75rem; font-size: var(--text-micro); letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--accent-soft); text-align: center;
}

@media (max-width: 860px) {
  /* The two-column split stacks, and the decorative block was left centred
     while the heading, the paragraph and the button above it were all ranged
     left. Three different alignments in one section is what read as out of
     sync, and the outlined "Soon." floating between the button and the note
     had nothing holding it.

     On a handset it becomes a plaque instead: ranged left with everything
     else, sitting on its own rule, with the note as its label. It now reads
     as the status of the tour rather than as a stray piece of decoration. */
  .tour__soon {
    justify-self: stretch;
    display: flex; align-items: baseline; gap: 0.9rem; flex-wrap: wrap;
    margin-top: clamp(1.75rem, 5vw, 2.5rem);
    padding-top: clamp(1.25rem, 4vw, 1.75rem);
    border-top: var(--rule) solid var(--hairline-dark);
  }
  .tour__soonword {
    font-size: clamp(2.6rem, 15vw, 3.6rem);
    /* The hairline stroke reads as a smudge once the type is this small. */
    -webkit-text-stroke: 0;
    color: var(--accent-soft);
  }
  .tour__soonnote { margin-top: 0; text-align: left; }

  /* "The same method he brings to the stage" sat hard under the 3D book with
     only the grid gap between them. */
  .book .stack { padding-top: clamp(1.75rem, 6vw, 2.75rem); }
}

/* ============================================================ mobile fixes
   Round two, all measured against the reported behaviour.
   ======================================================================= */

@media (max-width: 860px) {

  /* --- 1. the hero -------------------------------------------------
     He was 292px wide sitting at x 96-388 of a 390px viewport: hard
     against the right edge, starting 343px down, with the copy's paper
     gradient washing over him. He read as a sticker in the corner
     rather than as the subject of the page.

     Desktop gives him 30% of the viewport width, centre-right, with the
     copy ranged left in front. On a single column the equivalent is
     centred and much taller, rising from behind the headline. */
  .stagehero { min-height: 94vh; }
  .stagehero__figwrap {
    height: min(70vh, 640px);
    left: 50%;
    --fig-x: -50%;
  }
  /* The gradient starts lower and clears sooner, so his torso and face
     are in open air instead of behind a wash of paper. */
  .stagehero__copy {
    background: linear-gradient(to top, var(--paper) 58%,
                                rgba(242, 238, 230, 0.86) 78%, transparent);
  }

  /* --- 2. the stakes cutout ----------------------------------------
     ⚠ On desktop the figure sits in the RIGHT column (measured at
     969px of 1440) with its shadow beneath it, also right. On mobile
     `justify-self: start` put Michael on the left while the shadow
     stayed anchored right, so the figure and its own shadow were at
     opposite ends of the section. He moves to the right, over it. */
  .stakes__cutout { justify-self: end; height: 320px; }

  /* --- 3. the menu call to action ----------------------------------
     Full width made it a slab: the pill ran the whole panel and read as
     a bar rather than a button. It now sizes to its content with a
     comfortable minimum, matching the buttons everywhere else. */
  .masthead__nav.is-open .nav__cta { width: auto; }
  .masthead__nav.is-open .nav__cta .btn {
    width: auto;
    min-width: 15rem;
    justify-content: space-between;
    padding-inline: 1.4rem 0.5rem;
  }

  /* --- 4. the journey year, hidden under the masthead ---------------
     ⚠ The stage is `position: sticky; top: 0`, but the masthead is
     sticky at top 0 as well and about 76px tall, so the year sat at
     y=32 UNDERNEATH it and was never visible while scrolling. The
     stage has to begin below the bar. --masthead-h is published by
     initMasthead from the real measured height. */
  .journey__stage {
    top: var(--masthead-h, 76px);
    min-height: calc(100vh - var(--masthead-h, 76px));
    padding-top: 0.5rem;
  }
}

/* --- 5. the menu opened over a dark section --------------------------
   ⚠ Scrolled to the footer, the masthead carries `.is-dark`, which
   paints nav links in --on-ground (cream). The open panel's own ground
   is --paper (also cream), so every menu item was cream on cream and
   the menu appeared empty. The panel brings its own surface, so it has
   to bring its own ink regardless of what the bar behind it is doing. */
.masthead.is-dark .masthead__nav.is-open a { color: var(--ink); }
.masthead.is-dark .masthead__nav.is-open a[aria-current="page"] { color: var(--accent-link); }
.masthead.is-dark .masthead__nav.is-open a::after { background: var(--accent); }
.masthead.is-dark .masthead__nav.is-open .nav__cta .btn { color: var(--ink); }

/* ------------------------------------------- the shelf on a short display
   A 1280x720 laptop has 720px to hold a 105px masthead, a heading, and a
   card that is a 3:2 photograph on top of a body of text. At the panel's
   own minimum width that still came to 807px, so the stage overflowed and
   the heading slid under the masthead at the end of the pin.

   ⚠ Height-only query, and above 860px wide, so it can never reach the
   phone layout or a normal desktop. From 801px tall none of this applies.

   The photograph goes to 16:9 rather than the panel losing its text: a
   slightly wider crop of a stage shot loses nothing, whereas dropping the
   audience line would remove the one sentence a planner is reading for. */
@media (min-width: 861px) and (max-height: 800px) {
  .showcase__stage {
    padding-block: calc(var(--masthead-h, 76px) + 0.4rem) 0.9rem;
    gap: 0.7rem;
  }
  .panel__img { aspect-ratio: 16 / 9; }
  .panel__body { padding: 0.9rem 1.1rem; gap: 0.4rem; }
  .showcase__head h2 { font-size: clamp(1.4rem, 2.4vw, 2rem); }
}

/* ------------------------------------------------- case-file measure
   ⚠ The case studies sit in a wide column with no cap of their own, so on a
   2560px monitor each paragraph ran to about 104 characters. Long lines are
   the specific failure of a wide screen: the eye loses its place tracking
   back to the start of the next one. 62 characters is the target here, and
   the cap is generous enough that nothing reflows below 1512px. */
.casefile__body p,
.casefile__body li { max-width: 62ch; }

/* ------------------------------------------------------------------ round 1
   The planner-risk lead-in (homepage, item 1C).

   Sentence case, not an uppercase label. The mock-up sets this as a spoken
   line above the headline rather than a category tag, and it reads as one:
   shouting it in caps turns a sentence into a breadcrumb. It sits at body
   size so it clears the mobile type floor without an exemption. */
.risk__eyebrow {
  margin: 0 0 clamp(0.75rem, 0.6rem + 0.5vw, 1.15rem);
  color: var(--ink-soft);
  font-size: clamp(0.9375rem, 0.9rem + 0.25vw, 1.0625rem);
  line-height: 1.5;
  max-width: 44ch;
}

/* ------------------------------------------------------------- round 1 hero
   The stage hero (homepage, items 1A and P2).

   Scoped to .stagehero--stage throughout, so every rule the original cutout
   hero relies on is still sitting above this block untouched. Restoring the
   old hero is deleting a class from the markup, not unpicking a stylesheet.
   The original is also kept whole in the client folder under
   "Hero V1 - original cutout hero".

   Composition. Michael stands in the left third of the graded frame and
   gestures right; the copy column sits in that gesture. The scrim is a
   one-directional wash rather than a flat overlay so the right-hand type gets
   its contrast without putting a grey film over his face. */
.stagehero--stage {
  position: relative;
  display: grid;
  align-items: end;
  /* ⚠ THE SECTION TAKES THE PICTURE'S SHAPE, rather than the picture being
     cropped to the section. Jarryd, 24 Sept, on the frame he supplied: make
     it fit to height, he likes seeing the tops of the audience's heads.
     Those heads run along the very bottom edge of this frame and Michael's
     scalp sits about 11% down from the top, so the two things a `cover` crop
     eats first are exactly the two things the frame was chosen for. Carrying
     its 1672:941 means there is almost nothing to crop at any width.

     The cap is still needed. Without it a 2560-wide window would open on a
     1441px hero and the rest of the page would begin below the fold. With it
     the overflow is 2% at 1440x900 and 7.5% at 2560x1440, and
     object-position below decides which 7.5%.

     --masthead-h is published by initMasthead and has a static fallback in
     :root, so this is correct before script runs too. svh, not vh, so the
     mobile browser chrome collapsing does not resize the hero mid-scroll. */
  min-height: 0;
  aspect-ratio: 1672 / 941;
  max-height: calc(100svh - var(--masthead-h, 107px));
  /* ⚠ width:100% is load-bearing, not a tidy-up. With `aspect-ratio` set and
     the width left auto, a max-height that bites makes the box preserve its
     ratio by SHRINKING ITS WIDTH: at 2560x1440 the hero came out 2368px wide
     and the page background showed as a cream strip down the right-hand side.
     Pinning the width makes height the thing that gives, which is what
     max-height is there to clamp and what object-position below is there to
     aim. */
  width: 100%;
  container-type: inline-size;
  padding: 0;
  overflow: hidden;
  background: var(--ground);
  isolation: isolate;
}

.stagehero--stage .stagehero__plate {
  position: absolute; inset: 0; z-index: 0;
  display: block;
}
.stagehero--stage .stagehero__plate img {
  width: 100%; height: 100%;
  /* ⚠ object-fit must be named. The default is `fill`, which silently
     stretches the frame to the slot and distorts him. That exact default cost
     an afternoon on the mobile pass. */
  object-fit: cover;
  /* ⚠ 85%, and it flipped from 14% when the frame changed on 24 Sept.
     The old frame had no audience in it, so the only thing to protect was the
     top of Michael's head and the crop was pulled upward. This frame has the
     audience along the bottom edge, which is the reason Jarryd chose it, and
     his scalp about 11% down from the top. The section above now carries the
     picture's own ratio, so there is only ever a few percent to lose, and 85%
     spends almost all of it on the empty ceiling above him rather than on the
     heads: at the worst case measured, 2560 x 1440, that is 6.4% off the top
     and 1.1% off the bottom, leaving roughly 4.6% of air above his hair and
     the audience intact.
     ⚠ Do not raise this back toward the old value without also removing the
     aspect-ratio above. The two numbers only make sense together. */
  object-position: 50% 85%;
  /* ⚠ 7 Oct, Stephanie on a 14 inch laptop: his head was cut off. A short,
     wide window (1366 x 610 with tabs and a bookmarks bar) caps the hero at
     two thirds of the frame's height, and 85% of that bite came off the top,
     past his scalp (12.5% down). So the top loss is now capped at 8% of the
     picture, which keeps his whole head, and anything beyond that comes off
     the bottom, where the audience heads run from 60% down. On a tall window
     the cap never bites and this is the same 85% as above.
     cqw is the section's width (container-type on .stagehero--stage); the
     picture is that wide and 56.28cqw tall whenever the hero is clamped. */
  object-position: 50% calc(-1 * min(
    0.85 * (56.28cqw - min(56.28cqw, 100svh - var(--masthead-h, 107px))),
    4.5cqw));
  /* and max-width:none, because the global img rule would otherwise fight the
     absolutely positioned parent and collapse the plate */
  max-width: none;
}

.stagehero--stage .stagehero__scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  /* ⚠ Softened on Jarryd's note of 17 Sept. The previous ramp hit 86% by the
     midpoint and 97% at the edge, which read as a navy panel with a
     photograph behind it rather than a photograph. More stops over a longer
     run gets the same contrast where the words actually are while leaving the
     room visible behind them. Verified after the change, not assumed: the
     headline reads 11.8:1 and the subhead 8.4:1 against the ground. */
  background:
    linear-gradient(100deg,
      transparent 0%,
      color-mix(in srgb, var(--ground) 14%, transparent) 26%,
      color-mix(in srgb, var(--ground) 48%, transparent) 44%,
      color-mix(in srgb, var(--ground) 74%, transparent) 60%,
      color-mix(in srgb, var(--ground) 88%, transparent) 78%,
      color-mix(in srgb, var(--ground) 93%, transparent) 100%),
    linear-gradient(to top,
      color-mix(in srgb, var(--ground) 62%, transparent) 0%,
      transparent 40%);
}

/* The name. Same device as the original hero: display letterforms on their own
   parallax rate. It bleeds off the bottom-left so it reads as a title card
   under the figure rather than a caption floating beside him. */
.stagehero--stage .stagehero__name {
  position: absolute; z-index: 2;
  /* ⚠ top and right MUST be reset here. The original hero's .stagehero__name
     sets top and right, and an absolutely positioned box given top AND bottom
     resolves by top, while left AND right stretch it to full width. Leaving
     them inherited put a 1458px translucent wordmark straight across his face.
     Setting bottom/left without clearing the opposite pair is the bug. */
  top: auto; right: auto;
  left: calc(var(--gutter) * -0.28); bottom: -0.1em;
  width: auto;
  margin: 0; pointer-events: none; text-align: left;
  font-family: var(--font-display);
  /* Smaller than the original hero's. There it sat on empty cream and could
     own the screen; here it shares the frame with a photograph, and a wordmark
     big enough to cross his face reads as a watermark rather than a title. */
  font-size: clamp(2.5rem, 0.5rem + 6vw, 7rem);
  line-height: 0.86;
  /* Capitals, to match the logotype. Negative tracking is for lowercase
     display type; set across capitals it closes the counters and the word
     turns into a block. Positive, and slightly tighter than the logotype's
     own, because this is set far larger. */
  letter-spacing: 0.005em;
  color: color-mix(in srgb, var(--on-ground) 18%, transparent);
}
.stagehero--stage .stagehero__name .row2 {
  color: color-mix(in srgb, var(--accent) 30%, transparent);
}

.stagehero--stage .stagehero__copy {
  position: relative; z-index: 3;
  grid-row: auto;
  /* ⚠ Three inherited properties have to be undone here, and each one was a
     separate wrong render before it was.
       grid-template-columns  the original copy block is a TWO column grid,
                              headline left and CTA right. Left alone, the
                              stacked layout below silently keeps two columns.
       margin-inline: auto    beats justify-self. The column looked centred no
                              matter what alignment was asked for, because a
                              real auto margin resolves before box alignment.
       max-width/width:100%   sizes the column to the page, not to the copy. */
  grid-template-columns: minmax(0, 1fr);
  margin-inline: 0;
  justify-self: end;
  width: min(52ch, 48%);
  max-width: none;
  display: grid; justify-items: start;
  gap: clamp(0.9rem, 0.7rem + 0.7vw, 1.4rem);
  padding: 0 var(--gutter) clamp(3.5rem, 2.5rem + 5vw, 7rem);
  color: var(--on-ground);
}
.stagehero--stage .stagehero__copy h1 {
  margin: 0;
  color: var(--on-ground);
  font-size: clamp(2.1rem, 1.05rem + 3.1vw, 4rem);
  line-height: 1.04;
  letter-spacing: -0.015em;
  max-width: 15ch;
}
.stagehero--stage .stagehero__copy h1 em {
  font-style: italic;
  color: var(--accent-soft);
}
.stagehero--stage .stagehero__sub {
  margin: 0; max-width: 42ch;
  color: color-mix(in srgb, var(--on-ground) 84%, transparent);
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.1875rem);
  line-height: 1.55;
}
.stagehero--stage .stagehero__aside {
  /* the original aside is right-aligned because it sat in the second column */
  display: flex; flex-direction: column; align-items: flex-start;
  justify-self: start; text-align: left;
  gap: 0.85rem;
  margin-top: clamp(0.4rem, 0.25rem + 0.5vw, 0.9rem);
}
.stagehero--stage .cta-note {
  max-width: 34ch;
  color: color-mix(in srgb, var(--on-ground) 70%, transparent);
}

/* Entrance. Mirrors the original hero's timing so the page still opens the
   way it did: name, then figure, then copy. */
.js .stagehero--stage .stagehero__plate {
  animation: figure-in 900ms var(--ease-out-expo) 120ms both;
}
.js .stagehero--stage .stagehero__copy > * {
  animation: rise 640ms var(--ease-out-expo) both;
}
.js .stagehero--stage .stagehero__copy > :nth-child(1) { animation-delay: 340ms; }
.js .stagehero--stage .stagehero__copy > :nth-child(2) { animation-delay: 420ms; }
.js .stagehero--stage .stagehero__copy > :nth-child(3) { animation-delay: 500ms; }

/* ---- Tablet and below: the copy stops being a right-hand column. ---- */
@media (max-width: 1024px) {
  .stagehero--stage .stagehero__copy { width: min(46ch, 66%); }
  .stagehero--stage .stagehero__name { font-size: clamp(4rem, 1rem + 11vw, 9rem); }
}

/* ---- Handsets. The tall crop puts Michael centre frame, so the copy moves
   underneath him and the scrim flips to a bottom-up wash. A right-hand column
   at 390px is four words wide and unreadable. ---- */
@media (max-width: 860px) {
  /* ⚠ ON A PHONE THE PICTURE STACKS ABOVE THE COPY instead of sitting behind
     it, and this replaced a portrait art-direction crop rather than an
     oversight. Until 24 Sept a 1400 x 2000 hero-stage-mk-tall filled the
     handset frame with Michael's face. The new source is 1672 x 941, so the
     same portrait crop out of it is 677px wide, which a 390px phone at 3x
     would have had to magnify 1.4x, and it would have had to cut either his
     head or the audience. Stacked, the whole frame shows at its own ratio,
     nothing is magnified, and the copy sits on the section's own navy where
     it needs no scrim at all. */
  .stagehero--stage {
    aspect-ratio: auto;
    max-height: none;
    min-height: 0;
    display: block;
    padding-bottom: clamp(2.5rem, 2rem + 4vw, 4rem);
  }
  /* ⚠ 5:4, not the frame's own 16:9, and the crop is horizontal ONLY.
     Stacked at its native ratio the picture is 219px tall on a 390px phone
     and Michael is about 90px of that, which is a photograph of a room with
     a man in it rather than a photograph of him. 5:4 is taller than the
     frame, so `cover` scales to height: the full height survives, which is
     what keeps his scalp and the audience heads, and the crop comes out of
     the width instead. Anchored at 0% because he stands at 31% across and
     the left edge is dead curtain; that puts him around 44% of the visible
     window, near enough to centre, and costs nothing that matters. */
  .stagehero--stage .stagehero__plate {
    position: static;
    display: block;
    aspect-ratio: 5 / 4;
  }
  .stagehero--stage .stagehero__plate img { object-position: 0% 50%; }

  /* ⚠ The old hero raises PAPER under the copy on handsets, so the headline
     can be read over a cutout on cream. Inherited here it painted a cream
     gradient over the bottom of a navy hero and rendered cream text on cream:
     the headline was not missing, it was invisible. The scrim already does
     this job, in the right colour. */
  .stagehero--stage .stagehero__copy { background: none; }

  /* The wordmark is dropped on handsets, not shrunk. The tall crop fills the
     frame with his face, the masthead logo is already saying MICHAEL KUTCHER
     six rows above it, and a translucent second wordmark across a portrait
     reads as a watermark. */
  .stagehero--stage .stagehero__name { display: none; }
  /* The scrim went with the overlay. Nothing is behind the type any more, so
     a gradient here would only put a navy film over the photograph above it. */
  .stagehero--stage .stagehero__scrim { display: none; }
  .stagehero--stage .stagehero__copy {
    width: 100%;
    margin-left: 0;
    padding-top: clamp(1.75rem, 1.4rem + 2vw, 2.5rem);
    /* minmax(0,...) not 1fr: 1fr is minmax(auto,1fr) and refuses to shrink
       below min-content, which is how a single-column collapse overflows */
    grid-template-columns: minmax(0, 1fr);
    padding-bottom: clamp(2rem, 1.2rem + 4vw, 3.5rem);
  }
  .stagehero--stage .stagehero__copy h1 { max-width: 20ch; }
}

/* --------------------------------------------- showcase, medium-height band
   Round one made the keynote card shelf 22px taller. The cause is content,
   not layout: "A Diffability True™ Perspective on Inclusion and Belonging"
   became "...on Human Potential and Belonging", which wraps to one more line
   in the panel. On a 900px-tall window that pushed the sticky stage to 922px,
   and a stage taller than its own pin cannot sit still: it scrolls, dragging
   its heading under the masthead at the end of the travel.

   The existing short-display block already solves this below 801px, but it is
   deliberately aggressive (it re-crops the panel photography to 16:9). Between
   801 and 940 the shortfall is 22px, so only the padding and the gap need to
   give. The photograph and the type are left alone.

   ⚠ Scoped with min-height: 801px so it cannot fight the block above, which
   owns everything shorter and sets the same two properties to tighter values. */
@media (min-width: 861px) and (min-height: 801px) and (max-height: 940px) {
  .showcase__stage {
    padding-block: calc(var(--masthead-h, 105px) + 0.4rem) 1rem;
    gap: 1rem;
  }
}

/* =========================================================== round 1: the kit
   The public media kit (items 4D and 4E).

   Deliberately NOT a card grid. This is reference material, and reference
   material reads as a ledger: numbered sections, hairline-separated rows, the
   action parked in the same place on every line so the eye stops hunting for
   it. The page it replaces was a default asset grid behind a password, which
   is the exact shape the anti-template rule exists to prevent.
   ------------------------------------------------------------------------ */

.pagehead__eyebrow {
  margin: 0 0 0.75rem; font-size: 0.75rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--accent-link);
}

/* A text link that carries its own arrow. Used across the kit pages. */
.arrowlink {
  display: inline-flex; align-items: baseline; gap: 0.45em;
  color: var(--accent-link); text-decoration: none;
  border-bottom: var(--rule) solid transparent;
  transition: border-color 160ms var(--ease-out-expo),
              gap 160ms var(--ease-out-expo);
}
.arrowlink:hover, .arrowlink:focus-visible {
  border-bottom-color: currentColor; gap: 0.75em;
}
.arrowlink--onDark { color: var(--accent-soft); }
.arrowlink--back:hover, .arrowlink--back:focus-visible { gap: 0.75em; }

/* ---- the in-page section nav ---- */
.kitnav {
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem;
  margin-top: clamp(1.5rem, 1rem + 2vw, 2.5rem);
  padding-top: clamp(1.25rem, 1rem + 1vw, 1.75rem);
  border-top: var(--rule) solid var(--hairline);
}
.kitnav a {
  color: var(--ink-soft); text-decoration: none;
  font-size: 0.9375rem;
  border-bottom: var(--rule) solid transparent;
  padding-block: 0.35rem;          /* a real touch target, not a bare word */
}
.kitnav a:hover, .kitnav a:focus-visible {
  color: var(--ink); border-bottom-color: var(--accent);
}

/* ---- a section ---- */
.kitsec { padding-block: clamp(3rem, 2rem + 4vw, 6rem); }
.kitsec + .kitsec { border-top: var(--rule) solid var(--hairline); }
.kitsec__inner { display: grid; gap: clamp(1.75rem, 1rem + 3vw, 3.5rem); }
@media (min-width: 900px) {
  .kitsec__inner { grid-template-columns: minmax(0, 22rem) minmax(0, 1fr); }
  .kitsec__head { position: sticky; top: calc(var(--masthead-h, 105px) + 2rem);
                  align-self: start; }
}
.kitsec__no {
  display: block; font-family: var(--font-display);
  font-size: clamp(2.5rem, 1.5rem + 3vw, 4rem); line-height: 1;
  color: var(--accent); opacity: 0.55; margin-bottom: 0.4rem;
}
.kitsec__head h2 { margin: 0; font-size: var(--text-title); }
.kitsec__lead { margin: 0.9rem 0 0; color: var(--ink-soft); max-width: 40ch; }
.kitsec__body { min-width: 0; }
.kitsec__list { list-style: none; margin: 0; padding: 0; }
.kitsec__more { margin: 1.5rem 0 0; }

/* ---- an asset row ---- */
.asset--row {
  display: grid; gap: 0.75rem 1.5rem;
  align-items: center;
  padding-block: clamp(1.1rem, 0.9rem + 0.6vw, 1.5rem);
  border-top: var(--rule) solid var(--hairline);
  /* minmax(0,...) not 1fr: 1fr is minmax(auto,1fr) and will not shrink below
     its content, which is how a two-column row overflows a phone. */
  grid-template-columns: minmax(0, 1fr);
}
.asset--row:last-child { border-bottom: var(--rule) solid var(--hairline); }
@media (min-width: 620px) {
  .asset--row { grid-template-columns: minmax(0, 1fr) auto; }
}
.asset__text { min-width: 0; }
.asset__name {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.0625rem, 1rem + 0.3vw, 1.25rem); color: var(--ink);
}
.asset__meta {
  margin: 0.15rem 0 0; font-size: 0.8125rem; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--accent-link);
}
.asset__note { margin: 0.4rem 0 0; color: var(--ink-soft); max-width: 52ch; }
.asset__actions { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.asset__act { flex: 0 0 auto; }

/* Pending. Visibly a different thing, and it says who owes it.
   ⚠ The row emits no href at all in this state, so it cannot silently become
   a working link that 404s in front of a client. */
.asset--pending .asset__name { color: var(--ink-soft); }
.asset__wait {
  display: inline-flex; align-items: center; gap: 0.5em;
  padding: 0.5rem 0.95rem;
  font-size: 0.8125rem; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--ink-soft);
  border: var(--rule) dashed var(--hairline); border-radius: 999px;
}
.asset__wait::before {
  content: ""; width: 0.5em; height: 0.5em; border-radius: 50%;
  background: var(--accent); opacity: 0.6;
}

/* ---- the photography grid ---- */
.shots {
  display: grid; gap: clamp(1rem, 0.7rem + 1.5vw, 2rem);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
  margin-top: clamp(1.5rem, 1rem + 2vw, 2.5rem);
}
.shot { margin: 0; }
.shot img {
  width: 100%;
  /* ⚠ height: auto is load bearing and its absence is invisible in the CSS.
     build.py's add_dimensions() writes real width and height ATTRIBUTES onto
     every img. With width:100% and no CSS height, the attribute height wins
     and aspect-ratio is ignored: the boxes computed to 242 x 1200 while
     DevTools cheerfully reported aspect-ratio: 3 / 4 as applied. Setting
     height:auto hands sizing back to the ratio. */
  height: auto;
  aspect-ratio: 3 / 4; object-fit: cover;
  background: var(--bone);
  border-radius: 2px;
}
.shot--wide img { aspect-ratio: 3 / 2; }
.shot__name { margin: 0.7rem 0 0; font-family: var(--font-display); color: var(--ink); }
.shot__note { margin: 0.2rem 0 0; font-size: 0.875rem; color: var(--ink-soft); }
.shot__links { display: flex; gap: 0.9rem; margin-top: 0.5rem; }
.shot__links a {
  font-size: 0.8125rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent-link); text-decoration: none;
  border-bottom: var(--rule) solid var(--hairline);
  padding-block: 0.35rem;
}
.shot__links a:hover, .shot__links a:focus-visible {
  border-bottom-color: currentColor;
}

/* ---- brand page ---- */
.marks {
  display: grid; gap: clamp(1rem, 0.7rem + 1.5vw, 1.75rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  margin-top: clamp(1.5rem, 1rem + 2vw, 2.5rem);
}
.mark { margin: 0; padding: clamp(1.5rem, 1rem + 2vw, 2.5rem); border-radius: 2px; }
.mark img { width: 100%; height: auto; display: block; }
.mark figcaption {
  margin-top: 1rem; font-size: 0.875rem;
  display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: baseline;
}
.mark figcaption a {
  font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase;
  text-decoration: none; border-bottom: var(--rule) solid currentColor;
  padding-block: 0.35rem;
}
.mark--light { background: var(--paper); border: var(--rule) solid var(--hairline); }
.mark--light figcaption { color: var(--ink-soft); }
.mark--light figcaption a { color: var(--accent-link); }
.mark--dark { background: var(--ground); }
.mark--dark figcaption { color: color-mix(in srgb, var(--on-ground) 80%, transparent); }
.mark--dark figcaption a { color: var(--accent-soft); }
.mark--mid { background: var(--bone); border: var(--rule) solid var(--hairline); }
.mark--mid figcaption { color: var(--ink-soft); }
.mark--mid figcaption a { color: var(--accent-link); }

.swatches {
  list-style: none; margin: clamp(1.5rem, 1rem + 2vw, 2.5rem) 0 0; padding: 0;
  display: grid; gap: 0;
}
.swatch {
  display: grid; align-items: center; gap: 0.35rem 1.25rem;
  grid-template-columns: auto minmax(0, 1fr);
  padding-block: 0.9rem;
  border-top: var(--rule) solid var(--hairline);
}
.swatch:last-child { border-bottom: var(--rule) solid var(--hairline); }
@media (min-width: 720px) {
  .swatch { grid-template-columns: 2.5rem 12rem 6rem minmax(0, 1fr); }
}
.swatch__chip {
  width: 2.5rem; height: 2.5rem; border-radius: 2px;
  border: var(--rule) solid var(--hairline);
}
.swatch__name { font-family: var(--font-display); color: var(--ink); }
.swatch__hex {
  font-family: var(--mono, ui-monospace, monospace); font-size: 0.875rem;
  color: var(--accent-link); background: none; cursor: pointer;
  border: 0; border-bottom: var(--rule) solid var(--hairline);
  padding: 0.4rem 0; text-align: left; justify-self: start;
}
.swatch__hex:hover, .swatch__hex:focus-visible { border-bottom-color: currentColor; }
.swatch__note { color: var(--ink-soft); font-size: 0.9375rem; }

.typespec {
  display: grid; gap: clamp(1.5rem, 1rem + 2vw, 3rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  margin-top: clamp(1.5rem, 1rem + 2vw, 2.5rem);
}
.typespec__face { margin: 0; font-size: clamp(2rem, 1.2rem + 3vw, 3.5rem);
                  line-height: 1.1; color: var(--ink); }
.typespec__use { margin: 0.6rem 0 0; color: var(--ink-soft); max-width: 36ch; }

.dont h3 { font-size: clamp(1.2rem, 1rem + 1vw, 1.75rem); font-style: italic; }

/* ------------------------------------------------- round 1 kit: touch + type
   The mobile audit found 22 issues on the three new kit pages and every one
   was mine. Two classes of mistake, both worth naming.

   TAP TARGETS. padding-block alone does not make a target: an inline link at
   14px with 0.35rem of padding measures 31px tall, and the floor is 44. These
   are the controls a planner uses on a phone while standing in a venue, so
   they are the last place to be stingy. min-height plus inline-flex centring
   gives a real box without changing how any of it looks on a desktop.

   TYPE. 14px captions are fine on a laptop and below this site's mobile floor.
   They go to 15px, which is also simply easier to read.

   ⚠ Selectors repeat the full chain rather than being grouped loosely, so
   specificity ties are decided by source order here and not by accident
   somewhere above. */
.kitnav a,
.shot__links a,
.mark figcaption a,
.swatch__hex {
  display: inline-flex; align-items: center;
  min-height: 44px;                 /* the floor, not a suggestion */
  padding-block: 0;
}
.shot__links { gap: 1.25rem; margin-top: 0.15rem; }
.mark figcaption { align-items: center; }

.shot__note,
.mark figcaption,
.swatch__note { font-size: 0.9375rem; }

/* The hex button keeps its underline as an affordance, but the underline now
   sits under the text rather than under a 44px box, which would float. */
.swatch__hex {
  border-bottom: 0; position: relative;
}
.swatch__hex::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 30%;
  border-bottom: var(--rule) solid var(--hairline);
}
.swatch__hex:hover::after, .swatch__hex:focus-visible::after {
  border-bottom-color: currentColor;
}

/* Height alone was not enough: "Web", "Print" and "PNG" are short words, so
   they cleared 44px vertically and still measured 29 to 42 wide. A tap target
   is a box, not a line. min-width finishes the job, and centring the text
   inside it means the underline reads as a small deliberate control rather
   than a rule that overshoots the word. */
.shot__links a,
.mark figcaption a {
  min-width: 44px; justify-content: center; padding-inline: 0.35rem;
}

/* Landscape frames need a wider column than portraits. At the portrait track
   width a room shot came out 242 x 161, which is a thumbnail of a room rather
   than a picture of one, and the whole point of this set is that a planner can
   see which room looks like theirs. */
.shots--wide {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
}



/* ========================================================= round 1: the desk
   Meeting Planners, the booking contact.

   Replaces a flat rule carrying four items plus a detached beige bar holding
   the TA mark. Those two read as unrelated furniture and made the agency look
   like a footnote to a phone number. This is one card: who represents him on
   the left, who to actually contact on the right, on navy so it anchors the
   page under the headline instead of drifting in it.
   ------------------------------------------------------------------------ */
.desk {
  margin-top: clamp(2rem, 1.5rem + 3vw, 3.5rem);
  display: grid;
  /* minmax(0,...) so the two columns can actually shrink on a tablet */
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  background: var(--ground);
  color: var(--on-ground);
  border-radius: 3px;
  overflow: clip;
}
@media (min-width: 880px) {
  .desk { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); }
}

.desk__agency {
  padding: clamp(1.6rem, 1.2rem + 2vw, 2.75rem);
  display: grid; align-content: start; gap: 1.1rem;
  background: color-mix(in srgb, var(--on-ground) 5%, transparent);
}
.desk__mark { display: inline-block; width: fit-content; }
.desk__mark img { display: block; width: clamp(150px, 14vw, 200px); height: auto; }
.desk__note {
  margin: 0; max-width: 40ch;
  color: color-mix(in srgb, var(--on-ground) 78%, transparent);
  font-size: 0.9375rem; line-height: 1.6;
}
.desk__note strong { color: var(--on-ground); font-weight: 600; }

.desk__person {
  padding: clamp(1.6rem, 1.2rem + 2vw, 2.75rem);
  display: grid; align-content: start; gap: 0.35rem;
}
.desk__label {
  margin: 0 0 0.5rem; font-size: 0.6875rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--accent-soft);
}
.desk__who {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2.1rem); line-height: 1.1;
}
.desk__role {
  margin: 0; color: color-mix(in srgb, var(--on-ground) 72%, transparent);
  font-size: 0.9375rem;
}
.desk__actions {
  margin-top: 1.25rem;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.9rem 1.5rem;
}
.desk__tel {
  color: var(--on-ground); text-decoration: none;
  font-size: 1.0625rem;
  /* a real target, not a bare line of text */
  display: inline-flex; align-items: center; min-height: 44px;
  border-bottom: var(--rule) solid color-mix(in srgb, var(--accent) 60%, transparent);
}
.desk__tel:hover, .desk__tel:focus-visible { border-bottom-color: var(--accent); }
.desk__mail { margin: 0.9rem 0 0; }
.desk__mail a {
  color: color-mix(in srgb, var(--on-ground) 76%, transparent);
  font-size: 0.9375rem; text-decoration: none;
  display: inline-flex; align-items: center; min-height: 44px;
  border-bottom: var(--rule) solid transparent;
  /* the address wraps rather than pushing the card wide on a phone */
  overflow-wrap: anywhere;
}
.desk__mail a:hover, .desk__mail a:focus-visible {
  color: var(--on-ground); border-bottom-color: var(--accent-soft);
}

/* --------------------------------------------- round 1: the brand guide block
   Media kit, section 05. The way in to the brand page.

   Each lockup sits on the ground it is FOR, which is the whole lesson: a
   planner looking at three identical swatches learns nothing, and a planner
   looking at the reversed mark on navy has already understood the rule. */
.brandguide {
  display: grid; gap: clamp(1.5rem, 1rem + 2.5vw, 2.75rem);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 900px) {
  .brandguide { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .brandguide__marks { grid-column: 1 / -1; }
}
.brandguide__marks {
  display: grid; gap: clamp(0.75rem, 0.5rem + 1.2vw, 1.25rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}
.brandcard {
  display: grid; gap: 0.35rem; align-content: start;
  padding: clamp(1.1rem, 0.9rem + 1vw, 1.6rem);
  border-radius: 2px; text-decoration: none;
  border: var(--rule) solid transparent;
  transition: transform 200ms var(--ease-out-expo);
}
.brandcard:hover, .brandcard:focus-visible { transform: translateY(-2px); }
.brandcard img { width: 100%; height: auto; display: block; margin-bottom: 0.6rem; }
.brandcard__name { font-family: var(--font-display); font-size: 1.0625rem; }
.brandcard__use { font-size: 0.875rem; }
.brandcard--light { background: var(--paper); border-color: var(--hairline); }
.brandcard--light .brandcard__name { color: var(--ink); }
.brandcard--light .brandcard__use { color: var(--ink-soft); }
.brandcard--dark { background: var(--ground); }
.brandcard--dark .brandcard__name { color: var(--on-ground); }
.brandcard--dark .brandcard__use {
  color: color-mix(in srgb, var(--on-ground) 72%, transparent);
}
.brandcard--mid { background: var(--bone); border-color: var(--hairline); }
.brandcard--mid .brandcard__name { color: var(--ink); }
.brandcard--mid .brandcard__use { color: var(--ink-soft); }

.brandguide__label {
  margin: 0 0 0.9rem; font-size: 0.6875rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--accent-link);
}
.brandchips { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.brandchips li {
  display: grid; align-items: center; gap: 0.9rem;
  grid-template-columns: 1.75rem minmax(0, 1fr) auto;
  padding-block: 0.7rem;
  border-top: var(--rule) solid var(--hairline);
}
.brandchips li:last-child { border-bottom: var(--rule) solid var(--hairline); }
.brandchip {
  width: 1.75rem; height: 1.75rem; border-radius: 2px;
  border: var(--rule) solid var(--hairline);
}
.brandchip__n { font-family: var(--font-display); color: var(--ink); }
.brandchip__h {
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 0.875rem; color: var(--ink-soft);
}
.brandguide__go { display: grid; gap: 1.1rem; align-content: start; }
.brandguide__go p { margin: 0; color: var(--ink-soft); max-width: 40ch; }
.brandguide__go .btn { justify-self: start; }


/* The interlude img is now wrapped in <picture> so a band can carry a separate
   handset frame. <picture> is an inline box by default and would collapse the
   absolutely positioned image inside it. */
.interlude picture { display: contents; }

.kitblock picture { display: contents; }
@media (max-width: 860px) {
  /* The portrait crop puts him centre-right; the copy sits above it. */
  .kitblock__bg { object-position: 62% 30%; }
  /* ⚠ The desktop scrim runs left to right, which is right when the copy is a
     left-hand column and wrong when it is stacked on top. On a phone the type
     sat across the brightest part of the frame, a lit projection screen. This
     one runs top to bottom instead: cover where the words are, clear where
     Michael is. */
  .kitblock::after {
    background: linear-gradient(to bottom,
      color-mix(in oklab, var(--ground) 92%, transparent) 0%,
      color-mix(in oklab, var(--ground) 78%, transparent) 46%,
      color-mix(in oklab, var(--ground) 30%, transparent) 78%,
      color-mix(in oklab, var(--ground) 10%, transparent) 100%);
  }
}



/* ⚠ The booking fallback addresses wrap rather than push the form wide.
   stephanie@taspeakersmanagement.com is 34 unbroken characters and a 320px
   phone gives the form a 272px column, so the default no-break-inside-a-word
   behaviour overflowed it by 17px. An email address is exactly the kind of
   long token that needs `anywhere`, and breaking one across lines is normal
   and readable; a horizontally scrolling booking form is not. */
.field__hint { overflow-wrap: anywhere; }

/* ---- the full testimonials page ---- */
/* -------------------------------------------------------- the ledger of all
   ⚠ COLUMNS, not a row grid, and that is the whole fix.

   This was `repeat(auto-fill, minmax(22rem, 1fr))` with `align-items: start`.
   The quotes run from 18 words to 62, so every row resolved to a different
   height, every column ended on a different line, and the last row held three
   of four cells. Jarryd, 18 Sept: "completely scattered around." He was
   describing the grid's arithmetic, not its spacing, and no amount of gap or
   padding could have answered it.

   Column flow has no rows to reconcile: an entry that runs long simply takes
   more of its column and the next one starts under it. There is also no final
   row left half empty, because there is no final row. */
.saidledger {
  columns: 2;
  column-gap: clamp(2.5rem, 2rem + 3vw, 5rem);
  /* A rule down the middle, so the page reads as one ledger with a spine
     rather than two lists that happen to be side by side. */
  column-rule: var(--rule) solid var(--hairline);
}
@media (max-width: 900px) { .saidledger { columns: 1; } }

.saidrow {
  /* A quote split from its attribution across a column boundary would be
     worse than the grid this replaces, so the entry is atomic. */
  break-inside: avoid;
  margin: 0 0 clamp(2rem, 1.6rem + 1.6vw, 3.25rem);
  padding-top: clamp(1.4rem, 1.1rem + 1.2vw, 2.25rem);
  border-top: var(--rule) solid var(--hairline);
  display: grid; gap: 0.9rem;
}
.saidrow__no {
  font-family: var(--font-display);
  font-size: 0.8125rem; letter-spacing: 0.1em;
  color: var(--accent);
}
.saidrow blockquote {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.3125rem);
  line-height: 1.45; color: var(--ink); text-wrap: pretty;
}
.saidrow blockquote::before { content: "“"; color: var(--accent); }
.saidrow figcaption { display: grid; gap: 0.15rem; }
.saidrow__name { font-weight: 600; color: var(--ink); }
.saidrow__role { font-size: 0.875rem; color: var(--ink-soft); }
/* ⚠ `.saidrow .orgmark`, not `.orgmark--row`. The modifier alone is the same
   specificity as the base rule and is declared EARLIER in this file, so the
   base's `justify-content: center` won and every mark sat centred in a
   left-aligned ledger. Two classes settles it wherever the blocks end up.

   The treatment is the one from "Planners who had to get it right", which is
   what Jarryd asked for on 19 Sept: a hairline above it, the mark set large
   enough to actually read, in its own colours rather than greyscale, held
   back so a row of them is evidence rather than a row of competing adverts,
   and restored on hover. The greyscale 29px version it replaces was sized
   for the marquee, where the marks repeat and scroll; here each one appears
   once and has to carry a name. */
.saidrow .orgmark {
  justify-content: flex-start;
  margin-top: clamp(0.7rem, 0.55rem + 0.5vw, 1rem);
  padding-top: clamp(0.6rem, 0.5rem + 0.4vw, 0.85rem);
  border-top: 1px solid var(--hairline);
  width: 100%; max-width: 15rem;
  min-height: 0;
}
.saidrow .orgmark img {
  height: clamp(34px, 3.2vw, 46px); max-width: 100%;
  filter: none; opacity: 0.72;
  mix-blend-mode: multiply;
}
.saidrow:hover .orgmark img { opacity: 1; }
/* ⚠ The typographic lockup has to hold the SAME slot, because twelve of the
   fifteen organisations are not in the logo library and are not ours to draw.
   Matching the mark's height keeps the ledger's rhythm: every entry ends on
   the same rule at the same depth, whether we hold the logo or not. */
/* ⚠ Set as a MARK, not as a caption. Jarryd asked three times for a logo on
   every testimonial and the answer is still that twelve of the fifteen do not
   exist anywhere I can reach: not in the repo, not in the client's own asset
   library, not on Michael's current website's logo wall, and not in a logo
   registry. All four were checked on 19 September.

   What was making that gap visible was not the absence of an image, it was
   the WEIGHT. A 46px logo beside 13px grey uppercase text reads as one row
   finished and the next one not. Set in the display serif at the same optical
   presence, on the same rule, at the same height, the two treatments read as
   one system: some organisations are shown as their mark, some as their name.
   Swap in a real logo the day it arrives and nothing else moves. */
.saidrow .orgmark--type {
  display: flex; align-items: center;
  min-height: clamp(34px, 3.2vw, 46px);
  font-family: var(--font-display);
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.1875rem);
  line-height: 1.2; letter-spacing: 0;
  text-transform: none; font-weight: 400;
  color: color-mix(in srgb, var(--ink) 78%, transparent);
}

/* ============================================== round 1: media kit previews
   A strip under each section showing what is actually inside it.

   Section 05 already worked this way: the three lockups sit on the grounds
   they are FOR, so the rule is understood before a word is read. This does
   the same for the other four. A ledger row tells you a file exists; a
   picture of it tells you whether it is the one you want.

   ⚠ Decorative throughout. The row above already carries the real download
   control, so these tiles are not links and the row is aria-hidden: a screen
   reader should not walk five thumbnails of files it was just told about. */
.peek {
  margin-top: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
  padding-top: clamp(1.1rem, 0.9rem + 1vw, 1.6rem);
  border-top: var(--rule) solid var(--hairline);
}
.peek__label {
  margin: 0 0 0.9rem; font-size: 0.6875rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--accent-link);
}
.peek__row {
  display: flex; gap: clamp(0.5rem, 0.4rem + 0.5vw, 0.9rem);
  /* Scrolls rather than wraps or shrinks. Five thumbnails squeezed into a
     390px row are five illegible smudges; a row you can push sideways keeps
     each one big enough to actually recognise. */
  overflow-x: auto;
  padding-bottom: 0.5rem;
  scrollbar-width: thin;
  scroll-snap-type: x proximity;
}
.peek__row > * { scroll-snap-align: start; }
.peek__note { margin: 0.8rem 0 0; font-size: 0.9375rem; color: var(--ink-soft); }

.peektile {
  flex: 0 0 auto;
  width: 7.5rem; height: 9.5rem;
  border: var(--rule) solid var(--hairline);
  border-radius: 2px;
  overflow: hidden;
  background: var(--bone);
  display: block;
  /* The tiles are <button>s now, so the UA's own chrome has to come off:
     padding, font inheritance and the focus ring are all reset here and the
     ring is put back deliberately below. */
  padding: 0; margin: 0; text-align: left; font: inherit; color: inherit;
  cursor: pointer;
  transition: border-color var(--duration) var(--ease-out-expo),
              transform var(--duration) var(--ease-out-expo);
}
button.peektile:hover, button.peektile:focus-visible {
  border-color: var(--accent);
  transform: translateY(-2px);
}
button.peektile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.peektile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.peektile--wide { width: 13rem; }
.peektile--pad {
  display: grid; place-items: center;
  padding: 0.9rem; height: 6rem; width: 11rem;
}
.peektile--pad img { height: auto; object-fit: contain; }
.peektile--light { background: var(--paper); }
.peektile--dark { background: var(--ground); border-color: var(--ground); }
.peektile--mid { background: var(--bone); }

/* A text asset previews as the text itself, small, clipped by its own box.
   ⚠ Not an image of text: a real paragraph at 9px scales with the user's own
   font settings and costs nothing to download. */
.peektile--text {
  width: 11rem; height: 9.5rem;
  background: var(--paper);
  padding: 0.85rem;
  display: grid; align-content: start; gap: 0.3rem;
  font-size: 0.5625rem; line-height: 1.5; color: var(--ink-soft);
  font-family: var(--font-display);
}
.peektile--text span { display: block; }

/* ============================================ round 1: testimonials, editorial
   Ported from 21st.dev "Editorial Testimonial" (jatin-yadav05).

   ⚠ This replaces a three-card deck I built first and got wrong. That deck was
   a card grid: three bordered boxes of uneven height with a portrait floating
   beside them. "No default card grids" is the first line of the standing
   anti-template rule on this project, and it also fought the rest of the site,
   which is editorial throughout.

   The ghosted numeral is not borrowed decoration: the media kit already numbers
   its sections exactly this way, so this reads as part of the same site. */
/* ⚠ Deliberately COMPACT. Jarryd, 18 Sept: "way too big, keep it elegant and
   smaller, I don't want this to be a big section." The first pass set the
   quote at 48px, which turned three sentences into a full screen and made a
   supporting proof point compete with the hero. Social proof earns its place
   by being credible, not by being loud. Everything below is roughly two
   thirds of what it was, and the block is capped so it reads as one tidy
   unit rather than a page-width banner. */
/* ⚠ Centred, and the reason is structural rather than taste. Jarryd, 18 Sept:
   "it is right up on that line, it has no padding and there's an awkward space
   to the right hand side of it." Both were true of the left-aligned version
   and neither was a padding problem. The ghosted numeral sat ON the section
   rule because it was the first thing in the grid with no offset, and a 46ch
   measure inside a 62rem block leaves a column of dead air on the right that
   no padding can fill, because the cause is ragged alignment. A symmetrical
   block has no right-hand void to explain. */
.said {
  display: grid;
  justify-items: center;
  gap: clamp(1.4rem, 1.1rem + 1.2vw, 2.25rem);
  max-width: 46rem;
  /* ⚠ margin-inline, not justify-self. The parent is .shell, a plain block, so
     a self-alignment property would do nothing at all here. That exact mistake
     has been made on this codebase before. */
  margin-inline: auto;
  padding-block-start: clamp(2.25rem, 1.8rem + 2vw, 4rem);
  text-align: center;
}

/* ⚠ The rule that makes hiding mean anything. .said__slide carries a display
   value, and [hidden] is a user-agent rule at the lowest specificity there is,
   so without this every quote renders at once. That exact bug shipped once on
   this section already. */
.said__slide[hidden] { display: none !important; }

.said__body { min-width: 0; width: 100%; }
.said__slide {
  margin: 0; display: grid; justify-items: center;
  gap: clamp(0.9rem, 0.75rem + 0.7vw, 1.4rem);
}

/* The hairline between quote and attribution, dissolved at both ends so it
   reads as a breath rather than a border. Straight from 21st.dev's "Centered
   Testimonial", which is where this whole arrangement comes from. */
.said__rule {
  display: block; width: min(22rem, 70%); height: 1px;
  background: linear-gradient(to right,
    transparent, var(--hairline) 22%, var(--hairline) 78%, transparent);
}

.said__slide blockquote {
  margin: 0;
  /* ⚠ --font-display, not --serif. There is no --serif token on this site and
     there never was: I invented the name and used it in twelve rules, every
     one of which fell back to the inherited sans without a word. An undefined
     custom property is not an error, it is just nothing, so a display serif
     quietly rendering as Inter looks like a design choice rather than a bug.
     Worth checking any new token against :root before trusting it. */
  font-family: var(--font-display);
  /* Display size. This is the one piece of type in the section and it should
     read like a pull quote in a magazine, not like body copy in a box. */
  /* 28px at the top end, not 48. Still display type and still the largest
     thing in the section, but a paragraph rather than a headline. */
  font-size: clamp(1.125rem, 1rem + 0.7vw, 1.75rem);
  line-height: 1.34;
  letter-spacing: -0.006em;
  color: var(--ink);
  max-width: 46ch;
  text-wrap: pretty;
}
.said__slide blockquote::before { content: "“"; color: var(--accent); }

.said__slide figcaption { display: grid; justify-items: center; gap: 0.2rem; }
.said__who {
  font-weight: 600; color: var(--ink);
  font-size: clamp(1rem, 0.95rem + 0.2vw, 1.0625rem);
}
.said__at { color: var(--ink-soft); font-size: 0.9375rem; }
.said__at i { font-style: normal; color: var(--accent); padding-inline: 0.35em; }

/* ------------------------------------------------ the organisation lockup
   Jarryd, 18 Sept: "attach a logo underneath the testimonial provider's name,
   so if it's Brian Walker from Transamerica, it has to hold the logo."

   ⚠ Both branches occupy the same slot at the same height. We hold real marks
   for three of the fifteen organisations and will not draw the other twelve,
   so where there is no logo the name is set as type IN the logo's place. The
   row lines up either way and there is never a hole where a mark should be.
   See ORG_LOGOS in build.py. */
.orgmark {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 2rem;
  margin-top: 0.55rem;
}
.orgmark img {
  height: calc(1.85rem * var(--logo-scale, 1));
  width: auto; max-width: 12rem;
  /* Same reason as the wall: with a set height and a max-width, `fill` would
     stretch a clamped mark rather than fit it. Sub-pixel here rather than the
     33% the wall was showing, but a client's wordmark should be incapable of
     distorting, not merely unlikely to. */
  object-fit: contain;
  filter: grayscale(1); opacity: 0.66;
  mix-blend-mode: multiply;
  transition: opacity var(--duration) var(--ease-out-expo),
              filter var(--duration) var(--ease-out-expo);
}
.orgmark:hover img { opacity: 1; filter: grayscale(0); }
.orgmark--type {
  font-size: 0.8125rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-soft); font-weight: 500;
}

.said__foot {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
  gap: 0.9rem clamp(1rem, 0.8rem + 1vw, 1.75rem);
  padding-top: clamp(1.1rem, 0.9rem + 1vw, 1.75rem);
  width: 100%;
  border-top: var(--rule) solid var(--hairline);
}
/* The controls do nothing without script, so they are not shown; the quote
   and the link to the full page still are. */
.said:not(.is-live) .said__bars,
.said:not(.is-live) .said__count,
.said:not(.is-live) .said__arrows { display: none; }

.said__bars { display: flex; gap: 0.35rem; }
.said__bar {
  display: block; width: 2.25rem; height: 2px;
  background: var(--hairline);
  transition: background-color 220ms var(--ease-out-expo);
}
.said__bar.is-on { background: var(--accent); }

.said__count {
  /* 15px, the mobile type floor. 0.8125rem reads fine on a laptop and is
     13px on a phone, which is under it. "2 of 3" is small type but it is
     still type somebody has to read. */
  margin: 0; font-size: 0.9375rem; letter-spacing: 0.08em;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.said__arrows { display: flex; gap: 0.35rem; }
.saidnav {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;              /* the tap-target floor, exactly */
  border: 0; border-radius: 999px;
  background: none; cursor: pointer;
  font-size: 1.5rem; line-height: 1; color: var(--ink-soft);
  transition: color 160ms var(--ease-out-expo),
              background-color 160ms var(--ease-out-expo);
}
.saidnav:hover, .saidnav:focus-visible {
  color: var(--ink);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.said__all {
  margin-left: auto;
  /* ⚠ A real target. The generic .arrowlink rule gives 44px of height to the
     links in the media kit, but this one sits in a flex row whose align-items
     shrink-wraps it back to its text box: measured 187x28 against a 44px
     floor. Naming the height here is what makes it a box rather than a line,
     and this is the primary way anyone reaches the testimonials page. */
  display: inline-flex; align-items: center; min-height: 44px;
}

/* ------------------------------------------------- the media kit preview
   Jarryd, 18 Sept: the examples clickable, opening "a bubble so it shows a
   preview". A real <dialog>, so Escape, focus trapping and inertness come
   from the browser rather than from an approximation that gets them wrong. */
/* ⚠ `display` ONLY when open, and this is the second time this exact trap has
   cost something on this codebase.

   `dialog:not([open]) { display: none }` is a user-agent rule at the lowest
   specificity there is. `.peekdlg { display: flex }` beats it, so the CLOSED
   dialog was laid out in the page flow: an empty 736x179 cream panel sitting
   above the footer of the media kit, with a close button that did nothing
   because there was nothing open to close. Jarryd reported it twice as "a
   random floating tab that doesn't close in the footer", and he was right
   both times; my first fix addressed the lightbox it was NOT.

   Every test I wrote asked `dialog.open`, which was correctly false the whole
   time. "Is the flag set" and "is it on the screen" are different questions,
   and only the second is what a reader sees. The .said__slide[hidden] bug
   earlier in this build was the same mistake in different clothes. */
.peekdlg {
  border: 0; padding: 0; border-radius: 3px;
  width: min(46rem, 92vw); max-height: 86vh;
  background: var(--paper); color: var(--ink);
  box-shadow: 0 40px 90px -30px rgba(17, 29, 46, 0.55);
  overflow: hidden;
}
.peekdlg[open] { display: flex; flex-direction: column; }
.peekdlg::backdrop { background: rgba(10, 21, 35, 0.72); }
.peekdlg__bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem;
  padding: 1rem clamp(1.1rem, 0.9rem + 1vw, 1.9rem);
  border-bottom: var(--rule) solid var(--hairline);
}
.peekdlg__title {
  margin: 0; font-size: 0.75rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-soft);
}
/* ⚠ 44px, not 30. The audit measured this at 30x30 on every handset width:
   below the minimum tap target, on the only control that closes the preview.
   A reader who cannot reliably shut a modal is trapped in it. */
.peekdlg__x {
  border: 0; background: transparent; cursor: pointer;
  font-size: 1.4rem; line-height: 1; color: var(--ink-soft);
  width: 44px; height: 44px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  margin-right: -0.5rem;
}
.peekdlg__x:hover, .peekdlg__x:focus-visible { color: var(--accent-link); }
.peekdlg__body {
  overflow: auto;
  padding: clamp(1.25rem, 1rem + 1.4vw, 2.25rem);
}

/* ------------------------------------------------ an image preview
   ⚠ A LIGHTBOX, not a card with a picture in it, and no caption bar at all.

   Jarryd, 19 Sept: "take away the captions on top of the images", and the
   same view is what he called "a random floating tab that doesn't close".
   Both were the same object. It was a 736px cream slab with a grey strip on
   top reading THIS IMAGE (the default label, because the logotype tiles were
   emitted without one), the wordmark bleeding edge to edge underneath. A
   portrait in it sat between two wide cream margins. Nothing in that frame
   said lightbox, so it read as a panel that had appeared by accident.

   Now the panel itself is invisible: the photograph sits on the backdrop with
   nothing around it. Only a logotype keeps a card, because a mark needs a
   ground and a reversed lockup is white. */
.peekdlg--img {
  width: auto; max-width: none;
  background: transparent; box-shadow: none; overflow: visible;
}
.peekdlg--img[open] { display: grid; justify-items: center; gap: 0; }
.peekdlg--img::backdrop { background: rgba(8, 17, 29, 0.9); }
.peekdlg--img .peekdlg__bar { border: 0; padding: 0; height: 0; }
.peekdlg--img .peekdlg__title { display: none; }
/* ⚠ Fixed to the viewport, not to the panel. The panel's height changes with
   every image, so anchoring the close control above it put the button under
   the masthead on a tall portrait. Top right of the screen is where a reader
   looks for it and it is in the same place every time. */
.peekdlg--img .peekdlg__x {
  position: fixed; top: max(1rem, env(safe-area-inset-top, 0px)); right: 1rem;
  color: var(--on-ground);
  background: color-mix(in srgb, var(--ground) 65%, transparent);
}
.peekdlg--img .peekdlg__x:hover,
.peekdlg--img .peekdlg__x:focus-visible {
  color: var(--accent-soft);
  background: color-mix(in srgb, var(--ground) 92%, transparent);
}
.peekdlg--img .peekdlg__body {
  padding: 0; background: transparent; box-shadow: none; overflow: visible;
  display: grid; place-items: center;
}
.peekdlg--img .peekdlg__foot {
  border: 0; background: transparent; padding: 1rem 0 0;
  justify-content: center;
}
.peekdlg--img .peekdlg__fmt {
  color: var(--on-ground); border-bottom: 0;
  padding: 0.55rem 1.1rem; border-radius: 999px;
  background: color-mix(in srgb, var(--ground) 65%, transparent);
  box-shadow: inset 0 0 0 1px var(--hairline-dark);
}
.peekdlg--img .peekdlg__fmt:hover,
.peekdlg--img .peekdlg__fmt:focus-visible {
  color: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-soft);
}
/* A logotype is a mark ON a ground, so it keeps a card. Padded so the mark is
   never bled to the edge, and on the ground the tile said it belongs on: the
   reversed lockup is white, and white on cream is an empty box. */
.peekdlg--img[data-peek-bg="light"] .peekdlg__body,
.peekdlg--img[data-peek-bg="mid"] .peekdlg__body,
.peekdlg--img[data-peek-bg="dark"] .peekdlg__body {
  padding: clamp(2rem, 6vw, 5rem); border-radius: 3px;
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.6);
}
.peekdlg--img[data-peek-bg="light"] .peekdlg__body { background: var(--paper); }
.peekdlg--img[data-peek-bg="mid"]   .peekdlg__body { background: var(--bone); }
.peekdlg--img[data-peek-bg="dark"]  .peekdlg__body { background: var(--ground); }
.peekdlg__body p {
  font-family: var(--font-display);
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.1875rem);
  line-height: 1.55; margin: 0 0 1em; max-width: 60ch;
  text-wrap: pretty;
}
.peekdlg__body p:last-child { margin-bottom: 0; }
/* An image preview fills the bubble rather than sitting in a padded card:
   the point is to see the photograph, not a frame around it. */
.peekdlg__body:has(.peekdlg__img) { padding: 0; }
/* ⚠ max-width, not width: 100%. Stretching every asset to the panel made a
   942px logotype fill a 736px box edge to edge with no margin, which reads as
   a clipped image rather than a mark. Sized to itself, capped to the viewport. */
.peekdlg__img {
  display: block; width: auto; height: auto;
  max-width: min(92vw, 60rem); max-height: 74vh; object-fit: contain;
}
.peekdlg__foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem 1.25rem;
  padding: 0.9rem clamp(1.1rem, 0.9rem + 1vw, 1.9rem);
  border-top: var(--rule) solid var(--hairline);
  background: var(--bone);
}
.peekdlg__fmt {
  font-size: 0.8125rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent-link); text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  padding-bottom: 1px;
}
.peekdlg__fmt:hover, .peekdlg__fmt:focus-visible { border-bottom-color: var(--accent); }

/* ------------------------------------------ extra formats on an asset row
   The .txt stays the primary Download; the PDF and the Word file sit beside
   it as quiet secondary links rather than two more pill buttons, because a
   row with four identical controls is a row nobody reads. */
.asset__fmts {
  display: flex; align-items: center; gap: 0.75rem;
  margin-left: 0.25rem;
}
.asset__fmt {
  font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-soft); text-decoration: none;
  border-bottom: 1px solid transparent; padding-bottom: 1px;
  white-space: nowrap;
}
.asset__fmt:hover, .asset__fmt:focus-visible {
  color: var(--accent-link); border-bottom-color: var(--accent);
}

/* ==================================================== Diffability True
   The old Squarespace store and donation page, folded into one page that
   sells nothing and takes nothing. See page_diffability() in build.py.

   ⚠ Deliberately NOT a product grid of bordered tiles. The garments are cut
   out of their studio sweeps (design/build_store_images.py) so they sit on
   the page as objects rather than as fifteen white rectangles, which means
   the layout can be a bench of things rather than a catalogue. */

/* ------------------------------------------------------------- the mark
   The Diffability fingerprint, lifted off the print on the black bodysuit
   because it exists nowhere as a file. ⚠ Used as a MASK rather than as an
   image, so the colour comes from a token: one asset that is correct on the
   paper ground, on navy, and in the green palette, instead of three PNGs
   that would have to be kept in step. See design/build_diffability_mark.py.

   ⚠ ../../assets, not ../assets. This stylesheet lives at site/css/site.css,
   so one level up is site/, not the repo root. Written with one ../ the mask
   resolved to /site/assets/, which 404s, and a mask that fails to load paints
   NOTHING: no error, no broken-image icon, just an element that is invisible
   and looks like a CSS bug. The two pattern bands already in this file use
   ../../; I wrote a third without checking them. */

/* The page opens on its own mark rather than on nothing. It bleeds off the
   right edge, large and quiet, so the heading still leads. */
.diffhero { position: relative; overflow: clip; }
.diffhero__text { position: relative; z-index: 1; }
/* ⚠ The bleed is done by the MASK, not by hanging the element off the edge.
   Positioned at right: -4rem it looked identical and the mobile audit caught
   it on every handset width: an absolutely positioned child still counts
   toward its parent's scrollable overflow even under `overflow: clip`, so the
   page reported 488px of content in a 430px viewport. Oversizing the mask and
   anchoring it left crops the mark against the element's own edge instead,
   which is the same picture and no overflow. */
.diffhero__mark {
  position: absolute; z-index: 0; pointer-events: none;
  right: 0; top: 50%; transform: translateY(-50%);
  width: clamp(11rem, 28vw, 26rem); aspect-ratio: 514 / 654;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  -webkit-mask: url("../../assets/diffability-mark.png") no-repeat left center / 150% auto;
  mask: url("../../assets/diffability-mark.png") no-repeat left center / 150% auto;
}
@media (max-width: 760px) { .diffhero__mark { opacity: 0.55; } }

/* ------------------------------------------------- the movement section
   ⚠ ONE section now, and no photographs in it. Jarryd, 19 Sept: "Why it
   matters needs to be combined with the movement. Drop the image of Michael
   and why it matters."

   The images were the smaller half. The tee shot is 1000x1500 and was being
   painted larger than that, so it was visibly soft, and a soft picture on
   the page about a trademark is the wrong place to be approximate. The
   bigger fault was that the two sections said one thing twice: the movement
   opened on the client's fundraising sentence, then a second section made
   the case for donating all over again. The fundraising copy now sits in the
   navy band, which is the section actually about money.

   Two columns, both of them carrying something: the lead states what
   Diffability True is, the build makes the case for it. */
.diffmove { position: relative; overflow: clip; }
.diffmove__in {
  position: relative; z-index: 1;
  display: grid; gap: clamp(2rem, 1.6rem + 2.5vw, 4.5rem);
  align-items: start;
}
@media (min-width: 900px) {
  .diffmove__in { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
.diffmove__lead { display: grid; gap: 1rem; align-content: start; max-width: 46ch; }
.diffmove__note {
  margin: 0.4rem 0 0; font-size: 0.9375rem; color: var(--ink-soft);
  padding-left: 0.9rem; border-left: 2px solid var(--accent);
  max-width: 40ch;
}
.diffmove__build { display: grid; gap: clamp(1rem, 0.8rem + 0.8vw, 1.6rem); }
/* The client's opening line, set as the kicker the build runs from. */
.diffmove__kicker {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(1.25rem, 1.05rem + 1vw, 1.9rem);
  line-height: 1.24; letter-spacing: -0.012em;
  color: var(--accent-link); text-wrap: pretty;
}
.diffmove__quote { margin: clamp(0.5rem, 1vw, 1rem) 0 0; max-width: 34ch; }

/* The fingerprint, bleeding off the left. ⚠ The bleed is done by the MASK,
   not by hanging the element off the edge: an absolutely positioned child
   still counts toward its parent's scrollable overflow even under
   `overflow: clip`, which the mobile audit caught as 488px of content in a
   430px viewport. Oversizing the mask crops it against the element's own
   edge instead, which is the same picture and no overflow. */
.diffwhy__mark {
  position: absolute; z-index: 0; pointer-events: none;
  /* ⚠ Right hand side, and the WHOLE glyph, because the D has to read.
     Jarryd, 19 Sept: "put it on the right hand side and make it so that
     we're able to see the D in the fingerprint."
     Cropping was the mistake. Anchored left it showed the outer ridges and
     cut the letter out; anchored right it would cut it from the other side,
     because the D sits in the middle of the glyph and any bleed takes it.
     Contained, the mark is a mark rather than a texture, and it still sits
     large and quiet in the space under the quote. */
  right: 0; bottom: 0;
  width: clamp(13rem, 30vw, 30rem); height: 92%;
  /* ⚠ 22%, up from 10. Jarryd, 19 Sept: "make that fingerprint a watermark
     we can see a little better." At 10 it was a rumour.
     It can go this far only because of WHERE it sits: the mark is anchored to
     the left edge and the text column starts well clear of it, so raising it
     darkens the ground under almost nothing. The one place they meet is the
     first few characters of the lead at narrow widths, which is why the
     handset rule below pulls it back. Contrast is checked by the desktop and
     mobile audits at every width, not assumed. */
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  -webkit-mask: url("../../assets/diffability-mark.png") no-repeat right bottom / contain;
  mask: url("../../assets/diffability-mark.png") no-repeat right bottom / contain;
}
@media (max-width: 899px) {
  /* One column on a handset, so the text runs straight across the mark. */
  .diffwhy__mark {
    width: 56vw; height: 40%;
    background: color-mix(in srgb, var(--accent) 12%, transparent);
  }
}

/* Six statements, each one a step, exactly as the client wrote them. They
   light one at a time on scroll, the same device as the homepage triad. */
.diffwhy { display: grid; gap: clamp(0.8rem, 0.65rem + 0.6vw, 1.25rem); }
.diffwhy p {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.0625rem, 1rem + 0.5vw, 1.4375rem);
  line-height: 1.32; letter-spacing: -0.008em;
  text-wrap: pretty;
  /* ⚠ The resting state is DIM, not invisible. A line that is not yet lit is
     still a line somebody can read, which matters without script and matters
     more for anyone who does not scroll the way the effect expects. */
  color: color-mix(in srgb, var(--ink) 34%, transparent);
  transition: color 620ms var(--ease-out-expo);
}
.diffwhy p.is-lit { color: var(--ink); }
.no-motion .diffwhy p, .diffwhy:not([data-armed]) p { color: var(--ink); }

.pillars {
  margin-top: clamp(2rem, 1.6rem + 1.6vw, 3.25rem);
  display: grid; gap: clamp(1.75rem, 1.4rem + 1.6vw, 3rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}
.pillar { display: grid; gap: 0.6rem; align-content: start; }
.pillar__no {
  font-family: var(--font-display); font-size: 0.8125rem;
  letter-spacing: 0.1em; color: var(--accent-soft);
}
.pillar h3 {
  margin: 0; font-size: clamp(1.125rem, 1rem + 0.5vw, 1.375rem);
  color: var(--on-ground);
}
.pillar p {
  margin: 0; color: color-mix(in srgb, var(--on-ground) 78%, transparent);
  max-width: 34ch;
}

/* ------------------------------------------------------------ support
   A panel, not a checkout. Nothing in here touches money. */
.giveband__eyebrow { color: var(--accent-soft); }
.giveband__head {
  display: grid; gap: clamp(1rem, 0.8rem + 1vw, 1.75rem);
  margin-bottom: clamp(2.25rem, 1.8rem + 2vw, 3.75rem);
}
@media (min-width: 900px) {
  .giveband__head {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(2rem, 4vw, 5rem); align-items: start;
  }
  .giveband__head .storyblock__eyebrow { grid-column: 1 / -1; }
}
.giveband__intro { display: grid; gap: 1rem; }
.giveband__intro p {
  margin: 0; max-width: 54ch;
  color: color-mix(in srgb, var(--on-ground) 80%, transparent);
}
.giveband__rule {
  height: var(--rule); background: var(--hairline-dark);
  margin: clamp(2.75rem, 2.2rem + 2.5vw, 5rem) 0;
}
.give {
  display: grid; gap: clamp(1.75rem, 1.4rem + 2vw, 3.5rem);
  align-items: start;
}
/* On the navy ground the panel inverts: the bone plate would have been a
   bright slab in the middle of a dark passage. */
.giveband .give__panel {
  background: color-mix(in srgb, var(--on-ground) 7%, transparent);
  border-left-color: var(--accent-soft);
}
.giveband .give__label,
.giveband .give__freq span { color: var(--accent-soft); }
.giveband .give__note {
  color: color-mix(in srgb, var(--on-ground) 72%, transparent);
  border-left-color: var(--accent-soft);
}
.giveband .giveamt {
  color: var(--on-ground);
  box-shadow: inset 0 0 0 1px var(--hairline-dark);
}
.giveband .giveamt:hover, .giveband .giveamt:focus-visible {
  box-shadow: inset 0 0 0 1px var(--accent-soft);
}
.giveband .giveamt.is-on {
  background: var(--accent); color: var(--on-accent); box-shadow: none;
}
.giveband .give__freq select {
  background: color-mix(in srgb, var(--ground) 60%, #000);
  color: var(--on-ground);
  box-shadow: inset 0 0 0 1px var(--hairline-dark);
}
@media (min-width: 880px) {
  .give { grid-template-columns: minmax(0, 1.1fr) minmax(19rem, 0.9fr); }
}
.give__text { display: grid; gap: 1rem; align-content: start; }
.give__note {
  /* Capped. On the navy band this note sits in a column with no measure of
     its own and ran to 114 characters on a 1728 display, which the desktop
     audit flags and a reader feels as a line they lose their place in. */
  max-width: 52ch;
  margin: 0; font-size: 0.9375rem; letter-spacing: 0.04em;
  color: var(--ink-soft); padding-left: 0.9rem;
  border-left: 2px solid var(--accent);
}
.give__panel {
  background: var(--bone); padding: clamp(1.4rem, 1.1rem + 1.4vw, 2.25rem);
  display: grid; gap: 1.1rem; align-content: start;
  border-left: 3px solid var(--accent);
}
.give__label {
  margin: 0; font-size: 0.6875rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-soft);
}
.give__amounts { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.giveamt {
  font: inherit; font-size: 0.9375rem;
  padding: 0.6rem 1.1rem; min-height: 44px;
  border: 0; border-radius: 999px; cursor: pointer;
  background: transparent; color: var(--ink);
  box-shadow: inset 0 0 0 1px rgba(17, 29, 46, 0.28);
  transition: box-shadow var(--duration) var(--ease-out-expo),
              background var(--duration) var(--ease-out-expo);
}
.giveamt:hover, .giveamt:focus-visible { box-shadow: inset 0 0 0 1px var(--ink); }
.giveamt.is-on {
  background: var(--accent); color: var(--on-accent); box-shadow: none;
}
.give__freq { display: grid; gap: 0.4rem; }
.give__freq span {
  font-size: 0.6875rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-soft);
}
.give__freq select {
  font: inherit; padding: 0.7rem 0.9rem; min-height: 44px;
  border: 0; box-shadow: inset 0 0 0 1px rgba(17, 29, 46, 0.28);
  border-radius: 2px; background: var(--paper); color: var(--ink);
}
.give__add { justify-self: start; }

/* -------------------------------------------------------------- the store */
.storehead { display: grid; gap: 0.9rem; max-width: 56ch; }
.storehead__note {
  margin: 0; font-size: 0.9375rem; color: var(--ink-soft);
}
.storecoll {
  margin: clamp(2.5rem, 2rem + 2vw, 4rem) 0 clamp(1.25rem, 1rem + 1vw, 2rem);
  font-size: 0.75rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-soft); font-weight: 500;
  padding-bottom: 0.7rem; border-bottom: var(--rule) solid var(--hairline);
}
.prods {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: clamp(1.75rem, 1.4rem + 1.6vw, 3rem);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
}
.prod { display: grid; gap: 1rem; align-content: start; }
/* No card, no border. The cut-out garment IS the tile, floating on the
   section's own ground with a soft pool of shadow under it so it sits on the
   page rather than in a box. */
.prod__shot {
  position: relative;
  aspect-ratio: 1 / 1;
  background: radial-gradient(ellipse at 50% 88%,
    rgba(17, 29, 46, 0.09), transparent 62%);
}
/* ⚠ Absolutely positioned, not a centred grid child. The garments have wildly
   different aspects (a youth tee is 1200x1037, a hoodie 1200x1852) and
   build.py writes real width and height attributes on every image, so while a
   lazy image is still loading it reserves ITS OWN box and pushes the square
   out of shape. A row of four then stepped down the page like a staircase
   until everything had loaded, which looks like a layout fault and is only
   visible on a slow connection or a first paint. Out of flow, the square
   holds whatever the image is doing. */
.prod__shot img {
  position: absolute; inset: 8%; margin: auto;
  max-width: 84%; max-height: 84%; width: auto; height: auto;
  object-fit: contain;
  filter: drop-shadow(0 18px 24px rgba(17, 29, 46, 0.14));
  transition: transform 520ms var(--ease-out-expo);
}
.prod:hover .prod__shot img { transform: translateY(-4px) scale(1.02); }
.prod__body { display: grid; gap: 0.55rem; }
.prod__name {
  margin: 0; font-family: var(--font-display);
  font-size: 1.0625rem; line-height: 1.25; color: var(--ink);
}
.prod__price {
  /* 15px. The audit holds every <p> to a readable minimum and a price is the
     one number on the card someone squints at. 14 was still under it. */
  margin: 0; font-size: 0.9375rem; letter-spacing: 0.06em;
  color: var(--ink-soft); font-variant-numeric: tabular-nums;
}
.prod__opts { display: grid; gap: 0.5rem; margin-top: 0.3rem; }
.prod__opt { display: grid; gap: 0.25rem; }
.prod__opt > span {
  font-size: 0.625rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-soft);
}
.prod__opt select, .prod__opt input {
  /* ⚠ 44px, not 40. Fifteen products with two controls each is thirty
     targets, every one of them under the minimum on every handset width: the
     audit counted 86 findings from this single declaration. A size picker
     that is fiddly to hit is a size picked wrong. */
  font: inherit; font-size: 0.875rem;
  padding: 0.5rem 0.6rem; min-height: 44px;
  /* ⚠ 16px on a handset, or iOS zooms the whole page the moment the control
     takes focus and leaves the reader scrolled sideways in a store. The
     desktop size stays smaller because the label above it does the work. */
  border: 0; border-radius: 2px;
  box-shadow: inset 0 0 0 1px rgba(17, 29, 46, 0.22);
  background: var(--paper); color: var(--ink);
  width: 100%;
}
@media (max-width: 860px) {
  .prod__opt select, .prod__opt input { font-size: 1rem; }
}
.prod__opt--qty { max-width: 6rem; }
.prod__add { margin-top: 0.4rem; justify-self: start; }

/* ------------------------------------------------------------- the request */
.request { display: grid; gap: clamp(2rem, 1.6rem + 2vw, 3.5rem); }
@media (min-width: 900px) {
  .request { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); }
}
.request__list { display: grid; gap: 1rem; align-content: start; }
.request__empty { margin: 0; color: var(--ink-soft); max-width: 40ch; }
.reqitems { list-style: none; margin: 0; padding: 0; display: grid; }
.reqitem {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 0.8rem 0;
  border-bottom: var(--rule) solid var(--hairline);
}
.reqitem:first-child { border-top: var(--rule) solid var(--hairline); }
.reqitem span { font-family: var(--font-display); font-size: 1.0625rem; }
.reqitem__x {
  border: 0; background: transparent; cursor: pointer;
  width: 44px; height: 44px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.25rem; line-height: 1; color: var(--ink-soft);
  border-radius: 999px;
}
.reqitem__x:hover, .reqitem__x:focus-visible { color: var(--accent-link); }


/* The Diffability hero is a photograph rather than the About page's montage,
   so it needs the same framing .vhero__v gives a video.

   ⚠ FIT TO HEIGHT, cropped like the homepage header, not shown whole.
   Jarryd, 24 Sept: use the frame from the header and keep the air it leaves
   above Michael's head. So this hero keeps .vhero's own min-height and
   .vhero__v's `cover`, and changes exactly one number.

   85%, the same number and the same reasoning as .stagehero--stage above.
   This hero is deeper than the header's, so `cover` takes a real bite out of
   the height rather than a few percent, and 85% spends that bite on the empty
   ceiling above Michael instead of on the audience along the bottom edge.
   Those heads are the reason the frame was chosen, so they are the thing the
   crop must not reach. If the header's crop is ever retuned, retune this with
   it: they are the same photograph. */
/* ⚠ 2 Oct: the hero is now the NELA-NJ room holding up Diffability shirts
   (Stephanie supplied it), not the header frame, so the numbers above no
   longer apply. Michael stands front and centre and the faces run across
   roughly 35% to 55% down the frame; 42% keeps that band in the window at
   desktop depths. */
.vhero--diff .vhero__v { object-position: 50% 42%; }

/* ⚠ Copy sits RIGHT on this hero, and only on this hero.
   Not a style preference and not a departure from round one's "hero text on
   the left", which was written about the About page's montage. It follows the
   photograph. In this frame Michael stands left and gestures RIGHT, so the
   headline lands where he is already pointing; that is the same call made on
   the homepage header when this shot was chosen, and the reason the supplied
   mock-up's left-hand copy was rejected there rather than mirroring him. The
   full argument is in design/build_hero_stage.py.
   Left-hand copy was tried here first and the words sat across his chest with
   his hand coming through the standfirst. */
@media (min-width: 861px) {
  .vhero--diff .vhero__copy > * { margin-inline-start: auto; }
}

/* ⚠ A SECOND scrim, running left to right, and this hero needs it where the
   About hero does not. .vhero::after fades up from the foot, which works on
   a frame that is dark at the bottom left. This frame has a white projection
   screen exactly where the type sits, so the heading landed on near-white and
   the brass eyebrow all but vanished. The bottom scrim stays, because it is
   what keeps the type sitting on ground rather than floating on a picture. */
/* ⚠ A CORNER, not a column, and the diagonal is what makes this frame work.
   Michael stands centre-left here and .vhero__copy sits hard left (round one,
   planner item P7: Jarryd asked for hero copy on the left, so the type does
   not move). A left-to-right ramp strong enough to carry the standfirst
   therefore also crossed his face, because it treats every height the same.
   It does not need to, because the two things sit in opposite corners.
   Measured on the built page at 1025x701, as fractions of the hero box: the
   copy runs 31% to 85% down and starts 48% across, while Michael lands around
   23% across with his face in the top third. So the wash runs to the top
   LEFT: heavy in the bottom-right corner the words occupy, and gone before it
   climbs to him. Type on ground, face untouched. */
.vhero--diff::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to top left,
    color-mix(in oklab, var(--ground) 92%, transparent) 0%,
    color-mix(in oklab, var(--ground) 74%, transparent) 26%,
    color-mix(in oklab, var(--ground) 36%, transparent) 48%,
    transparent 64%);
}
/* ⚠ On a phone this hero STACKS, exactly as the homepage one does, and for
   exactly the same reason: it is now the same photograph. Cover-cropping a
   16:9 frame into a portrait slot leaves a 31% wide window, which sliced
   Michael in half at the left edge and would have magnified 518px of source
   across a 390px screen at 3x. The 5:4 box and the 0% anchor are the header's
   numbers; see .stagehero--stage. With nothing behind the type any more, both
   scrims come off rather than laying navy over the picture above. */
@media (max-width: 860px) {
  .vhero--diff {
    display: block;
    min-height: 0;
    padding-block: 0 clamp(2.5rem, 2rem + 4vw, 4rem);
  }
  .vhero--diff .vhero__v {
    position: static;
    width: 100%; height: auto;
    /* Phones get diffability-group-m, already cropped to the faces. */
    aspect-ratio: auto;
  }
  .vhero--diff::before, .vhero--diff::after { display: none; }
  .vhero--diff .vhero__copy {
    padding-top: clamp(1.75rem, 1.4rem + 2vw, 2.5rem);
  }
}

/* The publisher's 1,920 x 1,080 promotional banner, full width across the top
   of the Book page once the book is live. aspect-ratio pairs with height:auto,
   because the img carries real width/height attributes and aspect-ratio is
   silently ignored without it (the "stretched photos" trap). */
.book-banner { margin: 0; }
.book-banner img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }

/* ------------------------------------------------ footer, round two (29 Sept)
   Social links are icons now, and the contact column is tighter ("feels too
   spread out"). Footer only: no global spacing token is touched.

   ⚠ Selectors are deliberately heavy. `.footer a:not(.btn)` (0,2,1) sets
   display, padding and a hover translate, and the handset block further up
   makes footer links `display: block` with 0.72rem padding. The button here
   needs its own box, so it has to out-score both. */
.footer ul.footer__social {
  flex-direction: row; flex-wrap: wrap; gap: 0.25rem; margin-left: -0.6rem;
}
.footer ul.footer__social a.footer__socialbtn:not(.btn) {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; min-height: 44px; padding: 0;
  border-radius: 50%; color: rgba(242, 238, 230, 0.82);
  transition: color var(--duration) var(--ease-out-expo),
              background-color var(--duration) var(--ease-out-expo);
}
.footer ul.footer__social a.footer__socialbtn:not(.btn):hover,
.footer ul.footer__social a.footer__socialbtn:not(.btn):focus-visible {
  color: var(--accent-soft); transform: none;
  background-color: rgba(242, 238, 230, 0.09);
}
.footer ul.footer__social a.footer__socialbtn:focus-visible {
  outline: 2px solid var(--accent-soft); outline-offset: 2px;
}
.footer__social-h { margin-top: 1.1rem; }

/* Contact and information block: the row gap was a 0.7rem flex gap on top of
   0.42rem of padding per link, which put each row about 45px from the next. */
@media (min-width: 861px) {
  .footer__grid { row-gap: 1.5rem; column-gap: clamp(1.25rem, 2.2vw, 2.25rem); }
  .footer__grid ul { gap: 0.1rem; }
  .footer h2, .footer h3 { margin-bottom: 0.5rem; }
}
@media (max-width: 860px) {
  .footer__grid { row-gap: 1.25rem; }
  .footer__grid > * + * { padding-top: 0; }
}

/* ------------------------------------------- Diffability hero, 2 Oct (round 2b)
   ⚠ STACKED AT EVERY WIDTH NOW, not only on a phone. The hero became the
   NELA-NJ room holding up Diffability shirts, and that frame is faces edge to
   edge: overlaid copy, on either side, landed the headline across Michael's
   face or a row of attendees. So the picture sits whole above the type, the
   same arrangement the handset rules above already use, and both scrims come
   off because nothing is laid over the photograph any more.
   2:1 keeps the band of faces (35% to 55% down the source) with room to spare. */
@media (min-width: 861px) {
  .vhero--diff {
    display: block;
    min-height: 0;
    padding-block: 0 clamp(2.5rem, 2rem + 3vw, 4.5rem);
  }
  .vhero--diff .vhero__v {
    position: static;
    width: 100%; height: auto;
    /* The file itself is the crop now (just over the back row of heads), so
       the box takes the picture's own shape rather than cutting it again. */
    aspect-ratio: auto;
  }
  .vhero--diff::before, .vhero--diff::after { display: none; }
  .vhero--diff .vhero__copy {
    padding-top: clamp(2rem, 1.6rem + 2vw, 3rem);
    margin-inline: auto;
  }
  .vhero--diff .vhero__copy > * { margin-inline-start: 0; }
}

/* The AAHFN kit frame is a PORTRAIT since 4 Oct (Michael's own pick, grey suit
   on the blue drape), and every tile that previews it is landscape. Centred,
   the crop took his head off. He stands in the top third, so anchor there.
   Matched on the file rather than a class because the same image appears in
   three different tile components. */
img[src*="michael-kutcher-stage-01"] { object-position: 50% 12%; }

/* Book page shop buttons (12 Oct). The main shop is the solid button, any
   others sit beside it as outlines, and they wrap on a phone. */
.book__shops {
  display: flex; flex-wrap: wrap; gap: .75rem;
  margin-top: .5rem;
}

/* The tour list beside the calendar (12 Oct). Grid on the left, the dates
   as a list on the right, so nobody has to click a dot to find a city. On a
   phone the list comes first: it is the quicker read on a small screen. */
.cal-layout--agenda {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(15rem, 1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
.cal-layout--agenda .cal { grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }
.agenda {
  margin-top: 2.5rem;
  position: sticky; top: calc(var(--masthead-h, 107px) + 1.5rem);
  padding: 1.5rem 1.75rem;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.04);
  box-shadow: inset 0 0 0 1px rgba(176, 141, 87, 0.28);
}
.agenda__eyebrow {
  font-size: .75rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent-soft); margin: 0 0 .75rem;
}
.agenda__month {
  font-family: var(--font-display); font-weight: 400;
  font-size: 1.25rem; color: var(--on-ground);
  margin: 1.25rem 0 .5rem;
}
.agenda__month:first-of-type { margin-top: 0; }
.agenda__list { list-style: none; margin: 0; padding: 0; }
.agenda__item {
  display: grid; grid-template-columns: 3rem 1fr; gap: .9rem;
  padding: .8rem 0;
  border-top: 1px solid rgba(242, 238, 230, 0.12);
}
.section--ground .agenda__date { margin: 0; display: flex; flex-direction: column; line-height: 1; }
.agenda__day {
  font-family: var(--font-display); font-size: 1.6rem; color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.agenda__dow {
  font-size: .7rem; letter-spacing: .08em; text-transform: uppercase;
  color: rgba(242, 238, 230, 0.6); margin-top: .3rem;
}
.section--ground .agenda__where { margin: 0; color: var(--on-ground); font-weight: 600; }
.section--ground .agenda__venue,
.section--ground .agenda__note { margin: .15rem 0 0; color: rgba(242, 238, 230, 0.72); font-size: .9rem; }
.agenda__link {
  display: inline-block; margin-top: .35rem;
  font-size: .85rem; color: var(--accent-soft);
}
.agenda__link:hover, .agenda__link:focus-visible { color: var(--on-ground); }
@media (max-width: 900px) {
  .cal-layout--agenda { grid-template-columns: 1fr; }
  .cal-layout--agenda .agenda { order: -1; position: static; }
}

/* Footer sign-up (12 Oct), on every page. The form is the same component as
   everywhere else; on the navy footer only its small type needs lifting. */
.footer__news { margin-top: 1.75rem; max-width: 26rem; }
.footer__news h2 { margin-bottom: .75rem; }
.footer__news .form { margin: 0; }
.footer__news .field__hint { color: rgba(239, 229, 213, 0.6); }
.footer__news .signup { max-width: 100%; }
