/* ═══════════════════════════════════════════════════════════════════════════
   PLATFORM — the modern web features almost nothing ships yet.
   ───────────────────────────────────────────────────────────────────────────
   Everything in this file is a progressive enhancement guarded by @supports.
   A browser without any of it gets the site exactly as it was: the JS reveal
   in site.js still runs, the pages still navigate, nothing is missing. Nothing
   here is load-bearing, which is the only reason it is safe to use features
   this new on a production site.

     1. Cross-document View Transitions — page-to-page morphing on a STATIC
        site with no framework and no router.
     2. Scroll-driven animations — reveals that run on the compositor with
        zero JavaScript and zero main-thread work.
     3. @property — custom properties the browser can actually interpolate.
     4. Container queries — cards that respond to their own width.
     5. :has() — layout that reacts to its own content.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1. cross-document view transitions ────────────────────────────────────
   Navigations between pages morph instead of blanking. The wordmark and the
   header are named, so they persist across the navigation rather than being
   torn down and rebuilt — the site reads as one continuous surface.

   This is the whole feature on a static site: no SPA, no router, no hydration,
   no client-side JavaScript at all. Chrome and Edge honour it today; every
   other browser performs an ordinary navigation and notices nothing. */

@view-transition { navigation: auto; }

::view-transition-group(root) { animation-duration: 340ms; }
::view-transition-old(root) {
  animation: vt-out 240ms cubic-bezier(.4, 0, 1, 1) both;
}
::view-transition-new(root) {
  animation: vt-in 340ms cubic-bezier(0, 0, .2, 1) both;
}
@keyframes vt-out { to { opacity: 0; transform: translateY(-8px) } }
@keyframes vt-in { from { opacity: 0; transform: translateY(12px) } }

/* Named elements survive the navigation instead of cross-fading with the page. */
.site-head   { view-transition-name: site-head; }
.wordmark    { view-transition-name: wordmark; }
.site-foot   { view-transition-name: site-foot; }

/* A tile morphing into the project page's hero shot is the payoff. The name is
   assigned per-card by script so it is unique for the one card being clicked —
   two elements sharing a transition name in the same document is an error that
   silently disables the whole transition. */
.tile__link.is-transitioning .tile__media,
.card-x__link.is-transitioning .card-x__shot img { view-transition-name: project-shot; }
.project__shot img { view-transition-name: project-shot; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ── 2. scroll-driven animations, no JavaScript ────────────────────────────
   `animation-timeline: view()` drives an animation from an element's position
   in the scrollport. It runs on the compositor, so it costs no main-thread
   work at all — an IntersectionObserver plus a class toggle plus a CSS
   transition is three moving parts to achieve something the browser can now do
   natively in two declarations.

   site.js still reveals `[data-reveal]` for browsers without this. The two
   systems use different attributes on purpose so they can never fight over the
   same element. */

@supports (animation-timeline: view()) {
  .reveal {
    animation: reveal-in linear both;
    animation-timeline: view();
    /* Starts as the element enters the bottom of the viewport and completes
       well before the middle — a reveal still running at centre screen reads
       as lag rather than as motion. */
    animation-range: entry 8% cover 34%;
  }
  @keyframes reveal-in {
    from { opacity: 0; transform: translateY(26px); }
    to   { opacity: 1; transform: none; }
  }

  /* The hero image drifts as the page scrolls — parallax with no scroll
     listener, no rAF loop and no jank. */
  .hero__media img {
    animation: hero-drift linear both;
    animation-timeline: view();
    animation-range: cover;
  }
  @keyframes hero-drift {
    from { transform: scale(1.06) translateY(0); }
    to   { transform: scale(1.06) translateY(6%); }
  }

  /* A progress rule across the top of the page, driven by document scroll. */
  .scroll-rule {
    position: fixed; z-index: 200; inset: 0 0 auto 0; height: 2px;
    transform-origin: 0 50%; background: var(--gold);
    animation: scroll-progress linear both;
    animation-timeline: scroll(root block);
  }
  @keyframes scroll-progress { from { transform: scaleX(0) } to { transform: scaleX(1) } }
}
/* Without support the rule would sit at full width across the top, so it only
   exists where it can actually be driven. */
@supports not (animation-timeline: view()) { .scroll-rule { display: none; } }

@media (prefers-reduced-motion: reduce) {
  .reveal, .hero__media img { animation: none !important; opacity: 1 !important; transform: none !important; }
}

/* A scroll-driven animation has no scroll position in print, so every element
   below the first page stays at its 0% keyframe — which for a reveal is
   opacity 0. Printing this page produced a blank document. Anything that
   renders the page without scrolling it (print, PDF export, a full-page
   screenshot, an archiver) needs the finished state. */
@media print {
  .reveal, .hero__media img {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  .scroll-rule, .hero__cue, .hero__scope { display: none !important; }
}

/* ── 3. @property — interpolatable custom properties ───────────────────────
   A custom property is a string unless it is registered, which is why
   gradients built from `var()` normally snap instead of animating. Registering
   the angle makes the browser tween it. */

@property --sheen {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
.btn {
  background-image: conic-gradient(from var(--sheen),
    transparent 0deg, rgba(255,255,255,.16) 24deg, transparent 60deg);
  transition: --sheen 900ms var(--ease-out), background var(--dur-1) var(--ease-io),
              color var(--dur-1) var(--ease-io), border-color var(--dur-1) var(--ease-io);
}
.btn:hover { --sheen: 360deg; }
@media (prefers-reduced-motion: reduce) { .btn { background-image: none; } }

/* ── 4. container queries ──────────────────────────────────────────────────
   A card decides its own type size from ITS width, not the viewport's. The
   same component then works in a three-up grid, a two-up and a sidebar with
   no breakpoints written per layout. */

.cards { container-type: inline-size; }
.card-x { container-type: inline-size; }
@container (min-width: 420px) {
  .card-x__body { padding: clamp(22px, 2.2vw, 32px); }
  .card-x__title { font-size: clamp(21px, 2.2vw, 28px); }
}
@container (max-width: 300px) {
  .card-x__note { display: none; }   /* below this width the note is unreadable anyway */
}

/* ── 5. :has() — layout reacting to its own content ────────────────────────
   A band containing no figure gets its copy centred; one with a figure stays
   two-column. Previously this needed a template flag threaded through from the
   data layer. */

.band:has(.about-band__figure) .about-band__copy { max-width: 56ch; }
.band:not(:has(.about-band__figure)) .about-band { grid-template-columns: 1fr; }

/* A card with no image loses its media row rather than reserving empty space. */
.card-x__link:not(:has(.card-x__shot)) { grid-template-rows: 1fr; }

/* The header knows when the mobile menu is open and can dim behind it. */
.site-head:has([data-nav-toggle][aria-expanded="true"]) { background: var(--bg); }
