/* =========================================================================
   motion.css — effects taken off gensler-experiments.
   Catalogue and exact timings: docs/motion-research.md.

   Loaded after styles.css and home.css so it can override the shared
   reveal-on-scroll, and only on the pages that also load motion.js.
   Every curve and length comes from the motion tokens in styles.css.
   ========================================================================= */

/* ------------------------------------------------------- scroll lines -- §2.2
   Gensler's SCROLL_LINES: the reveal it puts on every heading and every
   paragraph of prose — a longer rise and a longer curve than the reveal the
   rest of the page uses. Nothing is masked or split; the text is simply
   further down and takes longer to arrive.

   It rides the shared reveal rather than watching the viewport itself: the
   element keeps data-reveal, so it keeps its place in its group's cascade,
   its wait behind the page-move curtain, and .is-in from script.js. Only the
   distance and the clock change, and both are beaten in on specificity so
   the base rule is replaced rather than layered. */
[data-reveal][data-scroll-lines] {
  transform: translate3d(0, 30px, 0);
  transition: opacity   var(--t-line) var(--e-out),
              transform var(--t-line) var(--e-out);
}
[data-reveal][data-scroll-lines].is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal][data-scroll-lines],
  [data-reveal][data-scroll-lines].is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ----------------------------------------------------------- fade up -- §2.2
   The card half of the same family. Gensler does not put SCROLL_LINES on
   cards — it keeps the curve and swaps the numbers: a shorter rise (24
   rather than 30) and a shorter clock, because a card is a block arriving,
   not a line being read.

   Two presets, and the split is deliberate in the original: a card standing
   on its own takes FADE_UP's 1.256s, while a card in a row takes
   STAGGER_UP's 0.9s — staggered things have to be quicker or the last one in
   the row is still arriving long after the reader has moved on. Ours already
   cascade at 60ms through data-reveal-group, so the row cards take the
   shorter clock and keep the cascade the page already had. */
:root { --t-card: .9s; }   /* Gensler STAGGER_UP.duration */

/* 24 is Gensler's FADE_UP. It is a default rather than a constant because
   the preset is right for a card and short for a panel the width of the
   page — the same reason [data-stagger-item] carries --stagger-y. */
[data-reveal][data-fade-up] {
  transform: translate3d(0, var(--fade-y, 24px), 0);
}
/* Cards travel further than the panels do — the same --stagger-y the
   one-at-a-time rows use, so every card on the page rises by one number. */
[data-reveal][data-stagger-item] {
  transform: translate3d(0, var(--stagger-y, 56px), 0);
}
/* `background-color` rides along in all three presets below, and it has
   nothing to do with the reveal.

   `transition` is a shorthand: it replaces the whole property list rather
   than adding to it. These presets are (0,2,0) and a component's own rule is
   usually (0,1,0), so every hover transition a card declared for itself was
   being silently thrown away — the card kept the declaration, the property
   simply stopped participating. Four teal fills on the two pages snapped
   instead of arriving because of it: `.hp-svc`, `.hp-post`, `.pdp-feat`,
   `.pdp-path`, each with a correct `background-color .25s` of its own that
   never ran.

   Declaring it here is what makes those work again, and it is declared for
   everything rather than for the four: a card that never changes colour
   transitions nothing, and the next one that does will not have to find this
   out the hard way. The numbers are the ones the fills that *did* work
   already used (`.hp-tile`, `.hp-logo`, `.pdp-row__text`, `.pdp-helps__item`),
   so every teal surface on the site now arrives at one speed.

   `--e-inout` is `--hp-ease` from home.css to the digit; the global token is
   named here so this sheet keeps depending only on styles.css. */
[data-reveal][data-fade-up] {
  transition: opacity          var(--t-block) var(--e-out),
              transform        var(--t-block) var(--e-out),
              background-color .25s           var(--e-inout);
}
[data-reveal][data-stagger-item] {
  transition: opacity          var(--t-card) var(--e-out),
              transform        var(--t-card) var(--e-out),
              background-color .25s          var(--e-inout),
              /* the hover stroke rides this — it was not in the list, so on
                 the product page's cards the line appeared in one frame */
              box-shadow       var(--t-tap)  var(--e-out),
              border-radius    var(--t-tap)  var(--e-out);
}
[data-reveal][data-fade-up].is-in,
[data-reveal][data-stagger-item].is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal][data-fade-up],
  [data-reveal][data-stagger-item],
  [data-reveal][data-fade-up].is-in,
  [data-reveal][data-stagger-item].is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ------------------------------------------------- staggered children -- §2.2
   Cards that arrive one after another from a single trigger.

   Deliberately NOT one observer per card. These live in a carousel whose
   viewport clips: the fourth card starts 39px from the right edge and the
   ones after it are outside altogether, so a card watched on its own never
   intersects and would sit invisible until the reader paged across to it.
   The container is what gets watched — it already carries data-reveal — and
   the children follow it on a delay. That is also how Gensler does it:
   initStaggerUp observes the container, not the items.

   Values are STAGGER_UP's: the same 24 and the same 0.9s the other cards on
   the page use. motion.js sets the per-child delay from the attribute. */
/* The arrival of every card that comes one at a time: it rises and fades.

   This carried CardMask — the angled sliver that opens to the rectangle —
   until the client asked for the unfold to be the platform cards' signature
   and nothing else's. What is left is the plain rise, which is what the rest
   of the page does anyway; the sliver now appears exactly once, in the six
   cards under "Everything AI needs", where it means something.

   Watching the container and not the cards is what makes this usable in a
   carousel at all: the fourth card starts 39px from the right edge and the
   ones after it are off the rail entirely, so a card watched on its own
   would never intersect and would sit invisible until the reader paged
   across to it.

   `transition` is a shorthand: anything left to a lower rule is dropped, not
   merged — the trap at 0.10. */
[data-stagger-in] [data-stagger-child] {
  opacity: 0;
  /* In from the RIGHT, not up from below. Every one of these lives in a
     carousel, and a rail whose cards rise into place says nothing about what
     is off the edge; cards that come in from beyond the right edge say the
     row continues out there before the reader has touched anything.

     Two parts. The lead is what every card travels. The rake is what its
     place in the row adds on top, so the fourth card starts further out than
     the first and the row arrives as a rake rather than one sliding plate —
     motion.js publishes the index as --stagger-i. The last card of a seven
     starts 276 out. There is a floor under these numbers: at the 64 + 26
     this began on, the horizontal travel was swamped by the fade and the row
     just looked like it was appearing rather than arriving from anywhere.

     The viewport's own `overflow: hidden` does the rest: a card starting a
     few hundred out is genuinely off the rail, so it does not appear beside
     the page's content and then move — it comes in from outside it. */
  transform: translate3d(
    calc(var(--stagger-lead, 72px) + var(--stagger-i, 0) * var(--stagger-rake, 34px)),
    0, 0);
  transition: opacity          1.2s  var(--e-reveal),
              transform        1.2s  var(--e-reveal),
              background-color .25s  var(--e-inout),
              box-shadow       var(--t-tap) var(--e-out),
              border-radius    var(--t-tap) var(--e-out);
}
[data-stagger-in].is-in [data-stagger-child] {
  opacity: 1;
  transform: none;
}

/* The container is the trigger, not a participant: without this it would
   fade in as one block and the cards would then fade in again inside it. */
[data-reveal][data-stagger-in] {
  opacity: 1;
  transform: none;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  [data-stagger-in] [data-stagger-child],
  [data-stagger-in].is-in [data-stagger-child] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ------------------------------------------------------- rolling text -- §3.3
   Palmer's trick, on the footer's links: the second copy of every letter is
   not a second set of elements, it is a text-shadow one line further down.
   The letters roll up, the copies underneath take their place, and the DOM is
   half the size it would be with real duplicates.

   The roll IS the colour change. The link rests grey and the shadow copy is
   drawn white, so what arrives on hover is the white row — no colour
   transition needed, and the palette is the one the footer already had.

   motion.js splits the label and sets --roll-lh, the shadow's offset, which
   has to be an absolute length. */
.roll {
  display: inline-flex;
  overflow: hidden;
  vertical-align: top;
  text-shadow: 0 var(--roll-lh) 0 var(--roll-to, currentColor);
}
.roll > span {
  display: block;
  flex: none;
  white-space: pre;
  line-height: var(--roll-lh);
  /* Safari flickers along the subpixel edge of a transformed inline box
     without this — the same reason Palmer carries it */
  backface-visibility: hidden;
  transition: transform var(--t-tap) var(--e-out);
}
a.is-rolled:hover .roll > span,
a.is-rolled:focus-visible .roll > span {
  transform: translateY(-100%);
}

.hf__links a.is-rolled,
.hf__legal a.is-rolled { --roll-to: #fff; }

/* The link's own grey-to-white fade is what the roll replaces. Left running,
   the letters would brighten on their way out and the two rows would read as
   one blur instead of one row replacing another. The colours are unchanged:
   grey at rest, white on hover — it is the roll that delivers the white. */
.hf__links a.is-rolled:hover,
.hf__legal a.is-rolled:hover { color: rgba(255, 255, 255, .5); }

@media (prefers-reduced-motion: reduce) {
  .roll > span { transition: none; }
  a.is-rolled:hover .roll > span,
  a.is-rolled:focus-visible .roll > span { transform: none; }
  /* with no roll to carry it, the colour fade is the hover again */
  .hf__links a.is-rolled:hover,
  .hf__legal a.is-rolled:hover { color: #fff; }
}

/* ---------------------------------------------------------- line flip -- §3.1
   The whole line arriving from under a mask, rather than character by
   character. Gensler's text-flip: a track holding the text slides up from one
   height below, and the element it sits in does the clipping.

   The element itself is the mask — motion.js only wraps its contents in the
   track. Nothing is inserted around it, so its box, its classes and, in the
   FAQ, its place as a flex item with `flex: 1 1 auto; min-width: 0` are
   exactly what they were.

   The padding is the price of that: the cap trim sits the box on the
   baseline, so a mask flush with it would cut the tail off a g or a y for
   good, not just while the line is arriving. The negative margin gives the
   room back to the layout. In em, and on the element itself, so it is the
   element's own type size that sets it — the trap the first version of this
   fell into was putting the em on a wrapper, where it resolved against the
   parent's 16px and came out four times too small. */
.lineflip {
  overflow: hidden;
  padding-bottom: .3em;
  margin-bottom: -.3em;
}
.lineflip__track {
  display: block;
  /* 100% is the track's own box, and the cap trim makes that box shorter than
     the mask it has to clear — 12px against 17 on the FAQ's 18px type. The
     .4em covers the .3em of padding the mask carries plus a little, so the
     line is genuinely gone at rest whether it is one line or three. */
  transform: translateY(calc(100% + .4em));
}
.lineflip.is-in .lineflip__track {
  transform: none;
  transition: transform var(--t-line) var(--e-out);
}

@media (prefers-reduced-motion: reduce) {
  .lineflip { overflow: visible; padding-bottom: 0; margin-bottom: 0; }
  .lineflip__track,
  .lineflip.is-in .lineflip__track { transform: none; transition: none; }
}

/* ---------------------------------------------------------- char flip -- §3.4
   How 287,000 arrives on Gensler, and the same movement on a headline. There
   is a mask, but there is one per character, not one across the line: every
   character sits in its own clip with two identical copies stacked inside it,
   the track starts one character height down so the clip shows the empty
   space above the top copy, and each track slides back to zero a beat after
   the one before it. The beat is what the eye reads as a wave.

   motion.js builds the structure and measures the height — it cannot be
   assumed, because the clip has to be exactly one character tall or the
   characters arrive through a gap. */
.charflip {
  display: block;
}

/* An element that flips per character owns its own entrance, so the shared
   reveal must not also fade or lift it. The hook is the class motion.js adds,
   not the attribute: before the script runs the element is still hidden by
   [data-reveal] as normal, so nothing flashes, and if the script never runs
   the plain reveal is what happens. Keeping data-reveal on it also keeps its
   place in its group, so the siblings' cascade is unchanged. */
[data-reveal].charflip {
  opacity: 1;
  transform: none;
  transition: none;
}

/* The eyebrow's mark rides inside this: a box the size of the mark that
   takes its place in the flex row, so the row measures exactly as before and
   only the mark moves. JS sets the width and height from the mark itself. */
.eyebrow-flip__clip {
  flex: none;
  display: block;
  overflow: hidden;
}

/* Same neutraliser as above, for the eyebrow's own wrapper: the choreography
   owns the entrance, so the shared reveal must not fade or lift the line as
   well. Hooked on the class JS adds, for the same reasons. */
[data-reveal].eyebrow-flip {
  opacity: 1;
  transform: none;
  transition: none;
}

/* Every character is its own inline block, so without this a line could break
   between any two of them and split a word down the middle. Words refuse to
   break; the plain spaces motion.js leaves between them are where the line is
   allowed to go. */
.charflip__word {
  display: inline-block;
  white-space: nowrap;
}

.charflip__clip {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
}
.charflip__track {
  display: flex;
  flex-direction: column;
  /* No will-change here on purpose. There are 35 of these on the home page,
     and a standing will-change is a standing compositor layer for each — paid
     for the whole visit to save a few milliseconds once. The WAAPI animation
     promotes the track itself while it runs, which is when it matters. */
}
.charflip__char {
  display: block;
  white-space: pre;
  line-height: inherit;
}
