/*
  @scrim/scenes — the primary path.

  This stylesheet is the whole of the common case.  A scene that fades a background,
  crossfades a stack of images or reveals its content as it arrives needs no JavaScript
  at all: the browser runs those animations off the main thread, against a timeline it
  recomputes from live layout, which is why they stay glued to the scroll in a way no
  scroll handler can match.

  Read it in three parts, in this order, because that is the order it was designed in.

    1. The static state.  What a scene looks like when nothing animates — no support,
       no JavaScript, or a reader who asked for less motion.  This is designed first and
       on purpose (RISKS.md R6): if Firefox slips, this is what a third of readers see,
       and there must be exactly one "no animation" design rather than two.
    2. The CSS path, behind `@supports (animation-timeline: view())` and
       `prefers-reduced-motion: no-preference`.  There is no polyfill and there will not
       be one; `@supports` is the only fallback there is.
    3. The hooks the Motion-driven runtime needs in place before it can animate, which
       it switches on by setting `data-scrim-driver="fallback"` on <html>.

  Only `opacity` and `transform` are animated anywhere in here.  Those two run on the
  compositor; animating `width`, `height` or `margin` would force layout every frame and
  drag the whole animation back onto the main thread.

  The pattern — a named view timeline, paired `@supports` branches, reduced motion
  handled in both directions — is railway.com's, which ships it in production today.
*/

/* ========================================================================== */
/* 1. The static state                                                        */
/* ========================================================================== */

[data-scrim-scene] {
  /* The fixed colour plane of the `background` effect is positioned against the
     viewport, but it is a child of the scene, so the scene must not be transformed or
     filtered: either would make it the containing block and pin the plane to the scene. */
  position: relative;

  /* The two enumerated knobs, resolved into custom properties once.  The runtime reads
     the same two attributes through the same two tables in `contract.ts`. */
  --scrim-range: cover;
  --scrim-ease: linear;
}

[data-scrim-scene][data-scrim-effect='reveal'] {
  --scrim-range: entry;
}

/* Explicit ranges win over the per-effect default above, by specificity. */
[data-scrim-scene][data-scrim-range='cover'] {
  --scrim-range: cover;
}
[data-scrim-scene][data-scrim-range='contain'] {
  --scrim-range: contain;
}
[data-scrim-scene][data-scrim-range='entry'] {
  --scrim-range: entry;
}
[data-scrim-scene][data-scrim-range='exit'] {
  --scrim-range: exit;
}

[data-scrim-scene][data-scrim-ease='dwell'] {
  /* The remix.run idea: hold near the ends, move briskly between them.  The same four
     numbers are `DWELL_BEZIER` in contract.ts. */
  --scrim-ease: cubic-bezier(0.85, 0, 0.15, 1);
}

/* With no animation, a `background` scene is simply a section painted in its own
   colour.  That is a design someone would choose on purpose, which is the test. */
[data-scrim-scene][data-scrim-effect='background'] {
  background-color: var(--scene-background, transparent);
}

/* With no animation, a crossfade stack is a stack: every layer in flow, in document
   order, all of them visible.  A layer is content, and content that only exists at some
   scroll offset is content a screen-reader user never receives (RISKS.md R6). */
[data-scrim-layers] {
  display: grid;
  gap: var(--scrim-layer-gap, 1rem);
}

[data-scrim-layers] > * {
  min-width: 0;
}

/* Layer index and count, counted by CSS so that a document never has to state them.
   Six is the ceiling; a seventh layer simply stops being part of the crossfade, which
   degrades to "it is still there and still visible" rather than to a broken page. */
/* Published so the calc() below and `layerWindow()` in contract.ts read the same two
   numbers.  Change them here and in contract.ts together; `verifyContract()` does not
   cover these, because a range is not a keyframe. */
:root {
  --scrim-crossfade-start: 0.15;
  --scrim-crossfade-span: 0.5;
}

[data-scrim-layers] {
  --layer-count: 1;
}
[data-scrim-layers]:has(> :nth-child(2):last-child) {
  --layer-count: 2;
}
[data-scrim-layers]:has(> :nth-child(3):last-child) {
  --layer-count: 3;
}
[data-scrim-layers]:has(> :nth-child(4):last-child) {
  --layer-count: 4;
}
[data-scrim-layers]:has(> :nth-child(5):last-child) {
  --layer-count: 5;
}
[data-scrim-layers]:has(> :nth-child(6):last-child) {
  --layer-count: 6;
}

[data-scrim-layers] > :nth-child(1) {
  --layer-index: 0;
}
[data-scrim-layers] > :nth-child(2) {
  --layer-index: 1;
}
[data-scrim-layers] > :nth-child(3) {
  --layer-index: 2;
}
[data-scrim-layers] > :nth-child(4) {
  --layer-index: 3;
}
[data-scrim-layers] > :nth-child(5) {
  --layer-index: 4;
}
[data-scrim-layers] > :nth-child(6) {
  --layer-index: 5;
}

/* ========================================================================== */
/* Keyframes, shared by both paths                                            */
/* ========================================================================== */

/* Offsets here are progress through the scene's range, not time — a scroll-driven
   animation's "duration" is the range.  The same numbers are BACKDROP_KEYFRAMES and
   REVEAL_KEYFRAMES in contract.ts, because CSS keyframe offsets cannot be custom
   properties and so cannot be shared.  `verifyContract()` reads these rules back at
   runtime and reports drift. */

@keyframes scrim-backdrop {
  0% {
    opacity: 0;
  }
  35%,
  65% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes scrim-reveal {
  0% {
    opacity: 0;
    transform: translateY(1.5rem);
  }
  80%,
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes scrim-layer-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ========================================================================== */
/* 2. The CSS path                                                            */
/* ========================================================================== */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* The runtime stands these rules down by writing a different driver on <html>.
       No attribute at all means no runtime, which is the point: with JavaScript off,
       this branch is still the one that runs.  It is also how a demo can force the
       fallback path on a browser that supports the real one. */
    :is(:root:not([data-scrim-driver]), :root[data-scrim-driver='css']) {
      [data-scrim-scene] {
        /* One name, redeclared on every scene.  Timeline lookup walks up from the
           animating element, so each scene's descendants find their own scene. */
        view-timeline-name: --scrim-scene;
      }

      [data-scrim-scene][data-scrim-effect='background'] {
        /* The colour moves to the plane below; the section itself stops carrying it. */
        background-color: transparent;

        &::before {
          content: '';
          position: fixed;
          inset: 0;
          z-index: -1;
          pointer-events: none;
          background-color: var(--scene-background, transparent);
          animation: scrim-backdrop both;
          animation-timeline: --scrim-scene;
          animation-timing-function: var(--scrim-ease);
          animation-range: var(--scrim-range);
        }
      }

      /* The stack overlaps only once something is going to fade between its layers. */
      [data-scrim-scene][data-scrim-effect='crossfade'] [data-scrim-layers] {
        grid-template-areas: 'scrim-stack';
        gap: 0;

        & > * {
          grid-area: scrim-stack;
        }

        & > :not(:first-child) {
          animation: scrim-layer-in both;
          animation-timeline: --scrim-scene;
          animation-timing-function: var(--scrim-ease);
          /* The identical window as `layerWindow()` in contract.ts: layer i of n fades in
             across the slice of [start, start + span] that belongs to it.  The sequence
             deliberately does not fill the range — `cover` ends once the scene is gone,
             so a sequence that ran to 100% would land its last layer off-screen. */
          animation-range: var(--scrim-range)
              calc(
                100% *
                  (var(--scrim-crossfade-start) + var(--scrim-crossfade-span) * (var(--layer-index) - 1) /
                    (var(--layer-count) - 1))
              )
            var(--scrim-range)
              calc(
                100% *
                  (var(--scrim-crossfade-start) + var(--scrim-crossfade-span) * var(--layer-index) /
                    (var(--layer-count) - 1))
              );
        }
      }

      [data-scrim-scene][data-scrim-effect='reveal'] > * {
        animation: scrim-reveal both;
        animation-timeline: --scrim-scene;
        animation-timing-function: var(--scrim-ease);
        animation-range: var(--scrim-range);
      }
    }
  }
}

/* ========================================================================== */
/* 3. What the Motion-driven fallback needs                                   */
/* ========================================================================== */

/* These rules are unguarded on purpose: they only bite once the runtime has set
   `data-scrim-driver="fallback"`, and it only does that when it is actually going to
   animate — never under reduced motion.  Everything continuous is written inline by the
   runtime as `opacity` and `transform`, i.e. the same two properties as above. */

:root[data-scrim-driver='fallback'] [data-scrim-scene][data-scrim-effect='background'] {
  background-color: transparent;
}

/* The CSS path fades a `::before`.  The runtime cannot style a pseudo-element, so it
   appends one of these instead.  Same box, same keyframes. */
[data-scrim-backdrop] {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  background-color: var(--scene-background, transparent);
}

:root[data-scrim-driver='fallback'] [data-scrim-scene][data-scrim-effect='crossfade'] [data-scrim-layers] {
  grid-template-areas: 'scrim-stack';
  gap: 0;
}

:root[data-scrim-driver='fallback'] [data-scrim-scene][data-scrim-effect='crossfade'] [data-scrim-layers] > * {
  grid-area: scrim-stack;
}
