/* =========================================================
   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 now 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 did before — no
   element is hidden behind an animation that cannot run, which is
   the failure mode of the usual "add .is-visible with JS" approach.

   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,
  .enquiry,
  .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) {
    animation-range: entry 5% cover 34%;
  }

  .card-grid .card:nth-child(3n) {
    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(--faint);
    }

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

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

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

  .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%;
  }

  /* ---------- Eyebrow rule ----------
     The trailing rule draws itself out from the label. scaleX rather
     than width so it stays on the compositor; the translateY is
     carried over from the static rule it replaces. */

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

    to {
      transform: translateY(-1px) scaleX(1);
      opacity: 0.45;
    }
  }

  .eyebrow::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 light pool behind it drifts the other way, which is what
     makes the hero feel like it has depth rather than one flat plane
     sliding away. */
  @keyframes veil-drift {
    to {
      transform: translateY(3rem) scale(1.08);
    }
  }

  .hero::before {
    animation: veil-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 light pool follows at +3rem, and the watermark sits
     between them at +1.25rem, so the hero comes apart into three
     planes on the way out instead of two. It fades as it goes: at
     0.04 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 20 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 muted tone to full strength across its own length, so the
       leading edge is the brightest part of the bar. */
    background: linear-gradient(90deg, var(--muted), var(--fg));
    transform: scaleX(0);
    transform-origin: 0 50%;
    animation: progress linear both;
    animation-timeline: scroll(root block);
  }

  /* ---------- Header on scroll ----------
     The kit's is-scrolled state, without the class or the listener that
     normally sets it. The header is sticky at every width, so it settles at
     every width — but into a different surface on each. */

  /* On a phone the bar stays opaque and only its edge changes. The settled
     state used to thin the background to 88% and blur what passed behind it,
     and at that size the result was body text legibly crossing the strip above
     the wordmark — read as a rendering fault, not as depth. The shadow does
     the whole job of lifting the bar off the page. */
  @keyframes header-settle {
    to {
      border-bottom-color: var(--line-soft);
      box-shadow: 0 12px 34px -28px rgb(0 0 0 / 0.75);
    }
  }

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

  @media (min-width: 64em) {
    /* Room for the glass here: one row of links, and nothing of the page ever
       sits close enough behind the bar to be read through it. */
    @keyframes header-settle-wide {
      to {
        background: color-mix(in srgb, var(--bg) 88%, transparent);
        border-bottom-color: var(--line-soft);
        box-shadow: 0 12px 34px -28px rgb(0 0 0 / 0.75);
      }
    }

    .site-header {
      backdrop-filter: blur(14px) saturate(1.1);
      animation-name: header-settle-wide;
    }

    /* 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 {
        padding-block: 0.75rem;
      }
    }

    .site-header .brand {
      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,
  .enquiry,
  .page-head__lead,
  .contact-address,
  .site-footer__brand,
  .site-footer__group,
  .eyebrow::after,
  .hero__inner,
  .hero__mark,
  .hero::before,
  .cta__text,
  .cta__actions,
  .site-header,
  .site-header .brand,
  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;
  }
}
