/* =========================================================
   Scroll-driven motion.

   Every effect here is driven by a scroll() or view() timeline.
   Nothing polls, nothing observes, and none of it runs on the main
   thread. The site does ship one script, the assistant, but it is
   inert until somebody opens it: none of the page's motion depends
   on script running, so a reader who blocks it gets the same page
   moving the same way.

   The whole file sits behind @supports. Where scroll timelines are
   not available the page renders exactly as it does otherwise — no
   element is hidden behind an animation that cannot run, which is
   the failure mode of the usual "add .is-visible with JS" approach.

   The illustrations have their own arrival animation, in styles.css
   next to .skull, because it is theirs rather than the page's: they
   rise and rotate slightly, the way a drawing is laid on a desk.

   Supported in Chromium 115+, Safari 26+, Firefox 144+.
   Everything older gets the static site.
   ========================================================= */

@supports (animation-timeline: view()) {
  /* ---------- Reveal ----------
     One keyframe pair does most of the work. Elements animate on
     their own view timeline, so a long page reveals continuously as
     it is read rather than firing everything at one trigger point. */

  @keyframes rise {
    from {
      opacity: 0;
      transform: translateY(26px);
    }

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

  .section__head,
  .prose p,
  .pullquote,
  .stage,
  .card,
  .person,
  .detail,
  .tier,
  .enquiry,
  .facts li,
  .page-head__lead,
  .contact-address,
  .site-footer__brand,
  .site-footer__group {
    animation: rise linear both;
    animation-timeline: view();
    /* Begins just after the element edges into view and completes
       well before the middle of the screen, so nothing is still
       moving by the time it is in a comfortable reading position. */
    animation-range: entry 5% cover 26%;
  }

  /* Stagger. Cells in the same grid row cross into view together, so
     a shared range would land them simultaneously. Shifting the range
     per column turns that into a cascade running with the reading
     direction. The ladder covers three columns, which is the widest
     the card grid ever gets. */
  .card-grid .card:nth-child(3n + 2),
  .stages .stage:nth-child(2n),
  .people .person:nth-child(2n),
  .facts li:nth-child(2) {
    animation-range: entry 5% cover 34%;
  }

  .card-grid .card:nth-child(3n),
  .facts li:nth-child(3) {
    animation-range: entry 5% cover 42%;
  }

  /* ---------- Reading focus ----------
     Body copy arrives dimmed and sharpens to full strength as it reaches the
     middle of the screen, so whatever you are level with is the most legible
     thing on the page. It resolves later and over a longer range than the
     reveal above, which is what makes it read as focus pulling rather than as
     a second fade-in.

     This animates colour while the reveal animates opacity and transform, so
     the two run on the same element without either overriding the other — the
     reason the reveal is not simply extended to cover both. */

  @keyframes text-focus {
    from {
      color: var(--text-3);
    }

    to {
      color: var(--text-2);
    }
  }

  /* The standfirst resolves to the full ink, not to body-copy contrast, so it
     needs its own pair — sharing text-focus would flatten it. */
  @keyframes text-focus-lead {
    from {
      color: var(--text-2);
    }

    to {
      color: var(--ink);
    }
  }

  .prose p,
  .section__lead {
    animation: rise linear both, text-focus linear both;
    animation-timeline: view(), view();
    animation-range:
      entry 5% cover 26%,
      entry 25% cover 55%;
  }

  .prose > p:first-child {
    animation: rise linear both, text-focus-lead linear both;
    animation-timeline: view(), view();
    animation-range:
      entry 5% cover 26%,
      entry 25% cover 55%;
  }

  /* The detail rows already reveal as whole cells, so only their body copy
     takes the focus pass. */
  .detail__body {
    animation: text-focus linear both;
    animation-timeline: view();
    animation-range: entry 25% cover 55%;
  }

  /* The night bands invert the two ends, so the pass runs the same way against
     a dark ground instead of fading copy towards the background. */
  .section--night :is(.section__lead, .prose p, .prose > p:first-child, .detail__body) {
    animation-name: rise, none;
  }

  /* ---------- The label's rule ----------
     The trailing rule draws itself out from the label. scaleX rather than width
     so it stays on the compositor. */

  @keyframes rule-draw {
    from {
      transform: scaleX(0);
      opacity: 0;
    }

    to {
      transform: scaleX(1);
      opacity: 0.4;
    }
  }

  .label::after {
    transform-origin: left center;
    animation: rule-draw linear both;
    animation-timeline: view();
    animation-range: entry 10% cover 30%;
  }

  /* ---------- Hero ----------
     The hero drifts up and dims as it leaves, so the page reads as moving past
     it rather than the hero simply scrolling off. The range starts at exit 15%
     — before that the hero is untouched, so it is at full strength for as long
     as it is the thing being looked at. */

  @keyframes hero-exit {
    to {
      opacity: 0.3;
      transform: translateY(-2.5rem);
    }
  }

  .hero__inner {
    animation: hero-exit linear both;
    animation-timeline: view();
    animation-range: exit 15% exit 100%;
  }

  /* The dot grid behind the figure drifts the other way, which is what makes
     the hero feel like it has depth rather than one flat plane sliding away.
     A slower, smaller movement than the old light pool took: this is drafting
     paper rather than a lit room, and paper that slides reads as a mistake. */
  @keyframes grid-drift {
    to {
      transform: translateY(1.75rem);
    }
  }

  .hero::before {
    animation: grid-drift linear both;
    animation-timeline: view();
    animation-range: exit 0% exit 100%;
  }

  /* The mark takes a third speed, between the two. The copy leaves at -2.5rem,
     the grid follows at +1.75rem, and the watermark sits between them, so the
     hero comes apart into three planes on the way out instead of two. It fades
     as it goes: at 0.05 it is barely there to begin with, and a shape that
     holds its weight while everything around it dims starts to announce
     itself. */
  @keyframes hero-mark-drift {
    to {
      opacity: 0;
      transform: translateY(1.25rem) scale(1.05);
    }
  }

  .hero__mark {
    animation: hero-mark-drift linear both;
    animation-timeline: view();
    animation-range: exit 0% exit 100%;
  }

  /* ---------- Closing call to action ----------
     The conversion point, so it gets a longer, later entrance than a body
     section — it arrives after the reader has stopped, rather than while they
     are still scrolling past. */

  .cta__text,
  .cta__actions {
    animation: rise linear both;
    animation-timeline: view();
    animation-range: entry 10% cover 40%;
  }

  .cta__actions {
    animation-range: entry 16% cover 48%;
  }
}

@supports (animation-timeline: scroll()) {
  /* ---------- Reading progress ----------
     A hairline that fills as the document is read. On a page someone is
     skimming, it is the cheapest possible answer to "how much of this is left"
     — and it is one element with no state to keep. */

  @keyframes progress {
    from {
      transform: scaleX(0);
    }

    to {
      transform: scaleX(1);
    }
  }

  body::after {
    content: "";
    position: fixed;
    /* Below the status bar, not behind it: the viewport reaches the top of the
       screen now, and a 2px bar under the clock would not be readable. */
    inset: env(safe-area-inset-top) 0 auto;
    /* Above the sticky header at 30 so it stays visible over it, but below the
       skip link at 100, which has to be able to cover it when focused. */
    z-index: 90;
    height: 2px;
    /* Ramps from the accent to the ink across its own length, so the leading
       edge is the part the eye lands on. */
    background: linear-gradient(90deg, var(--accent), var(--ink));
    transform: scaleX(0);
    transform-origin: 0 50%;
    animation: progress linear both;
    animation-timeline: scroll(root block);
  }

  /* ---------- Header on scroll ----------
     The settled state, without the class or the listener that normally sets it.
     The header is sticky at every width, so it settles at every width. */

  @keyframes header-settle {
    to {
      border-bottom-color: var(--rule-mid);
      box-shadow: 0 12px 34px -30px rgb(23 20 28 / 0.65);
    }
  }

  .site-header {
    animation: header-settle linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 90px;
  }

  @media (min-width: 52em) {
    /* One row here, so it also tightens as it settles — which is what signals
       the header has changed mode without needing a colour to say so. */
    @keyframes header-tighten {
      to {
        min-height: 3.5rem;
      }
    }

    .site-header__inner {
      animation: header-tighten linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 90px;
    }
  }
}

/* ---------- Motion guard ----------
   The global reduced-motion block in theme.css collapses animation-duration,
   which does nothing here: a scroll-driven animation takes its progress from
   the scroll position, not from a duration. These have to be switched off by
   name. */

@media (prefers-reduced-motion: reduce) {
  .section__head,
  .prose p,
  .prose > p:first-child,
  .section__lead,
  .detail__body,
  .pullquote,
  .stage,
  .card,
  .person,
  .detail,
  .tier,
  .enquiry,
  .facts li,
  .page-head__lead,
  .contact-address,
  .site-footer__brand,
  .site-footer__group,
  .label::after,
  .hero__inner,
  .hero__mark,
  .hero::before,
  .cta__text,
  .cta__actions,
  .site-header,
  .site-header__inner,
  body::after {
    animation: none !important;
  }

  /* Parking the bar at full width would state that the page has been read to
     the end, so it is removed rather than frozen. */
  body::after {
    display: none;
  }
}
