/* ==========================================================================
   SPARTAN HOMES — ANIMATIONS
   Keyframes + the transition classes motion.js hooks into.
   Requires: tokens.css
   Contract: JS only ever adds/removes classes and sets --reveal-i / --p.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. REVEAL ON SCROLL
   Markup:
     <div data-reveal>…</div>                       fade + rise
     <div data-reveal="left">…</div>                slide from left
     <div data-reveal="right">…</div>               slide from right
     <div data-reveal="scale">…</div>               settle in
     <div data-reveal="mask"><img …></div>          image wipe reveal
     <ul data-reveal-group>  <li>…</li> </ul>       staggered children
     [data-reveal-delay="200"]                      extra ms delay

   JS adds .is-visible once the element crosses the threshold.
   If JS never runs, .no-js fallback below keeps everything visible.
   -------------------------------------------------------------------------- */
[data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-rise), 0);
  transition:
    opacity var(--reveal-dur) var(--ease) var(--reveal-delay, 0ms),
    transform var(--reveal-dur) var(--ease) var(--reveal-delay, 0ms),
    clip-path var(--reveal-dur) var(--ease) var(--reveal-delay, 0ms);
}

/* will-change is a promise, not a hint — it costs a compositor layer for as
   long as it is set. Only promise it while the element is actually animating.
   JS adds .is-revealing on show() and clears it in the same 1600ms timeout
   that adds .is-settled. */
[data-reveal].is-revealing,
[data-reveal-group].is-revealing > * { will-change: opacity, transform; }

[data-reveal="left"]  { transform: translate3d(calc(var(--reveal-rise) * -1.4), 0, 0); }
[data-reveal="right"] { transform: translate3d(calc(var(--reveal-rise) * 1.4), 0, 0); }

/* On a phone there is no horizontal room to slide into: a +39px resting offset
   on every [data-reveal="right"] pushes the document ~18px wider than the
   viewport, which body{overflow-x:hidden} then silently swallows. Below 48rem
   the horizontal reveals become vertical ones. Same motion, no sideways bleed. */
@media (max-width: 48rem) {
  [data-reveal="left"],
  [data-reveal="right"] { transform: translate3d(0, var(--reveal-rise), 0); }
}
[data-reveal="scale"] { transform: scale(0.965); opacity: 0; }
[data-reveal="fade"]  { transform: none; }

/* Image mask wipe — expensive-feeling, no layout shift */
[data-reveal="mask"] {
  opacity: 1;
  transform: none;
  clip-path: inset(0 0 100% 0);
}
[data-reveal="mask"] > img,
[data-reveal="mask"] > picture > img {
  transform: scale(1.08);
  transition: transform calc(var(--reveal-dur) * 1.4) var(--ease) var(--reveal-delay, 0ms);
}

[data-reveal].is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}
[data-reveal="scale"].is-visible { transform: scale(1); }
[data-reveal="mask"].is-visible { clip-path: inset(0 0 0 0); }
[data-reveal="mask"].is-visible > img,
[data-reveal="mask"].is-visible > picture > img { transform: scale(1); }

/* Once it has landed, drop the will-change hint (JS removes .is-revealing) */
[data-reveal].is-settled { will-change: auto; }

/* Staggered children. JS sets --reveal-i on each direct child. */
[data-reveal-group] > * {
  opacity: 0;
  transform: translate3d(0, var(--reveal-rise), 0);
  transition:
    opacity var(--reveal-dur) var(--ease),
    transform var(--reveal-dur) var(--ease);
  transition-delay: calc(var(--reveal-i, 0) * var(--reveal-stagger));
}
[data-reveal-group].is-visible > * {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* Word-by-word display headline. JS wraps words in <span class="word"><i>…</i></span> */
/* The mask box has to be TALLER than the line box: at --lh-snug (1.12) the
   inline-block content box is shorter than Vidaloka's glyph bounds, so a bare
   overflow:hidden clips ascenders and apostrophes. Pad the mask, then pull the
   padding back out of the layout with an equal negative margin. */
.reveal-words .word {
  display: inline-block;
  padding-block: 0.18em;
  margin-block: -0.18em;
  overflow: hidden;
  vertical-align: bottom;
}
/* font: inherit — not just font-style. This <i> is a mechanical wrapper motion.js
   injects around each word; it is not emphasis. base.css routes <i> to the
   Raleway italic face, and because that family ships ONLY an italic face,
   resetting font-style alone still leaves the headline in a slanted sans.
   Inherit the whole font shorthand so the wrapper is typographically invisible. */
.reveal-words .word > i {
  display: inline-block;
  font: inherit;
  transform: translateY(105%);
  transition: transform var(--dur-slowest) var(--ease);
  transition-delay: calc(var(--reveal-i, 0) * 70ms);
}
.reveal-words.is-visible .word > i { transform: translateY(0); }

/* --------------------------------------------------------------------------
   NO-JS SAFETY NET
   .no-js is on <html> from the server and is removed by motion.js ITSELF
   (motion.js line ~28) — never by an inline <script> in <head>. If it were
   stripped in <head>, a motion.js that 404s / is blocked / throws would leave
   every [data-reveal] section at opacity:0 forever: hero, then blank page.
   Everything below is what the page must look like when JS never arrives.
   -------------------------------------------------------------------------- */

/* 1. Revealed content stays visible */
.no-js [data-reveal],
.no-js [data-reveal-group] > *,
.no-js .reveal-words .word > i {
  opacity: 1 !important;
  transform: none !important;
  clip-path: none !important;
}

/* 2. The FAQ degrades to a plain, fully-open definition list.
      Without this, all nine answers are unreachable — while the FAQPage
      JSON-LD still hands them to Google. That's cloaking by accident. */
.no-js .faq__panel { grid-template-rows: 1fr; }
.no-js .faq__panel-inner { visibility: visible; }
.no-js .faq__icon { display: none; }
.no-js .faq__q { cursor: default; }

/* 3. The lead form is a single-step native POST, so it needs no fallback.
      (The old two-step form did — #step-2 was [hidden] and the only "next"
      button was type="button", so with no JS there was no path to a submit at
      all. That form is gone. Do not bring it back.) */

/* 4. Gallery captions are hover-revealed on pointer devices. With no JS the
      lightbox never binds, so the tile is just a link to the full image —
      still fine, still captioned. Nothing to do. */

/* --------------------------------------------------------------------------
   2. PARALLAX
   Markup: <img class="hero__img" data-parallax data-parallax-speed="0.18">
   JS writes --p (a px offset) and the transform reads it. Transform only.
   -------------------------------------------------------------------------- */
[data-parallax] {
  transform: translate3d(0, var(--p, 0px), 0);
  will-change: transform;
  backface-visibility: hidden;
}

/* Subtle scale-out on the hero as it leaves — optional, opt-in */
[data-parallax][data-parallax-zoom] {
  transform: translate3d(0, var(--p, 0px), 0) scale(var(--pz, 1));
}

/* --------------------------------------------------------------------------
   3. COUNT-UP
   Markup: <span class="num" data-count data-count-to="20" data-count-suffix="+">0</span>
   -------------------------------------------------------------------------- */
[data-count] {
  font-variant-numeric: tabular-nums lining-nums;
  display: inline-block;
}
[data-count].is-counting { opacity: 1; }

/* --------------------------------------------------------------------------
   4. KEYFRAMES
   -------------------------------------------------------------------------- */
@keyframes fadeRise {
  from { opacity: 0; transform: translate3d(0, var(--reveal-rise), 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes navItemIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes scrollCue {
  0%   { transform: scaleY(0); transform-origin: 50% 0; opacity: 0; }
  35%  { transform: scaleY(1); transform-origin: 50% 0; opacity: 1; }
  70%  { transform: scaleY(1); transform-origin: 50% 100%; opacity: 1; }
  100% { transform: scaleY(0); transform-origin: 50% 100%; opacity: 0; }
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

@keyframes lightboxIn {
  from { opacity: 0; transform: scale(0.985); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes ruleGrow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* Slow ken-burns drift for hero stills. Opt-in: class="hero__img is-drift" */
@keyframes drift {
  from { transform: scale(1) translate3d(0, 0, 0); }
  to   { transform: scale(1.06) translate3d(0, -1%, 0); }
}
.is-drift { animation: drift 24s var(--ease-in-out) forwards; }

/* --------------------------------------------------------------------------
   5. UTILITY ANIMATION CLASSES
   -------------------------------------------------------------------------- */
.anim-fade-rise { animation: fadeRise var(--dur-slower) var(--ease) both; }
.anim-fade-in   { animation: fadeIn var(--dur-slow) var(--ease) both; }
.anim-delay-1 { animation-delay: 100ms; }
.anim-delay-2 { animation-delay: 200ms; }
.anim-delay-3 { animation-delay: 300ms; }
.anim-delay-4 { animation-delay: 400ms; }
.anim-delay-5 { animation-delay: 500ms; }

/* Hero content entrance on page load (no observer needed above the fold).
   motion.js sets --i on every child, so the ladder can't run out of rungs —
   the old nth-child(1..5) list left children 6+ at delay 0, which landed the
   trust badges BEFORE the headline. Same contract as .nav-drawer__link. */
.hero__content > * {
  animation: fadeRise var(--dur-slowest) var(--ease) both;
  animation-delay: calc(120ms + (var(--i, 0) * 120ms));
}

/* --------------------------------------------------------------------------
   6. REDUCED MOTION — hard stop
   Durations are already ~1ms via tokens; this kills iteration + transform.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  [data-reveal],
  [data-reveal-group] > * {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }
  [data-reveal="mask"] > img { transform: none !important; }

  .reveal-words .word > i { transform: none !important; }

  [data-parallax] { transform: none !important; }

  .is-drift { animation: none !important; }
  .btn--primary::after { display: none !important; }
  .hero__content > * { animation: none !important; }
}
