/* ============================================================
   frankbeddor.com — fresh rebuild
   Clean contemporary independent-creator site: white ground,
   black text, generous air, hairline rules.
   Type families carried over from the previous pass.
   ============================================================ */

:root {
  /* ground + ink */
  --bg:          #ffffff;
  --surface:     #f7f7f6;
  --ink:         #000000;
  --ink-soft:    #3d3d3d;   /* secondary copy */
  --ink-mute:    #6b6b6b;   /* meta / captions — 5.3:1 on white */
  --rule:        #e6e4e0;
  --rule-strong: #111111;

  /* Single accent, deliberately black for now so the palette stays
     monochrome. Swap this one value to introduce a colour later. */
  --accent:      #000000;

  --on-dark:     #ffffff;
  --scrim:       rgba(0, 0, 0, .42);

  /* type */
  --display: 'Cinzel', 'Trajan Pro', Georgia, serif;
  --serif:   'Alice', Georgia, 'Times New Roman', serif;
  --sans:    'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* metrics */
  --wrap:    1320px;
  --gut:     clamp(1.25rem, 4vw, 3rem);
  --header-h: 76px;
  --ease:    cubic-bezier(.22, .61, .36, 1);
}

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

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

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

button { font: inherit; color: inherit; }

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

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

.skip {
  position: absolute; left: -9999px;
  background: var(--ink); color: var(--on-dark);
  padding: .8rem 1.4rem; z-index: 200;
  font-size: .8rem; letter-spacing: .12em; text-transform: uppercase;
}
.skip:focus { left: 1rem; top: 1rem; }

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

/* ============================================================
   Header
   ============================================================ */

.masthead {
  position: sticky; top: 0; z-index: 100;
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
}

.masthead .wrap {
  display: flex; align-items: center;
  gap: 2rem;
  min-height: var(--header-h);
}

.wordmark {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(1rem, 1.6vw, 1.22rem);
  letter-spacing: .2em;
  text-transform: uppercase;
  white-space: nowrap;
  margin-right: auto;
}

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

.nav {
  display: flex; align-items: center;
  gap: clamp(.5rem, 2vw, 2rem);
  list-style: none; margin: 0; padding: 0;
}

.nav-item { position: relative; }

.nav-link {
  display: inline-flex; align-items: center; gap: .4em;
  background: none; border: 0; cursor: pointer;
  padding: .5rem 0;
  font-family: var(--sans);
  font-size: .76rem;
  font-weight: 600;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
  position: relative;
}

/* Hairline that grows in on hover — the only nav ornament. */
.nav-link::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: -2px;
  height: 1px; background: var(--ink);
  transform: scaleX(0); transform-origin: left;
  transition: transform .28s var(--ease);
}
.nav-link:hover::after,
.nav-link[aria-expanded='true']::after,
.nav-link[aria-current='page']::after { transform: scaleX(1); }

.nav-link .chev {
  width: 9px; height: 9px;
  transition: transform .25s var(--ease);
}
.nav-link[aria-expanded='true'] .chev { transform: rotate(180deg); }

/* --- dropdown panel --- */

.nav-menu {
  position: absolute; top: calc(100% + 1px); left: -1.25rem;
  min-width: 264px;
  background: var(--bg);
  border: 1px solid var(--rule);
  box-shadow: 0 18px 40px -20px rgba(0, 0, 0, .28);
  padding: .5rem;
  list-style: none; margin: 0;
  opacity: 0; visibility: hidden;
  transform: translateY(-6px);
  /* Visibility must flip instantly on open, or a link focused straight after
     opening (ArrowDown from the trigger) isn't focusable yet. Only the close
     is delayed, so the fade still plays out. */
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility 0s linear .2s;
}
.nav-item[data-open='true'] .nav-menu {
  opacity: 1; visibility: visible; transform: translateY(0);
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility 0s linear 0s;
}

.nav-menu a {
  display: block;
  padding: .7rem 1rem;
  font-size: .88rem;
  letter-spacing: .01em;
  color: var(--ink-soft);
  transition: background .18s var(--ease), color .18s var(--ease);
}
.nav-menu a:hover, .nav-menu a:focus-visible {
  background: var(--surface);
  color: var(--ink);
}
/* --- a group inside the panel --- */

/* Biography lives inside Start Here as a group: one row that opens a
   small panel of its own with the three chapters in it. Frank's call
   that the chapters stay out of sight until that row is hovered or
   clicked — an earlier pass listed them inline under a heading,
   which put three extra rows in the panel whether you wanted them or
   not.

   The row is a button, not a link, because there is no /biography/
   page behind it. The chapters are the pages. */
.nav-menu .menu-group { position: relative; }

.menu-group-toggle {
  display: flex; align-items: center; justify-content: space-between;
  gap: .5rem;
  width: 100%;
  padding: .7rem 1rem;
  background: none; border: 0;
  font-family: inherit; font-size: .88rem;
  letter-spacing: .01em;
  text-align: left;
  color: var(--ink-soft);
  cursor: pointer;
  transition: background .18s var(--ease), color .18s var(--ease);
}
.menu-group-toggle:hover,
.menu-group-toggle:focus-visible,
.menu-group[data-open='true'] .menu-group-toggle {
  background: var(--surface);
  color: var(--ink);
}

/* The nav's own chevron, turned on its side: this panel opens
   sideways, so the arrow points the way it goes. */
.menu-group-toggle .chev {
  width: 10px; height: 6px; flex: none;
  transform: rotate(-90deg);
  transition: transform .2s var(--ease);
}

.menu-sub {
  position: absolute; top: -.5rem; left: 100%;
  min-width: 200px;
  background: var(--bg);
  border: 1px solid var(--rule);
  box-shadow: 0 18px 40px -20px rgba(0, 0, 0, .28);
  padding: .5rem;
  list-style: none; margin: 0;
  opacity: 0; visibility: hidden;
  transform: translateX(-6px);
  /* Same trick as .nav-menu: visibility flips instantly on open so a
     link focused straight afterwards is focusable, and only the
     close is delayed so the fade still plays. */
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility 0s linear .2s;
}
.menu-group[data-open='true'] .menu-sub {
  opacity: 1; visibility: visible; transform: translateX(0);
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility 0s linear 0s;
}

/* Not enough room to the right — initGroups() measures on open and
   flips it to the other side. CSS can't ask how wide the window is
   relative to an element, so this one bit has to be JS. */
.menu-group[data-flip='true'] .menu-sub {
  left: auto; right: 100%;
  transform: translateX(6px);
}
.menu-group[data-flip='true'][data-open='true'] .menu-sub { transform: translateX(0); }

.nav-menu .menu-note {
  display: block;
  font-size: .72rem;
  color: var(--ink-mute);
  margin-top: .15rem;
}

/* Store reads as the one commercial action. */
.nav-store {
  border: 1px solid var(--ink);
  padding: .55rem 1.1rem;
  transition: background .22s var(--ease), color .22s var(--ease);
}
.nav-store::after { content: none; }
.nav-store:hover { background: var(--ink); color: var(--on-dark); }

/* --- mobile toggle --- */

.nav-toggle {
  display: none;
  background: none; border: 1px solid var(--rule);
  padding: .55rem .9rem; cursor: pointer;
  font-size: .72rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
}

/* ============================================================
   Marquee carousel
   ============================================================ */

.marquee {
  position: relative;
  background: var(--surface);
  overflow: hidden;
}

.marquee-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.marquee-track::-webkit-scrollbar { display: none; }

.marquee-slide {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* 3:1, built to the 2000x667 slide art. The title and cover sit in the
   middle of each slide, so phones can take a slightly taller centre crop
   (see the 560px block) without touching the lettering. */
.marquee-media {
  aspect-ratio: 3 / 1;
  overflow: hidden;
  background: #ddd;
}
.marquee-media img {
  width: 100%; height: 100%;
  object-fit: cover;
}

/* The whole slide is the link. No button and no scrim sit over the art,
   so the key art reads exactly as it was drawn. */
.marquee-link {
  display: block;
  color: inherit;
  text-decoration: none;
}
.marquee-link:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: -3px;
}
.marquee-link img { transition: transform .5s var(--ease); }
@media (hover: hover) and (pointer: fine) {
  .marquee-link:hover img { transform: scale(1.015); }
}

/* --- controls --- */

.marquee-nav {
  position: absolute; top: 50%;
  transform: translateY(-50%);
  width: 46px; height: 46px;
  display: grid; place-items: center;
  background: rgba(255, 255, 255, .9);
  border: 0; cursor: pointer;
  color: var(--ink);
  transition: background .2s var(--ease);
}
.marquee-nav:hover { background: #fff; }
.marquee-nav[data-dir='prev'] { left: clamp(.5rem, 2vw, 1.5rem); }
.marquee-nav[data-dir='next'] { right: clamp(.5rem, 2vw, 1.5rem); }
.marquee-nav svg { width: 16px; height: 16px; }

.marquee-dots {
  position: absolute;
  bottom: clamp(.75rem, 2vw, 1.25rem);
  right: var(--gut);
  display: flex; gap: .5rem;
  list-style: none; margin: 0; padding: 0;
}
.marquee-dots button {
  width: 30px; height: 3px;
  background: rgba(255, 255, 255, .45);
  border: 0; padding: 0; cursor: pointer;
  transition: background .25s var(--ease);
}
.marquee-dots button[aria-current='true'] { background: #fff; }

/* ============================================================
   Shared page furniture
   ============================================================ */

.section {
  padding-block: clamp(3rem, 7vw, 5.5rem);
}
.section-tint { background: var(--surface); }

.section-title {
  font-family: var(--display);
  font-weight: 400;
  /* Was clamp(1.3rem, 2.6vw, 1.9rem), which left the section headings
     the smallest display type on the site — under .start-title and
     .creed-title at 3.1rem, and doing more structural work than either.
     Still deliberately below those and well below .work-title, so a
     section heading never reads as a page title. */
  font-size: clamp(1.75rem, 4vw, 2.75rem);
  /* Tracking eases off as the size goes up; .1em was set for 1.9rem and
     reads loose at 2.75rem. */
  letter-spacing: .08em;
  text-transform: uppercase;
  text-align: center;
  margin: 0 0 clamp(2rem, 4vw, 3rem);
  line-height: 1.25;
}

/* The control shared by .btn and .btn-outline. These were written as
   .btn plus an .btn-outline modifier, but three places use the
   modifier on its own, where it styled a raw browser button — so the
   shape lives here and each variant only chooses its colours. */
.btn, .btn-outline {
  display: inline-block;
  border: 1px solid var(--ink);
  padding: .85rem 2rem;
  font-size: .72rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  line-height: 1.2;
  cursor: pointer;
  transition: background .22s var(--ease), color .22s var(--ease);
}

/* Solid counterpart to .btn-light, for use on the white ground. */
.btn {
  background: var(--ink);
  color: var(--on-dark);
}
.btn:hover { background: var(--bg); color: var(--ink); }

/* Small uppercase label — the one recurring micro-type. */
.label {
  font-family: var(--sans);
  font-size: .72rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0;
}

/* ============================================================
   Product — single item
   Layout mirrors the previous site: bordered card on a tinted
   ground, image slider left, title/description/price/cart right.
   ============================================================ */

.product-card {
  display: grid;
  grid-template-columns: 1fr 1fr;
  background: var(--bg);
  border: 1px solid var(--rule);
  max-width: 1160px;
  margin-inline: auto;
}

/* --- gallery --- */

.gallery {
  position: relative;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  display: flex; flex-direction: column;
  justify-content: center;
  border-right: 1px solid var(--rule);
}

.gallery-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.gallery-track::-webkit-scrollbar { display: none; }

.gallery-slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* One fixed well so the slides don't jump between a portrait cover
   and a landscape interior spread. The image is sized to the well
   rather than max-height'd into it — an intrinsically tall image
   beats `aspect-ratio` and stretches the box otherwise. */
.gallery-media {
  width: 100%;
  aspect-ratio: 3 / 4;
  padding: clamp(.75rem, 2vw, 1.25rem);
  border: 1px solid var(--rule);
  background: var(--bg);
}
.gallery-media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.gallery-nav,
.quotes-nav {
  position: absolute; top: 50%;
  transform: translateY(-50%);
  width: 40px; height: 40px;
  display: grid; place-items: center;
  background: var(--bg);
  border: 1px solid var(--rule);
  cursor: pointer;
  color: var(--ink);
  transition: background .2s var(--ease), color .2s var(--ease);
  z-index: 2;
}
.gallery-nav:hover:not(:disabled),
.quotes-nav:hover:not(:disabled) { background: var(--ink); color: var(--on-dark); }
.gallery-nav:disabled,
.quotes-nav:disabled { opacity: .3; cursor: default; }
.gallery-nav[data-dir='prev'],
.quotes-nav[data-dir='prev'] { left: clamp(.25rem, 1vw, .75rem); }
.gallery-nav[data-dir='next'],
.quotes-nav[data-dir='next'] { right: clamp(.25rem, 1vw, .75rem); }
.gallery-nav svg,
.quotes-nav svg { width: 14px; height: 14px; }

.gallery-dots,
.quotes-dots {
  display: flex; justify-content: center;
  gap: .5rem;
  list-style: none; margin: 1.25rem 0 0; padding: 0;
}
.quotes-dots { margin-top: clamp(1.75rem, 3vw, 2.5rem); }
.gallery-dots button,
.quotes-dots button {
  width: 34px; height: 3px;
  background: var(--rule);
  border: 0; padding: 0; cursor: pointer;
  transition: background .25s var(--ease);
}
.gallery-dots button:hover,
.quotes-dots button:hover { background: var(--ink-mute); }
.gallery-dots button[aria-current='true'],
.quotes-dots button[aria-current='true'] { background: var(--ink); }

/* --- summary --- */

.product-info {
  padding: clamp(1.75rem, 3.5vw, 3rem);
  display: flex; flex-direction: column;
  justify-content: center;
}

.product-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.5rem, 3vw, 2.3rem);
  line-height: 1.15;
  letter-spacing: .04em;
  text-transform: uppercase;
  margin: 0 0 .6rem;
}

/* Format/edition line — sits between the title and the blurb. */
.product-subtitle {
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0 0 1.6rem;
}

.product-desc {
  color: var(--ink-soft);
  font-size: .97rem;
  margin-bottom: 2rem;
}
.product-desc p { margin: 0 0 1em; }
.product-desc p:last-child { margin-bottom: 0; }
.product-desc b { color: var(--ink); }

.product-buy {
  display: flex; flex-direction: column;
  gap: 1.5rem;
  border-top: 1px solid var(--rule);
  padding-top: 1.75rem;
}

/* Price and cart sit side by side, as on the previous site. */
.buy-row {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 3vw, 2.5rem);
}

/* --- format selector (variable products) --- */

.variants { display: flex; flex-direction: column; gap: .7rem; }

.variant-group { display: flex; gap: .6rem; }

.variant {
  position: relative;
  flex: 1 1 0;
  display: flex; flex-direction: column;
  gap: .2rem;
  padding: .8rem 1rem;
  border: 1px solid var(--rule);
  cursor: pointer;
  transition: border-color .2s var(--ease),
              background .2s var(--ease),
              color .2s var(--ease);
}

/* The radio stays in the page for keyboard and screen readers; the label
   around it carries the whole visual treatment. */
.variant input {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

.variant:hover { border-color: var(--ink-mute); }
.variant:has(input:checked) {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--on-dark);
}
.variant:has(input:focus-visible) { outline: 2px solid var(--ink); outline-offset: 2px; }

.variant-name {
  font-size: .7rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
}
.variant-price { font-family: var(--serif); font-size: 1.05rem; }

/* The save line disappears when a selected format isn't discounted. */
.price-line[hidden] { display: none; }

.price-block { flex: 1 1 50%; }

.price-line {
  display: flex; align-items: baseline;
  flex-wrap: wrap;
  gap: .6rem;
}
.price-line + .price-line { margin-top: .5rem; }

.price { margin: 0; font-family: var(--serif); }
.price del {
  color: var(--ink-mute);
  font-size: 1.05rem;
  margin-right: .4rem;
}
.price ins {
  text-decoration: none;
  color: var(--ink);
  font-size: 1.6rem;
}
/* Single-price products have no <ins> to carry the size. */
.price > .amount { font-size: 1.6rem; }

.price-save {
  font-family: var(--serif);
  font-size: 1.05rem;
  color: var(--ink-soft);
  margin: 0;
}

.cart-block { flex: 1 1 50%; }

.cart {
  display: flex; flex-wrap: wrap;
  align-items: stretch;
  gap: .75rem;
}

.qty {
  width: 4.5rem;
  padding: .8rem .6rem;
  border: 1px solid var(--rule);
  background: var(--bg);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1rem;
  text-align: center;
}
.qty:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* Shrinkable so quantity + button stay on one row in the half-width
   cart column; the note below is the only intended wrap. */
.cart .btn {
  flex: 1 1 7rem;
  padding-inline: 1.25rem;
}

/* Stands in for the cart until checkout exists — see the note in
   pages/product.html. Deliberately typeset as a statement rather than a
   disabled button: a greyed-out Add to cart invites a click and then
   reads as broken, where a line of type reads as intended. Sized to sit
   where the button was so the buy row keeps its shape. */
.ordering-soon {
  margin: 0;
  padding: .95rem 1.1rem;
  border: 1px solid var(--rule);
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-mute);
  text-align: center;
}

/* Concept build: nothing behind the button yet. */
.cart-note {
  flex: 1 0 100%;
  margin: .25rem 0 0;
  font-size: .68rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* ============================================================
   Testimonials
   ============================================================ */

/* One quote at a time. Flex slides all take the height of the tallest,
   so the section doesn't jump as you move between a two-line quote and
   a ten-line one. */
.quotes { position: relative; }

.quotes-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.quotes-track::-webkit-scrollbar { display: none; }

.quote {
  flex: 0 0 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  margin: 0;
  padding: 0 clamp(2.75rem, 8vw, 6rem);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
}

.quote p {
  font-family: var(--serif);
  font-size: clamp(1.05rem, 1.9vw, 1.4rem);
  line-height: 1.65;
  color: var(--ink-soft);
  margin: 0 0 1.75rem;
  max-width: 54ch;
}

.quote figcaption {
  display: flex; align-items: center;
  gap: .7rem;
  font-size: .7rem; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--ink);
}
.quote figcaption::before {
  content: '';
  flex: none;
  width: 1.6rem; height: 1px;
  background: var(--ink);
}

/* ============================================================
   Related products
   ============================================================ */

.product-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.25rem, 3vw, 2.5rem);
}

.card { display: flex; flex-direction: column; }

.card-media {
  aspect-ratio: 3 / 4;
  padding: clamp(1rem, 2vw, 1.75rem);
  background: var(--bg);
  border: 1px solid var(--rule);
  overflow: hidden;
}
.card-media img {
  width: 100%; height: 100%;
  object-fit: contain;
  transition: transform .4s var(--ease);
}
.card:hover .card-media img { transform: scale(1.04); }

.card-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: .95rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  line-height: 1.35;
  margin: 1rem 0 .4rem;
}
.card:hover .card-title { text-decoration: underline; text-underline-offset: .25em; }

/* Cards stretch to the tallest in their row; pinning the price to the
   bottom keeps prices on one line across the row however the titles wrap. */
.card .price { font-size: .95rem; margin-top: auto; padding-top: .35rem; }
.card .price del { font-size: .9rem; }
.card .price ins,
.card .price > .amount { font-size: 1.15rem; }

/* ============================================================
   Store gallery
   ============================================================ */

.store-head { text-align: center; }
.store-head .section-title { margin-bottom: .5rem; }

.store-count {
  font-size: .72rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0 0 clamp(2.5rem, 5vw, 4rem);
}

/* Four across once there's room — 47 items in three columns is a very
   long scroll. */
.store-grid {
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1.25rem, 2.5vw, 2.25rem) clamp(1rem, 2vw, 1.75rem);
}

/* A product without a page yet: same card, just not clickable. */
.card-unlinked { cursor: default; }
.card-unlinked .card-media img { transition: none; }
.card-unlinked:hover .card-media img { transform: none; }
.card-unlinked:hover .card-title { text-decoration: none; }

/* ============================================================
   Podcast
   ============================================================ */

/* Masthead. Same bordered card on a tint as a product page, but the
   art column is squarer — podcast cover art is 1:1, not 3:4. */
.show-card {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  align-items: center;
  background: var(--bg);
  border: 1px solid var(--rule);
  max-width: 1160px;
  margin-inline: auto;
}

.show-art {
  padding: clamp(1.5rem, 3vw, 2.5rem);
  border-right: 1px solid var(--rule);
}
.show-art img {
  width: 100%; height: auto;
  aspect-ratio: 1 / 1;
  object-fit: contain;
}

.show-intro { padding: clamp(1.75rem, 3.5vw, 3rem); }

/* The intro column is wider than a product's, so the blurb needs its own
   measure or it runs to ~95 characters on a large screen. */
.show-intro .product-desc { max-width: 62ch; }

/* The two headline listen links. They wrap rather than shrink, so the
   letter-spaced labels never get squeezed. */
.show-cta {
  display: flex; flex-wrap: wrap;
  gap: .75rem;
  border-top: 1px solid var(--rule);
  padding-top: 1.75rem;
}
/* These two labels are long for letter-spaced caps; the default .btn
   padding tips them onto separate lines around 1000px for no good reason. */
.show-cta .btn { padding-inline: 1.5rem; }

/* Outline counterpart to .btn, for the secondary action on white. */
.btn-outline {
  background: var(--bg);
  color: var(--ink);
}
.btn-outline:hover { background: var(--ink); color: var(--on-dark); }

/* --- episode player --- */

/* Apple sizes its own embed; the border keeps it from floating free of
   the page's hairline grammar. */
.show-embed {
  max-width: 1160px;
  margin-inline: auto;
  border: 1px solid var(--rule);
}
.show-embed iframe {
  display: block;
  width: 100%; height: 450px;
  border: 0;
  background: transparent;
}

/* --- episodes --- */

.ep-feature,
.episodes { max-width: 860px; margin-inline: auto; }

/* The one episode with something to press. Bordered card on the white
   ground, the counterpart to the show card at the top of the page. */
.ep-feature {
  border: 1px solid var(--rule);
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

/* The guest card, edge to edge across the top of the box. Every card in
   the archive is 16:9, so the frame is fixed at that and the odd
   hand-picked portrait crops to fit rather than changing the shape. */
.ep-feature-art {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}
.ep-feature-art img {
  width: 100%; height: 100%;
  object-fit: cover;
}

.ep-feature-body { padding: clamp(1.5rem, 3.5vw, 2.5rem); }

.ep-meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: .7rem;
  font-size: .7rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0 0 .7rem;
}

.ep-tag {
  background: var(--ink);
  color: var(--on-dark);
  padding: .3em .7em;
  letter-spacing: .18em;
}

.ep-feature-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.25rem, 2.4vw, 1.75rem);
  letter-spacing: .03em;
  line-height: 1.25;
  margin: 0 0 .8rem;
}

.ep-feature-blurb {
  font-family: var(--serif);
  color: var(--ink-soft);
  font-size: clamp(1rem, 1.5vw, 1.1rem);
  line-height: 1.6;
  margin: 0 0 1.6rem;
  max-width: 60ch;
}

.ep-feature-play {
  display: inline-flex; align-items: center; gap: .7rem;
}
.ep-feature-icon { display: grid; place-items: center; }
.ep-feature-icon svg { width: 12px; height: 12px; }

/* --- the list --- */

/* Hairline rows, no cards: 41 bordered boxes would fight the page, and
   the run reads faster as one column of rules. */
.episodes {
  list-style: none;
  padding: 0; margin-top: 0;
  border-top: 1px solid var(--rule);
}
.episodes > li { border-bottom: 1px solid var(--rule); }

/* The whole row is the control — a 44px-tall target across the full
   width, rather than a small circle to aim at. */
.ep-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  grid-template-areas: 'num title time icon';
  align-items: center;
  gap: clamp(.75rem, 2vw, 1.25rem);
  width: 100%;
  padding: .95rem .5rem;
  background: none; border: 0;
  text-align: left;
  cursor: pointer;
  transition: background .18s var(--ease);
}
.ep-row:hover { background: var(--surface); }

.ep-num {
  grid-area: num;
  display: inline-grid; place-items: center;
  min-width: 2.4em; height: 1.9em;
  font-size: .7rem; font-weight: 600;
  letter-spacing: .1em;
  color: var(--ink-mute);
  border: 1px solid var(--rule);
}
.ep-row:hover .ep-num { border-color: var(--ink); color: var(--ink); }

.ep-row-title {
  grid-area: title;
  font-family: var(--display);
  font-size: .95rem;
  letter-spacing: .03em;
  line-height: 1.35;
}

.ep-row-time {
  grid-area: time;
  font-size: .7rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-mute);
  white-space: nowrap;
}

.ep-row-icon {
  grid-area: icon;
  display: grid; place-items: center;
  width: 30px; height: 30px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  color: var(--ink);
  transition: background .18s var(--ease), color .18s var(--ease),
              border-color .18s var(--ease);
}
.ep-row-icon svg { width: 11px; height: 11px; }
.ep-row:hover .ep-row-icon { background: var(--ink); color: var(--on-dark); border-color: var(--ink); }

/* One SVG carries both glyphs; the button's state picks which shows, so
   there is no markup to swap and no second icon to load. */
.ep-icon-pause { display: none; }
.js-play[aria-expanded='true'] .ep-icon-play { display: none; }
.js-play[aria-expanded='true'] .ep-icon-pause { display: block; }
.ep-row[aria-expanded='true'] .ep-row-icon { background: var(--ink); color: var(--on-dark); border-color: var(--ink); }

/* Native controls: scrubbing, volume and speed for free, and they stay
   keyboard- and screen-reader-usable without any code of ours. */
.ep-audio {
  width: 100%;
  max-width: 520px;
  margin: 0 0 1rem .5rem;
}
.ep-feature .ep-audio { margin: 1.25rem 0 0; }

/* The second heading in the section needs the air a new section would
   normally give it. */
.section-title-next { margin-top: clamp(3rem, 6vw, 4.5rem); }

.episodes-more {
  display: flex; justify-content: center;
  margin-top: clamp(1.75rem, 3.5vw, 2.5rem);
}

/* --- more ways to listen --- */

/* Six outlets, so three across reads better than four-and-two. The
   narrower measure also keeps each tile close to the badge's own
   196px width rather than stranding it in a wide box. */
.listen-grid {
  list-style: none; margin: 0 auto; padding: 0;
  max-width: 860px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(.75rem, 1.5vw, 1.25rem);
}

/* The badges are supplied artwork at a fixed aspect; the tile holds the
   row height steady so a 47px badge and a 48px one still line up. */
.listen-tile {
  display: grid; place-items: center;
  height: 100%;
  padding: clamp(.9rem, 2vw, 1.4rem);
  border: 1px solid var(--rule);
  background: var(--bg);
  transition: border-color .22s var(--ease);
}
.listen-tile img { width: 100%; max-width: 196px; height: auto; }

a.listen-tile:hover { border-color: var(--rule-strong); }

/* ============================================================
   Work page — a single project, introduced
   Used by Tsunami Surf Sisters. Nothing here is product- or
   podcast-specific, so a second project page can reuse the whole
   block as-is.
   ============================================================ */

/* --- placeholder wells --- */

/* A labelled empty box, for slots whose artwork hasn't landed. It is
   deliberately plain: the moment a real <img> replaces it, nothing
   about the layout should shift. Delete this rule once the site has
   no .ph left. */
.ph {
  display: grid; place-items: center;
  aspect-ratio: 4 / 5;
  background: var(--surface);
  border: 1px dashed var(--rule);
  color: var(--ink-mute);
}
.ph::after {
  content: attr(data-ph);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  text-align: center;
  padding: 1rem;
}
.ph-wide { aspect-ratio: 16 / 9; }
.ph-square { aspect-ratio: 1 / 1; }
.ph-dark {
  background: #1a1a1a;
  border-color: rgba(255, 255, 255, .22);
  color: rgba(255, 255, 255, .5);
  aspect-ratio: auto; height: 100%;
}

/* --- hero --- */

/* The art runs full width; the title sits under it on the tint. It
   has been behind the title, which cost the sky a scrim heavy enough
   to grey it out, and beside it on black, which was too heavy for a
   property this bright. Nothing darkens the drawing now. */
.work-hero { background: var(--surface); }

/* Natural proportions set the height until the cap takes over. On a
   wide screen a 1.82:1 image would otherwise stand nearly a full
   viewport tall and push the title off the bottom of the screen.

   What the cap trims comes off the bottom rather than the middle:
   the rainbow across the top is the best thing in the frame, and the
   bottom of the picture is shadow. */
.work-hero-media img {
  width: 100%; height: auto;
  aspect-ratio: 2000 / 1100;
  max-height: min(72vh, 42rem);
  object-fit: cover;
  object-position: center 30%;
}

/* A scan of a printed page is a document, not key art. Bleeding it to
   the window edge would read as a mistake, and the height cap above
   would crop a line the ad is built around, so this variant shows the
   sheet whole, bounded, with the site's hairline for an edge.

   The width is what holds the height down instead: at the full 1320
   the ad alone is most of a laptop screen. */
.work-hero-inset .work-hero-media {
  max-width: 68rem;
  margin-inline: auto;
  padding-inline: var(--gut);
  padding-top: clamp(2rem, 4vw, 3rem);
}
.work-hero-inset .work-hero-media img {
  aspect-ratio: auto;
  max-height: none;
  border: 1px solid var(--rule);
}
.work-hero-body {
  padding-block: clamp(2.25rem, 5vw, 3.5rem);
  text-align: center;
}

.work-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2.25rem, 7vw, 4.75rem);
  letter-spacing: .04em;
  line-height: 1.05;
  margin: 0 auto 1.1rem;
  max-width: 16ch;
}

.work-logline {
  font-family: var(--serif);
  font-size: clamp(1.05rem, 2vw, 1.4rem);
  line-height: 1.55;
  color: var(--ink-soft);
  margin: 0 auto;
  max-width: 52ch;
}

/* A poster is portrait, and the hero above runs its art the full
   width of the window — a 2:3 image treated that way either stands a
   whole screen tall or gets cropped to a letterbox, and a one-sheet
   cropped is a one-sheet ruined. So this variant sets the poster
   beside the copy instead of above it. The title and logline lose
   their centring with the band they were centred in.

   Vertically centred rather than top-aligned: against a tall poster a
   short logline pinned to the top leaves a column of white below it
   that reads as a missing section. */
.work-hero-poster {
  padding-block: clamp(2.5rem, 6vw, 4.5rem);
  text-align: left;
}
.poster-layout {
  display: grid;
  /* A capped width rather than a share of the row: at 5fr the poster
     grew with the window and stood 700px tall on a laptop, which left
     the logline floating in the middle of a very tall band.

     The pair is then narrower than the wrap and centred in it. Given
     the full 1320 the logline hits its own 52ch ceiling around two
     thirds across and leaves the last third of the row empty, which
     reads as a column that failed to load rather than as air. */
  max-width: 66rem;
  margin-inline: auto;
  grid-template-columns: minmax(0, 28rem) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}
.poster-art { margin: 0; }
.poster-art img {
  width: 100%; height: auto;
  border: 1px solid var(--rule);
}
.work-hero-poster .work-title,
.work-hero-poster .work-logline { margin-inline: 0; }
.poster-copy .label { margin-bottom: 1rem; }

/* The comp line. Quieter than the logline and set apart from it,
   because it is shorthand about the film rather than the film's own
   pitch. The titles are cites and stay italic; "meets" is not part of
   either of them and stays upright. */
.work-comp {
  font-family: var(--serif);
  font-size: .95rem;
  letter-spacing: .01em;
  color: var(--ink-mute);
  margin: 1.4rem 0 0;
}
.work-comp cite { font-style: italic; }

/* The billing block. A one-sheet credit in the hero, for a work whose
   authorship needs more than one line to state — SoCal Paradise is
   written by one person from a story by two others, and a jacket says
   so on its face rather than making you turn it over.

   This is not the .team list. That one is a section of its own with a
   heading and ruled rows, and it earns that on a film page with eight
   credits; two rows given the same treatment would read as a page
   component that failed to fill. So: no heading, one hairline to set
   it off from the logline, and the roles in the same micro-type the
   .team list's dt uses, so the two say "credit" in the same voice.

   dt and dd sit in the dl's own grid rather than in .team's wrapper
   divs. Two rows of two cells, and the names line up down the page —
   which is the only reason the wrapper exists on .team, and it is not
   needed for a block this small. */
.work-billing {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: .45rem clamp(1rem, 2.5vw, 1.75rem);
  align-items: baseline;
  margin: 1.6rem 0 0;
  padding-top: 1.3rem;
  border-top: 1px solid var(--rule);
}
.work-billing dt {
  font-family: var(--sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
}
.work-billing dd {
  font-family: var(--display);
  font-size: 1.05rem;
  letter-spacing: .04em;
  line-height: 1.35;
  margin: 0;
}
/* A name is one word. On a phone the column is about 230px and
   "Frank Beddor & Liz Cavalier" broke after "Liz", which on a credit
   reads as a typesetting mistake. It now wraps at the ampersand or
   not at all. */
.work-billing dd span { white-space: nowrap; }

/* --- about --- */

/* Prose leads; the image is support, so it takes the narrower column
   and sticks while the copy scrolls past on a tall screen. A third of
   the width suits a portrait cover — at 5fr a 2:3 cover stood a long
   way past the bottom of the copy beside it. */
.work-split {
  display: grid;
  grid-template-columns: minmax(0, 8fr) minmax(0, 4fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}

/* A section title that opens a column rather than a band shouldn't be
   centred with the band titles. */
.work-section-title { text-align: left; }

.work-prose {
  font-family: var(--serif);
  font-size: 1.08rem;
  line-height: 1.75;
  color: var(--ink-soft);
  max-width: 62ch;
}
.work-prose p { margin: 0 0 1.1em; }
.work-prose p:last-child { margin-bottom: 0; }

.work-aside { margin: 0; position: sticky; top: calc(var(--header-h) + 2rem); }

/* The base img rule caps width but says nothing about height, so an
   image carrying width/height attributes keeps its full attribute
   height while the width shrinks to the column — stretched. Every
   other image on the site answers this in its own rule; this one
   scales. */
.work-aside img { width: 100%; height: auto; }
.work-aside figcaption {
  font-size: .78rem;
  color: var(--ink-mute);
  margin-top: .75rem;
  line-height: 1.5;
}

/* --- the sisters --- */

.work-cast {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vw, 2rem);
}

/* Two of the five carry a nickname, which pushed their ocean line
   below everyone else's. Cards stretch to the tallest in the row, so
   pinning the subheader to the bottom lines all five up again — the
   same thing the product cards do with their price. */
.cast-card { display: flex; flex-direction: column; }

.cast-card > img { margin-bottom: 1rem; }
/* The art is square and four of the five sit on their own white
   ground, so each card gets a white well with the site's hairline
   around it: the white-backed four stop floating on the tint, and
   Namaka's full-bleed panel sits in the same frame as the rest. */
.cast-card > img {
  width: 100%; height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--bg);
  border: 1px solid var(--rule);
}

/* Three voices were competing inside a card 150px wide — a
   title-case display name, an italic nickname in quotes, and a line
   of tracked capitals — and the ocean line wrapped a lone "OCEAN"
   onto a second line on all five. The name sets in capitals, as
   Frank writes them; the role breaks where it means to; and a
   hairline separates the two so the nickname's presence on two cards
   out of five reads as a difference in content rather than a hole. */

.cast-name {
  font-family: var(--display);
  font-weight: 400;
  font-size: .95rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  line-height: 1.3;
  margin: 0;
}

.cast-alias {
  display: block;
  font-family: var(--serif);
  font-size: .9rem;
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-mute);
  margin-top: .2rem;
}

/* Pinned to the foot of the card so all five line up whether or not
   there is a nickname above. */
.cast-role {
  margin: auto 0 0;
  padding-top: .7rem;
  border-top: 1px solid var(--rule);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  line-height: 1.5;
  color: var(--ink-mute);
}

/* "Guardian of the" is the same on every card; the ocean is what
   differs, so the lead-in takes the quieter weight and the break
   between them is deliberate rather than whatever the column width
   decided. */
.cast-role-lead {
  display: block;
  font-weight: 400;
  color: var(--ink-mute);
  opacity: .7;
}

/* --- gallery --- */

/* Rows are stated in the markup rather than left to wrapping. One
   flex line that wraps on its own can't be told where to break — with
   a basis of zero the images never wrap at all and a whole gallery
   lands on a single line — and a basis big enough to force a break
   puts it wherever the arithmetic falls rather than where the pictures
   want it. A .work-row is the author saying "these share a row". */
.work-gallery {
  display: flex;
  flex-direction: column;
  gap: clamp(.75rem, 1.5vw, 1.25rem);
}

/* The art arrives in whatever shape it was drawn or shot in, so a row
   divides itself rather than forcing every piece into one well. Each
   image grows in proportion to its own aspect ratio, which means the
   row shares out its width so that everything in it comes out the same
   height — a portrait panel simply takes a narrower share than a
   landscape one. A row holding one image gives it the full width.

   Set --ar to width / height on each image. */
.work-row {
  display: flex;
  gap: inherit;
}
.work-row img {
  flex: var(--ar, 1) 1 0;
  min-width: 0;
  width: 100%; height: auto;
  /* Some of the art carries a drawn or photographed edge of its own;
     the rest sits on white and would have no edge at all against the
     page. */
  border: 1px solid var(--rule);
}

/* The Author page's slideshow of readers' Instagram posts, above its
   gallery. It borrows the product slider's track, arrows and dots; only
   the frame is its own. One well shaped to the tallest post, so the
   arrows don't jump as the slides change, held to a width at which the
   captions read and a post still fits a laptop screen. */
.ig-carousel {
  position: relative;
  max-width: 54rem;
  margin: 0 auto clamp(2rem, 4vw, 3rem);
}
/* The image is taken out of flow so its own 1600-odd pixels can't
   stretch the well past its aspect ratio — the same trap the product
   gallery's note warns about. */
.ig-media {
  position: relative;
  aspect-ratio: 1.356;
  border: 1px solid var(--rule);
  background: var(--bg);
}
.ig-media img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
}

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

/* The blog's video poster, given a band of its own on a work page.
   The button, the badge and the swap-in behaviour are that component's
   and are untouched — see .video-play and initVideos(). Only the
   framing differs: .post-video pulls itself out past the gutter to
   escape an article's narrow measure, and inside a .wrap there is
   nothing to escape.

   The shape is settable because not every film is 16:9. A 4:3 segment
   in a 16:9 well is pillarboxed by the player and cropped top and
   bottom on the poster, so the well takes the video's own shape and
   the still shows whole. Set --video-ar on .work-video. */
.work-video {
  --video-ar: 16 / 9;
  /* Bounded by height rather than width, so the shape decides how wide
     the well gets. A 4:3 segment given the full measure stands over
     800px tall and upscales its poster; this way it simply sits
     narrower than a widescreen one, and both come out the same height.
     The ratio divides inside the calc, which is why it can be written
     as a ratio and still be arithmetic. */
  max-width: min(68rem, calc(34rem * var(--video-ar)));
  margin-inline: auto;
}
.work-video .video-play,
.work-video iframe { aspect-ratio: var(--video-ar); }

/* --- the team --- */

.team { max-width: 760px; margin: 0 auto; }

.team-row {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
  gap: clamp(1rem, 3vw, 2.5rem);
  padding-block: clamp(1.1rem, 2.5vw, 1.5rem);
  border-top: 1px solid var(--rule);
}
.team-row:last-child { border-bottom: 1px solid var(--rule); }

/* The role is the label, so it takes the site's micro-type and sits
   on the name's first line rather than the top of the cell. */
.team-row dt {
  font-size: .68rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
  padding-top: .4rem;
}

.team-row dd { margin: 0; }

.team-name {
  font-family: var(--display);
  font-size: 1.05rem;
  letter-spacing: .04em;
  line-height: 1.45;
  margin: 0;
}

/* What they are known for. Titles of other works, so italic. */
.team-credits {
  font-family: var(--serif);
  font-style: italic;
  font-size: .9rem;
  line-height: 1.5;
  color: var(--ink-mute);
  margin: .3rem 0 0;
}
.team-credits cite { font-style: inherit; }

/* --- the writers --- */

/* Two writers, each with a paragraph rather than a credit line, which
   is why this isn't the .team list the other work pages use. It keeps
   that list's shape though — a centred column of rows, portrait then
   bio — because two of these side by side left the bios about thirty
   characters wide on a laptop, which is a newspaper column, not a
   measure to read a paragraph in. One under the other, each gets the
   full 52rem.

   The portrait column is a fixed width rather than a fraction: at
   3fr it grew with the measure, and a headshot has a size it wants to
   be. */
.writers {
  list-style: none; margin: 0 auto; padding: 0;
  max-width: 52rem;
  display: grid;
  gap: clamp(2rem, 4vw, 3rem);
}
.writer {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: clamp(1.5rem, 3vw, 2.25rem);
  align-items: start;
}

/* One shape for both wells, so a headshot and a placeholder standing
   next to each other come out the same size. The photograph is nearly
   square and the well is 4:5, so it is cropped rather than fitted —
   off the bottom, which on a headshot is shirt. */
.writer-portrait {
  width: 100%; height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: center top;
  border: 1px solid var(--rule);
}

.writer-name {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.3rem;
  letter-spacing: .04em;
  margin: 0 0 .7rem;
}

/* Optional: what the person did on THIS project, where the section
   heading alone doesn't say it. Same micro-type as the .team list's
   role labels, because it is the same piece of information — a role
   against a name — just set under the name instead of beside it.
   Pulled up under the name, which carries its own bottom margin for
   the cards that run straight into the bio. */
.writer-role {
  font-size: .68rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: -.45rem 0 .7rem;
}
.writer-bio {
  font-family: var(--serif);
  font-size: .98rem;
  line-height: 1.7;
  color: var(--ink-soft);
  margin: 0;
}
.writer-bio cite { font-style: italic; }
.writer-bio a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-underline-offset: .18em;
}
.writer-bio a:hover { text-decoration-color: var(--ink); }

/* A copy slot that is filled but not finished. Same job as .ph, for
   words instead of pictures: it marks a paragraph I drafted on
   someone's behalf, so the gap shows on the page and not only in the
   template. A paragraph its subject actually wrote never carries one,
   however old it is.

   Nothing carries it at the moment — Dumped, which introduced it, has
   both bios from their authors now. Kept for the work pages still to
   be filled; delete it if it is still unused when they are done. */
.revise {
  font-family: var(--sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-mute);
  border: 1px dashed var(--rule);
  padding: .5rem .7rem;
  margin: 1rem 0 0;
  display: inline-block;
  line-height: 1.5;
}

/* ============================================================
   Policy pages — Shipping & Returns, Privacy Policy

   The plainest pages on the site, and deliberately so. Somebody
   reading these wants a fact, not an experience: a centred title, one
   column of prose, nothing else. Shared between both so the two read
   as the same voice.
   ============================================================ */

/* A two-sentence returns policy left the footer occupying the bottom
   half of the screen, which reads as an unfinished page rather than a
   deliberately spare one. The minimum height holds the fold open and
   centres what little there is; `safe` means a long policy that
   outgrows it aligns to the top instead of overflowing off-screen. */
.policy {
  padding-block: clamp(3.5rem, 8vw, 6rem);
  min-height: 58vh;
  display: grid;
  align-content: safe center;
}

/* The centring above is for a two-sentence page. A policy that runs for
   screens wants its title at the top like any other article. */
.policy-long { align-content: start; }
.policy-long .policy-prose { text-align: left; }

.policy-head {
  text-align: center;
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

.policy-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.6rem, 3.4vw, 2.4rem);
  letter-spacing: .06em;
  text-transform: uppercase;
  line-height: 1.15;
  margin: 0;
}

/* Same measure as the essay. These pages can be two sentences or two
   screens; the column holds either without the short one looking
   stranded. */
.policy-prose {
  max-width: 56ch;
  margin-inline: auto;
  font-family: var(--serif);
  font-size: 1.08rem;
  line-height: 1.8;
  color: var(--ink-soft);
}
.policy-prose p { margin: 0 0 1.3em; }
.policy-prose p:last-child { margin-bottom: 0; }

.policy-prose h2 {
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink);
  margin: 2.4em 0 .9em;
}

/* Sub-headings inside a section ("How do I withdraw my consent?"). A
   size below the section headings and in the serif, so a long policy
   reads as prose with signposts rather than as a form. */
.policy-prose h3 {
  font-family: var(--serif);
  font-size: 1.08rem;
  font-weight: 400;
  font-style: italic;
  color: var(--ink);
  margin: 1.8em 0 .6em;
}

.policy-list {
  margin: 0 0 1.3em;
  padding-left: 1.2em;
}
.policy-list li { margin-bottom: .7em; }
.policy-list li:last-child { margin-bottom: 0; }
.policy-list b {
  font-family: var(--sans);
  font-size: .86em;
  font-weight: 600;
  color: var(--ink);
}

/* Postal addresses read as blocks, not sentences. */
.policy-address {
  font-style: normal;
  color: var(--ink);
  line-height: 1.55;
}

.policy-prose a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: .2em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule);
  transition: text-decoration-color .2s var(--ease);
}
.policy-prose a:hover { text-decoration-color: var(--ink); }

/* ============================================================
   Footer
   ============================================================ */

.colophon {
  background: var(--ink);
  color: var(--on-dark);
  padding-block: clamp(3rem, 6vw, 4.5rem) 0;
}

/* The link columns and the contact block, side by side and vertically
   centred against each other.

   WHY THIS IS NOT ONE GRID. The contact block has to sit an equal
   distance from the last column of nav TEXT and from the right edge of
   the page. In a three-column grid the second column is a fixed 14rem
   while its longest label is nearer 10, so there is dead column to the
   right of the text that the eye reads as part of the gap — and a grid
   gutter would land on one side of the contact block only. Wrapping the
   columns lets margin-inline:auto split the whole of the leftover space
   in two, and letting the last column shrink to its text makes the
   measurement start where the words stop. */
.colophon-top {
  display: flex;
  align-items: center;
  padding-bottom: clamp(2.5rem, 5vw, 3.5rem);
}

.colophon-links {
  display: flex;
  gap: clamp(2rem, 5vw, 4rem);
}
/* Fixed so the second column starts in the same place it always has;
   the last one shrinks to its longest label so nothing empty is counted
   as spacing. */
.colophon-links nav { width: 14rem; }
.colophon-links nav:last-child { width: auto; }

/* The contact details. Serif and a size up, so an address and a phone
   number are legible as contact details instead of reading as two more
   rows of navigation, which is what they did when they sat in the link
   column. */
/* The block is only as wide as the address, its widest line, so
   centring inside it puts the phone number under the middle of the
   address rather than under the middle of the footer cell. */
.colophon-contact {
  display: grid;
  justify-items: center;
  /* align-self centres the block against the link columns beside it,
     which are taller. align-content then packs its own two rows to
     their text — without it the block distributes whatever height it
     is given between them, which put about ninety pixels of air
     between the address and the number. */
  /* Equal air on both sides: auto margins take the space left over
     between the columns and the page edge and halve it. */
  margin-inline: auto;
  align-content: start;
  gap: .5rem;
  width: max-content;
  max-width: 100%;
}
/* .colophon a sets .9rem and is declared later, so the class alone loses
   on source order. The extra .colophon here is what makes this win. */
.colophon .colophon-contact a {
  font-family: var(--serif);
  font-size: clamp(1.08rem, 1.4vw, 1.25rem);
  color: var(--on-dark);
  /* An address is one long word; let it break rather than push the
     footer sideways on a phone. */
  overflow-wrap: anywhere;
}
.colophon .colophon-contact a:hover { text-decoration: underline; text-underline-offset: .25em; }

.colophon h2 {
  font-family: var(--sans);
  font-size: .72rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: rgba(255, 255, 255, .55);
  margin: 0 0 1.25rem;
}

.colophon ul { list-style: none; margin: 0; padding: 0; }
.colophon li + li { margin-top: .6rem; }
.colophon a { color: rgba(255, 255, 255, .82); font-size: .9rem; }
.colophon a:hover { color: var(--on-dark); text-decoration: underline; text-underline-offset: .25em; }

.signup p {
  font-family: var(--serif);
  font-size: 1rem;
  color: rgba(255, 255, 255, .82);
  margin: 0 0 1.25rem;
  max-width: 34ch;
}

.signup-form { display: flex; gap: .75rem; flex-wrap: wrap; }
.signup-form input {
  flex: 1 1 14rem;
  padding: .85rem 1rem;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, .35);
  color: var(--on-dark);
  font: inherit; font-size: .9rem;
}
.signup-form input::placeholder { color: rgba(255, 255, 255, .45); }
.signup-form input:focus-visible { outline: 2px solid var(--on-dark); outline-offset: 2px; }

.colophon .btn-light { border-color: rgba(255, 255, 255, .55); }

.colophon-bottom {
  display: flex; flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1.5rem;
  border-top: 1px solid rgba(255, 255, 255, .18);
  font-size: .78rem;
  color: rgba(255, 255, 255, .6);
}
.colophon-bottom a { font-size: .78rem; }
.colophon-bottom ul { display: flex; gap: 1.5rem; }
.colophon-bottom li + li { margin-top: 0; }

/* ============================================================
   Build placeholder — remove once real sections land
   ============================================================ */

.stub {
  padding-block: clamp(4rem, 12vw, 8rem);
  text-align: center;
  color: var(--ink-mute);
}
.stub p {
  font-size: .74rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  margin: 0;
}

/* ============================================================
   Responsive
   ============================================================ */

@media (max-width: 940px) {
  .nav-toggle { display: block; }

  .masthead .wrap { flex-wrap: wrap; }

  .nav {
    display: none;
    order: 3;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding-bottom: 1rem;
    border-top: 1px solid var(--rule);
    margin-top: .25rem;
  }
  .nav.is-open { display: flex; }

  .nav-item { border-bottom: 1px solid var(--rule); }

  .nav-link {
    width: 100%;
    justify-content: space-between;
    padding: 1.05rem .25rem;
  }
  .nav-link::after { content: none; }

  /* Dropdowns become accordions. */
  .nav-menu {
    position: static;
    opacity: 1; visibility: visible;
    transform: none;
    border: 0; box-shadow: none;
    min-width: 0;
    padding: 0 0 .75rem;
    display: none;
    background: var(--surface);
  }
  .nav-item[data-open='true'] .nav-menu { display: block; }

  /* A flyout needs horizontal room a phone hasn't got, and the panel
     around it is already an accordion — so the group opens downward
     in place instead, and the chevron points down like the ones
     above it. */
  .menu-sub {
    position: static;
    opacity: 1; visibility: visible; transform: none;
    /* The panel's own white ground would punch a hole in the tinted
       accordion it is sitting inside. */
    background: none;
    border: 0; box-shadow: none; min-width: 0;
    display: none;
    padding: 0 0 .35rem 1.6rem;
    transition: none;
  }
  .menu-group[data-open='true'] .menu-sub { display: block; }
  .menu-group-toggle { padding-block: 1rem; }
  .menu-group-toggle .chev { transform: rotate(0deg); }
  .menu-group[data-open='true'] .menu-group-toggle .chev { transform: rotate(180deg); }

  .nav-store {
    display: inline-block;
    margin-top: 1rem;
    text-align: center;
  }
  .nav-item:has(.nav-store) { border-bottom: 0; }

  .product-card { grid-template-columns: 1fr; }
  .gallery { border-right: 0; border-bottom: 1px solid var(--rule); }
  .buy-row { flex-direction: column; align-items: stretch; gap: 1.5rem; }
  .price-block, .cart-block { flex: 1 1 auto; }

  .product-grid { grid-template-columns: 1fr 1fr; }
  .colophon-top { flex-wrap: wrap; align-items: start; }
  .colophon-links { flex: 1 1 100%; }
  .colophon-links nav { width: auto; flex: 1 1 0; }
  /* Below the columns now, and left with them rather than centred. */
  .colophon-contact {
    flex: 1 1 100%;
    margin-inline: 0;
    justify-items: start;
    margin-top: clamp(2rem, 5vw, 3rem);
  }
  .signup { grid-column: 1 / -1; }

  .store-grid { grid-template-columns: repeat(3, 1fr); }

  .quote { padding-inline: clamp(2.25rem, 9vw, 3rem); }

  .marquee-nav { display: none; }
  .marquee-dots { left: var(--gut); right: auto; }

  .show-card { grid-template-columns: 1fr; }
  .show-art { border-right: 0; border-bottom: 1px solid var(--rule); }
  .show-art img { max-width: 360px; margin-inline: auto; }

  .listen-grid { grid-template-columns: repeat(2, 1fr); }

  /* Work page: the aside drops under the prose, so it has nothing left
     to stick to. */
  .work-split { grid-template-columns: 1fr; }
  .work-aside { position: static; max-width: 30rem; }

  .work-cast { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  /* A 5fr poster column at this width is a thumbnail, so the poster
     goes back above the copy. */
  .poster-layout { grid-template-columns: 1fr; }
  .poster-art { max-width: 24rem; }
}

@media (max-width: 560px) {
  /* trims only the outer background; keeps the title and cover whole */
  .marquee-media { aspect-ratio: 11 / 5; }

  .product-grid { grid-template-columns: 1fr; }
  .store-grid { grid-template-columns: repeat(2, 1fr); }
  .store-grid .card-title { font-size: .82rem; }
  /* Stacked, the two columns are one list: there are no headings to
     separate, so the join between them takes the same .6rem as any
     other pair of links. The old column gap left a hole between "Why I
     Create" and "The Looking Glass Wars" that read as a section break
     nothing had declared. */
  .colophon-links { flex-direction: column; gap: .6rem; }
  .gallery-nav { width: 34px; height: 34px; }

  .show-cta { flex-direction: column; align-items: stretch; }
  .show-cta .btn { text-align: center; }

  /* Four columns leaves the title about twelve characters wide, so the
     duration drops under it and the title gets the full row. */
  .ep-row {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      'num title icon'
      '.   time  icon';
    column-gap: .75rem; row-gap: .2rem;
  }
  .ep-num, .ep-row-icon { align-self: center; }

  .work-cast { grid-template-columns: 1fr; }

  .writer { grid-template-columns: 1fr; }
  .writer-portrait { max-width: 15rem; }

  /* Two columns leave the names about ten characters wide. */
  .team-row { grid-template-columns: 1fr; gap: .35rem; }
  .team-row dt { padding-top: 0; }

  /* Sharing a row out by aspect leaves each piece a couple of inches
     wide on a phone. One per row instead — as full-width items that
     wrap, not by turning the row into a column, where a basis of 100%
     would mean height. */
  .work-row { flex-wrap: wrap; }
  .work-row img { flex: 1 0 100%; }
}

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

/* ============================================================
   Blog
   ============================================================ */

/* --- the meta line, shared by the feature and the cards --- */

.post-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  font-size: .68rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0 0 .55rem;
}

/* Same black chip the podcast uses for an episode number. */
.post-tag {
  background: var(--ink); color: var(--on-dark);
  padding: .25rem .5rem;
  font-size: .62rem; letter-spacing: .14em;
}

.post-author {
  font-size: .72rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: .6rem 0 0;
}

/* --- featured post --- */

/* Bordered card on the white ground, the page's one wide image. The
   banner runs full bleed inside the border and the copy sits under it,
   so the art is never cropped to make room for text on top of it. */
.post-feature {
  display: block;
  border: 1px solid var(--rule);
  max-width: 1160px;
  margin-inline: auto;
}

.post-feature-media {
  aspect-ratio: 3 / 1;
  overflow: hidden;
  border-bottom: 1px solid var(--rule);
}
.post-feature-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .5s var(--ease);
}
.post-feature:hover .post-feature-media img { transform: scale(1.03); }

.post-feature-body { padding: clamp(1.5rem, 3.5vw, 2.5rem); }

.post-feature-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.15rem, 2.2vw, 1.55rem);
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1.35;
  margin: 0 0 .7rem;
}
.post-feature:hover .post-feature-title {
  text-decoration: underline; text-underline-offset: .25em;
}

.post-feature-blurb {
  font-family: var(--serif);
  font-size: 1.02rem;
  color: var(--ink-soft);
  max-width: 62ch;
  margin: 0;
}

/* --- the archive --- */

/* Three across. A square tile at half the page width is a 600px block
   of art per post; at a third it sits in proportion to its title. */
.post-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 4vw, 3rem) clamp(1.25rem, 2.5vw, 2.25rem);
}

.post-card { display: flex; flex-direction: column; }
/* display:flex outranks the hidden attribute — without this the
   deferred cards render as if the toggle were already pressed. */
.post-card[hidden] { display: none; }

/* Square, cropped from the middle of the archive's 1500x500 marquees.
   Those banners put their subject dead centre and fill the outer
   thirds with texture, so the crop keeps the subject and drops the
   padding — a tile with presence rather than a letterbox strip. */
.post-media {
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid var(--rule);
  margin-bottom: .9rem;
}
.post-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .4s var(--ease);
}
.post-card:hover .post-media img { transform: scale(1.04); }

.post-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: .95rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  line-height: 1.4;
  margin: 0;
}
.post-card:hover .post-title {
  text-decoration: underline; text-underline-offset: .25em;
}

/* Bylines line up across a row however the titles wrap. */
.post-card .post-author { margin-top: auto; padding-top: .6rem; }

.posts-more {
  display: flex; justify-content: center;
  margin-top: clamp(2rem, 4vw, 3rem);
}
/* display:flex outranks the hidden attribute, so say it again. The
   control is only there when there is something left to show. */
.posts-more[hidden], .episodes-more[hidden] { display: none; }

@media (max-width: 900px) {
  .post-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 560px) {
  .post-grid { grid-template-columns: 1fr; }
}

/* ============================================================
   News
   ============================================================ */

/* A sentence under a page title, where the title alone doesn't say
   what the page holds. "News" could be anything; these are ten press
   pieces. */
.section-standfirst {
  font-family: var(--serif);
  font-size: 1.05rem;
  color: var(--ink-soft);
  text-align: center;
  max-width: 46ch;
  margin: -1.5rem auto clamp(2.5rem, 5vw, 3.5rem);
}

/* Two across. There is no art on these cards — see pages/news.html —
   so a third column would only make the columns narrow enough to wrap
   every headline. */
.news-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(1.25rem, 2.5vw, 2rem);
  max-width: 1160px;
  margin-inline: auto;
}

.news-card {
  display: flex; flex-direction: column;
  border: 1px solid var(--rule);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  transition: border-color .22s var(--ease);
}
.news-card:hover { border-color: var(--ink); }

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

/* The plate every card opens with. Fixed height so the headlines
   start on the same line across a row, however tall the mark inside
   it is — the trimmed logos run from a 9:1 strip (the Globe and Mail)
   to a stacked square (Arizona State). */
.news-plate {
  display: flex;
  align-items: center; justify-content: center;
  height: 5.5rem;
  margin-bottom: clamp(1.1rem, 2vw, 1.5rem);
  padding-bottom: clamp(1.1rem, 2vw, 1.5rem);
  border-bottom: 1px solid var(--rule);
}

/* --mark-w is worked out per mark in build.py so that all eight cover
   the same area — the one thing that makes eight logos drawn to eight
   different specs read as one set. Capping height and width instead
   left the long wordmarks looking twice the size of the stacked ones.
   The percentage cap is only there for a narrow card. */
.news-mark {
  width: var(--mark-w);
  height: auto;
  max-width: 78%;
  object-fit: contain;
}

/* Two of the ten were never posted with a logo. The outlet is set in
   the page's display face at the same weight the marks carry, so the
   plate is filled rather than empty. */
.news-wordmark {
  font-family: var(--display);
  font-size: clamp(.95rem, 1.4vw, 1.15rem);
  font-weight: 400;
  letter-spacing: .14em;
  text-transform: uppercase;
  line-height: 1.5;
  text-align: center;
  max-width: 17rem;
  margin: 0;
}

.news-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1rem, 1.5vw, 1.2rem);
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1.4;
  margin: 0 0 .75rem;
}
.news-card:hover .news-title {
  text-decoration: underline; text-underline-offset: .25em;
}

.news-blurb {
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.7;
  color: var(--ink-soft);
  margin: 0;
}

@media (max-width: 720px) {
  .news-grid { grid-template-columns: 1fr; }
}

/* ============================================================
   A blog post
   ============================================================ */

/* One column. The old page put a newsletter ad and a related-posts
   strip down the right for the article's whole length; the reading
   column is the page. */
.post { padding-block: 0 clamp(3rem, 7vw, 6rem); }

/* The 1500x500 marquee, full width and uncropped — it is the one place
   the art has all the room it was drawn for. */
.post-hero {
  aspect-ratio: 3 / 1;
  overflow: hidden;
  background: var(--surface);
}
.post-hero img { width: 100%; height: 100%; object-fit: cover; }

.post-header,
.post-body,
.post-foot {
  width: 100%;
  max-width: 44rem;              /* ~68 characters of the serif */
  margin-inline: auto;
  padding-inline: var(--gut);
}

.post-header {
  text-align: center;
  padding-block: clamp(2.5rem, 5vw, 4rem) clamp(1.75rem, 3.5vw, 2.5rem);
}
.post-header .post-meta { justify-content: center; }

.post-heading {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.5rem, 3.6vw, 2.35rem);
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1.25;
  margin: 0 0 .9rem;
}

.post-byline {
  font-size: .72rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0;
}

/* A news post has no hero — the header is the top of the page, so it
   needs the air the image was providing. */
.post-news .post-header {
  padding-top: clamp(3rem, 6vw, 4.5rem);
}

/* The same plate the cards open with, a size up: this is the one
   place the mark has room, and it is doing the job the stretched
   banner used to do badly. */
.post-news .news-plate {
  height: 6.5rem;
  max-width: 22rem;
  margin-inline: auto;
  margin-bottom: 1.6rem;
  padding-bottom: 1.5rem;
}
/* The same marks, a third bigger: on the post there is one of them
   and it has the page's full measure to sit in. */
.post-news .news-mark {
  width: calc(var(--mark-w) * 1.35);
  max-width: 85%;
}

/* --- the prose --- */

/* Serif for the running text. A 1,500-word median article is a
   different reading job from a product blurb. */
.post-body {
  font-family: var(--serif);
  font-size: 1.12rem;
  line-height: 1.75;
  color: var(--ink-soft);
}

.post-body > p { margin: 0 0 1.15em; }
.post-body > p:last-child { margin-bottom: 0; }

/* Shared with a work page's prose, which is the same thing in a
   narrower column: running text where a link has to announce itself,
   since the site's links are otherwise undecorated. */
.post-body a,
.work-prose a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: .2em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule);
  transition: text-decoration-color .2s var(--ease);
}
.post-body a:hover,
.work-prose a:hover { text-decoration-color: var(--ink); }

.post-body strong, .post-body b { color: var(--ink); font-weight: 700; }

.post-body h2, .post-body h3 {
  font-family: var(--display);
  font-weight: 400;
  color: var(--ink);
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1.35;
  margin: clamp(2.25rem, 4.5vw, 3.25rem) 0 .9rem;
}
.post-body h2 { font-size: clamp(1.1rem, 2.2vw, 1.35rem); }
.post-body h3 { font-size: clamp(1rem, 1.9vw, 1.15rem); }

.post-body ul, .post-body ol { margin: 0 0 1.15em; padding-left: 1.3em; }
.post-body li { margin-bottom: .45em; }

.post-body hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: clamp(2.25rem, 4.5vw, 3.25rem) auto;
  width: 100%;
}

.post-body blockquote {
  margin: clamp(1.75rem, 3.5vw, 2.5rem) 0;
  padding-left: clamp(1rem, 2.5vw, 1.75rem);
  border-left: 2px solid var(--ink);
  font-size: 1.05rem;
}
.post-body blockquote p { margin: 0 0 .6em; }
.post-body blockquote cite {
  display: block;
  font-family: var(--sans);
  font-style: normal;
  font-size: .72rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
}

/* Figures break the measure — the column is set for text, and an image
   held to 44rem looks starved. */
.post-figure {
  margin: clamp(2rem, 4vw, 3rem) calc(var(--gut) * -1);
}
.post-figure img { width: 100%; height: auto; }
.post-figure figcaption {
  font-family: var(--sans);
  font-size: .8rem;
  color: var(--ink-mute);
  text-align: center;
  margin-top: .7rem;
  padding-inline: var(--gut);
}

/* --- the three voices of "Rise of the Autocards" --- */

/* Letters, memos and newspaper copy were each set in their own face;
   that typography is how the story tells you who is speaking, so the
   distinction is carried over rather than flattened.

   Not carried over literally: the letters used the theme's heading
   face, and ours is Cinzel — a titling face that is illegible for
   paragraphs of correspondence. The letters take the italic of the
   body serif instead, which is what a letter reads as anyway. The
   memos keep the roman, the press copy goes to the sans. */
.post-body .voice-letter { font-style: italic; }
.post-body .voice-press  { font-family: var(--sans); font-size: .98rem; }

.post-body .align-center {
  text-align: center;
  font-family: var(--sans);
  font-size: .78rem; font-weight: 700;
  letter-spacing: .2em;
  color: var(--ink);
}

.post-body .size-large  { font-size: 1.3em; }
.post-body .size-medium { font-size: 1.12em; }
.post-body .size-small  { font-size: .9em; }

/* --- video --- */

/* A poster with a play button, standing in for a YouTube embed until
   the reader asks for it. Breaks the reading measure like any figure:
   it is an image, and an image wants width. */
.post-video {
  margin: clamp(2rem, 4vw, 3rem) calc(var(--gut) * -1);
}

.video-play {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  padding: 0;
  border: 0;
  background: var(--ink);
  cursor: pointer;
  overflow: hidden;
}

/* The 4:3 poster sizes carry the 16:9 frame letterboxed inside them,
   so cropping to 16:9 recovers the picture either way. */
.video-play img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .4s var(--ease), opacity .3s var(--ease);
}
.video-play:hover img { transform: scale(1.03); opacity: .85; }

/* Two videos in the archive have no poster left; the panel still plays. */
.video-play-bare { background: var(--surface); border: 1px solid var(--rule); }

.video-badge {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: clamp(54px, 8vw, 68px);
  pointer-events: none;
}
.video-badge svg { width: 100%; height: auto; }

/* YouTube's own mark: grey until you reach for it, then red. */
.video-badge .yt-body {
  fill: #1f1f1f;
  fill-opacity: .82;
  transition: fill .2s var(--ease), fill-opacity .2s var(--ease);
}
.video-badge .yt-arrow { fill: #fff; }
.video-play:hover .video-badge .yt-body,
.video-play:focus-visible .video-badge .yt-body {
  fill: #f00;
  fill-opacity: 1;
}

.video-play:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* The real player, once it has been asked for. */
.post-video iframe,
.work-video iframe {
  display: block;
  width: 100%;
  border: 0;
}

/* The shape is shared with the poster it replaces. A work page sets it
   per video on .work-video and states it earlier in this file, so it
   can't be folded into the rule above — same specificity, and the
   later one would win. */
.post-video iframe { aspect-ratio: 16 / 9; }

.post-video figcaption {
  font-family: var(--sans);
  font-size: .8rem;
  color: var(--ink-mute);
  text-align: center;
  margin-top: .7rem;
  padding-inline: var(--gut);
}

/* --- a self-hosted video --- */

.post-figure video {
  display: block;
  width: 100%;
  height: auto;
  background: var(--ink);
}

/* --- verse --- */

/* wp-block-verse is a poem or a lyric, not code. The line breaks are
   the content; the monospace a <pre> would otherwise get would be a
   mistake. */
.post-verse {
  font-family: var(--serif);
  font-size: 1.02rem;
  line-height: 1.8;
  white-space: pre-wrap;
  margin: clamp(1.75rem, 3.5vw, 2.5rem) 0;
  padding-left: clamp(1rem, 2.5vw, 1.75rem);
  border-left: 2px solid var(--rule);
  overflow-x: auto;
}

/* --- TikTok --- */

/* A link, not an embed: TikTok's player wants a script from their
   servers on every page view, which is what the video posters exist to
   avoid. It carries the handle and what the poster wrote, so it says
   where it goes rather than reading as a bare URL. */
.post-tiktok {
  margin: clamp(1.75rem, 3.5vw, 2.5rem) 0;
}

.post-tiktok a {
  display: block;
  padding: clamp(1rem, 2.2vw, 1.4rem) clamp(1.1rem, 2.4vw, 1.5rem);
  background: var(--surface);
  border: 1px solid var(--rule);
  transition: border-color .2s var(--ease);
}
.post-tiktok a:hover { border-color: var(--ink); }

.post-tiktok span { display: block; }

.tiktok-label {
  font-family: var(--sans);
  font-size: .66rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: .45rem;
}

.tiktok-handle {
  font-family: var(--display);
  font-size: 1rem;
  letter-spacing: .04em;
  color: var(--ink);
}

.tiktok-caption {
  font-size: .92rem;
  line-height: 1.6;
  color: var(--ink-soft);
  margin-top: .3rem;
}

/* --- author bio --- */

/* The card that closes the article: a portrait with the bio set
   around it.

   Held to the reading measure, unlike the figures above it. A figure
   breaks the measure because an image wants width; this card is
   mostly prose, and prose set wider than the article it closes reads
   as a mistake.

   The portrait is floated rather than sitting in its own grid cell.
   A cell has to resolve a conflict that has no good answer: stretch
   the picture to whatever height the bio needs and a long bio squeezes
   it into a strip, or hold its proportions and a short bio leaves the
   cell half empty. The archive's bios run from 305 to 866 characters,
   so both failures would show. Floated, the text sets around the
   picture and closes underneath it — there is no cell left to fill at
   any length. */
/* "Meet the author" sits outside the card, labelling it the way a
   section heading labels a section. Inside, it sat a line above the
   name doing the same job twice. */
/* Scoped to .post-body so it outranks the generic heading rules that
   style every h2 and h3 in an article. */
.post-body .post-bio-heading {
  font-family: var(--sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: clamp(2.5rem, 5vw, 4rem) 0 .9rem;
}

.post-bio {
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: clamp(1.35rem, 2.8vw, 1.9rem);
  margin: 0;
}
.post-bio::after { content: ""; display: block; clear: both; }

/* Headshots in the archive run from 2:3 portrait to 3:2 landscape, so
   the frame is fixed and the image cropped into it — anchored high,
   because what they all have in common is a face near the top. */
.post-bio-photo {
  float: left;
  width: 38%;
  max-width: 13rem;
  margin: .3rem clamp(1.1rem, 2.2vw, 1.5rem) .6rem 0;
}
.post-bio-photo img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: center top;
  border: 1px solid var(--rule);
  background: var(--bg);
}

.post-body .post-bio-name {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.05rem, 2vw, 1.3rem);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink);
  line-height: 1.3;
  margin: 0 0 .7rem;
}

.post-bio-body p {
  font-size: .92rem;
  line-height: 1.7;
  margin: 0 0 .8em;
}
.post-bio-body p:last-child { margin-bottom: 0; }

/* The article closes on a rule in most posts; the heading provides the
   separation on its own. */
.post-body hr + .post-bio-heading { margin-top: clamp(2rem, 4vw, 2.75rem); }
.post-body .post-bio-body p { color: var(--ink-soft); }

.post-foot {
  display: flex; justify-content: center;
  margin-top: clamp(2.5rem, 5vw, 4rem);
}

/* Too narrow to set text beside a picture: the portrait becomes a
   band across the top and the bio runs full width under it. */
@media (max-width: 620px) {
  .post-bio-photo {
    float: none;
    width: 100%;
    max-width: none;
    margin: 0 0 1.1rem;
  }
  .post-bio-photo img { aspect-ratio: 3 / 2; }
}

/* ============================================================
   Biography
   ============================================================ */

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

/* Portrait beside the name rather than above it, for the same reason
   .work-hero-poster sets a one-sheet beside its logline: the site's
   ordinary hero runs its art the full width of the window, and a
   4:5 photograph treated that way either stands a whole screen tall
   or gets cropped to a letterbox.

   It doesn't reuse .poster-layout, because a headshot is not a
   poster and wants different arithmetic. A one-sheet is 2:3 and
   carries its own typography, so it can take that grid's 28rem. This
   is one person's face: at 28rem it is a very large head, and the
   file Frank supplied is 480px across, so it would be upscaling on
   any retina screen as well. 24rem is the width at which the
   portrait is still plainly the subject of the page and still
   sharp — see the template for the note asking him for the
   full-resolution original. */
.bio-hero { padding-block: clamp(2.5rem, 6vw, 4.5rem); }

.bio-layout {
  /* Narrower than the wrap and centred in it: the name is two or
     three words and the standfirst caps itself at 46ch, so at the
     full 1320 the copy column would be mostly empty, which reads as
     a section that failed to load rather than as air. */
  max-width: 58rem;
  margin-inline: auto;
  display: grid;
  /* The copy column carries a floor, and the portrait yields to it.
     24rem is a cap the portrait gives up a little of rather than a
     width it holds: without the floor it kept all 24rem and the copy
     column took whatever was left, which fell to 435px in the last
     dozen pixels before the 940px breakpoint stacks the two.

     That was originally about an eyebrow that had to set on one
     line. The eyebrows are gone — Frank's call; the heroes now open
     straight on the name — and the floor stays because the column
     still holds a display-size title and a standfirst under it, and
     at 435px "The Producer" breaks across two lines while the
     standfirst goes to six. 28rem is where both settle. */
  grid-template-columns: minmax(0, 24rem) minmax(28rem, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}

.bio-portrait { margin: 0; }
.bio-portrait img {
  width: 100%; height: auto;
  border: 1px solid var(--rule);
}

/* A chapter title of four words rather than one. "World Champion
   Freestyle Skier" at .work-title's 4.75rem runs to three lines in a
   448px column and stands taller than the portrait beside it, which
   turns the hero into a wall of display type with a photograph
   attached. Two steps down, and the line break is written into the
   markup rather than left to the column width — it breaks after
   "Champion" because that is where the sense breaks.

   Only the Skier page uses it. The other two chapters are one word
   each and want the full size. */
.bio-hero .work-title-long {
  font-size: clamp(1.75rem, 3.8vw, 2.6rem);
  line-height: 1.15;
  max-width: none;
}

/* .work-title centres itself inside its own measure, which is right
   in a centred hero band and wrong beside a picture. */
.bio-hero .work-title { margin-inline: 0; margin-bottom: 0; }

/* --- the bio itself --- */

/* .work-prose is written for the 8fr column of a .work-split, where
   the grid places it. Standing on its own it needs centring. */
.bio-prose { margin-inline: auto; }

/* --- the line he closes on --- */

/* One sentence by the subject of the page, so it gets the size and
   nothing else around it. Not the .quote the review carousel uses:
   that one is a slide in a scrolling track with an attribution rule
   under it, and an attribution here would only repeat the name at
   the top of the page. */
.bio-quote {
  max-width: 46rem;
  margin: 0 auto;
  text-align: center;
  font-family: var(--serif);
  font-size: clamp(1.1rem, 2.3vw, 1.55rem);
  line-height: 1.55;
  color: var(--ink);
}
.bio-quote p { margin: 0; }

/* Attribution. There is no source to credit: the old About page ran
   this line unattributed — it simply sat at the foot of the
   third-person bio in quotation marks, after the paragraph about The
   Juliet — it appears nowhere else in the archive, it is still
   unattributed on the live site, and no interview it might have come
   from is findable. So the citation states the one thing that is
   certain, which is also the thing the reader actually needs: the
   bio above is third person and this is suddenly first person, and
   the name marks the switch.

   Same micro-type and leading hairline as the review carousel's
   .quote figcaption, so a quotation reads as the same kind of object
   wherever it appears on the site. It is not that rule reused,
   though — that one is a flex child of a carousel slide and centres
   itself by the slide's own alignment. */
.bio-quote-cite {
  display: flex; align-items: center; justify-content: center;
  gap: .7rem;
  margin-top: clamp(1.25rem, 2.5vw, 1.75rem);
  font-family: var(--sans);
  font-size: .7rem; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--ink);
}
.bio-quote-cite::before {
  content: '';
  flex: none;
  width: 1.6rem; height: 1px;
  background: var(--ink);
}

/* Prose set directly under a quotation, which happens on the
   Producer page: the quote leads the band and the paragraphs answer
   it. Both have zero margin of their own, so without this they touch
   and read as one block in two different faces. The gap is wider
   than a paragraph break because it is a change of speaker. */
.bio-quote + .work-prose { margin-top: clamp(2rem, 4vw, 3rem); }

@media (max-width: 940px) {
  /* A 24rem column at this width is a passport photo, so the
     portrait goes back above the copy, as the poster pages do. */
  .bio-layout { grid-template-columns: 1fr; }
  .bio-portrait { max-width: 20rem; }
}

/* ============================================================
   Why I Create
   ============================================================ */

/* The mission statement. Every other page on the site is built out
   of bands — tint, white, tint — because every other page is a set
   of separate things: a hero, a gallery, a cast, a shelf. This one
   is a single continuous piece of writing, so it gets a single
   continuous white column instead, and its structure comes from
   type and air rather than from background colour. That is the
   whole idea of the page; don't stripe it later. */

.creed { padding-block: clamp(3rem, 6vw, 4.5rem) clamp(4rem, 9vw, 7rem); }

/* --- the head --- */

.creed-head {
  text-align: center;
  max-width: 44rem;
  /* The rule under the head wants the same air under it as over it,
     so the head carries the second half of the gap. */
  margin: 0 auto clamp(2.25rem, 4vw, 3rem);
}

.creed-head .label { margin-bottom: 1.2rem; }

/* Smaller than .work-title. That one announces a property across the
   full width of a hero band and has nothing under it but a logline;
   this one sits at the top of a narrow column of running text, and
   at 4.75rem the title would be shouting over the thing it
   introduces. */
.creed-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.9rem, 5vw, 3.1rem);
  letter-spacing: .05em;
  line-height: 1.15;
  margin: 0 0 1.3rem;
}

.creed-standfirst {
  font-family: var(--serif);
  font-size: clamp(1.15rem, 2.4vw, 1.5rem);
  line-height: 1.5;
  color: var(--ink);
  margin: 0 auto;
  max-width: 30ch;
}

/* The page's one ornament, used twice: under the head and above the
   signature. Short, centred, full black — the hairline the rest of
   the site draws between rows, stood on its own. */
.creed-rule {
  width: 2.75rem;
  height: 1px;
  border: 0;
  background: var(--rule-strong);
  margin: clamp(2.25rem, 4vw, 3rem) auto 0;
}

/* --- the essay --- */

/* Narrower than .work-prose's 62ch. That measure is for a column
   standing beside a picture in a grid, where the page has other
   things to look at; this is the only thing on the page, and a
   1,000-word first-person piece read straight through wants the
   shorter line. */
.creed-measure {
  max-width: 56ch;
  margin-inline: auto;
}

.creed-prose {
  font-family: var(--serif);
  font-size: 1.12rem;
  line-height: 1.8;
  color: var(--ink-soft);
}
.creed-prose p { margin: 0 0 1.3em; }
.creed-prose p:last-child { margin-bottom: 0; }

/* The four lines that carry the turns in the piece. Same face as the
   prose around them, so it stays one voice — this is him raising it,
   not the site interrupting — but full black, three sizes up, centred
   and given more air above and below than any paragraph break on the
   page.

   No quotation marks and no rules around them: they aren't quotations
   from somewhere else, they are sentences of his own essay in their
   proper place in it. */
.creed-beat {
  font-family: var(--serif);
  font-size: clamp(1.35rem, 3.2vw, 1.8rem);
  line-height: 1.45;
  color: var(--ink);
  text-align: center;
  text-wrap: balance;
  margin: clamp(2.75rem, 6vw, 4.25rem) auto;
  /* Held well inside the measure so a beat can never come out as a
     block of text — two lines is the most any of them takes. */
  max-width: 32ch;
}

/* The ghost line at the size of the prose rather than the size of the
   other three. It is the most loaded sentence in the piece, and at
   1.8rem the type was doing the feeling for it — said quietly it
   lands harder. It keeps its centring and its air, so it is still a
   beat and not a paragraph; only the voice drops.

   Full measure, because at this size the cap above would break one
   sentence into three short centred lines. */
.creed-beat-quiet {
  font-size: 1.12rem;
  line-height: 1.8;
  max-width: none;
}

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

/* His closing invitation, as the one control on the page. It gets a
   beat's worth of air above it and nothing after it, so the button
   is the last thing on the page and the page ends on the thing it
   wants you to do.

   The label is a whole sentence rather than the two or three words a
   button usually carries, so it comes out around 24 characters of
   letter-spaced caps — a wide, low control. That is the point: at
   .btn's ordinary width it would read as a form submit at the bottom
   of an essay. */
.creed-close {
  text-align: center;
  margin-top: clamp(2.75rem, 6vw, 4.25rem);
}

/* The arrow on the closing button, and nowhere else on the site. The
   button's .18em tracking already leaves a gap after the last letter,
   so the margin here is the difference between that gap and a space
   the eye reads as deliberate. It carries no tracking of its own —
   letter-spacing on a single glyph is just a right margin, and it
   would push the arrow off its own centre. */
.btn-arrow {
  letter-spacing: 0;
  margin-left: .55em;
}

/* ============================================================
   Where Do I Start
   ============================================================ */

/* The newcomer's route through The Looking Glass Wars: a line down
   the left with five stops hung off it, then a quiet band pointing at
   everything that isn't Wonderland.

   THE ANIMATION IS THE PAGE'S ONE INDULGENCE AND IT IS DELIBERATELY
   SMALL. A stop fades up as it arrives, its dot lands, and the
   segment of line below it draws downward — so the route appears to
   be drawn as you travel it. Nothing loops, nothing pulses, nothing
   moves once it has arrived. The whole effect is gated on
   .route[data-animate="on"], which only JavaScript sets and only when
   the reader hasn't asked for reduced motion: with the attribute
   absent every rule below is inert and the page is simply a list of
   books, which is what it has to be without script anyway.

   Don't add a second animated thing here later. One moving idea on a
   page that exists to give a plain answer is already the ceiling. */

/* --- the head --- */

/* No art above it: the eleven covers below ARE the picture, and a
   hero would be a twelfth image in front of them. So the page opens
   on type, sized under .work-title — this is a signpost, not a
   property announcement. */
/* The title is the first thing on the page — no eyebrow over it, see
   the note in the template — so the band opens with a little more air
   than it would need under a label. */
.start-head {
  text-align: center;
  padding-block: clamp(3.5rem, 7vw, 5.5rem) clamp(2.5rem, 5vw, 4rem);
}

.start-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.9rem, 5vw, 3.1rem);
  letter-spacing: .05em;
  line-height: 1.15;
  margin: 0 0 1.3rem;
}

/* Two sentences, one to a line. Each <span> is its own block, so the
   sentences can never share a line and leave an orphan word from the
   first sitting under it — which is exactly what "short story." did
   when this was one wrapping paragraph.

   The measure is set wide enough that the longer of the two sits on a
   single line on a desktop screen. Below that it wraps, but it wraps
   inside its own block, so the two sentences stay stacked. */
.start-standfirst {
  font-family: var(--serif);
  font-size: clamp(1.15rem, 2.4vw, 1.5rem);
  line-height: 1.5;
  margin: 0 auto;
  max-width: 52ch;
}
.start-standfirst span {
  display: block;
  text-wrap: balance;
}

/* --- the route --- */

.route { padding-block: clamp(1rem, 3vw, 2rem) clamp(3rem, 7vw, 5rem); }

.route-list {
  /* --rail is the width of the column the line runs down; --gap is
     the distance between one stop and the next, and .route-line
     reaches across it to the dot below. --dot is the marker's
     diameter, and the line, the dot's offset and the tail are all
     measured from it, so the marker can be resized in one place. */
  --rail: clamp(2.25rem, 4vw, 3.25rem);
  --gap: clamp(2.75rem, 6vw, 4.75rem);
  --dot: 11px;
  /* Where the dot's top sits: roughly half a line of the kicker it
     has to line up with, less half the dot. */
  --dot-top: .28rem;

  display: grid;
  gap: var(--gap);
  list-style: none;
  margin: 0 auto;
  padding: 0;
  /* Cut to what the three columns actually use, not to the wrap. At
     the full 1320 the copy hit its own measure a third of the way
     across and left a dead right-hand edge, which read as a column
     that failed to load and pulled the whole route off-centre under a
     centred head. */
  max-width: 54rem;
}

.route-stop {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 17rem) minmax(0, 1fr);
  column-gap: clamp(1.25rem, 3vw, 2.75rem);
  align-items: start;
}

/* The rail stretches the full height of its row, so the line has
   somewhere to hang from no matter which column is taller. */
.route-rail {
  position: relative;
  grid-column: 1;
  grid-row: 1;
  align-self: stretch;
}

.route-dot {
  position: absolute;
  top: var(--dot-top);
  left: calc(50% - var(--dot) / 2);
  width: var(--dot); height: var(--dot);
  border-radius: 50%;
  background: var(--rule-strong);
}

/* Hollow, for a stop that hasn't happened yet. */
.route-dot-open {
  background: var(--bg);
  box-shadow: inset 0 0 0 1.5px var(--rule-strong);
}

/* Each stop draws the segment BELOW its own dot, not the segment
   above it — that is what lets a segment animate as part of the stop
   that starts it. It therefore has to reach down across the list's
   row gap and meet the top of the next dot, hence the negative
   bottom. */
.route-line {
  position: absolute;
  top: calc(var(--dot-top) + var(--dot) + .5rem);
  bottom: calc((var(--gap) + var(--dot-top)) * -1);
  left: calc(50% - .5px);
  width: 1px;
  background: var(--rule-strong);
}

/* The last segment. It has no dot to reach, so it runs a fixed
   distance, goes dashed, and fades out: the route is unfinished, not
   broken. Delete the .route-line-tail class from the markup the day a
   sixth stop lands under it and it becomes an ordinary segment. */
.route-line-tail {
  bottom: auto;
  height: clamp(3.5rem, 7vw, 5rem);
  background: repeating-linear-gradient(
    to bottom, var(--rule-strong) 0 5px, transparent 5px 11px);
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent);
  mask-image: linear-gradient(to bottom, #000, transparent);
}

/* --- the covers --- */

/* Every cover on the page is the same width, whether its stop holds
   one or six, and a stop with more of them simply wraps onto another
   row. An earlier pass fanned them like a hand of cards, which looked
   better in isolation and worse here: it made each stop's covers a
   different size, so the six graphic novels came out half the height
   of the novel above them and the row stopped reading as a shelf. */
.route-covers {
  --cover-w: clamp(4.75rem, 8vw, 7rem);
  grid-column: 2;
  grid-row: 1;
  display: flex;
  flex-wrap: wrap;
  gap: .7rem;
}

/* Stop 01 is the book the whole page is recommending, so its cover is
   the one thing here allowed to be bigger than the others. */
.route-covers-lead { --cover-w: clamp(8rem, 13vw, 11.5rem); }

/* Plain blocks, not anchors. The covers linked to their product pages
   until Frank took the shop off this page — see the template. They
   have no hover state now because there is nothing to press. */
.route-cover {
  flex: 0 0 var(--cover-w);
  display: block;
}
.route-cover img {
  width: 100%; height: auto;
  border: 1px solid var(--rule);
}

/* --- the copy --- */

.route-body {
  grid-column: 3;
  grid-row: 1;
  max-width: 46ch;
}

.route-kicker { margin-bottom: .7rem; }

.route-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.35rem, 2.8vw, 1.95rem);
  letter-spacing: .04em;
  line-height: 1.2;
  margin: 0;
}

/* The ampersand in a two-book stop. Lighter and italic, so a title
   that is really two titles reads as a pair rather than as one long
   name. */
.route-amp {
  font-family: var(--serif);
  font-style: italic;
  color: var(--ink-mute);
}

/* What the thing physically is — novel, graphic novel, where it falls
   in the sequence. The site's micro-type would make this a second
   kicker under the title, so it takes the serif at a small size
   instead and stays clearly subordinate to both. */
.route-format {
  font-family: var(--serif);
  font-size: .9rem;
  font-style: italic;
  color: var(--ink-mute);
  margin: .45rem 0 0;
}

.route-copy {
  font-family: var(--serif);
  font-size: 1.05rem;
  line-height: 1.7;
  color: var(--ink-soft);
  margin: 1rem 0 0;
}
.route-copy cite { font-style: italic; }

/* The one place on the route where the reader is told they may do
   something else. Indented behind a hairline so it reads as a note in
   the margin of the advice rather than as more of the advice. */
.route-aside {
  font-family: var(--sans);
  font-size: .88rem;
  line-height: 1.6;
  color: var(--ink-mute);
  border-left: 1px solid var(--rule);
  padding-left: 1rem;
  margin: 1.1rem 0 0;
}

/* --- the reveal --- */

/* Every rule in this block is gated on the attribute, so nothing here
   can hide content from a reader whose script didn't run. See the
   note at the top of this section. */
.route[data-animate='on'] .route-covers,
.route[data-animate='on'] .route-body {
  opacity: 0;
  transform: translateY(14px);
}
.route[data-animate='on'] .route-dot { transform: scale(0); }
.route[data-animate='on'] .route-line {
  transform: scaleY(0);
  transform-origin: top center;
}

.route[data-animate='on'] .is-in .route-covers,
.route[data-animate='on'] .is-in .route-body {
  opacity: 1;
  transform: none;
  transition: opacity .6s var(--ease), transform .6s var(--ease);
}
/* The copy follows the covers in by a beat, so the stop assembles
   left to right the way it is read. */
.route[data-animate='on'] .is-in .route-body { transition-delay: .1s; }

/* A little overshoot on the marker and nowhere else — it is the one
   element small enough to get away with it. */
.route[data-animate='on'] .is-in .route-dot {
  transform: scale(1);
  transition: transform .45s cubic-bezier(.34, 1.4, .5, 1) .05s;
}
/* And then the line draws down towards the next stop, which is the
   effect the whole thing exists for. */
.route[data-animate='on'] .is-in .route-line {
  transform: scaleY(1);
  transition: transform .8s var(--ease) .22s;
}

/* --- beyond Wonderland --- */

/* Prose, not a list. This band carried a line per property with a
   logline beside each; it is now three paragraphs about the company
   that makes them. Left-aligned in a column centred under the centred
   section title, which is how every other run of prose on this site
   sits under a heading. */
.start-elsewhere {
  max-width: 58ch;
  margin-inline: auto;
  font-family: var(--serif);
  font-size: 1.05rem;
  line-height: 1.7;
  color: var(--ink-soft);
}
.start-elsewhere p { margin: 0 0 1.3em; }
.start-elsewhere p:last-child { margin-bottom: 0; }

/* The turn at the end. Full black and one size up, with a beat of air
   over it — it closes the page, and it is the only thing left on it
   that asks the reader to go anywhere.

   Not a button. The store control that used to sit below this band
   was cut with the rest of the selling, and setting this sentence as
   a control would put it straight back in a different typeface. */
.start-elsewhere-close {
  font-size: 1.18rem;
  color: var(--ink);
  margin-top: clamp(1.75rem, 3.5vw, 2.5rem);
  text-wrap: pretty;
}

/* --- narrow --- */

@media (max-width: 860px) {
  /* The covers move above the copy and the rail keeps its column, so
     the line still runs unbroken down the left of the page. Losing
     the rail on narrow screens would cost the page its one idea. */
  .route-stop { grid-template-columns: var(--rail) minmax(0, 1fr); }
  .route-rail { grid-row: 1 / span 2; }
  .route-covers { grid-column: 2; grid-row: 1; }
  .route-body {
    grid-column: 2;
    grid-row: 2;
    max-width: none;
    margin-top: 1.5rem;
  }
  /* Covers first now, so the copy is the thing that follows in. */
  .route[data-animate='on'] .is-in .route-body { transition-delay: .12s; }
}

@media (max-width: 560px) {
  .route-list { --rail: 1.75rem; }
  .route-covers { --cover-w: clamp(4.5rem, 26vw, 6.5rem); }
  .route-covers-lead { --cover-w: clamp(7.5rem, 44vw, 10rem); }
}


/* ============================================================
   The Looking Glass Wars — the franchise page.

   Three components started here: the Myth/Truth pair under the hero,
   the case paragraphs, and the map of six panels. Everything else on
   that page is borrowed — .work-hero, .section, .section-title, .ph,
   .revise — which is why this block is short for the size of the
   page it dresses.

   .ip-map and its panels are now shared with the Liz Cavalier
   Collection, which is the other page on the site that lays out
   several properties at once and shows the state of each. Anything
   changed below lands on both, and that page's own two variations —
   .liz-quote and the gap under .ip-deeper — sit with the components
   they vary rather than in a section of their own. (A third,
   .ip-copy-wanted, dressed a panel whose logline had not been written
   yet; the last of those was filled in on 2026-10-08 and the rule
   went with it.) .ip-myth and .ip-case are still the franchise
   page's alone.
   ============================================================ */

/* The hero body sits closer to the picture than a work page's does.
   There is an eyebrow above the title here, which a work page mostly
   doesn't have, and at the usual padding the three elements drifted
   apart into a stack of unrelated lines. */
.ip-hero .work-hero-body { padding-block: clamp(1.75rem, 4vw, 2.75rem); }
.ip-hero .label { margin-bottom: 1rem; }

/* The Myth / Truth pair.

   TWO ROWS OF ONE OBJECT, not two stacked sentences. An earlier pass
   set them as two blocks with a 1px rule across the measure between
   them, and the rule was the wrong instrument: a line drawn across
   the column is what this site uses to END something, so it read as a
   divider and the pair came apart. The whole force of the thing is
   that the second statement answers the first, and a divider is a
   piece of furniture that says they are unrelated.

   Three things hold it together now. The labels share a column, so
   both halves start at the same left edge. The hairline sits BETWEEN
   the columns rather than between the rows, so the one line on the
   block runs past both of them — a line that spans a pair binds it;
   a line laid across a pair cuts it in half. And the row gap is
   carried as padding inside the cells rather than as grid gap, so
   that hairline is continuous instead of arriving in two pieces.

   The turn is therefore left to do its work in the type alone: the
   second sentence is a size up and full black against the first's
   softer grey. One signal, not three. */
.ip-myth {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  row-gap: 0;
  column-gap: 0;
  width: fit-content;
  max-width: 54ch;
  margin: 0 auto;
  /* The hero is centred and this block is not. It is centred AS A
     BLOCK, and ranging its contents left is the point — the shared
     left edge is what makes the two halves one object. */
  text-align: left;
  font-family: var(--serif);
  font-size: clamp(1.02rem, 1.9vw, 1.3rem);
  line-height: 1.55;
  color: var(--ink-soft);
}

/* The labels. Identical for both rows — it is the sentences that
   turn, not their headings, and giving the second label its own
   weight was a third signal competing with the other two. */
.ip-myth dt {
  font-family: var(--sans);
  font-size: .7rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
  white-space: nowrap;
  padding-block: .6rem;
  padding-right: 1.15rem;
}

/* The hairline lives here, on the left of the copy column, so it is
   drawn once per row and the two segments meet. Hence padding-block
   rather than a row gap: a gap would show as a nick in the line. */
.ip-myth dd {
  margin: 0;
  border-left: 1px solid var(--rule);
  padding-block: .6rem;
  padding-left: 1.15rem;
  text-wrap: pretty;
}

/* The turn, carried entirely by the type. */
.ip-myth-turn + .ip-myth-turn {
  font-size: 1.18em;
  color: var(--ink);
}

@media (max-width: 560px) {
  /* The label column costs too much of a narrow measure, so the
     labels go above their sentences and the hairline moves to the
     outside of the whole block — still one line past both rows, still
     the thing holding them together. */
  .ip-myth {
    grid-template-columns: minmax(0, 1fr);
    width: auto;
    border-left: 1px solid var(--rule);
    padding-left: 1.1rem;
  }
  .ip-myth dt {
    padding: 0 0 .3rem;
  }
  .ip-myth dd {
    border-left: 0;
    padding: 0 0 1.1rem;
  }
  .ip-myth dd:last-child { padding-bottom: 0; }
}

/* --- the case --- */

/* A single column of prose, centred in the band and narrow. It is an
   argument, so it wants to be read as one voice rather than laid out
   as three columns of equal-looking claims. */
.ip-case {
  max-width: 58ch;
  margin-inline: auto;
  font-family: var(--serif);
  font-size: 1.05rem;
  color: var(--ink-soft);
}
.ip-case p { margin: 0 0 1.4rem; }

/* The turn. Full black, a size up, and given air — it is the sentence
   the two paragraphs above exist to set up. */
.ip-case-close {
  font-size: 1.28rem;
  color: var(--ink);
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  text-wrap: pretty;
}
.ip-case .revise { font-family: var(--sans); }

/* --- the map --- */

/* FOUR ACROSS, and the count is doing real work here. There are
   eleven strands — twelve until the tabletop card was parked waiting
   on its box art — and eleven over four columns is 4/4/3. Twelve was
   4/4/4, so putting that card back needs nothing but the card.

   Watch the remainder whenever a strand is added or cut. Ten gave
   4/4/2, also fine. The reason this is four and not three is that at
   three columns ten gave 3/3/3/1 — a single card stranded on a row of
   its own, which reads as a mistake rather than as the end of a list.
   Three columns would also make a 370px card out of art, a status and
   a title, which is wasteful; 276px is generous for that much.

   Four only works because the cards got shorter. They were art,
   status, title, format line and a blurb; they are now art, status and
   title, so a 276px column is generous rather than cramped. If a blurb
   ever comes back, this has to go back to three.

   Not auto-fit: a fluid track count would change the row arithmetic at
   widths nobody chose, and the whole point of the numbers above is
   that they were chosen. */
.ip-map {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(2rem, 3.5vw, 2.75rem) clamp(1.25rem, 2.4vw, 2rem);
}

.ip-panel {
  display: flex;
  flex-direction: column;
  position: relative;
}

/* THE CARD CONTROL. The button is only around the title — that keeps
   the h3 a real h3 and the accessible name short — but its ::after is
   stretched over the whole panel, so the click target is the card.
   z-index 1 puts it over the art without needing one on anything
   else. */
.ip-card-btn {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.ip-card-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* The affordance, and it is deliberately quiet: the art well's
   hairline goes to full black and the title picks up an underline.
   Nothing lifts, scales or shadows — this page is hairlines and air,
   and a card that jumps under the pointer would be the only thing on
   it that moves.

   :focus-visible within gets the same treatment so a keyboard lands
   somewhere visible, and the browser's own focus ring still draws
   around the title. */
.ip-panel:has(.ip-card-btn:hover) .ip-media img,
.ip-panel:has(.ip-card-btn:hover) .ph,
.ip-panel:has(.ip-card-btn:focus-visible) .ip-media img,
.ip-panel:has(.ip-card-btn:focus-visible) .ph {
  border-color: var(--rule-strong);
}
.ip-card-btn:hover,
.ip-card-btn:focus-visible {
  text-decoration: underline;
  text-underline-offset: .14em;
  text-decoration-thickness: 1px;
}
/* --- the Liz Cavalier Collection's two variations --- */

/* The pull quote on that page sits below a .work-split rather than
   inside its prose column — see the template for why — so it needs
   the space between bands that being the last thing in one would
   otherwise have given it. Space and not a rule: on this site a line
   across the measure ends a thing, and this quote IS the ending. */
.liz-quote { margin-top: clamp(2.25rem, 4.5vw, 3.25rem); }

/* .ip-deeper's paragraph has no bottom margin — on the franchise page
   it is the last thing in its band and nothing follows it. On the Liz
   Cavalier Collection the video does, and the two sat on top of each
   other.

   The gap is .section-title's own bottom margin, reused rather than
   picked by eye: in every other band on this site that clamp is the
   distance between the words that introduce a thing and the thing
   itself, which is exactly what this is. */
.ip-deeper + .work-video { margin-top: clamp(2rem, 4vw, 3rem); }


/* The art well. A hairline around it, because several of these will
   eventually hold pale concept art that would otherwise bleed into
   the white ground. .ph draws its own dashed border while the well is
   empty, so the two never stack. */
.ip-media { margin-bottom: 1.1rem; }
.ip-media img {
  width: 100%; height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border: 1px solid var(--rule);
}

/* The status line. A dot and a phrase, filled for a thing that exists
   and hollow for one that doesn't — the same vocabulary as the route
   markers on Where Do I Start, so a reader who has seen that page
   already knows how to read this one. */
.ip-status {
  font-family: var(--sans);
  font-size: .68rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink);
  margin: 0 0 .7rem;
  display: flex; align-items: center; gap: .55rem;
}
.ip-status-soon { color: var(--ink-mute); }

.ip-dot {
  flex: 0 0 auto;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--ink);
}
.ip-dot-open {
  background: none;
  box-shadow: inset 0 0 0 1px var(--ink-mute);
}

.ip-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.2rem, 2.2vw, 1.55rem);
  letter-spacing: .04em;
  line-height: 1.2;
  margin: 0 0 .45rem;
}

.ip-format {
  font-family: var(--sans);
  font-size: .8rem;
  color: var(--ink-mute);
  margin: 0 0 .85rem;
}

.ip-copy {
  font-family: var(--serif);
  font-size: .98rem;
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0;
}

/* The exit link, pushed to the bottom of the panel by the auto margin
   so that every link in a row sits on the same line however much copy
   is above it. Underlined rather than set as a button: six buttons in
   a grid would turn an overview into a menu. */
.ip-exit {
  font-family: var(--sans);
  font-size: .8rem; font-weight: 600;
  letter-spacing: .04em;
  margin: 1.1rem 0 0;
  padding-top: .9rem;
  border-top: 1px solid var(--rule);
}
.ip-panel .ip-exit { margin-top: auto; }
.ip-exit a {
  text-decoration: underline;
  text-underline-offset: .22em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule-strong);
}
.ip-exit a:hover { text-decoration-color: transparent; }

/* --- the hand-off --- */

.ip-deeper {
  max-width: 52ch;
  margin-inline: auto;
  text-align: center;
  font-family: var(--serif);
  font-size: clamp(1.05rem, 2vw, 1.25rem);
  line-height: 1.6;
  color: var(--ink-soft);
}
/* text-wrap: pretty because this paragraph ends on "lives at" and runs
   into the button. Left to itself the line broke after "lives" and
   stranded "at" alone directly above the control, which made the
   sentence look like it had stopped rather than continued. */
.ip-deeper p {
  margin: 0;
  text-wrap: pretty;
}

/* --- the door --- */

/* THE DOMAIN, SET AS TYPE RATHER THAN AS A BUTTON. It was a solid
   black .btn and it was the heaviest object on a page made otherwise
   of hairlines and air — a slab that size reads as commerce, which is
   the one thing this page has been careful not to do.

   It is the same serif as the sentence it finishes, one size up and
   full black, because the sentence above it stops on "lives at" and
   this is the rest of it. */
.ip-door {
  display: inline-flex;
  align-items: center;
  gap: clamp(.55rem, 1.4vw, 1rem);
  margin-top: clamp(1.1rem, 2.2vw, 1.5rem);
  font-family: var(--serif);
  font-size: clamp(1.2rem, 2.6vw, 1.7rem);
  line-height: 1.3;
  letter-spacing: .01em;
  color: var(--ink);
}

/* The sheet's copy of the door. Same control, one step down, because
   inside a panel it is closing a section rather than closing the whole
   page. */
.ip-door-sm {
  font-size: clamp(1.05rem, 2vw, 1.3rem);
  margin-top: .9rem;
}
.ip-door-sm .ip-door-flourish { width: clamp(38px, 5vw, 52px); }

/* At rest it is an ordinary underlined link and nothing else. The
   underline is on the label rather than the anchor so the flourishes
   never get one. */
.ip-door-label {
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: .12em;
  transition: border-color .3s var(--ease);
}

/* THE EXCHANGE. On hover, focus or press the underline gives way and
   the ornament opens out in its place: the utilitarian mark that says
   "this is a link" steps aside for the decorative one that says "this
   is a door". Losing an underline on hover is already this page's
   idiom — the eight panel exits do it — so the gesture is borrowed
   rather than invented. */
.ip-door:hover .ip-door-label,
.ip-door:focus-visible .ip-door-label,
.ip-door:active .ip-door-label {
  border-color: transparent;
}

/* Drawn once pointing inward; the left-hand copy is the same file
   flipped. Colour comes from currentColor, so the ornament is the
   same ink as the word it frames.

   THE WIDTH IS ALWAYS IN THE LAYOUT even though the opacity is zero,
   so the domain does not jump sideways when the flourish arrives.
   Nothing on this page may move under the pointer. */
.ip-door-flourish {
  flex: 0 0 auto;
  width: clamp(44px, 6.5vw, 66px);
  height: auto;
  opacity: 0;
  transition: opacity .32s var(--ease), transform .46s var(--ease);
}

/* They start tucked in behind the word and open outward, which is why
   each side needs its own pair of values: "outward" is a different
   direction on the left than on the right. The left one carries the
   mirror in the same transform, so both states must restate it. */
.ip-door-flourish:first-child { transform: translateX(9px) scaleX(-1); }
.ip-door-flourish:last-child  { transform: translateX(-9px); }

.ip-door:hover   .ip-door-flourish:first-child,
.ip-door:focus-visible .ip-door-flourish:first-child,
.ip-door:active  .ip-door-flourish:first-child { transform: translateX(0) scaleX(-1); }

.ip-door:hover   .ip-door-flourish:last-child,
.ip-door:focus-visible .ip-door-flourish:last-child,
.ip-door:active  .ip-door-flourish:last-child { transform: translateX(0); }

.ip-door:hover .ip-door-flourish,
.ip-door:focus-visible .ip-door-flourish,
.ip-door:active .ip-door-flourish { opacity: 1; }

/* On a small phone the ornament is the first thing to give. At the
   default sizes the door comes to 326px, which clears a 375px screen
   by nine pixels and does not clear a 360px one — and the flourishes
   are flex: 0 0 auto, so instead of shrinking they would push the
   domain into a second line. Smaller ornament, slightly smaller type,
   and it fits a 320px measure with room to spare. */
@media (max-width: 420px) {
  .ip-door {
    font-size: 1.08rem;
    gap: .45rem;
  }
  .ip-door-flourish { width: 34px; }
}

/* The ornament still arrives, it just stops travelling. Note the rest
   state has to keep its mirror rather than going to transform: none,
   or the left-hand flourish would unflip. */
@media (prefers-reduced-motion: reduce) {
  .ip-door-flourish { transition: opacity .2s linear; }
  .ip-door-flourish:first-child,
  .ip-door:hover .ip-door-flourish:first-child,
  .ip-door:focus-visible .ip-door-flourish:first-child,
  .ip-door:active .ip-door-flourish:first-child { transform: scaleX(-1); }
  .ip-door-flourish:last-child,
  .ip-door:hover .ip-door-flourish:last-child,
  .ip-door:focus-visible .ip-door-flourish:last-child,
  .ip-door:active .ip-door-flourish:last-child { transform: none; }
}

/* --- narrow --- */

@media (max-width: 1180px) {
  /* Three, and this is the one band where the orphan comes back —
     3/3/3/1. It is a narrow range of widths and the alternative was
     jumping straight from four columns to two, which makes a 500px
     card out of an art well and a title. */
  .ip-map { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 900px) {
  /* Two, which is five clean rows. */
  .ip-map { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 640px) {
  .ip-map { grid-template-columns: 1fr; }
  /* One column means the exit links no longer need to align across a
     row, and pinning them to the bottom just adds a gap under short
     panels. */
  .ip-panel .ip-exit { margin-top: 1.1rem; }
}

/* ============================================================
   The life pages — Skier / Producer / Author
   ============================================================ */

/* Three pages, one shape. Each opens on a portrait beside a name,
   the way the single Biography page did, so they read as three
   chapters of one thing rather than three unrelated pages. They
   reuse .bio-hero and its grid outright; only the standfirst below
   the title is new. */

/* The one line under the name that says what the chapter is about.
   Serif, because it is a sentence rather than a label, and the
   tracked capitals above it are already doing the label's job. */
.life-standfirst {
  font-family: var(--serif);
  font-size: clamp(1rem, 1.6vw, 1.15rem);
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 1.1rem 0 0;
  max-width: 46ch;
}

}


/* ============================================================
   The sheets — the lightbox behind each card on the franchise page.

   A native <dialog>, so the scrim is ::backdrop rather than a fixed
   div, focus is trapped by the browser, Escape closes it and the page
   behind goes inert. See the markup for why that was worth it.
   ============================================================ */

.ip-sheet {
  /* A <dialog> is display:none until opened, so none of this costs
     anything while the page is just sitting there. */
  width: min(58rem, calc(100vw - 2.5rem));
  max-height: min(88vh, 56rem);
  padding: 0;
  border: 1px solid var(--rule-strong);
  background: var(--bg);
  color: var(--ink);
  overflow: hidden;
}

/* Only [open], because `dialog:not([open]) { display: none }` is a UA
   rule and an unqualified author `display: flex` here would beat it
   and leave every sheet on the page permanently visible. */
.ip-sheet[open] {
  display: flex;
  flex-direction: column;
}

/* The scrim. Warmer and lighter than a flat black wash, so the page
   behind stays legible as context rather than being switched off. */
.ip-sheet::backdrop {
  background: rgba(20, 18, 16, .55);
}

/* THE SCROLL LIVES ON THE INNER WRAPPER, not the dialog, so the
   dialog's border stays put while the content moves inside it.

   The dialog has to become a flex column for that to work. An earlier
   pass gave the wrapper max-height: inherit and it silently did
   nothing — the dialog clipped at 88vh with overflow: hidden and the
   content below simply vanished, unreachable. As a flex column with
   min-height: 0 on the child, the wrapper is told how tall it may be
   and takes the scrollbar itself.

   min-height: 0 is the load-bearing line: a flex item defaults to
   min-height: auto, which refuses to shrink below its content, which
   is exactly the bug above wearing a different hat. */
.ip-sheet-inner {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: clamp(2rem, 4vw, 3.25rem) clamp(1.5rem, 4vw, 3.25rem)
           clamp(2rem, 4vw, 3rem);
  overscroll-behavior: contain;
}

/* The close control, pinned to the dialog rather than to the scrolling
   wrapper — see the markup. It carries the page background because
   content passes under it, and sits above that content in the stack. */
.ip-sheet-close {
  position: absolute;
  z-index: 2;
  background: var(--bg);
  top: clamp(.9rem, 2vw, 1.4rem);
  right: clamp(.9rem, 2vw, 1.4rem);
  width: 2.25rem; height: 2.25rem;
  display: grid; place-items: center;
  border: 1px solid var(--rule);
  color: var(--ink-mute);
  cursor: pointer;
  transition: border-color .2s var(--ease), color .2s var(--ease);
}
.ip-sheet-close:hover,
.ip-sheet-close:focus-visible {
  border-color: var(--rule-strong);
  color: var(--ink);
}
.ip-sheet-close svg { width: 15px; height: 15px; }

/* --- the head --- */

/* Centred, like every other head on this site, and carrying the same
   status line as the card it came from — that continuity is what tells
   a reader the sheet belongs to the thing they just pressed. */
.ip-sheet-head {
  text-align: center;
  padding-right: 2.5rem;
  padding-left: 2.5rem;
}
.ip-sheet-head .ip-status { justify-content: center; }

.ip-sheet-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.6rem, 3.6vw, 2.4rem);
  letter-spacing: .04em;
  line-height: 1.15;
  margin: 0 0 .5rem;
}

.ip-sheet-lead {
  font-family: var(--serif);
  font-size: clamp(1.05rem, 2vw, 1.22rem);
  line-height: 1.6;
  color: var(--ink-soft);
  text-align: center;
  max-width: 48ch;
  margin: clamp(1.5rem, 3vw, 2.25rem) auto 0;
  text-wrap: pretty;
}

/* The sub-line under a lead. Frank asked for the musical's credits
   sentence to drop below its claim rather than run on at lead size,
   and the reasoning generalises: a lead makes the claim, a sub carries
   the evidence for it.

   Smaller, greyer and on the same centred axis, so it reads as the
   second half of one block rather than as a new paragraph. Sized at a
   fixed .95rem rather than a clamp — the lead above it already scales,
   and two clamps stacked made the gap between them breathe unevenly
   across the range. */
.ip-sheet-sub {
  font-family: var(--serif);
  font-size: .95rem;
  line-height: 1.6;
  color: var(--ink-mute);
  text-align: center;
  max-width: 56ch;
  margin: .9rem auto 0;
  text-wrap: pretty;
}

/* A cite inside the sub keeps its italic but not the extra weight the
   surrounding grey would otherwise wash out. */
.ip-sheet-sub cite { color: var(--ink-soft); }

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

/* An ordered list, because five novels in a series are ordered, and a
   screen reader should be told so. The numbers are suppressed: the
   covers carry the sequence visually and a numeral beside each one
   would compete with the reading-order page, which is where numbering
   actually means something. */
.ip-books {
  list-style: none;
  margin: clamp(2rem, 4vw, 3rem) 0 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}

/* Cover beside copy, and a hairline between each pair — the same
   grammar as the rest of the site, and it makes five rows scan as one
   shelf instead of five cards. */
.ip-book {
  position: relative;
  display: grid;
  grid-template-columns: clamp(3.75rem, 8vw, 5.25rem) minmax(0, 1fr);
  gap: clamp(1rem, 2.5vw, 1.75rem);
  align-items: start;
  padding: clamp(1.1rem, 2.4vw, 1.6rem) 0;
  border-bottom: 1px solid var(--rule);
}

.ip-book-cover {
  width: 100%; height: auto;
  border: 1px solid var(--rule);
}

.ip-book-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.02rem, 2vw, 1.22rem);
  letter-spacing: .03em;
  line-height: 1.25;
  margin: 0 0 .4rem;
}

/* THE WHOLE ROW IS THE LINK, not just the title. The anchor stays on
   the title — that keeps the h3 a real heading and the accessible name
   short — and its ::after is stretched over the row, so the cover, the
   description and the space around them all carry the click. Same
   device as .ip-card-btn on the grid above.

   It applies only to rows that HAVE a link. Hatter M's six volumes are
   .ip-book rows with no anchor in them, so they get no overlay and
   stay inert, which is correct — there are no product pages for those.

   ⚠ ONE COST, and it is the known cost of this pattern: the overlay
   sits above the text, so the description in a linked row can no
   longer be selected with the mouse. Accepted deliberately — Frank
   asked that a click anywhere land on the book.

   Underlined at rest and the underline gives way on hover, which is
   this page's idiom already: .ip-exit did it, and so does the door.
   Because the overlay is part of the anchor, hovering anywhere in the
   row triggers that, and the cover's hairline goes to full black with
   it. */
.ip-book:has(.ip-book-title a) { cursor: pointer; }

.ip-book-title a::after {
  content: '';
  position: absolute;
  inset: 0;
}

.ip-book:has(.ip-book-title a:hover) .ip-book-cover,
.ip-book:has(.ip-book-title a:focus-visible) .ip-book-cover {
  border-color: var(--rule-strong);
}

.ip-book-title a {
  text-decoration: underline;
  text-underline-offset: .16em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--rule-strong);
  transition: text-decoration-color .2s var(--ease);
}
.ip-book-title a:hover,
.ip-book-title a:focus-visible {
  text-decoration-color: transparent;
}

.ip-book-line {
  font-family: var(--serif);
  font-size: .96rem;
  line-height: 1.55;
  color: var(--ink-soft);
  margin: 0;
  max-width: 52ch;
}

/* --- prose and credits --- */

/* The body of a sheet that has no list in it — the soundtrack, the
   game, television. Narrower than the sheet and centred, because
   three paragraphs set to the full width of a 58rem dialog is a
   newspaper column. */
.ip-sheet-prose {
  max-width: 54ch;
  margin: clamp(1.75rem, 3.5vw, 2.5rem) auto 0;
  font-family: var(--serif);
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--ink-soft);
}
.ip-sheet-prose p { margin: 0 0 1.1rem; }
.ip-sheet-prose p:last-child { margin-bottom: 0; }
.ip-sheet-prose a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: .18em;
}

/* Credits — people rather than books, so no cover column. Same
   hairline rhythm as .ip-books, which is what makes a sheet of
   artists and a sheet of novels feel like the same object. */
.ip-credits {
  list-style: none;
  margin: clamp(1.75rem, 3.5vw, 2.5rem) 0 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}
.ip-credit {
  padding: clamp(1rem, 2.2vw, 1.4rem) 0;
  border-bottom: 1px solid var(--rule);
}
.ip-credit-name {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.02rem, 2vw, 1.2rem);
  letter-spacing: .03em;
  line-height: 1.25;
  margin: 0;
}

/* The role, between the name and the line. Same micro-type as the
   status labels, so a reader already knows to read it as a tag
   rather than as the start of the sentence below it. */
.ip-credit-meta {
  font-family: var(--sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: .3rem 0 .45rem;
}
.ip-credit-line {
  font-family: var(--serif);
  font-size: .96rem;
  line-height: 1.55;
  color: var(--ink-soft);
  margin: 0;
  max-width: 58ch;
}

/* --- the foot --- */

.ip-sheet-foot {
  text-align: center;
  padding-top: clamp(1.75rem, 3.5vw, 2.5rem);
}

/* The line that makes the door worth pressing: it says what is on the
   other side that isn't in the sheet. Sized under the lead, because it
   is a caption to the control beneath it and not another paragraph. */
.ip-sheet-foot-line {
  font-family: var(--serif);
  font-size: 1rem;
  color: var(--ink-mute);
  margin: 0;
  max-width: 42ch;
  margin-inline: auto;
  text-wrap: pretty;
}

/* --- motion --- */

/* The sheet fades and rises a little as it opens. allow-discrete is
   what lets display:none participate, so the dialog can animate in
   rather than appearing fully formed. Browsers without it simply get
   the sheet, which is the behaviour this replaced. */
@media (prefers-reduced-motion: no-preference) {
  .ip-sheet {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity .28s var(--ease), transform .32s var(--ease),
                overlay .32s allow-discrete, display .32s allow-discrete;
  }
  .ip-sheet[open] {
    opacity: 1;
    transform: translateY(0);
  }
  @starting-style {
    .ip-sheet[open] {
      opacity: 0;
      transform: translateY(14px);
    }
  }

  .ip-sheet::backdrop { opacity: 0; transition: opacity .28s var(--ease),
                        overlay .28s allow-discrete, display .28s allow-discrete; }
  .ip-sheet[open]::backdrop { opacity: 1; }
  @starting-style { .ip-sheet[open]::backdrop { opacity: 0; } }
}

/* --- narrow --- */

@media (max-width: 560px) {
  .ip-sheet {
    width: 100vw;
    max-width: 100vw;
    max-height: 92vh;
    /* Full-bleed on a phone, and the bottom edge is the only one left
       drawn — the sheet reaches the other three. */
    border-width: 1px 0 0;
    margin: auto 0 0;
  }
  .ip-book {
    grid-template-columns: 3.5rem minmax(0, 1fr);
    gap: 1rem;
  }
}

/* ============================================================
   Home page segments
   ============================================================ */

/* The three 3D covers. They are renders with their own shadow baked
   in on a transparent ground, so they get no border and no frame —
   anything drawn round them fights the shadow. */
.cover-row {
  list-style: none;
  margin: clamp(2rem, 4vw, 3rem) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1rem, 3vw, 2.5rem);
  align-items: end;
  /* Centred to sit under the centred title, like every other block
     the section headers lead into. */
  max-width: 54rem;
  margin-inline: auto;
}

.cover img {
  width: 100%;
  height: auto;
}

/* One row of actions closing a segment. */
.home-cta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: .75rem;
  margin: clamp(1.75rem, 4vw, 2.5rem) 0 0;
}

@media (max-width: 640px) {
  /* Three covers at 375px would be 100px each — too small to read the
     issue lettering. Two up, the third centred under them: four tracks,
     each cover spanning two, so the odd one out can start in the second
     track and sit dead centre at the same width as the pair above. */
  .cover-row { grid-template-columns: repeat(4, 1fr); }
  .cover { grid-column: span 2; }
  .cover:last-child:nth-child(odd) { grid-column: 2 / span 2; }

  /* The home page's blog teaser only — the one .post-feature followed
     by a .home-cta. A 3:1 banner at phone width is a strip about 110px
     tall and the art stops reading, so it crops square from the middle,
     the same crop the blog archive's cards already use. Desktop keeps
     the full banner. */
  .wrap:has(> .home-cta) > .post-feature .post-feature-media { aspect-ratio: 1 / 1; }
}

/* ============================================================
   Bio teases — Author, Producer, Skier
   ============================================================ */

/* Heading and logline are one target. Nothing about it looks like a
   link until you point at it: the page already has three headings that
   are not links, and underlining these would make the set inconsistent. */
.bio-tease-head {
  display: block;
  color: inherit;
  text-decoration: none;
}
.bio-tease-head .section-title { transition: color .2s var(--ease); }
.bio-tease-head:hover .section-title,
.bio-tease-head:focus-visible .section-title { color: var(--ink-mute); }
.bio-tease-head:focus-visible { outline: 2px solid var(--ink); outline-offset: 6px; }

/* --- the revised tease copy: a pull-line, then Frank's own prose --- */

/* The line under the heading. Not uppercase: the heading above it
   already is, and two runs of caps stacked read as shouting. */
.bio-tease-lede {
  font-family: var(--serif);
  font-size: clamp(1.15rem, 2.4vw, 1.6rem);
  line-height: 1.4;
  color: var(--ink);
  text-align: center;
  /* No measure cap: the longest of these is 40 characters and fits on one
     line at any desktop width. A 24ch cap was breaking every one of them
     and leaving a single word stranded underneath. */
  margin: -1rem auto clamp(1.75rem, 3.5vw, 2.5rem);
  /* Where it must wrap — a phone — break the line evenly rather than
     pushing one word onto its own line. */
  text-wrap: balance;
}

/* The single picture. A link round an image and nothing else — no
   well, no border: these are photographs of Frank rather than mixed
   archive material, so there is no ragged set of shapes to reconcile
   and a frame would only make three identical boxes. */
.bio-tease-shot {
  display: block;
  max-width: 34rem;
  margin: clamp(2rem, 4vw, 2.75rem) auto 0;
}
.bio-tease-shot img {
  width: 100%;
  height: auto;
  transition: transform .45s var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .bio-tease-shot:hover img { transform: scale(1.02); }
}
.bio-tease-shot:focus-visible { outline: 2px solid var(--ink); outline-offset: 5px; }

/* A lead-in sentence set above a quotation (Author page's close),
   and a quotation that runs to more than one paragraph. */
.work-prose + .bio-quote { margin-top: clamp(1.5rem, 3vw, 2.25rem); }
.bio-quote p + p { margin-top: 1em; }
