/* 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; }
}
/* The old view steps back to 90% brightness, not 40%: on the cream page 40%
   was a dark grey frame mid-wipe (r6). Cream at 90% stays a light warm tone,
   enough for the incoming edge to read. */
@keyframes vt-out-fwd { to { transform: translateX(-4%); filter: brightness(0.9) blur(3px); } }
@keyframes vt-out-back { to { transform: translateX(4%); filter: brightness(0.9) 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%);
  /* On cream a light only reads as something brighter than the page, so the
     glow is stronger than it was on ink and the core is pure white. */
  background: radial-gradient(50% 46% at 50% 50%, rgb(255 255 255 / 0.62), rgb(255 253 246 / 0.22) 55%, rgb(255 253 246 / 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 255 255 / 0) 0%, rgb(255 255 255 / 0.9) 22%, rgb(255 255 255 / 1) 50%, rgb(255 255 255 / 0.9) 78%, rgb(255 255 255 / 0) 100%);
  box-shadow: 0 0 10px 2px rgb(255 255 255 / 0.7);
}
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 arrival (the badge from a speck, its shadow with it) is in
   home.css with the rest of the stage. The dark stage's intro that lived here
   (the pool, the floor line, the softbox strips switching on, and an opacity
   fade on the mark) went with r6's cream stage; r7 took the dead rules out. */

/* 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 fills, the
   softbox switching on, and fades as the stage comes up. It has no ground of
   its own: a navy line on the cream page (r6; it was a cream line on ink). */
.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-navy-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-navy);
  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(var(--gist-navy-rgb) / 0); box-shadow: 0 0 14px 3px rgb(255 255 255 / 0.85), 0 0 48px 10px rgb(255 255 255 / 0.5); }
.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; }
  /* Fixed UI never prints (r8 minors). A fixed element repeats on every
     printed page: the skip link, parked just above the screen, printed "Skip
     the menu" at the foot of every page but the last, over the text. The
     preloader, the transition light, the phone menu and the video player are
     fixed too, so they go the same way if a page is printed while one shows.
     Home's stage and header stay: Home prints as one page and the badge is
     its content. */
  .skip-link, .preloader, .vt-light, .menu-sheet, .player { display: none !important; }
  /* A chapter's opener (its rule, numeral and label) stays with the heading
     under it, so a numeral can't end a page on its own (Services' 07 did).
     On Services the heading sits in the next box (the service's grid), and
     Chrome still broke at the grid's top, so the service's name and its line
     hold together too. h1 to h3 ask not to end a page, but Chrome can still
     leave one at a page foot (AI + VFX does); that is a print limit, not a bug.
     Quote's questions (a numbered legend and its answers) stay whole: "4 Is
     co-op involved?" ended a page with its answers on the next. The footer's
     last row (the legal lines and Back to top) stays whole too, so Back to
     top never ends up alone on a last page. */
  .sec-head { break-inside: avoid; break-after: avoid; }
  .service__head { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }
  .qstep { break-inside: avoid; }
  .footer__base { break-inside: avoid; }
}

/* ---------- 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; }
  /* The rule above turns every element's default "all" into a 160 ms
     transition. main.js (fitLeads) narrows the page-head lead when a view
     opens, and that width change must land at once, not slide the text. */
  .page-head__lead { transition-property: none !important; }
  /* Home's corner line is right-aligned with a max-width in ch, so when the
     web font (Barlow since r8) arrives after first paint the box can narrow
     and the line slid (about 20 px with r5's font). Same fix: it lands at
     once. */
  .home__corner { transition-property: none !important; }
}
