/* Certinia platform module — styles.
   Built by tools/build-razor.mjs from platform-module.html. Do not edit here.

   The webfont is a <link> in the view rather than an @import at the top of
   this file: @import blocks rendering until it resolves, and a <link> in the
   head does not. If Certinia's layout already loads Plus Jakarta Sans and
   STIX Two Text, delete that <link> — loading the same faces twice is pure
   latency. */

/* ===========================================================================
   Lenis required stylesheet — the ONLY unscoped rules in this block.
   These are not our styling; Lenis needs them on <html> to scroll correctly,
   and they only take effect once Lenis adds its own classes. Copied verbatim
   from lenis/dist/lenis.css (1.3.x). If the host page already loads Lenis, this
   is a harmless duplicate. Delete it along with CONFIG.smoothScroll if the site
   decides against momentum scrolling.
   =========================================================================== */
html.lenis,
html.lenis body { height:auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow:clip; }
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch],
.lenis [data-lenis-prevent-vertical],
.lenis [data-lenis-prevent-horizontal] { overscroll-behavior:contain; }
.lenis.lenis-smooth iframe { pointer-events:none; }
.lenis.lenis-autoToggle {
  transition-property:overflow;
  transition-duration:1ms;
  transition-behavior:allow-discrete;
}

/* ===========================================================================
   Certinia 2026 — Platform module
   Figma: Ewr4mmRh9sVQoZmoEQf99k  node 126:3269
   Every selector below is scoped under .cx-platform so nothing leaks into the
   host page. No :root vars, no element selectors, no global reset.
   =========================================================================== */

.cx-platform{
  /* ---- brand tokens (Figma variables) ---------------------------------- */
  --cx-white:#fff9f9;
  --cx-black:#151c20;
  --cx-creme:#f3f1e4;
  --cx-sage:#3e6a6b;
  --cx-sand:#e3ddd5;
  --cx-green:#00c2a2;
  --cx-plum:#ab39bf;
  --cx-knight:#3d4451;
  --cx-fuchsia:#ffbaff;
  /* Used by the outcome-leaves band and two of its stat boxes. */
  --cx-aurora:#bbedff;
  --cx-aurora-500:#005170;
  --cx-aurora-200:#8ee1ff;   /* slider controls, 113:85 */
  --cx-stone:#b0a9a1;
  --cx-charcoal:#252c38;
  --cx-neutral-100:#e3e9ec;
  --cx-fuchsia-100:#fce5ff;

  /* ---- fluid gutter -----------------------------------------------------
     112px at 1440 = 7.78vw. Grows proportionally, clamped at both ends so it
     never collapses on a phone or swallows the page on a 5K display.        */
  --cx-gutter:clamp(20px, 7.78vw, 224px);

  /* ---- fluid type scale -------------------------------------------------
     Each ramp is (design px / 1440 * 100)vw, clamped. Text MEASURE is held
     in em (see .cx-h2 / .cx-body max-width) so it tracks the clamped size
     rather than the viewport — that is what keeps line length readable when
     the layout itself is uncapped.                                          */
  /* Display type is driven by the SMALLER of the two axes, not by width.
     4.65vw alone meant a wide, short window inflated the type while the space
     holding it shrank: at 1728x900 the h2 went to 80px inside sections whose
     budget is 100vh, and on the leaves it ate the section's padding whole (the
     gap above the heading measured 55px at both 1440x900 and 1728x1000,
     because the heading grew 134 -> 161px and absorbed the extra).

     Both terms are the design's own 67px and 56px at the 1440x900 reference —
     67/1440 = 4.65vw and 67/900 = 7.44vh — so nothing changes there, and the
     min() only bites when the window is wider than it is tall. */
  --cx-h2:clamp(34px, min(4.65vw, 7.44vh), 84px);   /* Digital/h2  67px @1440 */
  --cx-h3:clamp(30px, min(3.89vw, 6.22vh), 70px);   /* Digital/h3  56px @1440 */
  /* Digital/h4 36px @1440. Axis-aware for the same reason h2 and h3 are:
     the closing statement and the CTA headline are both three-line blocks
     inside a fixed box, so a wide, short window must not inflate them. */
  --cx-h4:clamp(20px, min(2.5vw, 4vh), 36px);

  /* The five journey labels, in BOTH places they appear: orbiting the rosette
     as .cx-badge and landed in the tab bar as .cx-tab. Ben's rule is that the
     two must be the same size, so they share one property rather than two
     ramps that agree by coincidence.
     Driven by the smaller axis, like the display type, because the rosette the
     badges orbit is vh-bound while the tab bar is vw-bound — min() is what lets
     one number serve both. 24px at 1440x900, against the design's 18. */
  --cx-journey:clamp(15px, min(1.8vw, 2.7vh), 30px);
  --cx-h6:clamp(19px, 1.67vw, 28px);      /* Digital/h6  24px @1440 */
  --cx-subtitle:clamp(19px, 1.67vw, 28px);
  --cx-body:clamp(15px, 1.11vw, 19px);    /* Body        16px @1440 */
  --cx-label:clamp(14px, 1.25vw, 20px);   /* h7 semibold 18px @1440 */

  --cx-serif:"STIX Two Text", "Times New Roman", Times, serif;
  --cx-sans:"Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* ---- the bloom, and everything that orbits it -------------------------
     817px in a 1440x900 frame = 56.7vw / 90.8vh. Taking the min of the two
     means the bloom is width-bound on narrow viewports and HEIGHT-bound on
     wide ones, so it fills the screen instead of overflowing it. Satellites
     are positioned in multiples of --cx-bloom so the whole radial
     composition scales as one object.

     THIS IS THE MAIN DIAL, and it is set by the LARGEST beat. Beat 3 draws at
     bloomScale 1.14 x artboardFill 0.947 = 1.080 of this box, so 104vh put a
     1010px rosette in a 900px viewport and clipped 55px off the top and the
     bottom. 96vh lands beat 3 at ~1.03vh (12px of bleed a side) — still the biggest moment in the
     sequence, but whole. Figma can afford a 1662px rosette because its beat-3
     frame is 2144px tall; a viewport is not, and something has to give.

     96vh kept the rosette WHOLE but made it proportionally 56% of the design's
     (drawn-r 466 against Figma's 832 in a 1440 frame). The cost was not that
     the rosette looked small — it bleeds, so it reads big — but that the badge
     ring shrank with it. At 96vh the ring was 1.19x the copy's half-width
     against the design's 1.41x, so "Manage" landed ON the headline and "Plan"
     on "need it" (see tools/shots/beat3-1440x900.png before this change).

     108vh puts drawn-r at 525, which clears all five badges: Manage's right
     edge 12px clear of the copy, Plan's left edge 24px clear, Sell 35px below
     the stage top, Deliver 52px above the bottom. Cost is ~69px/side of
     rosette bleed instead of 12. The arms fade out, so bleed reads as intent —
     Figma's own beat-3 rosette overflows its 1440 frame by 112px a side.      */
  --cx-bloom:min(70vw, 108vh);

  /* The light leaks' box is this times --lw/--lh, and --cx-drawn-r rides in as
     a scale() on top. Any constant works — it only sets the unscaled size the
     browser lays out once. 500px keeps the scale factor near 1 at common
     viewports, so the rasterised layer is close to its displayed size. */
  --cx-leak-base:500px;

  position:relative;
  background:var(--cx-white);
  color:var(--cx-black);
  font-family:var(--cx-sans);
  overflow-x:clip;
  isolation:isolate;
}

.cx-platform *,
.cx-platform *::before,
.cx-platform *::after{box-sizing:border-box;}

/* :where() contributes ZERO specificity, so this reset weighs only as much as
   `.cx-platform` (0,1,0) and any later single-class rule beats it on source
   order. Written as `.cx-platform p` it was (0,1,1) and silently overrode every
   margin set by a class — .cx-panel__lede, .cx-panel__label, .cx-title__sub and
   .cx-pillar__h were ALL computing to 0px. That is the "squished" spacing, and
   it was wrong in the Title section too, not just this panel. */
.cx-platform :where(p, h2, h3, h4){margin:0;}

/* ---- shared type ------------------------------------------------------- */
.cx-h2{
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:var(--cx-h2);
  line-height:1.0;
  letter-spacing:-0.028em;      /* -1.9px @67px, expressed relatively */
  /* 9.6em holds the 3-line measure at any size. The 65vh cap is what keeps the
     headline out of the journey badges: --cx-h2 is vw-driven (correct — the
     design's 67px is page-relative), but the rosette the badges orbit is
     vh-bound, so on wide-and-short viewports the box outran the ring and
     "Manage" landed on "Agentic" while "Plan" landed on "it". 1728x900 was the
     case that showed it.

     This was 65vh, sized when the badges were still a quarter inside the rim.
     Now that --r is calibrated against the drawn radius the ring is far wider —
     Manage's inner edge clears the headline by 118px at 1440x900 and 62px at
     1728x900 — so 65vh was biting by 3px and needlessly forcing a third line.
     88vh (792px at 900 tall) never binds at the measures that actually occur
     and still guards the wide-and-short case that caused the collision. */
  max-width:min(9.6em, 88vh);
  text-wrap:balance;
}
.cx-h3{
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:var(--cx-h3);
  line-height:1.1;
  letter-spacing:-0.034em;
  max-width:11em;
  text-wrap:balance;
}
.cx-h2 em,
.cx-h3 em{font-style:normal;color:var(--cx-plum);}

.cx-body{
  font-size:var(--cx-body);
  line-height:1.4;
  max-width:34em;
  color:var(--cx-black);
}
.cx-subtitle{
  font-size:var(--cx-subtitle);
  font-weight:600;
  line-height:1.15;
}

/* =======================================================================
   BUTTONS — canonical, from Figma 126:3137 (btn/Default)

     plum #ab39bf, creme #f3f1e4, radius 3, padding 18 x 20,
     Plus Jakarta Sans SemiBold 18/27, tracking -0.198px, nowrap

   The metric that was wrong everywhere: Figma trims the text box to the CAP
   HEIGHT (text-box-trim: trim-both, text-box-edge: cap alphabetic). That is
   why its buttons measure 53px tall — ~13px of cap plus 2x20 of padding. With
   an untrimmed box, line-height 27 gives a 27px content box and a 67px
   button: 14px too tall, and because the em box carries descender space the
   label sits visibly low inside it.

   Both branches below land on 53px at 18px. The fallback compensates with
   padding (0.722em x 2 + 27 = 53); where the trim is supported the design's
   own 20px padding is used and the cap is centred exactly.

   Colour is the only thing a variant sets: --btn-bg, --btn-fg, and
   --btn-shift, which is the direction hover moves in. Dark buttons lighten,
   bright ones darken — "a little darker, or lighter, depending on their
   colour".                                                                */
.cx-btn,
.cx-leaf__cta,
.cx-hero__cta,
.cx-appr__cta,
.cx-award__cta,
.cx-news__link,
.cx-cta__link,
.cx-pillar__cta,
.cx-hero__link{
  --btn-bg:var(--cx-plum,#ab39bf);
  --btn-fg:var(--cx-creme,#f3f1e4);
  --btn-shift:#000;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:.556em;                       /* 10 of 18 */
  box-sizing:border-box;
  flex:0 0 auto;
  font-family:var(--cx-sans);
  font-weight:600;
  font-size:clamp(13px,1.25vw,18px);
  line-height:1.5;                  /* 27 of 18 */
  letter-spacing:-0.011em;          /* -0.198px at 18px */
  white-space:nowrap;
  text-decoration:none;
  border:0;
  border-radius:3px;
  cursor:pointer;
  /* 0.722*2 + 1.5 line-height = 2.944em = 53px at 18px, which is the height
     Figma's btn/Default measures. Figma gets there with text-box-trim to the
     cap height and 20px padding; that property only trims a BLOCK container's
     inline content, and these are inline-flex for the icon, so it applied and
     did nothing (67px either way). Compensating with padding is exact,
     universal, and leaves the label ~1.5px optically high, which is what
     every button on the web does. */
  padding:.722em 1em;
  background:var(--btn-bg);
  color:var(--btn-fg);
  transition:background-color .2s ease, color .2s ease;
}
.cx-btn:hover,
.cx-leaf__cta:hover,
.cx-hero__cta:hover,
.cx-award__cta:hover{
  background-color:color-mix(in oklab, var(--btn-bg) 86%, var(--btn-shift));
}
/* The two text-only buttons carry no fill, so hover moves the LABEL instead
   — shifting a transparent background does nothing visible. */
.cx-appr__cta:hover,
.cx-news__link:hover,
.cx-cta__link:hover,
.cx-pillar__cta:hover,
.cx-hero__link:hover{
  color:color-mix(in oklab, var(--btn-fg) 78%, var(--btn-shift));
}
.cx-btn:focus-visible,
.cx-leaf__cta:focus-visible,
.cx-hero__cta:focus-visible,
.cx-appr__cta:focus-visible,
.cx-award__cta:focus-visible,
.cx-news__link:focus-visible,
.cx-cta__link:focus-visible,
.cx-pillar__cta:focus-visible,
.cx-hero__link:focus-visible{
  outline:2px solid var(--cx-plum,#ab39bf);
  outline-offset:3px;
}
.cx-btn img,
.cx-leaf__cta img,
.cx-hero__cta img,
.cx-appr__cta img,
.cx-award__cta img,
.cx-news__link img,
.cx-cta__link img,
.cx-pillar__cta img,
.cx-hero__link img{ width:.667em; height:.667em; display:block; flex:0 0 auto; }
/* "Talk to sales" alone takes the taller 12x15 caret (126:3746); every
   other caret on the page is the 12x12 one. */
.cx-cta__link img{ height:.833em; }

/* ---- colourways. Dark grounds lighten on hover, bright ones darken. ---- */
/* Beat CTAs — knight (126:3491 and siblings). */
.cx-btn          { --btn-bg:var(--cx-knight,#3d4451); --btn-shift:#fff; }
.cx-hero__cta    { --btn-bg:var(--cx-plum,#ab39bf);   --btn-shift:#000; }
.cx-leaf__cta--aurora500{ --btn-bg:var(--cx-aurora-500,#005170); --btn-fg:var(--cx-green,#00c2a2);  --btn-shift:#fff; }
.cx-leaf__cta--plum     { --btn-bg:var(--cx-plum,#ab39bf);       --btn-fg:var(--cx-aurora,#bbedff); --btn-shift:#000; }
.cx-leaf__cta--green    { --btn-bg:var(--cx-green,#00c2a2);      --btn-fg:var(--cx-charcoal,#252c38); --btn-shift:#000; }
.cx-award__cta   { --btn-bg:var(--cx-aurora-500,#005170); --btn-fg:var(--cx-green,#00c2a2); --btn-shift:#fff; }
/* Text-only: Figma gives these a fully transparent fill and real padding, so
   the hit area matches a solid button. */
.cx-appr__cta    { --btn-bg:transparent; --btn-fg:var(--cx-plum,#ab39bf);   --btn-shift:#000; }
.cx-hero__link   { --btn-bg:transparent; --btn-fg:var(--cx-aurora,#bbedff); --btn-shift:#fff; font-weight:400; }
/* Closing module (126:3650). "Discover our latest insights" is sand text
   with the GREEN caret Figma actually uses; "Talk to sales" is aurora at
   regular weight with the 12x15 caret. */
.cx-news__link   { --btn-bg:transparent; --btn-fg:var(--cx-sand,#e3ddd5);   --btn-shift:#fff; }
.cx-cta__link    { --btn-bg:transparent; --btn-fg:var(--cx-aurora,#bbedff); --btn-shift:#fff; font-weight:400; }
.cx-cta__demo    { --btn-bg:var(--cx-charcoal,#252c38); --btn-fg:var(--cx-sand,#e3ddd5); --btn-shift:#fff; }
.cx-btn:active{transform:translateY(1px);}

/* =======================================================================
   BACKGROUND — full-bleed. Texture wash + the pale "gradient rectangles"
   that parallax section to section (annotation 126:2894).
   ======================================================================= */
.cx-bg{
  position:absolute;
  inset:0;
  z-index:0;
  pointer-events:none;
  overflow:clip;
}
.cx-bg__wash{
  position:absolute;
  left:50%;
  top:0;
  width:170vw;
  height:100%;
  transform:translateX(-50%);
  background:
    radial-gradient(60% 38% at 50% 26%, rgba(255,255,255,.85) 0%, rgba(255,249,249,0) 70%),
    radial-gradient(52% 30% at 12% 58%, rgba(252,229,255,.55) 0%, rgba(252,229,255,0) 72%),
    radial-gradient(52% 30% at 88% 72%, rgba(227,233,236,.6) 0%, rgba(227,233,236,0) 72%),
    radial-gradient(40% 26% at 6% 86%, rgba(255,186,138,.30) 0%, rgba(255,186,138,0) 70%),
    radial-gradient(40% 26% at 96% 92%, rgba(255,186,138,.26) 0%, rgba(255,186,138,0) 70%),
    linear-gradient(180deg, var(--cx-white) 0%, #fffafc 42%, #fdf6fb 72%, var(--cx-white) 100%);
}

/* The gradient rectangles (Figma "static boxes", 126:3331).
   Rendering a single box in isolation shows what they actually are: a flat
   #e8ecf0 slab, CRISP along the top and sides, dissolving downward and
   feathering out past its own bottom edge. The first pass had them as blurred
   low-alpha bars — wrong shape (they are 193-453 x 214-357 in the 1440 frame,
   not thin strips), wrong edges, and far too faint.

   left/top/width are percentages of the 1440-wide frame, and height comes from
   aspect-ratio, so the whole set scales with viewport width. */
.cx-rects{position:absolute;inset:0;}
/* Real values, from get_design_context on 126:3344 / 126:3347:
     background: var(--neutrals/black-100, #e3e9ec)
     blur(25px)  opacity 60%  border-radius 3px
   Two earlier passes got this wrong by eye — first as a 7px-blurred faint bar,
   then as a CRISP gradient slab after an isolated render looked sharp at
   thumbnail size. It is neither: a flat fill with a genuine 25px layer blur.
   The blur is expressed in vw (25/1440 = 1.74vw) so it scales with the
   composition rather than staying a fixed 25px at 2560. */
.cx-rect{
  position:absolute;
  background:var(--cx-neutral-100);
  opacity:.60;
  border-radius:3px;
  filter:blur(clamp(12px, 1.74vw, 34px));
  /* the downward dissolve is Figma's "Bottom blur" FOREGROUND_BLUR r=50,
     approximated with a mask so the slab feathers out at its lower edge */
  -webkit-mask-image:linear-gradient(180deg, #000 0%, #000 54%, transparent 100%);
          mask-image:linear-gradient(180deg, #000 0%, #000 54%, transparent 100%);
  will-change:transform;
}
/* generated from the Figma node coordinates — see ANIMATION-BRIEF.md */
.cx-rect--a{left:57.09%; top:81.88%; width:23.91%; aspect-ratio:1.354;}
.cx-rect--b{left:4.73%;  top:58.61%; width:23.03%; aspect-ratio:1.505;}
.cx-rect--c{left:39.80%; top:62.87%; width:13.39%; aspect-ratio:0.901;}
.cx-rect--d{left:-8.84%; top:31.72%; width:20.13%; aspect-ratio:1.308;}
.cx-rect--e{left:73.20%; top:1.86%;  width:23.03%; aspect-ratio:1.505;}
.cx-rect--f{left:15.16%; top:43.53%; width:13.39%; aspect-ratio:0.901;}
.cx-rect--g{left:74.31%; top:41.95%; width:13.39%; aspect-ratio:0.901;}
.cx-rect--h{left:1.40%;  top:67.93%; width:30.38%; aspect-ratio:1.225;}
.cx-rect--i{left:87.70%; top:32.41%; width:21.41%; aspect-ratio:1.228;}
.cx-rect--j{left:-7.06%; top:23.54%; width:31.49%; aspect-ratio:1.540;}
.cx-rect--k{left:68.97%; top:19.19%; width:31.49%; aspect-ratio:1.540;}
.cx-rect--l{left:-9.42%; top:3.59%;  width:30.38%; aspect-ratio:1.225;}
.cx-rect--m{left:74.31%; top:8.43%;  width:13.39%; aspect-ratio:0.901;}

/* =======================================================================
   BEAT 1 — "Title". Scrolls normally; not part of the pinned stage.
   ======================================================================= */
.cx-title{
  position:relative;
  z-index:1;
  /* Generous bottom padding is the gap between the pillar row and the bloom.
     The stage pins, so extra room here just means a little more scrolling
     before the pin engages — it does not shorten the pinned sequence. */
  /* The Title beat runs a WIDER gutter than the rest of the module: Figma puts
     its 1050px content block at left 195px in a 1440 frame (195/1440 = 13.54vw),
     not the 112px used elsewhere. */
  padding:clamp(72px,11vh,190px) clamp(20px,13.54vw,380px) clamp(140px,26vh,400px);
  display:grid;
  justify-items:center;
  text-align:center;
}
/* The Title headline gets a wider measure than the in-bloom headlines: Figma
   sets it on two lines at ~780px against 67px type (≈11.6em), where the SSO
   headline is a much tighter 574px (≈8.6em). One shared max-width forced this
   one onto three lines. */
.cx-title__h{ text-align:center; max-width:12em; }
/* Figma 126:3352: Plus Jakarta Sans MEDIUM 18/27, tracking -0.198px, 524px
   wide. Was inheriting .cx-body's Regular 16px on a 32em measure. */
.cx-title__sub{
  margin-top:clamp(20px,2.22vw,32px);   /* caps at the design's 32px */
  text-align:center;
  font-size:var(--cx-label);            /* 18px @1440 */
  font-weight:500;
  line-height:1.5;                      /* 27/18 */
  letter-spacing:-0.011em;
  max-width:29.1em;                     /* 524px @18px */
}

/* Photo that deliberately bleeds off the left edge of the frame.
   Anchored to the VIEWPORT edge, not the content box — at 2560px it stays
   pinned to the edge instead of drifting into the middle. */
.cx-bleed{
  position:absolute;
  overflow:hidden;
  background:var(--cx-neutral-100);
  will-change:transform;
}
.cx-bleed img,
.cx-bleed picture{width:100%;height:100%;object-fit:cover;display:block;}
/* Figma 126:3376: left -6.11%, width 21.32% of the 1440 frame, aspect 307/324.
   Percentages of the viewport, so it keeps bleeding off the edge at 2560. */
.cx-bleed--title-l{
  left:-6.11%;
  top:clamp(24px,4.5vh,90px);
  /* 21.32% is the Figma proportion. Note the design ALREADY overlaps the
     content column horizontally at 1440 (photo right edge 219 vs content left
     195) — it works because the photo sits vertically above the pillar row. So
     the constraint that matters is height, not width: bound the height to the
     headline band and the collision cannot happen at any width. The img is
     object-fit:cover, so the crop just tightens. */
  width:min(21.32%, 360px);
  height:clamp(150px, 30vh, 400px);
  border-radius:3px;
}

/* =======================================================================
   AWARD (126:3588)

   Headline, one dark card with the IDC chart, and seven decorative pieces on
   a parallax layer. Figma's second slide and the gradient/texture behind the
   chart are both left out — see the markup note.                           */
.cx-award{
  position:relative;
  /* Above its neighbours. The parallax pieces overhang this section on
     purpose — the aurora square sits ~38px above its top edge in Figma, and
     the parallax travel carries the low group a little past the bottom one —
     so they have to paint OVER the sections either side rather than being
     clipped by them. Both neighbours are z-index 1. The bottom overhang is
     BOUNDED by the stage's reserve; see that note. */
  z-index:2;
  background:var(--cx-white,#fff9f9);
  padding:clamp(56px,6.94vw,100px) 0;
  /* Deliberately NOT hidden: clipping here is what cut the 3x box off at the
     bottom and the aurora square at the top. Horizontal safety comes from
     .cx-platform's own overflow-x:clip, so nothing can push a scrollbar. */
  overflow:visible;
}
.cx-award__inner{
  position:relative;
  z-index:1;
  /* One expression instead of width + max-width + padding on content-box.
     That combination made the box 100% of the parent and then ADDED the
     gutter outside it, so at 390px the column was 450px wide and the card
     started 30px off the left edge and ran 30px off the right — headline,
     CTA and chart all clipped. It was wrong on desktop too, just less
     visibly: at 1280 the card ran ~36px past the viewport.

     min() keeps the design's 1216 column wherever there is room for it and
     falls back to whatever the viewport leaves after the gutters. */
  width:min(1216px, 100% - 2 * var(--cx-gutter));
  margin:0 auto;
  box-sizing:border-box;
  display:flex;
  flex-direction:column;
  gap:clamp(32px,5vw,72px);
}
.cx-award__h{
  margin:0;
  font-family:var(--cx-serif);
  font-weight:400;
  /* Capped at the design's 67px rather than following --cx-h2 up to 84. The
     decoration around this section is placed against a two-line, 134px-tall
     headline; letting it grow to 168px at 2560 pushed the card down and put
     the aurora square and the 2M box on top of the text. Growing it was a
     side effect of the shared clamp, not a design requirement. */
  font-size:min(var(--cx-h2), 67px);
  line-height:1;
  letter-spacing:-0.019em;      /* -1.273px at 67px */
  color:var(--cx-black,#151c20);
}

/* ---- the card --------------------------------------------------------- */
.cx-award__card{
  background:var(--cx-charcoal,#252c38);
  border-radius:6px;
  display:grid;
  /* 617 of 1216 for the copy, the rest for the chart. */
  grid-template-columns:minmax(0,617fr) minmax(0,599fr);
  align-items:center;
  overflow:hidden;
}
.cx-award__copy{
  padding:clamp(28px,3.89vw,56px) clamp(18px,2.22vw,32px);
  display:flex;
  flex-direction:column;
  /* Figma gaps the block 48 and the heading pair 16; the CTA is the third
     child, so the 48 is applied as a margin on it instead of a shared gap. */
  gap:clamp(10px,1.11vw,16px);
  min-width:0;
}
.cx-award__cardh{
  margin:0;
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:var(--cx-h3);       /* Digital/h3 56/1.1 */
  line-height:1.1;
  letter-spacing:-0.019em;
  color:var(--cx-aurora,#bbedff);
}
.cx-award__cardp{
  margin:0;
  font-weight:500;              /* Plus Jakarta Sans MEDIUM, not Regular */
  font-size:clamp(14px,1.39vw,20px);
  line-height:1.5;
  letter-spacing:-0.019em;      /* -0.38px at 20px */
  color:var(--cx-creme,#f3f1e4);
}
.cx-award__cardp a{ color:inherit; text-decoration:underline; }


/* 8px of charcoal all round the chart, per 126:3601. */
.cx-award__viz{ padding:8px; min-width:0; }
.cx-award__viz picture,
.cx-award__viz img{
  display:block;
  width:100%;
  height:auto;
  /* Reserved, not incidental. height:auto with a lazy image meant the card
     grew when the chart arrived, which grew .cx-platform, which left the
     blend group's background COPY at a stale height — 38px of drift on the
     'bg copy registered' check, because syncRenderBg caches the height it
     last wrote and nothing re-fires when an image lands. */
  aspect-ratio:730/706;
  border-radius:6.509px;
  background:#fff;
}

/* ---- parallax layer (126:3628) ---------------------------------------- */
/* One scroll-derived offset, --cx-award-y, drives all seven; each piece
   scales it by its own --depth. Squares move most (pure decoration), the stat
   boxes least — they carry copy and should not drift far from their reading
   position. */
/* The card's own box, which the parallax layer is measured against.

   padding-bottom reserves the room the decoration needs BELOW the card. The
   pieces are placed from this box's top in shares of its width, so the lowest
   of them — the creme square — bottoms out at 72.7% of the card's width below
   the card's top (Figma: 1051.21 + 138.75 against a card top of 306). The
   card's own height is 47.7% of its width, fixed by the chart's aspect ratio
   and measured identical at 1280 through 2560, so the piece hangs 25.0% below
   the card and Figma clears it by a further 39px (1189.96 in a 1229 frame):
   72.7 - 47.7 + 3.2 = 28.2% of the card's width, less the ~70px the section's
   own padding already provides = 22.45%.

   This is not cosmetic slack. In Figma NOTHING overhangs this section's
   bottom — only the top, where the aurora square sits 38px above the edge —
   and the award section carries 336px under the card to hold the creme square
   and the two low stat boxes. Ours carried 70px, so those three landed on the
   closing module's dark ground and the creme square sat squarely on top of
   "The latest from Certinia." Overhanging a neighbour is the point; landing on
   its type is not. What is left is a bounded ~51px at the far end of the
   parallax travel, against 144px of clearance before the next headline. */
.cx-award__stage{
  position:relative;
  padding-bottom:22.45%;
}
.cx-award__parallax{
  position:absolute;
  inset:0;
  /* ABOVE the card. Figma puts this layer after the content block, and it
     matters: the $3T and 3x boxes overlap the card's lower edge in the design.
     Painting it underneath hid both of them behind the card entirely. */
  z-index:2;
  pointer-events:none;
}
.cx-award__sq,
.cx-award__stat{
  position:absolute;
  left:var(--x);
  top:0;
  /* --y is a share of the card's WIDTH, applied as margin-top, because a
     margin percentage resolves against the containing block's inline size
     where `top` resolves against its height. The card's height varies with
     the viewport (533 in Figma, ~440 at 2560 once the copy caps) so
     height-relative offsets did not lift these far enough and the aurora
     square landed on the headline. Width is capped at 1216, so these are the
     design's own px offsets from the card's top-left. */
  margin-top:var(--y);
  transform:translateY(calc(var(--cx-award-y, 0px) * var(--depth, 1)));
  will-change:transform;
}
.cx-award__sq{
  width:11.47%;                 /* 139.5 of the card's 1216 */
  aspect-ratio:139.5/138.746;
  border-radius:6.75px;
}
.cx-award__sq--aurora { background:var(--cx-aurora,#bbedff); }
.cx-award__sq--fuchsia{ background:var(--cx-fuchsia,#ffbaff); }
.cx-award__sq--plum   { background:var(--cx-plum,#ab39bf); }
.cx-award__sq--creme  { background:var(--cx-creme,#f3f1e4); }

.cx-award__stat{
  width:23.11%;                 /* 281 of the card's 1216 */
  aspect-ratio:281/139;
  border-radius:6px;
  padding:clamp(10px,1.25vw,18px) clamp(6px,.69vw,10px);
  box-sizing:border-box;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
}
.cx-award__n{
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:var(--cx-h2);       /* 67px at the reference, and axis-aware */
  line-height:1;
  letter-spacing:-0.019em;
  margin-bottom:-0.15em;        /* Figma's -10px at 67px */
}
.cx-award__l{
  font-weight:400;
  font-size:clamp(11px,1.11vw,16px);
  line-height:1.2;
}
.cx-award__stat--green{ background:var(--cx-green,#00c2a2); color:var(--cx-black,#151c20); }
.cx-award__stat--sage { background:var(--cx-sage,#3e6a6b);  color:var(--cx-white,#fff9f9); }
.cx-award__stat--stone{ background:var(--cx-stone,#b0a9a1); color:var(--cx-black,#151c20); }

@media (prefers-reduced-motion:reduce){
  .cx-award__sq, .cx-award__stat{ transform:none; }
}
@media (max-width:900px){
  .cx-award__card{ grid-template-columns:1fr; }
  /* The decoration is the first thing to go when there is no room for it to
     sit beside anything. */
  .cx-award__parallax{ display:none; }
}

/* =======================================================================
   CLOSING MODULE (126:3650) — insights slider + CTA banner

   One dark ground, two blocks: the news module (126:3651) and the CTA module
   (126:3682). Figma paints the ground with a PNG covering the whole
   1440x1800 frame, but sampled it carries no texture at all — a near-black
   #171d21 with a soft plum-grey bloom off the left edge and a lift at the
   top-left. Rebuilt from gradients rather than shipped: the export is a 4:3
   image and this section's height is content-driven, so cover-fitting it
   would re-scale and re-place that bloom at every height the module lands at.

   The section drops the page's usual "gutter padding + max-width child"
   pattern for a __row helper, because the slider has to BLEED — in the design
   the third card is cut by the frame edge, not by the 112px gutter. Each row
   pads to max(gutter, half the leftover), so its inside edge lands on the
   same 1216px column as every other section while the track runs off the
   right of the viewport. Percentages, not vw: vw includes the scrollbar and
   would leave the cards half a scrollbar out of line with the headline.

   The gaps are one number. Figma's news module is flex-col gap-36 over three
   rows (head / cards / controls) and the section itself is gap-36, so making
   those rows direct children of the section reproduces the design's spacing
   with a single gap rather than four margins.
   ======================================================================= */
.cx-close{
  position:relative;
  z-index:1;
  isolation:isolate;                 /* the banner blends; keep it in here */
  padding:clamp(64px,10vw,144px) 0;  /* py-144; the rows own the inline pad */
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:clamp(24px,2.5vw,36px);
  color:var(--cx-white,#fff9f9);
  background:
    radial-gradient(115% 52% at -6% 2%, rgba(98,88,118,.50), rgba(98,88,118,0) 62%),
    radial-gradient(78% 48% at 5% 86%,  rgba(74,76,98,.38),  rgba(74,76,98,0)  66%),
    radial-gradient(95% 62% at 58% 44%, #1d2529, #171d21 100%),
    #171d21;

  /* 405 of 1440. The wide card is 549 = the same ratio at 38.13vw, and the
     media height is shared by ALL cards so every caption starts on one line
     regardless of how wide its card is. */
  --cx-news-cardw:clamp(240px, 28.13vw, 405px);
  --cx-news-imgh:calc(var(--cx-news-cardw) * 452 / 405);
}
.cx-close__row{
  box-sizing:border-box;
  width:100%;
  padding-inline:max(var(--cx-gutter), calc((100% - 1216px) / 2));
}
.cx-close__row--bleed{ padding-inline-end:0; }

/* ---- news module head ------------------------------------------------- */
.cx-news__head{
  display:flex;
  align-items:flex-end;              /* items-end: link on the last baseline */
  justify-content:space-between;
  gap:clamp(16px,2.8vw,40px);
}
.cx-news__h{
  margin:0;
  font-family:var(--cx-serif);
  font-weight:400;
  /* Capped at the design's 67 for the same reason the award headline is:
     two display lines sitting BESIDE a link. Letting --cx-h2 run to 84 on a
     large screen pushes the link off its own row. */
  font-size:min(var(--cx-h2), 67px);
  line-height:1;
  letter-spacing:-0.019em;           /* -1.273px at 67px */
  color:var(--cx-aurora,#bbedff);
}

/* ---- the track -------------------------------------------------------
   A native overflow scroller, not a transformed track. It gets trackpad and
   touch swiping, keyboard arrows and a real scroll position for free, and
   snapping is the browser's job rather than a tween's. data-lenis-prevent-
   horizontal keeps Lenis off the horizontal axis over this element while
   leaving vertical wheel alone, so scrolling the page across the cards still
   works.

   scroll-padding-inline-start matches the track's own 10px pad (Figma wraps
   each card in p-10, which is where the 20px inter-card gap comes from), so
   a snapped card lands on the 1216 column instead of 10px left of it.       */
.cx-news__track{
  margin:0;
  padding:10px;
  list-style:none;
  display:flex;
  align-items:flex-start;
  gap:20px;
  overflow-x:auto;
  overflow-y:hidden;
  overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  scroll-padding-inline-start:10px;
  scrollbar-width:none;
  -ms-overflow-style:none;
}
.cx-news__track::-webkit-scrollbar{ display:none; }
.cx-news__track:focus-visible{ outline:2px solid var(--cx-aurora,#bbedff); outline-offset:2px; }

.cx-news__card{
  flex:0 0 auto;
  width:var(--w, var(--cx-news-cardw));
  scroll-snap-align:start;
}
/* Figma draws its third card at 549 against the other two at 405. That was
   carried through at first as deliberate editorial variety; Ben reads it as an
   artefact of the mock, so every card is one width now. The image behind it is
   the set's only landscape one and object-fit:cover crops it to the same slot
   as the rest.

   Nothing else had to change for this: the nav steps to each card's own
   measured snap position rather than by a fixed width, so it was already
   width-agnostic in both directions. */

/* The whole card is the destination — an editorial card that only responds on
   its caption reads as broken. Both an <a> and a <button> take it, because card
   2 opens a modal rather than navigating. */
.cx-news__hit{
  display:block;
  width:100%;
  text-align:left;
  appearance:none; border:0; background:none; padding:0;
  font:inherit; color:inherit; text-decoration:none;
  cursor:pointer;
}
.cx-news__hit:focus-visible{ outline:2px solid var(--cx-aurora,#bbedff); outline-offset:4px; border-radius:8px; }
.cx-news__hit .cx-news__media{ transition:transform .35s cubic-bezier(.2,.8,.3,1); }
.cx-news__hit:hover .cx-news__media{ transform:translateY(-4px); }
.cx-news__hit:hover .cx-news__cap{ color:var(--cx-white,#fff9f9); }
.cx-news__cap{ transition:color .25s ease; }

/* Card 2 is a video. Same badge language as the Veda panels' posters, sized
   for this smaller card — but corner-set rather than centred, because this
   poster is a branded title card and a centred badge landed on the lockup. It
   is now letterboxed into the card's own shape by tools/make-poster.mjs, so the
   badge sits on the lower band and clears the artwork entirely. */
.cx-news__playbadge{
  position:absolute;
  left:clamp(12px,1.2vw,18px);
  bottom:clamp(12px,1.2vw,18px);
  z-index:1;
  width:clamp(40px,3.6vw,54px);
  height:clamp(40px,3.6vw,54px);
  border-radius:50%;
  background:rgba(255,249,249,.94);
  box-shadow:0 6px 22px rgba(21,28,32,.28);
  transition:transform .3s cubic-bezier(.2,.8,.3,1), background-color .3s ease;
}
.cx-news__playbadge::before{
  content:"";
  position:absolute;
  left:54%; top:50%;
  translate:-50% -50%;
  width:34%; height:38%;
  background:var(--cx-charcoal,#252c38);
  clip-path:polygon(0 0, 100% 50%, 0 100%);
}
.cx-news__hit--play:hover .cx-news__playbadge{ transform:scale(1.07); background:#fff; }

.cx-news__media{
  position:relative;
  height:var(--cx-news-imgh);
  border-radius:6px;
  overflow:hidden;
}
.cx-news__media img{ width:100%; height:100%; object-fit:cover; display:block; }

.cx-news__cap{
  margin:clamp(16px,2.15vw,31px) 0 0;   /* gap-31 */
  padding-inline:12px;
  /* The design pairs a bold white lead-in with a muted stone continuation, and
     the placeholders were written that way. Certinia's real titles are single
     titles with nothing to lead in FROM, so the emphasis moves to the whole
     line — left at stone weight 400 they read as caption text with the
     headline missing. */
  font-weight:500;
  font-size:clamp(15px,1.67vw,24px);    /* Digital/h6 24/32 */
  line-height:1.333;
  color:var(--cx-white,#fff9f9);
  /* Reserved so the cards end on a common bottom edge whatever the titles run
     to — the longest is now four lines (the GSD report), where the design's
     were three. */
  min-height:5.34em;
}
/* Kept for a caption that does want the design's two-part treatment. */
.cx-news__cap b{ font-weight:700; }

/* ---- card 2's stat collage (126:3664) --------------------------------
   Photo band over an aurora->stone gradient with three stat boxes across the
   join. Figma also lays a 1715px rotated SVG texture in here; at 405px wide
   it resolves to nothing, so it is left out rather than shipped.

   One em is the design's 17.78px label on a 405px card, so both the number
   and the label scale with the CARD instead of with the viewport — which is
   what keeps the boxes readable when the slider narrows.                    */
.cx-news__media--collage{
  background:linear-gradient(134.56deg,
    var(--cx-aurora,#bbedff) 19.96%, var(--cx-stone,#b0a9a1) 98.68%);
  font-size:calc(var(--cx-news-cardw) * 0.0439);
}
.cx-news__band{ height:62.39%; }        /* 282 of 452 */
.cx-news__stat{
  position:absolute;
  left:var(--x); top:var(--y);
  width:var(--sw); height:var(--sh);
  box-sizing:border-box;
  padding:0 .71em;                      /* 12.697 of 17.78 */
  border-radius:3px;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
}
.cx-news__stat b{
  font-family:var(--cx-serif);
  font-weight:500;
  font-size:3.264em;                    /* 58.04 of 17.78 */
  line-height:1;
  letter-spacing:-0.019em;
  margin-bottom:-0.066em;               /* Figma's -3.809 at 58.04 */
}
.cx-news__stat span{ font-weight:400; line-height:1.2; }
.cx-news__stat--fuchsia{ background:var(--cx-fuchsia,#ffbaff); color:var(--cx-black,#151c20); }
.cx-news__stat--sage   { background:var(--cx-sage,#3e6a6b);    color:var(--cx-white,#fff9f9); }
.cx-news__stat--green  { background:var(--cx-green,#00c2a2);   color:var(--cx-black,#151c20); }

/* ---- controls (113:84) ----------------------------------------------
   56px circles, aurora-200 fill, knight arrow. Figma draws the prev button
   at opacity .3 — that is the disabled state, not a second colourway, so it
   is bound to :disabled and the slider reports its own ends.               */
.cx-news__ctrls{
  display:flex;
  justify-content:flex-end;
  gap:17px;
}
.cx-news__nav{
  appearance:none;
  -webkit-appearance:none;
  border:0;
  padding:0;
  cursor:pointer;
  flex:0 0 auto;
  width:clamp(40px,3.89vw,56px);
  height:clamp(40px,3.89vw,56px);
  border-radius:50%;
  background:var(--cx-aurora-200,#8ee1ff);
  color:var(--cx-knight,#3d4451);
  display:grid;
  place-items:center;
  transition:background-color .2s ease, opacity .2s ease;
}
.cx-news__nav svg{ width:32.1%; height:32.1%; display:block; fill:currentColor; }
.cx-news__nav:hover:not(:disabled){
  background-color:color-mix(in oklab, var(--cx-aurora-200,#8ee1ff) 86%, #000);
}
.cx-news__nav:disabled{ opacity:.3; cursor:default; }
.cx-news__nav:focus-visible{ outline:2px solid var(--cx-aurora,#bbedff); outline-offset:3px; }

/* ---- CTA module (126:3682) ------------------------------------------- */
.cx-cta{ padding-top:clamp(36px,5vw,72px); }
.cx-cta__statement{
  margin:0 0 clamp(14px,1.8vw,26px);
  text-align:center;
  font-weight:400;
  font-size:var(--cx-h4);
  line-height:1.4;
  letter-spacing:-0.019em;
  color:var(--cx-white,#fff9f9);
}
/* The closing answer to the statement above it. Set in the display serif at
   h2, which is the largest thing in the module — it has to out-rank the banner
   headline it sits above, or the beat reads as a caption. */
.cx-cta__certain{
  margin:0 0 clamp(36px,5vw,72px);
  text-align:center;
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:var(--cx-h2);
  line-height:1.0;
  letter-spacing:-0.028em;
  color:var(--cx-white,#fff9f9);
}
.cx-cta__banner{
  position:relative;
  isolation:isolate;                  /* contain the bloom's lighten blend */
  overflow:hidden;
  border-radius:6px;
  background:var(--cx-sage,#3e6a6b);
  padding:clamp(32px,5vw,72px) clamp(24px,7.22vw,104px);
}
/* Already exported cropped to the banner's own 1216x322, so it needs no
   placement — inset 0 and cover. mix-blend-lighten per 126:3691. */
.cx-cta__bloom{
  position:absolute;
  inset:0;
  z-index:0;
  mix-blend-mode:lighten;
  pointer-events:none;
}
.cx-cta__bloom img{ width:100%; height:100%; object-fit:cover; display:block; }
.cx-cta__body{
  position:relative;
  z-index:1;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:clamp(20px,4vw,56px);
}
.cx-cta__h{
  margin:0;
  font-weight:400;
  font-size:var(--cx-h4);
  /* Not the type's own 1.4. Figma stacks three 56px rows with 4.6px gaps
     (126:3719/3728/3735) = 177.2px of content, and 1.4 leading around a 56px
     mark gives 169 — the banner came out 8px short. 59.07/36 = 1.641 puts the
     gaps back through the line box, which is the only lever an inline mark
     responds to: vertical margins do not grow a line box. The mark takes the
     type's real leading back below so its own height stays Figma's 56. */
  line-height:1.641;
  letter-spacing:-0.019em;
  color:var(--cx-white,#fff9f9);
}
/* Figma's rows are 56px tall against a 50.4px line box — that difference is
   the mark's own 3px padding, so an inline-block with padding:3px reproduces
   the row height as a side effect instead of being hand-set. */
.cx-cta__mark{
  display:inline-block;
  line-height:1.4;
  padding:3px;
  margin-inline:3px;
  border-radius:3px;
}
.cx-cta__mark--aurora{ background:var(--cx-aurora,#bbedff); color:var(--cx-black,#151c20); }
.cx-cta__mark--plum  { background:var(--cx-plum,#ab39bf);   color:var(--cx-white,#fff9f9); }
.cx-cta__mark--green { background:var(--cx-green,#00c2a2);  color:var(--cx-black,#151c20); }
.cx-cta__btns{ display:flex; align-items:center; gap:10px; flex:0 0 auto; }

@media (max-width:900px){
  .cx-news__head{ flex-direction:column; align-items:flex-start; }
  .cx-cta__body { flex-direction:column; align-items:flex-start; }
}

/* =======================================================================
   APPROACH (126:3542)

   Creme ground, white cards. Everything is a share of the viewport off the
   1440 frame; the display type uses the module's own --cx-h2, which now
   follows the smaller axis, so this section cannot inflate on a wide, short
   window the way it would have.                                            */
.cx-appr{
  position:relative;
  z-index:1;
  background:var(--cx-creme,#f3f1e4);
  padding:clamp(56px,10vw,144px) var(--cx-gutter);
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:clamp(20px,2.5vw,36px);
}
.cx-appr__head{
  width:100%;
  max-width:1216px;
  display:flex;
  /* items-end in Figma: the CTA sits on the headline's last baseline, not
     centred against the whole block. */
  align-items:flex-end;
  justify-content:space-between;
  gap:clamp(16px,2.8vw,40px);
}
.cx-appr__h{
  margin:0;
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:var(--cx-h2);
  line-height:1;
  letter-spacing:-0.019em;      /* -1.273px at 67px */
  color:var(--cx-charcoal,#252c38);
  max-width:11.2em;             /* 745 of 67px */
}



.cx-appr__row{
  width:100%;
  max-width:1216px;
  margin:0; padding:0;
  list-style:none;
  display:grid;
  gap:clamp(12px,1.74vw,25px);
}
.cx-appr__row--three{ grid-template-columns:repeat(3,1fr); }
.cx-appr__row--two  { grid-template-columns:repeat(2,1fr); }

.cx-appr__card{
  background:#fff;              /* internal/white — pure, not --cx-white */
  border-radius:6px;
  /* Figma's padding is asymmetric: 40 top, 16 right, 32 bottom, 32 left. */
  padding:clamp(24px,2.78vw,40px) clamp(12px,1.11vw,16px)
          clamp(20px,2.22vw,32px) clamp(20px,2.22vw,32px);
  display:flex;
  flex-direction:column;
  gap:clamp(14px,1.67vw,24px);
}
/* The two wide cards set the icon BESIDE the text rather than above it. */
.cx-appr__card--wide{
  flex-direction:row;
  align-items:flex-start;
}
.cx-appr__icon{
  flex:0 0 auto;
  display:block;
  height:clamp(22px,2.22vw,32px);
}
.cx-appr__icon img{ height:100%; width:auto; display:block; }
.cx-appr__text{
  display:flex;
  flex-direction:column;
  gap:clamp(6px,.69vw,10px);
  min-width:0;
}
.cx-appr__cardh{
  margin:0;
  font-weight:600;
  font-size:clamp(17px,1.67vw,24px);   /* Sub-title 24/1.15 */
  line-height:1.15;
  color:var(--cx-knight,#3d4451);
}
.cx-appr__card--stat{
  /* Requests 61-63. Same white shell and same grid track as the benefit cards
     above, but the number leads: the icon slot is gone and the type comes from
     the outcome-card stats, so these read as figures rather than as a sixth
     benefit. */
  justify-content:center;
  gap:clamp(6px,.69vw,10px);
}
.cx-appr__statn{
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:clamp(38px,4.44vw,64px);
  line-height:1;
  letter-spacing:-0.028em;
  font-variant-numeric:tabular-nums;
}
/* The module's own three accents, in the order the closing banner uses them.
   --cx-green and --cx-aurora are both too light to set type in, so the deep
   members of those ramps carry it. */
.cx-appr__card--stat:nth-child(1) .cx-appr__statn{ color:var(--cx-aurora-500,#005170); }
.cx-appr__card--stat:nth-child(2) .cx-appr__statn{ color:var(--cx-plum,#ab39bf); }
.cx-appr__card--stat:nth-child(3) .cx-appr__statn{ color:var(--cx-sage,#3e6a6b); }
.cx-appr__statl{
  margin:0;
  font-weight:500;
  font-size:clamp(14px,1.18vw,17px);
  line-height:1.4;
  color:var(--cx-knight,#3d4451);
}
.cx-appr__cardp{
  margin:0;
  /* Request 60: "can the little font be any bigger ... or perhaps make the
     font bold". Both, moderately — 16 -> 17px and 400 -> 500. The client
     explicitly allowed the cards to grow to pay for it. */
  font-weight:500;
  font-size:clamp(14px,1.18vw,17px);
  line-height:1.4;
  color:var(--cx-knight,#3d4451);
}

@media (max-width:900px){
  .cx-appr__head{ flex-direction:column; align-items:flex-start; }
  .cx-appr__row--three,
  .cx-appr__row--two{ grid-template-columns:1fr; }
  /* Figma's asymmetric card padding (40/16/32/32) reads as a mistake at this
     width — 50 left against 42 right on a 329px card. */
  .cx-appr__card{ padding:clamp(22px,5.6vw,32px); }
}

/* =======================================================================
   LOGO MARQUEE (126:3156)

   Figma's row is 1580px in a 1440 frame and the set is incomplete, so this
   scrolls. That also disposes of the wide-screen question: there is always
   more track than viewport, and the edges are masked rather than hard-cut.  */
.cx-marquee{
  position:relative;
  z-index:1;
  background:var(--cx-white,#fff9f9);
  padding:clamp(32px,4.2vw,60px) 0;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:clamp(24px,3.33vw,48px);        /* 48 at 1440 */
  overflow:hidden;
}
.cx-marquee__h{
  margin:0;
  padding:0 var(--cx-gutter);
  /* Digital/h5, 32/1.5 at 1440, tracking -0.608px. Figma sets it nowrap; the
     string is ~1020px at 32px, so it only fits above ~1200 and has to be
     allowed to wrap below that rather than force a scrollbar. */
  font-size:clamp(18px,2.22vw,32px);
  font-weight:400;
  line-height:1.5;
  letter-spacing:-0.019em;
  color:#000;
  text-align:center;
  white-space:nowrap;
}
@media (max-width:1200px){ .cx-marquee__h{ white-space:normal; } }

.cx-marquee__viewport{
  width:100%;
  overflow:hidden;
  /* The fade Ben asked for, as a MASK rather than a pair of gradient overlays:
     the mask works whatever is behind the section, where an overlay has to be
     painted in the background's own colour and breaks the moment that
     changes. */
  -webkit-mask-image:linear-gradient(90deg,
    transparent 0, #000 7%, #000 93%, transparent 100%);
  mask-image:linear-gradient(90deg,
    transparent 0, #000 7%, #000 93%, transparent 100%);
}
.cx-marquee__track{
  display:flex;
  width:max-content;
  animation:cx-marquee-run 48s linear infinite;
  will-change:transform;
}
/* Travels exactly one set, which is what makes the seam invisible. */
@keyframes cx-marquee-run{
  from{ transform:translate3d(0,0,0); }
  to  { transform:translate3d(-33.3333%,0,0); }
}
.cx-marquee:hover .cx-marquee__track,
.cx-marquee:focus-within .cx-marquee__track{ animation-play-state:paused; }

.cx-marquee__set{
  display:flex;
  align-items:center;
  margin:0; padding:0;
  list-style:none;
  /* Figma spaces seven ~156px slots across 1580 with justify-between, which
     works out at ~78px between them. A marquee needs a real gap instead, since
     justify-between has nothing to distribute against once the track is
     max-content. */
  gap:clamp(28px,5.42vw,90px);
  padding-right:clamp(28px,5.42vw,90px);   /* the seam gap */
  height:clamp(46px,5.21vw,75px);          /* the 75px slot */
}
.cx-marquee__set li{
  display:flex;
  align-items:center;
  justify-content:center;
  flex:0 0 auto;
  height:100%;
}
/* Each mark carries its own height: --lh is the px value, --lv the same value
   as a share of 1440. min() keeps it at design size above 1440 rather than
   letting brand marks inflate, and the floor stops them vanishing on mobile.

   Request 7 replaced the seven Figma marks with the eighteen on the live
   homepage, in the live page's own order. The heights are not eyeballed and not
   uniform, because uniform is wrong here — Salesforce's cloud is nearly square
   where Wolters Kluwer's wordmark is 6.2:1, and giving them a common height
   makes the wordmark enormous or the cloud a stamp.

   They come from equal optical AREA: h = sqrt(A / inkAspect), with A = 2936,
   the median area implied by the seven heights Figma specified. Two things make
   that measurable rather than a guess. `tools/probe-logo-ink.mjs` reads each
   mark's real ink extent by drawing it into a canvas and taking the alpha
   bounding box — the file's own canvas lies, Flexera shipping a 680x680 square
   whose wordmark is 636x150 — and every file is then cropped to that ink, so
   the flex gap is measured to the mark and not to invisible padding. Rerun the
   probe after changing any logo; it refuses to emit a set with >5% slack. */
.cx-marquee__set img{
  height:max(calc(var(--lh) * 0.62), min(var(--lh), var(--lv)));
  width:auto;
  display:block;
}

@media (prefers-reduced-motion:reduce){
  .cx-marquee__track{ animation:none; }
}

/* =======================================================================
   HERO (start 126:4019, end 126:4048)

   All geometry is a share of the viewport, taken from the 1440 frame, so
   nothing is pinned to a desktop width. --cx-hero-in is written by JS and runs
   0 -> 1 across the entrance; every animated value is a lerp between the two
   states' own numbers.

   The card's width and its side padding move by the SAME amount (1216/39 ->
   1282/72), which holds the content box at 1138px throughout. That is why the
   copy neither moves nor reflows while the card opens up around it.        */
/* The hero copy's start state lives HERE, not in primeHero().

   primeHero() sets the same thing, but it runs inside buildDesktop(), which
   runs on document.fonts.ready — and between first paint and that moment the
   raw copy sits in the DOM at full opacity. Measured with
   tools/probe-hero-flash.mjs: 206ms at 1440x900 of the finished headline, lede
   and buttons on screen, which then vanished and animated in from the bottom.
   Ben's read was exactly right — it has to start at the start state.

   Same pattern as `.cx-tabs[data-state="pending"] .cx-tab{opacity:0}` further
   down: CSS owns the pre-animation state, JS clears the flag once it has the
   real one applied. primeHero() clears it synchronously after its gsap.set
   calls, so no frame can land between the two.

   Three ways out, because copy that is invisible forever is far worse than a
   flash: buildMobile() clears it (the mobile hero does not animate),
   prefers-reduced-motion overrides it, and a timeout in the boot block clears
   it unconditionally if neither build path ever ran. */
.cx-hero[data-cx-hero-state="pending"] .cx-hero__eyebrow,
.cx-hero[data-cx-hero-state="pending"] .cx-hero__h,
.cx-hero[data-cx-hero-state="pending"] .cx-hero__lede,
.cx-hero[data-cx-hero-state="pending"] .cx-hero__actions{
  opacity:0;
  /* See cx-hero-failsafe: reveals itself at 3s if the script never clears the
     flag. GSAP's inline opacity beats this on the normal path. */
  animation:cx-hero-failsafe 0s linear 3s forwards;
}
@media (prefers-reduced-motion:reduce){
  .cx-hero[data-cx-hero-state="pending"] .cx-hero__eyebrow,
  .cx-hero[data-cx-hero-state="pending"] .cx-hero__h,
  .cx-hero[data-cx-hero-state="pending"] .cx-hero__lede,
  .cx-hero[data-cx-hero-state="pending"] .cx-hero__actions{
    opacity:1;
  }
}

.cx-hero{
  --cx-hero-in: 0;
  /* 703px tall in a 1440 frame. Height tracks the width so the composition
     holds its proportions, with a floor for very wide, short windows. */
  position:relative;
  width:100%;
  aspect-ratio:1440/703;
  min-height:clamp(420px, 48.8vw, 900px);
  background:var(--cx-white,#fff9f9);
  overflow:hidden;         /* the woman bleeds off the right in both states */
  isolation:isolate;
}

/* ---- the card ---------------------------------------------------------- */
.cx-hero__card{
  position:absolute;
  top:calc(100% * 72 / 703);
  /* left 105 -> 72, width 1216 -> 1282, both of 1440 */
  left:calc((7.2917% - 2.2917% * var(--cx-hero-in)) );
  width:calc(84.4444% + 4.5833% * var(--cx-hero-in));
  height:calc(100% * 526 / 703);
  background:var(--cx-charcoal,#252c38);
  border-radius:6px;
  overflow:hidden;   /* clips the ring arcs, as Figma does */
}
/* The rosette, centred on the point Figma puts it and spinning about that
   centre. 126:4051 is 1252x1253 at left 647, top -88 inside a 1282x526 card,
   so its centre lands at 1273,538.5 — 99.3% across the card and just past its
   bottom edge, which is why only the upper-left of the form is ever visible.
   Positioning by CENTRE rather than by the group's top-left corner is what
   makes the rotation stable; anchoring the box and rotating inside it would
   wobble. */
.cx-hero__bloom{
  position:absolute;
  left:99.3%;
  top:102.38%;
  width:97.66%;                 /* 1252 of 1282 */
  aspect-ratio:1394/1412;       /* the asset's own */
  pointer-events:none;
  animation:cx-hero-spin 110s linear infinite;
  will-change:transform;
}
.cx-hero__bloom img{
  width:100%; height:100%;
  display:block;
}
/* translate stays inside the keyframes so the centring survives the rotation
   — a separate transform would replace it. */
@keyframes cx-hero-spin{
  from{ transform:translate(-50%,-50%) rotate(0deg); }
  to  { transform:translate(-50%,-50%) rotate(360deg); }
}
@media (prefers-reduced-motion:reduce){
  .cx-hero__bloom{ animation:none; transform:translate(-50%,-50%); }
}

/* ---- copy -------------------------------------------------------------- */
/* Positioned against the SECTION, so it is genuinely still while the card
   opens up behind it. 144/1440 left, (72+72)/703 top, 1138/1440 wide. */
.cx-hero__copy{
  position:absolute;
  z-index:1;
  left:10%;
  top:20.4836%;
  width:79.0278%;
  display:flex;
  flex-direction:column;
  align-items:flex-start;
}
.cx-hero__eyebrow{
  margin:0 0 0.703%;      /* 8 of 1138 */
  font-weight:600;
  font-size:clamp(13px,1.25vw,18px);      /* Digital/h7 semibold 18/24 */
  line-height:1.333;
  color:var(--cx-fuchsia,#ffbaff);
}
.cx-hero__h{
  margin:0;
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:clamp(28px,3.89vw,56px);      /* Digital/h3 56/1.1 at 1440 */
  line-height:1.1;
  letter-spacing:-0.019em;                /* -1.064px at 56px */
  color:var(--cx-white,#fff9f9);
  max-width:62.478%;      /* 711 of 1138 */
}
.cx-hero__lede{
  margin:1.406% 0 0;      /* 16 of 1138 */
  font-size:clamp(13px,1.11vw,16px);
  line-height:1.5;
  color:var(--cx-creme,#f3f1e4);
  max-width:38.312%;      /* 436 of 1138 */
}
.cx-hero__actions{
  display:flex;
  align-items:center;
  gap:clamp(10px,1.39vw,20px);
  margin-top:2.109%;      /* 24 of 1138 */
}


/* ---- the two photos ---------------------------------------------------- */
/* Both interpolate their own rect between the two states. Percentages of the
   SECTION, so they scale with it. */
.cx-hero__eye,
.cx-hero__woman{
  position:absolute;
  z-index:2;
  border-radius:4.649px;
  overflow:hidden;
  pointer-events:none;
}
.cx-hero__eye img, .cx-hero__eye picture,
.cx-hero__woman img, .cx-hero__woman picture{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  display:block;
}
/* eye: 897,567 214x186  ->  871,454 266x238  (of 1440x703) */
.cx-hero__eye{
  left:calc(62.2917% - 1.8056% * var(--cx-hero-in));
  top:calc(80.6543% - 16.0740% * var(--cx-hero-in));
  width:calc(14.8611% + 3.6111% * var(--cx-hero-in));
  height:calc(26.4581% + 7.3968% * var(--cx-hero-in));
}
/* woman: 1202,33 316x329  ->  1080,31 381x397 */
.cx-hero__woman{
  left:calc(83.4722% - 8.4722% * var(--cx-hero-in));
  top:calc(4.6941% - 0.2845% * var(--cx-hero-in));
  width:calc(21.9444% + 4.5139% * var(--cx-hero-in));
  height:calc(46.7994% + 9.6728% * var(--cx-hero-in));
  z-index:3;               /* she overlaps the eye as well as the card */
}

@media (prefers-reduced-motion:reduce){
  .cx-hero{ --cx-hero-in: 1; }
}

/* =======================================================================
   OUTCOME LEAVES (126:3216) — three stacking cards on an aurora band.

   PINNED, not free-scrolling. A sticky stage holds the whole section at the
   top of the viewport so the heading stays visible for the entire sequence,
   and the cards are moved into place by progress through the track. The first
   pass let the heading scroll away and stacked the cards with sticky
   positioning — which cost ~4.9 viewports of scroll, left dead aurora at the
   end, and released the last card upward over the others when its containing
   block ran out.

   NO FIXED ASPECT on the card. Figma's 1219/533 comes from a card drawn at a
   specific size, and holding it here is over-constrained: a pinned 67px
   heading plus two readable title bands plus a 606px-tall card does not fit
   900px, so something has to give — and shrinking the card would push its left
   edge inboard of the gutter, which is exactly what stops the leaf copy
   aligning with the heading. So the card takes the width (96.25vw, matching
   Figma's ~27px margins) and whatever height is left, and the photography
   crops with object-fit. The content fits comfortably: ~400px of copy and
   stats against ~520px of card at 900 tall.                                 */
.cx-leaves{
  --cx-leaf-headh: clamp(150px, 23vh, 290px);    /* the pinned heading */
  /* What a covered card shows. This is NOT a spacing choice — it is the
     strip through which you read "Win bigger" and "Deliver smarter" while
     the third card is landing, which is the entire point of the stack. I
     trimmed it twice while chasing the heading and cut those titles in half.
     measureBand() overwrites it with the real distance from the card's top
     edge to the bottom of its title; this is only the floor. */
  --cx-leaf-band: clamp(44px, 5.5vh, 80px);
  --cx-leaf-top: clamp(10px, 1.8vh, 26px);       /* first card's rest */
  --cx-leaf-gap: clamp(48px, 9vh, 115px);        /* space under the last card */
  /* Request 8 added a hold at the end of each card's segment, which is dead
     scroll. Without more track the travel itself has to happen in 78% of the
     pixels it used to and the cards visibly snap; 85 -> 108vh keeps the motion
     at the speed it was (85 / 0.78 = 109) and spends the extra on the hold. */
  --cx-leaf-travel: 108vh;                       /* scroll per card */
  /* How far the section itself rides up as the LAST card arrives. 126:3022
     shows the heading clipped at the top of the frame — the section gives up a
     little height so the third card fits, rather than the card being squeezed
     to fit under a fully visible heading. It buys the card that same amount of
     height back, which puts it near Figma's own 2.287 aspect instead of the
     much squatter shape a strict fit forces.

     Raised alongside --cx-leaf-headh so the two stay in step: more breathing
     room above the heading would otherwise come straight out of the card's
     height, and the heading's end-state visibility (headh minus shift) would
     drift. Moving both together keeps ~126px of heading showing when the last
     card is resting, which is what 126:3022 has. */
  /* The page giving a little as the last card lands. Big enough to read as
     movement — at clamp(16px,2.7vh,40px) it was 24px and nobody could see
     it happening. The heading clipping that results is the design's own
     (126:3022 draws it cut off at the top of the frame). */
  --cx-leaf-shift: clamp(44px, 8.5vh, 120px);
  /* The leaves' own display size, below Digital/h2 on purpose. Both the
     section heading and the card titles live inside a 100vh budget, so they
     follow the same precedent as the beat headlines (which are already vh) and
     run smaller than the design's 67px: 56px at 900 tall, 62px at 1000. The
     card content had 93-498px of vertical slack at every size measured, so
     this costs nothing in fit and buys the section its padding back. */
  --cx-leaf-h2: clamp(26px, 6.2vh, 72px);
  /* 96.25vw at every size — Figma's ~27px margin either side of 1440 is
     1.875vw. Deliberately uncapped: a fixed max (1600px was the first pass)
     left ~460px of empty aurora either side at 2560 and pinned the section to
     a desktop-era width, which is the opposite of how the rest of the module
     behaves. */
  --cx-leaf-w: 96.25vw;
  /* What is left for the card once the heading and the two exposed bands
     above the last one are taken out. No longer adds --cx-leaf-shift. The formula used to inflate the card by
     the shift it assumed, which then REQUIRED that shift to show the card,
     which clipped the heading by exactly the amount it had added. The shift
     is measured from the real content now (measureLeaves), so this is just a
     floor for a card whose copy is short. */
  --cx-leaf-h: calc(100vh - var(--cx-leaf-headh) - var(--cx-leaf-top)
                    - 2 * var(--cx-leaf-band) - var(--cx-leaf-gap)
                    + var(--cx-leaf-shift));
  position:relative;
  z-index:1;
  background:var(--cx-aurora, #bbedff);
}
/* Track length is 100vh for the pin plus one travel per card that has to
   arrive. Two cards arrive, so 100 + 2*85 = 270vh — against 486vh before. */
.cx-leaves__track{
  /* --cx-leaf-extra is written by JS when a card outgrows --cx-leaf-h: the
     shift has further to go, so the scroll has to be longer or the last
     segment just moves faster. */
  height:calc(100vh + 2 * var(--cx-leaf-travel) + var(--cx-leaf-extra, 0px));
}
.cx-leaves__stage{
  position:sticky; top:var(--cx-header, 0px);
  height:calc(100vh - var(--cx-header, 0px));
  overflow:hidden;
}
/* Everything in the stage rides this, so the heading and the cards move
   together when the section gives up height for the last card. JS writes
   --cx-leaf-shift-y. */
.cx-leaves__inner{
  position:absolute; inset:0;
  transform:translateY(calc(-1 * var(--cx-leaf-shift-y, 0px)));
  will-change:transform;
}
/* The heading rides the module's own gutter — Figma's x=112 in a 1440 frame is
   7.78vw, which is what --cx-gutter already resolves to. */
.cx-leaves__head{
  position:relative; z-index:3;
  height:var(--cx-leaf-headh);
  display:flex; align-items:center;
  padding:0 var(--cx-gutter);
}
.cx-leaves__h{ max-width:18em; margin:0; font-size:var(--cx-leaf-h2); }

.cx-leaf{
  position:absolute;
  left:50%;
  top:0;
  width:var(--cx-leaf-w);
  /* A floor AND a fill. min-height keeps a long card from clipping on a
     short window; the flex column below makes the body stretch to whatever
     height the card ends up with, so justify-content:space-between still has
     room to push the stats away from the copy. Plain `height` did the second
     part but not the first; plain `min-height` did the first but collapsed
     the body to its content and slammed the stats against the lede. */
  min-height:var(--cx-leaf-h);
  display:flex;
  flex-direction:column;
  border-radius:6px;
  overflow:hidden;
  isolation:isolate;
  /* JS writes --cx-leaf-y (px from the stage top) and --cx-leaf-in (0..1). The
     scale is the expand as a card lands — annotation 126:2895 — and it
     reconciles leaf 1 being drawn at the 112px gutter (1219 wide) while the
     stack lands them all at a common, wider size. A transform, so no reflow. */
  transform:
    translate(-50%, var(--cx-leaf-y, 100vh))
    scale(calc(0.94 + 0.06 * var(--cx-leaf-in, 1)));
  transform-origin:50% 0;
  will-change:transform, filter;
}

/* ---- card background -------------------------------------------------- */
.cx-leaf__bg{ position:absolute; inset:0; z-index:0; }
.cx-leaf__bg picture,
.cx-leaf__bg img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  display:block;
  /* The expand the annotation asks for, on the PHOTO rather than the card. */
  transform:scale(calc(1.07 - 0.07 * var(--cx-leaf-in, 1)));
  transform-origin:50% 50%;
}
/* Deliver smarter layers the woman over the backdrop, then the radial wash
   from 126:3008 — rgba(0,0,0,.8) to transparent, off the top-left corner. It
   is what makes the creme copy legible; without it that card is unreadable.
   Rebuilt in CSS rather than flattened into the jpg, because flattening bakes
   in the gradient's 1327px geometry and it stops tracking the card. */
.cx-leaf__wash{
  position:absolute; inset:0;
  background:radial-gradient(
    120% 155% at 2% 6%,
    rgba(0,0,0,.80) 0%, rgba(0,0,0,.55) 34%, rgba(0,0,0,0) 78%);
}
.cx-leaf--deliver .cx-leaf__bg{ background:linear-gradient(#e8e8ee, #ecedf2 46%, #e8e9ee); }

/* ---- card content ----------------------------------------------------- */
.cx-leaf__body{
  position:relative; z-index:1;
  /* flex:1 rather than height:100% — a percentage height against a parent
     whose specified height is auto resolves to auto, which is what emptied
     the space-between. */
  flex:1 1 auto;
  display:flex;
  flex-direction:column;
  justify-content:space-between;
  /* The leaf copy aligns with the HEADING, not with the card's own edge. The
     card is inset (100vw - width)/2 from the viewport, so the padding has to
     make up the difference to the gutter — otherwise the copy sits left of the
     heading, which is what it was doing. */
  padding:clamp(24px,3.4vh,48px)
          clamp(20px,2.9vw,52px)
          clamp(20px,2.6vh,38px)
          calc(var(--cx-gutter) - (100vw - var(--cx-leaf-w)) / 2);
  gap:clamp(8px,1.4vh,22px);
}
.cx-leaf__copy{
  display:flex; flex-direction:column;
  gap:clamp(8px,1vw,16px);
  min-height:0;
}
/* The MEASURE is on the body copy, not the whole column. Figma sizes each
   title to its content (w-min-content / min-w-full) and only constrains the
   lede and the list — 446-571px against cards of 1219-1463, i.e. ~37-42%.
   Constraining the column instead wrapped "Deliver smarter" onto two lines,
   which then doubled the height of its title band in the stack. */
.cx-leaf__lede,
.cx-leaf__list{ max-width:min(42%, 24em); }
.cx-leaf__h{
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:var(--cx-leaf-h2);
  line-height:1.0;
  letter-spacing:-0.019em;   /* -1.273px at 67px */
  margin:0;
  max-width:66%;
}
.cx-leaf__lede{
  font-size:clamp(15px,1.67vw,24px);   /* Digital/h6 24/32 at 1440 */
  line-height:1.333;
  margin:0;
}
.cx-leaf__list{
  margin:0;
  padding-left:1.5em;                  /* Figma indents 24px at 16px = 1.5em */
  font-size:clamp(13px,1.11vw,16px);
  line-height:1.35;
}
.cx-leaf__list li + li{ margin-top:.5em; }   /* 8px at 16px */

.cx-leaf__foot{
  display:flex;
  align-items:flex-end;
  justify-content:space-between;
  gap:clamp(12px,1.4vw,24px);
  flex:0 0 auto;
}
.cx-leaf__stats{ display:flex; gap:clamp(6px,.83vw,12px); }
.cx-stat{
  display:flex; flex-direction:column;
  align-items:center; justify-content:center;
  text-align:center;
  border-radius:3px;
  padding:clamp(8px,1.1vh,16px) clamp(6px,.7vw,10px);
  /* Figma's boxes are 139-207px wide with the label wrapping inside. A min
     width plus a cap keeps three of them on one row at any card size. */
  min-width:clamp(84px,10.6vw,207px);
  max-width:14em;
}
.cx-stat__n{
  font-family:var(--cx-serif);
  font-weight:400;
  /* Normalised to 36px. Figma has 67px on leaf 1 and 33.703px on leaf 2, which
     is a nudged instance rather than a decision. */
  font-size:clamp(20px,2.5vw,36px);
  line-height:1;
  letter-spacing:-0.019em;
  margin-bottom:-3px;
}
.cx-stat__l{
  font-size:clamp(10px,.97vw,14px);
  line-height:1.25;
}
.cx-stat--sage   { background:var(--cx-sage,#3e6a6b);   color:var(--cx-white,#fff9f9); }
.cx-stat--fuchsia{ background:#ffbaff;                  color:var(--cx-black,#151c20); }
.cx-stat--green  { background:#00c2a2;                  color:var(--cx-black,#151c20); }
.cx-stat--stone  { background:#b0a9a1;                  color:var(--cx-black,#151c20); }
.cx-stat--knight { background:#3d4451;                  color:var(--cx-white,#fff9f9); }
.cx-stat--plum   { background:#ab39bf;                  color:var(--cx-white,#fff9f9); }
.cx-stat--aurora { background:#bbedff;                  color:var(--cx-black,#151c20); }
.cx-stat--sand   { background:#e3ddd5;                  color:var(--cx-black,#151c20); }



/* Each card's CTA is its own colourway in the design — deliberately not
   normalised. */

/* Copy colours per card (126:2870 / 126:3008 / 126:3067). */
.cx-leaf--win     .cx-leaf__body{ color:var(--cx-black,#151c20); }
.cx-leaf--deliver .cx-leaf__body{ color:#f3f1e4; }
.cx-leaf--grow    .cx-leaf__body{ color:#3d4451; }
.cx-leaf--grow    .cx-leaf__h   { color:#2e4e50; }

@media (prefers-reduced-motion:reduce){
  .cx-leaf__bg img{ transform:none; }
}

/* Figma 126:3353: row gap 24px, py 16px; columns are flex-1 with 16px internal
   gaps. Previous values were roughly 3x the real column gap. */
.cx-pillars{
  margin-top:clamp(28px,3.33vw,48px);   /* caps at 48px (32 gap + 16 py) */
  width:100%;
  display:grid;
  grid-template-columns:repeat(3, minmax(0,1fr));
  gap:clamp(16px,1.67vw,32px);          /* 24px @1440 */
  text-align:left;
}
/* Exported Figma assets, not hand-drawn paths. Each icon has its OWN size in
   the design (126:3355 is 32x32, 126:3369 is 28x25.9, 126:3373 is 29.33x18.67),
   so they get per-icon boxes rather than one square. Both dimensions are set
   explicitly and the img fills the box — never width/height auto. */
.cx-pillar__icon{
  display:block;
  margin-bottom:clamp(12px,1.11vw,22px);   /* 16px @1440 */
}
.cx-pillar__icon img{
  display:block;
  width:100%;
  height:100%;
  object-fit:contain;
}
.cx-pillar__icon--people{ width:clamp(24px,2.22vw,44px);  height:clamp(24px,2.22vw,44px); }
.cx-pillar__icon--cs    { width:clamp(21px,1.94vw,38px);  height:clamp(19px,1.80vw,35px); }
.cx-pillar__icon--fin   { width:clamp(22px,2.04vw,40px);  height:clamp(14px,1.30vw,25px); }
/* Figma 126:3366/3370/3374 use the Sub-title token: Plus Jakarta Sans SemiBold
   24 / 1.15 — NOT Digital/h6, which is the same size at Regular 400. Wrong
   weight and wrong leading in the first pass. */
.cx-pillar__h{
  font-size:var(--cx-subtitle);
  font-weight:600;
  line-height:1.15;
  margin-bottom:.667em;      /* 16px @24px, the column's gap */
}
/* Requests 13-15, second pass. These started as underlined headings; Ben asked
   for a "Learn more" text button with an arrow below each card instead, which
   is also the pattern the rest of the module already uses for a text link.

   So it is the canonical button — it joins every selector list above — in the
   .cx-appr__cta colourway, which is the one built for plum-on-creme. Only the
   box differs: the inline padding is dropped so the label starts on the same
   optical line as the heading and body above it, and the vertical padding is
   halved because there is no fill to give it a hit area. */
.cx-pillar__cta{
  --btn-bg:transparent;
  --btn-fg:var(--cx-plum,#ab39bf);
  --btn-shift:#000;
  align-self:start;
  justify-content:flex-start;
  padding:.35em 0;
  margin-top:.1em;
}
.cx-pillar__p{
  font-size:var(--cx-body);
  line-height:1.4;
  max-width:22em;
}
.cx-pillar__badge{
  display:inline-grid;
  place-items:center;
  width:2.1em;height:1.35em;
  border-radius:99px;
  background:var(--cx-black);
  color:var(--cx-white);
  font-size:.62em;
  margin-bottom:.9em;
}

/* =======================================================================
   STAGE — pinned. Holds the Rive bloom plus beats 2, 3 and 4.
   ======================================================================= */
.cx-scroller{position:relative;z-index:1;}

.cx-stage{
  position:relative;
  /* The pin holds this at the top of the viewport, so it has to be the
     viewport MINUS the host header or its lower edge falls off screen. */
  height:calc(100vh - var(--cx-header, 0px));
  height:calc(100svh - var(--cx-header, 0px));
  overflow:clip;
  display:grid;
  place-items:center;
}

/* Rive canvas — dead centre, sized off --cx-bloom. */
/* The blend group. One z-index for the whole composite; NOTHING inside it may
   carry another, because any stacking context between a .cx-leak and the
   rosette turns its mix-blend-mode into a no-op. Paint order inside is plain
   tree order: bg -> bloom -> fade -> leaks. */
.cx-render{
  position:absolute;
  inset:0;
  z-index:0;
  isolation:isolate;      /* keeps the burns off the host Optimizely page */
  overflow:clip;          /* NOT a stacking context — safe here */
  pointer-events:none;
  /* NO MASK HERE. It was added today to feather the seam at the stage's
     bottom edge, and it is being taken off again because it sits on the one
     element that must isolate the blend group.

     .cx-leak carries plus-lighter, hard-light, color-burn and saturation, and
     the note above says why they have to stay inside an isolated stacking
     context: outside one they composite against the HOST page. A mask on the
     isolating element forces WebKit down a separate compositing path, and
     mask + mix-blend-mode is a long-standing fragile pair there — it can drop
     the isolation, intermittently, under memory pressure.

     Ben reported Safari failing intermittently and a client photographed a
     giant glowing purple disc over the page, which is what an escaped
     plus-lighter leak looks like. That is not proven to be this mask. But the
     seam it fixed is cosmetic and an escaped blend mode is catastrophic, so
     the trade only goes one way. The seam is back; it needs redoing as an
     overlay that paints OVER the group rather than a mask that changes how
     the group composites. */
}
/* Second instance of the module background, positioned by JS to sit exactly
   over the real one. See [data-cx-renderbg] in the script. */
.cx-render__bg{
  position:absolute;
  left:0; top:0;
  width:100%;
  height:100%;
  background:var(--cx-white);
}

.cx-bloom{
  position:absolute;
  left:50%; top:50%;
  width:var(--cx-bloom);
  height:var(--cx-bloom);
  transform:translate(-50%,-50%);
  pointer-events:none;
}
.cx-bloom canvas{width:100%;height:100%;display:block;}

/* Radial white fade over the bloom so the copy stays legible (Figma "fade"
   ellipse 126:3463 / 126:4750). */
.cx-bloomfade{
  position:absolute;
  left:50%; top:50%;
  width:calc(var(--cx-bloom) * 1.06);
  height:calc(var(--cx-bloom) * 1.06);
  transform:translate(-50%,-50%);
  pointer-events:none;
  background:radial-gradient(closest-side,
    rgba(255,252,252,.94) 0%,
    rgba(255,252,252,.80) 34%,
    rgba(255,252,252,.34) 58%,
    rgba(255,252,252,0)   78%);
}


/* =======================================================================
   LIGHT LEAKS + SPECULAR (126:3516, 126:3522, 126:3492)
   These are gradient ellipses whose whole job is to composite OVER the
   rosette — rendered in isolation they are nearly blank. Ben's hunch was
   right and then some: the leak stack uses FOUR different blend modes.

     126:3517  Ellipse 72   normal
     126:3518  Ellipse 73   plus-lighter
     126:3519  Ellipse 12   saturation
     126:3520  Ellipse 54   hard-light      rotate -18.46deg
     126:3521  Ellipse 52   color-burn      rotate  23.33deg
     126:3492  Ellipse 52   color-burn      (beat 2 / Context)

   Every size and offset below is derived from the Figma node coordinates and
   expressed as a multiple of --cx-drawn-r, the published drawn bloom radius,
   so the whole stack scales with the rosette at any viewport.

   These live inside `.cx-render` and carry NO z-index of their own — see the
   note on `.cx-render`. Every size and offset is a multiple of --cx-drawn-r,
   the published drawn bloom radius, so the stack scales with the rosette.
   Verified against get_design_context on 126:3516: the left/top it reports for
   the two ROTATED ellipses (126:3520, 126:3521) is the axis-aligned bounding
   box, which is what these offsets are built from. get_metadata reports
   something else for those two — the rotated corner — so do not "correct"
   these values from a metadata dump.
   ======================================================================= */
.cx-leaks{
  position:absolute;
  left:50%; top:50%;
  width:0; height:0;
  pointer-events:none;
}
/* Guard 1 of 3 — see "THE THREE SAFARI/FIREFOX GUARDS" in the script.

   Safari and Firefox render the external SVG filters as opaque translucent
   shapes instead of applying the parent blend mode, and the five blended
   layers are the most expensive thing on the stage. Only the leak overlays go;
   the rosette and the stage are untouched, so the composition still reads.

   Measured on real Safari, not in a probe. WebKit under Playwright shows this
   pair as pixel-identical at the rosette, which is exactly why the automated
   check cannot be the thing that decides it. */
.cx-platform.cx-safari .cx-leaks{display:none;}
.cx-platform.cx-firefox .cx-leaks{display:none;}
.cx-leak{
  position:absolute;
  left:0; top:0;
  /* SIZED FROM A CONSTANT, SCALED BY TRANSFORM — this is the answer to "why is
     the beat transition juddery on Safari but not Chrome".

     Every beat change tweens --cx-drawn-r on the root for 0.6s, and these five
     elements used to read it for their width and height. That is a LAYOUT
     invalidation, sixty times a second, on five elements that each carry
     mix-blend-mode over an opaque background copy inside an isolated group.

     Chrome composites those blended layers on the GPU and a geometry change is
     cheap. Safari does not accelerate color-burn and saturation the same way,
     and a size change forces it to re-rasterise the whole group — viewport
     sized, five layers deep — from every frame. Hence judder exactly AT the
     transition, and only there, and only in Safari.

     So the box is now a constant multiple of --cx-leak-base and the radius
     rides in as a uniform scale(). Same rendered geometry (proved by
     tools/probe-leak-geom.mjs, which diffs every leak's rect at every beat),
     but the per-frame work becomes a composited transform.

     scale() comes FIRST so the -50% centring and the --lx/--ly offsets are
     scaled with the box; putting it later would centre against the unscaled
     size. And it stays on the leak itself rather than on .cx-leaks — a
     transform on the PARENT would create a stacking context and re-isolate the
     leaks from the rosette they are supposed to blend with, which is the exact
     bug the note above this section warns about. */
  width:calc(var(--cx-leak-base) * var(--lw));
  height:calc(var(--cx-leak-base) * var(--lh));
  /* Order, and it is load-bearing. Read right to left, which is the order the
     element is actually transformed in:
       translate(-50%,-50%)  put the box's centre on the leaks' origin
       rotate(--lr)          spin about that centre, as the original did
       translate(lx,ly)      out to the leak's place on the ring
       scale(r / base)       and finally ride the radius
     Two earlier orders were wrong and the probe caught both: scale last left
     the offsets unscaled, and rotate last spun the box about its corner
     instead of its centre — which moved only the two rotated leaks, by 313px. */
  transform:
    scale(calc(var(--cx-drawn-r,300px) / var(--cx-leak-base)))
    translate(calc(var(--cx-leak-base) * var(--lx,0)),
              calc(var(--cx-leak-base) * var(--ly,0)))
    rotate(var(--lr,0deg))
    translate(-50%,-50%);
  /* The old transform had no scale, so transform-origin never mattered —
     translate and rotate about the centre give the same result as about the
     corner for a centred box. Adding scale() makes it matter: scaling about the
     default 50% 50% moved every leak. Pin it to the corner so the chain is a
     plain sequence from the leaks' own origin. */
  transform-origin:0 0;
  opacity:0;
  will-change:transform;
}
.cx-leak img{display:block;width:100%;height:100%;}

/* only the beat that owns them */
.cx-stage[data-leak="1"] .cx-leak--ctx,
.cx-stage[data-leak="2"] .cx-leak--act{opacity:1;}

/* data-leak is an attribute swap, so without a transition the whole leak stack
   appeared and swapped in one frame — no fade into beat 2, and a hard cut from
   beat 2's single burn to beat 3's five layers. .cx-bloomfade already had a
   transition, which is why the centre wash cross-faded while the leaks around
   it popped.

   0.6s ease-in-out matches the bloom's own scale tween (gsap power2.inOut,
   0.6s) and the --cx-drawn-r ride, so the leaks, the rosette and the geometry
   they are pinned to all move on one curve. The bloomfade is brought onto the
   same timing from 0.45s: at beat 3 it hands the centre wash over to the leak
   stack's two white radials, and a shorter fade there dipped the centre dark
   before the radials had arrived. */
.cx-leak{transition:opacity .6s ease-in-out;}

/* Beat 3 hands the centre wash over to the leak stack's own two white radials
   (126:3517 normal, 126:3518 plus-lighter) — that is all Figma has there; the
   frame carries no separate fade the way beat 2's does (126:3474 / 126:3486).
   Leaving ours stacked on top of those two put a third white layer under the
   burns, and color-burn against white is the identity, so the centre of the
   rosette came back colourless. */
.cx-bloomfade{transition:opacity .6s ease-in-out;}   /* matches .cx-leak */
.cx-stage[data-leak="2"] .cx-bloomfade{opacity:0;}

.cx-leak--e72{--lw:1.174;--lh:1.168;--lx:-0.121;--ly:-0.125;}
.cx-leak--e73{--lw:1.168;--lh:1.168;--lx:0.097;--ly:0.058;  mix-blend-mode:screen;}
@supports (mix-blend-mode:plus-lighter){
  .cx-leak--e73{mix-blend-mode:plus-lighter;}
}
.cx-leak--e12{--lw:1.851;--lh:1.586;--lx:0.153;--ly:-0.101;  mix-blend-mode:saturation;}
.cx-leak--e54{--lw:1.502;--lh:1.206;--lx:0.283;--ly:0.064;--lr:-18.46deg; mix-blend-mode:hard-light;}
.cx-leak--e52{--lw:3.302;--lh:1.374;--lx:-0.069;--ly:0.436;--lr:23.33deg; mix-blend-mode:color-burn;}
/* The specular is the one layer that is NOT anchored to the rosette. In Figma
   126:3522 is a 1440-wide frame pinned to the page edges, and both its ellipses
   sit almost entirely OUTSIDE it — centres at page x -249 and x 1689 — so only
   their blur bleeds in at the left and right margins. Sizing it off
   --cx-drawn-r (3.42R = 1726px here, against Figma's 2838px on a 1440 page)
   dragged both ellipses inland, which is why they read as two discrete orange
   circles instead of light entering from the edges.

   So it is anchored to the FRAME on both axes, and the two axes disagree:
   2838.03/1440 = 197.09vw across, 1139.22/1987 = 57.33vh down. Holding the
   export's own aspect instead put the right-hand bleed 124px below the fold,
   because Figma's beat-3 frame is 1987px tall and a viewport is not. The
   export ships preserveAspectRatio="none", so stretching it is what the file
   is built for. Its top edge lands on the rosette's centre line — in Figma the
   frame starts at y 990.5 against a rosette centred at 993.8. */
.cx-leak--spec{
  width:197.09vw;
  height:57.33vh;
  transform:translate(-50%, 0);
}
.cx-leak--ctx52{--lw:3.105;--lh:2.161;--lx:0.386;--ly:0.914; mix-blend-mode:color-burn;}

/* Mobile drops the leaks, so the group no longer needs an opaque ground —
   and there is no pin, so nothing would be keeping the copy registered with
   the real background anyway. Hide it or it covers the wash with the wrong
   slice of the module. */
@media (max-width:900px){ .cx-leaks, .cx-render__bg{display:none;} }

/* ---- beat copy layers: stacked, cross-faded ---------------------------- */
.cx-beat{
  position:absolute;
  inset:0;
  display:grid;
  place-items:center;
  padding:0 var(--cx-gutter);
  opacity:0;
  visibility:hidden;
}
.cx-beat[data-active="true"]{visibility:visible;}
.cx-beat__copy{
  position:relative;
  z-index:3;
  display:grid;
  justify-items:center;
  text-align:center;
  max-width:min(46em, 78vw);
}
.cx-beat__copy .cx-h2,
.cx-beat__copy .cx-h3{text-align:center;}
/* Beat headlines run smaller than Digital/h2's 67px, and deliberately.

   Figma's beat frames are 1355px (Context) and 1987px (Action) tall, so 67px
   display type sits in roughly twice the vertical room a pinned beat has. In
   100vh it crowds the rosette and pushes the lede and button toward the
   satellites. This is the same frame-height mismatch that forced the badge
   ring into an ellipse — the agency's frames were never a viewport.

   Scoped to .cx-beat, so the module title (126:3346) and the Veda heading keep
   the full 67px — those live in normal-flow sections with the height to carry
   it. vh rather than vw because everything else in the beat composition is
   height-driven: 6vh is 54px at 900 tall, ~19% down from 67px.

   The measure is in em, so the line breaks are unchanged — at 54px the two-line
   break needs ~474px and 9.6em gives 518px. */
.cx-beat__copy .cx-h2{font-size:clamp(28px, 6vh, 76px);}
.cx-beat__copy .cx-body{
  margin-top:clamp(14px,1.25vw,24px);
  text-align:center;
  max-width:31em;
}
.cx-beat__copy .cx-btn{margin-top:clamp(20px,2.2vw,42px);}

/* Mask wrapper for SplitText line reveals. */
.cx-line-mask{overflow:hidden;display:block;}

/* ---- satellites: logos, data pills, journey badges --------------------
   Polar placement. --a = angle in degrees, --r = radius as a multiple of the
   bloom's own size. The counter-rotate keeps each chip upright.            */
.cx-orbit{
  position:absolute;
  left:50%; top:50%;
  width:0; height:0;
  z-index:2;
  /* The badges are decorative, and after the Veda flight they sit exactly on
     top of the real tab buttons. Without this they swallow every click and the
     tab bar looks broken — which is precisely what happened. My own test
     called .click() on the element directly, which bypasses hit-testing, so it
     passed while the page was unusable. */
  pointer-events:none;
}
.cx-sat{
  position:absolute;
  left:0; top:0;
  /* --r is the satellite's distance as a multiple of the DRAWN bloom radius,
     taken straight off the Figma geometry (e.g. the SSO data pills sit at
     585px from a bloom of radius 408.5 = 1.43). --cx-drawn-r is that radius in
     px, published from JS per beat, because the Rive artboard is sized for the
     final rosette and so draws each state at a different fraction of the
     canvas. Calibrating against the canvas box instead of the drawn shape is
     what threw the ring out on the first pass — and then again on the design
     side, where --r was derived from the rosette FRAME (1664px) instead of its
     drawn extent (1329x1317, measured off an isolated Figma render at the same
     alpha>12 threshold verify.mjs uses). That inflated the denominator by 25%
     and put every badge a quarter of the way inside the rim.

     This was rotate(--a) translateX(r) rotate(calc(-1 * --a)), which composes
     to exactly translate(r*cos a, r*sin a) with the element left upright. Spelt
     out with trig instead, because the ring needs to compress on ONE axis:
     Figma's beat-3 frame is 1987px tall, so its circular badge ring does not
     fit a 900px stage. --cx-sat-ky squashes it into an ellipse — design radius
     horizontally, scaled vertically to fit — which keeps the angular
     arrangement and the on-rim placement without clipping Sell off the top.
     Defaults to 1, so every other orbit (SSO pills, logos) is untouched. */
  transform:
    translate(
      calc(var(--cx-drawn-r, 300px) * var(--r) * cos(var(--a))),
      calc(var(--cx-drawn-r, 300px) * var(--r) * sin(var(--a)) * var(--cx-sat-ky, 1))
    )
    translate(-50%,-50%);
  will-change:transform,opacity;
}
.cx-sat__inner{
  display:flex;
  align-items:center;
  gap:.66em;
  white-space:nowrap;
}

/* Data tags (126:3381, 126:3410, 126:3414, 126:3422). These were bare text on
   the background; in Figma each is the same glass chip as a journey badge —
   bg #FFF9F9 at 30%, backdrop-blur 8px, radius 6px — with a 32px icon and
   Plus Jakarta Sans SemiBold 24/32. Padding is asymmetric: 12px top, bottom
   and left, 32px right, with a 10px gap.

   Everything is in em off the font size, so the chip scales as one object.

   The font does NOT share .cx-badge's dial, though both are 24/32 SemiBold in
   a 1440 frame. Each belongs to the rosette of ITS OWN beat, and the two beats
   sit at very different fractions of their design size:

     beat 1  design drawn radius 408.5, build 414.1  ->  1.014, so ~24px
     beat 3  design drawn radius 658.5, build 524.7  ->  0.797, so ~19px

   Beat 1's rosette is essentially full design size, so these pills are too;
   beat 3's is 20% under, which is why the badges are. Reusing the badge value
   here (2.13vh) made the pills 20% too small. 2.7vh is 24.3px at 900 tall and
   tracks --cx-drawn-r's own vh basis from there. */
.cx-pill{
  display:flex;
  align-items:center;
  gap:.4167em;                 /* 10/24 */
  padding:.5em 1.3333em .5em .5em;  /* 12px / 32px / 12px / 12px at 24px */
  white-space:nowrap;
  /* 2.7vh reproduced Figma's 24px exactly at 900 tall (see the note above).
     Held against the real composition that reads too heavy — the chips compete
     with the headline instead of sitting behind it — so this is a deliberate
     20% under the design at 2.16vh, i.e. ~19.4px at 900. Everything else in
     the chip is in em, so the icon, padding and gap all come down with it. */
  font-size:clamp(14px, 2.16vh, 36px);
  font-weight:600;
  line-height:1.3333;          /* 32/24 */
  letter-spacing:0;
  color:var(--cx-black);
  background:rgba(255,249,249,.30);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  border-radius:6px;
}
.cx-pill__icon{
  width:1.3333em;height:1.3333em;   /* 32/24 */
  flex:0 0 auto;
  display:block;
}

/* ---- idle drift on beat 1's satellites --------------------------------
   The logos and data tags breathe while you sit on the beat, so a paused
   scroll is not a dead frame. Amplitude is deliberately small — 5-8px — because
   each satellite sits at the far end of a particle stream and the streams are
   drawn along FIXED rays; drift much further and a logo visibly detaches from
   the line feeding it. At 6px against a ~48px logo it reads as float, and the
   connection holds.

   The animation is on the chip, one level inside .cx-float, because GSAP owns
   .cx-float's transform for the beat reveal — see the markup note. */
.cx-float{display:block;}
@keyframes cx-drift{
  0%,100%{ transform:translate(0,0); }
  25%     { transform:translate(var(--fx,5px), var(--fy,-5px)); }
  50%     { transform:translate(calc(var(--fx,5px) * -0.55), calc(var(--fy,-5px) * -0.75)); }
  75%     { transform:translate(calc(var(--fx,5px) * 0.35), calc(var(--fy,-5px) * 0.6)); }
}
[data-cx-orbit="0"] .cx-pill,
[data-cx-orbit="0"] .cx-mark{
  animation:cx-drift var(--fd,10s) ease-in-out var(--fdl,0s) infinite;
  will-change:transform;
}
/* Off-beat the whole thing is hidden, so stop burning frames on ten infinite
   animations — same reasoning as the streams' data-flow="off". */
.cx-beat[data-active="false"] [data-cx-orbit="0"] .cx-pill,
.cx-beat[data-active="false"] [data-cx-orbit="0"] .cx-mark{
  animation-play-state:paused;
}
@media (prefers-reduced-motion:reduce){
  [data-cx-orbit="0"] .cx-pill,
  [data-cx-orbit="0"] .cx-mark{ animation:none; }
}

/* Integration marks. Bare logos on the background — no tile, no card; the
   Figma render has nothing behind them. ~49px in a 1440 frame = 3.4vw. */
.cx-mark{
  display:block;
  width:clamp(26px,3.4vw,60px);
  height:clamp(26px,3.4vw,60px);
}
.cx-mark img{width:100%;height:100%;object-fit:contain;display:block;}

/* Journey badges (beat 4). Real values from get_design_context on 126:3526:
   bg rgba(255,249,249,0.3) — Certinia WHITE at 30%, not grey — backdrop-blur
   8px, radius 6px, padding 12px, Plus Jakarta Sans SemiBold 24/32. The first
   pass had all six of those wrong. */
.cx-badge{
  display:inline-block;              /* transforms don't apply to inline spans */
  /* Was clamp(16px, 1.67vw, 30px) — the design's 24px, sized against the PAGE.
     But a badge belongs to the rosette, not the page: Figma draws 24px text
     against a 1317px rosette (24/658.5 = 0.0364 of its drawn radius), and our
     rosette is smaller than that on any real viewport, so page-relative type
     read oversized — 11.4% of the rosette against the design's 8.96%.

     vh, not --cx-drawn-r, even though the radius is what this tracks: the
     property is removed when the pin releases, and the .cx-flight__chip copies
     are mid-air across that boundary. The rosette is vh-bound anyway
     (min(70vw, 108vh)), so 2.13vh reproduces 0.0364 x drawn-r wherever height
     is the binding term — 19px at 900 tall, 23px at 1080, capped 30px at 1440.
     The mobile breakpoint hides this orbit, so the 70vw case never shows. */
  /* One shared ramp with .cx-tab — see --cx-journey. The note above explains
     why this used to be vh-only: a badge belongs to the rosette, and
     page-relative type read oversized against it. --cx-journey keeps the vh
     term in the min(), so the rosette still gets a say; it just no longer
     disagrees with the tab it flies into. */
  font-size:var(--cx-journey);
  font-weight:600;
  line-height:1.2;
  letter-spacing:-0.01em;
  color:var(--cx-black);
  background:rgba(255,249,249,.30);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  border-radius:6px;
  /* 12px against 24px text in Figma, i.e. exactly half an em. Expressed that
     way it tracks the font size instead of needing its own clamp. */
  padding:.5em;
}

/* =======================================================================
   BEAT 4 (index 3) — "Meet Veda". Figma 126:3320 + 126:3275 + 190:885.
   The five journey badges from the previous beat fly into this tab bar
   (annotation 126:2903), so the bar's chrome is deliberately identical to the
   badges': rgba(255,249,249,0.3) over an 8px backdrop blur at 6px radius.
   ======================================================================= */
.cx-veda{
  position:relative;
  z-index:3;
  display:grid;
  justify-items:center;
  /* 24 -> 17 at 1440; four of these gaps stack up the section, and the design's
     24 was measured against a shorter panel than Manage now needs. */
  gap:clamp(11px,1.18vw,22px);
  width:100%;
  max-width:min(86.5vw, 1500px);     /* the panel is 1245 of 1440 = 86.5% */
}
.cx-veda__h{
  font-family:var(--cx-serif);
  font-weight:400;
  font-size:var(--cx-h3);            /* 56px @1440 */
  line-height:1.1;
  letter-spacing:-0.019em;           /* -1.064px @56px */
  text-align:center;
  max-width:13.4em;                  /* 697px @56px, eased out slightly */
  text-wrap:balance;                 /* was missing — hence the orphaned line */
}
.cx-veda__sub{
  font-size:var(--cx-label);
  font-weight:500;
  line-height:1.5;
  letter-spacing:-0.011em;
  text-align:center;
  max-width:30.6em;                  /* 550px @18px */
}

/* The Veda section in normal flow. min-height 100vh so it holds the screen
   while someone reads and clicks, then scrolls on like any other section. */
.cx-veda-section{
  position:relative;
  z-index:1;
  /* The header eats the top of every viewport unit on the live site, so the
     floor is what is actually left, not the whole screen. */
  min-height:calc(100vh - var(--cx-header, 0px));
  min-height:calc(100svh - var(--cx-header, 0px));
  display:grid;
  place-items:center;
  /* Request 24, second half. The vh-driven padding was spending 180px of a
     900px viewport on air above and below a section that already overflows;
     these ramps give ~58px of it back without changing the 1440 composition
     (the clamps still reach 140/200 on a tall screen). */
  padding:clamp(32px,4.4vh,120px) var(--cx-gutter) clamp(40px,6.4vh,170px);
}

/* Flight layer: viewport-fixed, chips positioned in LAYER-LOCAL coordinates. */
.cx-flight{
  position:fixed;
  inset:0;
  z-index:60;
  pointer-events:none;
  visibility:hidden;
}
.cx-flight__chip{
  position:absolute;
  left:0; top:0;
  will-change:transform;
}

/* ---- tab bar (126:3324) ------------------------------------------------ */
/* Tab labels start hidden in CSS, not from JS. Doing it in buildDesktop meant
   the correct state arrived a frame or two late, which is visible as a flash. */
.cx-tabs[data-state="pending"] .cx-tab{opacity:0;}
.cx-tabs{
  display:flex;
  align-items:center;
  justify-content:space-between;
  width:min(54.86vw, 950px);         /* 790px @1440 */
  /* the tab is 1.95em of --cx-journey; this is that plus the 8px inset */
  height:calc(1.95 * var(--cx-journey) + 8px);
  padding:0 clamp(13px,1.67vw,30px) 0 clamp(3px,.42vw,8px);
  background:rgba(255,249,249,.30);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  border-radius:6px;
}
/* EVERY tab carries the selected box geometry; only the fill and text colour
   change. Two reasons. Selecting a tab must not reflow the bar, or the labels
   jump under the cursor as you switch. And the badge flight measures these
   rects — when the pill used to add min-width/padding on selection it reflowed
   the bar AFTER the badges had landed, which put the landing 15-53px out. */
.cx-tab{
  appearance:none; border:0; background:none; cursor:pointer;
  /* Request 21, as revised by Ben: bigger, but SANS — the serif read wrong
     here — and identical in size on the rosette and in the tab bar.

     Both sides now take --cx-journey, so "identical" is structural rather than
     two ramps that happen to agree at 1440. That also makes the flight's scale
     tween resolve to exactly 1: updateHandoff() computes `ts / fs` off the live
     computed sizes, so the chip now only travels, which is what "same size in
     both places" means in motion. */
  font-family:var(--cx-sans);
  font-size:var(--cx-journey);
  font-weight:600;
  line-height:1.2;
  letter-spacing:-0.01em;
  color:var(--cx-black);
  white-space:nowrap;
  border-radius:4px;                 /* Corner/Extra-small */
  display:grid; place-items:center;
  /* em-based, so the box tracks --cx-journey instead of needing a third ramp
     kept in sync by hand. */
  height:1.95em;
  min-width:4.2em;
  padding:0 .72em;
  transition:background .28s ease, color .28s ease;
}
.cx-tab[aria-selected="true"]{
  background:var(--cx-sage);
  color:var(--cx-creme);
}
.cx-tab:focus-visible{outline:2px solid var(--cx-sage);outline-offset:3px;}

/* ---- panels (126:3275) ------------------------------------------------- */
.cx-panels{position:relative;width:100%;margin-top:clamp(10px,1.4vw,26px);}
.cx-panel{
  display:grid;
  grid-template-columns:minmax(0,438fr) minmax(0,679fr);
  gap:clamp(16px,4.4vw,90px);
  align-items:start;
  position:absolute; inset:0;
  opacity:0; visibility:hidden;
}
.cx-panel[aria-hidden="false"]{opacity:1;visibility:visible;position:relative;}
/* Panel content is dense UI, not display type. The full-width rule (scale with
   vw) is right for headings and wrong here: at 2000px+ the rows were rendering
   ~25px and the six-row list stopped fitting a pinned viewport. So this block
   scales UP to the design size and then STOPS. Headings still scale freely. */

/* 126:3278 — Digital/h6: PJS Regular 24/32. */
.cx-panel__lede{
  font-size:clamp(18px,1.67vw,24px);   /* caps at the design's 24px */
  font-weight:400;
  line-height:1.333;                   /* 32/24 */
  letter-spacing:0;
  text-align:left;
  margin-bottom:clamp(22px,2.43vw,35px);  /* caps at the design's 35px */
}
/* 126:3280 — Digital/h7 semibold: PJS SemiBold 18/24. Was Medium. */
.cx-panel__label{
  font-size:clamp(15px,1.25vw,18px);   /* caps at the design's 18px */
  font-weight:600;
  line-height:1.333;                   /* 24/18 */
  letter-spacing:0;
  text-align:left;
  margin-bottom:clamp(16px,1.67vw,24px);  /* caps at the design's 24px */
}
.cx-agentlist{display:grid;gap:clamp(6px,.63vw,9px);}   /* 12 -> 9: see the Veda cap note */
/* rows are revealed with a stagger; keep them out of the way until then */
.cx-panel[aria-hidden="true"] .cx-agentrow{opacity:0;}
.cx-agentrow{
  display:flex;
  align-items:center;
  gap:clamp(8px,1.11vw,22px);        /* 16px @1440 */
  padding-right:clamp(8px,1.11vw,22px);
  /* 40 -> 36 at 1440. Manage carries seven rows since request 51-52, and the
     left column — not the video — is what sets the panel's height on the two
     busiest tabs. Four pixels a row and three off the gap is 46px back on
     Manage for a row that still clears its 34px avatar comfortably. */
  height:clamp(30px,2.5vw,45px);     /* 36px @1440 */
  background:var(--cx-row,#fce5ff);
  border-radius:6px;
  overflow:clip;
}
/* Each tab has ONE pair — the row fill and the tint behind the avatar — and
   the agent frames (273:835-839) are where they live. The previous reading
   came from 126:3282/3289 and varied by row POSITION instead, which put a
   creme and a green row at the top of every panel regardless of tab. */
.cx-panel                    { --cx-row:var(--cx-fuchsia-100,#fce5ff); --cx-tint:#f5bcfa; }
.cx-panel[data-cx-panel="1"] { --cx-row:#a5d7c3; --cx-tint:var(--cx-green,#00c2a2); }
.cx-panel[data-cx-panel="2"] { --cx-row:#deeef3; --cx-tint:var(--cx-aurora,#bbedff); }
.cx-panel[data-cx-panel="3"] { --cx-row:#c39ec7; --cx-tint:var(--cx-plum,#ab39bf); }
.cx-panel[data-cx-panel="4"] { --cx-row:#9eaaa9; --cx-tint:#4a6868; }
.cx-agentrow__avatar{
  flex:0 0 auto;
  width:clamp(48px,4.44vw,66px);     /* caps at the design's 64px */
  align-self:stretch;
  background:var(--cx-tint,#f5bcfa); /* only seen if a tile fails to load */
  /* The row already clips its own left corners, but each tile carries
     Figma's dark canvas in the corner pixels outside that radius (the export
     has no alpha), so clip here too — at exactly the row's radius — and the
     browser's own anti-aliasing removes them rather than leaving a dark
     fringe on a sub-pixel disagreement. */
  border-radius:6px 0 0 6px;
  overflow:clip;
}
.cx-agentrow__avatar picture{display:contents;}
/* Each tile is the avatar as Figma renders it, sliced out of the panel frame
   exported at 4x — tint composited, crop baked.

   The editable alternative was carrying Figma's per-avatar crop transform
   through as CSS percentages, and it was tried first. It did not reproduce
   the design: the generated top/height percentages place the source looser
   than Figma's own canvas does (a visible band of tint above the first
   headshot that the design does not have), and three of the twenty-five
   source fills export blank from the asset endpoint entirely — twice over —
   though the design plainly renders photographs there. Slicing the rendered
   panel is exact for all twenty-five and lands smaller: 412KB for the set.

   The cost is that a re-crop, a new agent, or a change to --cx-tint means a
   re-export from Figma rather than an edit here. Export the panel frame
   (273:835-839) at 4x and slice at 256x161 on 208.667px centres. */
.cx-agentrow__avatar img{
  width:100%;
  height:100%;
  object-fit:cover;
  display:block;
}
.cx-agentrow__name{
  flex:1 1 auto; min-width:0;
  font-size:clamp(15px,1.25vw,18px);   /* caps at the design's 18px */
  font-weight:500;
  line-height:1.5;
  letter-spacing:-0.011em;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  text-align:left;
}
.cx-agentrow__info{
  flex:0 0 auto;
  width:clamp(18px,1.67vw,24px);       /* caps at the design's 24px */
  height:clamp(18px,1.67vw,24px);
  opacity:.5;
}
.cx-agentrow__info svg{width:100%;height:100%;display:block;fill:none;stroke:currentColor;stroke-width:1.6;}

/* Right-hand product UI. Deliberately an empty card rather than a fabricated
   chart — the real screenshots are per-tab assets that still need exporting. */
.cx-panel__viz{
  /* Was a 679/584 slot sized for a product SCREENSHOT that never arrived.
     The real asset is a 1920x1080 screen recording, so the frame follows the
     content: width-driven at 16:9, with the brand gradient left as the mat
     around it rather than the ground behind an empty card. Shorter than the
     old slot, so the pinned Veda section still fits one viewport without the
     46vh guard doing the work. */
  position:relative;
  width:100%;
  max-width:679px;
  height:auto;
  justify-self:end;
  align-self:center;
  /* Capped so the three quiet tabs (Plan, Deliver, Expand) come down with the
     busy ones rather than holding the section at the poster's full 16:9 height.
     max-height with an auto width keeps the 16:9 letterbox intact — the mat
     absorbs the difference. */
  max-height:clamp(210px,23.6vw,340px);
  box-sizing:border-box;
  padding:clamp(8px,1.2vw,18px);
  border-radius:10px;
  background:
    radial-gradient(60% 55% at 14% 10%, rgba(255,186,255,.50) 0%, rgba(255,186,255,0) 70%),
    radial-gradient(55% 50% at 88% 16%, rgba(0,194,162,.36) 0%, rgba(0,194,162,0) 72%),
    radial-gradient(60% 55% at 86% 88%, rgba(171,57,191,.26) 0%, rgba(171,57,191,0) 74%),
    linear-gradient(180deg,#fffdfd,#fdf7fb);
}
.cx-panel__play{
  position:relative;
  display:block;
  width:100%;
  aspect-ratio:16/9;
  margin:0;
  padding:0;
  border:0;
  border-radius:6px;
  overflow:hidden;
  cursor:pointer;
  background:var(--cx-charcoal,#252c38);
  box-shadow:0 1px 2px rgba(21,28,32,.08), 0 14px 38px rgba(21,28,32,.10);
}
.cx-panel__play picture{display:contents;}
.cx-panel__play img{
  width:100%; height:100%;
  object-fit:cover;
  display:block;
  transition:transform .35s ease, filter .35s ease;
}
/* A screen recording of a dense UI is a busy poster. The scrim is what makes
   the play control and its label legible over any frame. */
.cx-panel__play::after{
  content:"";
  position:absolute; inset:0;
  background:linear-gradient(180deg, rgba(21,28,32,.34) 0%, rgba(21,28,32,.05) 42%, rgba(21,28,32,.62) 100%);
  transition:opacity .35s ease;
}
.cx-panel__playbadge{
  position:absolute;
  left:50%; top:50%;
  translate:-50% -50%;
  z-index:1;
  width:clamp(48px,4.9vw,68px);
  height:clamp(48px,4.9vw,68px);
  border-radius:50%;
  background:rgba(255,249,249,.94);
  box-shadow:0 6px 22px rgba(21,28,32,.28);
  transition:transform .3s cubic-bezier(.2,.8,.3,1), background-color .3s ease;
}
/* The triangle, drawn rather than an asset: one clip-path beats a request. */
.cx-panel__playbadge::before{
  content:"";
  position:absolute;
  left:54%; top:50%;
  translate:-50% -50%;
  width:34%; height:38%;
  background:var(--cx-charcoal,#252c38);
  clip-path:polygon(0 0, 100% 50%, 0 100%);
}
.cx-panel__playlabel{
  position:absolute;
  left:clamp(12px,1.4vw,20px);
  right:clamp(12px,1.4vw,20px);
  bottom:clamp(10px,1.2vw,16px);
  z-index:1;
  font-size:clamp(11px,.97vw,14px);
  font-weight:600;
  line-height:1.3;
  letter-spacing:-.006em;
  color:var(--cx-white,#fff9f9);
  text-align:left;
  text-shadow:0 1px 3px rgba(21,28,32,.5);
}
.cx-panel__playlabel b{
  font-weight:500;
  opacity:.72;
  font-variant-numeric:tabular-nums;
  margin-left:.4em;
}
.cx-panel__play:hover img{ transform:scale(1.028); }
.cx-panel__play:hover .cx-panel__playbadge{ transform:scale(1.07); background:#fff; }
.cx-panel__play:focus-visible{ outline:3px solid var(--cx-plum,#ab39bf); outline-offset:3px; }
@media (prefers-reduced-motion:reduce){
  .cx-panel__play img,
  .cx-panel__playbadge{ transition:none; }
  .cx-panel__play:hover img{ transform:none; }
}

/* ---- the modal ---------------------------------------------------------
   <dialog> rather than a hand-rolled overlay: Esc, focus trapping and the
   backdrop come with it, and showModal() renders in the top layer — above
   the host page's fixed header and outside any wrapper that clips us, which
   a z-index cannot promise inside someone else's stacking context. */
.cx-vmodal{
  /* Positioned explicitly rather than leaning on the UA stylesheet's
     `inset:0; margin:auto` for an open dialog. Certinia's site has its own
     rule for `dialog` and it wins over the UA sheet, which parked the modal
     in the top-left corner. Nothing in this block should depend on a default
     the host page can quietly redefine. */
  position:fixed;
  inset:0;
  margin:auto;
  width:min(1100px, 92vw);
  height:max-content;
  max-width:none;
  max-height:calc(100vh - 5rem);
  padding:0;
  border:0;
  border-radius:10px;
  background:var(--cx-black,#151c20);
  overflow:visible;
  color:var(--cx-white,#fff9f9);
}
.cx-vmodal::backdrop{
  background:rgba(12,15,18,.82);
  backdrop-filter:blur(3px);
}
.cx-vmodal__video{
  display:block;
  width:100%;
  aspect-ratio:16/9;
  border-radius:10px;
  background:#000;
}
/* The iframe player. `border:0` matters — a UA default border would sit inside
   the radius. */
.cx-vmodal__embed{ border:0; }

/* The hero's start state RELEASES ITSELF after 3s, whatever the script does.

   This is not belt-and-braces, it is the fix for a bug I shipped. The JS
   fallback was a `setTimeout(releaseHero, 2500)` placed at the END of the boot
   block — after buildVideoModal(), publishHeader(), applyFullBleed(),
   checkAssets() and initSmoothScroll(), all of which touch the host page, its
   CSP or Lenis. Any one of them throwing means the timeout is never registered
   and the copy stays hidden FOREVER. Ben's client saw exactly that: the hero
   card, rosette and both photos rendering with no headline, lede or buttons.

   A keyframe needs no script to fire, so there is now no JS path that can
   strand the copy. On the normal path the flag is gone inside ~500ms and this
   never runs; if fonts somehow took longer than 3s the copy appears and then
   plays its reveal, which is worse-looking and far better than blank.

   The lesson, written down because I got it backwards once: a safety net has
   to be installed BEFORE the thing it catches, not after it. */
@keyframes cx-hero-failsafe { to { opacity:1; } }

/* Hiding the idle player needs THIS rule, not the `hidden` attribute alone.
   The UA sheet's `[hidden]{display:none}` is a (0,1,0) type-and-attribute rule
   and `.cx-vmodal__video{display:block}` above is (0,1,0) too but comes later,
   so it wins — the attribute was set, `display` still computed to `block`, and
   both players rendered stacked: an empty <video> with its own controls at
   0:00 sitting on top of the live Vidyard iframe. Ben caught it on the site.

   Same failure as the dialog's own positioning earlier in this file: a UA or
   host default we were relying on, beaten by one of our own class rules. */
.cx-vmodal__video[hidden]{ display:none; }
.cx-vmodal__close{
  position:absolute;
  top:-14px; right:-14px;
  width:40px; height:40px;
  display:grid; place-items:center;
  border:0; border-radius:50%;
  background:var(--cx-white,#fff9f9);
  color:var(--cx-charcoal,#252c38);
  cursor:pointer;
  box-shadow:0 4px 16px rgba(0,0,0,.4);
}
.cx-vmodal__close svg{ width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:2.2; stroke-linecap:round; }
.cx-vmodal__close:focus-visible{ outline:3px solid var(--cx-plum,#ab39bf); outline-offset:2px; }
@media (max-width:600px){
  .cx-vmodal{ width:96vw; }
  .cx-vmodal__close{ top:-46px; right:0; }
}

/* The journey orbit lives on the STAGE, not inside a beat, so the badges
   survive the beat 3 -> beat 4 handoff and can fly into the tab bar. */
/* Only the journey ring is elliptical — it is the one taken from a 1987px-tall
   Figma frame. --cx-ring-ky is published from JS next to --cx-drawn-r, because
   CSS calc cannot divide a length by a length to get the ratio. */
.cx-orbit--journey{z-index:5; --cx-sat-ky:var(--cx-ring-ky, 1);}

/* ---- particle streams radiating from the bloom (annotation 126:2900) ---
   Generated, not exported. Figma has 18 `burst` SVGs drawn for a 1440 frame;
   generating the particles means each stream reaches the viewport edge at any
   width, and the size/spacing variation that makes it read as a stream rather
   than a dotted rule survives being rescaled. */
.cx-rays{
  position:absolute;
  inset:0;
  z-index:1;
  pointer-events:none;
  overflow:visible;
  opacity:0;
}
/* The receding rays. z-index MUST stay auto: this one lives inside .cx-render,
   and any z-index on a child of that group re-isolates it and the light leaks
   stop blending. Depth order comes from DOM position instead.
   The blur is atmospheric perspective — something heading away from the viewer
   goes very slightly soft, which sells the depth more cheaply than any amount
   of size or opacity difference. */
.cx-rays--back{ z-index:auto; filter:blur(0.7px); }
/* Each particle drifts outward along its own ray and fades at both ends of its
   cycle, so new ones keep emerging near the bloom while old ones dissolve at the
   far end. Opacity hitting 0 at 0% and 100% is what removes the seam — there is
   no wrap to hide. Phase is spread by a negative per-particle delay, so the
   stream is already in mid-flow the moment it appears.

   Driven by CSS rather than a rAF loop on purpose: ~400 circles is too many to
   rewrite from JS every frame while Rive and ScrollTrigger are also running. */
.cx-rays circle{
  fill:var(--cx-green);
  animation-name:cx-flow;
  animation-timing-function:linear;
  animation-iteration-count:infinite;
  will-change:transform,opacity;
}
.cx-rays[data-flow="off"] circle{
  animation-play-state:paused;   /* nothing repaints on the other beats */
}
@keyframes cx-flow{
  0%   { opacity:0;            transform:translate(0,0); }
  16%  { opacity:var(--op,.8); }
  74%  { opacity:var(--op,.8); }
  100% { opacity:0;            transform:translate(var(--dx,0px), var(--dy,0px)); }
}

/* ---- context beat photos: anchored to viewport edges ------------------- */
/* aspect ratios are the exported files' own dimensions, so nothing distorts */
/* Beat 2 photography (126:3493, 126:3494, 126:3495).

   These were anchored to the PAGE edges — right:0 / left:0 with a translateX
   push-off — which was wrong twice over. In the design none of the three is
   edge-anchored: measured in the 1440x1355 frame, the top-right photo sits
   48.3px CLEAR of the right edge, the left one 117.3px in, and only the
   bottom-right bleeds (36.3px). What they actually share is the ROSETTE: its
   drawn extent spans x 183-1327 and all three straddle its rim, exactly as the
   journey badges do. So they are placed off --cx-drawn-r, which also means they
   scale and stay attached to the rim at any viewport instead of drifting out
   into empty margin on a wide screen.

   Offsets are photo-centre minus rosette-centre (719.8, 761.8), over the drawn
   radius 574.2 — measured off an isolated render at the same alpha>12 threshold
   verify.mjs uses, not the frame box.

   HORIZONTAL is rosette-relative; VERTICAL stays a stage percentage, taken
   from the design's frame fractions. The frame is 1355 tall against a
   one-viewport stage, so the design's vertical offsets (up to -0.90 of the
   radius) would clip the top-right photo off the stage — the same mismatch the
   badge ring has, and a percentage sidesteps it without a second squash dial.

   Centring is done with NEGATIVE MARGINS, not a transform and not the
   standalone `translate` property. GSAP animates x/scaleX on these for the
   reveal, and it does not merely add to `transform` — it normalises the
   individual translate/rotate/scale properties INTO transform and sets
   `translate: none`. The x offset it caches while doing so is unreliable: with
   `translate:-50% -50%` here, exactly one photo per viewport ended up with
   tx=0 and sat half a width too far out (it passed at 1440 and 1920 and failed
   at 1280 and 2560, which is what gave the race away). Margins are outside the
   transform stack entirely, so GSAP cannot touch them.

   Height is therefore stated explicitly rather than left to aspect-ratio, so
   that --ar can also produce the vertical half-offset. */
.cx-bleed--ctx-tr,
.cx-bleed--ctx-br,
.cx-bleed--ctx-l{
  left:calc(50% + var(--cx-drawn-r, 300px) * var(--px));
  width:calc(var(--cx-drawn-r, 300px) * var(--pw));
  height:calc(var(--cx-drawn-r, 300px) * var(--pw) / var(--ar));
  margin-left:calc(var(--cx-drawn-r, 300px) * var(--pw) / -2);
  margin-top:calc(var(--cx-drawn-r, 300px) * var(--pw) / var(--ar) / -2);
  /* All three carry rounded-[3px] in Figma; .cx-bleed itself has no radius, so
     these were rendering square. Matches .cx-bleed--title-l, which sets it. */
  border-radius:3px;
}
.cx-bleed--ctx-tr{--px: 0.9095; --pw:0.5216; --ar:1.34005; top:18.2%;}
.cx-bleed--ctx-br{--px: 1.0423; --pw:0.5504; --ar:1.38596; top:61.4%;}
.cx-bleed--ctx-l {--px:-0.8422; --pw:0.4143; --ar:1.37530; top:62.9%;}
.cx-beat .cx-bleed{z-index:4;}

/* =======================================================================
   Scroll driver. The pin length lives in vh, never px, so the beats fire at
   the same scroll fractions on a 1440 monitor and a 2560 one.
   ======================================================================= */
.cx-track{height:300vh;}   /* 3 rosette beats; the Veda handoff is now
                              driven by the Veda section itself, not the pin */

/* =======================================================================
   MOBILE / REDUCED MOTION
   Below 900px the stage unpins, the four beats stack as static sections and
   all scroll-driven animation is off. Ben's brief: mobile must WORK, it does
   not need the choreography.
   ======================================================================= */
@media (max-width:900px){
  /* 112vw is wider than the screen by definition. The bloom sets the canvas
     size, so it was pushing a 406px canvas into a 390px viewport. Sized to
     the content column instead, with the vh term keeping it from dominating
     a short screen. */
  .cx-platform{--cx-bloom:min(84vw, 52vh);}

  .cx-track{display:none;}
  .cx-scroller{display:block;}

  .cx-stage{
    height:auto;
    min-height:0;
    display:block;
    padding:clamp(56px,9vh,90px) 0;
  }
  /* The radial composition is a desktop idea. On a phone the rosette stops
     being the subject and becomes the ground: it stays behind the beats at
     low opacity and sticks as they scroll past, and everything that hung off
     it — data tags, tool logos, journey badges, rays, leaks — is dropped
     rather than reflowed into a list that means nothing without the ring. */
  .cx-orbit{ display:none !important; }
  .cx-bloom{
    position:sticky;
    /* left/top and the centring translate come from the desktop rule, where
       this is absolutely positioned. Left in place they drag a sticky box
       half its own width off the left edge. */
    left:auto;
    transform:none;
    top:26vh;
    opacity:.17;
    margin:0 auto;
  }
  .cx-bloomfade,
  .cx-bloomfade{
    position:relative;
    left:auto; top:auto;
    transform:none;
    margin:0 auto;
  }
  /* The beats sit over the rosette, so they need to be above it. */
  .cx-beat{ z-index:1; }
  .cx-bloomfade{display:none;}

  .cx-beat{
    position:relative;
    inset:auto;
    opacity:1 !important;
    visibility:visible !important;
    display:block;
    padding:clamp(36px,7vh,64px) var(--cx-gutter);
    transform:none !important;
  }
  .cx-beat__copy{max-width:none;justify-items:start;text-align:left;}
  .cx-beat__copy .cx-h2,
  .cx-beat__copy .cx-h3,
  .cx-beat__copy .cx-body{text-align:left;}
  .cx-beat__copy .cx-body{margin-left:0;}

  /* Radial composition does not survive a 393px viewport. Orbits become a
     wrapped inline list; rays are dropped. */
  /* orbits are hidden above; nothing to reflow. */
  .cx-sat > *{transform:none !important;opacity:1 !important;}

  /* The badge -> tab flight is a desktop affordance. On mobile the journey
     badges are dropped entirely (the tab bar says the same five words) and the
     tab bar becomes a horizontally scrollable row. */
  .cx-orbit--journey{display:none !important;}
  /* no cross-boundary flight on mobile — there is no pin to hand off from */
  .cx-flight{display:none !important;}
  .cx-veda-section{min-height:0;padding:clamp(40px,7vh,80px) var(--cx-gutter);}
  .cx-veda{max-width:none;}
  .cx-tabs{
    width:100%;
    justify-content:flex-start;
    gap:.5em;
    overflow-x:auto;
    -webkit-overflow-scrolling:touch;
    scrollbar-width:none;
  }
  .cx-tabs::-webkit-scrollbar{display:none;}
  .cx-tab{opacity:1 !important;flex:0 0 auto;padding:.35em .7em;}
  .cx-panel{grid-template-columns:1fr;gap:clamp(14px,4vw,28px);}
  .cx-panel__viz{height:auto;width:100%;justify-self:stretch;}
  .cx-veda__h,.cx-veda__sub{text-align:left;max-width:none;}
  .cx-veda{justify-items:start;}
  .cx-rays{display:none;}

  /* ---- hero ------------------------------------------------------------
     The desktop hero is one absolutely-positioned composition sized in
     percentages of a 1440x703 frame: card, copy and both photos each carry
     their own rect. At 390px that frame collapses to a 420px floor, the card
     to 314px, and the copy — which is a SIBLING of the card, not a child —
     overflowed it and spilled onto the cream page background as grey text on
     cream. Unreadable, and the photos were landing on top of the headline.

     So on mobile the composition stops being a composition: the copy goes
     back into normal flow and sets the section's height, the card drops
     behind it at inset 0, and the two photos are dropped. The rosette inside
     the card still carries the visual weight. */
  .cx-hero{
    aspect-ratio:auto;
    min-height:0;
    height:auto;
  }
  .cx-hero__card{
    position:absolute;
    inset:0;
    left:0; top:0;
    width:auto; height:auto;
    border-radius:0;
  }
  .cx-hero__copy{
    position:relative;
    left:auto; top:auto;
    width:auto;
    padding:clamp(40px,11vw,72px) var(--cx-gutter) clamp(36px,10vw,64px);
  }
  .cx-hero__h,
  .cx-hero__lede{ max-width:none; }
  .cx-hero__eyebrow{ margin-bottom:.6em; }
  .cx-hero__lede{ margin-top:.9em; }
  .cx-hero__actions{
    margin-top:1.6em;
    flex-wrap:wrap;
    row-gap:.6em;
  }
  /* Placed for the 1440 frame; on a phone they land on the headline. */
  .cx-hero__eye,
  .cx-hero__woman{ display:none; }
  /* Ambient rotation is not worth a phone's battery. */
  .cx-hero__bloom{ animation:none; }

  /* ---- outcome leaves ---------------------------------------------------
     The leaves had NO mobile rules at all: the full pinned stack, with a
     card height derived from 100vh and display type sized in vh. At 844px
     tall that put the section heading at 52px on a 390px screen, where it
     wrapped to four lines and filled the viewport on its own.

     Same call as the beats — mobile must work, it does not need the
     choreography. The stack unpins and the three cards become three
     ordinary ones, sized by their own copy, and the type is sized off the
     WIDTH because that is the axis that is actually short here. */
  .cx-leaves__track{ height:auto; }
  .cx-leaves__stage{
    position:relative;
    top:auto;
    height:auto;
    overflow:visible;
  }
  /* inset:0 absolute on desktop, so it has no height of its own — left that
     way here the whole section collapsed and the block below painted through
     the cards. */
  .cx-leaves__inner{
    position:relative;
    inset:auto;
    transform:none !important;
  }
  .cx-leaves__head{
    height:auto;
    padding:clamp(44px,12vw,76px) var(--cx-gutter) clamp(20px,5.5vw,32px);
  }
  .cx-leaves__h{
    font-size:clamp(25px,6.6vw,36px);
    max-width:none;
    line-height:1.12;
  }
  /* ---- one left edge -----------------------------------------------------
     The module carries three different gutters by design: the module gutter,
     the title beat's wider 13.54vw (Figma puts its block at 195 of 1440), and
     the leaf copy's offset, which lines the copy up with a heading that sits
     OUTSIDE its card. At 1440 those read as considered. At 390 they read as
     three different left edges on the same page — 30, 52 and 53 measured.
     Below 900 they all collapse to --cx-gutter.

     The leaf card keeps a 10px inset so it still reads as a card on the
     aurora, and its body pads the remaining 20px, so its copy starts on the
     same 30px as everything else. */
  .cx-leaf{
    position:relative;
    left:auto; top:auto;
    width:calc(100% - 20px);
    height:auto;
    min-height:0;
    margin:0 auto clamp(18px,4.6vw,28px);
    transform:none !important;
    filter:none !important;
  }
  .cx-leaf:last-child{ margin-bottom:clamp(44px,11vw,72px); }
  .cx-leaf__body{
    height:auto;
    padding:clamp(28px,7vw,40px) 20px clamp(24px,6vw,34px);
    gap:clamp(14px,3.6vw,22px);
  }
  .cx-leaf__h{
    font-size:clamp(27px,7.4vw,40px);
    max-width:none;
  }
  /* ---- legibility over the photograph -----------------------------------
     On desktop the copy is capped at 42%/66% and sits in the clear part of the
     card; the photo bleeds in from the right, behind nothing. On mobile the
     copy spans the full card, so it lies directly ON the photograph — and
     "Deliver smarter" sets cream type over a pale image.

     Measured with tools/probe-leaf-contrast.mjs, sampling the ground behind
     each text block in 8px tiles (a mean hides a bright patch under one word):

                        worst-case ratio    needs
       Win bigger            6.3 - 7.0       4.5   ok
       Deliver smarter       1.15 - 1.37     4.5   unreadable
       Grow stronger         3.2 - 3.9       4.5   fails

     Deliver's existing radial wash is the desktop composition's — it fades out
     at 78% from the top-left of a 1327px card, so most of a tall phone card is
     unwashed. Grow has no wash at all and fails the other way: its dark type
     lands on the photo's dark patches.

     So each gets a scrim in the direction its own text needs, mobile only. The
     gradient is stronger at the top, where the heading and lede are, and eases
     toward the stats — which keeps some photograph visible instead of flattening
     the card to a colour field. */
  /* The scrim hangs off the CARD, not off .cx-leaf__bg — that layer is held at
     opacity .34 a few rules down (the photo is a wash on mobile), and opacity
     applies to a pseudo-element the same as to any child, so a scrim inside it
     had 34% of the alpha it asked for. An .86 black measured as a mid-blue and
     the ratio moved from 1.15 to 1.98 instead of clearing 4.5.

     ::after, not ::before: it has to paint ABOVE .cx-leaf__bg (z-index 0) and
     below .cx-leaf__body (z-index 1). A pseudo-element at the same z-index as
     a real sibling is ordered after it, and ::before would land under it. */
  .cx-leaf::after{
    content:"";
    position:absolute; inset:0;
    z-index:0;
    pointer-events:none;
  }
  .cx-leaf--win::after{ background:none; }
  .cx-leaf--deliver::after{
    background:linear-gradient(rgba(13,17,23,.80) 0%, rgba(13,17,23,.72) 62%, rgba(13,17,23,.52) 100%);
  }
  .cx-leaf--grow::after{
    background:linear-gradient(rgba(252,252,247,.62) 0%, rgba(252,252,247,.54) 62%, rgba(252,252,247,.36) 100%);
  }
  /* Deliver's desktop radial is the pinned composition's — it fades out at 78%
     from the top-left of a 1327px card, so on a phone it darkens a corner and
     leaves the rest. The scrim replaces it rather than compounding with it. */
  .cx-leaf--deliver .cx-leaf__wash{ display:none; }
  /* The 66%/42% caps hold the copy clear of the photo bleeding in from the
     right. Here the photo is a wash behind the whole card, so the copy can
     have the full width instead of wrapping every three words. */
  /* The 42%/66% caps hold the copy clear of a photo bleeding in from the
     right. Here the photo is a wash behind the whole card, so they only
     wrapped the lede at two words a line. The rule they come from names
     BOTH the lede and the list; clearing only the list left the lede at
     120px of the 285 it had. */
  .cx-leaf__lede,
  .cx-leaf__list{ max-width:none; }
  .cx-leaf__lede{ font-size:clamp(15px,4.2vw,19px); }
  /* The photo is a right-hand bleed on desktop; at this width it would sit
     under the copy, so it becomes a wash behind it instead. */
  .cx-leaf__bg,
  .cx-leaf__bg2{ opacity:.34; }
  .cx-leaf__foot{
    flex-direction:column;
    align-items:flex-start;
    gap:clamp(14px,3.6vw,20px);
  }
  .cx-leaf__stats{ width:100%; }
  .cx-bleed{display:none;}
  .cx-rects{display:none;}

  /* the bloom box is sized for the final rosette; on mobile the earlier states
     leave a lot of dead air, so tighten the box and let it clip */
  /* 104vw is wider than the screen by definition — this was pushing a 406px
     rosette (and canvas) into a 390px viewport. The stage clips rather than
     scrolls, so it read as "the rosette is cut off" instead of as overflow. */
  .cx-bloom{
    width:min(84vw, 52vh);
    height:min(84vw, 52vh);
  }

  .cx-pillars{grid-template-columns:1fr;gap:clamp(28px,5vh,44px);}
  .cx-title{
    text-align:left;
    justify-items:start;
    padding-top:clamp(56px,9vh,90px);
    /* Figma's wider title gutter is a 1440 idea; here it is just a third
       left edge. */
    padding-left:var(--cx-gutter);
    padding-right:var(--cx-gutter);
  }
  .cx-title__h,.cx-title__sub{text-align:left;}
}

@media (prefers-reduced-motion:reduce){
  .cx-platform *{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
  }
  .cx-bloom{animation:none !important;}
}
