/* ==========================================================================
   Suttons Business Park — Motion
   --------------------------------------------------------------------------
   Scroll reveals, Ken Burns, marquee, parallax hooks.
   Depends on: tokens.css
   --------------------------------------------------------------------------
   All timings come from the motion tokens so the whole site moves with one
   personality. Every effect degrades to "visible, static" when the user
   asks for reduced motion — see the block at the foot of this file.
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. SCROLL REVEAL
   --------------------------------------------------------------------------
   Add [data-reveal] to any element. An IntersectionObserver in app.js adds
   .is-revealed when it enters the viewport.

     <div data-reveal>…</div>                  fade + rise (default)
     <div data-reveal="left">…</div>           slide in from the left
     <div data-reveal="scale">…</div>          settle in
     <div data-reveal data-reveal-delay="2">   stagger step (1–6)

   .no-js on <html> is swapped to .js by the inline head script, so
   content stays visible if JS never runs.
   -------------------------------------------------------------------------- */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(var(--reveal-offset));
  transition: opacity   var(--dur-reveal) var(--ease-out),
              transform var(--dur-reveal) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

.js [data-reveal='left']  { transform: translateX(calc(var(--reveal-offset) * -1.5)); }
.js [data-reveal='right'] { transform: translateX(calc(var(--reveal-offset) *  1.5)); }
.js [data-reveal='scale'] { transform: scale(0.96); }
.js [data-reveal='fade']  { transform: none; }

.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  /* The hint is dropped the moment it has been used. Left on, every
     revealed element keeps a compositor layer for the life of the page,
     and there are enough of them now for that to be worth reclaiming. */
  will-change: auto;
}

/* An image settling back out of a slight crop, rather than sliding — which
   suits a photograph better than a shove upwards. Goes on the <img>; the
   frame around it already clips.

   No fade: this sits inside a figure that is itself revealing, and two
   opacity transitions over one photograph reads as a fault rather than an
   effect. The scale runs longer than the frame's rise so the picture is
   still settling as its frame arrives. */
.js [data-reveal='zoom'] {
  opacity: 1;
  transform: scale(1.14);
  transition: transform calc(var(--dur-reveal) * 1.6) var(--ease-out);
}

/* A rule that draws itself. Needs transform-origin, so it is its own case
   rather than a translate. */
.js [data-reveal='rule'] {
  opacity: 1;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform calc(var(--dur-reveal) * 1.1) var(--ease-out);
}
.js [data-reveal='rule'].is-revealed { transform: scaleX(1); }

/* Stagger steps */
[data-reveal-delay='1'] { --reveal-delay: calc(var(--stagger-step) * 1); }
[data-reveal-delay='2'] { --reveal-delay: calc(var(--stagger-step) * 2); }
[data-reveal-delay='3'] { --reveal-delay: calc(var(--stagger-step) * 3); }
[data-reveal-delay='4'] { --reveal-delay: calc(var(--stagger-step) * 4); }
[data-reveal-delay='5'] { --reveal-delay: calc(var(--stagger-step) * 5); }
[data-reveal-delay='6'] { --reveal-delay: calc(var(--stagger-step) * 6); }

/* Auto-stagger direct children of a group */
.js [data-reveal-group] > * {
  opacity: 0;
  transform: translateY(var(--reveal-offset));
  transition: opacity   var(--dur-reveal) var(--ease-out),
              transform var(--dur-reveal) var(--ease-out);
}
.js [data-reveal-group].is-revealed > * { opacity: 1; transform: none; }
.js [data-reveal-group].is-revealed > :nth-child(1)  { transition-delay: calc(var(--stagger-step) * 0); }
.js [data-reveal-group].is-revealed > :nth-child(2)  { transition-delay: calc(var(--stagger-step) * 1); }
.js [data-reveal-group].is-revealed > :nth-child(3)  { transition-delay: calc(var(--stagger-step) * 2); }
.js [data-reveal-group].is-revealed > :nth-child(4)  { transition-delay: calc(var(--stagger-step) * 3); }
.js [data-reveal-group].is-revealed > :nth-child(5)  { transition-delay: calc(var(--stagger-step) * 4); }
.js [data-reveal-group].is-revealed > :nth-child(6)  { transition-delay: calc(var(--stagger-step) * 5); }
.js [data-reveal-group].is-revealed > :nth-child(7)  { transition-delay: calc(var(--stagger-step) * 6); }
.js [data-reveal-group].is-revealed > :nth-child(8)  { transition-delay: calc(var(--stagger-step) * 7); }
.js [data-reveal-group].is-revealed > :nth-child(9)  { transition-delay: calc(var(--stagger-step) * 8); }
.js [data-reveal-group].is-revealed > :nth-child(10) { transition-delay: calc(var(--stagger-step) * 9); }


/* --------------------------------------------------------------------------
   1b. SPLIT-LINE HEADINGS
   --------------------------------------------------------------------------
   Add [data-split] to a heading and splitLines() in app.js wraps every word
   in a masked box. Each word rises out of its own box with a slight tilt,
   and words are delayed by the LINE they ended up on — so a heading comes
   up a line at a time, which is the effect, while the DOM stays word-based.

   Word boxes rather than line boxes on purpose: re-grouping words into line
   wrappers means rebuilding the element, and these headings carry <strong>
   and <br>. Splitting text nodes in place leaves all of that untouched.

   The element keeps its text in an aria-label and the split spans are
   hidden from assistive tech, so it is read as one heading, not as a list
   of words.
   -------------------------------------------------------------------------- */

.js [data-split] { --line: 0; }

/* The mask. Padding and equal negative margin keep descenders and accents
   from being sliced off by the overflow. */
.split__w {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding: 0.14em 0.02em 0.18em;
  margin: -0.14em -0.02em -0.18em;
}

.split__wi {
  display: inline-block;
  transform: translateY(115%) rotate(var(--split-tilt));
  transform-origin: 0% 100%;
  transition: transform var(--dur-split) var(--ease-out);
  transition-delay: calc(var(--line) * var(--stagger-step));
}

.js [data-split].is-revealed .split__wi { transform: none; }


/* --------------------------------------------------------------------------
   2. KEN BURNS
   --------------------------------------------------------------------------
   A slow scale + drift on a full-bleed image. Put it on the <img> inside an
   overflow-hidden frame (.hero__media, .media, .unit-card__media).

     <div class="hero__media">
       <img class="kenburns" src="…" alt="">
     </div>

   Direction modifiers change where the drift ends up so consecutive
   sections do not all move the same way.
   -------------------------------------------------------------------------- */

.kenburns {
  transform-origin: center;
  animation: sbp-kenburns var(--dur-kenburns) var(--ease-linear) infinite alternate;
}

.kenburns--tl { animation-name: sbp-kenburns-tl; }
.kenburns--br { animation-name: sbp-kenburns-br; }
.kenburns--in { animation-name: sbp-kenburns-in; }

/* Only run once the frame is on screen — app.js adds .is-inview */
.kenburns--lazy { animation-play-state: paused; }
.is-inview .kenburns--lazy { animation-play-state: running; }

@keyframes sbp-kenburns {
  from { transform: scale(1.0)  translate3d(0, 0, 0); }
  to   { transform: scale(1.12) translate3d(-1.5%, -1.5%, 0); }
}
@keyframes sbp-kenburns-tl {
  from { transform: scale(1.12) translate3d(1.5%, 1.5%, 0); }
  to   { transform: scale(1.0)  translate3d(0, 0, 0); }
}
@keyframes sbp-kenburns-br {
  from { transform: scale(1.0)  translate3d(0, 0, 0); }
  to   { transform: scale(1.12) translate3d(1.5%, 1.5%, 0); }
}
@keyframes sbp-kenburns-in {
  from { transform: scale(1.0); }
  to   { transform: scale(1.14); }
}


/* --------------------------------------------------------------------------
   3. MARQUEE  (occupier logo strip)
   -------------------------------------------------------------------------- */

@keyframes sbp-marquee {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}


/* --------------------------------------------------------------------------
   4. AMBIENT
   -------------------------------------------------------------------------- */

/* The hero scroll cue */
@keyframes sbp-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(6px); }
}
.anim-bob { animation: sbp-bob 2.4s var(--ease-in-out) infinite; }

/* Gold pulse behind the live-map pin */
@keyframes sbp-pulse {
  0%   { box-shadow: 0 0 0 0    rgba(224, 175, 90, 0.55); }
  70%  { box-shadow: 0 0 0 18px rgba(224, 175, 90, 0);    }
  100% { box-shadow: 0 0 0 0    rgba(224, 175, 90, 0);    }
}
.anim-pulse { animation: sbp-pulse 2.6s var(--ease-out) infinite; }

/* Panel / accordion content entrance */
@keyframes sbp-fade-up {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
.anim-fade-up { animation: sbp-fade-up var(--dur-base) var(--ease-out) both; }


/* --------------------------------------------------------------------------
   5. PARALLAX HOOK
   --------------------------------------------------------------------------
   app.js writes --parallax-y (in px) on scroll for elements carrying
   [data-parallax]. The value is a fraction of scroll distance set by
   data-parallax="0.15".
   -------------------------------------------------------------------------- */

[data-parallax] {
  will-change: transform;
  transform: translate3d(0, var(--parallax-y, 0), 0);
}


/* --------------------------------------------------------------------------
   5b. SCROLL-SCRUBBED EXPAND
   --------------------------------------------------------------------------
   A framed image that opens out of the page gutter to the full width of the
   window as it is scrolled to. app.js writes --expand, 0 to 1; the negative
   margins here spend it.

     <figure class="aerial__frame" data-expand style="--frame-ratio: 1.688">

   THE HEIGHT IS LOCKED, and that is the whole trick. These frames size
   themselves with aspect-ratio, so widening one also makes it taller — the
   page would grow by ~95px while you scrolled into it and shove everything
   below it down as it went. Instead the height is derived once from the
   frame's RESTING width, so it never changes: the frame widens, the crop
   opens out, and nothing else on the page moves at all.

   Anchoring to the resting rather than the full-bleed width also means the
   picture looks exactly as it did before at rest, and turns cinematic as it
   opens rather than starting tall and staying that way.
   -------------------------------------------------------------------------- */

/* The track is the frame plus the distance it stays pinned for; the pin
   inside it is sticky, centred in the window. The page never stops
   scrolling — the frame simply holds still while it does, which is what
   gives it the room to open without anything being taken over.

   --frame-h is the resting height: the capped container (1600) or the
   window, whichever is smaller, less the gutter either side.
   --container-inset steps down at 900 and 600, so this holds at every
   width without a breakpoint of its own. */
.expand-track {
  --frame-h: calc(
    (min(100vw, var(--container-cap)) - var(--container-inset) * 2)
    / var(--frame-ratio, 1.688)
  );
  height: calc(var(--frame-h) + var(--expand-pin, 60vh));
}

.expand-pin {
  position: sticky;
  /* Centred in the space BELOW the header, not in the window: the header is
     fixed and paints over the top of the page, so splitting the whole
     viewport would sit the frame low by half the header. max(0px, …) covers
     the case where the frame is taller than the space it has, and simply
     holds it under the header rather than pushing its top off-screen. */
  top: calc(
    var(--header-height)
    + max(0px, (100svh - var(--header-height) - var(--frame-h)) / 2)
  );
}

[data-expand] {
  height: var(--frame-h);
  aspect-ratio: auto;

  /* --expand-max is how far each edge has to travel to reach the window,
     measured in app.js. It cannot be written here: from the capped state the
     distance is (window - 1600 + gutter) / 2, and the only CSS route to the
     window's width is 100vw — which counts the scrollbar the frame must not
     grow into. A measured pixel figure has no such problem. */
  margin-inline: calc(var(--expand-max, 0px) * var(--expand, 0) * -1);
}

@media (prefers-reduced-motion: reduce) {
  /* Nothing scrubs and nothing pins: the frames simply sit open, and the
     track collapses to the frame so the page keeps its normal length. */
  [data-expand]   { margin-inline: calc(var(--expand-max, 0px) * -1); }
  .expand-track   { height: var(--frame-h); }
  .expand-pin     { position: static; }
}


/* --------------------------------------------------------------------------
   6. REDUCED MOTION
   --------------------------------------------------------------------------
   Non-negotiable. Everything lands in its final state instantly.
   -------------------------------------------------------------------------- */

@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;
  }

  html { scroll-behavior: auto; }

  .js [data-reveal],
  .js [data-reveal-group] > *,
  .js .split__wi {
    opacity: 1 !important;
    transform: none !important;
  }

  /* The mask would otherwise clip type that is no longer moving through it */
  .split__w { overflow: visible; }

  .kenburns,
  .logo-strip--marquee .logo-strip__track,
  .anim-bob,
  .anim-pulse { animation: none !important; }

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