/* ScrollVars: pinned presets (curtain, rail, deck, reading, counter).
 * Requires styles/core.css (or the aggregate styles.css) for the tokens. */

/* ---- Pin presets (continuous: track with { pin: true }) ---- */

/* Curtain: two panels sliding apart. Markup: a sticky full-screen stage with
   .sv-curtain-l / .sv-curtain-r panels over the revealed content. */
/* Registered so an engine with @property but no calc() division by a
   variable (Chrome 85-111 only) settles here at this value (1) below. */
@property --sv-r {
  syntax: '<number>';
  initial-value: 1;
  inherits: true;
}

.sv .sv-curtain-l {
  translate: calc(var(--sv-pin, 0) * -101%) 0;
}
.sv .sv-curtain-r {
  translate: calc(var(--sv-pin, 0) * 101%) 0;
}

/* Rail: horizontal travel on vertical scroll. Put on a max-content flex row
   inside a sticky stage. Starts one viewport offscreen to the right, ends
   right-edge aligned (min() keeps it moving even when the track fits). */
.sv .sv-rail {
  width: max-content;
  /* --sv-stage-width is measured by the driver; --sv-rail-start overrides it */
  translate: calc((1 - var(--sv-pin, 0)) * var(--sv-rail-start, var(--sv-stage-width, 100vw)) + var(--sv-pin, 0) * min(var(--sv-stage-width, 100vw) - 100%, 0px)) 0;
}

/* Deck: pinned card pile: each child flies away across its own slice of the
   pin. Children stack via grid (no absolute positioning needed); set
   --sv-count on the container to the number of cards (default 4). The first
   8 children are auto-indexed; beyond that set --sv-order manually. */
.sv .sv-deck {
  display: grid;
  place-items: center;
}
.sv .sv-deck > * {
  grid-area: 1 / 1;
  --sv-slice: clamp(0, calc(var(--sv-pin, 0) * var(--sv-count, 4) - var(--sv-order, 0)), 1);
  translate: 0 calc(var(--sv-order, 0) * 14px - var(--sv-slice) * 130vh);
  rotate: calc(var(--sv-slice) * -7deg);
  scale: calc(1 - var(--sv-order, 0) * 0.045 + var(--sv-slice) * 0.045);
}
.sv .sv-deck > :nth-child(1) { --sv-order: 0; z-index: 8; }
.sv .sv-deck > :nth-child(2) { --sv-order: 1; z-index: 7; }
.sv .sv-deck > :nth-child(3) { --sv-order: 2; z-index: 6; }
.sv .sv-deck > :nth-child(4) { --sv-order: 3; z-index: 5; }
.sv .sv-deck > :nth-child(5) { --sv-order: 4; z-index: 4; }
.sv .sv-deck > :nth-child(6) { --sv-order: 5; z-index: 3; }
.sv .sv-deck > :nth-child(7) { --sv-order: 6; z-index: 2; }
.sv .sv-deck > :nth-child(8) { --sv-order: 7; z-index: 1; }

/* Guided reading: children (word spans) lit progressively across the pin.
   Set --sv-count on the container to the word count and --sv-order on each
   span (0-based). The +3 lead keeps a few words warming up ahead. */
.sv .sv-reading > * {
  /* floor .55 keeps unread words at 4.5:1 on a dark theme (axe color-contrast);
     --sv-reading-floor: .13 for the dramatic look, at the cost of that audit */
  opacity: clamp(var(--sv-reading-floor, 0.55), calc(var(--sv-pin, 0) * (var(--sv-count, 1) + 3) - var(--sv-order, 0)), 1);
}

/* Counter: an integer counted up by the scroll, rendered without JS via
   @property + counter(). Set --sv-max on the element; the number appears
   as its ::after content. */
@property --sv-int {
  syntax: '<integer>';
  initial-value: 0;
  inherits: true;
}
/* :is(.sv, [data-sv]) here too: the no-JS guard below feeds --sv-int to
   attribute-only markup, whose digits come from these two rules */
:is(.sv, [data-sv]) .sv-counter {
  font-variant-numeric: tabular-nums;
  --sv-int: calc(var(--sv-pin, 0) * var(--sv-max, 100));
  counter-reset: sv-counter var(--sv-int);
}
:is(.sv, [data-sv]) .sv-counter::after {
  content: counter(sv-counter);
}

/* ---- Sequenced scrub (sv-range): per-child sub-range choreography ----
   The routine reason a timeline library gets added is not springs. It is
   "element A animates over 0–40% of the pin, B over 30–70%, C over 60–100%".
   sv-range derives a per-child 0..1 progress (--sv-r) from a slice of the
   parent clock: set --sv-from/--sv-to on each child. The clock is --sv-pin
   when pinned, else --sv-t (needs travel: true / data-sv-travel); override
   --sv-clock to choose explicitly.
   Needs calc() division (Chrome 112/Safari 16.4/FF 112) AND @property
   (Chrome 85/Safari 16.4/FF 128) together for full support; the two
   floors split at Chrome 85-111 (settles at the initial value, 1) and
   Firefox 112-127 (just animates). Use var(--sv-r, 1) anyway. */
/* the clock lives on the container at zero specificity: any author rule on it
   (`.mine { --sv-clock: var(--sv-t) }`) or an inline style wins */
:where(.sv .sv-range) {
  --sv-clock: var(--sv-pin, var(--sv-t, 0));
}
.sv .sv-range > * {
  --sv-r: clamp(
    0,
    calc((var(--sv-clock) - var(--sv-from, 0)) / (var(--sv-to, 1) - var(--sv-from, 0))),
    1
  );
}
/* Ready-made flavor: rise+fade each child across its own range. The fade
   starts at --sv-range-floor (.55, like sv-reading's floor): the sheet's
   text color stays at 4.5:1 on a dark ground while a child waits; a muted
   or accent color needs more, 0 is the dramatic look, at the cost of that
   audit. */
.sv .sv-range.sv-range-rise > * {
  opacity: calc(var(--sv-range-floor, 0.55) + (1 - var(--sv-range-floor, 0.55)) * var(--sv-r, 1));
  translate: 0 calc((1 - var(--sv-r, 1)) * var(--sv-distance, 2rem));
}

/* Pin stage: the sticky viewport inside a pinned wrapper. With the pin helper
   (data-sv-pin="320vh" / pin: '320vh' / <Track pin="320vh">) this class is the
   whole skeleton. Without JS (SSR markup) and under reduced motion it returns
   to flow instead of clipping. */
.sv-stage {
  position: sticky;
  top: var(--sv-pin-offset, 0px); /* a sticky header: `:root { --sv-pin-offset: 64px }` */
  height: calc(var(--sv-stage-height, 100vh) - var(--sv-pin-offset, 0px)); /* --sv-stage-height: an inner scroller's height when tracking with root */
  overflow: hidden;
}
html:not(.sv-on) .sv-stage {
  position: static;
  height: auto;
  overflow: visible;
}
/* Markup carries data-sv before the driver runs (scan() adds .sv with JS):
   nothing may hide or fly off, or stay stacked over the revealed content */
html:not(.sv-on) :is(.sv, [data-sv]) .sv-curtain-l,
html:not(.sv-on) :is(.sv, [data-sv]) .sv-curtain-r {
  display: none;
}
html:not(.sv-on) :is(.sv, [data-sv]) .sv-rail {
  translate: none;
  width: auto;
  flex-wrap: wrap;
}
html:not(.sv-on) :is(.sv, [data-sv]) .sv-deck {
  display: block;
}
html:not(.sv-on) :is(.sv, [data-sv]) .sv-deck > * {
  translate: none;
  rotate: none;
  scale: none;
}
html:not(.sv-on) :is(.sv, [data-sv]) .sv-reading > * {
  opacity: 1;
}
html:not(.sv-on) :is(.sv, [data-sv]) .sv-range > * {
  --sv-r: 1; /* the clock is 0 without the driver; the finished state, not opacity 0 */
}
html:not(.sv-on) :is(.sv, [data-sv]) .sv-counter {
  --sv-int: var(--sv-max, 100);
}

/* Plain-selector twins of the eight rules above (Firefox 72-77 has no :is()). */
html:not(.sv-on) .sv .sv-curtain-l,
html:not(.sv-on) [data-sv] .sv-curtain-l,
html:not(.sv-on) .sv .sv-curtain-r,
html:not(.sv-on) [data-sv] .sv-curtain-r {
  display: none;
}
html:not(.sv-on) .sv .sv-rail,
html:not(.sv-on) [data-sv] .sv-rail {
  translate: none;
  width: auto;
  flex-wrap: wrap;
}
html:not(.sv-on) .sv .sv-deck,
html:not(.sv-on) [data-sv] .sv-deck {
  display: block;
}
html:not(.sv-on) .sv .sv-deck > *,
html:not(.sv-on) [data-sv] .sv-deck > * {
  translate: none;
  rotate: none;
  scale: none;
}
html:not(.sv-on) .sv .sv-reading > *,
html:not(.sv-on) [data-sv] .sv-reading > * {
  opacity: 1;
}
html:not(.sv-on) .sv .sv-range > *,
html:not(.sv-on) [data-sv] .sv-range > * {
  --sv-r: 1;
}
html:not(.sv-on) .sv .sv-counter,
html:not(.sv-on) [data-sv] .sv-counter {
  --sv-int: var(--sv-max, 100);
}

/* RELEASED (`data-sv-off`, written when a tracker stops): the no-JS twin of
   every guard above, on an ancestor or on the element itself. One rule each,
   never a shared list: a parser without `:is()` drops the whole list. */
[data-sv-flow] .sv-stage,
[data-sv-off] .sv-stage,
.sv-stage[data-sv-flow],
.sv-stage[data-sv-off] {
  position: static;
  height: auto;
  overflow: visible;
}
[data-sv-flow] .sv-curtain-l,
[data-sv-off] .sv-curtain-l,
[data-sv-flow] .sv-curtain-r,
[data-sv-off] .sv-curtain-r,
.sv-curtain-l[data-sv-flow],
.sv-curtain-l[data-sv-off],
.sv-curtain-r[data-sv-flow],
.sv-curtain-r[data-sv-off] {
  display: none;
}
[data-sv-flow] .sv-rail,
[data-sv-off] .sv-rail,
.sv-rail[data-sv-flow],
.sv-rail[data-sv-off] {
  translate: none;
  width: auto;
  flex-wrap: wrap;
}
[data-sv-flow] .sv-deck,
[data-sv-off] .sv-deck,
.sv-deck[data-sv-flow],
.sv-deck[data-sv-off] {
  display: block;
}
[data-sv-flow] .sv-deck > *,
[data-sv-off] .sv-deck > *,
.sv-deck[data-sv-flow] > *,
.sv-deck[data-sv-off] > * {
  translate: none;
  rotate: none;
  scale: none;
}
[data-sv-flow] .sv-reading > *,
[data-sv-off] .sv-reading > *,
.sv-reading[data-sv-flow] > *,
.sv-reading[data-sv-off] > * {
  opacity: 1;
}
[data-sv-flow] .sv-range > *,
[data-sv-off] .sv-range > *,
.sv-range[data-sv-flow] > *,
.sv-range[data-sv-off] > * {
  --sv-r: 1;
}
[data-sv-flow] .sv-counter,
[data-sv-off] .sv-counter,
.sv-counter[data-sv-flow],
.sv-counter[data-sv-off] {
  --sv-int: var(--sv-max, 100);
}

/* Without individual transforms, release unsupported layouts. Compat keeps
   its animated rail and curtains pinned; static decks use data-sv-flow. */
@supports not (translate: 0) {
  html:not([data-sv-compat]) .sv-stage {
    position: static;
    height: auto;
    overflow: visible;
  }
  html:not([data-sv-compat]) .sv .sv-curtain-l,
  html:not([data-sv-compat]) .sv .sv-curtain-r {
    display: none;
  }
  html:not([data-sv-compat]) .sv .sv-rail {
    width: auto;
    flex-wrap: wrap;
  }
  .sv .sv-deck { display: block; }
}

/* ---- Accessibility ---- */

@media (prefers-reduced-motion: reduce) {
  .sv-stage {
    position: static;
    height: auto;
    overflow: visible;
  }
  :is(.sv, [data-sv]) .sv-counter {
    --sv-int: var(--sv-max, 100); /* the finished number, like every other scrub preset */
  }
  .sv .sv-curtain-l,
  .sv .sv-curtain-r {
    display: none;
  }
  .sv .sv-rail {
    translate: none;
    width: auto;
    flex-wrap: wrap;
  }
  .sv .sv-deck {
    display: block; /* stacked grid cells would overlap without the motion */
  }
  .sv .sv-deck > * {
    translate: none;
    rotate: none;
    scale: none;
  }
  .sv .sv-reading > * {
    opacity: 1;
  }
  .sv .sv-range > * {
    --sv-r: 1;
  }
}

/* The same under the page's own switch, html[data-sv-motion="reduce"]
   (core/motion.ts): a test keeps every rule in step with the block above. */
:where([data-sv-motion="reduce"]) .sv-stage {
  position: static;
  height: auto;
  overflow: visible;
}
:where([data-sv-motion="reduce"]) :is(.sv, [data-sv]) .sv-counter {
  --sv-int: var(--sv-max, 100);
}
:where([data-sv-motion="reduce"]) .sv .sv-curtain-l,
:where([data-sv-motion="reduce"]) .sv .sv-curtain-r {
  display: none;
}
:where([data-sv-motion="reduce"]) .sv .sv-rail {
  translate: none;
  width: auto;
  flex-wrap: wrap;
}
:where([data-sv-motion="reduce"]) .sv .sv-deck {
  display: block;
}
:where([data-sv-motion="reduce"]) .sv .sv-deck > * {
  translate: none;
  rotate: none;
  scale: none;
}
:where([data-sv-motion="reduce"]) .sv .sv-reading > * {
  opacity: 1;
}
:where([data-sv-motion="reduce"]) .sv .sv-range > * {
  --sv-r: 1;
}
