/*
 * Everything the scrubbing engine draws, and the two rules that keep the page
 * readable when it does not.
 *
 * Shipped as a file rather than injected by mount(): a stylesheet the project
 * imports sits inside the cascade the project owns, so any of it can be
 * overridden. Injected <style> would land last and win against the author.
 *
 * Every value worth changing is a custom property. Before the engine owned this
 * markup, restyling a beat meant editing utility classes in your own component;
 * that has to stay possible or the extraction is a downgrade. Set them anywhere
 * that wins the cascade — :root, the container, a media query.
 */

:root {
  /* Type */
  --st-beat-heading-size: clamp(1.75rem, 4vw, 3rem);
  --st-beat-body-size: clamp(1rem, 1.5vw, 1.125rem);
  --st-beat-heading-color: rgb(255 255 255 / 0.9);
  --st-beat-body-color: rgb(255 255 255 / 0.6);
  --st-beat-max-width: 36rem;
  --st-beat-gap: 0.75rem;

  /* The dark backing behind copy. Its strength is measured per frame at build
     time and applied at runtime; these only shape the falloff. */
  --st-scrim-mid-stop: 45%;
  --st-scrim-edge-stop: 75%;

  /* Chrome */
  --st-progress-height: 2px;
  --st-progress-track: rgb(0 0 0 / 0.3);
  --st-progress-fill: rgb(255 255 255 / 0.7);
  --st-hint-tracking: 0.35em;
  --st-hint-bg: rgb(0 0 0 / 0.45);
  --st-hint-color: rgb(255 255 255 / 0.8);

  --st-transition: 500ms;
}

/* --------------------------------------------------------------- overlays -- */

/*
 * One beat.
 *
 * Absolute inside the container, NOT fixed to the viewport. The container is
 * sticky, so while the hero is on screen the two look identical — and they stop
 * looking identical the moment the page has anything below the hero. A fixed
 * overlay belongs to the viewport rather than to the hero, so it keeps floating
 * over whatever comes next: the last heading sitting across a pricing table,
 * with the footage it was written for scrolled away above it.
 *
 * This relies on the container being a positioned element. Every template makes
 * it `position: sticky`, which is one; a hand-written page that does not will
 * see the overlays escape to whichever ancestor is.
 */
.st-beat {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  padding: 0 2rem;
  pointer-events: none;
  /* Set per beat by the engine from fadeOpacity. */
  opacity: 0;
}

/* Hidden rather than merely transparent: a transparent overlay still sits in
   the accessibility tree and still costs a compositor layer. */
.st-beat[data-hidden="true"] {
  visibility: hidden;
}

/*
 * Where a beat sits. `align` places it across the frame, `anchor` down it, and
 * the two are independent — that independence is load-bearing.
 *
 * Beats crossfade, so any two neighbours are on screen together for the whole
 * handoff. What keeps that readable rather than a double exposure is that they
 * are in different places. When `anchor: "bottom"` forced its own centring, two
 * bottom-anchored neighbours fell into the same box and faded through each
 * other — legible in neither state, and impossible to fix from the story file,
 * because `align` was the knob and it did nothing.
 */
.st-beat[data-align="left"] {
  justify-content: flex-start;
  text-align: left;
}
.st-beat[data-align="center"] {
  justify-content: center;
  text-align: center;
}
.st-beat[data-align="right"] {
  justify-content: flex-end;
  text-align: right;
}

/* Under the subject rather than across it. Horizontal placement still comes
   from `align` above, and scrimOpacity reads the same two bands. */
.st-beat[data-anchor="bottom"] {
  align-items: flex-end;
  padding-bottom: 6rem;
}

.st-beat__inner {
  position: relative;
  max-width: var(--st-beat-max-width);
}

/*
 * A radial gradient rather than a blurred box.
 *
 * A blur spreads the computed opacity over a wide halo, so almost none of it
 * lands behind the glyphs — the scrim measures correctly and then fails to
 * deliver. This holds full strength across the text and falls off past it.
 *
 * --st-scrim-opacity is written per frame by the engine.
 */
.st-beat__scrim {
  position: absolute;
  inset: -2.5rem -3rem;
  background: radial-gradient(
    ellipse at center,
    rgb(0 0 0 / var(--st-scrim-opacity, 0)) 0%,
    rgb(0 0 0 / calc(var(--st-scrim-opacity, 0) * 0.85)) var(--st-scrim-mid-stop),
    rgb(0 0 0 / 0) var(--st-scrim-edge-stop)
  );
}

.st-beat__text {
  position: relative;
  display: grid;
  gap: var(--st-beat-gap);
}

.st-beat__heading {
  margin: 0;
  font-size: var(--st-beat-heading-size);
  font-weight: 500;
  color: var(--st-beat-heading-color);
}

.st-beat__body {
  margin: 0;
  font-size: var(--st-beat-body-size);
  line-height: 1.6;
  color: var(--st-beat-body-color);
}

/* ---------------------------------------------------------------- chrome -- */

/*
 * Both of these carry their own dark backing rather than relying on the footage
 * behind them. They sit outside the measured scrim, so over bright frames a
 * plain white-on-transparent treatment disappears — and a scroll cue nobody can
 * see is the same as no cue at all.
 *
 * Absolute inside the container for the same reason the beats are: this chrome
 * reports the hero's progress, so it has to end where the hero ends rather than
 * ride the viewport down over the rest of the page.
 */
.st-progress {
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--st-progress-height);
  background: var(--st-progress-track);
  pointer-events: none;
}

.st-progress__fill {
  height: 100%;
  width: 0;
  background: var(--st-progress-fill);
  transition: width 75ms linear;
}

.st-hint {
  position: absolute;
  inset: auto 0 2rem 0;
  display: flex;
  justify-content: center;
  pointer-events: none;
  transition: opacity var(--st-transition);
}

.st-hint__pill {
  margin: 0;
  border-radius: 9999px;
  padding: 0.5rem 1rem;
  background: var(--st-hint-bg);
  color: var(--st-hint-color);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: var(--st-hint-tracking);
}

.st-loading {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.st-loading__pill {
  margin: 0;
  border-radius: 0.25rem;
  padding: 0.25rem 0.75rem;
  background: rgb(0 0 0 / 0.4);
  color: rgb(255 255 255 / 0.7);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------- the story -- */

/*
 * The outline is load-bearing twice: it is what assistive technology reads, and
 * it IS the page under reduced motion. Hidden here rather than by a framework's
 * utility class, so a template without that framework still hides it — and so
 * there is exactly ONE hiding mechanism. Two, interacting by source order, is
 * how it ends up visible on a page that should be scrubbing.
 */
.story-outline {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/*
 * Reduced motion.
 *
 * Scroll-scrubbing is motion driven by interaction, so it is turned off rather
 * than slowed down: the engine renders a single still and never starts its
 * decoder or its scroll listener.
 *
 * That would leave a page with no words on it, except the story is already
 * written out as prose for screen readers. Here it stops being
 * screen-reader-only and becomes the page — the same copy, in the same order,
 * that everyone else scrolls through.
 */
/*
 * Snapping the hero to its beats.
 *
 * Each anchor is a zero-size element the engine put at one beat's scroll
 * position, on a whole frame. `mandatory` is what makes the page always come to
 * rest on one: `proximity` never fires at these distances, because the gap
 * between two beats is most of a runway and far outside any browser's proximity
 * threshold.
 *
 * The rule under it is not optional. `mandatory` means the scroller must always
 * rest on a snap point, so with snap points only inside the hero there is
 * nowhere legal to stop below it and the browser drags the reader back to the
 * last anchor — every section, table and footer under the hero becomes
 * unreachable. Mandatory snapping is a whole-page commitment, so the siblings
 * after the runway become snap points too. That is the page below the hero, in
 * every template: the adapter renders its sections next to the runway, not
 * inside it.
 *
 * Opt out with `data-scrollytelling-snap="off"` on the html element. There has
 * to be an escape hatch: a page whose author wants readers to stop between
 * beats cannot get one back any other way, and a layout that puts its sections
 * somewhere other than beside the runway would otherwise trap them with no
 * warning.
 */
html:not([data-scrollytelling-snap="off"]) {
  scroll-snap-type: y mandatory;
}

.st-anchor {
  position: absolute;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
  scroll-snap-align: start;
}

/*
 * The anchors resolve `top` against this. Without it `100%` is the viewport and
 * every anchor collapses onto the top of the runway — all five beats snapping
 * to the same place, which looks like snapping that simply does not work.
 * Relative positioning does not disturb the sticky hero inside it.
 */
[data-scrollytelling-runway] {
  position: relative;
}

html:not([data-scrollytelling-snap="off"]) [data-scrollytelling-runway] ~ * {
  scroll-snap-align: start;
}

@media (prefers-reduced-motion: reduce) {
  /*
   * The runway is scroll distance for a scrub that is not going to happen.
   *
   * A server-rendered template sizes it before it can know the setting — the
   * height is in the HTML, and the media query is the only thing that ever
   * learns. Left alone, a visitor who asked for less motion gets the story as
   * prose and then seven screens of nothing under a stuck image: the one page
   * on the site with a scroll that does nothing.
   *
   * `!important` because the height is an inline style on the runway and the
   * poster, written by the adapter on the server. Nothing else beats those.
   */
  /*
   * No scrub, no runway, so no resting places to snap between — and snapping
   * a reader who asked for less motion is the opposite of what they asked for.
   */
  html {
    scroll-snap-type: none !important;
  }

  [data-scrollytelling-runway] {
    height: auto !important;
  }

  [data-scrollytelling] {
    position: static !important;
    height: auto !important;
  }

  /* The still, at its own size, below the prose — rather than a cropped fill
     of a viewport-high box that is no longer there. */
  [data-scrollytelling-poster] {
    position: static !important;
    inset: auto !important;
    height: auto !important;
  }

  .story-outline {
    position: static;
    width: auto;
    height: auto;
    max-width: 42rem;
    margin: 0 auto;
    padding: 4rem 1.5rem;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    display: grid;
    gap: 2.5rem;
  }

  .story-outline h1 {
    font-size: 1rem;
    font-weight: 500;
    letter-spacing: 0.35em;
    text-transform: uppercase;
    color: rgb(255 255 255 / 0.5);
  }

  .story-outline h2 {
    font-size: clamp(1.75rem, 4vw, 2.5rem);
    font-weight: 500;
    color: rgb(255 255 255 / 0.9);
  }

  .story-outline p {
    margin-top: 0.5rem;
    color: rgb(255 255 255 / 0.6);
    line-height: 1.6;
  }

  /* Whatever gets added to this page later is covered too. */
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
