/* ScrollVars: AGGREGATE stylesheet (generated from styles/*.css by
 * scripts/build-styles.mjs, edit the parts, not this file).
 * Modular imports ship less: styles/core.css alone covers entrances. */

/* 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;
}



/* 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;
}

/* ScrollVars: slider styles (the class is added by slider()). */

/* Slider: native rails + snap magnetism; the class is added by slider().
   Slides get --sd (signed distance from center, in slide widths) and
   .sv-active. Any CSS reading them animates the carousel. */
.sv-slider {
  display: flex;
  gap: var(--sv-gap, 16px);
  overflow-x: auto;
  scroll-snap-type: x var(--sv-snap, mandatory);
  overscroll-behavior-x: contain;
}
.sv-slider.sv-draggable {
  cursor: grab;
}
.sv-slider-y {
  flex-direction: column;
  overflow-x: hidden;
  overflow-y: auto;
  scroll-snap-type: y var(--sv-snap, mandatory);
  overscroll-behavior-y: contain;
}
.sv-slider > * {
  flex: 0 0 auto;
  scroll-snap-align: center;
}
.sv-slider.sv-dragging {
  scroll-snap-type: none;
  cursor: grabbing;
  user-select: none;
}
.sv-slider.sv-gliding {
  scroll-snap-type: none;
}


/* Columns: set --sv-per-view (fractional = peek) and breakpoints ARE media
   queries. Override the var per breakpoint (or via Tailwind:
   "[--sv-per-view:1.2] md:[--sv-per-view:2.5]"). Slides can span columns
   with --sv-span. Gate class sv-cols keeps natural sizing the default.
   sv-cols also works from the Slider shell (React's className prop lands
   on .sv-slider-shell, one level up from .sv-slider). */
.sv-slider.sv-cols > *,
.sv-slider-shell.sv-cols > .sv-slider > * {
  flex: 0 0
    calc(
      (100% - (var(--sv-per-view, 1) - 1) * var(--sv-gap, 16px)) / var(--sv-per-view, 1) *
        var(--sv-span, 1) +
        (var(--sv-span, 1) - 1) * var(--sv-gap, 16px)
    );
}

/* Chrome: arrows + dots with stable class hooks. Appearance and position
   are var knobs. Override them globally (:root / .sv-slider-shell) for the
   whole project, or on one slider (className/style) for that instance.
   Beyond the knobs, restyle the classes directly or pass custom icons /
   renderDot in React. */
.sv-slider-shell {
  position: relative;
}
.sv-arrow {
  position: absolute;
  top: var(--sv-arrow-top, 50%);
  translate: 0 -50%;
  z-index: 2;
  width: var(--sv-arrow-size, 44px);
  height: var(--sv-arrow-size, 44px);
  display: grid;
  place-items: center;
  border-radius: var(--sv-arrow-radius, 999px);
  border: 1px solid var(--sv-arrow-border, rgba(128, 128, 128, 0.35));
  background: var(--sv-arrow-bg, rgba(0, 0, 0, 0.35));
  color: var(--sv-arrow-color, currentColor);
  font-size: calc(var(--sv-arrow-size, 44px) * 0.4);
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.sv-arrow-prev {
  left: var(--sv-arrow-inset, 12px);
}
/* Autoplay pause/resume (APG carousel: a visible rotation control, first in
   the tab order). Same knob family as the arrows. */
.sv-pause {
  position: absolute;
  top: var(--sv-pause-inset, 10px);
  right: var(--sv-pause-inset, 10px);
  z-index: 2;
  width: var(--sv-pause-size, 32px);
  height: var(--sv-pause-size, 32px);
  display: grid;
  place-items: center;
  border-radius: var(--sv-arrow-radius, 999px);
  border: 1px solid var(--sv-arrow-border, rgba(128, 128, 128, 0.35));
  background: var(--sv-arrow-bg, rgba(0, 0, 0, 0.35));
  color: var(--sv-arrow-color, currentColor);
  font-size: calc(var(--sv-pause-size, 32px) * 0.38);
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.sv-arrow-next {
  right: var(--sv-arrow-inset, 12px);
}
.sv-dots {
  display: flex;
  flex-wrap: wrap; /* a crowded row wraps rather than shrinking the targets */
  gap: var(--sv-dot-gap, 8px);
  justify-content: var(--sv-dots-justify, center);
  margin-top: var(--sv-dots-offset, 14px);
}
/* The button is the target and the dot is drawn inside it: WCAG 2.5.8 asks
   24 by 24 CSS pixels of target, an 8px dot with 8px gaps had a third of
   that. --sv-dot-size still sizes what you see, --sv-dot-target what you hit. */
.sv-dot {
  display: grid;
  place-items: center;
  flex: none;
  width: var(--sv-dot-target, 24px);
  height: var(--sv-dot-target, 24px);
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
}
.sv-dot::before {
  content: '';
  width: var(--sv-dot-size, 8px);
  height: var(--sv-dot-size, 8px);
  border-radius: 999px;
  background: var(--sv-dot-color, rgba(128, 128, 128, 0.35));
  transition: background 300ms, scale 300ms;
}
.sv-dot.on::before {
  background: var(--sv-dot-active, currentColor);
  scale: 1.25;
}

@media (prefers-reduced-motion: reduce) {
  .sv-dot::before {
    transition: 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-dot::before {
  transition: none;
}
/* ScrollVars: pointer tilt preset (use with trackPointer / usePointer). */

/* ---- Pointer preset (use with trackPointer / usePointer) ---- */

.sv-tilt {
  --mx: 0;
  --my: 0;
  position: relative;
  overflow: hidden;
  transform: perspective(900px) rotateY(calc(var(--mx) * var(--sv-tilt, 14deg)))
    rotateX(calc(var(--my) * var(--sv-tilt, 14deg) * -1));
  transform-style: preserve-3d;
  transition: transform 140ms ease-out;
}
.sv-tilt.sv-pointer-leave {
  transition: transform 600ms var(--sv-ease, cubic-bezier(0.28, 0.84, 0.42, 1));
}
.sv-tilt::after {
  content: '';
  position: absolute;
  inset: -40%;
  background: radial-gradient(
    circle at center,
    rgba(255, 255, 255, 0.14),
    transparent 42%
  );
  translate: calc(var(--mx) * 38%) calc(var(--my) * 38%);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce), (hover: none) {
  /* .sv-tilt.sv-pointer-leave (line 15) matches the plain .sv-tilt selector's
     specificity here, so add it too: the override ties and wins on source
     order instead of losing to the leave-transition rule's extra class. */
  .sv-tilt,
  .sv-tilt.sv-pointer-leave {
    transform: none;
    transition: none;
  }
  .sv-tilt::after {
    display: 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-tilt,
:where([data-sv-motion="reduce"]) .sv-tilt.sv-pointer-leave {
  transform: none;
  transition: none;
}
:where([data-sv-motion="reduce"]) .sv-tilt::after {
  display: none;
}

/* ScrollVars: interaction-state presets (use with toggles(), the Popover
 * API, or any class you flip yourself. Including sv-live by hand, which
 * replays the whole entrance system on demand). */

/* Pop: entry/exit for popovers, dialogs and display-toggled panels via the
   modern discrete-transition trio (@starting-style + allow-discrete).
   Works with [popover] / <dialog> out of the box, or with the `sv-open`
   class that toggles() flips. */
.sv-pop {
  --sv-pop-distance: 0.75rem;
  --sv-pop-duration: 350ms;
  transition:
    opacity var(--sv-pop-duration) var(--sv-ease, ease),
    translate var(--sv-pop-duration) var(--sv-ease, ease),
    display var(--sv-pop-duration) allow-discrete,
    overlay var(--sv-pop-duration) allow-discrete;
}
[popover].sv-pop:not(:popover-open) {
  opacity: 0;
  translate: 0 var(--sv-pop-distance);
}
/* only where <dialog> is implemented: the type selector matches the unknown
   element too, and hiding THAT would break the documented static fallback
   panel (nothing else hides it there, no UA display:none) */
@supports selector(dialog:modal) {
  dialog.sv-pop:not([open]) {
    opacity: 0;
    translate: 0 var(--sv-pop-distance);
  }
}
.sv-pop:not([popover]):not(dialog):not(.sv-open) {
  display: none;
  opacity: 0;
  translate: 0 var(--sv-pop-distance);
}
@starting-style {
  [popover].sv-pop:popover-open,
  dialog.sv-pop[open],
  .sv-pop.sv-open {
    opacity: 0;
    translate: 0 var(--sv-pop-distance);
  }
}

/* Rotating words: a clipped column of words; set --sv-word (integer index)
   to slide one row per word. Old word exits up, the next rises from below.
   Discrete value + transition = smooth, the house idiom. */
.sv-words {
  display: inline-flex;
  flex-direction: column;
  /* baseline (the default) on an inline flex container exports the FIRST
     word's baseline. So the rotator aligns with surrounding text at any
     host line-height. (bottom only lined up when the host was ~1.15 too.) */
  vertical-align: baseline;
  height: 1.15em;
  overflow: hidden;
}
.sv-words > * {
  display: block;
  height: 1.15em;
  line-height: 1.15;
  translate: 0 calc(var(--sv-word, 0) * -1.15em);
  transition: translate var(--sv-duration, 650ms)
    var(--sv-ease, cubic-bezier(0.28, 0.84, 0.42, 1));
}

/* Acts: a time-driven MASTER CLOCK in pure CSS. The same slice idiom the
   scroll scenes use, but the clock is a registered custom property in
   transition. Trigger with a class: sv-open (toggles/click) or sv-live
   (scroll-triggered timed choreography. Zero JS). Define acts as slices:

     .stage  { --a1: clamp(0, var(--sv-act), 1);
               --a2: clamp(0, calc(var(--sv-act) - 1), 1); }
     .title  { opacity: var(--a1); translate: 0 calc((1 - var(--a1)) * 2rem); }
     .cards  { scale: calc(0.9 + var(--a2) * 0.1); }

   Knobs: --sv-acts-count (how many acts, default 3) and --sv-acts-duration
   (whole timeline, default 2s). Retiming is relative: change one number
   and every act rescales. Reversible and interruptible by nature: remove
   the class mid-flight and the transition RETARGETS instead of restarting.
   Needs @property (Chrome 85 / FF 128 / Safari 16.4); older browsers snap
   straight to the finished state. Content never hidden.
   --sv-acts-settle is internal, set only by toggles() during its boot
   settle (src/core/toggles.ts), which also holds an element's own inline
   transition-duration LONGHAND for the same settle. What remains unguarded:
   an author RULE, not an inline style, that overrides transition-duration
   or the whole transition shorthand on .sv-acts at higher specificity than
   this one still owns the settle timing instead of it. */
@property --sv-act {
  syntax: '<number>';
  initial-value: 0;
  inherits: true;
}
.sv-acts {
  --sv-act: 0;
  transition: --sv-act var(--sv-acts-settle, var(--sv-acts-duration, 2s)) linear;
}
/* sv-ui (set by toggles(), src/core/toggles.ts) marks each toggle TARGET
   (the trigger itself when there is no separate target): without the
   :not(.sv-ui) here, a click-only widget on a page whose scroll driver
   never boots would be frozen at the finished state forever instead of
   starting at act zero and animating on click. A widget the click driver
   does not control (plain scroll reveal, no toggle) is not marked, so it
   keeps the no-JS/no-boot fallback below. */
html:not(.sv-on) .sv-acts:not(.sv-ui) {
  --sv-act: var(--sv-acts-count, 3); /* no JS at all: the finished state, not act zero */
}
.sv-acts.sv-open {
  --sv-act: var(--sv-acts-count, 3);
}
/* Driven from the inherited --sv-live flag, like the entrance presets: the
   nearest tracker (not just any live ancestor) decides whether this acts
   clock is live, so a nested non-live tracker keeps its own clock at 0.
   :not(.sv-open) excludes an opened widget: without it this rule (0,3,0)
   outranks .sv-acts.sv-open (0,2,0) above, so an opened widget sitting
   inside (or itself a) non-live tracker would compute --sv-act: 0 instead
   of staying at the finished value. */
.sv-on :is(.sv, [data-sv]) .sv-acts:not(.sv-open),
.sv-on :is(.sv, [data-sv]).sv-acts:not(.sv-open) {
  --sv-act: calc(var(--sv-live) * var(--sv-acts-count, 3));
}

@media (prefers-reduced-motion: reduce) {
  .sv-pop,
  .sv-words > *,
  .sv-acts {
    transition-duration: 0.01ms;
  }
}

/* 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-pop,
:where([data-sv-motion="reduce"]) .sv-words > *,
:where([data-sv-motion="reduce"]) .sv-acts {
  transition-duration: 0.01ms;
}

/* ScrollVars: UI presets (marquee, accordion) for the React kit and
 * vanilla markup alike. */

/* Marquee: an infinite strip (logos, taglines). The track holds TWO copies
   of the content and slides one copy-width; knobs: --sv-marquee-duration
   (one loop) and --sv-gap. Pauses on hover by default
   (--sv-marquee-hover: running to disable). */
.sv-marquee {
  overflow: hidden;
  display: flex;
}
.sv-marquee-track {
  display: flex;
  align-items: center;
  gap: var(--sv-gap, 48px);
  width: max-content;
  padding-right: var(--sv-gap, 48px);
  animation: sv-marquee var(--sv-marquee-duration, 30s) linear infinite;
}
/* the duplicate copy: a class, not inline style, so reduced motion below
   overrides it without !important. */
.sv-marquee-track > .sv-marquee-dup {
  display: contents;
}
.sv-marquee:hover .sv-marquee-track,
.sv-marquee:focus-within .sv-marquee-track {
  animation-play-state: var(--sv-marquee-hover, paused);
}
/* While the track itself holds focus, present it statically and wrapped
   with the duplicate hidden (WCAG 2.4.7): pausing only stops the slide, and
   a focused item can sit translated off the strip for most of the loop.
   Keyed on the TRACK, not .sv-marquee: the controlled marquee's pause
   button is inside .sv-marquee but outside the track, so focusing it must
   not reflow the strip. */
.sv-marquee-track:focus-within {
  animation: none;
  width: auto;
  flex-wrap: wrap;
}
.sv-marquee-track:focus-within > .sv-marquee-dup {
  display: none;
}
@keyframes sv-marquee {
  to {
    translate: -50% 0;
  }
}

/* A controlled strip stays still until its pause button has been wired. */
.sv-marquee-controlled { position: relative; }
.sv-marquee-controlled > .sv-marquee-track:not(.sv-ui) {
  animation: none; width: auto; flex-wrap: wrap;
}
.sv-marquee-controlled > .sv-marquee-track:not(.sv-ui) > .sv-marquee-dup { display: none; }
.sv-marquee-controlled > .sv-marquee-track.sv-paused { animation-play-state: paused; }
/* Off screen or the tab in the background (toggles(), IntersectionObserver
   + visibilitychange): paused independently of the user's own pause
   button above, either one holds it. */
.sv-marquee-track.sv-marquee-offscreen { animation-play-state: paused; }
.sv-marquee-pause { display: none; }
.sv-marquee-track.sv-ui + .sv-marquee-pause {
  display: block; position: absolute; right: 8px; bottom: 8px; z-index: 1;
  padding: 6px 10px; border: 1px solid currentColor; border-radius: 4px;
  background: Canvas; color: CanvasText; font: inherit; font-size: 12px; cursor: pointer;
}
.sv-marquee-pause[aria-pressed="true"] { font-weight: bold; }

/* Accordion: native <details> animated: accessibility for free, exclusive
   groups via the native name attribute. Needs interpolate-size (Chrome 129+);
   older browsers open/close without the height animation. */
.sv-accordion {
  interpolate-size: allow-keywords;
}
.sv-accordion > summary {
  cursor: pointer;
}
.sv-accordion::details-content {
  opacity: 0;
  block-size: 0;
  overflow: hidden;
  transition:
    block-size var(--sv-duration, 350ms) var(--sv-ease, ease),
    opacity var(--sv-duration, 350ms) var(--sv-ease, ease),
    content-visibility var(--sv-duration, 350ms) allow-discrete;
}
.sv-accordion[open]::details-content {
  opacity: 1;
  block-size: auto;
}

@media (prefers-reduced-motion: reduce) {
  .sv-marquee-track {
    animation: none;
    width: auto;
    flex-wrap: wrap;
  }
  /* wins on source order, no !important; narrower than [aria-hidden] so
     a consumer's own aria-hidden child is not caught. */
  .sv-marquee-track > .sv-marquee-dup {
    display: none;
  }
  .sv-accordion::details-content {
    transition: 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-marquee-track {
  animation: none;
  width: auto;
  flex-wrap: wrap;
}
:where([data-sv-motion="reduce"]) .sv-marquee-track > .sv-marquee-dup {
  display: none;
}
:where([data-sv-motion="reduce"]) .sv-accordion::details-content {
  transition: none;
}
