/* Motion styles that pair with motion.js. Copy the parts you need and rename freely.
   Hidden "before" states only exist inside prefers-reduced-motion: no-preference, so with
   reduced motion (or without JavaScript marking things as shown) the content is simply there. */

:root {
  /* the whole palette: every entrance eases OUT, slowly; nothing bounces */
  --ease-out-sine: cubic-bezier(.39, .575, .565, 1);
  --ease-out-cubic: cubic-bezier(.215, .61, .355, 1); /* GSAP power2.out */
  --ease-out-quart: cubic-bezier(.165, .84, .44, 1);  /* GSAP power3.out */
  --ease-out-quint: cubic-bezier(.23, 1, .32, 1);     /* GSAP power4.out */
  --ease-out-expo: cubic-bezier(.19, 1, .22, 1);      /* GSAP expo.out */
  /* the other family: a short push off, then a long settle. Used for text sliding out of masks */
  --ease-soft: cubic-bezier(.35, 0, 0, 1);
  --ease-in-out-quart: cubic-bezier(.77, 0, .175, 1); /* GSAP power3.inOut */
  --ease-in-out-cubic: cubic-bezier(.645, .045, .355, 1); /* GSAP power2.inOut */
  /* the heaviest of that family: waits, pushes hard, settles long. Hovers and picture swaps at .75s */
  --ease-push: cubic-bezier(.65, .05, 0, 1);
  /* a gentle in-out with a long tail, for anything that changes the whole view: 1.5s open, .8s close */
  --ease-view: cubic-bezier(.29, .05, .06, .92);
  --ease-in-out-sine: cubic-bezier(.445, .05, .55, .95);
}

/* Put this on a wrapper element around the page (not on <body>: there it is handed to the
   viewport and stops clipping). 3D "before" states stick out sideways and would add a horizontal
   scrollbar on phones; clip (not hidden) hides that without breaking position: sticky inside. */
.mo-root { overflow-x: clip; }

.mo-word { display: inline-block; }
.mo-char { display: inline-block; }
/* a line that hides whatever is outside it; the padding keeps descenders (g, y) from being cut */
.mo-line { display: block; overflow: hidden; padding-bottom: .12em; margin-bottom: -.12em; }
.mo-chars { overflow: hidden; padding-bottom: .12em; margin-bottom: -.12em; }

/* slot-machine letters (slotLetters()): a one-line window onto columns of copies */
.mo-slot { display: inline-flex; overflow: hidden; height: 1.15em; line-height: 1.15; vertical-align: bottom; }
.mo-slot-col { display: flex; flex-direction: column; }
.mo-slot-col > span { height: 1.15em; }

/* odometer (odometer()): each column holds 0-9 and shows digit --n */
.mo-odometer { display: inline-flex; overflow: hidden; height: 1em; line-height: 1; font-variant-numeric: tabular-nums; }
.mo-odometer-col { display: flex; flex-direction: column; transform: translateY(calc(var(--n, 0) * -10%)); transition: transform .5s var(--ease-soft); }
.mo-odometer-col > span { height: 1em; }

/* flight (flight()): the travelling element; its size and place are set from JavaScript */
.mo-flyer { position: absolute; top: 0; left: 0; overflow: hidden; border-radius: 14px; will-change: transform; }
.mo-flyer > * { display: block; width: 100%; height: 100%; object-fit: cover; }

/* window for a picture that slides inside it; the moving part is added below, so that with
   reduced motion the picture just fills the window */
.mo-parallax { overflow: hidden; }
.mo-parallax > * { height: 100%; }

@media (prefers-reduced-motion: no-preference) {
  /* ---- title swing: the block turns in around a pivot 150px behind the text, words switch on in turn.
     The two translateZ() put the pivot behind the element without a wrapper; do not swap them for
     transform-origin with a z offset, which magnifies the element at rest when perspective() is
     in the same transform. */
  .mo-swing { opacity: 0; transform: perspective(1000px) translateZ(-150px) rotateY(-50deg) rotateX(-20deg) translateZ(150px); }
  .mo-swing[data-shown] { opacity: 1; transform: perspective(1000px) translateZ(-150px) rotateY(0deg) rotateX(0deg) translateZ(150px); transition: transform 1.5s var(--ease-out-quart); }
  .mo-swing .mo-word, .mo-caption .mo-word { opacity: 0; }
  .mo-swing[data-shown] .mo-word, .mo-caption[data-shown] .mo-word { opacity: 1; transition: opacity .08s linear calc(var(--i) * 70ms); }

  /* ---- text sliding out from behind its own line (splitLines() then reveal()).
     Add mo-alternate to bring every second line down from above instead. */
  .mo-lines .mo-word { transform: translateY(120%); }
  .mo-lines.mo-alternate .mo-line:nth-child(even) .mo-word { transform: translateY(-120%); }
  /* :nth-child(n) only lifts this rule above the alternate one */
  .mo-lines[data-shown] .mo-line:nth-child(n) .mo-word { transform: translateY(0); transition: transform 1s var(--ease-soft) calc(var(--i) * 80ms); }

  /* ---- letters rising with a slight turn (splitChars() then reveal()) */
  .mo-chars .mo-char { transform: translateY(1.1em) rotate(10deg); }
  .mo-chars[data-shown] .mo-char { transform: translateY(0) rotate(0deg); transition: transform .67s var(--ease-soft) calc(var(--i) * 40ms); }

  /* ---- slot-machine letters roll down into place, middle first */
  .mo-slot .mo-slot-col { transform: translateY(-100%); }
  .mo-slot[data-shown] .mo-slot-col { transform: translateY(0); transition: transform 1s var(--ease-in-out-quart) calc(var(--d) * 30ms); }

  /* ---- page-load entrance for hero pieces: set data-ready on <html> once the hero art is loaded.
     Give each piece its turn with style="--i: 0", "--i: 1", … */
  .mo-tilt-in { opacity: 0; transform: perspective(1000px) translateZ(-150px) rotateY(-20deg) rotateX(40deg) translateZ(150px); }
  [data-ready] .mo-tilt-in { opacity: 1; transform: perspective(1000px) translateZ(-150px) rotateY(0deg) rotateX(0deg) translateZ(150px); transition: opacity 1s var(--ease-out-quint) calc(var(--i, 0) * .1s), transform 1s var(--ease-out-quint) calc(var(--i, 0) * .1s); }
  .mo-rise-in { opacity: 0; transform: translateY(50px); }
  [data-ready] .mo-rise-in { opacity: 1; transform: none; transition: opacity 1s var(--ease-out-quint) calc(var(--i, 0) * .1s), transform 1s var(--ease-out-quint) calc(var(--i, 0) * .1s); }

  /* ---- cards flap up, hinged at their top edge (the hinge keeps the 3D box inside the layout box) */
  .mo-flap { opacity: 0; transform-origin: 50% 0; transform: perspective(1000px) translateY(100px) rotateX(-40deg); }
  .mo-flap[data-shown] { opacity: 1; transform: perspective(1000px) translateY(0) rotateX(0deg); transition: opacity 1.3s var(--ease-out-quint) var(--wait, 0ms), transform 1.3s var(--ease-out-quint) var(--wait, 0ms); }

  /* ---- smaller pieces rise and grow slightly */
  .mo-rise { opacity: 0; transform: translateY(50px) scale(.95); }
  .mo-rise[data-shown] { opacity: 1; transform: none; transition: opacity 1s var(--ease-out-cubic) var(--wait, 0ms), transform 1s var(--ease-out-cubic) var(--wait, 0ms); }

  /* ---- lean towards the pointer: tilt() sets --rx, --ry, --s on the hovered element */
  .mo-tilt { perspective: 500px; }
  .mo-tilt > * { transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) scale(var(--s, 1)); transition: transform .4s var(--ease-out-cubic); }

  /* ---- drift: drift() keeps --p, --mx, --my on the parent; children set their own depth --d */
  .mo-drift > * { translate: calc(var(--mx, 0) * var(--d, 1) * -20px) calc(var(--my, 0) * var(--d, 1) * -20px + (.5 - var(--p, .5)) * var(--d, 1) * 120px); will-change: translate; }

  /* ---- picture that slides inside its window as the window crosses the screen (--p from drift()
     or scrub()). The picture is a quarter taller than the window so its edges never show. */
  .mo-parallax > * { position: relative; top: -12.5%; height: 125%; transform: translateY(calc(var(--p, .5) * 20% - 10%)); }

  /* ---- header slides away on the way down (navHide()) */
  .mo-nav { transition: transform .5s; }
  .mo-nav[data-hidden] { transform: translateY(-110%); }

  /* ---- highlight wipe (splitLines() then reveal()): each line is uncovered left to right behind a
     coloured bar, which then lets go of its left edge and shrinks away to the right. --wipe is the
     bar's colour. Add mo-centre for centred text, mo-reverse to run right to left. */
  .mo-wipe .mo-line { position: relative; width: fit-content; clip-path: inset(0 100% 0 0); }
  .mo-wipe.mo-centre .mo-line { margin-inline: auto; }
  .mo-wipe .mo-line::after { content: ''; position: absolute; inset: 0; background: var(--wipe, currentColor); transform-origin: 100% 50%; }
  .mo-wipe.mo-reverse .mo-line { clip-path: inset(0 0 0 100%); }
  .mo-wipe.mo-reverse .mo-line::after { transform-origin: 0 50%; }
  .mo-wipe[data-shown] .mo-line:nth-child(n) { clip-path: inset(0 0 0 0); transition: clip-path .6s var(--ease-out-cubic) calc(var(--line) * 150ms + var(--wait, 0ms)); }
  .mo-wipe[data-shown] .mo-line::after { transform: scaleX(0); transition: transform .57s var(--ease-in-out-cubic) calc(var(--line) * 150ms + 370ms + var(--wait, 0ms)); }

  /* ---- a full-bleed layer that closes down to a card while its runway is pinned (progress() with
     range 'pin' on the runway; the layer fills the sticky stage). It finishes part-way through the
     pin (--shrink-by 2 = by half way) and then holds. --shrink-x / --shrink-y: how much is cut
     from each side. The child eases back so more of the picture fits the card. */
  .mo-shrink { --q: min(1, var(--p, 0) * var(--shrink-by, 2)); clip-path: inset(calc(var(--q) * var(--shrink-y, 30%)) calc(var(--q) * var(--shrink-x, 33%)) round calc(var(--q) * 18px)); }
  .mo-shrink > * { transform: scale(calc(1 - var(--q) * .4)); }

  /* ---- a curved lower edge that drops like a visor as the element crosses the screen (progress()) */
  .mo-visor { clip-path: ellipse(var(--visor, 70%) calc(28% + min(1, var(--p, 1) * 2) * 72%) at 50% 0%); }

  /* ---- staircase (stairs()): column --col sits that many steps lower, and the step shrinks to
     nothing across the pass. The padding leaves room for the columns that hang low at first. */
  .mo-stairs { padding-bottom: calc(var(--step, 72px) * 2); }
  .mo-stairs > * { translate: 0 calc(var(--col, 0) * var(--step, 72px) * (1 - var(--p, 1))); }

  /* ---- covered stage: the section after a .mo-runway slides up over its pinned stage during the
     last screen of the pin. Give that section a background. */
  .mo-cover { margin-top: -100vh; margin-top: -100lvh; }

  /* ---- slanted seam: a section whose top edge comes up tilted and levels as it reaches the top
     of the screen (progress(section, { range: 'enter' })). Use alone or together with mo-cover. */
  .mo-slant { clip-path: polygon(0 calc((1 - var(--p, 1)) * var(--slant, 16vw)), 100% 0, 100% 100%, 0 100%); }

  /* ---- drag to spin (spin() writes --spin) */
  .mo-spin { transform: perspective(900px) rotateY(var(--spin, 0deg)); cursor: grab; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
  .mo-spin[data-dragging] { cursor: grabbing; }

  /* ---- lean with the scroll speed (velocity() writes --v, -1 … 1); --skew is the lean at full speed */
  .mo-skew { transform: skewY(calc(var(--v, 0) * var(--skew, -3deg))); }
}
.mo-cover { position: relative; z-index: 1; }

/* ---- pinned stage: a tall runway with a screen-high sticky stage inside. frame() and steps() read
        the scroll through the runway. lvh keeps the stage from resizing when a phone's address
        bar slides away; the runway's height sets how long the pin lasts (250vh = 150vh of pin). */
.mo-runway { height: 250vh; }
.mo-stage { position: sticky; top: 0; height: 100vh; height: 100lvh; overflow: hidden; }
.mo-mask { position: absolute; inset: 0; }
@media (prefers-reduced-motion: reduce) {
  /* no pinning: the stage becomes an ordinary block in the flow (still positioned, so layers
     placed absolutely inside it stay inside it) */
  .mo-runway { height: auto; }
  .mo-stage { position: relative; height: auto; min-height: 60vh; }
  .mo-mask { position: relative; min-height: 60vh; }
}

/* ---- pinned steps (steps()): the active one is full strength and open */
.mo-steps > * { opacity: .4; transition: opacity .5s; }
.mo-steps > [data-active] { opacity: 1; }
.mo-step-body { max-height: 0; overflow: hidden; transition: max-height .5s var(--ease-out-cubic); }
.mo-steps > [data-active] .mo-step-body { max-height: 14em; }

/* ---- theme by scroll (themeOnScroll()): the background snaps, the text follows */
.mo-themed { transition: color .5s, background-color .15s ease-out; }

/* ---- button whose label is replaced by a copy rising from below:
        <a class="mo-btn"><span>Label</span><span aria-hidden="true">Label</span></a> */
.mo-btn { position: relative; display: inline-block; }
.mo-btn > span { display: block; transition: opacity .1s; }
.mo-btn > span + span { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0; transform: translateY(8px); transition: opacity .2s var(--ease-out-sine), transform .5s var(--ease-out-quart); }
@media (hover: hover) {
  .mo-btn:hover > span:first-child { opacity: 0; }
  .mo-btn:hover > span + span { opacity: 1; transform: none; }
}

/* ---- button flooded by its own dot: <a class="mo-flood"><i></i><span>Label</span><b>→</b></a>
        --flood is the fill colour, --flood-ink the label colour on top of it */
.mo-flood { position: relative; display: inline-flex; align-items: center; gap: .8em; overflow: hidden; padding: .9em 1.5em .9em 1.3em; border-radius: 999px; background: #fff; color: #111; text-decoration: none; transition: background .2s var(--ease-soft); }
.mo-flood > i { width: .45em; height: .45em; border-radius: 50%; background: currentColor; transition: background .5s var(--ease-soft), transform .4s var(--ease-soft); }
.mo-flood > span { position: relative; transition: color .5s var(--ease-soft), transform .4s var(--ease-soft); }
.mo-flood > b { position: absolute; right: 1.2em; font-weight: inherit; color: var(--flood-ink, #fff); transform: translateX(3.2em); transition: transform .4s cubic-bezier(.4, 0, 0, 1); }
@media (hover: hover) {
  .mo-flood:hover { background: var(--flood, #5b47ff); transition: background .5s var(--ease-soft) .3s; }
  .mo-flood:hover > i { background: var(--flood, #5b47ff); transform: translateX(3.2em) scale(30); }
  .mo-flood:hover > span { color: var(--flood-ink, #fff); transform: translateX(-1.25em); }
  .mo-flood:hover > b { transform: none; }
}

/* ---- link whose label rolls up and is replaced by a copy from below:
        <a class="mo-roll"><span>Label</span><span aria-hidden="true">Label</span></a> */
.mo-roll { position: relative; display: inline-block; overflow: hidden; vertical-align: bottom; }
.mo-roll > span { display: block; transition: transform .4s cubic-bezier(.4, 0, .1, 1); }
.mo-roll > span + span { position: absolute; inset: 0; transform: translateY(100%); }
@media (hover: hover) {
  .mo-roll:hover > span:first-child { transform: translateY(-100%); }
  .mo-roll:hover > span + span { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .mo-odometer-col { transition: none; }
}

/* ---- sliding pill behind the hovered nav item (navPill()); left, right, opacity in that order */
.mo-pill { position: absolute; top: 0; bottom: 0; border-radius: 999px; opacity: 0; pointer-events: none; transition: left .45s var(--ease-out-quart), right .45s var(--ease-out-quart), opacity .2s; }

/* ---- elliptical curtain: a panel uncovered by an ellipse dropping from its top edge. Toggle
        data-open (menus, drawers). Closed, it is also hidden from the keyboard. */
.mo-curtain { clip-path: ellipse(120% 0% at 50% 0%); visibility: hidden; transition: clip-path .8s var(--ease-out-quart), visibility 0s .8s; }
.mo-curtain[data-open] { clip-path: ellipse(120% 100% at 50% 20%); visibility: visible; transition: clip-path .8s var(--ease-out-quart); }

/* ---- hover swap: <a class="mo-swap"><img><img class="mo-swap-top"></a>. The second picture drops
        in through the same ellipse while the first pushes forward. */
.mo-swap { position: relative; display: block; overflow: hidden; }
.mo-swap > * { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .75s var(--ease-push), clip-path .75s var(--ease-push); }
.mo-swap > .mo-swap-top { position: absolute; inset: 0; clip-path: ellipse(100% 0% at 50% 0%); transform: scale(1.05); }
@media (hover: hover) {
  .mo-swap:hover > :first-child { transform: scale(1.1); }
  .mo-swap:hover > .mo-swap-top { clip-path: ellipse(100% 120% at 50% 0%); transform: scale(1); }
}

/* ---- letters that roll up one after another on hover, each replaced by its own copy from below
        (splitChars() first; one line only). The copy is a text-shadow one line down, so there is
        no extra markup. Hovering a link or button around it counts too. */
.mo-roll-chars { display: inline-block; overflow: hidden; height: 1.2em; line-height: 1.2; vertical-align: bottom; }
.mo-roll-chars .mo-char { text-shadow: 0 1.2em 0 currentColor; transition: transform .4s var(--ease-push) calc(var(--i) * 15ms); }
@media (hover: hover) {
  .mo-roll-chars:hover .mo-char, :is(a, button):hover .mo-roll-chars .mo-char { transform: translateY(-1.2em); }
}

/* ---- sideways gallery (sideways()): an ordinary column until the helper switches it on */
.mo-sideways-track { display: flex; flex-direction: column; gap: 24px; }
.mo-sideways[data-on] > .mo-sideways-stage { position: sticky; top: 0; display: flex; align-items: center; height: 100vh; height: 100lvh; overflow: hidden; }
.mo-sideways[data-on] .mo-sideways-track { flex-direction: row; align-items: center; width: max-content; transform: translate3d(var(--x, 0px), 0, 0); will-change: transform; }

/* ---- colour blended by scroll (progress() writes --p): --blend-from to --blend-to. The squared
        term puts most of the change early. */
.mo-blend { --blend: calc((1 - (1 - var(--p, 0)) * (1 - var(--p, 0))) * 100%); background-color: color-mix(in srgb, var(--blend-to, #f4f4ed) var(--blend), var(--blend-from, #282c20)); }

/* ---- card fan (fan()): a plain wrapping row until the helper switches it on. --spread is the
        sideways distance per place, --drop how fast the arc falls away, --turn the angle per
        place, --start how far below the stack begins, --room how far neighbours move aside. */
.mo-fan { display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; }
.mo-fan[data-on] { display: grid; place-items: center; gap: 0; }
.mo-fan[data-on] > * {
  grid-area: 1 / 1;
  z-index: calc(10 - var(--a));
  translate: calc(var(--k) * var(--spread, clamp(2.5rem, 10vw, 11rem)) * var(--p, 1)) calc(var(--a) * var(--a) * var(--drop, .8rem) * var(--p, 1) + (1 - var(--p, 1)) * var(--start, 8rem));
  rotate: calc(var(--k) * var(--turn, 7deg) * var(--p, 1));
  transform: translateX(calc(var(--push, 0) * var(--room, 6rem)));
  transition: transform .5s var(--ease-out-quart);
}
.mo-fan[data-on] > [data-lift] { z-index: 20; transform: translateY(-2rem) scale(1.08); }

/* ---- marquee (marquee()): a wrapping row until the helper switches it on. --gap spaces the
        items; --push is how far a full pass of scrolling shifts the row (negative: with the scroll). */
.mo-marquee { display: flex; flex-wrap: wrap; gap: var(--gap, 2em); }
.mo-marquee[data-on] { display: block; overflow: hidden; }
.mo-marquee-track { display: flex; width: max-content; margin-left: calc(-1 * var(--w, 0px)); translate: calc((var(--p, .5) - .5) * var(--push, -20vw)) 0; animation: mo-marquee var(--time, 30s) linear infinite; }
.mo-marquee-set { display: flex; flex: none; gap: var(--gap, 2em); padding-right: var(--gap, 2em); white-space: nowrap; }
.mo-marquee[data-reverse] .mo-marquee-track { animation-direction: reverse; }
.mo-marquee[data-paused] .mo-marquee-track { animation-play-state: paused; }
@keyframes mo-marquee { to { transform: translateX(calc(-1 * var(--w, 0px))); } }

/* ---- peek (peek()): <div class="mo-peek"><img><img class="mo-peek-top"></div>. The top picture
        shows only through three soft spots that trail the pointer; with no mouse it stays hidden. */
.mo-peek { position: relative; overflow: hidden; }
.mo-peek-top {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; pointer-events: none;
  --spot: #000 55%, transparent 100%;
  -webkit-mask-image: radial-gradient(circle var(--r1, 0px) at var(--x1, 0px) var(--y1, 0px), var(--spot)), radial-gradient(circle var(--r2, 0px) at var(--x2, 0px) var(--y2, 0px), var(--spot)), radial-gradient(circle var(--r3, 0px) at var(--x3, 0px) var(--y3, 0px), var(--spot));
  mask-image: radial-gradient(circle var(--r1, 0px) at var(--x1, 0px) var(--y1, 0px), var(--spot)), radial-gradient(circle var(--r2, 0px) at var(--x2, 0px) var(--y2, 0px), var(--spot)), radial-gradient(circle var(--r3, 0px) at var(--x3, 0px) var(--y3, 0px), var(--spot));
}

/* ---- helix of cards (helix()): an ordinary list until the helper switches it on.
        --step is the angle between cards, --r the radius, --rise the drop from one card to the
        next; the runway's height gives every card about two thirds of a screen of scrolling. */
.mo-helix-ring { display: grid; gap: 24px; }
.mo-helix[data-on] { --step: 50deg; --r: min(62vw, 560px); --rise: 90px; height: calc(100vh + var(--n, 6) * 65vh); }
.mo-helix[data-on] > .mo-stage { perspective: 1400px; }
/* until the helper is running the stage is not pinned and nothing is cut off: a list taller than
   the screen inside a screen-high stage with overflow hidden would otherwise lose its lower part */
.mo-helix:not([data-on]) > .mo-stage { position: relative; height: auto; overflow: visible; }
.mo-helix[data-on] .mo-helix-ring { position: absolute; inset: 0; display: block; transform-style: preserve-3d; transform: translateZ(calc(var(--r) * -1)) translateY(calc(var(--t, 0) * var(--rise) * -1)) rotateY(calc(var(--t, 0) * var(--step))); will-change: transform; }
.mo-helix[data-on] .mo-helix-ring > * { position: absolute; top: 50%; left: 50%; backface-visibility: hidden; transform: translate(-50%, -50%) rotateY(calc(var(--i) * var(--step) * -1)) translateY(calc(var(--i) * var(--rise))) translateZ(var(--r)); }

/* ---- one video at a time (oneVideo()): <div class="mo-video"><img><video data-src="…"></video></div>.
        The still shows until this one's video is really playing, then the video fades in over it. */
.mo-video { position: relative; overflow: hidden; }
.mo-video > img, .mo-video > video { display: block; width: 100%; height: 100%; object-fit: cover; }
.mo-video > video { position: absolute; inset: 0; opacity: 0; transition: opacity .5s var(--ease-out-sine) .3s; }
.mo-video[data-playing] > video { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .mo-curtain, .mo-curtain[data-open], .mo-swap > *, .mo-roll-chars .mo-char, .mo-fan[data-on] > *, .mo-video > video { transition: none; }
}
