/* ScrollVars: preset styles.
 *
 * Naming: everything is namespaced `sv-` (scrollvars). Presets read the
 * variables the driver writes; any CSS of your own that reads them is an
 * equally valid preset.
 *
 * Driver output:            Knobs you set:
 *   --sv-view  -1..1          --sv-distance  travel length (default 6rem)
 *   --sv-t      0..1          --sv-order     stagger position (0, 1, 2…)
 *   --sv-scene  0..N-1        --sv-stagger   delay step (default 90ms)
 *   .sv-live   (class)        --sv-duration / --sv-ease
 *
 * All hiding rules are gated on `html.sv-on` (added by the driver), so if
 * JavaScript never runs the page stays fully visible.
 */

/* Knob defaults sit at zero specificity, so an author `:root { --sv-stagger: 120ms }`
   wins whatever the import order. The driver outputs reset per tracked element. */
:where(:root) {
  --sv-distance: 6rem;
  --sv-ease: cubic-bezier(0.28, 0.84, 0.42, 1);
  --sv-duration: 800ms;
  --sv-stagger: 90ms;
}
.sv {
  --sv-view: 0;
  --sv-t: 0;
  --sv-scene: 0;
  --sv-pin: initial; /* a nested .sv never reads an ancestor's pin clock */
}

/* ---- Entrance presets (binary). The nearest tracker owns --sv-live (0 or 1)
   and every entrance child animates from it, so a live ancestor can never
   reveal a child of a nested tracker that is not live yet, and server markup
   that carries data-sv hides before scan() has run. ---- */

.sv,
[data-sv] {
  --sv-live: 0;
}
.sv.sv-live {
  --sv-live: 1;
}

.sv-on :is(.sv, [data-sv]) :is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r),
.sv-on :is(.sv, [data-sv]):is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r),
.sv-on :is(.sv, [data-sv]).sv-auto > :not(.sv-skip) {
  opacity: var(--sv-live);
  transition:
    opacity var(--sv-duration) var(--sv-ease),
    translate var(--sv-duration) var(--sv-ease);
  transition-delay: calc(var(--sv-order, 0) * var(--sv-stagger));
}
/* A self-tracked sv-rise/auto child feeds translate into its own live
   read; :where() excludes it free. */
.sv-on :is(.sv, [data-sv]) .sv-rise:where(:not(.sv):not([data-sv])),
.sv-on :is(.sv, [data-sv]).sv-auto > :not(.sv-skip):where(:not(.sv):not([data-sv])) {
  translate: 0 calc((1 - var(--sv-live)) * var(--sv-distance));
}
.sv-on :is(.sv, [data-sv]) .sv-slide-l,
.sv-on :is(.sv, [data-sv]).sv-slide-l {
  translate: calc((1 - var(--sv-live)) * var(--sv-distance) * -2) 0;
}
.sv-on :is(.sv, [data-sv]) .sv-slide-r,
.sv-on :is(.sv, [data-sv]).sv-slide-r {
  translate: calc((1 - var(--sv-live)) * var(--sv-distance) * 2) 0;
}

/* ---- sv-split: text broken into word/char spans (split()/data-sv-split/
   <Split>). Spans carry --sv-order, the container --sv-count, so every
   order-driven preset works on text. sv-split-rise = staggered entrance. */
/* Non-replaced inline boxes ignore `translate`; the first child is the
   screen-reader span (position absolute), left untouched. */
.sv-split > span[aria-hidden] {
  display: inline-block;
}
.sv-on :is(.sv, [data-sv]) .sv-split-rise > span,
.sv-on :is(.sv, [data-sv]).sv-split-rise > span {
  opacity: var(--sv-live);
  translate: 0 calc((1 - var(--sv-live)) * 0.6em);
  transition:
    opacity var(--sv-duration) var(--sv-ease),
    translate var(--sv-duration) var(--sv-ease);
  transition-delay: calc(var(--sv-order, 0) * var(--sv-stagger));
}

/* Automatic stagger order for sv-auto (and manual lists using sv-stagger).
   :nth-child(1) resets first so it never inherits an ancestor's order. */
.sv-auto > :nth-child(1),  .sv-stagger > :nth-child(1)  { --sv-order: 0; }
.sv-auto > :nth-child(2),  .sv-stagger > :nth-child(2)  { --sv-order: 1; }
.sv-auto > :nth-child(3),  .sv-stagger > :nth-child(3)  { --sv-order: 2; }
.sv-auto > :nth-child(4),  .sv-stagger > :nth-child(4)  { --sv-order: 3; }
.sv-auto > :nth-child(5),  .sv-stagger > :nth-child(5)  { --sv-order: 4; }
.sv-auto > :nth-child(6),  .sv-stagger > :nth-child(6)  { --sv-order: 5; }
.sv-auto > :nth-child(7),  .sv-stagger > :nth-child(7)  { --sv-order: 6; }
.sv-auto > :nth-child(8),  .sv-stagger > :nth-child(8)  { --sv-order: 7; }
.sv-auto > :nth-child(9),  .sv-stagger > :nth-child(9)  { --sv-order: 8; }
.sv-auto > :nth-child(10), .sv-stagger > :nth-child(10) { --sv-order: 9; }
.sv-auto > :nth-child(n + 11), .sv-stagger > :nth-child(n + 11) { --sv-order: 10; }

/* ---- Drift preset (continuous: follows the scroll, NO transition) ---- */

.sv .sv-drift {
  --sv-abs-view: max(var(--sv-view), calc(var(--sv-view) * -1));
  opacity: calc(1 - var(--sv-abs-view));
  translate: 0 clamp(-100vh, calc(var(--sv-view) * var(--sv-distance) * -1), 100vh);
}
/* A focused control keeps the scroll fade otherwise (WCAG 2.4.7): a fully
   visible edge case reads opacity 0.51, never hidden but still dim. Must
   sit after the rule above (source-order wins at equal specificity). */
.sv .sv-drift:focus-within {
  opacity: 1;
}

/* ---- Spread preset: a centered deck that fans out into its row ---- */
/* Children start stacked at the container's center (slight fan) and spread
   into their natural flex slots. Two clocks drive it:
     entrance: add .sv-spread-in: spreads (with transition + stagger) the
                moment the tracked section goes live
     scrub. Map --sv-spread from a driver variable yourself, e.g.
                .mine > * { --sv-spread: clamp(0, calc(var(--sv-t) * 2), 1); }
   Index children with .sv-stagger (auto --sv-order) or set --sv-order by
   hand; set --sv-mid to (N−1)/2 on the container (default 1 = 3 cards). */
.sv-spread {
  display: flex;
  gap: var(--sv-gap, 16px);
  justify-content: center;
}
.sv-spread > * {
  --sv-d: calc(var(--sv-order, 0) - var(--sv-mid, 1));
  translate: calc(var(--sv-d) * (var(--sv-spread, 0) - 1) * (100% + var(--sv-gap, 16px))) 0;
  rotate: calc(var(--sv-d) * (1 - var(--sv-spread, 0)) * -5deg);
}
/* SSR markup carries .sv before any JS runs: without the driver the cards stay in flow */
html:not(.sv-on) .sv-spread > * {
  translate: none;
  rotate: none;
}
/* Same for a RELEASED tracker (`data-sv-off`, see pin.css), on an ancestor or
   on the spread itself, which is where the marker lands in the scrub idiom. */
[data-sv-flow] .sv-spread > *,
[data-sv-off] .sv-spread > *,
.sv-spread[data-sv-flow] > *,
.sv-spread[data-sv-off] > * {
  translate: none;
  rotate: none;
}
.sv-on .sv .sv-spread.sv-spread-in > *,
.sv-on .sv.sv-spread.sv-spread-in > * {
  transition:
    translate var(--sv-duration, 800ms) var(--sv-ease, ease),
    rotate var(--sv-duration, 800ms) var(--sv-ease, ease);
  transition-delay: calc(var(--sv-order, 0) * var(--sv-stagger, 90ms));
}
/* Driven from the inherited --sv-live flag, like the entrance presets: the
   nearest tracker (not just any live ancestor) decides whether this spread
   is live, so a nested non-live tracker keeps its own spread stacked. The
   self-tracked case (the tracked element IS the spread) needs its own
   compound selector, same gap the entrance presets already close above. */
.sv-on :is(.sv, [data-sv]) .sv-spread.sv-spread-in > *,
.sv-on :is(.sv, [data-sv]).sv-spread.sv-spread-in > * {
  --sv-spread: var(--sv-live);
}

/* ---- Pure CSS tier: zero JS where animation-timeline is supported ---- */

@keyframes sv-in-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes sv-in-rise {
  from { opacity: 0; translate: 0 4rem; }
  to   { opacity: 1; translate: 0 0; }
}

@supports (animation-timeline: view()) {
  .sv-view-fade,
  .sv-view-rise {
    animation: sv-in-fade linear both;
    animation-timeline: view();
    animation-range: entry 10% entry 70%;
  }
  .sv-view-rise {
    animation-name: sv-in-rise;
  }
  .sv-view-fade:focus-within,
  .sv-view-rise:focus-within {
    animation: none;
    opacity: 1;
    translate: none;
  }
}

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

@media (prefers-reduced-motion: reduce) {
  /* Matches the animating rule's selector shape (line 127) so the override
     ties on specificity and wins on source order, not just on the ambient
     .sv-spread > * rule it also covers. */
  .sv-spread > *,
  .sv-on .sv .sv-spread.sv-spread-in > *,
  .sv-on .sv.sv-spread.sv-spread-in > * {
    translate: none;
    rotate: none;
    transition: none;
  }
  .sv-on :is(.sv, [data-sv]) .sv-split-rise > span,
  .sv-on :is(.sv, [data-sv]).sv-split-rise > span,
  .sv-on :is(.sv, [data-sv]) :is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r, .sv-drift),
  .sv-on :is(.sv, [data-sv]):is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r, .sv-drift),
  .sv-on :is(.sv, [data-sv]).sv-auto > :not(.sv-skip),
  .sv-view-fade,
  .sv-view-rise {
    transition: none;
    animation: none;
    opacity: 1;
    translate: none;
  }
}

/* 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-spread > *,
.sv-on:where([data-sv-motion="reduce"]) .sv .sv-spread.sv-spread-in > *,
.sv-on:where([data-sv-motion="reduce"]) .sv.sv-spread.sv-spread-in > * {
  translate: none;
  rotate: none;
  transition: none;
}
.sv-on:where([data-sv-motion="reduce"]) :is(.sv, [data-sv]) .sv-split-rise > span,
.sv-on:where([data-sv-motion="reduce"]) :is(.sv, [data-sv]).sv-split-rise > span,
.sv-on:where([data-sv-motion="reduce"]) :is(.sv, [data-sv]) :is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r, .sv-drift),
.sv-on:where([data-sv-motion="reduce"]) :is(.sv, [data-sv]):is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r, .sv-drift),
.sv-on:where([data-sv-motion="reduce"]) :is(.sv, [data-sv]).sv-auto > :not(.sv-skip),
:where([data-sv-motion="reduce"]) .sv-view-fade,
:where([data-sv-motion="reduce"]) .sv-view-rise {
  transition: none;
  animation: none;
  opacity: 1;
  translate: none;
}

/* A focused entrance child must be visible NOW, not after its stagger delay
   and transition run (WCAG 2.4.7). This must sit AFTER the entrance rules
   above (same source-order trap as the reduced-motion resets): at equal or
   higher specificity the later rule wins. :focus-within matches the element
   itself too, so one selector covers both a focused entrance item and one
   that contains the focused control. The self-tracked case (the tracked
   element IS the preset, e.g. `<pre class="sv sv-rise" tabindex="0">`)
   needs its own selector: :is(.sv, [data-sv]) as a DESCENDANT combinator
   above never matches when there is no separate ancestor. */
.sv-on :is(.sv, [data-sv]) :is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r):focus-within,
.sv-on :is(.sv, [data-sv]):is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r):focus-within,
.sv-on :is(.sv, [data-sv]).sv-auto > :not(.sv-skip):focus-within,
.sv-on :is(.sv, [data-sv]) .sv-split-rise:focus-within > span,
.sv-on :is(.sv, [data-sv]).sv-split-rise:focus-within > span {
  transition: none;
  opacity: 1;
  translate: none;
}


