/* ==========================================================================
   UVEC: motion.css
   ---------------------------------------------------------------------------
   A motion LAYER. It adds nothing to the page and removes nothing from it:
   every rule here only changes how an element arrives, how it responds to a
   pointer, or how a background photograph drifts. Final resting position,
   size, colour and type are the same values style.css already sets, so the
   page at rest is byte-for-byte the page that was designed.

   It reuses style.css's own motion vocabulary rather than inventing a second
   one: --ease-out is the single curve for the whole site, and the reveal
   distance and duration below are the numbers .rise already uses.

   Load order matters: this file must come AFTER style.css so the few rules
   that refine an existing one (stagger delays, post-reveal hover) win on
   source order instead of needing !important.

   Fails safe in three directions:
     · JavaScript off  -> html.no-js keeps every guard below inert, so nothing
                          is ever hidden and nothing is ever offset.
     · reduce motion   -> every reveal, drift and count renders instantly in
                          its final state; the page is the original static one.
     · observer misses -> after 2.5s motion.js and main.js sweep in whatever is
                          already on screen, and keep sweeping on scroll until
                          the observer proves it fires.
   ========================================================================== */

:root {
  /* One curve, the one style.css already uses everywhere. */
  --m-ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* One duration scale. --m-dur matches .rise's 0.8s so an element revealed by
     this file and one revealed by style.css read as the same gesture. */
  --m-dur:       0.8s;   /* section / card reveal                    */
  --m-dur-lift:  0.34s;  /* hover lift, once the reveal has finished */
  --m-dur-nav:   0.42s;  /* header slide                             */

  --m-dist: 26px;        /* reveal travel, == .rise's translateY     */
  --m-step: 0.09s;       /* stagger between siblings in the same row */

  --m-px: 0.10;          /* parallax travel, as a fraction of band height */
}

/* Shorter distance and a quicker beat on small screens: the same reveal at
   26px over 0.8s reads as sluggish when the whole card is 200px tall. */
@media (max-width: 700px) {
  :root { --m-dist: 16px; --m-dur: 0.62s; --m-step: 0.07s; }
}

/* ==========================================================================
   1 · Stagger for the reveals style.css already owns
   --------------------------------------------------------------------------
   .rise carries an authored delay (.d1-.d4) wherever the designer wanted
   heading-then-body ordering, and no delay at all in the grids - so a ten-tile
   stat wall arrived as one block. motion.js measures which siblings share a
   visual row and sets --m-i to the column index; this turns that into the
   delay. Only flat groups get .m-stag, so authored .d1-.d4 ordering is left
   exactly as written.
   ========================================================================== */
.rise.m-stag { transition-delay: calc(var(--m-i, 0) * var(--m-step)); }

/* --------------------------------------------------------------------------
   Cards that were being asked to fade in with nothing to fade them.
   .pcard, .dcard, .vcard, .cert, .cap, .dprod and .program each declare their
   own `transition` for the hover properties they animate. `transition` is a
   shorthand, so every one of those declarations replaced .rise's list wholesale
   and left the reveal without an interpolation: the card was still starting at
   opacity 0 and 26px low, then arriving in a single frame. .pcard lost both
   properties; the rest lost opacity.

   Restored for the reveal only. Once the reveal has played, .m-hoverable below
   takes the property over at hover length, and on touch - where .m-hoverable
   has no rules - the component's own transition takes back over untouched.
   -------------------------------------------------------------------------- */
html:not(.no-js) :is(.pcard, .dcard, .vcard, .cert, .cap, .dprod, .program).rise:not(.m-hoverable) {
  transition: opacity var(--m-dur) var(--m-ease), transform var(--m-dur) var(--m-ease);
}

/* Mobile: shorten the travel and the duration of the site's own reveals too,
   so the whole page keeps one rhythm. Scoped to :not(.in-view) so the
   revealed state in style.css still wins. */
@media (max-width: 700px) {
  html:not(.no-js) .rise:not(.in-view) {
    transform: translateY(var(--m-dist));
    transition-duration: var(--m-dur);
  }
}

/* ==========================================================================
   2 · Reveals for the groups that had none
   --------------------------------------------------------------------------
   The hidden state is declared here, structurally, rather than applied by
   script: a class added at DOMContentLoaded would let an above-the-fold card
   paint once and then blink out. Listed as a selector, the card is simply
   never painted visible in the first place.

   The list below is mirrored exactly by GROUPS in motion.js. Keep them
   together.

   Driven by an animation rather than a transition on purpose: when it ends
   motion.js adds .m-done, which drops the animation and hands `transform`
   back to the cascade - which is what lets the hover lift in section 5 exist
   at all on an element that was revealed by translating it.
   ========================================================================== */
@keyframes m-rise-in {
  from { opacity: 0; transform: translateY(var(--m-dist)); }
  to   { opacity: 1; transform: none; }
}

html:not(.no-js) :is(
  .clients__bar,
  .hslider__card,
  .svcmx__row,
  .filterbar__field,
  .filterbar__count,
  .groupband__title,
  .groupband__count,
  .footer__intro,
  .footer__col,
  .footer__legal
) { opacity: 0; }

html:not(.no-js) .m-in {
  animation: m-rise-in var(--m-dur) var(--m-ease) both;
  animation-delay: calc(var(--m-i, 0) * var(--m-step));
}
/* Resting state once the reveal has played: no animation holding the
   properties down, so hover and the sliders' own transforms work normally. */
html:not(.no-js) .m-done { opacity: 1; animation: none; }

/* ==========================================================================
   3 · Parallax on large section photographs
   --------------------------------------------------------------------------
   Only where a photograph is the full background of a band, and only where
   there is room to move it: the image is scaled to 112% of the band and hung
   6% high, so it can travel +/-5% without ever showing an edge. The travel is
   centred, which means the crop at mid-scroll - the way the band is seen -
   is the crop style.css set. motion.js drives translate3d only.

   Off entirely for reduced motion, for coarse pointers and for narrow
   viewports, where the same rule would only cost battery: there the img is
   left at style.css's exact 100%/inset:0 and the crop is unchanged.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) and (min-width: 861px) and (hover: hover) and (pointer: fine) {
  html:not(.no-js) .cycle { overflow: hidden; }
  html:not(.no-js) :is(.page-hero__bg, .groupband__bg) > img,
  html:not(.no-js) .cycle > .cycle__stage {
    position: absolute;
    left: 0;
    top: -6%;
    width: 100%;
    height: 112%;
    will-change: transform;
  }
}

/* ==========================================================================
   4 · Header: compact-on-scroll is already style.css's; this adds the retreat
   --------------------------------------------------------------------------
   Slides out of the way once the visitor is reading downward and comes back
   the moment they scroll up. The transition list repeats style.css's own so
   that adding transform does not drop the background/height cross-fade the
   .is-solid state depends on.
   ========================================================================== */
html:not(.no-js) .header {
  transition: background var(--dur-fast) var(--ease-out),
              height var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              transform var(--m-dur-nav) var(--m-ease);
}
html:not(.no-js) .header.m-nav-away { transform: translateY(-100%); }
/* Never while the mobile overlay is open: the close button lives in the bar. */
html.mnav-open .header.m-nav-away { transform: none; }

/* ==========================================================================
   5 · Micro-interactions
   --------------------------------------------------------------------------
   style.css already carries most of this: the .vcard/.cert/.cap/.dprod family
   lifts, the card photography zooms inside its frame, .pcard wipes in a cyan
   hairline, .nav__link draws its underline in from the left. The gap was
   timing, not intent - those cards are also .rise elements, so their hover
   lift inherited .rise's 0.8s reveal transition and answered the pointer a
   beat late. .m-hoverable is added once the reveal has finished: it gives
   transform a hover-length duration without touching the reveal.

   Links that already carry a standing cyan rule (.footer__email,
   .dcard__link, .contact__detail-val a) are left alone on purpose - drawing a
   second line in over the first would read as a doubled underline, not as an
   accent.
   ========================================================================== */
@media (hover: hover) and (pointer: fine) {
  html:not(.no-js) .m-hoverable {
    /* The full hover set, not just transform: this declaration replaces the
       element's own transition, and .btn/.btn-pill cross-fade background and
       colour on hover. Leaving those out would have made the buttons snap. */
    transition: transform var(--m-dur-lift) var(--m-ease),
                box-shadow var(--m-dur-lift) var(--m-ease),
                border-color var(--m-dur-lift) var(--m-ease),
                background-color var(--m-dur-lift) var(--m-ease),
                color var(--m-dur-lift) var(--m-ease),
                opacity var(--m-dur-lift) var(--m-ease);
  }

  /* The one card style.css deliberately left flat, because .rise was using
     its transform. After .m-hoverable that is no longer true, so it can now
     take the same lift as every other card on the site. */
  html:not(.no-js) .pcard.m-hoverable:hover { transform: translateY(-4px); }

  /* The office row's action link is the last uppercase link on the site with
     no underline of its own. Same gesture, same hairline, same origin as
     .nav__link::after. */
  .office__link { position: relative; }
  .office__link::after {
    content: "";
    position: absolute;
    left: 0; bottom: -3px;
    width: 100%; height: 2px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--m-dur-lift) var(--m-ease);
  }
  .office__link:hover::after { transform: scaleX(1); }

  /* Footer link columns: the site's own idiom for a list row under the
     pointer (.linklist__row) is a nudge inward. Transform, so the column
     keeps its width and nothing round it reflows. */
  .footer__col a { transition: color var(--dur-fast) var(--ease-out), transform var(--m-dur-lift) var(--m-ease); }
  .footer__col a:hover { transform: translateX(4px); }

  /* Buttons: a hair of lift under the shadow style.css already fades in.
     1.5px, not a bounce. */
  .btn:hover, .btn-pill:hover { transform: translateY(-1.5px); }
}

/* ==========================================================================
   6 · Counters
   --------------------------------------------------------------------------
   motion.js counts the number already written in the HTML up from zero and
   restores the original string, character for character, on the last frame.
   While it runs, the figure is locked to the height it will end at and set to
   tabular figures: a four-digit number narrowing to one digit must not be
   allowed to re-wrap the line or jitter its own glyph widths. Both are
   removed when the count lands, so the resting figure is untouched.
   ========================================================================== */
.m-counting { font-variant-numeric: tabular-nums; }

/* ==========================================================================
   7 · In-page anchors
   --------------------------------------------------------------------------
   style.css already scrolls smoothly. What it did not have was clearance: the
   header is fixed, so #solar landed underneath it. scroll-margin affects only
   where a scroll stops - it has no effect on layout at any other time.
   ========================================================================== */
html:not(.no-js) [id]:not(html):not(body):not(#main) {
  scroll-margin-top: calc(var(--header-solid-h) + 24px);
}

/* ==========================================================================
   8 · Reduced motion
   --------------------------------------------------------------------------
   Everything this file adds, withdrawn. What is left is the original static
   site: nothing hidden, nothing offset, nothing drifting, no header retreat.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  html :is(
    .clients__bar,
    .hslider__card,
    .svcmx__row,
    .filterbar__field,
    .filterbar__count,
    .groupband__title,
    .groupband__count,
    .footer__intro,
    .footer__col,
    .footer__legal
  ) { opacity: 1 !important; animation: none !important; transform: none !important; }

  html .m-in, html .m-done { animation: none !important; }
  html .rise.m-stag { transition-delay: 0s !important; }
  html .header.m-nav-away { transform: none !important; }
  html .m-hoverable { transition: none !important; }
}

/* ==========================================================================
   9 · Print
   --------------------------------------------------------------------------
   A reveal that has not been scrolled to yet is an element at opacity 0, and
   on paper there is no scrolling to do. Everything prints in its final state,
   which is the state the page was designed in.
   ========================================================================== */
@media print {
  html :is(
    .rise,
    .reveal-line > span,
    .clients__bar,
    .hslider__card,
    .svcmx__row,
    .filterbar__field,
    .filterbar__count,
    .groupband__title,
    .groupband__count,
    .footer__intro,
    .footer__col,
    .footer__legal
  ) { opacity: 1 !important; transform: none !important; animation: none !important; }
  html .header.m-nav-away { transform: none !important; }
}
