/* =====================================================================
   KERRY PASTINE, kp.css
   The whole design system. One file, no build step, no imports beyond
   the two self-hosted font faces below.

   Lifted from the approved home mockup and generalised in three places,
   all of them deliberate:

     1. `--signal` is now a real cascade variable rather than a per-block
        hard-coded colour. Set it once on a section and the heading
        accent, the row hover, the link fill and the rule all follow.
        That is the whole reason the palette can carry four worlds
        without four stylesheets.
     2. `.cut` (the row), `.page` (the interior hero) and `.sec` (the
        band) are the three patterns every interior page is built from.
     3. The nav keeps all four items at every width. The mockup hid two
        of them under 620px, which is fine for a mockup and a navigation
        failure for a site.

   Order: fonts, tokens, base, primitives, components, reveals, reduced
   motion. Reduced motion stays last so it wins without !important where
   it can.
   ===================================================================== */

/* ---------------------------------------------------------------------
   FONTS. Two families. Bodoni Moda is deliberately never loaded: a
   high-contrast Didone set large is the most common move on the web
   right now. The width axis on Archivo does the work the third face used
   to do.
   --------------------------------------------------------------------- */
@font-face {
  font-family: 'Archivo';
  src: url('../fonts/archivo-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-stretch: 62% 125%;
  font-display: swap;
}
/* The latin-ext face is deliberately absent. Nothing on the site paints a
   character in U+0100-02BA, U+02BD-02C5, U+1E00-1E9F or U+2C60-2C7F — the â
   in Alcântara and the ã in São Paulo are U+00E2 and U+00E3, both inside the
   latin subset above. Adding a word that needs the range means subsetting and
   shipping archivo-latin-ext.woff2 first; the declaration pointed at a file
   that was never there. */
@font-face {
  font-family: 'Instrument Sans';
  src: url('../fonts/instrument-sans-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-stretch: 75% 100%;
  font-display: swap;
}

/* ---------------------------------------------------------------------
   TOKENS. The four colours were sampled out of Kerry's own stage
   photographs and canvases, not invented. All four clear WCAG AA as body
   text on the ground, so colour here is load bearing.
   --------------------------------------------------------------------- */
:root {
  --void: #0a090b;
  --void-2: #131117;
  --bone: #f4f1ea;
  --ash: #8f8c98;

  --mag: #ff16d6;
  --blue: #0093fc;
  --flame: #ff4a0a;
  --acid: #f2db1f;

  /* The one variable every section overrides. */
  --signal: var(--mag);

  --gut: clamp(1.15rem, 4.5vw, 4.5rem);
  --dsp: 'Archivo', system-ui, sans-serif;
  --txt: 'Instrument Sans', system-ui, sans-serif;

  /* The micro-caps step. Eyebrows, nav, buttons, chips, legends and the works
     filters all set the same condensed uppercase face at a small size and read
     as one system on screen, but they were seven independent clamps, so moving
     one left the other six behind. They take this instead: the ones that want a
     different size take it as a ratio, and the two inside the nav keep their
     own floor and slope: with no script the six links stay in the bar at
     360px and have to fit next to the wordmark, so only the desktop cap is
     safe to raise there.

     Raised twice. 0.78/0.95rem was the original; art.html's legend went to
     1.02vw/1.05rem on 17 August; Kerry, 18 August: "the eyebrow text/font is
     still too small everywhere." So this is a third and larger step — a 1rem
     floor and a 1.25rem cap — and the word everywhere is the operative part.
     Three rules had been left off the token and were dragging the average down:
     .panel-idx, which is the card eyebrow itself, .pit-line cite, and .prose h3.
     They are on it now, so this line moves all of them.

     One rule stays off it on purpose. .badge-ring sets type around a circle of
     fixed diameter, where a larger size is not a larger label but a label that
     no longer closes the ring. */
  --cap: clamp(1rem, 1.28vw, 1.25rem);

  /* Two curves. Travel and colour. Nothing else. */
  --ez: cubic-bezier(0.16, 1, 0.3, 1);
}

/* Set the signal on any element and everything inside follows. */
.s-mag {
  --signal: var(--mag);
}
.s-blue {
  --signal: var(--blue);
}
.s-flame {
  --signal: var(--flame);
}
.s-acid {
  --signal: var(--acid);
}

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

/* scroll-padding-top is measured, not estimated. .nav is padding 1.1rem
   top and bottom around its tallest child, .nav-mark, which caps at
   0.92rem on the body's 1.5 line-height: 35.2 + 22.08 = 57.3px. 4.5rem
   (72px) clears it by 15px at every viewport above 566px, and by more
   below it, where the wordmark shrinks with 2.6vw.

   works.html stacks a sticky .sift under the blended nav. That bar owns
   its own nav band already — 3.5rem of top padding — so it does not add
   to the nav, it replaces it: 56 + 32.4 (a .sift-b at 0.74rem, 0.62em
   padding) + 17.6 + 1px border = 107px. 7.5rem (120px) leaves the same
   13px of air. :has() rather than a page class, because the rule should
   follow the bar wherever it is used next. */
html {
  scroll-behavior: smooth;
  scroll-padding-top: 4.5rem;
  background: var(--void);
}

html:has(.sift) {
  scroll-padding-top: 7.5rem;
}

body {
  margin: 0;
  background: var(--void);
  color: var(--bone);
  font-family: var(--txt);
  /* The preferred term was bare vw, so it only cleared the 1.02rem floor at
     1419px and only reached the ceiling at 1725px. Every phone, every tablet
     and every laptop under 1440 therefore read the site at the floor, 16.3px,
     which is browser-default size for 56ch of long-form on a dark ground. A
     rem addend plus a gentler rate makes the whole range live: 17.5px at
     390px, 18.4px at 768px, 20px at 1440px, and the 1.32rem ceiling from
     1920px up. Everything else on the site is rem or --cap, both of them
     root-relative, so nothing but inherited body copy moves.

     line-height stays at 1.5. The scroll-padding-top note above measures the
     nav off it. */
  font-size: clamp(1.09rem, 1.04rem + 0.23vw, 1.32rem);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* clip, not hidden: hidden on the body kills position: sticky. */
  overflow-x: clip;
}

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

a {
  color: inherit;
}

/* Quoted material keeps its element, because a testimonial that is not a
   <blockquote> is a lie to a screen reader. It does not keep the
   browser's 40px indent, which fights every grid on the site. */
blockquote {
  margin: 0;
}

:focus-visible {
  outline: 3px solid var(--acid);
  outline-offset: 3px;
}

::selection {
  background: var(--signal);
  color: #0a090b;
}

/* Display type. One class, pushed to the ends of the Archivo axes. */
.d {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 125, 'wght' 900;
  line-height: 0.8;
  letter-spacing: -0.028em;
  text-transform: uppercase;
  margin: 0;
  text-wrap: balance;
}

/* The accent inside a heading. Every display heading uses <em> for it,
   so a section changes colour by changing one variable. */
.d em {
  font-style: normal;
  color: var(--signal);
}

/* Condensed micro-label. The only small uppercase anywhere. */
.lab {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  font-size: var(--cap);
  letter-spacing: 0.2em;
  margin: 0;
}

/* A label that ends in an action. The studio rail on art.html is the only
   place this happens, and .lab had no link treatment at all, so the anchor
   would have inherited the label's own colour and read as plain text. Same
   pairing as .prose a, with the offset opened up because .lab is letter-spaced
   caps and a tight underline sits close enough to the baseline to read as a
   strikethrough. Kerry, 18 August. */
.lab a {
  color: var(--signal);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.3em;
}

/* The hot variant. .lab declares no colour so it inherits; this takes the
   section's accent instead, which is what nearly every label on the site
   wants. Follows the cascade, so a label inside .s-acid reads acid without
   knowing it.

   Doubled selector on purpose. This replaced an inline style that outranked
   everything, and sixteen of its uses sit inside .panel-grid or .thread-body,
   whose `p` rules are (0,1,1) and would beat a lone .lab--hot. At (0,2,0) it
   clears generic body copy but still loses to a deliberate container override
   like .foot .lab, which is the order we want. */
.lab.lab--hot {
  color: var(--signal);
}

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

/* Honeypot, on the contact form only. Not .sr: that hides a field from
   the eye and leaves it to a screen reader, which is right for a label and
   wrong for a trap. This one is hidden from everybody, and the markup adds
   aria-hidden and tabindex="-1" so no keyboard or reader ever lands on it.
   Web3Forms throws the submission away if it arrives filled. */
.hp {
  display: none;
}

.quiet {
  color: var(--ash);
}

/* Skip link. Off-screen until it is focused, then it is the loudest
   thing on the page, which is the point of it.
   fixed, not absolute: absolute pins it to the top of the document, so
   tabbing into it after any scrolling slides it into view somewhere far
   above the fold where nobody sees it. It is the first control on every
   page and it has to work at every scroll position. */
.skip {
  position: fixed;
  left: var(--gut);
  top: 0.6rem;
  z-index: 100;
  transform: translateY(-250%);
  background: var(--acid);
  color: #0a090b;
  padding: 0.7em 1.1em;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 800;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.72rem;
  text-decoration: none;
  transition: transform 0.25s var(--ez);
}

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

/* Grain. One inline SVG turbulence: no request, no file, no flash. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='260' height='260' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------------------------------------------------------------------
   NAV. Six items and a wordmark: Home, Coaching, Art, Music, Take the
   quiz, Talk to Kerry.

   This block used to say five items and no drawer, on the argument that a
   menu button is a whole interaction to hide four links. Kerry asked for
   six items and a hamburger on 17 August 2026, and with two of the six
   now being three-word labels the old argument no longer holds: the set
   is roughly twice the width it was, and past a certain viewport it wraps
   into the page. So there is a drawer, and the comment says so.

   mix-blend-mode: difference means the bar stays legible over a
   photograph, a magenta panel and a flat void without a single per-page
   override. The one place it has to be switched off is an open drawer,
   which paints its own solid ground — see .nav--open below.
   --------------------------------------------------------------------- */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem var(--gut);
  mix-blend-mode: difference;
  pointer-events: none;
}

.nav > * {
  pointer-events: auto;
}

/* The wordmark shrinks with the viewport rather than truncating to
   initials. It no longer has to share a line with the whole set — below
   1200px that is a drawer — but it does share one with the menu button,
   and the name is the thing worth keeping whole. */
.nav-mark {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 125, 'wght' 900;
  text-transform: uppercase;
  font-size: clamp(0.72rem, 2.6vw, calc(var(--cap) * 1.08));
  letter-spacing: -0.01em;
  text-decoration: none;
  color: #fff;
  white-space: nowrap;
}

.nav-set {
  display: flex;
  gap: clamp(0.7rem, 2vw, 2.2rem);
}

.nav-set a {
  color: #fff;
  text-decoration: none;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  font-size: clamp(0.6rem, 1.6vw, calc(var(--cap) * 0.88));
  letter-spacing: clamp(0.08em, 0.5vw, 0.18em);
  position: relative;
  white-space: nowrap;
}

.nav-set a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.34em;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.2, 1);
}

.nav-set a:hover::after,
.nav-set a:focus-visible::after,
.nav-set a[aria-current]::after {
  transform: scaleX(1);
}

/* The menu button. display: none is the default state at every width, and
   only the media query below turns it on, so a wide viewport never renders it
   and neither does a browser with no script — see the comment in the markup.

   2.75rem is a 44px target. The negative margins pull that target back off
   the bar so it does not make the bar taller than the wordmark needs, which
   is the one measurement that has to stay put: the nav is fixed over six
   different heroes and its height is baked into all of their crops. */
.nav-burger {
  display: none;
  position: relative;
  width: 2.75rem;
  height: 2.75rem;
  margin: -0.75rem -0.6rem -0.75rem 0;
  padding: 0;
  appearance: none;
  background: none;
  border: 0;
  cursor: pointer;
  color: #fff;
}

/* Three bars from one element: the span is the middle one and its two
   pseudo-elements are the outer two, offset by half a gap each. Rotating the
   outer two onto each other and dropping the middle one is the X, and it is a
   pure transform, so the reduced-motion block flattens it with everything
   else rather than needing a rule of its own. */
.nav-burger span,
.nav-burger span::before,
.nav-burger span::after {
  position: absolute;
  width: 1.5rem;
  height: 2px;
  background: currentColor;
  transition: transform 0.32s var(--ez), background-color 0.2s ease;
}

.nav-burger span {
  top: 50%;
  left: 50%;
  margin: -1px 0 0 -0.75rem;
}

.nav-burger span::before,
.nav-burger span::after {
  content: '';
  top: 0;
  left: 0;
}

.nav-burger span::before {
  transform: translateY(-0.5rem);
}

.nav-burger span::after {
  transform: translateY(0.5rem);
}

/* The X. The middle bar goes, and the outer two come back to the centre and
   cross. Not scoped to the media query: the class only ever lands while the
   drawer is open, and the drawer only exists below 1200px.

   The middle bar goes by turning its background transparent, not by opacity.
   ::before and ::after are children of the span, so opacity: 0 on the span
   composited the whole group away and took both arms of the X with it: an
   open drawer with no visible way to close it. Only the span's own bar has a
   background to drop, so dropping it leaves the two arms lit. */
.nav--open .nav-burger span {
  background: transparent;
}

.nav--open .nav-burger span::before {
  transform: rotate(45deg);
}

.nav--open .nav-burger span::after {
  transform: rotate(-45deg);
}

/* ---------------------------------------------------------------------
   THE DRAWER. Below 1200px the seven labels no longer fit on one line
   with the wordmark, so the set becomes a full-screen panel behind the
   bar.

   1200 is an estimate rather than a measurement, because nothing in this
   environment can render Archivo at 'wdth' 62 and tell me where the set
   actually wraps. It is deliberately early: a menu button on a small
   laptop that could have held the links is a mild waste of a click, and a
   nav wrapping into the hero is a broken page. Move this one number if it
   collapses sooner than it needs to.

   It was 1080 while the set held six links. ABOUT went in between HOME
   and COACHING, which is about a tenth more text and one more gap, so
   the estimate moved by the same proportion and keeps the old margin. */
@media (max-width: 1200px) {
  .js .nav-burger {
    display: block;
  }

  .js .nav-set {
    display: none;
  }

  /* display, not opacity. A drawer faded to zero is still in the
     accessibility tree and still collects Tab, which is a worse fault than
     a missing transition — and the reduced-motion block flattens every
     duration on this site anyway, so half the visitors would never see the
     fade the extra state bought. */
  .js .nav--open .nav-set {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: -1;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    gap: clamp(0.9rem, 3.2vh, 1.9rem);
    padding: 6rem var(--gut) var(--gut);
    background: var(--void);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* In the bar these are micro-caps. In the drawer there is a screen to
     spend, so they are set at reading size and lose the condensed width. */
  .js .nav--open .nav-set a {
    font-variation-settings: 'wdth' 100, 'wght' 800;
    font-size: clamp(1.7rem, 8vw, 3.2rem);
    letter-spacing: -0.01em;
  }

  /* difference is what keeps the bar readable over a photograph. Over the
     drawer's own near-black ground it would invert it: white type on white.
     Scoped inside the query so a resize that outran the script cannot leave
     the desktop bar unblended. */
  .nav--open {
    mix-blend-mode: normal;
  }
}

/* The drawer covers the viewport, so the page behind it must not scroll
   under the thumb. overscroll-behavior above stops the drawer's own scroll
   from chaining; this stops the page from scrolling at all. */
.nav-lock,
.nav-lock body {
  overflow: hidden;
}

/* ---------------------------------------------------------------------
   THE PIT. Full-viewport hero, home page only. Six frames cross-fading
   under a name set in difference blend, so the letters invert against
   whatever stage light is behind them at that second.
   --------------------------------------------------------------------- */
.pit {
  position: relative;
  height: 100svh;
  min-height: 34rem;
  overflow: hidden;
  isolation: isolate;
  background: var(--void);
}

.pit-frame {
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: pit-cut 33s infinite;
}

.pit-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* All 33 live shots are 2000x1333 with her face in the upper third. */
  object-position: center 34%;
  animation: pit-drift 33s infinite;
}

.pit-frame:nth-child(1),
.pit-frame:nth-child(1) img {
  animation-delay: 0s;
}
.pit-frame:nth-child(2),
.pit-frame:nth-child(2) img {
  animation-delay: -27.5s;
}
.pit-frame:nth-child(3),
.pit-frame:nth-child(3) img {
  animation-delay: -22s;
}
.pit-frame:nth-child(4),
.pit-frame:nth-child(4) img {
  animation-delay: -16.5s;
}
.pit-frame:nth-child(5),
.pit-frame:nth-child(5) img {
  animation-delay: -11s;
}
.pit-frame:nth-child(6),
.pit-frame:nth-child(6) img {
  animation-delay: -5.5s;
}

@keyframes pit-cut {
  0% {
    opacity: 0;
  }
  1.6% {
    opacity: 1;
  }
  15% {
    opacity: 1;
  }
  17.5% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

@keyframes pit-drift {
  0% {
    transform: scale(1.16) translate3d(1.2%, 0, 0);
  }
  17.5% {
    transform: scale(1.02) translate3d(-1.2%, 0, 0);
  }
  100% {
    transform: scale(1.02) translate3d(-1.2%, 0, 0);
  }
}

/* Guarantees the difference blend lands on something dark enough that
   the name is legible on every frame, not just the dark ones. */
.pit-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
      180deg,
      rgba(10, 9, 11, 0.62) 0%,
      rgba(10, 9, 11, 0.22) 32%,
      rgba(10, 9, 11, 0.5) 72%,
      rgba(10, 9, 11, 0.88) 100%
    ),
    radial-gradient(
      120% 90% at 50% 45%,
      rgba(10, 9, 11, 0) 30%,
      rgba(10, 9, 11, 0.6) 100%
    );
}

.pit-type {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  padding: 0 var(--gut);
  mix-blend-mode: difference;
  color: #fff;
}

.pit-name {
  font-size: clamp(3.6rem, 16.4vw, 16rem);
  line-height: 0.76;
  letter-spacing: -0.038em;
  font-variation-settings: 'wdth' 125, 'wght' 900;
}

.pit-name span {
  display: block;
  clip-path: inset(0 0 102% 0);
  animation: wipe-up 1.05s var(--ez) forwards;
}

.pit-name span:nth-child(1) {
  animation-delay: 0.15s;
}
.pit-name span:nth-child(2) {
  animation-delay: 0.3s;
}

@keyframes wipe-up {
  to {
    clip-path: inset(0 0 -12% 0);
  }
}

.pit-foot {
  position: absolute;
  left: var(--gut);
  right: var(--gut);
  bottom: clamp(1.2rem, 3.4vh, 2.6rem);
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.4rem 2rem;
  z-index: 3;
}

/* The quote reads fine in a narrow window and wrong on a desktop, and the
   reason is that the cite was the only fixed type size in a hero where
   everything else scales. .pit-name caps at 16rem, .pit-line at 1.5rem, and
   the cite sat at 0.68rem at every width the site will ever be opened at — so
   the name-to-cite ratio went from about 6:1 on a phone to about 23:1 on a
   large screen. The quote did not stay small, it got smaller relative to
   everything beside it as the screen grew. It looks like it was written from
   .lab, which does the same job in the same face, and had the fluid part
   dropped. Both sizes are clamps now, and the measure goes 30ch to 34ch so
   that the larger size does not buy back a line. */
.pit-line {
  max-width: 34ch;
  font-size: clamp(1.15rem, 1.9vw, 2rem);
  line-height: 1.22;
  margin: 0;
  text-wrap: balance;
}

/* --ash was close to the worst available colour here. .pit-type is
   difference-blended with color: #fff, and white is the safe choice under
   difference precisely because it inverts whatever sits behind it. --ash is
   #8f8c98, a mid grey, and difference between two similar mid-tones is near
   black — so on the stage frames that live in that same tonal band the cite
   was not merely small, it came and went as pit-cut rotated the six frames.
   Same mechanism as P1-6: difference guarantees change, not contrast.

   Mixed toward --bone rather than set to it, so the attribution keeps its
   place under the quote instead of competing with it. If it now reads too
   bright, the move is back toward --ash and a quieter cite — the quote is the
   part that has to survive all six frames, not this. */
.pit-line cite {
  display: block;
  margin-top: 0.5rem;
  font-style: normal;
  color: color-mix(in oklab, var(--bone) 78%, var(--ash));
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  /* On the token at a ratio, not off it. The note above wants this quieter than
     the quote it attributes, which is a matter of colour and of being smaller
     than the eyebrows — 0.9 keeps both, and it now moves when they move. */
  font-size: calc(var(--cap) * 0.9);
  letter-spacing: 0.18em;
}

/* ---------------------------------------------------------------------
   CHIPS. Solid blocks of signal colour with the ground punched out of
   them. Used for the three roles in the hero and for any short set of
   facts that would otherwise become a bulleted list.
   --------------------------------------------------------------------- */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.42rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.chips > * {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  font-size: var(--cap);
  letter-spacing: 0.16em;
  padding: 0.4em 0.85em;
  background: var(--signal);
  color: #0a090b;
}

/* The hero runs all four colours across the three roles on purpose:
   it is the only place on the site that states the whole palette. */
.chips--all > *:nth-child(1) {
  background: var(--mag);
}
.chips--all > *:nth-child(2) {
  background: var(--flame);
}
.chips--all > *:nth-child(3) {
  background: var(--acid);
}
.chips--all > *:nth-child(4) {
  background: var(--blue);
}

/* ---------------------------------------------------------------------
   THE PAGE HEAD. Every interior page opens with this: a label, one
   enormous heading, and at most one paragraph. Optionally bedded on a
   full-bleed photograph.
   --------------------------------------------------------------------- */
.page {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--void);
  padding: clamp(6.5rem, 17vh, 11rem) var(--gut) clamp(2.6rem, 6vw, 5rem);
}

.page-bed {
  position: absolute;
  inset: 0;
  z-index: -2;
  display: flex;
}

/* Five photographs whose aspect ratios run from 0.73 to 1.50, forced through
   one box of roughly 2.3:1. cover therefore throws away 36% of the music
   shot and 69% of the about portrait, so a single object-position cannot
   serve all five: the 34% below is where the faces sit in the landscape
   stage shot and nowhere near where they sit in the four portraits. */
.page-bed img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center var(--bed-y, 34%);
  flex: 1;
  min-width: 0;
  animation: bed-settle 7s var(--ez) both;
}

/* The settle. The home page hero has two things the interior heroes do not:
   the difference blend and slow motion on the photograph. The blend does not
   travel — P1-11 gives four reasons — but the motion should.

   Read pit-drift against its 33s duration and it is not a loop: it moves for
   the first 5.8 seconds and then holds for 27 more while the next frame fades
   in. It is a settle that repeats only because six frames share one timeline.
   The interior hero has one photograph that never changes, so it wants the
   shape, not the keyframes: once, on load, then nothing.

   Three numbers that are deliberate. It ends at scale(1), not the pit's 1.02,
   because the 2% overscale there only exists to stop a cross-fade between two
   frames popping, and there is no second frame here — the resting state should
   be the crop that object-position was tuned for. It starts at 1.09 rather
   than 1.16 because two of these beds are near-square portraits (coaching is
   945x1004) and a 16% overscale in a 2.3:1 band crops into the face on the
   first frame. And the fill is both rather than forwards, or the first painted
   frame is the untransformed image, which flashes to 1.09 and back.

   No reduced-motion rule, and that omission is the point: the global block
   sets animation: none !important, and because this rests at transform: none
   in all but name, killing it leaves the image exactly where it belongs.
   .pit-frame img needs an explicit transform: none only because pit-drift
   rests at scale(1.02). Adding one here would be cargo cult.

   .page is overflow: hidden, so the overscale cannot produce a scrollbar. */
@keyframes bed-settle {
  from {
    transform: scale(1.09) translate3d(0.9%, 0, 0);
  }
  to {
    transform: scale(1) translate3d(0, 0, 0);
  }
}

/* The Ken Burns. Kerry, 18 August, for talk.html. It replaces bed-settle on
   that one hero rather than running after it, because the two are the same
   idea at different lengths and stacking them means two rules animating
   transform on one element: bed-burns has no backwards fill, so during a
   delay the settle would hold at scale(1) and then pop to wherever the burns
   begins. One animation, and its first leg is the entrance.

   infinite alternate rather than a loop, so it never cuts back to the start.
   ease-in-out rather than --ez, which is a settle curve: it spends most of
   its time nearly stopped, which is right for something arriving once and
   wrong for something that has to keep moving for 28 seconds.

   The scale floor is 1.05, not 1. A translate in a transform list runs before
   the scale, so a 1.1% offset shows as 1.16% of the box, and the overscale is
   the only thing keeping the image edge outside the frame. 1.05 leaves 2.5%
   of slack each side against a worst case of 1.16%. At the 1.15 end there is
   7.5%. Neither frame can expose the bed.

   No reduced-motion rule, for the reason given above bed-settle: the global
   block kills animation outright, which leaves this at transform: none, and
   none is the crop --bed-y was tuned for. */
.page-bed--burns img {
  animation: bed-burns 28s ease-in-out infinite alternate;
}

@keyframes bed-burns {
  from {
    transform: scale(1.05) translate3d(-1.1%, 0.8%, 0);
  }
  to {
    transform: scale(1.15) translate3d(1.2%, -1.1%, 0);
  }
}

.page-bed--face-high {
  --bed-y: 13%;
}

.page-bed--face-mid {
  --bed-y: 20%;
}

/* A floor, not a height. Three of the five heroes carry a .go-row and two
   do not, and the ledes run 122 to 191 characters, so content-driven height
   varied page to page. The floor is set above the tallest of them instead,
   which pins all five to the same height. */
.page:has(.page-bed) {
  min-height: 84svh;
  display: grid;
  align-content: end;
}

/* Two scrims in one pseudo-element, because they are solving two unrelated
   problems and the second one cannot be folded into the first.

   The 6deg gradient is the type scrim: near-opaque at the bottom where the
   headline and lede sit, opening up toward the top so the photograph is still
   a photograph. Raising its stops was P1-6's prescribed fix for the nav as
   well, on the theory that one change would serve both. It does not, for two
   reasons that only became clear with the numbers in front of us:

   1. It stops short. The top stop reached 0.34. The home page's .pit-scrim,
      which is the one difference-blended nav on this site that demonstrably
      works, guarantees 0.62 before the blend lands. Going to 0.62 here would
      mean darkening the whole upper hero to match, which is exactly the mud
      this document warns against twice.
   2. It is tilted. At 6deg across a 1920px hero the gradient's stops shift
      about 200px vertically between the left and right edges. That is
      invisible on a soft bottom-weighted wash and useless for a band that has
      to line up with a horizontal nav — one end of the wordmark would be
      protected and the other would not.

   So the nav gets its own band: untilted, pinned to the top, held flat at 0.62
   across the 3.6rem the nav actually occupies, then faded out by 8.5rem so
   there is no edge to see. Under the nav the two scrims compose to about 0.75,
   which is past the home page's proven threshold rather than short of it, and
   below 8.5rem nothing changes at all. */
.page:has(.page-bed)::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(
        180deg,
        rgba(10, 9, 11, 0.62) 0,
        rgba(10, 9, 11, 0.62) 3.6rem,
        rgba(10, 9, 11, 0) 8.5rem
      )
      no-repeat,
    linear-gradient(
      6deg,
      rgba(10, 9, 11, 0.95) 8%,
      rgba(10, 9, 11, 0.62) 60%,
      rgba(10, 9, 11, 0.34) 100%
    );
}

.page-idx {
  color: var(--signal);
  margin-bottom: 0.9rem;
}

/* Music's eyebrow carries the front of the band name and the headline carries
   the back, so the two are one sentence and the eyebrow cannot be the accent
   colour: the whole name would read as a single magenta block. Bone puts the
   contrast back where the other heroes have it, at the break between the two
   lines rather than between label and headline. */
.page-idx--bone {
  color: var(--bone);
}

/* The default scale is for a headline on a flat ground, where the type can be
   as large as it likes because nothing is behind it. */
.page-h {
  font-size: clamp(2.9rem, var(--h-vw, 11.5vw), var(--h-cap, 11rem));
  letter-spacing: -0.035em;
}

/* The wrap backstop, for the reason set out at length above .panel-h: a display
   word that will not fit is clipped rather than broken, and hyphens: auto alone
   does not prevent it. .page-h carries the --bed and --post variants with it, so
   these four selectors are every large heading on the site. Nothing moves for
   text that already fits — anywhere only breaks a word when the line has no
   other option, which is also what makes it safe on the post titles, where the
   headline is a sentence rather than two words. */
.page-h,
.sec-h,
.thread-h,
.call-h {
  hyphens: auto;
  overflow-wrap: anywhere;
}

/* A headline over a photograph is a different problem. At the default scale a
   bottom-anchored block covers roughly the lower three quarters of the hero,
   which leaves no band for a face, and the crop cannot solve it because the
   subjects are wherever the photographer put them. This step is what the three
   section heroes share so they read as one set. It goes through the same
   custom-property idiom as --rd rather than a second font-size rule, so the
   2.9rem floor stays shared and small screens are unaffected. */
.page-h--bed {
  --h-vw: 7.8vw;
  --h-cap: 7.4rem;
}

/* A third problem again. The section heroes carry two or three words, so they
   can be set at a size where the words are the picture. A post title is a
   sentence: "You are not having a midlife crisis. You are outgrowing your
   life." is twelve words, and at the default 11rem cap it is eight lines and
   most of a laptop screen before the first paragraph. This is the step where
   a full sentence still reads as a headline rather than as a wall. Same
   custom-property idiom as --h-vw above, so the 2.9rem floor stays shared and
   a phone is unaffected. */
.page-h--post {
  --h-vw: 5.4vw;
  --h-cap: 4.6rem;
}

.page-lede {
  margin: clamp(1.2rem, 2.6vw, 2rem) 0 0;
  max-width: 46ch;
  font-size: clamp(1.05rem, 1.5vw, 1.4rem);
  line-height: 1.32;
  color: #e2dfd9;
  text-wrap: pretty;
}

/* ---------------------------------------------------------------------
   THE BAND. Standard content section. No container, no max-width: body
   copy is held by ch, everything else runs to the gutters.
   --------------------------------------------------------------------- */
.sec {
  position: relative;
  z-index: 4;
  background: var(--void);
  padding: clamp(3.5rem, 8vw, 8rem) var(--gut);
}

.sec--alt {
  background: var(--void-2);
}

.sec--tight {
  padding-block: clamp(2.4rem, 5vw, 4.5rem);
}

.sec-h {
  font-size: clamp(2.2rem, 7vw, 6.5rem);
  margin-bottom: clamp(1.6rem, 4vw, 3rem);
}

.sec-h--sm {
  font-size: clamp(1.6rem, 4vw, 3.4rem);
}

/* Long-form. The only measure control on the site. */
.prose {
  max-width: 56ch;
}

/* An intro paragraph followed by the thing it introduces. Two pages already
   declare this exact gap inline and keep it, since an attribute outranks a
   class. Where a .prose is the last thing in its section the margin lands
   inside the section's own padding and collapses against a following sibling
   that sets its own margin-top, so nothing on the built pages moves. Same
   reason as .sec > .go-row: press.html has no attribute budget to spend. */
.sec > .prose {
  margin-bottom: clamp(1.8rem, 4vw, 3rem);
}

.prose > * {
  margin: 0 0 1.05em;
}

.prose > *:last-child {
  margin-bottom: 0;
}

/* h2 joined h3 here for the blog. An essay's own headings sit under the
   page h1 with no section heading between them, so h2 is the honest level
   and skipping to h3 would break the outline on seven pages. No h2 has ever
   appeared inside a .prose elsewhere on the site: the section headings are
   .sec-h, which is a sibling of .prose, never a child. */
.prose h2,
.prose h3 {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 800;
  text-transform: uppercase;
  /* These are headings inside eight essays and they were set smaller than the
     eyebrows above them, which is the wrong way round. On the token now, one
     step up, so a subhead outranks a label. */
  font-size: calc(var(--cap) * 1.1);
  letter-spacing: 0.16em;
  color: var(--signal);
  margin-top: 2.2em;
}

.prose a {
  color: var(--signal);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
}

.prose ul,
.prose ol {
  padding-left: 1.1em;
}

.prose li {
  margin-bottom: 0.5em;
}

/* Pull quote. The rule is the signal colour, so it reads as part of the
   section rather than as furniture. */
.pull {
  margin: 0;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 125, 'wght' 900;
  text-transform: uppercase;
  font-size: clamp(1.5rem, 3.4vw, 3rem);
  line-height: 0.92;
  letter-spacing: -0.03em;
  color: var(--bone);
  border-left: 6px solid var(--signal);
  padding-left: clamp(0.8rem, 1.6vw, 1.4rem);
}

/* ---------------------------------------------------------------------
   THE LINK. A block of signal colour that fills from the bottom. Used
   for every primary action on the site.
   --------------------------------------------------------------------- */
.go {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  text-decoration: none;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 800;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: calc(var(--cap) * 1.1);
  padding: 1em 1.5em;
  color: #0a090b;
  background: var(--signal);
  position: relative;
  overflow: hidden;
  border: 0;
  cursor: pointer;
  transition: color 0.32s ease;
}

.go span {
  position: relative;
  z-index: 1;
}

.go::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bone);
  transform: translateY(101%);
  transition: transform 0.42s var(--ez);
}

.go:hover::before,
.go:focus-visible::before {
  transform: translateY(0);
}

.go i {
  font-style: normal;
  position: relative;
  z-index: 1;
  transition: transform 0.42s var(--ez);
}

.go:hover i {
  transform: translateX(0.45em);
}

/* Secondary. Same shape, hollow, fills with the signal instead of bone.
   Two of these side by side would read as a pair of buttons, which is
   what the cards used to do, so use one per group. */
.go--ghost {
  background: transparent;
  color: var(--signal);
  box-shadow: inset 0 0 0 1px currentColor;
}

.go--ghost::before {
  background: var(--signal);
}

.go--ghost:hover,
.go--ghost:focus-visible {
  color: #0a090b;
}

.go-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  align-items: center;
}

/* A hero row always wants the same gap above it. Three of the pages currently
   set it with a style attribute, which wins over this and so nothing moves;
   this is here so the fourth does not have to add a fourth attribute, and so
   the three can be deleted in the P2-15 spacing pass without a regression. */
.page > .go-row {
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
}

/* The same argument one level down. Eleven sections set this gap with a style
   attribute and every one of them wins over this rule, so adding it moves
   nothing today; it exists so press.html, which has a style budget of zero,
   can have the gap without becoming the twelfth. Direct child only: the bare
   .go-row markup elsewhere on the site sits inside .thread-body, .price and
   .page, all of which space it themselves. */
.sec > .go-row {
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
}

/* ---------------------------------------------------------------------
   RIBBON. Two rows of giant type that read scroll velocity and
   direction. Decorative, always aria-hidden.
   --------------------------------------------------------------------- */
.ribbon {
  position: relative;
  z-index: 4;
  padding: clamp(1.6rem, 4vw, 3.4rem) 0;
  background: var(--void);
  border-block: 1px solid rgba(244, 241, 234, 0.1);
  overflow: hidden;
}

.rib-row {
  display: flex;
  white-space: nowrap;
  will-change: transform;
}

.rib-row p {
  margin: 0;
  flex: none;
  padding-right: 0.5em;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 125, 'wght' 800;
  text-transform: uppercase;
  font-size: clamp(2.6rem, 8vw, 8rem);
  line-height: 0.94;
  letter-spacing: -0.02em;
}

.rib-row:nth-child(2) p {
  color: var(--signal);
  font-variation-settings: 'wdth' 62, 'wght' 900;
  font-size: clamp(2rem, 6vw, 6rem);
}

.rib-row i {
  font-style: normal;
  color: var(--acid);
  padding: 0 0.14em;
}

.rib-row:nth-child(2) i {
  color: var(--blue);
}

/* The acid band. Same machinery, inverted. */
.chapters {
  position: relative;
  z-index: 4;
  background: var(--acid);
  color: #100e05;
  overflow: hidden;
  padding: clamp(0.8rem, 2vw, 1.6rem) 0;
}

.chapters .rib-row p {
  color: #100e05;
  font-variation-settings: 'wdth' 125, 'wght' 900;
  font-size: clamp(1.9rem, 5.4vw, 5rem);
}

.chapters .rib-row i {
  color: rgba(16, 14, 5, 0.42);
}

/* ---------------------------------------------------------------------
   THE STACK. Sticky panels layering over each other. The padding-bottom
   on .stack is not decoration: it is what gives the last panel a sticky
   range to travel through. Remove it and the last panel never pins.
   --------------------------------------------------------------------- */
.stack {
  position: relative;
  background: var(--void);
  padding-bottom: 34svh;
}

/* The foot. Kerry, 17 August, on the home panels: the text, subtext and
   button all needed raising, with roughly 100px between the bottom of the
   button and the bottom of the card. 6.25rem is that 100px, and it scales
   down rather than eating a third of a phone screen. */
.panel {
  --panel-foot: clamp(2.25rem, 7vw, 6.25rem);
  position: sticky;
  top: 0;
  min-height: 100svh;
  overflow: hidden;
  display: grid;
  /* Kerry, 18 August: the M of RETROMODERNISM was still being cut off after the
     type came down. This line is why. A grid with no declared columns gets one
     implicit auto track, and an auto track is sized to its content's max-content
     width — so the track grew to fit fourteen expanded uppercase letters, pushed
     past the padding box, and overflow: hidden above shaved the tail. Shrinking
     the font moved the threshold; it could not remove it.

     minmax(0, 1fr) pins the track to the panel's own width, which is what
     .panel-grid below has always done. Same idiom, and it was missing on the one
     grid that holds the headline. */
  grid-template-columns: minmax(0, 1fr);
  align-content: end;
  padding: var(--gut);
  padding-bottom: calc(var(--gut) + var(--panel-foot));
  isolation: isolate;
  transform-origin: 50% 30%;
  will-change: transform, filter;
}

/* height was 100svh flat, which is what clipped art.html. Those panels carry
   two paragraphs and a pull quote, and once they stack to one column the
   content is taller than the screen — the overflow: hidden above then cut it
   off and the next panel slid over what was left. min-height lets a tall
   panel grow instead of losing its bottom half. Panels that fit are
   unaffected, which is all three on index.html. */
@media (max-width: 899px) {
  .panel {
    position: static;
    min-height: auto;
    padding-top: calc(var(--gut) + var(--panel-foot));
  }
}

.panel-bed {
  position: absolute;
  inset: 0;
  z-index: -2;
  display: flex;
}

.panel-bed img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  flex: 1;
  min-width: 0;
}

/* The scrim over the bed. Kerry, 18 August: the copy on art.html's second
   panel was unreadable against sold-tokyo-2.jpg. The stops were the reason.
   0.42 at the midpoint is fine over a dark canvas and useless over a bright
   one, and that painting is large fields of near-white and yellow with black
   cut through it. 0.42 black over white composites to about #949494, which
   puts .panel-grid's #e2dfd9 at roughly 1.3:1. The panel is align-content:
   end, so on a tall column the paragraphs and the pull quote climb straight
   into that band.

   0.82 at 46% composites to about #2e2e2e and takes the same text to 10:1,
   which holds over the worst case rather than over the average one. The top
   stop came up with it so a headline that reaches the light end still has 3:1.
   The paintings read dimmer for it. They are aria-hidden decoration and the
   copy is not. */
.panel::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    6deg,
    rgba(10, 9, 11, 0.97) 8%,
    rgba(10, 9, 11, 0.82) 46%,
    rgba(10, 9, 11, 0.55) 100%
  );
}

/* Below 900 the panel goes static and single-column, so the copy stops sitting
   in the bottom third and fills the whole box, top stop included. A gradient
   cannot help there because there is no light end left to spare: the scrim
   goes flat and dark instead, and the painting reads as texture. */
@media (max-width: 899px) {
  .panel::after {
    background: linear-gradient(
      6deg,
      rgba(10, 9, 11, 0.94) 0%,
      rgba(10, 9, 11, 0.86) 100%
    );
  }
}

.panel-idx {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 900;
  /* Was its own clamp(0.8rem, 1vw, 1rem) and should never have been: this is
     the eyebrow over every card heading, the most-looked-at label on the home
     page, and it sat below the token the rest of the small caps share. */
  font-size: var(--cap);
  letter-spacing: 0.2em;
  color: var(--signal);
  margin: 0 0 0.9rem;
}

/* Kerry, 17 August: "RETROMODERN is getting cut off." It was. The display face
   is Archivo at 'wdth' 125 — expanded, not condensed — so the fourteen uppercase
   letters of RETROMODERNISM, as the heading then read, were wider at 11.5vw than
   the line they sat on, and .panel above is overflow: hidden, so the tail of the
   word was simply cut away.

   This is the same problem .page-h--bed already solved for the three section
   heroes: a headline over a photograph cannot be as large as one on a flat
   ground. Same numbers, for the same reason, so the panels and the heroes read
   as one set. The floor comes down too, because a phone is where the longest
   word is tightest.

   hyphens was supposed to be the net under that, and on 18 August Kerry said the
   M was still gone. hyphens: auto was doing nothing, for a reason worth writing
   down: a hyphenation opportunity does not reduce an element's min-content width
   in any shipping browser. The box therefore never got narrower than the whole
   word, the line never had to wrap, and the break the net depended on was never
   reached. It was a net under a fall that does not happen.

   overflow-wrap: anywhere is the net that works, because it is the one value
   that does lower min-content width. With hyphens: auto still present the break
   takes a hyphen wherever the dictionary offers one, and only breaks bare as a
   last resort. A break is worse than no break and better than a word with its
   ending shaved off, which is what shipped.

   Postscript: Kerry then cut the word itself, and the heading reads RETROMODERN
   ON CANVAS. Eleven letters instead of fourteen. It is still the longest word in
   any panel heading — the rest top out at seven — so this is the rule that keeps
   deciding the tightest line. Everything above stays: the mechanism was general,
   any long word in this face at this size would clip the same way on a narrow
   enough screen, and the stylesheet does not get to assume the copy stays
   short. */
.panel-h {
  font-size: clamp(2.4rem, 7.8vw, 7.4rem);
  letter-spacing: -0.035em;
  hyphens: auto;
  overflow-wrap: anywhere;
}

.panel-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.4rem 3rem;
  align-items: end;
  margin-top: clamp(1rem, 2.4vw, 2rem);
}

@media (min-width: 900px) {
  .panel-grid {
    grid-template-columns: minmax(0, 1fr) minmax(20rem, 30rem);
  }

  /* index.html's coaching panel is the only grid whose second column holds
     two buttons rather than one, and 30rem is 480px. The pair measures about
     515px once --cap tops out, which it does at 1562px, so on exactly the
     widest screens the row ran out of track and wrapped: Start here sitting
     on top of Take the quiz on a desktop with room to spare either side.

     max-content is the width the row wants with no wrapping, so the track is
     cut to the two buttons and never to more. Below 1562px that asks for less
     than the flat 30rem did, so the paragraph beside it gains room rather
     than losing it, and under 900px the panel is a single column and the
     buttons stack because there is genuinely no room for two. */
  .panel-grid:has(> .go-row) {
    grid-template-columns: minmax(0, 1fr) minmax(20rem, max-content);
  }
}

/* align-items: end is right for index.html, where the second column is a
   button that should sit level with the last line of the paragraph beside it.
   It is wrong on about.html and coaching.html, where that column is an accent
   label over its own paragraph: the label adds a line at the top, the two
   columns still meet at the bottom, and the lone paragraph on the left is
   pushed a line or more below the text it reads with. Kerry, 19 August, on the
   Know Thyself panel. Only a bare paragraph moves, and only when it is the
   shorter column — the tallest item in a row sets that row's height, so this
   is a no-op on index.html and on any panel whose columns already match.

   The :has() arm is the same fault one level down. coaching.html's first
   chapter now labels both of its columns, so neither column is a bare
   paragraph any more, and bottom alignment would stagger the two labels
   against each other — the more obvious version of the same misread. A column
   that opens on a hot label is a titled block and hangs from its title.
   .lab--hot rather than .lab is what keeps art.html out of this: its columns
   open on a .lab.panel-idx above a headline, and that page is built around the
   headline hanging from the bottom of the panel. */
.panel-grid > p,
.panel-grid > div:has(> .lab--hot:first-child) {
  align-self: start;
}

.panel-grid p {
  margin: 0;
  max-width: 46ch;
  color: #e2dfd9;
}

/* margin: 0 was written for index.html, where every panel column holds exactly
   one paragraph. art.html's columns hold two or three, and they were running
   together into a single block with no line between them — part of what Kerry
   read on 17 August as the card text being unreadable.

   :not(.lab) on both sides is the whole point of the selector. about.html and
   coaching.html put an accent label directly above its paragraph, and a label
   is meant to sit on the text it names, not float above it. This spaces body
   copy from body copy and nothing else. */
.panel-grid p:not(.lab) + p:not(.lab) {
  margin-top: 0.9em;
}

/* The one pull quote on the site sits between two of those paragraphs, in
   art.html's second panel. .pull is margin: 0 by design, so its container
   decides the spacing, and .panel-grid never did: the p + p rule above cannot
   see across a blockquote, so the quote had nothing above it and the paragraph
   after it had nothing above either. Three blocks read as one. Kerry, 18
   August. em rather than rem, because the gap should track the quote's own
   display size and that is a clamp. */
.panel-grid .pull {
  margin-block: 0.85em;
}

/* A button at the foot of a column of prose. On index.html the button is a
   grid item and the grid's own row gap already holds it off the text, so this
   is deliberately scoped to the nested case: art.html's "See the abstracts",
   which sits inside a column div and had nothing between it and the paragraph
   above it at all. */
.panel-grid > div > .go {
  margin-top: clamp(1.4rem, 3vw, 2.2rem);
}

/* index.html's coaching panel carries two buttons, so the grid item is now a
   .go-row rather than a bare .go. The rule above cannot tell that div from
   art.html's column div and would space both buttons off a paragraph that is
   already held away by the grid's own row gap. The row is the grid item; the
   gap is its job. */
.panel-grid > .go-row > .go {
  margin-top: 0;
}

/* ---------------------------------------------------------------------
   THE ROW. A list where every item is a full-width rule with type on it.
   Tracks, videos, posts, dates, chapters: anything that used to be a
   card is one of these. Set --signal on the wrapper.
   --------------------------------------------------------------------- */
.cut {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.9rem 1.4rem;
  padding: clamp(0.8rem, 1.8vw, 1.5rem) 0;
  border-top: 1px solid rgba(244, 241, 234, 0.16);
  text-decoration: none;
  color: inherit;
  transition: padding-left 0.4s var(--ez), background-color 0.4s ease;
}

.cut:last-of-type {
  border-bottom: 1px solid rgba(244, 241, 234, 0.16);
}

a.cut:hover,
a.cut:focus-visible {
  padding-left: clamp(0.6rem, 2vw, 2rem);
  background: color-mix(in srgb, var(--signal) 11%, transparent);
}

/* 0.72rem and 0.7rem below were two more hard-coded micro-sizes that P2-17
   missed when it put the rest of the site's small caps on --cap. Kerry, 17
   August, on art.html's Straight answers: "the blue question marks and the
   words to the right are way too small — enlarge them, like you enlarged the
   eyebrow text." That is the same token and the same fix. .cut is used on the
   blog index, on music.html and in the FAQ, so every row on the site grows
   with it, which is the point of having the token at all. */
.cut-n {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  font-size: var(--cap);
  letter-spacing: 0.18em;
  color: var(--signal);
}

.cut-t {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 125, 'wght' 800;
  text-transform: uppercase;
  font-size: clamp(1.3rem, 4.2vw, 3.2rem);
  line-height: 0.95;
  letter-spacing: -0.025em;
  transition: color 0.3s ease;
}

a.cut:hover .cut-t {
  color: var(--signal);
}

.cut-m {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 600;
  text-transform: uppercase;
  font-size: var(--cap);
  letter-spacing: 0.16em;
  color: var(--ash);
  text-align: right;
}

/* A row that carries a sentence under the title rather than a stat. */
.cut-d {
  grid-column: 2 / -1;
  margin: 0.55rem 0 0;
  max-width: 60ch;
  font-size: 0.96rem;
  line-height: 1.45;
  color: var(--ash);
}

@media (max-width: 700px) {
  .cut {
    grid-template-columns: auto minmax(0, 1fr);
  }
  .cut-m {
    grid-column: 2;
    text-align: left;
  }
}

/* ---------------------------------------------------------------------
   FULL-BLEED PHOTOGRAPHIC SECTION. A band with a photograph behind it
   and the ground turned up until the type is safe.
   --------------------------------------------------------------------- */
.bed {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.bed-img {
  position: absolute;
  inset: 0;
  z-index: -2;
}

.bed-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 30%;
  filter: saturate(1.15);
}

.bed::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(10, 9, 11, 0.82);
}

/* ---------------------------------------------------------------------
   THE WALL. Canvases on a horizontal rail driven by vertical scroll.
   The height on .wall is the scroll budget: more height, slower rail.
   --------------------------------------------------------------------- */
.wall {
  position: relative;
  height: 340svh;
  background: var(--void);
}

.wall-pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(1.2rem, 3vw, 2.4rem);
  overflow: hidden;
}

.wall-head {
  padding: 0 var(--gut);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem 2rem;
}

.wall-head h2 {
  font-size: clamp(2rem, 6.5vw, 6rem);
}

.wall-head .lab {
  color: var(--ash);
}

.wall-rail {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2.2vw, 2.6rem);
  padding: 0 var(--gut);
  width: max-content;
  will-change: transform;
}

.canvas {
  position: relative;
  flex: none;
  height: clamp(15rem, 46vh, 30rem);
  margin: 0;
}

/* Each canvas deep-links to its own row in the catalogue, which the
   gallery already understands: works.html#celestial-grid opens that
   painting. The rail is a preview, so make the preview go somewhere. */
.canvas a {
  display: block;
  height: 100%;
  text-decoration: none;
}

.canvas img {
  height: 100%;
  width: auto;
  object-fit: contain;
  filter: saturate(1.08);
  transition: transform 0.5s var(--ez);
}

.canvas:hover img {
  transform: scale(1.035);
}

.canvas figcaption {
  position: absolute;
  left: 0;
  bottom: -2.6rem;
  display: flex;
  gap: 0.8rem;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(-0.4rem);
  transition: opacity 0.34s ease, transform 0.34s ease;
}

.canvas:hover figcaption,
.canvas:focus-within figcaption {
  opacity: 1;
  transform: translateY(0);
}

/* No hover means no way in: the caption is inside the link, so tapping
   navigates rather than revealing it, and the title and size of every
   painting on the wall would be unreachable. Show them outright. Same
   reset the reduced-motion block already makes, for the same reason. */
@media (hover: none) {
  .canvas figcaption {
    opacity: 1;
    transform: none;
  }
}

.canvas figcaption b {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 800;
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.15em;
  color: var(--signal);
}

.canvas figcaption span {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 500;
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.15em;
  color: var(--ash);
}

/* ---------------------------------------------------------------------
   THE SPLIT. Photograph on one side, long-form on the other.
   --------------------------------------------------------------------- */
.thread {
  position: relative;
  z-index: 4;
  background: var(--void-2);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 950px) {
  .thread {
    grid-template-columns: 0.85fr 1.15fr;
  }
}

.thread-pic {
  position: relative;
  min-height: 60svh;
  overflow: hidden;
}

.thread-pic img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 22%;
}

.thread-body {
  padding: clamp(3rem, 7vw, 7rem) var(--gut);
  display: grid;
  align-content: center;
  gap: clamp(1.2rem, 2.6vw, 2rem);
}

.thread-h {
  font-size: clamp(2rem, 5vw, 4.4rem);
  letter-spacing: -0.03em;
}

.thread-body p {
  margin: 0;
  max-width: 56ch;
  color: #dcd9d3;
}

/* ---------------------------------------------------------------------
   NUMBERS. Prices, stats, chapter counts. No boxes: a rule on top and
   the number set enormous is the whole component.
   --------------------------------------------------------------------- */
.prices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: clamp(1.6rem, 3vw, 3rem);
}

.price {
  border-top: 3px solid var(--sig, var(--signal));
  padding-top: 1rem;
  display: grid;
  gap: 0.55rem;
  align-content: start;
}

.prices--all .price:nth-child(1) {
  --sig: var(--blue);
}
.prices--all .price:nth-child(2) {
  --sig: var(--mag);
}
.prices--all .price:nth-child(3) {
  --sig: var(--flame);
}
.prices--all .price:nth-child(4) {
  --sig: var(--acid);
}

.price b {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 125, 'wght' 900;
  font-size: clamp(2.2rem, 5.2vw, 4.2rem);
  line-height: 0.86;
  letter-spacing: -0.038em;
  color: var(--sig, var(--signal));
}

.price b i {
  font-style: normal;
  font-variation-settings: 'wdth' 62, 'wght' 700;
  font-size: 0.26em;
  letter-spacing: 0.14em;
  color: var(--ash);
  text-transform: uppercase;
}

.price .lab {
  color: var(--bone);
}

.price p {
  margin: 0;
  font-size: 0.94rem;
  line-height: 1.42;
  color: var(--ash);
}

.price a {
  color: var(--sig, var(--signal));
}

/* ---------------------------------------------------------------------
   THE BIOS. Three blocks of text a producer is meant to select, copy and
   paste into a running order, so the only job here is to make the copy
   boundary obvious and leave the text alone.

   .price was the near miss and is worth writing down: same top rule, same
   per-child signal, same label. It is a grid of auto-fit minmax(13rem)
   columns, and a 330-word speaker introduction in a 13rem column is a
   ribbon. So this is one column, always, with the measure held at the
   paragraph rather than the block. The rule up top is the same 3px in the
   same rotation, because a producer reading both sections should not have
   to learn two visual languages on one page.
   --------------------------------------------------------------------- */
.epk {
  display: grid;
  gap: clamp(2.4rem, 5vw, 4rem);
}

.epk-b {
  border-top: 3px solid var(--sig, var(--signal));
  padding-top: 1.1rem;
  display: grid;
  gap: 0.85rem;
  align-content: start;
}

.epk-b:nth-child(1) {
  --sig: var(--blue);
}
.epk-b:nth-child(2) {
  --sig: var(--mag);
}
.epk-b:nth-child(3) {
  --sig: var(--flame);
}

.epk-b .lab {
  color: var(--bone);
}

.epk-b p {
  margin: 0;
  max-width: 62ch;
  color: #dcd9d3;
}

/* The copy target is a wrapper inside the block rather than the block itself,
   because app.js copies innerText and a producer wants the bio without the
   label above it or the word "Copy" underneath. Paragraph margins are zeroed
   for the outer grid, so the wrapper has to carry its own. */
.epk-b > div {
  display: grid;
  gap: 0.8rem;
}

/* The copy button sits under the text it copies, not beside the heading,
   so that what got copied is unambiguous when three of these stack. */
.epk-b .go-row {
  margin-top: 0.5rem;
}

/* ---------------------------------------------------------------------
   THE CALL. One giant line and the rotating circular title.
   --------------------------------------------------------------------- */
.call {
  position: relative;
  z-index: 4;
  background: var(--void);
  padding: clamp(4rem, 10vw, 9rem) var(--gut);
  display: grid;
  gap: clamp(2.4rem, 5vw, 4rem);
  align-items: center;
  grid-template-columns: minmax(0, 1fr);
  justify-items: start;
}

@media (min-width: 900px) {
  .call {
    grid-template-columns: minmax(0, 1fr) auto;
  }
}

.call-h {
  font-size: clamp(2.6rem, 9vw, 8.5rem);
  letter-spacing: -0.038em;
}

/* Bone, not ash. This paragraph is the only body copy in its section and it
   sits under a heading the size of a poster, five times over a photograph.
   Ash reads as a caption there and greys out the one sentence that has to be
   read. Every other body paragraph on the site is bone; this is now too. */
.call-sub {
  margin: 1.4rem 0 0;
  max-width: 42ch;
  color: var(--bone);
}

.badge {
  position: relative;
  width: clamp(11rem, 17vw, 15rem);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  text-decoration: none;
  justify-self: center;
}

/* The ring is built by v3.js: one transform per character, then the
   whole thing rotates. Forty spans by hand would be forty spans to
   re-type every time the words change. */
.badge-ring {
  position: absolute;
  inset: 0;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 800;
  text-transform: uppercase;
  font-size: clamp(0.72rem, 1.05vw, 0.95rem);
  letter-spacing: 0;
  color: var(--acid);
  animation: spin 22s linear infinite;
}

.badge:hover .badge-ring {
  animation-duration: 6s;
}

.badge-ring span {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: rotate(calc(360deg / var(--n) * var(--i)))
    translateY(calc(var(--r) * -1));
  transform-origin: 0 0;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.badge-core {
  width: 62%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--mag);
  color: #0a090b;
  display: grid;
  place-items: center;
  text-align: center;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 125, 'wght' 900;
  text-transform: uppercase;
  font-size: clamp(0.95rem, 1.5vw, 1.35rem);
  line-height: 0.88;
  letter-spacing: -0.02em;
  transition: transform 0.45s var(--ez), background-color 0.35s ease;
}

.badge:hover .badge-core,
.badge:focus-visible .badge-core {
  transform: scale(1.09);
  background: var(--bone);
}

/* ---------------------------------------------------------------------
   FOOTER
   --------------------------------------------------------------------- */
.foot {
  position: relative;
  z-index: 4;
  background: var(--void);
  padding: clamp(3rem, 6vw, 5rem) var(--gut) clamp(2rem, 4vw, 3rem);
  display: grid;
  gap: clamp(2rem, 4vw, 3rem);
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  border-top: 1px solid rgba(244, 241, 234, 0.12);
}

.foot h3 {
  margin: 0 0 0.9rem;
}

.foot ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.5rem;
}

.foot a {
  text-decoration: none;
  color: #cfccd6;
  transition: color 0.24s ease;
}

.foot a:hover,
.foot a:focus-visible {
  color: var(--mag);
}

.foot .lab {
  color: var(--ash);
}

.foot-end {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.8rem 2rem;
  padding-top: clamp(1.6rem, 3vw, 2.4rem);
  border-top: 1px solid rgba(244, 241, 234, 0.12);
}

/* ---------------------------------------------------------------------
   THE QUIZ. Six questions, one screen each. This is the only place on
   the site where a page changes under you without a navigation, so it
   is also the only place that needs a progress indicator: without one
   the visitor cannot tell whether they are near the end, and that is
   what makes people abandon a quiz.

   The whole thing is one column at every width. Six long options do not
   want a grid; a grid turns them into a wall of equal weight and makes
   the reader compare rather than recognise.
   --------------------------------------------------------------------- */
.quiz {
  --signal: var(--acid);
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding: clamp(6rem, 12vw, 9rem) var(--gut) clamp(3rem, 7vw, 5rem);
}

.quiz-card {
  width: 100%;
  max-width: 44rem;
  margin-inline: auto;
}

/* Screens swap by [hidden]. The attribute is doing real work here, so it
   needs to beat the display the element would otherwise take. */
.quiz [hidden] {
  display: none;
}

.quiz-h {
  font-size: clamp(2.1rem, 6.4vw, 4.4rem);
  line-height: 0.94;
  letter-spacing: -0.02em;
  text-wrap: balance;
  margin: 0 0 clamp(1rem, 2.4vw, 1.6rem);
}

.quiz-lede {
  font-size: clamp(1rem, 1.7vw, 1.2rem);
  line-height: 1.62;
  color: #cfccd6;
  max-width: 46ch;
  margin: 0 0 clamp(1.8rem, 4vw, 2.6rem);
}

/* Progress. A hairline that fills, and the count in words above it,
   because a bar alone does not say how many are left. */
.quiz-meter {
  display: block;
  height: 2px;
  background: rgba(244, 241, 234, 0.16);
  margin: 0.7rem 0 clamp(2rem, 4.5vw, 3rem);
}

.quiz-meter i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--signal);
  transition: width 0.45s var(--ez);
}

.quiz-q {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 110, 'wght' 800;
  font-size: clamp(1.35rem, 3.4vw, 2.3rem);
  line-height: 1.14;
  letter-spacing: -0.015em;
  text-wrap: balance;
  margin: 0 0 clamp(1.4rem, 3vw, 2.2rem);
}

.quiz-opts {
  display: grid;
  gap: 0.55rem;
  margin: 0 0 clamp(1.8rem, 4vw, 2.6rem);
}

/* An option is a row, not a button-looking thing. Same left-fill
   behaviour as .cut, so the quiz feels like the rest of the site
   rather than a form bolted onto it. */
.quiz-opt {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: clamp(0.95rem, 1.5vw, 1.05rem);
  line-height: 1.5;
  color: var(--bone);
  background: transparent;
  border: 0;
  border-top: 1px solid rgba(244, 241, 234, 0.16);
  padding: clamp(0.9rem, 2vw, 1.15rem) 0;
  cursor: pointer;
  transition: padding-left 0.4s var(--ez), background 0.3s ease, color 0.3s ease;
}

.quiz-opts .quiz-opt:last-child {
  border-bottom: 1px solid rgba(244, 241, 234, 0.16);
}

.quiz-opt:hover,
.quiz-opt:focus-visible {
  padding-left: clamp(0.6rem, 2vw, 1.4rem);
  background: color-mix(in srgb, var(--signal) 10%, transparent);
}

/* aria-checked, not aria-pressed: the six options are one choice out of
   six, and quiz.js marks them as radios in a radiogroup. aria-pressed
   promised six independent toggles. */
.quiz-opt[aria-checked='true'] {
  padding-left: clamp(0.6rem, 2vw, 1.4rem);
  background: color-mix(in srgb, var(--signal) 16%, transparent);
  color: var(--signal);
}

/* The marker sits in the padding the fill opens up, so nothing shifts
   when it appears. */
.quiz-opt::before {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  width: 3px;
  height: 0;
  background: var(--signal);
  transform: translateY(-50%);
  transition: height 0.4s var(--ez);
}

.quiz-opt[aria-checked='true']::before {
  height: 62%;
}

.quiz-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.quiz-back {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.7rem;
  color: var(--ash);
  background: none;
  border: 0;
  padding: 0.5rem 0;
  cursor: pointer;
  transition: color 0.24s ease;
}

.quiz-back:hover,
.quiz-back:focus-visible {
  color: var(--bone);
}

.quiz-back[hidden] {
  visibility: hidden;
  display: block;
}

/* The result. The stage name is the loudest thing on the page, because
   it is the one word the visitor came for. */
.quiz-stage {
  font-size: clamp(2.6rem, 11vw, 7rem);
  line-height: 0.88;
  letter-spacing: -0.03em;
  color: var(--signal);
  margin: 0 0 clamp(1rem, 2.5vw, 1.6rem);
}

.quiz-body {
  font-size: clamp(1rem, 1.7vw, 1.15rem);
  line-height: 1.68;
  color: #cfccd6;
  max-width: 52ch;
  margin: 0 0 clamp(2rem, 4.5vw, 3rem);
}

/* Capture. One field, one button, one line of consequence. */
.grab {
  border-top: 1px solid rgba(244, 241, 234, 0.16);
  padding-top: clamp(1.6rem, 3.5vw, 2.4rem);
}

.grab-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 1.2rem 0 0;
}

.grab input[type='email'] {
  flex: 1 1 15rem;
  min-width: 0;
  font: inherit;
  font-size: 1rem;
  color: var(--bone);
  background: rgba(244, 241, 234, 0.06);
  border: 1px solid rgba(244, 241, 234, 0.22);
  padding: 0.95rem 1rem;
  transition: border-color 0.24s ease;
}

.grab input[type='email']::placeholder {
  color: var(--ash);
}

/* The border change is a second signal, not the only one. The global
   :focus-visible ring at the top of this file stays: this is the one
   place on the site a keyboard user is stuck for a while, and a 1px
   border-colour swap is not an indicator (WCAG 2.2 SC 2.4.11). */
.grab input[type='email']:focus-visible {
  border-color: var(--signal);
}

/* Solid signal block, the same shape as .go filled. */
.grab button {
  flex: 0 0 auto;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 800;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.74rem;
  color: #0a090b;
  background: var(--signal);
  border: 0;
  padding: 1.05rem 1.6rem;
  cursor: pointer;
  transition: opacity 0.24s ease;
}

.grab button:hover,
.grab button:focus-visible {
  opacity: 0.84;
}

.grab button[disabled] {
  opacity: 0.45;
  cursor: default;
}

/* The status line is the only thing that tells the visitor whether the
   address arrived. It is polite-live so a screen reader hears it without
   losing its place. */
.grab-status {
  margin: 0.9rem 0 0;
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--signal);
  min-height: 1.4em;
}

.grab-status:empty {
  min-height: 0;
}

.grab-fine {
  margin: 1rem 0 0;
  font-size: 0.82rem;
  line-height: 1.6;
  color: var(--ash);
}

.grab-fine a {
  color: #cfccd6;
}

/* ---------------------------------------------------------------------
   THE TALK FORM. The only form on the site with more than one field, and
   the only place a fieldset earns its keep.

   The topic picker is five real radio inputs. The input is moved out of
   sight rather than hidden, so it keeps its role, its label and its
   keyboard behaviour, and the block around it lights up through :has().
   A row of divs with click handlers would look identical and be
   unusable with a keyboard.
   --------------------------------------------------------------------- */
.talk {
  display: grid;
  gap: clamp(2.2rem, 5vw, 4.5rem);
  align-items: start;
}

@media (min-width: 950px) {
  .talk {
    grid-template-columns: minmax(0, 1.15fr) minmax(17rem, 0.85fr);
  }
}

.pick {
  border: 0;
  padding: 0;
  margin: 0 0 clamp(1.8rem, 4vw, 2.8rem);
  min-width: 0;
}

.pick legend {
  padding: 0;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  font-size: var(--cap);
  letter-spacing: 0.2em;
  color: var(--ash);
}

.pick-set {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.6rem;
  margin-top: 1.1rem;
}

.pick-o {
  position: relative;
  display: block;
  padding: 1rem 1.1rem;
  border: 1px solid rgba(244, 241, 234, 0.18);
  cursor: pointer;
  transition:
    border-color 0.24s ease,
    background 0.24s ease;
}

.pick-o input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.pick-o b {
  display: block;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 800;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  font-size: 0.76rem;
  color: var(--bone);
}

.pick-o span {
  display: block;
  margin-top: 0.5rem;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--ash);
}

.pick-o:hover {
  border-color: rgba(244, 241, 234, 0.42);
}

.pick-o:has(input:checked) {
  border-color: var(--signal);
  background: color-mix(in srgb, var(--signal) 13%, transparent);
}

.pick-o:has(input:checked) b {
  color: var(--signal);
}

.pick-o:has(input:focus-visible) {
  outline: 3px solid var(--acid);
  outline-offset: 3px;
}

.field {
  display: grid;
  gap: 0.5rem;
  margin: 0 0 1.1rem;
}

.field > label {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  color: var(--ash);
}

.field input,
.field textarea {
  font: inherit;
  font-size: 1rem;
  width: 100%;
  color: var(--bone);
  background: rgba(244, 241, 234, 0.06);
  border: 1px solid rgba(244, 241, 234, 0.22);
  padding: 0.95rem 1rem;
  transition: border-color 0.24s ease;
}

.field textarea {
  min-height: 9.5rem;
  resize: vertical;
  line-height: 1.55;
}

.field input::placeholder,
.field textarea::placeholder {
  color: var(--ash);
}

/* Same as .grab above: the border-colour swap is the second signal and
   the global :focus-visible ring is the first. */
.field input:focus-visible,
.field textarea:focus-visible {
  border-color: var(--signal);
}

/* The aside is a stack of label-and-answer pairs, not a table: a table
   would need a header row nobody would read. */
.talk-side {
  display: grid;
  gap: clamp(1.3rem, 2.6vw, 1.9rem);
  align-content: start;
  border-top: 3px solid var(--signal);
  padding-top: clamp(1.2rem, 2.6vw, 1.8rem);
}

.talk-side p {
  margin: 0.4rem 0 0;
  font-size: 0.95rem;
  line-height: 1.55;
  color: #cfccd6;
}

.talk-side .lab {
  color: var(--ash);
}

/* ---------------------------------------------------------------------
   REVEALS. clip-path wipes, never fades, because a fade is what every
   template on the internet does.

   Gated on html.js: with JavaScript off nothing ever adds .in, and an
   ungated .rv would hide half the page permanently. The class is set by
   a one-line inline script in <head> so there is no unclipped flash.
   --------------------------------------------------------------------- */
.js .rv {
  clip-path: inset(0 0 100% 0);
  transition: clip-path 0.95s var(--ez) var(--rd, 0s);
}

.js .rv.in {
  clip-path: inset(0 0 -14% 0);
}

/* The second beat. One step behind its neighbour, for the body copy that
   follows a heading into view. The only --rd the site actually uses. */
.rv--late {
  --rd: 0.12s;
}

/* Same gate, opposite direction. app.js has always said the print button "is
   hidden without JS because Ctrl or Cmd plus P does the same job"; there was
   no rule doing the hiding, so without JS it rendered as a button that did
   nothing when pressed. The copy buttons are the worse case of the two: a
   dead Copy leaves a producer believing they have a bio on the clipboard.
   The bios themselves are ordinary text on the page either way, so nothing
   is lost by taking the controls away. */
html:not(.js) [data-print],
html:not(.js) [data-copy] {
  display: none;
}

/* ---------------------------------------------------------------------
   THE CATALOGUE. A hundred and thirty paintings on one page.

   Two decisions, and the first one is the whole brief. Kerry asked for
   space. The previous catalogue packed the canvases edge to edge and
   they cancelled each other out, so the gap here is enormous on purpose:
   a whole painting's worth of air between one painting and the next. It
   looks wasteful in a screenshot and correct on a wall.

   The second is CSS columns rather than a row grid. The work runs from
   80 by 80 to 60 by 150, and a row grid either crops the long ones or
   leaves a hole beside them. Columns let every canvas keep its own
   proportion, which is the only honest way to show a painting you are
   asking somebody to buy.
   --------------------------------------------------------------------- */
.sift {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  /* The top padding is not decoration. The nav is fixed and blended, and
     this is the empty band it sits in when the bar is stuck. */
  padding: 3.5rem var(--gut) 1.1rem;
  background: var(--void);
  border-bottom: 1px solid rgba(244, 241, 234, 0.1);
}

.sift-b {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  font-size: calc(var(--cap) * 0.9);
  letter-spacing: 0.1em;
  color: var(--bone);
  background: none;
  border: 0;
  box-shadow: inset 0 0 0 1px rgba(244, 241, 234, 0.22);
  padding: 0.62em 1em;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
}

.sift-b:hover {
  box-shadow: inset 0 0 0 1px var(--signal);
  color: var(--signal);
}

.sift-b[aria-pressed='true'] {
  background: var(--signal);
  box-shadow: inset 0 0 0 1px var(--signal);
  color: var(--void);
}

.sift-b i {
  font-style: normal;
  opacity: 0.6;
  margin-left: 0.5em;
}

.sift-b[aria-pressed='true'] i {
  opacity: 0.75;
}

.mosaic {
  padding: clamp(3rem, 7vw, 6rem) var(--gut) clamp(4rem, 9vw, 8rem);
  column-width: clamp(15rem, 24vw, 23rem);
  column-gap: clamp(2.6rem, 6.5vw, 7rem);
}

.tile {
  break-inside: avoid;
  margin: 0 0 clamp(4rem, 9vw, 9rem);
  /* break-inside is advisory in some engines. inline-block on the child
     of a column makes it binding everywhere. */
  display: inline-block;
  width: 100%;
}

.tile-open {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
  overflow: hidden;
}

.tile-open img {
  display: block;
  width: 100%;
  height: auto;
  transition: transform 0.8s var(--ez), filter 0.5s ease;
}

.tile-open:hover img,
.tile-open:focus-visible img {
  transform: scale(1.035);
  filter: saturate(1.12);
}

.tile-cap {
  display: grid;
  gap: 0.18rem;
  padding-top: 0.95rem;
}

.tile-t {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 100, 'wght' 800;
  font-size: clamp(0.95rem, 1.5vw, 1.15rem);
  line-height: 1.15;
  letter-spacing: -0.01em;
}

.tile-m {
  font-size: 0.82rem;
  color: var(--ash);
}

.tile-tag {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  text-transform: uppercase;
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  color: var(--signal);
  margin-top: 0.2rem;
}

.tile[hidden] {
  display: none;
}

/* The count line, and the empty state that should never fire but will
   the first time somebody adds a category with nothing in it. */
.sift-count {
  padding: clamp(1.4rem, 3vw, 2rem) var(--gut) 0;
  color: var(--ash);
  font-size: 0.85rem;
}

/* ---------------------------------------------------------------------
   THE LENS. A native <dialog>, because showModal() gives a focus trap,
   an inert background and Escape for free, and every hand-rolled
   lightbox on the internet gets at least one of those three wrong.
   --------------------------------------------------------------------- */
.lens {
  width: min(96vw, 92rem);
  max-width: none;
  max-height: 94svh;
  padding: 0;
  border: 0;
  background: var(--void-2);
  color: var(--bone);
  overflow: visible;
}

.lens::backdrop {
  background: rgba(6, 5, 7, 0.94);
}

.lens-in {
  display: grid;
  gap: clamp(1.2rem, 3vw, 2.4rem);
  padding: clamp(1.4rem, 3vw, 2.6rem);
  max-height: 94svh;
  overflow-y: auto;
}

@media (min-width: 60rem) {
  .lens-in {
    grid-template-columns: minmax(0, 1.35fr) minmax(17rem, 0.65fr);
    align-items: start;
  }
}

.lens-pic {
  display: grid;
  place-items: center;
  min-height: 0;
}

.lens-pic img {
  max-width: 100%;
  max-height: 74svh;
  width: auto;
  height: auto;
  object-fit: contain;
}

.lens-body h2 {
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 110, 'wght' 900;
  font-size: clamp(1.5rem, 3.4vw, 2.6rem);
  line-height: 0.98;
  letter-spacing: -0.02em;
  margin: 0 0 0.5rem;
}

.lens-meta {
  color: var(--ash);
  font-size: 0.9rem;
  margin: 0 0 1.1rem;
}

.lens-note {
  max-width: 46ch;
  margin: 0 0 1.4rem;
}

.lens-x,
.lens-step {
  position: absolute;
  z-index: 2;
  font-family: var(--dsp);
  font-variation-settings: 'wdth' 62, 'wght' 700;
  font-size: 1rem;
  line-height: 1;
  color: var(--bone);
  background: var(--void);
  border: 0;
  box-shadow: inset 0 0 0 1px rgba(244, 241, 234, 0.25);
  width: 2.9rem;
  height: 2.9rem;
  cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease;
}

.lens-x:hover,
.lens-step:hover {
  background: var(--signal);
  color: var(--void);
}

.lens-x {
  top: -1.45rem;
  right: -1.45rem;
  border-radius: 50%;
}

.lens-step {
  top: 50%;
  transform: translateY(-50%);
}

.lens-step[data-step='-1'] {
  left: -1.45rem;
}

.lens-step[data-step='1'] {
  right: -1.45rem;
}

/* The offsets hang off the edge of the screen long before 60rem.
   .lens is min(96vw, 92rem), so below a 1533px viewport it leaves
   exactly 2vw of margin each side and the buttons sit 1.45rem (23.2px)
   outside it. 2vw is less than 23.2px below 1160px, so the geometry
   breaks there, not at 960px. 75rem clears it with room to spare. */
@media (max-width: 75rem) {
  .lens-x {
    top: 0.5rem;
    right: 0.5rem;
  }
  .lens-step {
    top: auto;
    bottom: 0.5rem;
    transform: none;
  }
  .lens-step[data-step='-1'] {
    left: 0.5rem;
  }
  .lens-step[data-step='1'] {
    right: 0.5rem;
  }
}

/* The room shot. Half the questions Kerry answers are about size, so
   the picture that answers them is one button away rather than a page
   away. */
.lens-room {
  margin-top: 1.2rem;
}

.lens-room img {
  display: block;
  width: 100%;
  height: auto;
}

.lens-room[hidden] {
  display: none;
}

/* ---------------------------------------------------------------------
   REDUCED MOTION. Everything above resolves to a static, legible page.
   Nothing is merely slowed down: the sticky stack becomes a stack of
   normal sections and both rails become scrollable with the thumb.
   --------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition-duration: 0.01ms !important;
  }

  .pit-frame {
    opacity: 0;
  }
  .pit-frame:nth-child(1) {
    opacity: 1;
  }
  .pit-frame img {
    transform: none;
  }
  .pit-name span {
    clip-path: none;
  }

  .js .rv {
    clip-path: none;
  }

  /* The scroll container is the parent, not the row. .rib-row is a flex
     row of flex: none children, so it is sized by its own content and
     has nothing of its own to scroll; the overflow it creates belongs to
     .ribbon and .chapters, which clip it at :769 and :812. Same shape as
     the .wall-pin fix below, and it was the same mistake. */
  .ribbon,
  .chapters {
    overflow-x: auto;
    overflow-y: hidden;
  }
  .rib-row {
    transform: none !important;
  }

  .stack {
    padding-bottom: 0;
  }
  .panel {
    position: static;
    height: auto;
    min-height: 0;
    padding-block: clamp(3rem, 8vw, 6rem);
    transform: none !important;
    filter: none !important;
  }
  .panel-bed {
    position: absolute;
  }

  .wall {
    height: auto;
  }
  /* .wall-rail is width: max-content at :1067 — a box sized to its own
     content has no overflow of its own, so overflow-x on it was inert
     and the fourteen canvases stayed unreachable. The rail overflows
     .wall-pin, and .wall-pin is what clips it (overflow: hidden, :1042),
     so the reset has to land there. Unpinning it was never enough. */
  .wall-pin {
    position: static;
    height: auto;
    overflow-x: auto;
    overflow-y: hidden;
    padding-block: clamp(3rem, 7vw, 5rem);
  }
  .wall-rail {
    transform: none !important;
    padding-bottom: 3rem;
  }
  .canvas figcaption {
    opacity: 1;
    transform: none;
  }

  /* The quiz keeps every state change, it just arrives instantly. The one
     thing worth undoing is the full-height centring: with motion off there
     is no sense of a screen swapping, so a tall empty column above each
     question reads as a broken page rather than a deliberate one. */
  .quiz {
    min-height: 0;
    padding-top: clamp(5rem, 10vw, 7rem);
  }
  .quiz-meter i {
    transition: none;
  }

  .tile-open:hover img,
  .tile-open:focus-visible img {
    transform: none;
    filter: none;
  }
}

/* ---------------------------------------------------------------------
   PRINT.

   app.js has carried a [data-print] handler and a comment saying "the press
   page has a print stylesheet that turns it into a one-sheet" since it was
   written. There was no such stylesheet. The button worked and printed the
   screen: a fixed nav stamped across the top, a hero photograph bleeding
   four inches of near-black ink, and every .rv block clipped to nothing
   because an IntersectionObserver never fires against a print viewport. A
   producer who pressed it got one dark page and no bio.

   press.html is the only caller. The rules are still written generally,
   because the next person to press Ctrl+P is at least as likely to be on
   about.html or book.html, and a page that prints badly is a page nobody
   knew was broken.

   Colour is handled at the token level rather than component by component:
   redefining the eight variables under :root turns the whole system over in
   one place, and the four accents collapse to near-black on purpose. A press
   kit gets printed on an office laser, and magenta at #ff16d6 lands there as
   a mid grey with no more meaning than the ink it cost.
   --------------------------------------------------------------------- */
@media print {
  @page {
    margin: 14mm;
  }

  :root {
    --void: #fff;
    --void-2: #fff;
    --bone: #17161a;
    --ash: #55525c;
    --signal: #17161a;
    --mag: #17161a;
    --blue: #17161a;
    --flame: #17161a;
    --acid: #17161a;
  }

  /* Several components set a literal hex rather than a token (.page-lede,
     .panel-grid p, .thread-body p, .epk-b p), so the token switch above does
     not reach them. This does, bluntly. The softer greys below win it back on
     specificity, which is why they come after and are also !important. */
  body,
  body * {
    background: transparent !important;
    color: #17161a !important;
    box-shadow: none !important;
    text-shadow: none !important;
    mix-blend-mode: normal !important;
    filter: none !important;
  }

  .quiet,
  .cut-m,
  .price p,
  .grab-fine {
    color: #55525c !important;
  }

  /* Reveals are clip-path gated on html.js, and .in is added by an
     IntersectionObserver. Nothing below the fold ever intersects a print
     viewport, so without this the printout is the hero and six inches of
     white. Same reason motion has to be off rather than instant. */
  .js .rv,
  .js .rv.in {
    clip-path: none !important;
  }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }

  /* Chrome, decoration, and the controls that mean nothing on paper. The
     copy button is the clearest case: it prints as the word "Copy" under a
     bio that has already been copied, onto a sheet. */
  .grain,
  .nav,
  .skip,
  .ribbon,
  .chapters,
  .foot,
  .badge,
  .page-bed,
  .bed-img,
  .panel-bed,
  .go,
  .go-row,
  .grab,
  [data-print],
  [data-copy] {
    display: none !important;
  }

  .page::after,
  .page:has(.page-bed)::after,
  .panel::after {
    display: none !important;
  }

  /* Every band on this site is a full-bleed grid with viewport padding and a
     sticky or stacked position. On paper it is a document: blocks, in order,
     with a gap between them. */
  .page,
  .sec,
  .stack,
  .panel,
  .thread,
  .thread-body,
  .call {
    display: block !important;
    position: static !important;
    min-height: 0 !important;
    overflow: visible !important;
    padding: 0 0 8mm !important;
  }

  body {
    font-size: 10pt;
    line-height: 1.45;
  }

  /* The screen headline is 11.5vw. On A4 that is a five-inch word. */
  .page-h {
    font-size: 26pt !important;
    line-height: 0.98;
    margin: 0 0 3mm;
  }

  .page-lede {
    font-size: 11pt !important;
    max-width: none;
  }

  .sec-h,
  .thread-h,
  .call-h,
  .panel-h {
    font-size: 15pt !important;
    line-height: 1.05;
    margin: 0 0 3mm;
  }

  .lab,
  .page-idx {
    font-size: 8pt !important;
  }

  .prose p,
  .epk-b p,
  .panel-grid p,
  .thread-body p,
  .price p {
    max-width: none;
  }

  /* The three beds above are decoration and go. This one is the deliverable:
     a press sheet with no photograph of the person on it is half a press
     sheet. .thread-pic is 60svh of cover-cropped image on screen, which on
     paper is most of a page of ink, so it prints as a thumbnail at its own
     aspect ratio instead. */
  .thread-pic {
    min-height: 0 !important;
    margin-bottom: 4mm;
  }

  .thread-pic img {
    width: 55mm !important;
    height: auto !important;
    object-fit: contain !important;
  }

  /* The row rules are rgba(bone, 0.16), which was a hairline on black and is
     nothing at all on white. */
  .cut {
    border-color: rgba(23, 22, 26, 0.3) !important;
  }

  a {
    color: inherit !important;
    text-decoration: underline;
  }

  /* A bio split across a page break is a bio a producer has to reassemble. */
  .epk-b,
  .price,
  .cut,
  .pull {
    break-inside: avoid;
  }

  h1,
  h2,
  h3 {
    break-after: avoid;
  }

  .print-only {
    display: block !important;
  }
}

/* The other half of .print-only: hidden on screen, shown by the block above.
   It exists for one line — a printed sheet has to carry the address of the
   page it came from, and on screen that line is in the browser's chrome. */
.print-only {
  display: none;
}
