@layer motion {
  /* Custom properties that get animated MUST be registered, or they snap
     between values instead of interpolating. */
  /* Initial value is 100% — FULLY REVEALED — for the same reason .reveal
     defaults to visible: if JS is disabled the inline head script never runs,
     data-motion is never set, and a 0% default would clip the hero to nothing.
     The frame is only closed when an intro is actually going to open it. */
  @property --frame-reveal {
    syntax: '<percentage>'; inherits: false; initial-value: 100%;
  }

  /* Deliberately NOT closed here. The frame is open by default and JS closes
     it a moment before it plays the reveal — so a stalled timeline, a slow
     device or an error mid-flight all leave a visible hero rather than an
     empty box. CSS closing it would make visibility depend on JS finishing. */

  /* --- Scroll-driven reveals -------------------------------------------
     The visible state is the DEFAULT. The animation only exists inside the
     @supports guard. Written the other way round (opacity:0 by default), a
     browser without scroll-driven animation support — Firefox today — shows
     a blank page. */
  .reveal { opacity: 1; transform: none; }

  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .reveal {
        animation: sf-reveal linear both;
        animation-timeline: view();
        animation-range: entry 0% cover 32%;
      }
      @keyframes sf-reveal {
        from { opacity: 0; transform: translateY(1.5rem); }
        to   { opacity: 1; transform: none; }
      }
    }
  }

  /* --- Cross-document view transitions ---------------------------------
     Both documents opt in; the matching view-transition-name pairs a homepage
     panel with its world hero. Works with zero JS. Firefox simply navigates
     normally, which is a perfectly good outcome. */
  @view-transition { navigation: auto; }

  ::view-transition-group(*) { animation-duration: var(--dur-vt); }

  /* The pairs that make a homepage panel morph into that world's hero.
     Declared in CSS, not inline: GSAP rewrites the style attribute of any
     element it animates, which silently dropped an inline name. */
  [data-world='home'] .panel--sports     { view-transition-name: vt-sports; }
  [data-world='home'] .panel--weddings   { view-transition-name: vt-weddings; }
  [data-world='home'] .panel--commercial { view-transition-name: vt-commercial; }

  [data-world='sports'] .hero__frame     { view-transition-name: vt-sports; }
  [data-world='weddings'] .hero__frame   { view-transition-name: vt-weddings; }
  [data-world='commercial'] .hero__frame { view-transition-name: vt-commercial; }

  /* --- Assembly --------------------------------------------------------
     The shape of the reveal is per-world CSS; JS only tweens one number. */
  .assembly { position: absolute; inset: 0; pointer-events: none; }
  .piece { position: absolute; fill: var(--ink); }
  .piece text { fill: var(--surface); font-family: var(--font-display); font-size: 20px; font-weight: 700; }

  /* With JS off or motion disabled the furniture simply sits where it lands. */
  [data-motion='off'] .piece,
  [data-motion='settled'] .piece { opacity: 1; transform: none; }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: .001ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: .001ms !important;
    }
    ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
      animation: none !important;
    }
  }
}
