/* Scroll stage. From the hero to the last media section the screen stays put:
   each [data-scene] pins its [data-stage] while scrolling changes it in place,
   and the next scene wipes in over it. Shared by Coup de Foudre, LOV and The Club.
   Edit .workspace/scroll-stage/src/ and run node .workspace/scroll-stage/sync.mjs;
   the copies in the sites are overwritten. */

/* One progress value per scene, 0 to 1, animated by its scroll position.
   Where scroll timelines are missing, js/scroll-stage.js writes it instead. */
@property --p { syntax: '<number>'; inherits: true; initial-value: 0; }
/* Registered so they compute to plain numbers the script can read. */
@property --n { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --i { syntax: '<number>'; inherits: true; initial-value: 0; }
@property --entry { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --steps { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --cover { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --len { syntax: '<number>'; inherits: true; initial-value: 3.5; }

@keyframes stage-progress { to { --p: 1; } }
@keyframes stage-rise { from { opacity: 0; translate: 0 40px; } }

/* Display titles stack one word per line: the box is as wide as its longest word.
   Words of one or two letters stay with the next word through a no-break space
   (written in the pages, kept by js/scroll-stage.js when a translation changes the text). */
[data-scroll-reveal="heading"],
[data-items] > * :is(figcaption strong, h3) {
  display: block;
  width: min-content;
  max-width: 100%;
  /* A word never breaks inside; otherwise the longest word is one letter wide. */
  overflow-wrap: normal !important;
  word-break: normal !important;
  hyphens: manual !important;
}

/* Stage layout and motion. Browsers without scroll timelines skip this block;
   js/scroll-stage.js applies it for them and drives --p itself.
   !important marks the mechanics a site's own layout must never override. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) and (min-height: 480px) {
    :root {
      --stage-on: 1;
      /* Pace, in screens of scroll. */
      --stage-hold: .5;
      --stage-card: .5;
    }

    /* Scene: a tall scroll range. Its length in screens is
       entry (its wipe in) + steps (its own changes) + hold + cover (the next wipe). */
    [data-scene] {
      --entry: 1;
      --steps: 1;
      --cover: 1;
      --len: calc(var(--entry) + var(--steps) + var(--stage-hold) + var(--cover));
      position: relative !important;
      height: calc((var(--len) + 1) * 100svh) !important;
      min-height: 0 !important;
      margin: 0 !important;
      padding: 0 !important;
      background: none !important;
      /* A stage scaled past 100% must not widen the page; clip is not a scroll container, so sticky still works. */
      overflow-x: clip !important;
      scroll-margin-top: calc(var(--entry) * -100svh);
      /* Scenes overlap; only what a stage shows may take taps and paint. */
      pointer-events: none;
    }
    [data-scene="hero"] { --entry: 0; }
    [data-scene="rail"] { --steps: calc(var(--n) * var(--stage-card)); }
    [data-scene="deck"] { --steps: calc(var(--n) - 1); }
    [data-scene="text"] { --steps: var(--n); }
    [data-scene]:not(:has(~ [data-scene])) { --cover: 0; }
    /* The next scene starts two screens early, so its stage is already pinned
       while the previous one is still pinned beneath it. */
    [data-scene] ~ [data-scene] { margin-top: -200svh !important; }
    [data-scene] > :not([data-stage]) { scroll-margin-top: calc(var(--entry) * -100svh); }

    /* Item counts come from the page itself. */
    [data-scene]:has([data-items] > :nth-child(2)) { --n: 2; }
    [data-scene]:has([data-items] > :nth-child(3)) { --n: 3; }
    [data-scene]:has([data-items] > :nth-child(4)) { --n: 4; }
    [data-scene]:has([data-items] > :nth-child(5)) { --n: 5; }
    [data-scene]:has([data-items] > :nth-child(6)) { --n: 6; }
    [data-scene]:has([data-items] > :nth-child(7)) { --n: 7; }
    [data-scene]:has([data-items] > :nth-child(8)) { --n: 8; }
    [data-scene]:has([data-items] > :nth-child(9)) { --n: 9; }
    [data-scene]:has([data-items] > :nth-child(10)) { --n: 10; }
    [data-scene]:has([data-items] > :nth-child(11)) { --n: 11; }
    [data-scene]:has([data-items] > :nth-child(12)) { --n: 12; }
    [data-scene]:has([data-items] > :nth-child(13)) { --n: 13; }
    [data-scene]:has([data-items] > :nth-child(14)) { --n: 14; }
    [data-scene]:has([data-items] > :nth-child(15)) { --n: 15; }
    [data-scene]:has([data-items] > :nth-child(16)) { --n: 16; }
    [data-items] > :nth-child(2) { --i: 1; }
    [data-items] > :nth-child(3) { --i: 2; }
    [data-items] > :nth-child(4) { --i: 3; }
    [data-items] > :nth-child(5) { --i: 4; }
    [data-items] > :nth-child(6) { --i: 5; }
    [data-items] > :nth-child(7) { --i: 6; }
    [data-items] > :nth-child(8) { --i: 7; }
    [data-items] > :nth-child(9) { --i: 8; }
    [data-items] > :nth-child(10) { --i: 9; }
    [data-items] > :nth-child(11) { --i: 10; }
    [data-items] > :nth-child(12) { --i: 11; }
    [data-items] > :nth-child(13) { --i: 12; }
    [data-items] > :nth-child(14) { --i: 13; }
    [data-items] > :nth-child(15) { --i: 14; }
    [data-items] > :nth-child(16) { --i: 15; }

    /* Stage: one screen, pinned. --s is screens scrolled into the scene;
       --in, --k and --out are its entry, step and cover windows. */
    [data-stage] {
      --s: calc(var(--p) * var(--len));
      --in: clamp(0, var(--s) + 1 - var(--entry), 1);
      --in-e: calc(var(--in) * var(--in) * (3 - 2 * var(--in)));
      --k: clamp(0, var(--s) - var(--entry), var(--steps));
      --out: clamp(0, var(--s) - var(--len) + var(--cover), 1);
      position: sticky !important;
      top: 0 !important;
      box-sizing: border-box !important;
      height: 100svh !important;
      min-height: 0 !important;
      max-height: none !important;
      margin: 0 !important;
      padding-bottom: calc(var(--stage-dock, 0px) + 16px) !important;
      overflow: clip !important;
      pointer-events: auto;
      /* When toolbars collapse the screen grows past 100svh; the shadow fills that band. */
      box-shadow: 0 calc(100lvh - 100svh) 0 0 var(--stage-skirt, transparent);
      transform-origin: 50% 35%;
      /* Rises from the bottom edge, settling from 115%; once covered it is pushed back, up and dimmed. */
      clip-path: inset(calc((1 - var(--in-e)) * (100% + 100lvh - 100svh)) 0 calc(100svh - 100lvh) 0);
      scale: calc((1.15 - .15 * var(--in-e)) * (1 - .08 * var(--out)));
      translate: 0 calc(var(--out) * -6svh);
      opacity: calc(1 - .55 * var(--out));
    }
    [data-stage] [data-scroll-reveal] {
      --h: clamp(0, (var(--in) - .3) / .7, 1);
      translate: 0 calc((1 - var(--h)) * .8em);
      clip-path: inset(calc((1 - var(--h)) * 100%) -.3em -.3em -.3em);
      opacity: var(--h);
    }
    [data-stage] [data-scroll-reveal="detail"] { --h: clamp(0, (var(--in) - .5) / .5, 1); }

    /* Content between the stage and its items fills the space above the dock. */
    [data-stage]:has([data-items]) { display: flex !important; flex-direction: column !important; }
    [data-stage]:has([data-items]) > *,
    [data-stage] :has([data-items]) > * { flex: 0 0 auto !important; }
    [data-stage] :has([data-items]) { display: flex !important; flex-direction: column !important; flex: 1 1 0 !important; min-height: 0 !important; }

    /* Hero: the title lifts away while the picture pushes in. */
    [data-scene="hero"] > [data-stage] { --ke: calc(var(--k) * var(--k) * (3 - 2 * var(--k))); }
    [data-scene="hero"] [data-scroll-depth] { scale: calc(1 + .2 * var(--ke)); }
    [data-scene="hero"] [data-scroll-depth="title"] {
      scale: none;
      translate: 0 calc(var(--ke) * -30svh);
      opacity: calc(1 - 1.4 * var(--ke));
    }

    /* Rail: cards deal in from below, then the track travels exactly its overflow.
       Padding by half the free width makes the first and last cards settle centred;
       a site gives its card width as --stage-card-width (100cqb is the window height). */
    [data-scene="rail"] :has(> [data-items]) { container-type: size !important; overflow: hidden !important; scroll-snap-type: none !important; }
    [data-scene="rail"] [data-items] {
      height: 100% !important;
      padding-inline: max(0px, (100cqi - var(--stage-card-width, 100cqi)) / 2) !important;
      translate: calc(min(0px, 100cqi - 100%) * var(--k) / max(var(--steps), .001)) 0;
    }
    [data-scene="rail"] [data-items] > * {
      --deal: clamp(0, (var(--in) - .3 - min(var(--i), 3) * .12) / .45, 1);
      --c: calc(var(--k) / max(var(--steps), .001) * (var(--n) - 1));
      --lift: max(0, 1 - max(var(--c) - var(--i), var(--i) - var(--c)));
      translate: 0 calc((1 - var(--deal) * (2 - var(--deal))) * 32svh);
      scale: calc(1 + .05 * var(--lift));
    }

    /* Deck: slides share one frame; each next slide wipes up over the last.
       A site may narrow the slides with --stage-slide-width (100cqb is the frame height). */
    [data-scene="deck"] [data-items] { position: relative !important; flex: 1 1 0 !important; min-height: 0 !important; container-type: size !important; }
    [data-scene="deck"] [data-items] > * {
      --t: clamp(0, var(--k) - var(--i) + 1, 1);
      --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
      --u: clamp(0, var(--k) - var(--i), 1);
      position: absolute !important;
      inset: 0 !important;
      width: var(--stage-slide-width, auto) !important;
      height: auto !important;
      /* Slides fill the frame; a site's own aspect ratio would leave a band (WebKit honours it here). */
      aspect-ratio: auto !important;
      margin: 0 auto !important;
      clip-path: inset(calc((1 - var(--e)) * 100%) 0 0);
      scale: calc(1 - .08 * var(--u));
      opacity: calc(1 - .55 * var(--u));
    }
    [data-scene="deck"] [data-items] > * > :is(picture, video) { scale: calc(1.25 - .25 * var(--e) * var(--in-e)); }
    [data-scene="deck"] [data-items] > * > :not(picture, video, img) { translate: 0 calc((1 - var(--e)) * 48px); }

    /* Frame: one photograph opens from a window to the full frame. */
    [data-scene="frame"] [data-items] {
      --e: calc(var(--k) * var(--k) * (3 - 2 * var(--k)));
      flex: 1 1 0 !important;
      min-height: 0 !important;
      height: auto !important;
      clip-path: inset(calc((1 - var(--e)) * 12%) calc((1 - var(--e)) * 8%));
    }
    [data-scene="frame"] [data-items] img { scale: calc(1.3 - .3 * var(--e)); }

    /* Text: lines arrive one at a time and stay. */
    [data-scene="text"] > [data-stage] { justify-content: center; }
    [data-scene="text"] [data-items] > * {
      --t: clamp(0, var(--k) - var(--i), 1);
      translate: 0 calc((1 - var(--t)) * 40px);
      opacity: var(--t);
    }
  }
}

/* Native browsers: scroll position drives --p, and rows outside the stage rise as they enter.
   (This condition must not name animation-timeline: the script copies only the block above.) */
@supports (animation-range: contain) {
  @media (prefers-reduced-motion: no-preference) and (min-height: 480px) {
    :root:not(.stage-fallback) [data-scene] {
      animation: stage-progress linear both;
      animation-timeline: view();
      animation-range: contain 0% contain 100%;
    }
    [data-scroll-reveal]:not([data-stage] *) {
      animation: stage-rise linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 30%;
    }
    [data-scroll-reveal]:not([data-stage] *):focus-within { animation: none; }
  }
}
