/* Motion core (Task 8): first-view entrances and scroll reveals.
   Every hidden pre-reveal state is scoped to html.js, so without JS the page is simply the end state.
   html.is-loaded (fonts ready) starts the hero intro; [data-reveal].is-in is set once per element on scroll.
   Only transform, opacity, clip-path and filter (blur <= 6px) move. */

/* ---- M1: hero headline rises out of a line mask, then the lede ---- */
.js .hero-title .line > span { transform: translateY(130%); filter: blur(6px); }
.js.is-loaded .hero-title .line > span { transform: none; filter: none; transition: transform 900ms var(--ease-expo), filter 900ms var(--ease-expo); }
.js.is-loaded .hero-title .line:nth-child(2) > span { transition-delay: 90ms; }
.js .hero-lede { opacity: 0; transform: translateY(10px); }
.js.is-loaded .hero-lede { opacity: 1; transform: none; transition: opacity 700ms var(--ease-out) 260ms, transform 700ms var(--ease-out) 260ms; }

/* ---- M2: the five cards arrive as a held fan, then are dealt out into the row ---- */
/* --n (card count) is set on the row by pages/home.mjs, --i per card. Deal-in owns .svc-card's transform; flip and tilt live on .svc-inner and are untouched. The pivot sits
   below the card, like a hand holding the deck, so the fan opens at the top. The fan fades in as one group
   (opacity on the row, cards stay opaque) so stacked cards never show each other's names through them,
   and it is solid before the deal starts: the deal is the motion you see, not a cross-fade. */
.js .svc-row { opacity: 0; }
.js.is-loaded .svc-row { opacity: 1; transition: opacity 300ms var(--ease-out) 180ms; }
.js .svc-card { --fan-c: calc((var(--n) - 1) / 2); --fan-step: 6deg; transform-origin: 50% 160%; transform: translateX(calc((var(--fan-c) - var(--i)) * 100%)) rotate(calc((var(--i) - var(--fan-c)) * var(--fan-step))) scale(.94); }
/* In the snap row only the first card is in view, so the deck sits on it and is dealt to the right, top card
   first, uncovering card I instead of burying it until the last card leaves. */
@media (max-width: 1180px) { .js .svc-card { --fan-c: 0; --fan-step: 2.5deg; --deal: calc(var(--n) - 1 - var(--i)); } }
.js.is-loaded .svc-card { transform: none; transition: transform 900ms var(--ease-expo) calc(480ms + var(--deal, var(--i)) * 70ms); }

/* ---- M6: the halftone art settles from a faint, slightly large print just behind each card ---- */
.js .svc-art { opacity: .4; transform: scale(1.06); }
.js.is-loaded .svc-art { opacity: 1; transform: none; transition: opacity 1000ms var(--ease-out), transform 1000ms var(--ease-out); transition-delay: calc(560ms + var(--i, 0) * 70ms); }
/* The intro is over (~1.9s): drop its long, delayed transitions so Task 9's tilt and flip start from a clean slate. */
.js.intro-done .svc-row, .js.intro-done .svc-card, .js.intro-done .svc-art { transition: none; }

/* ---- M5: the light beams drift; they ease at each end instead of bouncing off a wall ---- */
.hero-beams i { animation: beam 22s var(--ease-in-out) infinite alternate; }
.hero-beams i:nth-child(2) { animation-duration: 26s; animation-delay: -8s; }
.hero-beams i:nth-child(3) { animation-duration: 18s; animation-delay: -3s; }
@keyframes beam { from { transform: translateX(-40px); } to { transform: translateX(40px); } }
.hero.is-offscreen .hero-beams i, .hero.is-offscreen .swivel-word > span { animation-play-state: paused; }

/* ---- Generic reveal: a short rise. Delay comes from the batch stagger in js/reveal.js ---- */
.js [data-reveal]:not(.case-tile):not([data-split]) { opacity: 0; transform: translateY(14px); }
.js [data-reveal].is-in:not(.case-tile):not([data-split]) { opacity: 1; transform: none; transition: opacity 700ms var(--ease-out) var(--d, 0ms), transform 700ms var(--ease-out) var(--d, 0ms); }

/* ---- M12 words, M23 letters: each rises through its own mask ---- */
/* Hidden from first paint, before the module splits it, so a heading never flashes visible then hidden.
   On .is-in the heading itself snaps to visible and its words carry the reveal. Each word or letter sets its own --d
   (js/split.js: 35ms per word, 25ms per letter), which shadows the batch stagger reveal.js puts on the heading. */
.js [data-split]:not(.is-in) { opacity: 0; }
/* The padding/negative-margin pair grows the clip box past the tight display line-height, so ascenders,
   descenders and italic overhang are never cut, while the line box stays exactly as before. */
.js [data-split] .w, .js [data-split] .c { display: inline-block; overflow: clip; vertical-align: top; padding: .16em .08em .22em; margin: -.16em -.08em -.22em; }
/* The wordmark has no descenders and sits right above the footer rule: its mask closes at the baseline so rising
   letters never cross the rule. */
.js .footer-wordmark .c { padding-bottom: .03em; margin-bottom: -.03em; }
.js [data-split] .w > span, .js [data-split] .c > span { display: inline-block; opacity: 0; transform: translateY(70%); filter: blur(4px); }
.js [data-split].is-in .w > span, .js [data-split].is-in .c > span { opacity: 1; transform: none; filter: none; transition: opacity 700ms var(--ease-out) var(--d), transform 800ms var(--ease-expo) var(--d), filter 700ms var(--ease-out) var(--d); }

/* ---- M9: case tile opens from an inset window while the cover settles from a slight zoom ---- */
.js .case-tile .tile-media { clip-path: inset(12% 6% 12% 6% round var(--r-tile)); }
.js .case-tile .tile-media > img, .js .case-tile .tile-fallback { transform: scale(1.12); }
.js .case-tile .tile-caption > * { opacity: 0; transform: translateY(12px); }
.js .case-tile.is-in .tile-media { clip-path: inset(0 0 0 0 round var(--r-tile)); transition: clip-path 1000ms var(--ease-expo) var(--d, 0ms); }
.js .case-tile.is-in .tile-media > img, .js .case-tile.is-in .tile-fallback { transform: none; transition: transform 1000ms var(--ease-expo) var(--d, 0ms); }
.js .case-tile.is-in .tile-caption > * { opacity: 1; transform: none; transition: opacity 600ms var(--ease-out), transform 600ms var(--ease-out); transition-delay: calc(var(--d, 0ms) + 120ms); }
.js .case-tile.is-in .tile-caption > :nth-child(2) { transition-delay: calc(var(--d, 0ms) + 180ms); }
.js .case-tile.is-in .tile-caption > :nth-child(3) { transition-delay: calc(var(--d, 0ms) + 240ms); }

/* ---- M10: tile hover, pointer devices only ---- */
/* After the reveal (.is-settled, ~1s) the cover swaps the 1000ms reveal transition for a quick one with no
   delay, so the zoom eases out as fast as it eased in. */
.js .case-tile.is-settled .tile-media > img, .js .case-tile.is-settled .tile-fallback { transition: transform 350ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .tile-arrow { transition: transform 200ms var(--ease-out), background-color 200ms ease, color 200ms ease; }
  .js .case-tile.is-settled:hover .tile-media > img, .js .case-tile.is-settled:hover .tile-fallback { transform: scale(1.035); }
  .case-tile:hover .tile-arrow { transform: rotate(0deg); background-color: var(--ink); color: #fff; }
}

/* Paper never waits for a scroll that cannot happen. */
@media print {
  .js .svc-row, .js [data-split], .js .hero-title .line > span, .js .hero-lede, .js .svc-card, .js .svc-art, .js [data-reveal], .js [data-split] .w > span, .js [data-split] .c > span,
  .js .case-tile .tile-media, .js .case-tile .tile-media > img, .js .case-tile .tile-fallback, .js .case-tile .tile-caption > * {
    opacity: 1 !important; transform: none !important; filter: none !important; clip-path: none !important;
  }
}

/* ---- M20: page-to-page view transitions. The root cross-fades; a tile's cover (view-transition-name cover-<slug>)
   morphs into the case page's cover when both exist. Only when motion is allowed. ---- */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 250ms; animation-timing-function: var(--ease-out); }
  ::view-transition-group(*) { animation-duration: 450ms; animation-timing-function: var(--ease-expo); }
  /* A cover with no partner on the other page (every tile but the clicked one) fades with the root instead of
     hanging over the new page for the longer morph. */
  ::view-transition-old(*):only-child, ::view-transition-new(*):only-child { animation-duration: 250ms; }
}

/* Reduced motion: nothing travels, blurs or unmasks; elements still arrive with a short fade. Keep this block last. */
@media (prefers-reduced-motion: reduce) {
  .js .hero-title .line > span, .js .hero-lede, .js .svc-card, .js .svc-art, .js [data-reveal],
  .js .case-tile .tile-media, .js .case-tile .tile-media > img, .js .case-tile .tile-fallback, .js .case-tile .tile-caption > * {
    transform: none !important; filter: none !important; clip-path: none !important;
  }
  .js [data-reveal]:not(.is-in) { opacity: 0; }
  .js [data-reveal].is-in, .js .case-tile .tile-caption > * { opacity: 1; }
  .js .hero-lede, .js .svc-row, .js .svc-art, .js [data-reveal] { transition: opacity 200ms var(--ease-out) !important; }
  .js .svc-card, .js .case-tile .tile-caption > * { transition: none !important; }
  .hero-beams i, .swivel-word > span { animation: none !important; }
}
