    /* Europa @font-face. Adobe exposes 300/400/700 (+ 400/700 italic); our design
       asks for 800 in places, so 800 is mapped to the real 700 outlines rather
       than left to the browser to synthesise a fake-bold. */
    @font-face {
      font-family: 'europa';
      font-weight: 300;
      font-style: normal;
      font-display: swap;
      src: url('../fonts/europa-300.woff2') format('woff2');
    }

    @font-face {
      font-family: 'europa';
      font-weight: 400;
      font-style: normal;
      font-display: swap;
      src: url('../fonts/europa-400.woff2') format('woff2');
    }

    @font-face {
      font-family: 'europa';
      font-weight: 700;
      font-style: normal;
      font-display: swap;
      src: url('../fonts/europa-700.woff2') format('woff2');
    }

    @font-face {
      font-family: 'europa';
      font-weight: 800;
      font-style: normal;
      font-display: swap;
      src: url('../fonts/europa-700.woff2') format('woff2');
    }

    @font-face {
      font-family: 'europa';
      font-weight: 400;
      font-style: italic;
      font-display: swap;
      src: url('../fonts/europa-400i.woff2') format('woff2');
    }

    @font-face {
      font-family: 'europa';
      font-weight: 700;
      font-style: italic;
      font-display: swap;
      src: url('../fonts/europa-700i.woff2') format('woff2');
    }

    @font-face {
      font-family: 'europa';
      font-weight: 800;
      font-style: italic;
      font-display: swap;
      src: url('../fonts/europa-700i.woff2') format('woff2');
    }

    /* Calluna Sans — the FFWPU brand body face. Their kit ships only 300 and
       700 (+ italics), no regular 400, so body text renders in the 300 'light'
       weight exactly as it does on ffwpu.org.uk. Bold/strong maps to 700. */
    @font-face {
      font-family: 'calluna-sans';
      font-weight: 300;
      font-style: normal;
      font-display: swap;
      src: url('../fonts/calluna-sans-300.woff2') format('woff2');
    }

    @font-face {
      font-family: 'calluna-sans';
      font-weight: 700;
      font-style: normal;
      font-display: swap;
      src: url('../fonts/calluna-sans-700.woff2') format('woff2');
    }

    @font-face {
      font-family: 'calluna-sans';
      font-weight: 300;
      font-style: italic;
      font-display: swap;
      src: url('../fonts/calluna-sans-300i.woff2') format('woff2');
    }

    @font-face {
      font-family: 'calluna-sans';
      font-weight: 700;
      font-style: italic;
      font-display: swap;
      src: url('../fonts/calluna-sans-700i.woff2') format('woff2');
    }

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

    :root {
      --brand: #0C6E9E;
      --brand-dark: #023E62;
      --brand-light: #EAF6FB;
      --ink: #062033;
      --sky: #ACDDEB;
      --navy: #023E62;
      --ink-800: #111827;
      --ink-700: #1F2937;
      --muted: #6B7280;
      --border: #E5E7EB;
      --bg: #FFFFFF;
      --bg-subtle: #F9FAFB;
      --gold: #C8962B;
      --gold-light: #FEF3C7;
      --red: #DC2626;
      --red-light: #FEF2F2;
      --radius: 10px;
    }

    /* Smooth scrolling is motion too — anyone who has asked the OS for less of it
       gets the instant jump instead. */
    @media (prefers-reduced-motion: no-preference) {
      html {
        scroll-behavior: smooth;
      }
    }

    /* Nav height plus a little air. Without this, every in-page anchor lands
       flush with the viewport top and the sticky nav sits on top of it — most
       visible on mobile, where section padding (48px) is less than the nav. */
    :target,
    section[id] {
      scroll-margin-top: 72px;
    }

    /* Every photograph is offered as WebP first and JPEG second, which means the
       img is wrapped in a picture. A picture is an inline box by default, and
       that box would sit between each image and the parent it was written to fit
       — the bio portrait is sized at height:100%, and a percentage height cannot
       resolve against a wrapper that has no height of its own. display:contents
       takes the wrapper out of the box tree entirely, so every img lays out
       against the parent it always laid out against and no existing rule has to
       know the picture is there. */
    picture {
      display: contents;
    }

    /* One focus ring for the whole page. :focus-visible keeps it off mouse
       clicks, so it costs the pointer user nothing and gives the keyboard user
       everything. */
    a:focus-visible,
    button:focus-visible,
    [tabindex]:focus-visible {
      outline: 3px solid var(--brand);
      outline-offset: 3px;
      border-radius: 4px;
    }

    /* On the dark bands the brand blue disappears into the navy. */
    .hero a:focus-visible,
    .qband a:focus-visible,
    .condition-section a:focus-visible,
    .condition-section button:focus-visible,
    footer a:focus-visible,
    .vp button:focus-visible,
    .vp [tabindex]:focus-visible {
      outline-color: var(--sky);
    }

    body {
      font-family: 'calluna-sans', 'Inter', system-ui, -apple-system, sans-serif;
      background: var(--bg);
      color: var(--ink);
      line-height: 1.6;
      -webkit-font-smoothing: antialiased;
      /* Calluna's x-height is much smaller than Inter's, so at the same px it
         read too small. Normalise it up to Inter's aspect (~0.545) so body copy
         is the size it was. font-size-adjust inherits, so the Europa display
         faces opt out below — their sizes are already tuned. */
      font-size-adjust: 0.545;
    }

    .nav-logo-main,
    .nav-logo-sub,
    .hero h1,
    .section-title,
    .book-content h2,
    .bio-text h2,
    .footer-brand-name,
    .speech-quote,
    .condition-intro h2,
    .modal-title,
    .qband blockquote {
      font-size-adjust: none;
    }

    /* ── NAV ── */
    nav {
      position: sticky;
      top: 0;
      z-index: 100;
      background: rgba(255, 255, 255, 0.95);
      backdrop-filter: blur(12px);
      border-bottom: 1px solid var(--border);
    }

    /* Set by the head script on weak hardware or a poor connection. The bar is
       already 95% white, so losing the blur costs a sliver of translucency and
       nothing else — and it takes a per-frame GPU pass off a scrolling page.
       The modal's backdrop goes the same way, and darkens slightly to make up
       for the separation the blur was giving it. */
    html.lite nav {
      background: #fff;
      -webkit-backdrop-filter: none;
      backdrop-filter: none;
    }

    html.lite .modal-backdrop {
      background: rgba(6, 32, 51, 0.86);
      -webkit-backdrop-filter: none;
      backdrop-filter: none;
    }

    .nav-inner {
      max-width: 1200px;
      margin: 0 auto;
      padding: 0 1.5rem;
      height: 64px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 2rem;
    }

    .nav-logo {
      display: flex;
      align-items: center;
      gap: 0.65rem;
      text-decoration: none;
    }

    .nav-logo-mark {
      width: 36px;
      height: 36px;
      display: block;
      flex-shrink: 0;
    }

    .nav-logo-text {
      display: flex;
      flex-direction: column;
      line-height: 1.1;
    }

    .nav-logo-main {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-size: 1.05rem;
      font-weight: 700;
      color: var(--ink);
    }

    .nav-logo-sub {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-style: italic;
      font-size: 0.78rem;
      color: var(--brand);
      margin-top: 2px;
    }

    .nav-links {
      display: flex;
      align-items: center;
      gap: 0.25rem;
      list-style: none;
    }

    .nav-links a {
      font-size: 0.85rem;
      font-weight: 500;
      color: var(--ink-700);
      text-decoration: none;
      padding: 0.4rem 0.75rem;
      border-radius: 6px;
      transition: background 0.15s, color 0.15s;
    }

    .nav-links a:hover {
      background: var(--bg-subtle);
      color: var(--ink);
    }

    /* The row's 0.25rem gap is right between plain links; against a solid
       button it reads as a collision, because the button's own padding starts
       where the gap ends. It gets a little more room to itself. */
    .nav-cta {
      background: var(--brand) !important;
      color: #fff !important;
      font-weight: 700 !important;
      font-size: 0.82rem !important;
      margin-left: 0.6rem;
    }

    .nav-cta:hover {
      background: var(--brand-dark) !important;
    }

    .nav-toggle {
      display: none;
      background: none;
      border: none;
      cursor: pointer;
      padding: 0.35rem;
      color: var(--ink);
      -webkit-tap-highlight-color: transparent;
      border-radius: 6px;
      transition: background 0.15s;
    }

    .nav-toggle:active {
      background: var(--bg-subtle);
    }

    /* Burger ↔ X crossfade. 44px is the floor for a thumb — the icon inside
       stays 22px, the tap area around it does the growing. */
    .nav-toggle {
      position: relative;
      width: 44px;
      height: 44px;
      margin-right: -0.35rem;
    }

    .nav-toggle svg {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      transition: opacity 0.25s ease-in-out, transform 0.3s ease-in-out;
    }

    .icon-burger {
      opacity: 1;
      transform: translate(-50%, -50%) rotate(0deg) !important;
    }

    .icon-close {
      opacity: 0;
      transform: translate(-50%, -50%) rotate(-45deg) scale(0.8) !important;
    }

    .nav-toggle.open .icon-burger {
      opacity: 0;
      transform: translate(-50%, -50%) rotate(45deg) scale(0.8) !important;
    }

    .nav-toggle.open .icon-close {
      opacity: 1;
      transform: translate(-50%, -50%) rotate(0deg) !important;
    }

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

      /* Slide-down drawer using max-height */
      .nav-links {
        display: flex;
        flex-direction: column;
        position: absolute;
        top: 64px;
        left: 0;
        right: 0;
        background: #fff;
        border-bottom: 1px solid var(--border);
        padding: 0 1.5rem;
        gap: 0.1rem;
        max-height: 0;
        overflow: hidden;
        opacity: 0;
        pointer-events: none;
        /* pointer-events alone still leaves the links in the tab order, so a
           keyboard user tabs through six invisible items. visibility takes them
           out of the accessibility tree; the delay lets the close animation
           finish before they vanish. */
        visibility: hidden;
        transition: max-height 0.35s ease-in-out, opacity 0.25s ease-in-out, padding 0.35s ease-in-out,
          visibility 0s linear 0.35s;
      }

      .nav-links.open {
        visibility: visible;
        transition: max-height 0.35s ease-in-out, opacity 0.25s ease-in-out, padding 0.35s ease-in-out,
          visibility 0s;
        max-height: 420px;
        opacity: 1;
        pointer-events: auto;
        padding: 0.75rem 1.5rem;
      }

      .nav-links a {
        padding: 0.65rem 0.5rem;
        display: block;
      }

      /* In the drawer the links are a stack, not a row — the button sits in
         line with them and the nudge would only indent it. */
      .nav-cta {
        margin-left: 0;
      }
    }

    /* ── ALERT ── */
    .alert-banner {
      background: var(--red-light);
      border-bottom: 1px solid #FECACA;
      text-align: center;
      padding: 0.55rem 1.5rem;
      font-size: 0.82rem;
      font-weight: 500;
      color: var(--red);
    }

    .alert-banner strong {
      font-weight: 700;
    }

    .alert-banner a {
      color: var(--red);
      font-weight: 700;
      text-decoration: underline;
    }

    /* ── HERO ── */
    .hero {
      background-color: var(--ink);
      color: #fff;
      padding: 6rem 1.5rem 5rem;
      text-align: center;
      position: relative;
      overflow: hidden;
      isolation: isolate;
    }

    /* The photograph gets its own layer so it can be mirrored — she faces left to
       right, into the page. isolation on .hero keeps z-index:-1 above its own
       background-colour rather than falling behind it. */
    .hero::after {
      content: '';
      position: absolute;
      inset: 0;
      z-index: -1;
      pointer-events: none;
      background: var(--hero-bg) center 32% / cover no-repeat;
      transform: scaleX(-1);
    }

    /* The copy must be lifted out of static flow, or the absolutely-positioned
       ::before wash paints over it and dims the type along with the photo. */
    .hero>.container {
      position: relative;
      z-index: 1;
    }

    /* Wash + vignette + sky bloom, over the photo but under the orbs and the copy. */
    .hero::before {
      content: '';
      position: absolute;
      inset: 0;
      pointer-events: none;
      background:
        radial-gradient(ellipse 70% 60% at 50% 0%, rgba(172, 221, 235, 0.18) 0%, transparent 70%),
        radial-gradient(ellipse 90% 95% at 50% 45%, transparent 0%, rgba(2, 32, 51, 0.26) 58%, rgba(2, 32, 51, 0.68) 100%),
        linear-gradient(160deg, rgba(2, 32, 51, 0.80) 0%, rgba(8, 48, 76, 0.75) 55%, rgba(2, 62, 98, 0.70) 100%);
    }

    /* The same light as the one over her message, on the hero's dark ground.
       It is the section this effect was always going to suit best: white beams
       need something to fall on, and a navy photograph gives them far more than
       pale blue does — which is why the alphas here are roughly half.

       Kept to the top and the shoulders, and pulled well clear of the middle,
       because her face and the headline are there and neither wants light
       crossing it. See .words-rays for how the two fans interfere. */
    .hero-rays {
      position: absolute;
      inset: 0;
      z-index: 0;
      pointer-events: none;
      overflow: hidden;
      mix-blend-mode: plus-lighter;
      /* Never reaching full strength even at the top, and gone earlier down the
         frame, so the light is something you notice on second look rather than
         the first thing the hero says. */
      -webkit-mask-image: radial-gradient(125% 88% at 50% -22%, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.34) 42%, transparent 70%);
      mask-image: radial-gradient(125% 88% at 50% -22%, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.34) 42%, transparent 70%);
    }

    .hero-rays::before,
    .hero-rays::after {
      content: '';
      position: absolute;
      top: -130%;
      left: 42%;
      width: 300%;
      aspect-ratio: 1;
      translate: -50% -50%;
      transform-origin: 50% 50%;
      /* Cooler than the light over her message — this one is coming through the
         same sky the hero's own bloom is tinted with, not through a window. */
      background-image:
        repeating-conic-gradient(from 0deg at 50% 50%,
          rgba(226, 244, 252, 0) 0deg,
          rgba(226, 244, 252, 0) 2.2deg,
          rgba(226, 244, 252, 0.11) 3.4deg,
          rgba(226, 244, 252, 0.11) 4.6deg,
          rgba(226, 244, 252, 0) 6.1deg,
          rgba(226, 244, 252, 0) 10.4deg,
          rgba(226, 244, 252, 0.047) 11.5deg,
          rgba(226, 244, 252, 0.047) 12.3deg,
          rgba(226, 244, 252, 0) 13.9deg,
          rgba(226, 244, 252, 0) 18.6deg,
          rgba(226, 244, 252, 0.072) 19.7deg,
          rgba(226, 244, 252, 0.072) 20.6deg,
          rgba(226, 244, 252, 0) 22.4deg,
          rgba(226, 244, 252, 0) 26deg),
        repeating-conic-gradient(from 9deg at 50% 50%,
          rgba(226, 244, 252, 0) 0deg,
          rgba(226, 244, 252, 0) 3.2deg,
          rgba(226, 244, 252, 0.064) 5.8deg,
          rgba(226, 244, 252, 0.064) 8.4deg,
          rgba(226, 244, 252, 0) 11.6deg,
          rgba(226, 244, 252, 0) 17.5deg);
      background-blend-mode: difference;
      -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 14%, rgba(0, 0, 0, 0.55) 42%, transparent 74%);
      mask-image: radial-gradient(circle at 50% 50%, #000 14%, rgba(0, 0, 0, 0.55) 42%, transparent 74%);
    }

    .hero-rays::before {
      filter: blur(11px);
      animation: heroRays 44s ease-in-out infinite alternate;
    }

    .hero-rays::after {
      filter: blur(34px);
      opacity: 0.58;
      animation: heroRaysBloom 53s ease-in-out infinite alternate;
    }

    /* Slower than the light downstairs. This is the first thing anyone sees and
       it should look still until you have been looking a while. */
    @keyframes heroRays {
      from {
        transform: rotate(-4.5deg);
      }

      to {
        transform: rotate(4.5deg);
      }
    }

    @keyframes heroRaysBloom {
      from {
        transform: rotate(-3deg);
      }

      to {
        transform: rotate(5.8deg);
      }
    }

    @media (prefers-reduced-motion: reduce) {

      .hero-rays::before,
      .hero-rays::after {
        animation: none;
      }
    }

    @keyframes pulse {

      0%,
      100% {
        opacity: 1;
        transform: scale(1);
      }

      50% {
        opacity: 0.5;
        transform: scale(0.8);
      }
    }

    .hero h1 {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-size: clamp(2.4rem, 6vw, 4.2rem);
      font-weight: 800;
      line-height: 1.1;
      max-width: 820px;
      margin: 0 auto 1.5rem;
      letter-spacing: -0.01em;
    }

    .hero h1 em {
      font-style: normal;
      color: var(--sky);
      /* The blue clause is one line by design — the <br> splits it from the
         white clause, and nowrap keeps "…God is alive." from wrapping to an
         orphan. The narrow-screen font-size below is tuned so it always fits. */
      white-space: nowrap;
    }

    /* Below 640px the base clamp's 2.4rem floor is wider than the blue clause
       can fit, so it would clip under nowrap. This fluid size keeps the blue
       clause on one line from 320px up, while the white clause wraps to two. */
    @media (max-width: 640px) {
      .hero h1 {
        font-size: clamp(1.3rem, 6.8vw, 2.6rem);
      }
    }

    .hero-sub {
      font-size: clamp(1rem, 2vw, 1.18rem);
      color: rgba(255, 255, 255, 0.72);
      max-width: 600px;
      margin: 0 auto 2.5rem;
      line-height: 1.65;
    }

    .hero-stats {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: 1rem 2.5rem;
      margin-bottom: 2.5rem;
    }

    .hero-stat {
      text-align: center;
    }

    .hero-stat-num {
      display: block;
      font-size: 1.9rem;
      font-weight: 800;
      color: #fff;
      line-height: 1;
    }

    .hero-stat-label {
      font-size: 0.75rem;
      color: rgba(255, 255, 255, 0.55);
      text-transform: uppercase;
      letter-spacing: 0.08em;
      font-weight: 500;
      margin-top: 0.25rem;
      display: block;
    }

    .hero-buttons {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: 0.75rem;
    }

    /* The one CTA that asks for something rather than offering something, and
       it waits until the asking is earned. It sits at the foot of her messages
       — after the deck, where someone has just been reading her words — rather
       than in the hero, where it was competing with the two buttons above it
       for a reader who had not yet been given a reason. */
    .words-cta {
      display: flex;
      justify-content: center;
      margin-top: 2.25rem;
    }

    /* ── BUTTONS ── */
    .btn {
      display: inline-flex;
      align-items: center;
      gap: 0.4rem;
      font-size: 0.9rem;
      font-weight: 600;
      padding: 0.75rem 1.5rem;
      border-radius: 8px;
      text-decoration: none;
      transition: all 0.15s;
      border: none;
      cursor: pointer;
    }

    .btn-primary {
      background: var(--brand);
      color: #fff;
    }

    .btn-primary:hover {
      background: #0E82BC;
      transform: translateY(-1px);
      box-shadow: 0 6px 20px rgba(12, 110, 158, 0.4);
    }

    .btn-ghost {
      background: rgba(255, 255, 255, 0.1);
      color: #fff;
      border: 1px solid rgba(255, 255, 255, 0.25);
    }

    .btn-ghost:hover {
      background: rgba(255, 255, 255, 0.18);
    }

    .btn-white {
      background: #fff;
      color: var(--brand-dark);
      font-weight: 700;
    }

    .btn-white:hover {
      background: #f0f9ff;
      transform: translateY(-1px);
    }

    .btn-outline-white {
      background: transparent;
      color: #fff;
      border: 2px solid rgba(255, 255, 255, 0.5);
    }

    .btn-outline-white:hover {
      border-color: #fff;
      background: rgba(255, 255, 255, 0.08);
    }

    .btn-ink {
      background: var(--ink);
      color: #fff;
    }

    .btn-ink:hover {
      background: var(--ink-800);
      transform: translateY(-1px);
    }

    /* ── SECTIONS ── */
    section {
      padding: 5rem 1.5rem;
    }

    .container {
      max-width: 1200px;
      margin: 0 auto;
    }

    .section-title {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-size: clamp(1.7rem, 3.5vw, 2.5rem);
      font-weight: 800;
      line-height: 1.15;
      color: var(--ink);
      margin-bottom: 1rem;
    }

    .section-sub {
      font-size: 1rem;
      color: var(--muted);
      max-width: 560px;
      line-height: 1.65;
    }

    .section-head {
      margin-bottom: 3rem;
    }

    /* ── SECTION DIVIDER (agorax hairline) ── */
    .section-divider {
      width: 100%;
      height: 1px;
      border: 0;
      /* Pulled up through the section's top padding so the hairline lands on the
         seam between the two sections rather than adrift below it. */
      margin: -5rem auto 3.5rem;
      background: linear-gradient(to right,
          transparent,
          rgba(15, 23, 42, 0.28),
          transparent);
    }

    /* ── TIMELINE ── */
    .timeline-section {
      background: var(--bg);
    }

    .timeline {
      position: relative;
      padding-left: 2rem;
    }

    .timeline::before {
      content: '';
      position: absolute;
      left: 7px;
      top: 12px;
      bottom: 0;
      width: 2px;
      background: linear-gradient(180deg, var(--brand) 0%, var(--border) 100%);
    }

    .timeline-item {
      position: relative;
      padding-bottom: 2.5rem;
    }

    .timeline-item:last-child {
      padding-bottom: 0;
    }

    .timeline-dot {
      position: absolute;
      left: -2rem;
      top: 4px;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: var(--brand);
      border: 3px solid var(--bg);
      box-shadow: 0 0 0 2px var(--brand);
    }

    .timeline-date {
      font-size: 0.75rem;
      font-weight: 600;
      color: var(--brand);
      text-transform: uppercase;
      letter-spacing: 0.06em;
      margin-bottom: 0.35rem;
    }

    .timeline-title {
      font-size: 1.05rem;
      font-weight: 700;
      color: var(--ink);
      margin-bottom: 0.35rem;
      line-height: 1.35;
    }

    .timeline-body {
      font-size: 0.9rem;
      color: var(--muted);
      line-height: 1.6;
      max-width: 680px;
    }

    /* ── BOOK ── */
    .book-section {
      background: var(--bg-subtle);
      color: var(--ink);
    }

    /* Book left, text right — the cover is what the eye lands on first, and it
       reads in the same order as the markup, so nothing needs reordering on
       mobile either. */
    .book-grid {
      display: grid;
      grid-template-columns: auto 1fr;
      gap: 3.5rem;
      align-items: center;
    }

    @media (max-width: 680px) {
      .book-grid {
        grid-template-columns: 1fr;
        gap: 2rem;
      }
    }

    .book-cover {
      width: 180px;
      height: 260px;
      background: linear-gradient(145deg, #08304C 0%, #023E62 100%);
      border-radius: 6px;
      position: relative;
      box-shadow: 6px 6px 24px rgba(0, 0, 0, 0.5), -2px 0 8px rgba(0, 0, 0, 0.3);
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 1.5rem 1rem;
      text-align: center;
      border-left: 6px solid var(--brand);
      flex-shrink: 0;
    }

    .book-content h2 {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-size: clamp(1.5rem, 3vw, 2.2rem);
      font-weight: 800;
      color: var(--ink);
      line-height: 1.2;
      margin-bottom: 1rem;
    }

    .book-content p {
      font-size: 0.95rem;
      color: var(--muted);
      line-height: 1.75;
      margin-bottom: 0.9rem;
    }

    .book-content p:last-of-type {
      margin-bottom: 1.5rem;
    }

    .book-content strong {
      color: var(--ink);
    }

    /* ── QUOTE ── */
    .quote-section {
      background: linear-gradient(135deg, #08304C 0%, #023E62 100%);
      color: #fff;
      text-align: center;
    }

    /* ── BIO ── */
    /* Full-bleed: photo owns the left column, cut away on a diagonal so the
       text column reads as the page continuing past it. */
    .bio-section {
      background: var(--bg);
      padding: 0;
    }

    .bio-grid {
      display: grid;
      grid-template-columns: minmax(280px, 38%) 1fr;
      align-items: stretch;
    }

    .bio-media {
      position: relative;
      overflow: hidden;
      clip-path: polygon(0 0, 100% 0, calc(100% - 5rem) 100%, 0 100%);
    }

    .bio-photo {
      width: 100%;
      height: 100%;
      min-height: 420px;
      object-fit: cover;
      object-position: center 18%;
      display: block;
    }

    /* Left padding must clear the 5rem diagonal, or the photo's top-right corner
       runs straight into the first line of text. */
    .bio-text {
      padding: 3.5rem 3rem 3.5rem 3.25rem;
      max-width: 720px;
      align-self: center;
    }

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

      .bio-media {
        clip-path: polygon(0 0, 100% 0, 100% calc(100% - 3rem), 0 100%);
      }

      .bio-photo {
        min-height: 260px;
        max-height: 340px;
        object-position: center 15%;
      }

      .bio-text {
        padding: 2rem 1.25rem 2.5rem;
      }
    }

    .bio-text h2 {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-size: clamp(1.6rem, 3vw, 2.2rem);
      font-weight: 800;
      margin-bottom: 1.25rem;
      color: var(--ink);
      line-height: 1.2;
    }

    .bio-text p {
      color: var(--muted);
      font-size: 0.95rem;
      line-height: 1.75;
      margin-bottom: 1rem;
    }

    .bio-text p strong {
      color: var(--ink);
      font-weight: 600;
    }

    /* The five strands of her work. Custom markers so the bullets sit on the
       brand palette rather than the browser's default black disc. */
    .bio-list {
      list-style: none;
      margin: -0.25rem 0 1.25rem;
      padding: 0;
    }

    .bio-list li {
      position: relative;
      padding-left: 1.25rem;
      color: var(--muted);
      font-size: 0.95rem;
      line-height: 1.65;
      margin-bottom: 0.4rem;
    }

    .bio-list li::before {
      content: "";
      position: absolute;
      left: 0.15rem;
      top: 0.62em;
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: var(--brand);
    }

    /* ── GLOBAL ── */

    /* ── CTA ── */

    /* ── QUOTES ── */
    /* ── FOOTER ── */
    footer {
      background: var(--ink);
      color: rgba(255, 255, 255, 0.65);
      padding: 3rem 1.5rem 2rem;
    }

    /* One block now that the link column is gone. The max-width is measure,
       not decoration: the sentence would otherwise run the full 1200px. */
    .footer-inner {
      max-width: 1200px;
      margin: 0 auto;
    }

    .footer-brand {
      max-width: 640px;
    }

    .footer-brand-name {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-size: 1.1rem;
      font-weight: 700;
      color: #fff;
      margin-bottom: 0.5rem;
    }

    .footer-brand p {
      font-size: 0.83rem;
      line-height: 1.6;
    }


    /* ── RESPONSIVE ── */
    @media (max-width: 640px) {
      section {
        padding: 3rem 1.25rem;
      }

      /* The hairline is pulled up by exactly the section's top padding so it
         lands on the seam. That padding is smaller here, so the pull has to
         shrink with it — at -5rem it climbed into the video above. */
      .section-divider {
        margin: -3rem auto 2.5rem;
      }

      .hero {
        padding: 3.5rem 1.25rem 3rem;
      }

      /* On a phone the tall crop swallows her face — `cover` keeps the centre and
         throws the sides away, and her face sits off-centre. The photo is
         mirrored, so walking the crop all the way to 100% brings her face fully
         into frame and centres it behind the message. */
      .hero::after {
        background-position: 100% 28%;
      }

      .section-head {
        margin-bottom: 1.75rem;
      }

      /* Hero */
      .hero-stat-num {
        font-size: 1.5rem;
      }

      .hero-stats {
        gap: 0.75rem 1.5rem;
      }

      .hero-sub {
        margin-bottom: 1.75rem;
      }

      .hero-buttons {
        flex-direction: column;
        align-items: center;
      }

      .hero-buttons .btn,
      .words-cta .btn {
        width: 100%;
        max-width: 300px;
        justify-content: center;
      }



    }

    @media (max-width: 480px) {
      .alert-banner {
        font-size: 0.75rem;
        padding: 0.5rem 1rem;
      }

      .nav-logo-main {
        font-size: 0.9rem;
      }
    }

    /* ── GRAIN TEXTURE (fixed overlay) ── */
    body::after {
      content: '';
      position: fixed;
      inset: 0;
      pointer-events: none;
      z-index: 9998;
      opacity: 0.038;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
      background-size: 300px 300px;
    }

    /* ── SCROLL REVEAL ── */
    @media (prefers-reduced-motion: no-preference) {

      /* Fade only — no travel. Timing borrowed from the provex project (0.8s ease-out). */
      [data-reveal] {
        opacity: 0;
        transition: opacity 0.8s ease-out;
      }

      [data-reveal].is-visible {
        opacity: 1;
      }

      [data-reveal][data-delay='1'] {
        transition-delay: 0.10s;
      }

      [data-reveal][data-delay='2'] {
        transition-delay: 0.20s;
      }

      [data-reveal][data-delay='3'] {
        transition-delay: 0.30s;
      }

      [data-reveal][data-delay='4'] {
        transition-delay: 0.40s;
      }
    }

    /* ── HERO: animated orbs ── */
    /* Two soft sky glows in the top-right and bottom-left corners, mostly off
       the canvas, giving the navy somewhere to lift towards.

       They used to drift, and the drift is gone. It could not be seen: a 30px
       travel, on a shape whose edges are already spread by a 90px blur, at 30%
       and 16% alpha, three-quarters of it outside the frame, underneath the ray
       fan. But it was not free — the keyframes scaled as well as translated, and
       scaling a blurred layer makes the browser run the 90px blur again rather
       than move a picture it has already drawn. That was the most expensive
       thing on the hero, paid on every frame, for motion nobody could see. The
       glow stays; only the invisible part went. */
    .hero-orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(90px);
      pointer-events: none;
    }

    .hero-orb-1 {
      width: 700px;
      height: 700px;
      background: radial-gradient(circle, rgba(172, 221, 235, 0.30) 0%, transparent 65%);
      top: -300px;
      right: -200px;
    }

    .hero-orb-2 {
      width: 500px;
      height: 500px;
      background: radial-gradient(circle, rgba(172, 221, 235, 0.16) 0%, transparent 70%);
      bottom: -200px;
      left: -150px;
    }

    /* ── HERO STATS: dividers ── */
    .hero-stats {
      align-items: center;
    }

    .hero-stat-sep {
      width: 1px;
      height: 36px;
      background: rgba(255, 255, 255, 0.14);
    }

    @media (max-width: 580px) {
      .hero-stat-sep {
        display: none;
      }
    }

    /* ── SECTION HEAD: accent line ── */

    /* ── BIO: subtle dot pattern ── */
    .bio-section {
      background-image:
        radial-gradient(rgba(12, 110, 158, 0.07) 1px, transparent 1px);
      background-size: 28px 28px;
      background-color: var(--bg);
    }

    /* ── VIDEO SECTION ── */
    /* The tint fades out before the foot of the section, so it meets the white
       timeline below without a hard seam — the hairline is the only edge. */
    .video-section {
      background: linear-gradient(180deg,
          var(--bg-subtle) 0%,
          var(--bg-subtle) 55%,
          var(--bg) 100%);
    }

    .video-section .section-title {
      color: var(--ink);
    }

    .video-section .section-sub {
      color: var(--muted);
      margin-bottom: 1.5rem;
    }

    /* Her work on the left, the film on the right — the copy that used to open
       the biography now reads alongside the film that covers the same ground. */
    .video-grid {
      display: grid;
      grid-template-columns: 1fr 1.05fr;
      gap: 3.25rem;
      align-items: center;
    }

    .video-copy p {
      color: var(--muted);
      font-size: 0.95rem;
      line-height: 1.75;
      margin-bottom: 1rem;
    }

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

    /* The player is its own column now, so it fills that column rather than
       being centred in a single wide one. */
    .video-grid .vp {
      max-width: none;
      width: 100%;
    }

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

    /* ── PICTURE DECK ──
       A stack of photographs you drag through, the way you would go through a
       pile of prints on a table. Every card is positioned by one number — its
       distance from the front of the deck — so the fan, the tilt, the shrink
       and the fade all come out of the same drag. The photo section this was
       written for is gone; the messages deck in #words is what uses it now. */
    .deck {
      position: relative;
      height: 430px;
      margin-top: 2.25rem;
      display: flex;
      align-items: center;
      justify-content: center;
      /* Horizontal drags belong to the deck; vertical ones still scroll the page. */
      touch-action: pan-y;
      user-select: none;
      -webkit-user-select: none;
    }

    /* One surface takes every pointer event, so a drag never lands on an image
       and turns into the browser's own drag-the-picture gesture. */
    .deck-surface {
      position: absolute;
      inset: 0;
      z-index: 50;
      cursor: grab;
    }

    .deck-surface:active {
      cursor: grabbing;
    }

    .deck-card {
      position: absolute;
      width: 268px;
      height: 372px;
      border-radius: 14px;
      overflow: hidden;
      background: #0B2A3F;
      box-shadow: 0 26px 54px -26px rgba(2, 62, 98, 0.65);
      pointer-events: none;
      will-change: transform, opacity;
    }

    .deck-card img {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    /* The further back a card is, the more it falls into shadow — depth the eye
       reads before it reads the fan. Set from script, per card. */
    .deck-dim {
      position: absolute;
      inset: 0;
      background: #02121F;
      opacity: 0;
    }

    .deck-shade {
      position: absolute;
      inset: 0;
      background: linear-gradient(to top, rgba(2, 18, 31, 0.88) 0%, rgba(2, 18, 31, 0.2) 52%, transparent 78%);
    }

    /* Only the card at the front carries its words; the rest are photographs. */
    .deck-caption {
      position: absolute;
      left: 1.1rem;
      right: 1.1rem;
      bottom: 1.15rem;
      color: #fff;
    }

    .deck-title {
      font-size: 1rem;
      font-weight: 700;
      line-height: 1.3;
    }

    .deck-note {
      margin-top: 0.2rem;
      font-size: 0.79rem;
      line-height: 1.45;
      color: rgba(255, 255, 255, 0.75);
    }

    .deck-nav {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 1rem;
      margin-top: 1.4rem;
    }


    .deck-btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 40px;
      height: 40px;
      border-radius: 999px;
      border: 1px solid rgba(2, 62, 98, 0.18);
      background: #fff;
      color: var(--brand);
      cursor: pointer;
      transition: background 0.15s, border-color 0.15s, color 0.15s;
    }

    .deck-btn:hover {
      background: var(--brand);
      border-color: var(--brand);
      color: #fff;
    }

    .deck-hint {
      font-size: 0.78rem;
      letter-spacing: 0.04em;
      text-transform: uppercase;
      font-weight: 600;
      color: var(--muted);
      /* On a phone it wraps onto two lines between the arrows, and a wrapped
         line has to be centred itself or the row only looks centred. */
      text-align: center;
    }

    @media (max-width: 640px) {
      .deck {
        height: 360px;
      }

      .deck-card {
        width: 214px;
        height: 300px;
      }
    }

    /* ── BOOK SECTION: pink glow accent ── */
    .book-section {
      position: relative;
      overflow: hidden;
    }

    .book-section::before {
      content: '';
      position: absolute;
      inset: 0;
      pointer-events: none;
      background: radial-gradient(ellipse 55% 90% at 95% 50%, rgba(12, 110, 158, 0.07) 0%, transparent 60%);
    }

    /* ── TIMELINE: left-rail colour ── */
    .timeline::before {
      background: linear-gradient(180deg, var(--brand) 0%, rgba(12, 110, 158, 0.14) 100%);
    }


    /* ── HERO: message framing ── */
    /* Shiny animated pill — the quantum-raffle treatment: a translucent capsule
       with a hairline border and a light band that sweeps across on a long rest
       cycle (quick pass, then a pause). Tuned to the hero's sky palette. */
    .hero-eyebrow {
      display: inline-block;
      position: relative;
      overflow: hidden;
      font-size: 0.72rem;
      font-weight: 700;
      letter-spacing: 0.16em;
      text-transform: uppercase;
      color: var(--sky);
      margin-bottom: 1.25rem;
      padding: 0.5rem 1.15rem;
      border-radius: 999px;
      background: rgba(172, 221, 235, 0.08);
      border: 1px solid rgba(172, 221, 235, 0.28);
      box-shadow: 0 1px 12px rgba(2, 32, 51, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.06);
    }

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

    /* The sweeping light. Same shimmer-slow rhythm as the raffle pill: it rests
       off-screen, crosses once, then rests again. */
    .hero-eyebrow::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(100deg,
          transparent 20%,
          rgba(255, 255, 255, 0.35) 50%,
          transparent 80%);
      transform: translateX(-100%);
      animation: pill-shimmer 7s infinite;
    }

    @keyframes pill-shimmer {

      0%,
      35% {
        transform: translateX(-100%);
      }

      55%,
      100% {
        transform: translateX(100%);
      }
    }

    /* A forever-looping sweep is exactly what reduced-motion asks us to stop. */
    @media (prefers-reduced-motion: reduce) {
      .hero-eyebrow::before {
        animation: none;
        display: none;
      }
    }

    .hero-attr {
      font-size: 0.85rem;
      font-weight: 500;
      color: rgba(255, 255, 255, 0.6);
      letter-spacing: 0.04em;
      margin-bottom: 2rem;
    }

    .hero h1 {
      font-style: italic;
    }

    /* ── HER WORDS ── */
    /* The hero above is near-black navy; this section is where the page opens
       back up. Two soft sky glows over a white-to-pale gradient, so the cards
       read as lit from behind rather than sitting on flat grey. */
    /* A nearly white field with the blue only just in it. The cards are warm
       paper and need something to sit on, but the light is the subject here, so
       the ground gives it the least colour it can get away with and still read
       as lit. It lifts to white at the foot, so it meets the bio section below
       without a seam. */
    .words-section {
      position: relative;
      isolation: isolate;
      /* The rays are deliberately larger than the section so their own edges
         never show; this is what keeps them off the hero above it. */
      overflow: hidden;
      background:
        radial-gradient(1200px 620px at 8% -16%, rgba(198, 232, 243, 0.7), transparent 62%),
        radial-gradient(1000px 540px at 96% 6%, rgba(12, 110, 158, 0.1), transparent 64%),
        radial-gradient(900px 620px at 52% 116%, rgba(12, 110, 158, 0.08), transparent 62%),
        /* Nearly white, but not white: the ground keeps just enough blue in it
           for the light to have something to lift. White rays on a white field
           are invisible however bright you make them, so the colour here is the
           smallest amount that still lets a beam read as brighter than the page. */
        linear-gradient(180deg, #E9F3FA 0%, #E4F0F8 40%, #E7F2FA 72%, #FAFDFE 100%);
    }

    /* The tooth of the field itself, so the blue isn't a flat wash of colour.
       Sits under everything — the cards are opaque, so it only shows in the
       gutters. */
    .words-section::after {
      content: '';
      position: absolute;
      inset: 0;
      z-index: 0;
      pointer-events: none;
      opacity: 0.32;
      mix-blend-mode: multiply;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23s)' opacity='0.13'/%3E%3C/svg%3E");
      background-size: 220px 220px;
    }

    /* Light falling across the section from a source above and to the left.

       The rays are a repeating CONIC gradient, not a linear one, because that is
       the whole difference between light and wallpaper: parallel bands of even
       width read as stripes, whereas a fan from a point tapers — tight and bright
       where it starts, spreading and thinning as it travels. The source sits well
       outside the section (the layer below is centred 55% above its top edge), so
       the point the rays converge on is never on screen; what you see is the part
       of the fan that has already spread, which is how light through a gap
       actually falls.

       The angular stops inside each 26deg repeat are deliberately uneven — three
       rays of different widths and strengths — so the eye never finds the period.

       Three things are what make it read as light rather than as pale stripes,
       and all three are on the frame or its two layers:

       1. It ADDS. plus-lighter sums the layer into the blue underneath instead
          of painting white over it. Opaque white on colour is fog; light that
          adds drives the ground up towards white where the beams are strongest
          and leaves it untouched where they are not. This is also why the ray
          alphas below look low — additive light needs far less than it seems.
       2. It BLOOMS. ::before is the hard core of each beam; ::after is the same
          fan blurred four times as far and dimmer. A bright core inside a soft
          halo is what a light source does to a camera and to an eye, and it is
          the single biggest difference between this and a flat band.
       3. It comes from SOMEWHERE. The frame carries a warm hotspot at the top
          left, so the beams visibly leave a place rather than merely existing.

       The light is very slightly warm (255 252 244) against the cold blue of the
       section — sunlight is never the same colour as the sky it crosses. */
    .words-rays {
      position: absolute;
      inset: 0;
      z-index: 0;
      pointer-events: none;
      overflow: hidden;
      mix-blend-mode: plus-lighter;
      /* Where the light enters. */
      background-image: radial-gradient(80% 62% at 16% -14%, rgba(255, 250, 238, 0.24) 0%, rgba(255, 250, 240, 0.08) 38%, transparent 72%);
      /* One side only. The fan is a full circle, so left to itself it lights
         both margins evenly and the section reads as symmetrically decorated
         rather than lit from somewhere. The mask keeps the side the source is
         actually on and falls away to nothing a little past the middle, so the
         light crosses the page and stops — the right-hand margin is bare page.
         The angle leans a few degrees past horizontal so the fade runs slightly
         downhill too, and the quotes at the foot stay clean. */
      -webkit-mask-image: linear-gradient(107deg, #000 0%, rgba(0, 0, 0, 0.82) 20%, rgba(0, 0, 0, 0.38) 42%, rgba(0, 0, 0, 0.1) 60%, transparent 74%);
      mask-image: linear-gradient(107deg, #000 0%, rgba(0, 0, 0, 0.82) 20%, rgba(0, 0, 0, 0.38) 42%, rgba(0, 0, 0, 0.1) 60%, transparent 74%);
    }

    /* Both layers are the same square, centred on the light source and big
       enough that their own edges are always well outside the section. */
    .words-rays::before,
    .words-rays::after {
      content: '';
      position: absolute;
      top: -105%;
      left: 20%;
      width: 380%;
      aspect-ratio: 1;
      translate: -50% -50%;
      transform-origin: 50% 50%;
      /* Two fans, not one, and this is the whole reason the beams stop looking
         machined. The periods are 26deg and 17.5deg — they don't divide into
         each other, so the two patterns fall in and out of step across the
         width of the fan. background-blend-mode: difference then makes that
         relationship visible: where a beam of one lands on a beam of the other
         they cancel towards black and drop out, and where they miss they
         survive. What you get is beams of uneven width and brightness whose
         repeat is too long to find by eye — which is what lets this hold still
         and still not read as a pattern.

         This is lifted from how the Aceternity aurora actually works — it draws
         the same stripes twice at different scales and combines them with
         mix-blend-mode: difference. Two conics at different angular periods is
         the same idea in the geometry that gives rays rather than smears. */
      background-image:
        repeating-conic-gradient(from 0deg at 50% 50%,
          rgba(255, 252, 244, 0) 0deg,
          rgba(255, 252, 244, 0) 3.1deg,
          rgba(255, 252, 244, 0.5) 3.8deg,
          rgba(255, 252, 244, 0.5) 4.5deg,
          rgba(255, 252, 244, 0) 5.2deg,
          rgba(255, 252, 244, 0) 11.1deg,
          rgba(255, 252, 244, 0.19) 11.7deg,
          rgba(255, 252, 244, 0.19) 12.2deg,
          rgba(255, 252, 244, 0) 12.8deg,
          rgba(255, 252, 244, 0) 19.4deg,
          rgba(255, 252, 244, 0.32) 20deg,
          rgba(255, 252, 244, 0.32) 20.6deg,
          rgba(255, 252, 244, 0) 21.2deg,
          rgba(255, 252, 244, 0) 26deg),
        repeating-conic-gradient(from 9deg at 50% 50%,
          rgba(255, 252, 244, 0) 0deg,
          rgba(255, 252, 244, 0) 5.3deg,
          rgba(255, 252, 244, 0.27) 6.6deg,
          rgba(255, 252, 244, 0.27) 8.2deg,
          rgba(255, 252, 244, 0) 9.5deg,
          rgba(255, 252, 244, 0) 17.5deg);
      background-blend-mode: difference;
      /* Concentric with the source, so the beams dim with distance travelled. */
      -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 12%, rgba(0, 0, 0, 0.62) 44%, transparent 80%);
      mask-image: radial-gradient(circle at 50% 50%, #000 12%, rgba(0, 0, 0, 0.62) 44%, transparent 80%);
    }

    /* The core: sharp enough that you can count the beams. */
    .words-rays::before {
      filter: blur(7px);
    }

    /* The bloom around it — a bright core inside a soft halo is what a light
       source does to a camera and to an eye.

       This light used to move: the fan leaned slowly one way and back while the
       two conics wound past each other, so beams brightened and faded in place.
       It is held still now, and it is better for it. The section it crosses is
       where her words are read, and light that keeps shifting under a paragraph
       asks to be watched. Standing still, it reads as a room with a window in
       it. That it also happens to be the cheapest thing on the page to draw —
       drawn once, then never again — is a straight gift. */
    .words-rays::after {
      filter: blur(30px);
      opacity: 0.75;
    }

    /* The cards and heading ride above the light. */
    .words-section>.container {
      position: relative;
      z-index: 1;
    }

    /* Light nobody is looking at costs exactly as much as light somebody is.
       The hero's fan is the only one that still moves, and left alone it would
       keep turning while the reader is four sections further down. A script
       parks it the moment the hero leaves the viewport and starts it again just
       before it comes back. The class is only ever added by that script, so if
       it never runs the light simply behaves as it did before. */
    .rays-still::before,
    .rays-still::after {
      animation-play-state: paused !important;
    }

    /* A hairline of sky along the top edge, where the navy hero ends. */
    .words-section::before {
      content: '';
      position: absolute;
      inset: 0 0 auto;
      height: 1px;
      background: linear-gradient(90deg, transparent, rgba(172, 221, 235, 0.9), transparent);
      pointer-events: none;
      z-index: 2;
    }

    /* ── HER WORDS: the deck ──
       Nine photographs of this community, one per message. The pairing is the
       point: the picture is this community doing the thing the message is
       about, so a card is an argument before it is a link.

       The same fan as the photographs at the foot of the page, and deliberately
       the same — one gesture on this page, used twice: once for her words, once
       for the people who keep them. The rail that stood here asked the reader to
       take in nine cards at once and gave each of them a corner of a screen; the
       fan asks for one card at a time and gives it the whole of the reader.

       Everything structural is inherited from .deck / .deck-card above. What
       follows is only what a card of words needs and a card of photographs
       doesn't. */
    /* Centred over the fan, which is centred itself — the same as the deck of
       photographs at the foot of the page. */
    .words-section .section-head {
      text-align: center;
    }

    .words-section .section-sub {
      margin-left: auto;
      margin-right: auto;
    }

    .words-deck {
      height: 468px;
      margin-top: 2.25rem;
    }

    /* A little larger than the photographs below, because this caption carries a
       quote where theirs carries a line. */
    .words-deck .deck-card {
      width: 286px;
      height: 402px;
      color: #fff;
      text-decoration: none;
    }

    /* Her words lie on the photograph, so the foot of the frame has to hold them
       against whatever that photograph happens to be doing. The shade starts
       higher and lands darker than the one on the deck below. */
    .words-deck .deck-shade {
      background: linear-gradient(to top,
          rgba(2, 18, 31, 0.95) 0%,
          rgba(2, 18, 31, 0.8) 36%,
          rgba(2, 18, 31, 0.34) 64%,
          transparent 88%);
    }

    .words-deck .deck-caption {
      left: 1.25rem;
      right: 1.25rem;
      bottom: 1.3rem;
      display: flex;
      flex-direction: column;
      gap: 0.55rem;
    }

    /* One line above the quote, naming what the message is about. Dates live on
       the message pages themselves; on a card they only competed with the
       words. */
    .speech-topic {
      display: flex;
      align-items: center;
      gap: 0.6rem;
      font-size: 0.7rem;
      font-weight: 700;
      letter-spacing: 0.11em;
      text-transform: uppercase;
      color: rgba(255, 255, 255, 0.86);
    }

    .speech-topic::before {
      content: "";
      flex: none;
      width: 18px;
      height: 1px;
      background: var(--sky);
    }

    .speech-quote {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-style: italic;
      font-size: 1.06rem;
      line-height: 1.5;
      color: #fff;
    }

    /* This line is the control. The card carries the href for semantics and for
       the keyboard, but a press only opens the message if it lands here — so the
       line has to read as the target rather than as a caption, and it has to
       answer the pointer. It cannot do that with :hover: the drag surface covers
       every card and swallows the pointer before it reaches anything, so the
       hover state is put on and taken off by the deck script instead. */
    .speech-cta {
      display: inline-flex;
      align-items: center;
      gap: 0.4rem;
      margin-top: 0.1rem;
      font-size: 0.76rem;
      font-weight: 700;
      letter-spacing: 0.02em;
      color: var(--sky);
      text-decoration: underline;
      text-decoration-color: transparent;
      text-decoration-thickness: 1px;
      text-underline-offset: 4px;
      transition: color 0.15s, text-decoration-color 0.15s;
    }

    .speech-cta.is-hot {
      color: #fff;
      text-decoration-color: rgba(255, 255, 255, 0.55);
    }

    /* The surface swallows every pointer event, so the card underneath can never
       take focus in its own right — which makes the surface the control, and a
       control that can be tabbed to has to show it. */
    .deck-surface:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 6px;
      border-radius: 18px;
    }

    .words-nav {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 1rem;
      margin-top: 1.4rem;
    }

    /* A phone has the height to spare and none of the width, and the quote needs
       the room more than the fan needs to be small — so the card gives up less
       here than the photographs below do. */
    @media (max-width: 640px) {
      .words-deck {
        height: 424px;
      }

      .words-deck .deck-card {
        width: 246px;
        height: 350px;
      }

      .words-deck .deck-caption {
        left: 1rem;
        right: 1rem;
        bottom: 1.05rem;
      }

      .speech-quote {
        font-size: 0.95rem;
      }

      /* Narrow enough that the arrows and the hint were fighting for the width. */
      .words-nav {
        gap: 0.75rem;
      }
    }

    /* ── TIMELINE: upcoming marker ── */
    .timeline-item-next .timeline-dot {
      background: var(--bg);
      border: 3px solid var(--bg);
      box-shadow: 0 0 0 2px var(--brand);
    }

    .timeline-item-next .timeline-date {
      color: var(--ink);
    }

    .timeline-body a {
      color: var(--brand);
      font-weight: 600;
      text-decoration: none;
    }

    .timeline-body a:hover {
      text-decoration: underline;
    }

    /* ── CONDITION ── */
    .condition-section {
      background: linear-gradient(160deg, var(--ink) 0%, #08304C 100%);
      color: #fff;
    }

    .condition-wrap {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 4rem;
      align-items: start;
    }

    @media (max-width: 820px) {
      .condition-wrap {
        grid-template-columns: 1fr;
        gap: 2.5rem;
      }
    }

    /* The cards column runs much longer than the invitation beside it, so the
       invitation holds its place while they scroll past — the left half stays
       readable for the whole section instead of scrolling away at the top.
       align-items:start above keeps the intro at its content height, which is
       what leaves it room to travel inside its (full-height) grid area.
       Offset clears the sticky 64px nav. Only where there are two columns. */
    @media (min-width: 821px) {
      .condition-intro {
        position: sticky;
        top: calc(64px + 1.5rem);
      }
    }

    /* The invitation is ~470px tall; below this the sign-off would be pinned
       off the bottom of the screen, so it scrolls normally instead. */
    @media (min-width: 821px) and (max-height: 560px) {
      .condition-intro {
        position: static;
      }
    }

    .condition-intro h2 {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-size: clamp(1.7rem, 3.5vw, 2.4rem);
      font-weight: 800;
      line-height: 1.15;
      margin-bottom: 1rem;
    }

    .condition-intro p {
      font-size: 0.95rem;
      color: rgba(255, 255, 255, 0.72);
      line-height: 1.75;
      margin-bottom: 0.9rem;
    }

    /* The invitation runs longer than the old one-line sub, so it gets a wider
       measure — 34ch would have broken it into a ribbon. */
    .condition-invite {
      margin-top: 1.4rem;
      max-width: 46ch;
    }

    .condition-intro .condition-invite p {
      font-size: 0.95rem;
      line-height: 1.75;
      color: rgba(255, 255, 255, 0.72);
      margin-bottom: 0.9rem;
    }

    .condition-intro .condition-invite p:last-child {
      margin-bottom: 0;
    }

    /* The body sits at 72% white, so bold alone would barely register — the
       names and the message title come up to full white to carry the emphasis. */
    .condition-intro .condition-invite strong {
      font-weight: 700;
      color: #fff;
    }

    /* Who is inviting you, said first — it leads the message rather than
       signing off after it, so it takes the same weight as the names below. */
    .condition-intro .condition-invite-from {
      font-weight: 700;
      color: #fff;
    }

    .condition-cards {
      display: flex;
      flex-direction: column;
      gap: 1rem;
    }

    .condition-card {
      position: relative;
      background: rgba(255, 255, 255, 0.05);
      border: 1px solid rgba(255, 255, 255, 0.12);
      border-radius: var(--radius);
      padding: 1.4rem 1.5rem;
      transition: background 0.15s, border-color 0.15s, transform 0.15s, box-shadow 0.15s;
    }

    /* A card that carries a link is the link. With the text button gone, the
       anchor is nothing but the hit area stretched over the whole card; its
       label stays in the markup for screen readers and for the status bar. */
    .condition-card:has(.condition-link) {
      cursor: pointer;
    }

    /* Nothing on the card says "button", so the hover has to say it: the
       surface comes up, the edge takes the sky the tag and the date already
       use, and the whole card lifts a little off the section. Only cards that
       carry a link react — the card still waiting on a registration URL must
       not promise a click it can't honour. Focus-within gets the same look so
       tabbing to a card reads the same as pointing at it. */
    .condition-card:has(.condition-link):hover,
    .condition-card:has(.condition-link):focus-within {
      background: rgba(255, 255, 255, 0.09);
      border-color: rgba(172, 221, 235, 0.45);
      transform: translateY(-2px);
      box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22);
    }

    /* The lift is the part that moves; the colour change alone carries the
       affordance for anyone who's asked motion to stop. */
    @media (prefers-reduced-motion: reduce) {

      .condition-card:has(.condition-link):hover,
      .condition-card:has(.condition-link):focus-within {
        transform: none;
      }
    }

    /* inline-block so the pill hugs its label instead of stretching the card. */
    .condition-tag {
      display: inline-block;
      font-size: 0.68rem;
      font-weight: 700;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--sky);
      background: rgba(172, 221, 235, 0.1);
      border: 1px solid rgba(172, 221, 235, 0.28);
      border-radius: 999px;
      padding: 0.28rem 0.75rem;
      margin-bottom: 0.7rem;
    }

    .condition-name {
      font-size: 1.05rem;
      font-weight: 700;
      color: #fff;
      margin-bottom: 0.4rem;
    }

    .condition-body {
      font-size: 0.88rem;
      color: rgba(255, 255, 255, 0.65);
      line-height: 1.6;
    }

    .condition-tbc {
      margin-top: 0.75rem;
      display: inline-block;
      font-size: 0.72rem;
      font-weight: 600;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      color: rgba(255, 255, 255, 0.58);
      border: 1px dashed rgba(255, 255, 255, 0.25);
      border-radius: 5px;
      padding: 0.2rem 0.55rem;
    }

    .condition-meta {
      margin-top: 1rem;
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.75rem 1.5rem;
    }

    .condition-dates {
      display: inline-block;
      font-size: 0.72rem;
      font-weight: 700;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      color: var(--sky);
      border: 1px solid rgba(172, 221, 235, 0.35);
      background: rgba(172, 221, 235, 0.1);
      border-radius: 5px;
      padding: 0.2rem 0.55rem;
    }

    /* Covers the card, shows nothing: the label is clipped to a pixel rather
       than display:none so assistive tech still announces where the card goes.
       Focus is the one time it has to be visible — hence the ring on the card. */
    .condition-link {
      position: absolute;
      inset: 0;
      z-index: 1;
      border-radius: var(--radius);
      font-size: 0;
      color: transparent;
      text-decoration: none;
      overflow: hidden;
    }

    .condition-link:focus-visible {
      outline: 2px solid var(--sky);
      outline-offset: 3px;
    }

    /* ── MODAL ── */
    .modal {
      position: fixed;
      inset: 0;
      z-index: 200;
      display: none;
    }

    /* The frame is the gutter. Sizing the panel in vh was the bug on a phone:
       vh is measured against the viewport with the browser's chrome hidden, so
       a panel at 86vh plus a 7vh margin is taller than what you can actually
       see while the address bar is showing, and the top of it — the title, the
       close button — is pushed off the screen. Making the open modal a padded
       flex box and capping the panel at 100% of it removes the arithmetic
       entirely: the panel can never be taller than the box it is centred in,
       whatever the chrome is doing, and the padding is a gutter that is always
       there. The safe-area insets keep it clear of the notch and the home bar. */
    .modal.is-open {
      display: flex;
      align-items: center;
      justify-content: center;
      padding:
        max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right))
        max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left));
    }

    .modal-backdrop {
      position: absolute;
      inset: 0;
      background: rgba(6, 32, 51, 0.72);
      -webkit-backdrop-filter: blur(3px);
      backdrop-filter: blur(3px);
    }

    .modal-panel {
      position: relative;
      z-index: 1;
      width: min(660px, 100%);
      /* 100% of the padded frame, so the gutter above survives any content. */
      max-height: min(100%, 900px);
      margin: 0;
      background: var(--bg);
      color: var(--ink);
      border-radius: 14px;
      overflow: hidden;
      box-shadow: 0 30px 80px rgba(6, 32, 51, 0.45);
      display: flex;
      flex-direction: column;
    }

    .modal-head {
      flex-shrink: 0;
      padding: 1.4rem 3.25rem 1.1rem 1.75rem;
      border-bottom: 1px solid var(--border);
      background: var(--bg);
    }

    .modal-eyebrow {
      font-size: 0.95rem;
      font-weight: 700;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      color: var(--brand);
      margin-bottom: 0.1rem;
    }

    .modal-where {
      font-size: 0.72rem;
      font-weight: 700;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
      margin-bottom: 0.5rem;
    }

    .modal-title {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-size: 1.35rem;
      font-weight: 800;
      line-height: 1.25;
    }

    .modal-close {
      position: absolute;
      top: 0.9rem;
      right: 0.9rem;
      z-index: 2;
      width: 36px;
      height: 36px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border: 1px solid var(--border);
      background: var(--bg);
      color: var(--muted);
      border-radius: 999px;
      cursor: pointer;
      transition: all 0.15s;
    }

    .modal-close:hover {
      background: var(--bg-subtle);
      color: var(--ink);
    }

    .modal-body {
      overflow-y: auto;
      -webkit-overflow-scrolling: touch;
      padding: 1.5rem 1.75rem 1.75rem;
    }

    .modal-body p {
      font-size: 0.92rem;
      line-height: 1.75;
      color: var(--ink-700);
      margin-bottom: 0.9rem;
    }

    .modal-body p:last-child {
      margin-bottom: 0;
    }

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

    .modal-body em {
      font-style: italic;
    }

    .modal-list {
      list-style: none;
      margin: 0 0 0.9rem;
      padding: 0;
    }

    .modal-list li {
      position: relative;
      font-size: 0.92rem;
      line-height: 1.7;
      color: var(--ink-700);
      padding-left: 1.5rem;
      margin-bottom: 0.85rem;
    }

    .modal-list li:last-child {
      margin-bottom: 0;
    }

    .modal-list li::before {
      content: '';
      position: absolute;
      left: 0.4rem;
      top: 0.65rem;
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: var(--brand);
    }

    .modal-list li strong {
      display: block;
      font-size: 0.95rem;
      margin-bottom: 0.1rem;
    }

    .modal-list li span {
      display: block;
      color: var(--muted);
      font-size: 0.88rem;
      line-height: 1.65;
    }

    .modal-lead {
      font-weight: 600;
      color: var(--ink) !important;
    }

    .modal-points {
      list-style: none;
      counter-reset: pp;
      margin: 0.6rem 0 0;
      padding: 0;
    }

    .modal-points li {
      position: relative;
      counter-increment: pp;
      padding-left: 1.75rem;
      margin-bottom: 0.5rem;
      font-size: 0.88rem;
      line-height: 1.6;
      color: var(--ink-700);
    }

    .modal-points li:last-child {
      margin-bottom: 0;
    }

    .modal-points li::before {
      content: counter(pp);
      position: absolute;
      left: 0;
      top: 0.1rem;
      width: 1.15rem;
      height: 1.15rem;
      border-radius: 50%;
      background: var(--brand-light);
      color: var(--brand);
      font-size: 0.68rem;
      font-weight: 700;
      line-height: 1.15rem;
      text-align: center;
    }

    .modal-files {
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
      margin-bottom: 1.1rem;
    }

    .modal-file {
      display: flex;
      align-items: flex-start;
      gap: 0.75rem;
      padding: 0.85rem 1rem;
      border: 1px solid var(--border);
      border-radius: 8px;
      background: var(--bg-subtle);
      color: var(--brand);
      text-decoration: none;
      transition: border-color 0.15s, background 0.15s;
    }

    .modal-file:hover {
      border-color: var(--brand);
      background: var(--brand-light);
    }

    .modal-file span {
      display: block;
      font-size: 0.78rem;
      color: var(--muted);
      line-height: 1.5;
    }

    .modal-file strong {
      display: block;
      font-size: 0.88rem;
      font-weight: 600;
      color: var(--ink);
      margin-bottom: 0.1rem;
      line-height: 1.4;
    }

    .modal-foot {
      flex-shrink: 0;
      padding: 1.1rem 1.75rem;
      border-top: 1px solid var(--border);
      background: var(--bg-subtle);
    }

    .modal-foot .btn {
      width: 100%;
      justify-content: center;
    }

    .modal-foot-note {
      display: block;
      margin-top: 0.7rem;
      font-size: 0.78rem;
      color: var(--muted);
      text-align: center;
    }

    @media (max-width: 640px) {
      /* It used to be a sheet pinned to the bottom edge, full-bleed and square
         across the foot. Two things were wrong with that here: it had no gutter
         at all, so it read as the page having broken rather than as a card
         sitting above it; and a form pinned to the bottom is the worst place to
         put one on a phone, because the keyboard opens over exactly that edge.
         It is now the same centred, guttered card as everywhere else, just with
         the paddings pulled in. */
      .modal-head {
        padding: 1.15rem 3rem 1rem 1.25rem;
      }

      .modal-title {
        font-size: 1.15rem;
      }

      .modal-body {
        padding: 1.25rem 1.25rem 1.5rem;
      }

      .modal-foot {
        padding: 1rem 1.25rem calc(1rem + env(safe-area-inset-bottom));
      }

      .modal-body p,
      .modal-list li {
        font-size: 0.9rem;
      }
    }

    @media (prefers-reduced-motion: no-preference) {
      .modal.is-open .modal-panel {
        animation: modalRise 0.28s cubic-bezier(0.2, 0.7, 0.3, 1);
      }

      .modal.is-open .modal-backdrop {
        animation: modalFade 0.22s ease;
      }
    }

    @keyframes modalFade {
      from {
        opacity: 0;
      }

      to {
        opacity: 1;
      }
    }

    @keyframes modalRise {
      from {
        opacity: 0;
        transform: translateY(16px);
      }

      to {
        opacity: 1;
        transform: none;
      }
    }

    /* overflow:hidden on the body is the usual way to hold the page still and
       it does not work on iOS — Safari scrolls the document behind the dialog
       anyway, which is the reason the page moved under the open modal. Taking
       the body out of flow is the thing that actually stops it. The cost is
       that a fixed body forgets where it was scrolled to, so the script stores
       the position, offsets the body by it to keep the view from jumping, and
       puts it back on close. */
    body.modal-open {
      position: fixed;
      left: 0;
      right: 0;
      width: 100%;
      overflow: hidden;
      overscroll-behavior: none;
    }

    /* ── REGISTRATION FORM (inside the modal) ── */
    .reg-form {
      margin-top: 1.25rem;
    }

    .reg-field {
      margin-bottom: 0.9rem;
    }

    .reg-label {
      display: block;
      font-size: 0.78rem;
      font-weight: 700;
      letter-spacing: 0.04em;
      text-transform: uppercase;
      color: var(--muted);
      margin-bottom: 0.35rem;
    }

    .reg-input {
      width: 100%;
      font: inherit;
      font-size: 0.95rem;
      color: var(--ink);
      background: var(--bg);
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 0.7rem 0.85rem;
      transition: border-color 0.15s, box-shadow 0.15s;
    }

    .reg-input:focus {
      outline: none;
      border-color: var(--brand);
      box-shadow: 0 0 0 3px rgba(12, 110, 158, 0.15);
    }

    /* Only after a failed submit — :invalid alone would redden an empty field
       the moment the modal opens, before anyone has typed a character. */
    .reg-form.is-checked .reg-input:invalid {
      border-color: var(--red);
    }

    /* Off-screen rather than display:none, so bots that skip hidden fields
       still fill it. Real people never see or tab into it. */
    .reg-hp {
      position: absolute;
      left: -9999px;
      width: 1px;
      height: 1px;
      overflow: hidden;
    }

    .reg-submit {
      width: 100%;
      justify-content: center;
      margin-top: 0.3rem;
    }

    .reg-submit[disabled] {
      opacity: 0.65;
      cursor: default;
      transform: none;
      box-shadow: none;
    }

    .reg-status {
      font-size: 0.85rem;
      line-height: 1.5;
      color: var(--red);
      background: var(--red-light);
      border-radius: 8px;
      padding: 0.6rem 0.75rem;
      margin-bottom: 0.8rem;
    }

    .reg-note {
      margin-top: 0.8rem;
      font-size: 0.78rem;
      line-height: 1.6;
      color: var(--muted);
    }

    .reg-done {
      margin-top: 1.25rem;
      border: 1px solid rgba(12, 110, 158, 0.25);
      background: var(--brand-light);
      border-radius: var(--radius);
      padding: 1.1rem 1.25rem;
    }

    .reg-done strong {
      display: block;
      font-size: 1rem;
      color: var(--ink);
      margin-bottom: 0.3rem;
    }

    .reg-done span {
      font-size: 0.88rem;
      line-height: 1.65;
      color: var(--ink-700);
    }

    /* ── SHARE ── */
    .share-section {
      background: var(--bg);
    }

    .share-section .section-head {
      text-align: center;
    }

    .share-section .section-sub {
      margin: 0 auto;
    }

    .share-row {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: 0.6rem;
    }

    .share-btn {
      display: inline-flex;
      align-items: center;
      gap: 0.45rem;
      font-family: inherit;
      font-size: 0.88rem;
      font-weight: 600;
      padding: 0.7rem 1.3rem;
      border-radius: 999px;
      cursor: pointer;
      background: var(--bg-subtle);
      color: var(--brand-dark);
      border: 1.5px solid var(--border);
      text-decoration: none;
      transition: background 0.15s, color 0.15s, border-color 0.15s;
    }

    .share-btn:hover {
      background: var(--navy);
      color: #fff;
      border-color: var(--navy);
    }



    /* ── TIMELINE: centred ── */
    .timeline-section .section-head {
      text-align: center;
    }

    .timeline-section .section-sub {
      margin-left: auto;
      margin-right: auto;
    }

    .timeline {
      max-width: 620px;
      margin: 0 auto;
    }


    /* ── QUOTE BANDS (scattered through the page) ── */
    .qband {
      position: relative;
      overflow: hidden;
      min-height: 360px;
      padding: 4.5rem 1.5rem;
      display: flex;
      align-items: center;
      justify-content: center;
      background-size: cover;
      background-position: center 28%;
      text-align: center;
    }

    /* Vignette, not a flat wash: the centre opens up so the photo reads, the edges
       close to navy so the band fades into the sections above and below. The top
       layer is the hero's own sky bloom — same light source across the page. */
    .qband::before {
      content: '';
      position: absolute;
      inset: 0;
      background:
        radial-gradient(ellipse 75% 65% at 50% 0%, rgba(172, 221, 235, 0.16) 0%, transparent 70%),
        radial-gradient(ellipse 85% 100% at 50% 45%,
          rgba(2, 32, 51, 0.80) 0%,
          rgba(2, 32, 51, 0.90) 55%,
          rgba(2, 32, 51, 0.97) 100%);
    }

    .qband-inner {
      position: relative;
      z-index: 2;
      max-width: 860px;
      margin: 0 auto;
    }

    .qband blockquote {
      font-family: 'europa', system-ui, -apple-system, sans-serif;
      font-style: italic;
      font-weight: 700;
      font-size: clamp(1.7rem, 3.8vw, 2.7rem);
      line-height: 1.4;
      color: #fff;
      text-shadow: 0 2px 24px rgba(2, 32, 51, 0.7);
    }

    .qband figcaption {
      margin-top: 1.35rem;
      font-size: 0.72rem;
      font-weight: 600;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: var(--sky);
    }

    @media (max-width: 640px) {
      .qband {
        min-height: 280px;
        padding: 3rem 1.25rem;
      }
    }


    /* ── BOOK: 3D mockup ── */
    /* Sits on the bottom edge of the grid and hangs 8rem past it — 3rem more than
       the section's own padding, so .book-section's overflow:hidden crops it and
       the cover bleeds off the bottom of the section. */
    .book-mock {
      perspective: 1400px;
      flex-shrink: 0;
      display: block;
      text-decoration: none;
      align-self: end;
      margin-bottom: -8rem;
    }

    .book-3d {
      position: relative;
      display: block;
      width: 420px;
      transform: rotateY(24deg) rotateX(4deg);
      border-radius: 2px 7px 7px 2px;
      overflow: hidden;
      box-shadow: -26px 44px 90px rgba(0, 0, 0, 0.55), 0 2px 10px rgba(0, 0, 0, 0.45);
    }

    .book-3d img {
      display: block;
      width: 100%;
      height: auto;
    }

    /* Spine shadow + page edge. A front cover has its spine on the left whichever
       way the mockup is turned — the tilt brings that edge toward the reader now
       instead of away, but it is still the spine. */
    .book-3d::after {
      content: '';
      position: absolute;
      left: 0;
      top: 0;
      bottom: 0;
      width: 22px;
      background: linear-gradient(90deg, rgba(0, 0, 0, 0.5) 0%, rgba(255, 255, 255, 0.14) 55%, rgba(0, 0, 0, 0.10) 100%);
      pointer-events: none;
    }

    @media (max-width: 680px) {
      .book-mock {
        display: flex;
        justify-content: center;
        align-self: auto;
        margin-bottom: 0;
      }

      .book-3d {
        width: 250px;
        transform: rotateY(16deg) rotateX(3deg);
      }
    }


    /* ── VIDEO PLAYER (self-hosted; native <video>, custom controls) ── */
    /* Keep the whole player on screen: cap its width off the viewport height
       (allowing for nav + heading) so a 16:9 box can never be taller than the
       space available. On tall screens the 860px grid width still wins. */
    .vp {
      position: relative;
      aspect-ratio: 16 / 9;
      background: #000;
      border-radius: 12px;
      overflow: hidden;
      max-width: calc((100vh - 15rem) * 16 / 9);
      margin-inline: auto;
      box-shadow: 0 24px 64px rgba(2, 32, 51, 0.28), 0 0 0 1px rgba(2, 62, 98, 0.08);
    }

    @supports (height: 100dvh) {
      .vp {
        max-width: calc((100dvh - 15rem) * 16 / 9);
      }
    }

    .vp video {
      width: 100%;
      height: 100%;
      display: block;
      object-fit: contain;
      background: #000;
      cursor: pointer;
    }

    .vp-big {
      position: absolute;
      inset: 0;
      margin: auto;
      width: 76px;
      height: 76px;
      border: none;
      border-radius: 50%;
      cursor: pointer;
      background: rgba(255, 255, 255, 0.94);
      color: var(--navy);
      display: flex;
      align-items: center;
      justify-content: center;
      padding-left: 4px;
      transition: transform 0.2s ease, background 0.2s ease;
      box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
    }

    .vp:hover .vp-big,
    .vp-big:hover {
      background: #fff;
      transform: scale(1.06);
    }

    .vp:hover .vp-big:active {
      transform: scale(0.98);
    }

    .vp.is-started .vp-big {
      display: none;
    }

    .vp-bar {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      display: none;
      align-items: center;
      gap: 0.4rem;
      /* The buttons carry their own 44px height now, so the bar gives back most
         of its vertical padding and finishes about the same height as before. */
      padding: 0.1rem 0.5rem 0.3rem;
      background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.75));
      opacity: 1;
      transition: opacity 0.25s ease;
    }

    .vp.is-started .vp-bar {
      display: flex;
    }

    .vp.is-playing .vp-bar {
      opacity: 0;
    }

    .vp:hover .vp-bar {
      opacity: 1;
    }

    /* The glyphs stay 17px; the padding is what makes them thumb-sized. */
    .vp-btn {
      background: none;
      border: none;
      color: #fff;
      cursor: pointer;
      padding: 0;
      width: 44px;
      height: 44px;
      display: flex;
      align-items: center;
      justify-content: center;
      opacity: 0.9;
    }

    .vp-btn:hover {
      opacity: 1;
    }

    /* The visible track is 4px, but the whole 22px row is clickable/draggable. */
    .vp-seek {
      flex: 1;
      height: 22px;
      display: flex;
      align-items: center;
      cursor: pointer;
      position: relative;
      touch-action: none;
    }

    .vp-seek::before {
      content: '';
      position: absolute;
      left: 0;
      right: 0;
      top: 50%;
      transform: translateY(-50%);
      height: 4px;
      background: rgba(255, 255, 255, 0.3);
      border-radius: 3px;
      transition: height 0.12s;
    }

    .vp-seek-fill {
      position: relative;
      z-index: 1;
      height: 4px;
      width: 0;
      background: var(--sky);
      border-radius: 3px;
      transition: height 0.12s;
    }

    .vp-seek:hover::before,
    .vp-seek:focus-visible::before,
    .vp-seek.is-scrubbing::before,
    .vp-seek:hover .vp-seek-fill,
    .vp-seek:focus-visible .vp-seek-fill,
    .vp-seek.is-scrubbing .vp-seek-fill {
      height: 7px;
    }

    .vp-seek::after {
      content: '';
      position: absolute;
      top: 50%;
      left: 0;
      width: 13px;
      height: 13px;
      margin-left: -6.5px;
      transform: translateY(-50%) scale(0);
      transform-origin: center;
      border-radius: 50%;
      background: var(--sky);
      transition: transform 0.12s;
      z-index: 2;
      left: var(--seek-pos, 0%);
    }

    .vp-seek:hover::after,
    .vp-seek.is-scrubbing::after {
      transform: translateY(-50%) scale(1);
    }

    .vp-time {
      font-size: 0.72rem;
      font-weight: 600;
      color: #fff;
      font-variant-numeric: tabular-nums;
      white-space: nowrap;
    }

    @media (max-width: 560px) {
      .vp-time {
        display: none;
      }
    }

    /* ── TOUCH TARGETS ──
       Scoped to coarse pointers, so nothing here changes what a mouse user sees.
       Anything a thumb is meant to hit gets 44px of height. */
    @media (pointer: coarse) {

      /* The 36px logo mark sets the link's height; the bar has 64px to spare. */
      .nav-logo {
        min-height: 44px;
      }

      /* The drawer is a stack of small type — the text stays its size, the row
         grows around it. */
      .nav-links a {
        min-height: 44px;
        display: flex;
        align-items: center;
      }

    }

/* Inter (fallback face for calluna-sans) */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuOKfMZg.ttf) format('truetype');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf) format('truetype');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf) format('truetype');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf) format('truetype');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf) format('truetype');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZg.ttf) format('truetype');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuBWYMZg.ttf) format('truetype');
}
