/* Motion: one language for the whole site. Ease-out, never bouncy.

   The cut. Views change with a light wipe: a hard edge crosses the screen
   with a thin strip light riding it, the old view pushes back and dims, and
   the new one settles in. 800 ms, every time. Going forward in nav order
   (Home, Work, Services, AI + VFX, About, Get a quote) wipes right to left;
   going back wipes left to right. The mark flies into the header slot on the
   same clock and curve (home.css).

   Hovers use the same wipe in miniature: underlines and fills grow from the
   left and leave to the right, 240 to 280 ms (base.css, layout.css,
   work.css, services.css).

   Reveals. Headlines rise line by line behind a mask (600 ms, 120 ms apart).
   Everything else fades up 16 px (600 ms, 120 ms apart). Work posters wipe in.

   Reduced motion turns all of it off (bottom). */

:root {
  --vt-dur: 800ms;
  --vt-ease: var(--motion-ease-out);
  --reveal-dur: 600ms;
  --reveal-step: 120ms;
}

/* ---------- The cut: view transitions between routes ---------- */
@keyframes vt-in-fwd {
  from { clip-path: inset(0 0 0 100%); transform: translateX(5%); }
  to { clip-path: inset(0 0 0 0); transform: none; }
}
@keyframes vt-in-back {
  from { clip-path: inset(0 100% 0 0); transform: translateX(-5%); }
  to { clip-path: inset(0 0 0 0); transform: none; }
}
@keyframes vt-out-fwd { to { transform: translateX(-4%); filter: brightness(0.4) blur(3px); } }
@keyframes vt-out-back { to { transform: translateX(4%); filter: brightness(0.4) blur(3px); } }
/* The strip light rides the wipe edge: 105vw to 0 forward, -5vw to 100vw back. */
@keyframes vt-light-fwd {
  from { transform: translateX(calc(105vw - 50%)); }
  to { transform: translateX(-50%); }
}
@keyframes vt-light-back {
  from { transform: translateX(calc(-5vw - 50%)); }
  to { transform: translateX(calc(100vw - 50%)); }
}
@keyframes vt-light-fade { 0%, 45% { opacity: 1; } 100% { opacity: 0; } }
@keyframes vt-fade-out { to { opacity: 0; } }
@keyframes vt-ui-in { from { opacity: 0; transform: translateY(12px); } }

/* No crossfade blending: the wipe is a cut, not a dissolve. */
html.vt-route::view-transition-old(root),
html.vt-route::view-transition-new(root) { mix-blend-mode: normal; }
html.vt-fwd::view-transition-old(root) { animation: vt-out-fwd var(--vt-dur) var(--vt-ease) both; }
html.vt-fwd::view-transition-new(root) { animation: vt-in-fwd var(--vt-dur) var(--vt-ease) both; }
html.vt-back::view-transition-old(root) { animation: vt-out-back var(--vt-dur) var(--vt-ease) both; }
html.vt-back::view-transition-new(root) { animation: vt-in-back var(--vt-dur) var(--vt-ease) both; }

/* The stage keeps rendering live through a transition: no old snapshot, and
   the live one plays as is while the mark flies to the header. Coming Home,
   the lit stage wipes in with the rest of the frame. */
::view-transition-group(stage) { animation: none; }
::view-transition-old(stage) { display: none; }
::view-transition-new(stage) { animation: none; mix-blend-mode: normal; }
html.vt-to-home::view-transition-new(stage) { animation: vt-in-back var(--vt-dur) var(--vt-ease) both; }

/* Home's line and buttons: out in 160 ms, back in once the wipe has passed. */
html.vt-route::view-transition-old(home-ui) { animation: vt-fade-out 160ms var(--motion-ease-in) both; mix-blend-mode: normal; }
html.vt-route::view-transition-new(home-ui) { animation: vt-ui-in 600ms var(--ease-out) 280ms both; mix-blend-mode: normal; }

/* The header holds still while the frame changes under it. */
html.vt-route::view-transition-group(site-header) { animation-duration: var(--vt-dur); animation-timing-function: var(--vt-ease); }
html.vt-route::view-transition-old(site-header),
html.vt-route::view-transition-new(site-header) { animation-duration: 420ms; }

/* The strip light. Only in the new state, so it has no old snapshot; it sits
   off screen at rest, so it never shows outside a transition. */
.vt-light {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 90;
  width: clamp(120px, 12vw, 200px);
  height: 100vh;
  pointer-events: none;
  transform: translateX(-110%);
  background: radial-gradient(50% 46% at 50% 50%, rgb(255 246 226 / 0.2), rgb(255 246 226 / 0.06) 55%, rgb(255 246 226 / 0) 100%);
}
.vt-light::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 5%;
  bottom: 5%;
  width: 2px;
  margin-left: -1px;
  border-radius: 2px;
  background: linear-gradient(to bottom, rgb(255 250 238 / 0) 0%, rgb(255 250 238 / 0.8) 22%, rgb(255 252 244 / 1) 50%, rgb(255 250 238 / 0.8) 78%, rgb(255 250 238 / 0) 100%);
}
html.vt-light-on .vt-light { display: block; view-transition-name: vt-light; }
html.vt-route::view-transition-new(vt-light) { animation: none; }
html.vt-fwd::view-transition-group(vt-light) { animation: vt-light-fwd var(--vt-dur) var(--vt-ease) both, vt-light-fade var(--vt-dur) linear both; }
html.vt-back::view-transition-group(vt-light) { animation: vt-light-back var(--vt-dur) var(--vt-ease) both, vt-light-fade var(--vt-dur) linear both; }

/* Filter changes on Work: cards glide to their new spots, no page wipe. */
html.vt-filter::view-transition-old(root),
html.vt-filter::view-transition-new(root) { animation: none; }
html.vt-filter .work-card { view-transition-name: var(--vt-name); }
html.vt-filter::view-transition-group(*) { animation-duration: 520ms; animation-timing-function: var(--ease-out); }

/* Without the View Transitions API, router.js plays the same wipe on the
   incoming view (and the stage, coming Home) with the Web Animations API. */

/* ---------- Reveals inside scrolling views ---------- */
/* Hidden only while scripts run and motion is on. shared.js reveals what's
   on screen, and sweeps up anything an observer missed, so nothing stays at 0. */
html.js:not(.reduce-motion) [data-reveal] {
  --reveal-at: calc(var(--i, 0) * var(--reveal-step) + var(--reveal-delay, 0ms));
  opacity: 0;
  transform: translateY(var(--motion-distance-reveal));
  transition:
    opacity var(--reveal-dur) var(--ease-out) var(--reveal-at),
    transform var(--reveal-dur) var(--ease-out) var(--reveal-at);
}
html.js:not(.reduce-motion) [data-reveal].is-revealed { opacity: 1; transform: none; }

/* Headlines: words split by shared.js, each line rises behind its mask. The
   words start 140% down so no ascender peeks over the mask's lower edge. The
   screen reader copy can't be selected, so copying a headline copies it once. */
.w { display: inline-block; overflow: hidden; vertical-align: top; padding: 0.1em 0 0.16em; margin: -0.1em 0 -0.16em; }
.w__i { display: inline-block; }
[data-split] > .visually-hidden { -webkit-user-select: none; user-select: none; }
html.js:not(.reduce-motion) [data-reveal="lines"] { opacity: 1; transform: none; }
html.js:not(.reduce-motion) [data-reveal="lines"] .w__i {
  transform: translateY(140%);
  transition: transform var(--reveal-dur) var(--motion-ease-out-expo) calc(var(--reveal-at) + var(--line, 0) * var(--reveal-step));
}
html.js:not(.reduce-motion) [data-reveal="lines"].is-revealed .w__i { transform: none; }

/* Work posters wipe in left to right as their card rises, and settle from a
   touch of scale. */
html.js:not(.reduce-motion) .work-card[data-reveal] .work-card__poster {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1000ms var(--motion-ease-out-expo) calc(var(--reveal-at) + 60ms);
}
html.js:not(.reduce-motion) .work-card[data-reveal].is-revealed .work-card__poster { clip-path: inset(0 0 0 0); }
html.js:not(.reduce-motion) .work-card[data-reveal] .work-card__poster img { transform: scale(1.06); transition: transform 1200ms var(--motion-ease-out-expo) var(--reveal-at); }
html.js:not(.reduce-motion) .work-card[data-reveal].is-revealed .work-card__poster img { transform: none; }

/* Not found: the stall lines paint in, the empty stall last. */
@keyframes stall-in { from { transform: scaleY(0); } }
@keyframes stall-top { from { transform: scaleX(0); } }
html:not(.reduce-motion) .view--notfound .nf__lot span { transform-origin: 50% 100%; animation: stall-in 700ms var(--ease-out) calc(260ms + var(--n, 0) * 70ms) both; }
html:not(.reduce-motion) .view--notfound .nf__lot::before { transform-origin: 50% 50%; animation: stall-top 900ms var(--ease-out) 200ms both; }
.nf__lot span:nth-child(1) { --n: 0; }
.nf__lot span:nth-child(2) { --n: 1; }
.nf__lot span:nth-child(3) { --n: 2; }
.nf__lot span:nth-child(5) { --n: 3; }
.nf__lot span:nth-child(6) { --n: 4; }
.nf__lot span:nth-child(4) { --n: 6; }

/* ---------- Home intro ---------- */
/* Two clocks, run by main.js (plan 7.5). .stage-intro starts when the stage
   is ready ('stage:ready' from the 3D, or the still). .ui-intro starts at the
   earlier of "stage ready + 1.6 s" and "boot + 1.2 s", once the fonts are in.
   Until each starts, its parts hold hidden. A failsafe shows them at 6 s if
   the script never gets that far. */
@keyframes failsafe-show { to { opacity: 1; } }
html.js:not(.reduce-motion):not(.ui-ready) [data-intro],
html.js.is-home:not(.reduce-motion):not(.ui-ready) .site-header,
html.js:not(.reduce-motion):not(.stage-ready) .stage__studio,
html.js:not(.reduce-motion):not(.stage-ready) .stage__still { opacity: 0; animation: failsafe-show 1ms linear 6s forwards; }

/* The still's own intro: the studio comes up, the softboxes switch on one
   after another, the mark rises into place, and a glint runs across it. */
@keyframes intro-strip { from { opacity: 0; } to { opacity: var(--o, 0.5); } }
@keyframes intro-fade { from { opacity: 0; } }
@keyframes intro-mark {
  from { opacity: 0; transform: translateY(18px) scale(0.965); }
  to { opacity: 1; transform: none; }
}
html.stage-intro .stage__pool { animation: intro-fade 1400ms var(--ease-out) both; }
html.stage-intro .stage__floor { animation: intro-fade 1400ms var(--ease-out) 500ms both; }
html.stage-intro .stage__strip { animation: intro-strip 700ms var(--ease-out) both; }
html.stage-intro .stage__strip:nth-child(3) { animation-delay: 80ms; }
html.stage-intro .stage__strip:nth-child(6) { animation-delay: 200ms; }
html.stage-intro .stage__strip:nth-child(4) { animation-delay: 320ms; }
html.stage-intro .stage__strip:nth-child(5) { animation-delay: 440ms; }
html.stage-intro .stage__tilt { animation: intro-mark 1300ms var(--motion-ease-out-expo) 200ms both; }

/* The UI, plan section 4: nav fades in and drops 8 px; the line rises behind
   a line mask, 600 ms per line, 120 ms apart; the buttons fade up 16 px,
   120 ms apart; then the corner details. Offsets match the plan's 1.6, 1.75,
   2.1 and 2.4 s marks, counted from the nav. */
@keyframes intro-drop { from { opacity: 0; transform: translateY(-8px); } }
@keyframes intro-line { from { transform: translateY(140%); } }
@keyframes intro-rise { from { opacity: 0; transform: translateY(16px); } }
@keyframes intro-corner { from { opacity: 0; transform: translateY(6px); } }
@keyframes intro-draw { from { transform: scaleX(0); } }
html.ui-intro .site-header { animation: intro-drop 600ms var(--ease-out) both; }
html.ui-intro .home__line .w__i { animation: intro-line 600ms var(--motion-ease-out-expo) calc(150ms + var(--line, 0) * 120ms) both; }
html.ui-intro .home__ctas > * { animation: intro-rise 600ms var(--ease-out) 500ms both; }
html.ui-intro .home__ctas > * + * { animation-delay: 620ms; }
html.ui-intro .home__corner { animation: intro-corner 600ms var(--ease-out) 900ms both; }
html.ui-intro .home__corner::before { animation: intro-draw 800ms var(--motion-ease-out-expo) 800ms both; }

/* ---------- Preloader (plan 7.8) ---------- */
/* Home only. Shows if the stage isn't ready in 300 ms, grows with real
   progress and never moves backward. When the stage is ready it brightens
   and softens, the softbox switching on, and fades as the stage comes up. */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 400ms var(--ease-out);
}
.preloader.is-visible { opacity: 1; }
.preloader.is-done { transition: opacity 360ms var(--ease-out) 120ms; }
.preloader__line {
  position: relative;
  width: min(42vw, 520px);
  height: 2px;
  border-radius: 2px;
  background: rgb(var(--gist-cream-rgb) / 0.12);
  transition: background-color 240ms var(--ease-out), box-shadow 240ms var(--ease-out);
}
.preloader__line::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--gist-cream);
  transform-origin: left;
  transform: scaleX(var(--p, 0.04));
  transition: transform 500ms var(--ease-out), filter 240ms var(--ease-out);
}
.preloader.is-done .preloader__line { background: rgb(255 250 238 / 0.9); box-shadow: 0 0 14px 3px rgb(255 246 226 / 0.45), 0 0 48px 10px rgb(255 246 226 / 0.16); }
.preloader.is-done .preloader__line::after { transform: scaleX(1); filter: blur(0.6px); transition-duration: 200ms; }

/* ---------- Print ---------- */
@media print {
  [data-reveal], [data-intro], .w__i, .stage__still, .stage__studio, .site-header { opacity: 1 !important; transform: none !important; clip-path: none !important; }
}

/* ---------- Reduced motion ---------- */
/* No wipes, flights, reveals, intro or drift. Hovers keep a quick color change. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    transition-duration: 160ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
  .stage__tilt { transform: none !important; }
  .vt-light { display: none !important; }
}
