/* =========================================================================
   cw-motion.css
   Motion kit: reveal-on-scroll presets, text effects, marquee, carousel,
   card-mask — pairs with cw-core.js and cw-motion.js.

   Data-attribute API this sheet responds to:
     [data-reveal]           base fade-up reveal; cw-core.js adds .is-in
     [data-scroll-lines]     longer rise + longer curve for headings/prose
     [data-fade-up]          block preset (24px rise, --t-block clock)
     [data-stagger-item]     card preset (56px rise, --t-card clock)
     [data-reveal-group]     60ms cascade, delays set inline by cw-core.js
     [data-stagger-in] /
       [data-stagger-child]  container-triggered rake-in from the right;
                             cw-motion.js publishes the index as --stagger-i
     [data-char-flip]        per-character masked flip (cw-motion.js builds
                             .charflip structure and the WAAPI animation)
     [data-line-flip]        whole-line masked flip (.lineflip / __track)
     [data-eyebrow]          eyebrow composite entrance (.eyebrow-flip)
     [data-hover-roll]       per-character hover roll (JS-driven)
   Classes set by cw-core.js: .is-in (revealed), .is-open (menus/lightbox).
   Carousel classes set by cw-core.js: .is-dragging, .is-end, .is-static.
   JS-set custom properties: --roll-lh (roll shadow offset), --stagger-i.

   Extracted from the cw-landing prototype (motion.css + styles.css +
   home.css), 2026-09-21.
   ========================================================================= */

/* ------------------------------------------------------------------ tokens --
   One vocabulary for every transition on the site (styles.css), plus the
   home.css variables the extracted rules below reference — defined here so
   this file is self-contained. */
:root {
  /* motion — one vocabulary for every transition on the site.
     The first two are the curves the page already ran on: 45 rules use the
     first and seven the second, so they are named here rather than replaced,
     and nothing existing changes. The rest come off the three reference sites
     (docs/motion-research.md) and are the set new work draws from. */
  --e-page:     cubic-bezier(.22, 1, .36, 1);      /* the site's own curve */
  --e-inout:    cubic-bezier(.65, 0, .35, 1);      /* footer, form */
  --e-out:      cubic-bezier(.53, .2, 0, 1);       /* the standard exit */
  --e-in-out:   cubic-bezier(.615, .05, 0, 1);     /* the standard throw */
  --e-reveal:   cubic-bezier(.615, .05, .3, 1);    /* masks — gentle landing */
  --e-expo-out: cubic-bezier(.16, 1, .3, 1);       /* GSAP expo.out */
  --e-power3:   cubic-bezier(.215, .61, .355, 1);  /* GSAP power3.out */
  --e-snap:     cubic-bezier(.82, .11, .37, .82);  /* micro-hovers, quick */
  --e-in:       cubic-bezier(.55, .055, .675, 1);  /* leaving, hiding */

  /* Durations on the scale all three references share: a hover is a quarter
     of a second, a block arriving is over a second, a full-screen move is
     nearly two. Reach for one of these rather than typing a number. */
  --t-hover:  .256s;
  --t-tap:    .512s;
  --t-enter:  .768s;
  --t-block: 1.256s;
  --t-line:  1.512s;
  --t-scene: 1.768s;

  /* Gensler STAGGER_UP.duration (motion.css — merged into this block) */
  --t-card: .9s;

  /* home.css variables the extracted rules reference */
  --hp-ease: cubic-bezier(.65, 0, .35, 1);
  --hp-ink:        #02030b;
  --hp-ring:       rgba(2, 3, 11, .30);   /* the circled arrows at rest   */
  --hp-ring-hover: var(--hp-ink);        /* the ring, hovered — black */
  --hp-g20:    clamp(12px, 1.389vw, 20px);

  /* Default for the roll's shadow offset. cw-motion.js measures the real
     line box and overwrites this with an absolute length per link; 1lh keeps
     the roll sane if the script has not run yet. (--fade-y, --stagger-y,
     --stagger-lead, --stagger-i and --stagger-rake carry their defaults
     inline as var() fallbacks, as in the prototype.) */
  --roll-lh: 1lh;
}

/* ═════════════════════════════════════════════════════════════════════════
   1. Base reveal-on-scroll (styles.css)
   ═══════════════════════════════════════════════════════════════════════ */

/* ------------------------------------------------------- reveal-on-scroll -- */
[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition: opacity .7s cubic-bezier(.22, 1, .36, 1),
              transform .7s cubic-bezier(.22, 1, .36, 1);
}
[data-reveal].is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1; transform: none; transition: none; }
}

/* ═════════════════════════════════════════════════════════════════════════
   2. Motion presets (motion.css — effects taken off gensler-experiments.
      Loaded after the base reveal so it can override it; every curve and
      length comes from the motion tokens above.)
   ═══════════════════════════════════════════════════════════════════════ */

/* ------------------------------------------------------- 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 cw-core.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. */
/* --t-card (Gensler STAGGER_UP.duration) is defined in the :root block above. */

/* 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. cw-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 —
     cw-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.

   cw-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 — cw-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.

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

/* ═════════════════════════════════════════════════════════════════════════
   3. Home-page motion extracts (home.css)
   ═══════════════════════════════════════════════════════════════════════ */

/* ---------------------------------------------------- 3a. logo marquee ----
   The row has to clip horizontally — it holds the seven logos twice and
   slides — but `overflow` clips both axes, so a shadow on a card would be
   cut off flush at the row's top and bottom edges. The padding is the room
   the shadow needs *inside* the clip, and the negative margin gives that
   room back to the layout, so the logos sit exactly where they sat.

   The gap between the two rows then has to be restated: the first row's
   -20 bottom collapses with whatever the second declares, and the two
   paddings add 40 between the logos, so the second needs 17 - 40 to land on
   the 17 the frame draws. It is more negative than -20, which is what makes
   the collapse pick it. */
.hp-marquee {
  /* Was 20: enough for the hover shadow, not for the 44 the cards now rise
     through — the bottom of the arrival was being cut by this very box.
     Padding and the negative margin below cancel, so this costs no layout. */
  --hp-logo-air: 52px;
  overflow: hidden;
  padding: var(--hp-logo-air) 0;
  margin: calc(var(--hp-logo-air) * -1) 0;
}
.hp-marquee + .hp-marquee { margin-top: calc(17px - var(--hp-logo-air) * 2); }

/* The rows run on a clock of their own — cw-core.js advances this transform
   at a constant speed, so the logos move whether or not the page does, and
   slow to a fifth under the cursor. The row holds the seven logos twice,
   which is what lets that transform wrap seamlessly. */
.hp-marquee__row {
  display: flex;
  gap: var(--hp-g20);
  width: max-content;
  will-change: transform;
}

/* The logo cards rise 44 and fade in, stepped along the row. They opened on
   the CardMask for a while; the unfold is the platform cards' alone now, at
   the client's request, and this is the plain arrival underneath it.

   Driven from the marquee rather than from each card, and that is not a
   shortcut. Every row holds its seven logos twice so the scroll transform can
   wrap, and the row is far wider than its clipped box — a per-card observer
   would never see the copies parked outside it, and they would sit at
   opacity 0 forever, appearing as gaps as the row wrapped around. One
   observer on the marquee opens all of them, the off-screen ones included.

   So the marquee itself stops moving: it is the trigger now, not a thing that
   arrives. (0,3,0) on the second selector, over the base [data-reveal]. */
.hp-marquee[data-reveal],
.hp-marquee[data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: none;
}

.hp-marquee[data-reveal] .hp-logo {
  --hp-logo-in: 0ms;
  opacity: 0;
  transform: translate3d(0, 44px, 0);
  /* The delay rides the three arrival properties only. Put on the shorthand
     it would sit on box-shadow as well, and the seventh card would make you
     wait out its place in the queue before it lifted under the cursor. */
  transition: opacity       1.2s var(--e-reveal) var(--hp-logo-in),
              transform     1.2s var(--e-reveal) var(--hp-logo-in),
              box-shadow    var(--t-tap) var(--e-out),
              border-radius var(--t-tap) var(--e-out);
}
.hp-marquee[data-reveal].is-in .hp-logo {
  opacity: 1;
  transform: none;
}

/* Stepped by position in the row, not by index: 7n+1 and 7n+8 are the same
   logo, so a card and its copy open on the same beat and the wrap stays
   invisible. */
.hp-marquee .hp-logo:nth-child(7n+2) { --hp-logo-in:  60ms; }
.hp-marquee .hp-logo:nth-child(7n+3) { --hp-logo-in: 120ms; }
.hp-marquee .hp-logo:nth-child(7n+4) { --hp-logo-in: 180ms; }
.hp-marquee .hp-logo:nth-child(7n+5) { --hp-logo-in: 240ms; }
.hp-marquee .hp-logo:nth-child(7n+6) { --hp-logo-in: 300ms; }
.hp-marquee .hp-logo:nth-child(7n+7) { --hp-logo-in: 360ms; }

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

/* --------------------------------------- 3b. service-card CardMask -------
   CardMask — the handoff's own arrival, back again. The card is an angled
   sliver that opens to the full rectangle while it rises 44 and fades in.
   Four points at both ends of the polygon, which is what lets it interpolate
   at all; a different count between the two keyframes and it would jump.

   (0,2,0), so it beats the base [data-reveal] outright rather than tying with
   it. Everything the card transitions is named here on purpose: `transition`
   is a shorthand and replaces the list, so anything left to a lower rule
   would be silently dropped — the trap at 0.10. There is nothing else to
   name now that the hover fill is gone. */
.hp-svc[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 44px, 0);
  clip-path: polygon(0% 6%, 61% 37%, 100% 94%, 30% 67%);
  transition: opacity       1.2s var(--e-reveal),
              transform     1.2s var(--e-reveal),
              clip-path     1.2s var(--e-reveal),
              box-shadow    var(--t-tap) var(--e-out),
              border-radius var(--t-tap) var(--e-out);
}
/* Six angles, one per card, so the row does not open six times the same way.

   Each is a thin rectangle through the card's centre, rotated, and long
   enough that its ends fall outside the box — the edges do the trimming, so
   each one is a true sliver rather than a shape fitted by eye.

   The vertex ORDER is the whole trick. clip-path interpolates point to point,
   and the open state is (0,0) (100,0) (100,100) (0,100) in that order; a
   sliver whose first vertex starts nowhere near the top-left has to drag
   across the box to reach it, and on the way the quad folds through a
   hairline — rendered, 136° and 162° collapsed to a line at 15% and then
   swelled back out. So each ring is rotated to send every vertex to its
   nearest corner. All six widen monotonically now; checked frame by frame at
   0 / .15 / .35 / .6 / 1.

   (0,3,0), over the (0,2,0) that sets the shared shape above — and placed
   ABOVE the open state on purpose. `.hp-svc[data-reveal].is-in` weighs
   (0,3,0) as well, so nothing but source order separates them. Below it,
   each card kept its own sliver after arriving: the clip never changed,
   there was nothing to transition, and the only thing that ever opened the
   card was the inline release 2.2s later — one frame of sliver, one frame
   of whole card. */
.hp-svc[data-reveal]:nth-of-type(1) { clip-path: polygon(-84% -17%, 190% 105%, 184% 117%, -90% -5%); }  /* 24° */
.hp-svc[data-reveal]:nth-of-type(2) { clip-path: polygon(-48% -64%, -37% -73%, 148% 164%, 137% 173%); }  /* 52° */
.hp-svc[data-reveal]:nth-of-type(3) { clip-path: polygon(12% -95%, 26% -98%, 88% 195%, 74% 198%); }  /* 78° */
.hp-svc[data-reveal]:nth-of-type(4) { clip-path: polygon(90% -95%, 103% -90%, 10% 195%, -3% 190%); }  /* 108° */
.hp-svc[data-reveal]:nth-of-type(5) { clip-path: polygon(-63% 149%, 153% -59%, 163% -49%, -53% 159%); }  /* 136° */
.hp-svc[data-reveal]:nth-of-type(6) { clip-path: polygon(-95% 90%, 190% -3%, 195% 10%, -90% 103%); }  /* 162° */

.hp-svc[data-reveal].is-in {
  opacity: 1;
  transform: none;
  clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}

@media (prefers-reduced-motion: reduce) {
  /* :nth-of-type(n) matches every card and carries the (0,3,0) needed to
     out-weigh the six per-card slivers above; without it they would survive
     into reduced motion and the cards would arrive as splinters. */
  .hp-svc[data-reveal]:nth-of-type(n),
  .hp-svc[data-reveal].is-in {
    opacity: 1;
    transform: none;
    clip-path: none;
    transition: none;
  }
}

/* ----------------------------------------------- 3c. carousel motion -----
   Three of them on the page and one behaviour between them: a track that
   translates by whole pages, a chevron that advances one, dots that jump,
   and pointer drag. Layout declarations (display, gap, padding) are built
   natively in Webflow — only the motion/interaction declarations ride here. */
.hp-carousel__track {
  will-change: transform;
  transition: transform .7s cubic-bezier(.22, 1, .36, 1);
  cursor: grab;
  touch-action: pan-y;
}
.hp-carousel.is-dragging .hp-carousel__track { transition: none; cursor: grabbing; }

/* The arrow takes the circled ring the rest of the page uses, and its hover
   with it. Ring geometry/colour at rest is native; the motion is here. */
.hp-carousel__next {
  /* 2026-09-21 (client): same as `.hp-arrow` — the CTA's .12s linear. */
  --dur:  .12s;
  --hp-ease: linear;
  transition: transform var(--dur) var(--hp-ease);
}
.hp-carousel__next::before {
  transition: box-shadow var(--dur) var(--hp-ease);
}
@media (hover: hover) and (min-width: 721px) {
.hp-carousel__next:is(:hover, :focus-visible)::before {
  box-shadow: inset 0 0 0 1.5px var(--hp-ring-hover);
}
}
/* the glyph rides above the ring, and is the only thing the press moves */
.hp-carousel__next svg { position: relative; transition: transform var(--dur) var(--hp-ease), scale var(--dur) var(--hp-ease); }
/* the glyph grows 20% on hover, like every round arrow; `scale` so it keeps the is-end flip */
@media (hover: hover) and (min-width: 721px) {
.hp-carousel__next:is(:hover, :focus-visible) svg { scale: 1.3; }
}
.hp-carousel__next:active { transform: scale(.94); }
.hp-carousel__next[disabled] { opacity: 0; pointer-events: none; }

/* The side it points is the direction it goes. On the last page there is
   nothing further right to reach, so it turns around and goes back a page —
   `is-end` is set by cw-core.js, which owns the index, and it swaps the label
   from Next to Previous at the same moment. */
.hp-carousel.is-end .hp-carousel__next svg { transform: scaleX(-1); }

.hp-carousel__dots button {
  transition: background .25s var(--hp-ease);
}
.hp-carousel__dots button[aria-selected="true"] { background: var(--hp-ink); }

@media (min-width: 1441px) {
  /* Gone rather than hidden: an invisible row of controls still kept its 74
     under the cards, and the section's air measured 274 instead of 200. */
  .hp-carousel.is-static .hp-carousel__foot { display: none; }
  .hp-carousel.is-static .hp-carousel__track { cursor: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .hp-carousel__track { transition: none; }
  .hp-arrow__ring, .hp-arrow__clip, .hp-arrow__icon, .hp-carousel__next svg { transition: none; }
  .hp-tile--photo img, .hp-post__shot img { transition: none; }
}

/* -------------------------------------------- 3d. Forge loop rows --------
   The row carries data-reveal and data-stagger-item, and motion.css sets a
   `transition` on that pair to carry the card reveal. `transition` is a
   shorthand: it replaces the whole property list rather than adding to it,
   and that rule is the more specific of the two — so a transition declared on
   .hp-loop__row alone is silently dropped and the property snaps instead of
   moving. Everything the row transitions has to be declared together, in the
   rule that wins.

   Both attributes are named on purpose. An attribute selector weighs the
   same as a class, so .hp-loop__row[data-stagger-item] only ties with
   [data-reveal][data-stagger-item] — and a tie goes to the sheet loaded
   later, which is motion.css. Three simple selectors against two wins it
   outright. */
.hp-loop__row[data-reveal][data-stagger-item] {
  transition: opacity      var(--t-card) var(--e-out),
              transform    var(--t-card) var(--e-out),
              border-color .25s         var(--e-out);
}

/* ── the rule under the hovered row goes black ────────────────────────
   The line below a row is the *next* row's border-top, which is why this is
   a sibling selector and not something on the hovered row itself. The last
   row has no next sibling, so it darkens its own bottom border instead.

   Colour only, at the 1px the row already had. */
@media (hover: hover) and (min-width: 721px) {
.hp-loop__row:is(:hover, :focus-visible) + .hp-loop__row {
  border-top-color: var(--hp-ink);
}
}
@media (hover: hover) and (min-width: 721px) {
.hp-loop__row:last-child:is(:hover, :focus-visible) {
  border-bottom-color: var(--hp-ink);
}
}

/* 2026-09-15: the copy comes up out of its own mask as one line (cw-motion.js
   buildLineFlip), on the word's clock rather than the line flip's own — the
   word beside it flips over DURATION 1256ms on --e-in-out, and a line on
   1512ms --e-out lagged it and read as a second, separate movement. */
.hp-loop__copy.lineflip.is-in .lineflip__track {
  transition: transform var(--t-block) var(--e-in-out);
}

/* -------------------------------------------- 3e. footer link roll -------
   The footer `.hf__links a.is-rolled` / `.hf__legal a.is-rolled` colour
   rules live in the motion.css section above (rolling text, §3.3) — they are
   not duplicated in home.css, so nothing further is needed here. */

/* =========================================================================
   Deliberately left out of this file:

   — Card hover system (border-radius flattening, 1px stroke via box-shadow,
     blue-headline hover on .hp-loop__word / .hp-svc__title / .hp-post .hp-h4 /
     .hp-story__head, teal fills): built natively in Webflow Designer with
     hover states and transitions.
   — Image zooms (.hp-story__shot img / .hp-tile--photo img / .hp-post__shot
     img scale-on-hover): native Webflow hover interactions.
   — Section seams (angled clip-path wedges, @keyframes seam swings,
     @supports (animation-timeline: view()) scroll-driven variants):
     separate file, cw-seams.css.
   — Button / circled-arrow resting styles (ring geometry, colours, sizes):
     native Webflow styles; only their motion/hover transitions ride above.
   — Hero blur-entrance variant (.hero [data-reveal]) and the
     [data-loadstyle] switches: cpu-page only, not part of this kit.
   — Video lightbox choreography (.vlb): lives with the lightbox embed, not
     the motion kit.
   — All layout, typography and colour rules for sections: Webflow Designer.
   ========================================================================= */

/* ── fail-open guard (1.1.3) ───────────────────────────────────────────
   The reveal system hides content until cw-core.js opens it. If that file
   never runs — 404, adblock, CSP, a bad paste — hidden must not mean gone.
   cw-core.js stamps html.cw-js the moment it executes; while the stamp is
   absent, every hidden element force-unhides 2.5s after styles apply. With
   healthy JS the stamp lands before the timer and nothing here ever fires. */
html:not(.cw-js) [data-reveal],
html:not(.cw-js) [data-stagger-in] [data-stagger-child] {
  animation: cw-unhide 1ms linear 2.5s forwards;
}
@keyframes cw-unhide {
  to { opacity: 1; transform: none; clip-path: none; filter: none; }
}
