/* motion.css — tokens, states and page transition of the motion kit (API and rules: README.md).
   Everything here animates transform, opacity (and scale / translate) only. The page as authored is the END state:
   a class that holds something back is only ever added by a script, or by the head snippet before the first paint. */

/* ---------- tokens ---------- */
:root {
  /* carried over from the source kit, same names and values */
  --motion-ease: cubic-bezier(.22, 1, .36, 1);        /* the one UI ease */
  --motion-fast: 120ms;                               /* state change: colour, border, focus ring */
  --motion-base: 160ms;                               /* open / close, hover lift */
  --motion-slow: 400ms;                               /* reveal, cross-fade */
  --motion-page: 400ms;                               /* page transition */
  --motion-controls-hover-lift: 2px;
  --motion-controls-press-scale: 0.98;                /* the plain press of a tappable block that is not a button */
  --motion-press-scale: 0.96;                         /* press bounce: sink, then spring back past the size */
  --motion-press-in: 90ms;
  --motion-press-out: 440ms;
  --motion-press-out-fallback: 230ms;
  --motion-press-spring: linear(0, 0.323 2.9%, 0.593 5.6%, 0.809 8.1%, 0.989 10.6%, 1.133 13.1%, 1.24 15.6%, 1.319 18.3%, 1.362 21%, 1.374 24.4%, 1.346 28.3%, 1.049 45.8%, 0.99 50.8%, 0.953 56%, 0.94 65%, 1 88.8%, 1);
  --motion-press-fallback: cubic-bezier(0.34, 2.323, 0.64, 1);
  --motion-nav-icon: 320ms;                           /* the ☰ ⇄ ✕ turn; the bars' slide and its delay scale with it */
  --motion-nav-close: 240ms;                          /* a menu going back up, or lifted away over the next page */
  --motion-morph: 380ms;                              /* one named element travelling from a page to the next */
  /* carried over as values (literals in the source kit's stylesheet), named here */
  --motion-ease-page: cubic-bezier(.32, .72, 0, 1);   /* the page push */
  --motion-ease-inout: cubic-bezier(.4, 0, .2, 1);    /* something leaving or turning */
  --motion-stagger: 60ms;                             /* reveal: block to block */
  --motion-rise: 12px;                                /* reveal: starts this far under its place */
  /* new */
  --motion-tilt: 8deg;                                /* .m-tilt at the element's edge */
  --motion-perspective: 800px;
}

/* ---------- page transition (cross-document view transition; the head snippet of README.md adds the direction) ----------
   Both pages opt in by loading this file. Reduced motion: no opt-in at all. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}
/* an element named on both pages travels from its old place to its new one */
::view-transition-group(*) { animation-duration: var(--motion-morph); animation-timing-function: var(--motion-ease); }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: var(--motion-page); animation-timing-function: var(--motion-ease-page); }

/* <html data-vt="push">: the next page pushes in from the right over the old one, which slides a little left and
   dims; back (a step back in the history, a link up) runs the other way. Ported from the source kit's page
   transition. Without the attribute the browser's cross-fade runs.
   The dim is the page at 94 % over black, which is brightness(.94) without a filter on something that moves. */
@keyframes m-push-in-right { from { transform: translateX(100%); } }
@keyframes m-push-out-left { to { transform: translateX(-28%); opacity: .94; } }
@keyframes m-push-in-left { from { transform: translateX(-28%); opacity: .94; } }
@keyframes m-push-out-right { to { transform: translateX(100%); } }
@keyframes m-hide { from, to { opacity: 0; } }
@keyframes m-panel-out { to { opacity: 0; transform: translateY(-10px); } }
html[data-vt="push"]::view-transition-group(root) { animation: none; }
html[data-vt="push"]::view-transition-image-pair(root) { background: #000; }
html[data-vt="push"]::view-transition-old(root),
html[data-vt="push"]::view-transition-new(root) { mix-blend-mode: normal; animation-fill-mode: both; }
html[data-vt="push"]::view-transition-old(root) { animation-name: m-push-out-left; }
html[data-vt="push"]::view-transition-new(root) { animation-name: m-push-in-right; box-shadow: -12px 0 32px rgb(0 0 0 / .14); }
html[data-vt="push"]:active-view-transition-type(back)::view-transition-old(root) { z-index: 1; animation-name: m-push-out-right; box-shadow: -12px 0 32px rgb(0 0 0 / .14); }
html[data-vt="push"]:active-view-transition-type(back)::view-transition-new(root) { animation-name: m-push-in-left; box-shadow: none; }

/* A page left through an open menu (menu.js; the page's script names the open sheet m-panel just before its picture is
   taken): the sheet is lifted away over the next page. Through the menu that covers the whole page (type via-menu),
   the page under it is not shown again: what stands behind the sheet as it goes is the page's own background. */
::view-transition-group(m-panel) { animation: none; }
::view-transition-old(m-panel) { animation: m-panel-out var(--motion-nav-close) var(--motion-ease-inout) both; }
html[data-vt="push"]:active-view-transition-type(via-menu)::view-transition-old(root) { animation-name: m-hide; }
html[data-vt="push"]:active-view-transition-type(via-menu)::view-transition-image-pair(root) { background: none; }

/* .m-keep: one element per page (the header) that takes no part — the old one's picture is never shown, the new one
   stands still above the pages from the first frame. A view-transition-name must be unique in its page. */
.m-keep { view-transition-name: m-keep; }
::view-transition-group(m-keep) { animation: none; z-index: 2; }
::view-transition-old(m-keep) { display: none; }
::view-transition-new(m-keep) { animation: none; }

/* Without cross-document view transitions the page left cannot be drawn, so only the page arriving moves: the same
   way, for the same time (the head snippet puts m-push / m-push-back on <html> for a link inside the site).
   .m-page marks what moves; everything else (the header) stands still. The body needs overflow-x: clip. */
html.m-push .m-page { animation: m-push-in-right var(--motion-page) var(--motion-ease-page) backwards; }
html.m-push-back .m-page { animation: m-push-in-left var(--motion-page) var(--motion-ease-page) backwards; }

/* ---------- first-screen entrance, once a visit (needs the head snippet of README.md) ----------
   html.m-first: this page has not played its entrance in this visit — held from the first paint.
   html.m-go:    the page is live (shown, its view transition over) — it plays. Without the snippet: no entrance. */
@keyframes m-enter { from { opacity: 0; transform: translate3d(0, var(--motion-rise), 0); } }
html.m-first .m-enter { opacity: 0; }
html.m-first.m-go .m-enter { opacity: 1; animation: m-enter var(--motion-slow) var(--motion-ease) calc(var(--m-i, 0) * var(--motion-stagger)) backwards; }

/* ---------- reveal (reveal.js): .m-wait holds what is still below the screen, .m-in lets it arrive ---------- */
.m-wait { opacity: 0; transform: translate3d(0, var(--motion-rise), 0); }
[data-reveal~="fade"].m-wait, [data-reveal~="fade"] > .m-wait { transform: none; }
[data-reveal~="scale"].m-wait, [data-reveal~="scale"] > .m-wait { transform: scale(.96); }
.m-in {
  transition:
    opacity var(--motion-slow) var(--motion-ease) calc(var(--m-i, 0) * var(--motion-stagger)),
    transform var(--motion-slow) var(--motion-ease) calc(var(--m-i, 0) * var(--motion-stagger));
}

/* ---------- scroll-scrubbed keyframes (scroll.js) ----------
   .m-scrub  a tall track (--m-len); its .m-pin child stays on the screen while the track passes.
   .m-view   any element; progress = its way through the screen.
   .m-key    inside either (or on a .m-view itself): --m-kf names its @keyframes, scrubbed by the nearest track.
   With scroll-driven animations the browser runs them off the main thread; elsewhere scroll.js writes --m-p (0…1)
   on the track and the same keyframes follow it, paused, through a negative delay. One set of keyframes, two drivers.
   The kit owns the `animation` shorthand of .m-key (a shorthand written after it would reset animation-timeline). */
.m-scrub { position: relative; height: var(--m-len, 300vh); view-timeline: --m-tl block; --m-range: contain; }
.m-pin { position: sticky; top: 0; height: 100vh; height: 100svh; overflow: clip; }
.m-view { view-timeline: --m-tl block; --m-range: cover; }
.m-key { animation: var(--m-kf, none) 1s linear both; }
@supports (animation-timeline: view()) {
  .m-key { animation-timeline: --m-tl; animation-range: var(--m-range, cover); }
}
@supports not (animation-timeline: view()) {
  .m-key { animation-play-state: paused; animation-delay: calc(var(--m-p, 0) * -1s); }
}
html[data-scroll="js"] .m-key { animation-timeline: auto; animation-play-state: paused; animation-delay: calc(var(--m-p, 0) * -1s); } /* testing: the script path in any browser */

/* ---------- pointer (pointer.js) ---------- */
/* tilt(): --m-x / --m-y run −1…1. .m-layer inside a tilted element shifts by its own --m-depth (parallax). */
.m-tilt { transform: perspective(var(--motion-perspective)) rotateX(calc(var(--m-y, 0) * var(--motion-tilt) * -1)) rotateY(calc(var(--m-x, 0) * var(--motion-tilt))); }
.m-layer { translate: calc(var(--m-x, 0) * var(--m-depth, 12px)) calc(var(--m-y, 0) * var(--m-depth, 12px)); }
/* hold(): --m-hold runs 0…1. .m-fill inside is a ready-made bar. A long press must not select or call a menu. */
.m-hold { touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.m-fill { transform: scaleX(var(--m-hold, 0)); transform-origin: 0 50%; }
/* drag(): listeners are passive, so CSS says what the gesture may scroll. .m-drag-x leaves vertical scrolling. */
.m-drag { touch-action: none; user-select: none; -webkit-user-select: none; -webkit-user-drag: none; cursor: grab; }
.m-drag-x { touch-action: pan-y; }
.m-drag.m-active { cursor: grabbing; }
/* will-change only while something moves: pointer.js puts m-active on and takes it off. */
.m-active { will-change: transform; }

/* ---------- press bounce + hover lift (CSS only; ported from the source kit's press bounce) ----------
   .m-press  a block button: it sinks while pressed, springs back past its size, and rises a little under a mouse.
   .m-tap    the press alone: an icon button, a text button, a tappable block (words in a line never jump).
   `scale` and `translate` are their own properties, so the element's `transform` stays free. A `transition`
   shorthand replaces the whole list: an element that also eases its colours lists them again in its own rule.
   --press-ease switches to the linear() spring only where linear() exists (an unknown function would void the list). */
@media (prefers-reduced-motion: no-preference) {
  .m-press, .m-tap {
    --press-time: var(--motion-press-out-fallback);
    --press-ease: var(--motion-press-fallback);
    transition: scale var(--press-time) var(--press-ease), translate var(--motion-base) var(--motion-ease);
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
  }
  @supports (transition-timing-function: linear(0, 1)) {
    .m-press, .m-tap { --press-time: var(--motion-press-out); --press-ease: var(--motion-press-spring); }
  }
  .m-press:active, .m-tap:active { scale: var(--motion-press-scale); --press-time: var(--motion-press-in); --press-ease: var(--motion-ease); }
}
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .m-press:hover { translate: 0 calc(-1 * var(--motion-controls-hover-lift)); }
}

/* ---------- menus (menu.js) ---------- */
/* .m-door: the window of a menu that lowers like a garage door; its one child is the sheet that is seen. Not a
   scroll box (focus must never scroll the sheet inside its window): `clip` where the browser has it. */
.m-door { overflow: hidden; overflow: clip; }
.m-door[hidden] { display: none; }
/* While a menu covers the page (the page's script sets data-nav-lock), the page behind does not scroll. Where a
   scrollbar takes room its gutter stays, so nothing moves sideways. */
html[data-nav-lock] { overflow: hidden; scrollbar-gutter: stable; }

/* .m-burger: ☰ ⇄ ✕, two bars. <button class="m-burger" aria-expanded="false"><span><span></span></span><span><span></span></span></button>
   Opening, the bars close in to the centre (0.566 × the turn), then turn ±45° after 0.316 ×; closing, they turn back
   first (0.566 ×), then move apart (0.5 × after 0.316 ×). --m-bar is the bars' thickness. html[data-nav-arrive]: the
   page was reached through the open menu, so it shows ✕ until its page transition starts, then turns back. */
.m-burger { position: relative; width: 2.75rem; height: 2.75rem; }
.m-burger > span {
  position: absolute; left: .75rem; top: calc(50% - var(--m-bar, 1.5px) / 2); width: 1.25rem; height: var(--m-bar, 1.5px);
  transition: transform calc(var(--motion-nav-icon) * .566) cubic-bezier(.04, .04, .12, .96);
}
.m-burger > span > span {
  display: block; width: 100%; height: 100%; background: currentColor;
  transition: transform calc(var(--motion-nav-icon) * .5) cubic-bezier(.52, .16, .52, .84) calc(var(--motion-nav-icon) * .316);
}
.m-burger > span:first-child > span { transform: translateY(-3.5px) scaleX(.88); }
.m-burger > span:last-child > span { transform: translateY(3.5px) scaleX(.88); }
.m-burger[aria-expanded="true"] > span > span, html[data-nav-arrive] .m-burger > span > span {
  transform: none;
  transition: transform calc(var(--motion-nav-icon) * .566) cubic-bezier(.04, .04, .12, .96) 0s;
}
.m-burger[aria-expanded="true"] > span, html[data-nav-arrive] .m-burger > span {
  transition: transform var(--motion-nav-icon) cubic-bezier(.04, .04, .12, .96) calc(var(--motion-nav-icon) * .316);
}
.m-burger[aria-expanded="true"] > span:first-child, html[data-nav-arrive] .m-burger > span:first-child { transform: rotate(45deg); }
.m-burger[aria-expanded="true"] > span:last-child, html[data-nav-arrive] .m-burger > span:last-child { transform: rotate(-45deg); }

/* ---------- rendering hygiene ---------- */
/* A long section far below the fold is not laid out or painted until it comes near. Give an honest size estimate
   (--m-cv) so the scrollbar and scroll restoration stay right. Not on a .m-scrub, nor around [data-reveal] children. */
.m-cv { content-visibility: auto; contain-intrinsic-size: auto var(--m-cv, 100vh); }

/* ---------- tier a, reduced motion, print: nothing is held, nothing is pinned ---------- */
html[data-motion-tier="a"] .m-wait { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
  .m-key { animation: none !important; }
  .m-scrub { height: auto !important; }
  .m-pin { position: static; height: auto; }
  .m-tilt { transform: none; }
  .m-layer { translate: none; }
}
@media print {
  .m-wait, html.m-first .m-enter { opacity: 1 !important; transform: none !important; }
  .m-page { animation: none !important; }
  .m-key { animation: none !important; }
  .m-scrub { height: auto !important; }
  .m-pin { position: static; height: auto; }
}
