* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/*
  No overflow-x: hidden on html/body — that alone triggers a Chromium
  repaint bug that freezes the SVG pattern once script.js starts
  mutating it on scroll (found the hard way in test animation/). The
  oversized rotated pattern square is clipped by .hero-pattern-wrap
  instead, which is enough to prevent any horizontal scrollbar.
*/
/* Plain continuous scrolling — no scroll-snap and no JS section scroller (both
   off). The wheel / trackpad / arrows scroll the page normally; the scroll-linked
   effects (fold-1 tail fade, back-layer dim + fold wash, nav morph, fold
   entrances) track the scroll position as you go. (To bring back the one-swipe
   carousel: set scroll-snap-type: y proximity here AND re-enable
   initSectionScroller() in script.js.)

   Note: scroll-snap-type stays `none` regardless — `y proximity` is a root-scroller
   compositing prop in the same family as the banned overflow-x (see top of file)
   and starves the pattern layer's GPU raster headroom. */
html {
  scroll-snap-type: none;
}

/* Lenis smooth scroll (vendor/lenis) — recommended base rules. NB: deliberately NOT
   including Lenis's `.lenis.lenis-stopped { overflow: hidden }` — we never call stop(),
   and any html/body overflow re-triggers the banned repaint bug (see top of file). */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }

html, body {
  width: 100%;
  background: var(--page-bg);
  font-family: var(--font-ui);
  color: var(--ink);
}

/* Shared content inset — the Figma content column (8.33% + 8px each side), the
   same gutter the nav, fold-3, fold-2, layer 3, the contact page and the footer all align
   to. Declared ONCE here so the mobile override at the bottom of this file (a
   single :root rule in the <=620px block) reaches every fold; it used to be
   re-declared on five separate fold selectors, where a :root override could
   never win. On a phone 13vw is only ~50px of usable gutter loss, so it drops
   to a flat 20px there. */
/* THE PAGE COLOUR. Every opaque fold, the footer, the CTA band and the glow
   text-shadows all sit on this one value, so it can be re-graded in one place
   (it went #1F1F1F -> #070609 when the grain overlay landed — a near-black
   ground gives the screen-blended grain room to read). */
:root {
  --page-bg: #070609;
  --fold-inset: calc(13vw + 8px);
  /* Nav bar height. A token rather than a literal on .site-nav because the
     contact page pads its first fold by it (the fixed bar overlays the top of
     the page), and the mobile block overrides it to 60px. The two must not
     drift apart. */
  --nav-h: 70px;
  /* The two faces of the white CTAs that respond on hover — .nav-cta,
     .cta-button (fold 5). One pair of tokens so they can't
     drift. The HERO's .btn-solid is deliberately NOT one of them: it is pure
     white always, and answers a hover by growing + firing the pattern tint.
     The button BRIGHTENS on hover: it rests a step below white and lights up to
     pure white under the cursor. (Not the reverse — darkening a white CTA reads
     as it disabling itself.) Pure white is therefore the HOVER face, which means
     the resting face can't also be #fff; keep --white-cta-rest close enough to
     white that the button still reads as a white pill at rest. */
  --white-cta-rest:  #f0f0f0;
  --white-cta-hover: #ffffff;
  /* How far fold-2's weave box overhangs its sticky above and below. These are EXTRA
     pattern strips, not a crop: the vertical mask fade is exactly this tall, so each
     overhang is permanently faded and bleeds out of fold-2 into fold-3 above and
     the operator fold below — the pattern dissolves into the background instead of ending on a
     hard edge. Bigger = softer, further-reaching blend + a taller SVG grid to build
     (more vectors), so keep it modest. */
  --int-weave-overhang: 150px;
  /* How far the fold-2 weave's serrated left ending sits from the box's own left
     edge, in px. Lives here rather than in script.js because it must stay IN
     REGISTER with the left feather below (--int-weave-feather) — both are px from
     the same edge, so widening the box moves them together.
     NOTE the two current values are deliberately lopsided: the teeth sit at 10px
     inside a 460px ramp, i.e. at ~2% alpha, so the whole-block serration is computed
     but NOT legible — the left termination reads as a pure gradient dissolve. That's
     the intended look here. To make the serration visible again, raise this toward
     (or past) the feather length, or shorten the feather. */
  --int-weave-edge-inset: 10px;
  /* Length of the weave's LEFT feather — the ramp from fully transparent at the box's
     left edge to fully opaque. Bigger = more gradual dissolve into the copy column
     (and more of the serrated ending swallowed by the fade); smaller = the pattern
     arrives sooner and the serration reads more crisply. */
  --int-weave-feather: 460px;

  /* ==================================================================
     DESIGN TOKENS
     One name per ROLE, not per value. Before these existed the same job
     was done by slightly different literals depending on which fold was
     built when (#e0dfdf vs #cfcfcf vs #d5d5d5 for body copy; four
     separate clamp ramps for one heading level; 21 letter-spacings for
     ~8 roles). Each token below notes what it ABSORBED so the drift
     doesn't get re-introduced by copy-paste.

     NOT tokenized on purpose: page/fold GROUND colours and card FILLS
     (--page-bg, #1F1F1F, #0A0A0A, #17171a, rgba(7,6,9,…)), the fold-3
     chapter art palette, the operator role accents (#5d6de5 / #e55d5d,
     already scoped through --role-accent), and everything in script.js
     (the pattern greys and hover hues are read per-frame across ~4k
     strokes — routing them through getComputedStyle buys nothing).
     ================================================================== */

  /* --- ink: solid text colours ------------------------------------- */
  --ink:          #ffffff;  /* headings, lead copy, active/hover states */
  --ink-body:     #e0dfdf;  /* body copy, subtitles, table cells. Absorbed #cfcfcf */
  --ink-action:   #e4e4e4;  /* RESTING interactive text — nav links, operator link.
                               Brightens to var(--ink) on hover. */
  --ink-quiet:    #b9b8b8;  /* hero reassure line */
  --ink-muted:    #8f8f8f;  /* eyebrows, reassure star. Absorbed #8f8e8e */
  --ink-on-light: #1a1a1a;  /* text sitting ON the white CTAs */

  /* --- ink: white-on-dark alpha ladder ------------------------------
     Was twelve steps (.82 .8 .7 .64 .62 .6 .55 .5 .45 .42 .4 .28) doing
     four jobs. Now four. */
  --ink-a80: rgba(255, 255, 255, 0.82); /* form labels + input text. Absorbed .80 */
  --ink-a60: rgba(255, 255, 255, 0.60); /* secondary paragraphs. Absorbed .64 .62 .55 .50 */
  --ink-a45: rgba(255, 255, 255, 0.45); /* fine print, column labels. Absorbed .40 */
  --ink-a28: rgba(255, 255, 255, 0.28); /* inactive dock buttons */

  /* --- rules, borders, hairlines ------------------------------------ */
  --rule-ghost:      rgba(255, 255, 255, 0.42); /* ghost-button outlines (.nav-demo, .operator-link) */
  --rule-soft:       rgba(255, 255, 255, 0.12); /* card + row hairlines. Absorbed .15 .13 .10 */
  --rule-faint:      rgba(255, 255, 255, 0.08); /* Absorbed .07 */
  --rule-grey:       rgba(87, 87, 87, 0.55);    /* structural page rules. Absorbed rgba(79,79,79,.68) */
  --rule-grey-faint: rgba(87, 87, 87, 0.35);
  --rule-solid:      #575757;                    /* .nav-cta border, pricing table head underline */
  --rule-role:       rgba(142, 142, 142, 0.57);  /* .operator-role card at rest */

  /* --- radii --------------------------------------------------------
     The bare 2px and 7px elsewhere are decorative, not container radii,
     and stay literal. */
  --r-sm:   4px;
  --r-md:   8px;
  --r-lg:  12px;   /* absorbed a stray 14px on three cards */
  --r-pill: 999px;

  /* --- elevation + button hover -------------------------------------
     --shadow-btn was copy-pasted five times in two whitespace formats.
     --btn-hover-scale is the lift shared by .btn-solid and .cta-button.
     The hover COLOUR pair lives above (--white-cta-rest/hover). */
  --shadow-btn:     0 2px 6px rgba(0, 0, 0, 0.28), 0 6px 16px rgba(0, 0, 0, 0.22);
  --shadow-nav-cta: 0 4px 7px 0 #000;
  --btn-hover-scale: 1.08;
  --t-fast: 0.25s;
  --t-base: 0.35s;
  --ease:   cubic-bezier(0.4, 0, 0.2, 1);

  /* --- type: families ----------------------------------------------- */
  --font-ui:      'Manrope', sans-serif;
  --font-display: 'Roboto Serif', serif;

  /* --- type: size ---------------------------------------------------
     The clamps carry their own mobile floor, so most phone-width sizing
     comes along for free. Fold-3's chapter ramps and .cta-headline are
     deliberately NOT here — they're sculpted against viewport HEIGHT for
     a pinned fold, not part of the document type scale. */
  --fs-display: clamp(40px, 5.4vw, 72px);   /* hero headline */
  --fs-h1:      clamp(26px, 3vw, 42px);     /* page headline */
  --fs-h2:      clamp(24px, 2.6vw, 40px);   /* section heading. Absorbed 26/3vw/39 and 26/2.6vw/36 */
  --fs-h3:      clamp(20px, 1.8vw, 24px);
  --fs-title:   clamp(16px, 1.35vw, 18px);
  --fs-lead:    clamp(15px, 1.35vw, 18px);
  --fs-body:    clamp(14px, 1.15vw, 16px);
  --fs-eyebrow: clamp(12px, 0.85vw, 13px);  /* uppercase kickers only */
  /* Small CARD BODY — the agent-card blurb. Kept apart from --fs-eyebrow on
     purpose: it's running text, not a kicker, and the eyebrow ramp reads too
     small for it (13.7px -> 12.2px at 1440). */
  --fs-card:    clamp(13px, 1.05vw, 15px);
  --fs-lg:      18px;   /* large UI text: .btn, table name col, mobile nav link */
  --fs-xl:      20px;   /* hero subcopy, footer wordmark (was 19), table group (was 21) */
  --fs-ui:      16px;  /* nav links, buttons, submit */
  --fs-body-s:  15px;
  --fs-meta:    14px;
  --fs-small:   13px;
  --fs-xs:      12px;

  /* --- type: weight (already a clean 4-step; just naming it) --------- */
  --fw-light:    300;
  --fw-regular:  400;
  --fw-medium:   500;
  --fw-semibold: 600;

  /* --- type: tracking -----------------------------------------------
     21 values -> 8. The hero's -0.792px stays literal: that's
     display-size optical tracking, not a UI step. */
  --ls-eyebrow:   1.6px;   /* Absorbed 1.62 1.8 1.92 3.5 */
  --ls-wide:      1.2px;   /* Absorbed 1.1 1.3 1.4 */
  --ls-wordmark:  1.32px;  /* nav + footer wordmark */
  --ls-body-wide: 0.96px;
  --ls-body:      0.6px;   /* Absorbed 0.7 */
  --ls-meta:      0.4px;   /* Absorbed 0.2 0.3 */
  --ls-tight:    -0.2px;   /* Absorbed -0.176 -0.198 -0.242 */
  --ls-tight-lg: -0.6px;   /* Absorbed -0.3 */

  /* --- type: leading ------------------------------------------------- */
  --lh-tight: 1.2;   /* Absorbed 1.15 1.16 1.18 1.22 1.24 1.25 */
  --lh-snug:  1.3;
  --lh-cozy:    1.4;    /* card/eyebrow leading. Absorbed 1.36, 1.39 */
  --lh-body:  1.5;   /* Absorbed 1.45 1.55 */
  --lh-loose: 1.65;
}

a {
  color: inherit;
}

/* --- site nav (persistent, floats above both the hero and the
       Compliance Hub fold — not duplicated per section) --- */

/* Fixed 70px flex bar (Figma "Last Iteration" 103:7541): the NeighPalm wordmark
   on the left, Log In + "Try For Free" on the right, all always visible. The
   brand is TEXT ONLY here — the palm mark and the centred hero lockup it used to
   hand off from were both removed. */
.site-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  height: var(--nav-h);   /* token — see :root */
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Deliberately decoupled from the fold gutter (--fold-inset): the bar's
     content hugs the viewport edges instead of aligning with the folds. */
  padding: 0 28px;
  /* Bar chrome (Figma 103:7541): faint translucent fill and a hairline bottom
     border so the bar reads as a discrete surface over the hero pattern.

     DELIBERATELY NO backdrop-filter here. It used to carry blur(0.5px) — visually
     nothing — but any element with a backdrop-filter becomes a BACKDROP ROOT: its
     descendants can only sample what is painted INSIDE it, not what is behind it.
     This bar's interior is a 1%-alpha fill, i.e. nothing, so that 0.5px silently
     cancelled .nav-demo:hover's blur(8px) — the pill frosted empty space and the
     lattice behind it stayed sharp. Adding any blur back here re-breaks that.
     (The mobile block DOES set one, via the --nav-blur scroll ramp — safe only
     because .nav-actions, and so .nav-demo, is display:none at that breakpoint.) */
  background: rgba(3, 3, 3, 0.01);
  border-bottom: 1px solid var(--rule-grey);
}

/* The scroll-ramped blur lives on a SEPARATE pseudo-layer, not on .site-nav
   itself — that's what keeps the frost off the backdrop-root path. The element
   that carries backdrop-filter becomes a backdrop root, and .nav-demo is NOT a
   descendant of this ::before (it's a sibling in .nav-actions), so the pill's
   own :hover blur(8px) still samples the page behind the bar. --nav-blur is the
   0->1 ramp scrubbed by initNavHandoff (script.js) once the hero clears the top,
   so the bar frosts in GRADUALLY to 1.5px rather than snapping on.
   Knobs: the 0.5px baseline and the 1px ramp below. */
.site-nav::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* 0.5px baseline over fold-1, ramping to 1.5px once fold-3 arrives
     (--nav-blur is the 0->1 scroll scrub from initNavHandoff). Tuned by eye —
     just enough to soften the lattice under the bar without frosting it. */
  backdrop-filter: blur(calc(0.5px + var(--nav-blur, 0) * 1px));
  -webkit-backdrop-filter: blur(calc(0.5px + var(--nav-blur, 0) * 1px));
}

/* Pricing page has no hero scroll ramp, so pin the ramp at full — the bar sits
   at a steady 1.5px blur from the first frame. */
.page-pricing .site-nav { --nav-blur: 1; }

/* …until the comparison table reaches it. The sticky table header parks right
   under the bar, and a translucent bar over a scrolling table reads as a smear —
   so from there down the nav goes fully opaque and drops the frost entirely
   (the blur layer is pointless once nothing shows through). Toggled by the
   IntersectionObserver at the bottom of pricing.html. */
.page-pricing .site-nav.is-solid { background: var(--page-bg); }
.page-pricing .site-nav.is-solid::before { backdrop-filter: none; -webkit-backdrop-filter: none; }

/* Left group: just the wordmark now (the palm mark that sat beside it was
   removed). Kept as a flex wrapper so the bar's space-between still has a
   single left-hand child. */
.nav-lockup {
  display: flex;
  align-items: center;
}

/* Bar wordmark — the ONLY brand mark on the page's chrome, and always visible.
   It used to be hidden on the hero and cross-fade in at fold-3, handing over
   from a centred hero lockup; both that lockup and the handoff are gone. */
.nav-wordmark {
  font-family: var(--font-display);
  font-weight: var(--fw-light);   /* matched to .footer-wordmark */
  font-size: var(--fs-ui);
  font-variation-settings: 'opsz' 8, 'wdth' 100, 'GRAD' 0;
  letter-spacing: var(--ls-wordmark);
  white-space: nowrap;
}

/* Right-side actions: in-flow flex item on the right of the bar, always
   visible (Figma 103:7544). */
.nav-actions {
  display: flex;
  align-items: center;
  gap: clamp(15px, 1.26vw, 24px);
}

/* Mobile-only hamburger — desktop never shows it. All of its real styling
   lives in the ≤620px block at the bottom of this file. */
.nav-burger { display: none; }

/* "Log In" (Figma 103:7546): Manrope Regular 18px, light grey. */
.nav-link {
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-ui);
  letter-spacing: var(--ls-tight);
  color: var(--ink-action);
  text-decoration: none;
  white-space: nowrap;
}

/* "Try For Free" button (Figma 103:7547/7548): grey #dedede fill, #575757
   hairline border, 8px radius, hard drop shadow; Manrope Regular 16px near-black
   text. Its own class (not .btn) because the hero CTA differs. */
.nav-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
  font-weight: var(--fw-medium);
  font-size: var(--fs-ui);
  /* Vertical trim (Figma text-box-trim: trim-both) — crop the line box to the
     glyph cap/baseline so the 12px padding hugs the letters, matching Figma.
     line-height:1 is the cross-browser fallback where text-box-* is unsupported. */
  line-height: 1;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
  letter-spacing: var(--ls-tight);
  color: var(--ink-on-light);
  text-decoration: none;
  white-space: nowrap;
  padding: 12px 19px;
  background: var(--white-cta-rest);
  border: 1px solid var(--rule-solid);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-nav-cta);
  /* Hover: the face just darkens a step — no pop, and NO transition. The nav is
     persistent and sits over the whole page, so a button that grows up there reads
     as a bigger event than it is; the hero's .btn-solid keeps the grow, this one
     answers quietly. Deliberately instant: an eased darken on a small persistent
     control reads as lag, not polish — the state flips on the frame the cursor
     lands. background-color only, so the nav bar never reflows. */
}
.nav-cta:hover {
  /* --white-cta-hover, shared by every white CTA on the site
     (.cta-button, and the contact page's submit). A real, readable grey step — full-white to
     full-white leaves the control feeling dead under the cursor. */
  background: var(--white-cta-hover);
}

/* "Get a demo" — the bar's SECONDARY button. Same geometry as .nav-cta (identical
   type, padding, radius and the 1px border) so the pair sits at exactly the same
   height and the bar reads as one row; only the FILL differs. Outline rather than
   solid because two white pills side by side compete and neither wins.
   No box-shadow: the drop shadow is what makes .nav-cta read as the raised,
   primary control, so lifting it here would flatten the hierarchy again. */
.nav-demo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
  font-weight: var(--fw-medium);
  font-size: var(--fs-ui);
  line-height: 1;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
  letter-spacing: var(--ls-tight);
  /* Label colour matches "Plans" / "Log In" beside it (.nav-link): rests at
     #e4e4e4, brightens to pure white on hover. It used to rest at full white,
     which made the secondary pill read louder than the two plain links. */
  color: var(--ink-action);
  text-decoration: none;
  white-space: nowrap;
  padding: 12px 19px;
  background: transparent;
  border: 1px solid var(--rule-ghost);
  border-radius: var(--r-md);
  /* Frost at REST, not just on hover: the pill blurs whatever is behind it (the
     lattice pattern) so the label always sits on a soft plate. The blur rides the
     SAME --nav-blur 0->1 scroll ramp as .site-nav::before (inherited from
     .site-nav, scrubbed by initNavHandoff) — 1px over fold-1, 2px once the ramp is
     full — so the pill frosts in step with the bar. saturate(1.05) keeps the
     blurred pattern from going grey.
     Over the HERO the bar's own blur is 0, so the pill was at its floor exactly
     where it sits on the busiest lattice. The base is raised to 2px and the ramp
     flattened to 0.5px, so fold-1 reads frostier while the scrolled value lands
     about where it did. */
  backdrop-filter: blur(calc(2px + var(--nav-blur, 0) * 0.5px)) saturate(1.05);
  -webkit-backdrop-filter: blur(calc(2px + var(--nav-blur, 0) * 0.5px)) saturate(1.05);
}
/* Hover: the outline firms up to full white and a faint wash appears behind the
   label — it BRIGHTENS, like every other control on the page. Instant and
   colour-only, matching .nav-cta and .nav-link beside it: all three nav controls
   answer on the frame the cursor lands, so the bar reads as one timing. The frost
   is NOT part of hover — it lives on .nav-demo itself (above) at the same values in
   both states, so hovering changes only the outline and the wash. */
.nav-demo:hover {
  color: var(--ink);   /* same #e4e4e4 -> #fff step as .nav-link:hover */
  border-color: var(--ink);
  background: rgba(255, 255, 255, 0.10);
  /* A touch more frost under the cursor — same expression as the rest state
     with +2px, so it still rides the --nav-blur scroll ramp. */
  backdrop-filter: blur(calc(4px + var(--nav-blur, 0) * 0.5px)) saturate(1.05);
  -webkit-backdrop-filter: blur(calc(4px + var(--nav-blur, 0) * 0.5px)) saturate(1.05);
}

/* "Log In" is the quietest control on the page, so it gets the quietest hover:
   no pop, no underline — the label just BRIGHTENS a step, #e4e4e4 -> pure white.
   It lights up rather than dimming, matching .nav-cta beside it (which also rests
   a step below white and goes to --white-cta-hover): dimming a control under the
   cursor reads as it disabling itself. Colour-only and instant, like .nav-cta —
   both nav controls answer on the frame the cursor lands, so the pair reads as
   one bar rather than two timings. */
.nav-link:hover {
  color: var(--ink);
}

/* Respect reduced-motion: snap between nav states, no slide. */
@media (prefers-reduced-motion: reduce) {
  .nav-actions {
    transition: none;
  }

  /* CTA hover: no sweeping front, no easing. The reduced-motion branch of
     initHeroInversion writes the hover colour to --hover-tint on .scene-compliance
     and toggles .is-inverted, so the whole pattern takes the new colour at once.
     Same "different colour every hover" palette — just no traveling edge. */
  .btn-solid,
  .nav-cta,
  .nav-link {
    transition: none;             /* snap between hover states, no easing */
  }
  .hero-pattern-back {
    transition: none;             /* snap the back echo's tint, don't crossfade it */
  }
  .scene-compliance.is-inverted .hero-pattern-svg path,
  .scene-compliance.is-inverted .hero-pattern-svg line,
  .scene-compliance.is-inverted .hero-pattern-svg rect {
    stroke: var(--hover-tint, #6ea8ff);
  }
  /* Same deal for fold-5: no traveling front, the whole weave takes the colour at
     once (reduced-motion branch of initCtaInversion). */
  .cta-button { transition: none; animation: none; }   /* hold the glow static, don't pulse */
  .fold-cta.is-inverted .cta-weave-cells path {
    stroke: var(--hover-tint, #6ea8ff);
  }
  /* The per-tile radial glow rects are fills, not strokes — leave them alone. */
}

/* --- buttons --- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  /* Figma is 22px / 21×34px padding on a 1512px frame; scaled down here to
     read correctly at typical viewport widths. */
  font-size: var(--fs-lg);
  letter-spacing: var(--ls-tight);
  text-decoration: none;
  white-space: nowrap;
  padding: 12px 24px;
  border-radius: var(--r-md);
  /* Regular drop shadow (Figma 103:7535 CTA row) — a tight elevation, not the
     old ambient halo. */
  box-shadow: var(--shadow-btn);
}

/* Primary "Try For Free" (Figma node 75:4014): solid white, Roboto Regular. */
.btn-solid {
  position: relative;               /* anchors the two shadow layers below */
  /* Wider than the base .btn (24px) — it's the hero's only button now, so it
     carries more presence. Height is untouched. */
  padding-left: 40px;
  padding-right: 40px;
  /* PURE WHITE, ALWAYS — deliberately opted OUT of the --white-cta-rest /
     --white-cta-hover pair the other CTAs share. This is the hero's primary
     button and the origin of the pattern-tint sweep; any face change of its own
     competes with that. It has no background hover state at all. */
  background: #fff;
  /* Invisible 1px border for height parity with the other pills: auto-height
     means height = text + padding + BORDER, and box-sizing:border-box does NOT
     absorb it (that only applies when an explicit height is set). */
  border: 1px solid transparent;
  color: var(--ink-on-light);
  font-family: var(--font-ui);
  font-weight: var(--fw-medium);
  /* The dark halo does NOT live on this element. Interpolating the resting
     3-layer, 60px-blur halo down to the hover state's 1-layer shadow repainted
     that whole blurred box every frame of the grow — the ingoing stutter (worst
     right at the start, where the big halo is). Snapping it killed the stutter
     but made the halo pop. Instead the two shadows are pre-painted onto the
     ::before (resting) and ::after (hover) pseudo-elements and CROSSFADED by
     opacity: opacity is a compositor property, so each shadow is rastered once
     and the GPU just blends the layers — smooth fade, no per-frame blur repaint. */
  box-shadow: none;
  /* Hover: the pill only GROWS — it stays white throughout. The colour event is
     the SVG pattern taking a new tint, swept out of this button from JS
     (initHeroInversion), a different colour on every hover. */
  /* 0.28s, deliberately SHORTER than the pattern sweep (D = 0.9s in
     initHeroInversion). The pill snaps to size while the colour front is still
     crossing the field, which reads as the button firing the sweep off rather than
     travelling with it. Changing this does NOT change the sweep speed — the two are
     independent; keep the sweep at D. */
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
  /* Own compositor layer so the transform/bg change repaints only the button, not
     the pattern strokes it overlaps. */
  will-change: transform;
}

/* Two static shadow layers behind the pill, crossfaded on hover (see note above). */
.btn-solid::before,
.btn-solid::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  transition: opacity 0.45s ease;
  will-change: opacity;
}
.btn-solid::before {                /* resting — regular drop shadow */
  box-shadow: var(--shadow-btn);
  opacity: 1;
}
.btn-solid::after {                 /* hover — same tight elevation */
  box-shadow: var(--shadow-btn);
  opacity: 0;
}

/* WHITE ATTENTION GLOW — its own element (.btn-glow, a span inside the anchor)
   rather than a third pseudo-element, because ::before/::after are both already
   spoken for by the crossfaded elevation above.

   It is a SEPARATE layer specifically so the glow can breathe while the PILL
   stays perfectly still: the animation scales this span only, so the button's
   own face, text and layout box never move. The white halo is a static
   box-shadow rastered exactly once — the animation only scales and fades that
   finished raster, both compositor properties, so the ~4k-vector pattern behind
   the button never repaints.

   z-index:-1 puts it behind the pill's white background (.btn-solid is
   position:relative, so it forms the stacking context that traps it there) —
   only the part of the halo that spills past the pill's edge is ever visible. */
.btn-glow {
  position: absolute;
  inset: 0;                         /* top/right/bottom/left all 0 — matches the pill exactly */
  z-index: -1;
  border-radius: inherit;           /* same pill rounding, so the halo is pill-shaped */
  pointer-events: none;             /* never eats a click meant for the button */
  box-shadow: 0 0 22px rgba(255, 255, 255, 0.45),
              0 0 55px rgba(255, 255, 255, 0.22);
}


/* Hover: the pill only GROWS — it stays pure white throughout. The colour event
   is the SVG pattern tinting behind it (initHeroInversion); the button is the
   origin of that sweep, not a participant in it. transform:scale so the layout
   box (and the CTA row) never reflows. */
.btn-solid:hover {
  transform: scale(1.08);
}

/* Delay the shadow crossfade until the growing white circle has finished (~D=0.9s
   in JS): the halo change resolves AFTER the sweep, not on the same frames — so
   nothing heavy paints during the compositor-only grow. Delay is on the hover-IN
   rules only; hover-OUT (base ::before/::after, no delay) reverts immediately. */
.btn-solid:hover::before { opacity: 0; transition: opacity 0.35s ease 0.85s; }
.btn-solid:hover::after  { opacity: 1; transition: opacity 0.35s ease 0.85s; }

/* Keyboard focus ring for the nav links and hero CTAs. Mouse clicks don't
   trigger :focus-visible, so this only shows for keyboard/AT users. A white
   ring on the near-black ground stays visible whether it lands on the pattern
   or a light pill. (The carousel segments live inside aria-hidden and aren't
   keyboard-reachable, so they're deliberately excluded.) */
.nav-link:focus-visible,
.nav-cta:focus-visible,
.nav-demo:focus-visible,
.btn:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

/* --- scene-compliance: fuses the hero + Compliance Hub fold into one
       continuous scroll region so the background pattern can be a single
       persistent element spanning both (see .hero-pattern-wrap below) --- */

.scene-compliance {
  position: relative;
  background-color: var(--page-bg);
  /* Extra scroll length added to the BOTTOM of fold-1 (the hero). The front
     pattern extends down through it and fades to nothing before fold-3, so the
     hero doesn't end on a hard horizontal cut as it scrolls away — the pattern
     trails off. This tail belongs to fold-1: it's fully faded by the fold-3
     boundary, so none of it shows on fold-3. Tune here. */
  --fold1-tail: 15vh;
}

/* --- background pattern ---

  Sticky (not absolute), and a direct child of .scene-compliance rather than
  scoped inside .hero — it persists as ONE element across the hero -> static
  logo reel -> fold-2 (Integrations) scroll so it can stay put behind the reel
  and then morph (zoom + erode from the left) into the fold-2 weave, rather
  than being separate pattern instances with a seam between them.

  margin-bottom: -100vh cancels the 100vh of flow space this element would
  otherwise reserve (the standard "sticky background spans several stacked
  sections" trick) — the hero, reel, and fold-2 stack normally right after it
  instead of being pushed down by an extra viewport.
*/


.hero-pattern-wrap {
  /* Sticky child of .scene-compliance (not absolute inside .hero) so the pattern
     stays pinned to the viewport across the hero -> reel -> fold-2 scroll while
     it morphs into the woven fold-2 state. height:100vh + margin-bottom:-100vh is
     the standard "sticky background spanning stacked sections" trick: it pins one
     viewport tall but reserves zero flow height, so the hero, reel, and fold-2
     stack normally right after it. z-index 0 keeps it behind their content
     (z-index 1). */
  position: sticky;
  top: 0;
  height: 100vh;
  margin-bottom: -100vh;
  overflow: hidden;
  z-index: 0;
}

/* (The fold-3 bottom wash that used to live here — .hero-pattern-wrap::after,
   driven by --fold-fade — was removed with the reorder: fold-3 no longer follows
   the hero, and the pattern must persist UNWASHED behind the static logo reel.) */

/*
  No transform here on purpose — script.js sets the centered state via
  gsap.set(), and GSAP then OWNS this element's transform. One exception
  since the unification: GSAP animates `scale` on this element for the
  hero->fold-2 morph (playFoldMorph / resetFoldMorph / applyUnifiedStatic,
  1.0 -> WEAVE_ZOOM). Aside from that scale it doesn't move; undrawing lines
  is the only other animation. Because GSAP owns the whole transform once it
  sets any sub-property, keep ALL CSS transform off this element — a CSS
  transform here would silently fight the morph's GSAP scale.

  No width/height here either — script.js computes the minimal
  axis-aligned square that still fully covers the hero (side = max
  (heroWidth, heroHeight)) and sets it inline. A blanket 200vmax covers
  any possible viewport but tiles roughly 3x more invisible (clipped)
  area than necessary.
*/
.hero-pattern-rotate {
  position: absolute;
  top: 50%;
  left: 50%;
}

/* Front lattice wrapper — NON-sticky (unlike .hero-pattern-wrap). Absolute at
   the top of .scene-compliance so it scrolls up and off WITH the hero rather
   than staying pinned in the viewport. That's the whole fix: a sticky, GPU-
   promoted live SVG gets re-rasterised every frame while it's re-pinned during
   scroll (the flicker); a non-sticky one is just a cached layer the compositor
   slides away. overflow:hidden clips the oversized -45° field to the viewport,
   same as .hero-pattern-wrap does for the back. z-index 0 (below the hero
   content at z-index 1); it sits above the back wrap by DOM order. */
.hero-front-wrap {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  /* Covers the hero viewport PLUS the tail, so the front pattern continues below
     the fold. It's darkened to nothing in the tail by the hero vignette (which
     now extends over the tail) — one continuous fade, not a separate mask. */
  height: calc(100vh + var(--fold1-tail));
  overflow: hidden;
  z-index: 0;
}

/* Vestigial hook from the old light-theme inversion (a white ground disc that
   grew out of the CTA). The hover now only TINTS the pattern strokes — there is
   no white ground in either the animated or the reduced-motion path — so this
   element paints nothing. Kept hidden rather than removed because index.html
   still ships the node. */
.hero-invert-circle {
  position: absolute;
  pointer-events: none;
  visibility: hidden;
}

/* Same oversized square as .hero-pattern-rotate (script.js sizes it via
   gsap.set). Centered on the VIEWPORT (top: 50vh), not on the taller wrapper's
   own middle, so at rest the field sits exactly where the back layer does and
   the tail simply extends below. */
.hero-front-rotate {
  position: absolute;
  top: 50vh;
  left: 50%;
}

/* #4 depth layer: the far parallax plane — a SINGLE CSS-tiled <div> (not a second
   vector grid, which was ~15k paths and dropped the page to ~13fps at retina).
   Its background is a generated seamless weave tile (backWeaveTileUri) so it's the
   exact interleaved pattern, just smaller/darker/thinner. Sits behind the main SVG
   (z-index 0 vs 1) inside .hero-pattern-rotate. Unlike the front field it does NOT
   erode — it persists at its resting dim into fold-3 as the faint background echo
   behind the mockup (see applyErosion). The drift is a GPU texture-slide (will-change) —
   one layer, rasterised once then moved. background-image/-size + opacity are set
   in JS (buildBackLayer); rotation + drift in startBackDrift. Created only when
   BACK_LAYER_ENABLED (script.js); remove that flag to drop it. */
/* The weave tile is a MASK here, not a painted background — so the layer's whole
   colour is this one background-color, and the CTA hover can tint the back echo
   by changing it. --back-tint defaults to the resting white; initHeroInversion
   swaps it to the hover colour. The 0.9s transition matches the front field's
   sweep duration so both planes arrive together. Opacity is NOT touched by the
   hover — the echo keeps exactly its resting visibility (BACK_LAYER_OPACITY). */
.hero-pattern-back {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-color: var(--back-tint, #fff);
  transition: background-color 0.9s linear;
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  pointer-events: none;
  will-change: transform;
}

.hero-pattern-svg {
  display: block;
  position: relative; /* above .hero-pattern-back (z-index 0) */
  z-index: 1;
  width: 100%;
  height: 100%;
  /* The -45°-rotated cell field overflows the svg's own box at the corners;
     let it paint past the box and rely on .hero-pattern-wrap's overflow:
     hidden to clip to the viewport (the "oversized rotated pattern square
     clipped by the wrap" noted at the top of this file). */
  overflow: visible;
  /* NO will-change here. It was promoting this ~2040px² (≈66MB at DPR 2) SVG to
     its own GPU layer for the perpetual drift — but with the drift removed, that
     promotion had no purpose and was the whole bug: any cursor stroke-mutation
     invalidated the entire promoted picture, forcing a full-texture re-raster that
     outran the GPU and dropped tiles to black (checkerboard, worst in the corners
     that extend furthest off-screen). Un-promoted, the cursor's changes are a plain
     partial repaint of just the dirty region — cheap, and it cannot checkerboard. */
}

/* Figma node 75:4008 — the pattern field renders dim. The dim is now baked into
   an OPAQUE stroke colour (was white @ stroke-opacity PATTERN_DIM): a semi-
   transparent white over the #141414 scene ground composites to exactly this
   grey, so the field looks identical — but because the strokes are now fully
   opaque, the faint back-weave (.hero-pattern-back) no longer bleeds through
   them. #565656 == round(20 + 0.28*(255-20)). The cursor highlight brightens by
   lerping this colour toward white (the ticker in initPointerEffects), NOT by
   opacity anymore — see the stroke-colour write there. Group stays fully opaque. */
/* NO group opacity here — deliberately. Anything below 1 makes the browser
   composite all ~4k paths as ONE layer and re-flatten it every frame the erosion
   wave mutates a stroke, which glitched the opening animation (found 2026-07-30
   with the glitch-hunt harness). The 0.84 knock-back it used to give is baked
   into the stroke colours below instead: c = 0.84*colour + 0.16*--page-bg. */

.hero-pattern-svg path,
.hero-pattern-svg line,
.hero-pattern-svg rect {
  /* Hand-tuned by eye (was #565656, the opaque equivalent of white @ PATTERN_DIM
     over the old #1F1F1F ground). Cooler and darker to sit on --page-bg #070609,
     then flattened at 0.84 over that ground (was #3f3e47 + group opacity).
     Knocked back a further 15% toward --page-bg 2026-08-04 (was #36353d) — the
     dim is the COLOUR, never a group opacity (see the note above), so "lower the
     opacity" is done by mixing toward the ground: c = 0.85*colour + 0.15*#070609. */
  stroke: #2f2e35;
  /* Rounded stroke ends + corners so every arm terminates in a soft cap and the
     diamond/caret joins are radiused rather than mitred to sharp points. */
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The central diamond (+ its 4 arms) of each tile — tinted greyer than the
   linework so the node reads with a bit more weight at its centre. This replaced
   the removed per-node radial glow. #454545 == round(20 + 0.28*(194-20)): the
   opaque equivalent of the old #c2c2c2 @ PATTERN_DIM over the scene ground. */
.hero-pattern-svg .pattern-diamond {
  /* Hand-tuned by eye, in step with the linework above (was #454545, then
     #2b2b31 + the 0.84 group opacity now flattened in).
     Knocked back 15% toward the ground in step with the linework (was #25252b). */
  stroke: #202026;
}

/* The per-tile glow is a gradient FILL, not linework — keep the stroke rule
   above from drawing a 1px box around every tile. */
.hero-pattern-svg .pattern-node-glow {
  stroke: none;
}

/* --- hero (Figma node 75:4007 — centered column) --- */

.hero {
  position: relative;
  z-index: 1;
  /* 100vh of content + the pattern tail below it (fold-3 starts after both).
     .hero-content stays 100vh and centered, so the tail is empty flow space at
     the bottom that the front pattern (absolute) trails into. */
  min-height: calc(100vh + var(--fold1-tail));
  scroll-snap-align: start;
}

/* Hero-wide radial vignette (Figma node 75:4009): transparent centre
   darkening to the page background at the edges. Sits above the pattern
   (wrap z-0) and below the hero copy (z-1). */
.hero-vignette {
  /* FIXED to the viewport, not to the hero box. It darkens all four viewport
     edges evenly and, because it never scrolls, there is no crop line / seam
     where it ends — instead script.js (initVignetteFade) scrubs its OPACITY to 0
     over the first ~70vh of scroll, so it dissolves rather than sliding away.
     No bottom mask any more: the "all sides" look. .hero is position:relative
     with no transform, so fixed positioning resolves against the viewport.
     Level-1 intensity — a light wash, not a heavy frame. */
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Own compositor layer so the CTA-hover opacity fade (script.js initHeroInversion)
     is a GPU blend, not a per-frame repaint. Without this, fading this full-viewport
     gradient repainted the live mutating pattern behind it every frame — the whole
     ~450ms burst of dropped frames during the grow traced to exactly this element. */
  will-change: opacity;
  background:
    radial-gradient(ellipse at 50% 50%,
      rgba(7, 6, 9, 0) 0%,
      rgba(7, 6, 9, 0.08) 30%,
      rgba(7, 6, 9, 0.232) 60%,
      rgba(7, 6, 9, 0.36) 84%,
      rgba(7, 6, 9, 0.4) 100%);
}

/* Reduced motion skips initVignetteFade, so a FIXED vignette would tint every
   fold forever. Pin it to the hero box instead — settled, no scroll fade. */
@media (prefers-reduced-motion: reduce) {
  .hero-vignette {
    position: absolute;
    inset: 0 0 auto 0;
    height: calc(100vh + var(--fold1-tail));
  }
}

.hero-content {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Content stacks from the top so the 50vh title block lands the headline's
     bottom edge on the vertical centre; subcopy + CTAs then fall into the lower half. */
  justify-content: flex-start;
  text-align: center;
  /* Inter-child spacing is set per-gap via margins (title→subcopy 40, subcopy→CTAs 80),
     not a single flex gap. */
  gap: 0;
  /* No top padding — the title block owns the top 50vh so the centre line is exact. */
  padding: 0 clamp(20px, 4vw, 53px) clamp(40px, 8vh, 90px);
}

/* Top half of the hero, plus one headline line-height: the headline is pinned to
   this block's bottom, so extending the block by exactly one line pushes the title
   down until the TOP of its third (last) line rests on the vertical centre.
   font-size mirrors the headline so `em` == one headline line-height (1.16 × size). */
.hero-title-block {
  font-size: var(--fs-display);
  /* -45px raises the WHOLE hero stack (headline, subcopy, CTAs) by shortening
     the reserved block above the headline. Larger = higher. */
  height: calc(50vh + 0.6em + 5.5px - 45px);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  width: 100%;
}

.hero-headline {
  position: relative;               /* anchors the light-theme glow layer below */
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  /* opsz pinned to the bottom of the axis (text cut: sturdier, lower contrast).
     font-variation-settings resets weight, so wght:600 is re-declared here. */
  font-variation-settings: 'opsz' 8, 'wght' 600;
  font-size: var(--fs-display);   /* fluid: 40px floor on mobile up to the 72px design cap */
  line-height: var(--lh-tight);
  letter-spacing: -0.792px;
  text-transform: capitalize;
  color: var(--ink);
  /* NO text-shadow on the element itself — BOTH halos (dark + light glow) live on
     the promoted ::before/::after layers below. Rasterizing the 90px-blur dark halo
     in the single frame the invert front reached the headline was the 133ms spike
     the user isolated to "the headline text". With the shadows on their own layers
     the flip crossfades them by opacity (GPU composite) and only the cheap glyph
     colour repaints. */
  max-width: 820px;
}

/* Two pre-painted halo layers behind the headline glyphs, crossfaded on the flip.
   ::before = the dark theme halo (visible at rest, --dark 1→0 on invert).
   ::after  = the Figma 96:131 white glow (--glow 0→1 on invert).
   color:transparent so ONLY each halo renders; will-change:opacity + translateZ
   promote them so each blur is rastered ONCE while idle and the flip is composite-
   only. Text is duplicated via attr(data-light) (hardcoded in index.html so the
   dark halo is present at first paint, before JS runs); inset:0 + inherited
   font/align lay each layer exactly over the real glyphs. */
.hero-headline::before,
.hero-headline::after {
  content: attr(data-light);
  position: absolute;
  inset: 0;
  z-index: -1;                     /* behind the glyphs, over the pattern */
  font-variation-settings: 'opsz' 8, 'wght' 600;  /* match the glyph layer */
  color: transparent;
  pointer-events: none;
  will-change: opacity;
  transform: translateZ(0);
}
.hero-headline::before {
  text-shadow:
    0 2px 8px rgba(0, 0, 0, 0.62),
    0 4px 18px rgba(0, 0, 0, 0.62),
    0 8px 36px rgba(0, 0, 0, 0.62),
    0 16px 60px rgba(0, 0, 0, 0.62),
    0 28px 90px rgba(0, 0, 0, 0.55);
  opacity: var(--dark, 1);
}
.hero-headline::after {
  text-shadow: 0 6px 57.9px rgba(255, 255, 255, 0.42);
  opacity: var(--glow, 0);
}

.hero-subcopy {
  position: relative;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-xl);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-wordmark);
  color: var(--ink-body);
  /* Heavy glyph-hugging dark halo, no backing plate. The dark hugs each word;
     the gaps between words stay open (a text-shadow can't fill them) — that's
     the deliberate trade for keeping it box-free. */
  text-shadow:
    0 1px 3px rgba(0, 0, 0, 0.62),
    0 2px 8px rgba(0, 0, 0, 0.62),
    0 4px 18px rgba(0, 0, 0, 0.62),
    0 8px 34px rgba(0, 0, 0, 0.62),
    0 16px 54px rgba(0, 0, 0, 0.55);
  max-width: 520px;
  margin-top: 30px;  /* gap from the title block */
}

.hero-ctas {
  display: flex;
  align-items: center;
  /* Column now: the button pair, then the reassurance line beneath it. */
  flex-direction: column;
  justify-content: center;
  gap: 18px;
  margin-top: 65px;   /* gap from the subcopy */
}

/* Risk-reversal line under the hero CTAs. Same full halo recipe as .hero-subcopy —
   at 14px over the lattice the lighter version didn't lift the type enough. */
.hero-reassure {
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-meta);
  letter-spacing: var(--ls-meta);
  color: var(--ink-quiet);
  text-shadow:
    0 1px 3px rgba(0, 0, 0, 0.62),
    0 2px 8px rgba(0, 0, 0, 0.62),
    0 4px 18px rgba(0, 0, 0, 0.62),
    0 8px 34px rgba(0, 0, 0, 0.62),
    0 16px 54px rgba(0, 0, 0, 0.55);
  /* 1fr auto 1fr: the two side columns share the leftover space equally, which
     pins the middle (auto-width) ✦ to the exact horizontal centre. The halves
     then hug the star from the inside — right-aligned left, left-aligned right. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;              /* space either side of the star */
  width: 100%;
}
.hero-reassure > span:first-child { justify-self: end; }
.hero-reassure > span:last-child  { justify-self: start; }
.hero-reassure-star { color: var(--ink-muted); }
/* Brightens to white while the primary CTA is hovered — the fine print lights up
   with the button it belongs to. :has() on the shared .hero-ctas parent, since
   the <p> is a SIBLING of the button's wrapper, not a descendant. */
.hero-reassure { transition: color 0.28s ease, transform 0.28s ease; }
.hero-reassure-star { transition: color 0.28s ease; }
.hero-ctas:has(.btn-solid:hover) .hero-reassure,
.hero-ctas:has(.btn-solid:hover) .hero-reassure-star { color: var(--ink); }
/* Scale, not font-size: a transform is compositor-only, so the text doesn't
   reflow or shift the CTA stack while it grows. 1.1 ~= 14px reading as 15.4px. */
.hero-ctas:has(.btn-solid:hover) .hero-reassure { transform: scale(1.1); }

.hero-cta-buttons {
  display: flex;
  align-items: center;
  gap: 24px;
}

/* --- Compliance Hub fold (pinned scrollytelling) --- */

.fold-compliance {
  /* BACK INSIDE .scene-compliance, and TRANSPARENT: the sticky pattern (front
     lattice + the .hero-pattern-back echo behind it) is one continuous element
     from the hero down through here, so fold-3 must not paint over it. The flat
     #1F1F1F ground now comes from .scene-compliance itself. */
  position: relative;
  z-index: 1;
  min-height: 100vh;  scroll-snap-align: start;
  background: transparent;
}

/* This is the element script.js pins via ScrollTrigger for the fold's
   scroll distance — the pin itself reserves the extra scroll room, so
   .fold-compliance's own height only needs to cover one viewport. */
.fold-compliance-pin {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Even gap between heading -> image -> columns. Figma 103:7478 spaces the
     heading ~54px above the mockup and the columns ~48px below it. */
  gap: 74px;
  /* The logo reel now sits below (not fold-2's weave), so no overhang reservation
     is needed here — a plain symmetric bottom pad hands off to the reel. */
  padding: clamp(40px, 5.5vh, 68px) clamp(20px, 4vw, 53px) clamp(40px, 7vh, 78px);
  text-align: center;
  /* --fold-inset (the nav's Figma content column) is declared once on :root. */
  --fold-width: calc(100vw - 2 * var(--fold-inset));
  /* Bigger showcase image (Raycast-style). Height term stays a safety cap for
     short viewports, keeping the 1239:672 ratio; on normal screens the vw binds. */
  --fold-media-width: min(1193px, 84vw, calc(68vh * 1239 / 672));
}


/* Two stacked heading lines (Figma 83:638 / 83:639); faded in on collapse. */
.fold-heading {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Figma 103:7478: the two lines sit ~48px apart (line tops 132 -> 180). */
  gap: 6px;
  opacity: 0;
  /* Taken OUT of the centered flex stack (absolute) so it no longer pushes the
     showcase down — it sits ABOVE the fold frame, near the pin's top edge, and
     the image + dock center in the full height on their own. `top`, not
     transform, positions it (GSAP owns this element's transform). */
  position: absolute;
  /* Negative top lifts it ABOVE the pin's top edge, off the visible fold —
     it only comes into view as you scroll up into the section. */
  bottom: 100%;
  margin-bottom: clamp(24px, 5vh, 60px);
  left: 0;
  right: 0;
}

.fold-heading-line {
  width: 741.512px; /* visual-edit @1636px viewport */
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: clamp(18px, 1.85vw, 20px);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-eyebrow);
  color: var(--ink);
}

/* Visual edit @1636px: chapter 1's heading measure only — chapters 2/3 keep the
   shared 741.512px above. Scoped rather than folded into the base rule, which
   would re-wrap every chapter heading in fold-3. */
.fold-chapter--1 .fold-heading-line { width: 263.969px; }


/* The big product screenshot. Its opacity is 0 here and faded to 1 by
   script.js as fold-3 arrives (a plain fade — no line-into-image reveal). */
/* The frame's border as a separate ring so it can fade on its OWN schedule,
   independently of the (dark, near-invisible-when-masked) screenshot. Painted
   above the image + dark overlay; its mask dissolves the stroke well before the
   bottom so the outline clearly softens instead of ending in a hard line. */
.fold-mockup-frame::before {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid var(--rule-ghost);
  border-radius: var(--r-lg);
  pointer-events: none;
  z-index: 3;
  -webkit-mask-image: none;
  mask-image: none;
}

.fold-mockup-frame {
  --mockup-pad: clamp(6px, 0.8vw, 12px);
  position: relative;
  width: var(--fold-media-width);
  /* Native screenshot ratio so the image fits the frame exactly — no crop,
     no stretch, no letterbox. */
  aspect-ratio: 1239 / 672;
  border-radius: var(--r-lg);
  overflow: hidden;
  flex-shrink: 0;
  background: rgba(0, 0, 0, 0.4);
  box-shadow: none;   /* glow removed */
  opacity: 0;
  /* Even 60px gaps handled by the flex gap; no extra nudge. */
  margin-top: 0;
  /* The markup now lists .fold-belowstage BEFORE the frame (the alt look places
     both by grid-area, so DOM order is free there). Explicit flex order keeps
     the ORIGINAL centered stack reading image -> dock -> description. */
  order: 1;
}

/* Stacked absolutely; script.js crossfades opacity between the active
   step's panel and the rest. Inset by --mockup-pad so the frame's background
   shows as a matte/padding ring between the border and the screenshot; own
   overflow + radius clips the overscanned image to rounded inner corners. */
.fold-mockup-panel {
  position: absolute;
  inset: var(--mockup-pad);
  opacity: 0;
  overflow: hidden;
  border-radius: var(--r-lg);
}

/* Near-full crop of the dashboard with a slight overscan — exact percentages
   from Figma node 83:637. */
.fold-mockup-panel img {
  position: absolute;
  width: 107.46%;
  height: 113.29%;
  left: -3.58%;
  top: -4.76%;
  max-width: none;
}

/* Faint bottom darkening for the columns' legibility only. Kept light and low
   so it no longer covers the image where the frame's mask dissolves it — that
   way the fade reads on the actual screenshot, not on an opaque dark band. */
.fold-mockup-fade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: none;
}

/* Carousel indicator + text columns. Figma 103:7478 places the columns BELOW
   the image (~48px gap) near the bottom of the fold, not pulled up over it.
   Faded in on collapse. */
.fold-belowstage {
  position: relative;
  z-index: 2;
  order: 2;   /* see the note on .fold-mockup-frame — DOM has it first now */
  width: min(85vw, 1300px);
  margin-top: 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* image -> dock -> description */
  gap: 22px;
  opacity: 0;
}

/* Raycast-style icon dock: a rounded bar of switch buttons under the image.
   Each button flips the crossfading screenshot; the active one lights up and
   reveals its floating label (like Raycast's "Clipboard History" tooltip). */
.fold-dock {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.fold-dock-btn {
  position: relative;
  width: 52px;
  height: 52px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--rule-faint);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--ink-a28);
  cursor: pointer;
  transition: background 0.3s ease, color 0.3s ease, transform 0.3s ease;
}
.fold-dock-btn svg { width: 24px; height: 24px; }

.fold-dock-btn:hover {
  background: rgba(255, 255, 255, 0.07);
  color: var(--ink-a80);
}

/* Active button: filled tile + full-white glyph, matching the reference's
   selected dock icon. */
.fold-dock-btn--active {
  color: var(--ink);
  border-color: var(--rule-ghost);
  /* Scale, not width/height: a transform doesn't reflow the dock row, so the
     neighbouring tiles hold still while the active one grows (macOS-dock feel).
     transform is already in this button's transition list, so it animates.
     bottom origin: the tile grows upward off the dock's baseline instead of
     swelling from its centre, so its bottom edge stays flush with the row. */
  transform: scale(1.15);
  transform-origin: bottom center;
}

/* Floating label above the active button (Raycast tooltip). */
.fold-dock-label {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: 5px 10px;
  border-radius: 7px;
  background: rgba(20, 20, 22, 0.92);
  border: 1px solid var(--rule-soft);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  letter-spacing: var(--ls-meta);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.fold-dock-btn--active .fold-dock-label {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Description under the dock. All five stack in the same box; script.js fades
   the active one in and the rest out (a single crossfading line). */
.fold-desc {
  position: relative;
  width: min(70vw, 760px);
  height: 3em;
}

.fold-column {
  position: absolute;
  inset: 0;
  margin: 0;
  cursor: default;
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  text-align: center;
  color: var(--ink-a60);
  opacity: 0;
  transition: opacity 0.45s ease;
}

.fold-column-lead {
  font-weight: var(--fw-medium);
  color: var(--ink);
}

/* Active step's description is the only visible one. */
.fold-column--active {
  opacity: 1;
}

/* ============================================================================
   FOLD-3 ALT LOOK ("tabs") — LIVE. The class is set in index.html; the
   ORIGINAL look is everything above and is restored by removing that class,
   since this whole block only applies when .fold-compliance--alt is set. Reference: title on
   top, a pill tab selector under it, then description LEFT / image RIGHT.
   Same markup, restyled: .fold-belowstage goes display:contents so the dock
   and desc can take their own grid areas.
   ========================================================================== */
/* Composition per Figma node 151:59 (measured on its 1512px frame): ONE centred
   column, stacked heading -> mockup -> tab dock -> description. The old
   side-by-side (desc left / mockup right) is gone; every row is centre-aligned
   on the same axis, and the row gaps below are the Figma deltas expressed as a
   fraction of that 1512 frame. */
.fold-compliance--alt .fold-compliance-pin {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  grid-template-areas:
    'heading'
    'dock'
    'mockup'
    'desc';
  /* Pack the rows together and centre the block as a whole. Without this the
     grid's auto rows STRETCH to fill the 100vh pin. */
  align-content: center;
  row-gap: 0;   /* each row carries its own margin — the gaps differ */
  width: min(1480px, 100vw - 2 * var(--fold-inset));
  margin: 0 auto;
}

.fold-compliance--alt .fold-heading-line {
  font-family: var(--font-display);
  /* 34px at the 1636px edit viewport; same clamp idiom as the base rule
     (2.08vw · 1636 = 34px), bounds scaled by the same 34/40 factor. */
  /* Scales with the stage. The mockup is a vw width, so fixed-px type made the
     IMAGE look like the only thing shrinking on a narrower window. Every size
     in this fold is now vw-keyed off the 1512 design frame, bounded so it
     stops shrinking before it gets unreadable. 30px at 1512 = 1.98vw. */
  font-size: clamp(22px, 1.98vw, 32px);   /* tuned by eye */
  letter-spacing: 0;
}
.fold-compliance--alt .fold-heading {
  grid-area: heading;
  position: relative;     /* back in flow, but offsettable */
  margin: 0 0 6.5vw;      /* heading -> tab bar (99/1512), tuned by eye */
  /* Dropped 41px WITHOUT costing layout space. Must be `top`, NOT `translate`
     or `transform`: GSAP's entrance (`gsap.set(foldContent, …)`) writes inline
     `transform`/`translate` on this element and would wipe either one. */
  top: 2.7vw;   /* 41/1512 */
  justify-self: center;
  opacity: 1 !important;  /* beats GSAP's inline entrance opacity for the trial */
}
/* Dissolve the wrapper so .fold-dock and .fold-desc become grid items of the
   pin. NOTE: opacity/transform don't apply to a display:contents element, so
   the GSAP entrance fade on .fold-belowstage is a no-op here — fine for a
   look trial. */
.fold-compliance--alt .fold-belowstage { display: contents; }

/* Icon dock -> horizontal pill tab bar (labels inline, icons hidden). */
.fold-compliance--alt .fold-dock {
  grid-area: dock;
  justify-self: center;
  /* Figma 155:2755: the row sits ABOVE the mockup inside a fully-rounded
     hairline capsule; the active tab is a filled pill inside it. Padding is
     what gives the stroke clearance — spacing between labels is `gap`. */
  gap: 3vw;
  padding: 0.53vw 1.6vw;   /* 8/24 on 1512 */
  border: 1px solid var(--rule-soft);
  border-radius: var(--r-pill);
  background: transparent;
  margin: 0 0 2.2vw;   /* tab bar -> mockup top (33/1512) */
}
.fold-compliance--alt .fold-dock-btn {
  width: auto;
  height: auto;
  /* Just enough padding for the SELECTED tab's fill to read as a pill around
     the text — the spacing between tabs is the dock's gap, not this. */
  padding: 0.26vw 0.93vw;   /* 4/14 on 1512 */
  margin: 0 -0.93vw;   /* horizontal padding pulled back out — `gap` owns spacing */
  border: none;
  border-radius: var(--r-pill);
  color: var(--ink-a60);
}
.fold-compliance--alt .fold-dock-btn svg { display: none; }
.fold-compliance--alt .fold-dock-btn--active {
  transform: none;          /* no macOS-dock scale in tab mode */
  /* The selected tab is the ONLY filled thing in the row — a frosted pill. */
  background: rgba(255, 255, 255, 0.14);
  -webkit-backdrop-filter: blur(1px);
  backdrop-filter: blur(1px);
  border-color: transparent;
  color: var(--ink);
}
/* Tabs are a simple column-less row of text buttons — no marker glyphs. */
/* The floating tooltip label becomes the tab's inline text. */
.fold-compliance--alt .fold-dock-label {
  position: static;
  transform: none !important;
  opacity: 1;
  padding: 0;
  border: none;
  background: transparent;
  color: inherit;
  font-size: clamp(12px, 0.99vw, 16px);   /* 15/1512 */
}

/* Description: left column, lead promoted to a big two-line heading. */
/* Description: a narrow CENTRED paragraph under the dock (Figma 151:90 —
   403px wide on a 1512 frame, 17px/1.55, 60% white, centre-aligned). */
.fold-compliance--alt .fold-desc {
  grid-area: desc;
  width: auto;
  /* Wide enough that the LONGEST description (TPRM, ~120 chars at 20px/1.55)
     still breaks at two lines, and tall enough to hold two — so switching tabs
     never reflows the dock above or the fold below by a line's height. */
  max-width: min(44vw, 680px);
  min-width: 300px;
  height: auto;
  margin-top: 2.3vw;   /* mockup bottom -> copy (35/1512) */
}
.fold-compliance--alt .fold-column {
  position: static;         /* no stacked crossfade box — active one is in flow */
  display: none;
  text-align: center;
  font-size: clamp(15px, 1.32vw, 21px);   /* 20/1512 */
  line-height: var(--lh-body);
  min-height: 3.1em;   /* two lines at this size — no reflow between tabs */
  color: var(--ink-a60);
  opacity: 1;
}
.fold-compliance--alt .fold-column--active { display: block; }
/* The Figma composition shows the sentence only — the promoted lead headline
   belonged to the old two-column look. */
.fold-compliance--alt .fold-column-lead { display: none; }

/* Image on the right, filling its column; drop the bottom fade band (the
   dock/desc no longer sit on the image). */
/* Centred stage, Figma 151:91-92: 771×452 on a 1512 frame — 51vw at that ratio,
   12px radius, sitting on a 40% black plate. Height follows the ratio (not vh)
   so the crop stays put as the viewport changes. */
.fold-compliance--alt .fold-mockup-frame {
  grid-area: mockup;
  aspect-ratio: 772 / 474;   /* Figma 155:2755 stage, 772x474 on a 1512 frame */
  /* The crops are all %-based (object-position / scale), so they survive the
     stage getting BIGGER or SMALLER — but not the stage changing RATIO. A
     `max-height` cap did exactly that: it squashed the height while `width`
     stayed put, overriding aspect-ratio and re-framing every screenshot. So the
     height limit is folded into the WIDTH instead (52vh x 772/474), and
     aspect-ratio is then never contradicted. */
  width: min(50vw, calc(52vh * 772 / 474), 960px);
  height: auto;
  justify-self: center;
  /* Stage plate — tuned by eye in the frame harness. A translucent grey wash
     (not the screenshots' #0A0A0A; that colour lives on the inner panel now)
     so the stage reads as a lifted surface over the fold background. */
  background: rgba(97, 97, 97, 0.52);
  --mockup-pad: 4px;
  border-radius: var(--r-lg);
  box-shadow: 0 0 25px 6px rgba(0, 0, 0, 0.37);
  opacity: 1 !important;
}
/* The ring is a separate ::before so it can fade on its own; its radius must
   track the frame's or the corners disagree. */
.fold-compliance--alt .fold-mockup-frame::before {
  border: 1px solid var(--rule-soft);
  border-radius: var(--r-lg);
}
/* The inner panel is what the image letterboxes inside (object-fit: contain at
   zoom < 100%), so it carries the same chrome colour — otherwise the gap around
   a shrunken screenshot reads as a hole rather than part of the screen. */
.fold-compliance--alt .fold-mockup-panel {
  background: #0A0A0A;
}
.fold-compliance--alt .fold-mockup-fade { display: none; }
/* The frame no longer holds the screenshot's native ratio (it's width×height
   independent), so the image must COVER it — object-fit crops, it never
   stretches. Overrides the base rule's %-sized overscan, which would distort. */
.fold-compliance--alt .fold-mockup-panel img {
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}
/* Per-screenshot crop, tuned by eye in the harness panel. Four knobs each:
     object-fit       — contain shows the WHOLE screenshot (letterboxed on the
                        panel's #0A0A0A); cover fills the panel and crops
     object-position  — which part survives the crop (X% Y%); values PAST 0/100
                        push the image beyond the panel edge, where the panel's
                        own overflow:hidden clips it
     scale            — zoom; object-fit crops BEFORE transform, so shrinking a
                        `cover` image shrinks an already-trimmed one — hence the
                        contain panels
     transform-origin — matched to object-position so zoom and pan stay
                        independent; otherwise zooming re-centres the crop. */
.fold-compliance--alt .fold-mockup-panel[data-step="compliance"] img {   /* Risk */
  object-fit: contain;
  object-position: 40% 7%;
  transform: scale(1.09);
  transform-origin: 40% 7%;
}
.fold-compliance--alt .fold-mockup-panel[data-step="risk"] img {   /* Lenses */
  object-fit: contain;
  object-position: 105% 46%;
  transform: scale(0.98);
  transform-origin: 105% 46%;
}
.fold-compliance--alt .fold-mockup-panel[data-step="certification"] img {   /* Compliance */
  object-position: 34% 49%;
  transform: scale(1.42);
  transform-origin: 34% 49%;
}
.fold-compliance--alt .fold-mockup-panel[data-step="policies"] img {   /* Certification */
  object-fit: contain;
  object-position: 53% 79%;
  transform: scale(0.95);
  transform-origin: 53% 79%;
}
.fold-compliance--alt .fold-mockup-panel[data-step="evidence"] img {   /* TPRM */
  object-position: 33% 48%;
  transform: scale(1.42);
  transform-origin: 33% 48%;
}
.fold-compliance--alt .fold-mockup-panel[data-step="vendors"] img {   /* BCP */
  object-position: -1% -8%;
  transform: scale(1.24);
  transform-origin: -1% -8%;
}
.fold-compliance--alt .fold-mockup-panel[data-step="audits"] img {   /* Portal */
  object-position: 60% 35%;
  transform: scale(1.9);
  transform-origin: 60% 35%;
}
.fold-compliance--alt .fold-mockup-panel[data-step="vulnerability"] img {   /* Vulnerability */
  object-fit: contain;
  object-position: -5% 44%;
  transform: scale(1.48);
  transform-origin: -5% 44%;
}

/* ============================================================================
   FOLD-3 — EMBLEM look (LIVE).  Figma "home page" 164:364.
   The fold is now a single centred stack: the lattice emblem (frame 1048,
   442x442 on the 1512 frame), a two-line white heading, and a dimmer two-line
   subline. No dock, no screenshots — that markup is gone from index.html and
   the .fold-compliance--alt block above is dormant.
   The section stays TRANSPARENT so the existing back pattern keeps showing
   through, exactly as before.
   ========================================================================== */
/* The pin is no longer the stack — it is the WINDOW the chapter track slides
   behind. It keeps the vertical rhythm and loses its horizontal padding so each
   chapter is exactly one pin-width wide. `overflow: hidden` here is fine (the
   ban in the header is on html/body only — this is a local clip, same as
   .hero-pattern-wrap). */
.fold-compliance--emblem .fold-compliance-pin {
  /* The short-viewport tiers below push the old stack to flex-start with a big
     nav pad — that stack was taller than 100vh. This one isn't, so it stays
     centred and keeps a plain symmetric pad. (Class specificity, not order,
     is what beats those @media rules.) */
  justify-content: center;
  gap: 0;
  /* Symmetric — the stage is centred on the TRUE viewport centre. (Padding the
     top by --nav-h to "centre below the fixed bar" only pushes the shape half a
     bar lower, which reads worse: the bar is a thin overlay, not a wall.) */
  padding: clamp(40px, 5.5vh, 68px) 0;
  /* Clip SIDEWAYS only. `overflow: hidden` clipped both axes, so the wide
     chapter-3 row — which overhangs the pin vertically — got sliced off at the
     bottom edge (it read as fold-4 painting over fold-3's shapes). `clip` on one
     axis is the only value that lets the other stay `visible`; `hidden` would
     force the other axis to `auto`. */
  overflow-x: clip;
  overflow-y: visible;
  /* The row's fade-out with distance from the primary shape is done PER STROKE
     in script.js (emblemFalloff), not with a mask here: a mask on this element
     dims the chapter COPY as well, and the copy sits far from the primary. */
}

/* --- the EMBLEMS: one per chapter, riding inside it -------------------------
   Figma 172:1034 alternates the composition side by side. There is no fixed
   stage any more: each chapter holds its OWN copy of the artwork as a flex
   child, so the shape travels with its text and never erodes or draws on.
   --shape-w is the one size knob; it lives on the chapter so both the emblem
   and the row's centring can read it. */
.fold-compliance--emblem .fold-chapter {
  /* Tuned by eye 2026-08-02 (was 471, the Figma 172:1034 @1512 width). The vw/vh
     terms are the small-viewport guards — they only bite below ~1266px wide. */
  --shape-w: min(395px, 31.2vw, 52.5vh);
  /* The gap between the shape and its copy, and the copy's own column width —
     the row is (shape + gap + copy) centred, so these three are the layout. */
  --shape-gap: clamp(40px, 16vw, 242px);
  --copy-w: min(273px, 88vw);
  /* Per-CHAPTER stroke colour. script.js strokes each chapter's own emblem with
     its colour once, at init (a chapter with no var of its own inherits the
     previous one's). --glow is the halo colour, kept in step by the same code. */
  --ch1-color: #f3ff94;
  --ch2-color: #a98cff;   /* purple — chapter 2's row; its primary motif is white */
  --ch3-color: #9eb1ff;
  --glow: var(--ch1-color);
}

.fold-compliance--emblem .fold-emblem {
  /* A flex child of its chapter (that is what keeps the alternating
     shape-left / shape-right composition in CSS) — but GSAP owns its `y` and
     counter-translates it against the track, so it does NOT ride the scroll:
     every emblem sits on the same stage and chapters change by the shape
     DRAWING IN / OUT. Don't set a transform here; GSAP owns it.
     It is a fixed SQUARE stage; the artwork is absolutely positioned inside it
     by script.js so that every shape's central diamond lands on the same point
     at the same size (see alignEmblemDiamonds). */
  flex: 0 0 auto;
  position: relative;
  width: var(--shape-w);
  height: var(--shape-w);
  line-height: 0;
  /* Two passes, tuned by eye 2026-08-02. drop-shadow follows the alpha of the
     ACTUAL strokes, so it haloes the lattice rather than boxing it (box-shadow
     would frame the rectangle).
       1st — a 4px halo in the chapter's --glow, PLUS the flare: script.js
             punches --fx-bloom to 24px the moment a chapter finishes DRAWING
             and eases it back to 0 over ~0.85s (initFold3EmblemFx's
             flareBloom/watchBloom), so the two simply add and the shape blooms
             as it lands. Re-arms on scroll-up, so it plays again coming down.
       2nd — a tight 1px white pass at 10%, just enough to thicken the light. */
  filter: var(--emblem-halo);
}
/* DECLARED ON THE EMBLEM, not on the fold. A var() inside a custom property is
   substituted where that property is DECLARED — declaring the halo up on the
   fold froze --glow at the fold's value (white), so every chapter haloed white
   however script.js painted the emblem. Declaring it here resolves --glow per
   emblem, which is exactly where paintShapes writes the chapter colour. */
.fold-compliance--emblem,
.fold-compliance--emblem .fold-emblem {
  /* --halo-r is the base radius in px, written per emblem by paintShapes from
     fold3Shape.glowRadius[i] — chapter 3's pink carries further than the others
     at the same radius, so it wears a smaller one. */
  --emblem-halo: drop-shadow(0 0 calc(var(--halo-r, 4) * 1px + var(--fx-bloom, 0) * 1px) var(--glow, #ffffff))
    drop-shadow(0 0 1px rgba(255, 255, 255, 0.1));
}
/* CHAPTER 1'S WING ROW DOESN'T GLOW. A CSS filter haloes the WHOLE subtree and a
   child cannot opt out, so for the wide file (the only emblem with a
   `secondary shapes` group) the halo moves DOWN one level onto the primary
   shape. Thirty half-motifs each wearing a 4px bloom smeared the band into a
   green haze; the glow belongs to the white motif in the middle.
   (The Figma export misspells it `secodary` in chapter 3's file — match both, or
   chapter 3 keeps the emblem-level halo and glows whatever `no-glow` says.) */
.fold-compliance--emblem .fold-emblem:has([id^='secondary shape']),
.fold-compliance--emblem .fold-emblem:has([id^='secodary shape']) { filter: none; }
.fold-compliance--emblem .fold-emblem [id='primary shape'] { filter: var(--emblem-halo); }
/* …EXCEPT once chapter 1 has handed over to chapter 2: all that is left of
   chapter 1 is its row of diamonds, so haloing its primary GROUP would glow the
   wrong column. script.js adds this class and moves the halo onto the kept
   diamond inside chapter 2's primary motif (see tintKeptDiamonds). */
.fold-compliance--emblem .fold-emblem.is-handed-over [id='primary shape'] { filter: none; }
/* A chapter with glow off wears no halo at EITHER level. */
.fold-compliance--emblem .fold-emblem.no-glow { filter: none; }
/* The emblem's OWN transform lives on the inner <svg>, never on .fold-emblem —
   GSAP owns the host's `y` (the counter-translate that parks the stage), and a
   CSS transform there would fight it. Composing the effects as three custom
   properties in ONE fixed transform chain also stops them fighting each other:
     --fx-tx / --fx-ty  TILT    3D lean towards the cursor (fine pointers only)
     --fx-scale         spare scale slot — the SETTLE that used it was removed
                        2026-08-03; kept at its default 1 so a future effect can
                        ride the chain without re-plumbing it
   Values are written by initFold3EmblemFx() in script.js. The settle rides a
   custom PROPERTY rather than GSAP's own `scale`, deliberately: the moment GSAP
   writes a transform on this element it owns the whole chain and the tilt vars
   stop reaching the page. */
.fold-compliance--emblem .fold-emblem svg {
  transform-origin: 50% 50%;
  transform: perspective(900px)
    rotateX(calc(var(--fx-tx, 0) * 1deg)) rotateY(calc(var(--fx-ty, 0) * 1deg))
    scale(var(--fx-scale, 1));
}
/* MATCH THE DIAMONDS, not the boxes — but in JS, not here. The two files draw
   their central diamond at different fractions of their own viewBox and at
   different offsets inside it, so alignEmblemDiamonds() in script.js measures
   each one and sets width/left/top on the inlined <svg>. Nothing to re-derive
   by hand when a file is replaced. */
.fold-compliance--emblem .fold-emblem img,
.fold-compliance--emblem .fold-emblem svg {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;   /* the wide shape draws past its own box */
}
.fold-compliance--emblem .fold-emblem svg {
  position: absolute;
  left: 0;
  top: 0;
}

/* Every chapter now ships the SAME artwork at the same size, fully drawn from
   the first frame — nothing is hidden and nothing animates on. (The old
   erode/draw handover, its --shape2-scale and the hidden shape 2 are gone.) */

/* --- the TEXT track: the only thing that travels ---------------------------
   Children are flex:0 0 100%, so the track's own box stays pin-width while its
   scrollWidth is N x that — exactly the distance script.js scrubs x across. */
.fold-compliance--emblem .fold-chapters {
  display: flex;
  /* VERTICAL track: chapters stack and the text scrolls UP through the pin.
     The emblems ride this track only on DESKTOP, where parkStage counter-
     translates them per frame; on mobile they are moved out to the fixed
     .fold-emblem-stage (see the <=620px block) and this track carries copy
     alone. */
  flex-direction: column;
  align-items: stretch;
  width: 100%;
  /* GSAP owns `y` on this element; nothing else may set a transform here. */
  will-change: transform;
  /* The track covers the whole pin now — the copy sits BESIDE the shape (Figma
     172:1034), vertically centred on the same line as the shape's centre, so
     each chapter needs the full height to centre within. */
  position: absolute;
  inset: 0;
  margin-top: 0;
}

/* Figma 172:1034: shape beside the copy, and the sides ALTERNATE
   (left · right · left). Each chapter is a centred ROW of [emblem | gap | copy];
   even chapters flip it with row-reverse, which mirrors the pair without moving
   the centre. */
.fold-compliance--emblem .fold-chapter {
  flex: 0 0 100%;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: var(--shape-gap);
  padding: 0 clamp(20px, 4vw, 53px);
  text-align: left;
}
.fold-compliance--emblem .fold-chapter:nth-child(even) {
  flex-direction: row-reverse;
}
.fold-compliance--emblem .fold-chapter-copy {
  flex: 0 0 auto;
  width: var(--copy-w);
  /* A DARK HALO, not a glow (2026-08-05, user): the wing row runs straight
     behind the copy, so each line gets a soft pool of the fold's own background
     around it. Three text-shadows in the SAME dark, stacked wide→tight, so the
     pool falls off gradually instead of outlining the letters. */
  text-shadow: 0 0 18px rgba(12, 12, 12, 0.95), 0 0 8px rgba(12, 12, 12, 0.9),
    0 0 3px rgba(12, 12, 12, 0.85);
}

/* Reduced motion: initFold3Chapters never runs, so nothing slides the track —
   chapters 2 and 3 would stay clipped off-screen. Unroll into a plain vertical
   stack: the settled, all-content-visible state. Each chapter keeps its own
   emblem (it is a child now), stacked above its copy. */
@media (prefers-reduced-motion: reduce) {
  .fold-compliance--emblem .fold-compliance-pin { overflow: visible; }
  .fold-compliance--emblem .fold-chapters {
    /* back into flow — the RM branch is a plain vertical stack */
    position: static;
    flex-direction: column;
    gap: clamp(64px, 10vh, 140px);
    margin-top: clamp(48px, 8.1vh, 122px);
  }
  .fold-compliance--emblem .fold-chapter,
  .fold-compliance--emblem .fold-chapter:nth-child(even) {
    flex: 0 0 auto;
    width: 100%;
    /* Beside-each-other makes no sense in a vertical stack — the copy goes
       UNDER its shape, centred. */
    flex-direction: column;
    align-items: center;
    gap: clamp(32px, 5vh, 64px);
    text-align: center;
  }
  /* The stacked RM branch IS centred, so give the headline its centring back
     (and centre the sub's own narrower column with it). */
  .fold-compliance--emblem .fold-heading { align-items: center; }
  .fold-compliance--emblem .fold-sub { margin-inline: auto; }
}

/* The base .fold-heading is absolutely positioned ABOVE the pin (the old
   look parked it off-fold). Here it is a normal flow item in the stack. */
.fold-compliance--emblem .fold-heading {
  position: static;
  /* The 122 emblem->heading gap moved to .fold-chapters' margin-top (the emblem
     is a separate stage now), so the heading itself sits flush at the top of its
     chapter — which is what keeps the chapters' text vertically aligned. */
  margin: 0;
  gap: 0;
  opacity: 1;
  /* The base rule centres the line inside the flex column (it's the old centred
     fold heading). Here the copy column is left-aligned, and .fold-sub below is
     a plain block starting at the column's left edge — so a centred headline sat
     off by half the difference between the two widths. Start-align it. */
  align-items: flex-start;
}
.fold-compliance--emblem .fold-heading-line {
  /* Figma 164:366 — Manrope 400, 28px, white, "normal" line-height (~1.36).
     Visual edit: the chapter headline is SERIF now (Roboto Serif, already
     loaded in index.html) — the fold's editorial voice. */
  font-family: var(--font-display);
  /* Visual edit @1636px: the headline gets its own 254.531px column (narrower
     than the sub's 272.664) so it wraps where it was tuned to. Same min(px, vw)
     idiom as .fold-sub — the px arm wins at desktop widths. Widened to the sub's
     272.664 column: at 254.5, "Turn complexity into focus" broke to THREE lines
     once the font reached its 32px cap (above ~1636px). */
  width: min(272.664px, 88vw);
  /* Visual edit @1636px: 32px / 500. Fluid idiom kept, retargeted —
     1.956vw == 32px at 1636px, and the min/max arms scale with it. */
  font-size: clamp(23px, 1.956vw, 32px);
  font-weight: var(--fw-medium);
  line-height: var(--lh-cozy);
  letter-spacing: normal;
  color: var(--ink);
}

.fold-compliance--emblem .fold-sub {
  /* Figma 172:1108 — 20px, #d5d5d5, 399px column, 22px below the headline.
     (Figma says Inter; the project's fold type is Manrope — kept.) */
  margin: clamp(14px, 2.2vh, 22px) 0 0;
  /* Visual edit @1636px: 272.664px wide, 18px, #a8a8a8 — a narrower, quieter
     column under the serif headline. The fluid idiom is kept, retargeted:
     1.1vw == 18px at 1636px, and the px arm of the min() is the one that wins
     at desktop widths (88vw only takes over below ~310px). */
  width: min(272.664px, 88vw);
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: clamp(15px, 1.1vw, 18px);
  line-height: var(--lh-cozy);
  /* Visual edit @1636px: lifted from #a8a8a8 to #cfcfcf. */
  color: var(--ink-body);
  opacity: 1;
}

/* ============================================================================
   Logo reel — scrolling integration logos between fold-3 and fold-2.
   ========================================================================== */
.logo-reel {
  /* STATIC row inside .scene-compliance: transparent background so the unified
     hero weave stays visible behind it (it must not look like the page scrolled).
     No z-index/opaque fill — that would occlude the pinned pattern. */
  position: relative;
  z-index: 1;
  background: transparent;
  /* Sits below the hero fold, nudged up a touch so the two rows read closer to
     the fold line. */
  margin-top: -6vh;
  padding: clamp(40px, 7vh, 80px) 0;
}

/* The masked window: logos fade out at the left/right edges. Two rows stacked. */
.logo-reel-viewport {
  display: flex;
  flex-direction: column;
  gap: clamp(24px, 3.5vh, 44px);
  /* Vertical padding is glow headroom: the mask below paints only inside this
     box, so without it the logos' drop-shadow glow is clipped at the top and
     bottom rows. */
  /* Padding + row gap are sized so the LONGER row (7 marks x 120px) still fits
     on one line at ~1440px+ — bump either and the last mark wraps to its own row. */
  padding: 40px clamp(32px, 6vw, 140px);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 10%, #000 90%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 10%, #000 90%, transparent);
}

/* Desktop: the track is transparent to layout, so the row is a direct flex
   child of the viewport exactly as before. The clone is not rendered. */
.logo-reel-track { display: contents; }

/* One static, centred row — no marquee. */
.logo-reel-row {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 50px;
}

.logo-reel-row img {
  height: 71px;
  width: auto;
  /* Any coloured SVG -> flat gray. brightness(0) crushes to black, invert()
     lifts it back up to gray — invert(0.55) ≈ mid-gray. */
  /* + a soft white bloom so the logos lift off the weave behind them. */
  /* Bloom kept tight: a wide 6px halo reads as a soft/blurry mark against the
     dark weave, especially on wordmarks with fine strokes. */
  filter: brightness(0) invert(1) drop-shadow(0 0 2.5px rgba(255, 255, 255, 0.22));
  opacity: 1;
  transition: filter 0.3s ease;
}
/* The mobile marquee clone: hidden everywhere except the ≤620px block. Must come
   AFTER .logo-reel-row (same specificity, later wins). */
.logo-reel-row--clone { display: none; }

.logo-reel-row img:hover { filter: brightness(0) invert(1) drop-shadow(0 0 2.5px rgba(255, 255, 255, 0.22)); }

/* ============================================================================
   Integrations fold (fold 2, Figma node 89:1913)
   One fold, two scroll phases. A single position:sticky visual layer is held
   across two stacked 100vh phase blocks (same sticky-across-two-folds trick as
   .scene-compliance / .hero-pattern-wrap). Phase 1 -> Phase 2 is a single class
   toggle (.is-phase-2 on .integrations-sticky, set by script.js) that CSS
   animates: the copy highlight swaps and the logo marquee reveals + runs.
   ========================================================================== */

.scene-integrations {
  /* Intentionally NOT position:relative. The phase blocks below must keep a
     document-relative offsetTop for initSectionScroller's nearest-section math;
     a positioned ancestor here would make their offsetTop scene-relative. The
     sticky child still works — sticky is contained by its parent regardless. */
  background: var(--page-bg);
}

/* The persistent visual, pinned while you scroll through the phase blocks. It
   sits in NORMAL flow (its own 100vh screen at the top of the scene) and pins
   via position:sticky; when the scene's bottom is reached it releases and
   scrolls up and off like any section — THEN the operator fold follows in clean flow.
   (Earlier this used margin-bottom:-100vh to zero its footprint, which extended
   the pin range right up to the next fold's edge so it climbed OVER a frozen fold-2.
   That overlay trick — and its exit-spacer/opacity-fade band-aids — are gone.)
   overflow:hidden clips the rotated weave LOCALLY — never overflow-x on
   html/body (see the ban at the top of this file). */
.integrations-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  /* NOT overflow:hidden. The weave box deliberately overhangs this fold by
     --int-weave-overhang top and bottom, and those permanently-faded ends are
     MEANT to be seen — they bleed into fold-3 above and the operator fold below so the
     pattern blends into the background instead of ending on an edge. Clipping
     here would eat them. The copy phases (which translate ±82vh and DO need
     clipping) are contained by .integrations-inner's overflow:hidden instead.
     z-index 2 lifts those ends above .fold-compliance and .fold-agents, both of
     which are position:relative; z-index:1 over an opaque #1F1F1F background and
     would otherwise paint straight over them. Still below the nav (z-index 10). */
  z-index: 2;
}

/* Empty spacers: scroll distance + snap points + the phase-2 trigger. The sticky
   now takes its own 100vh flow screen (phase-1 dwell), so phase block 1 only
   needs a short lead before phase block 2 flips to phase-2 (overridden below). */
.integrations-phase-block {
  min-height: 100vh;
  scroll-snap-align: start;
}
/* Phase-1 block = the 60vh entry dwell (weave fills the screen while phase-1
   holds centred — see D0 in initFoldIntegrations) + the original 40vh lead-in.
   Phase-2 block stays a full viewport so phase-2 reads before the sticky
   scrolls off. */
.integrations-phase-block[data-phase="1"] { min-height: 100vh; }
/* Phase-3 block is SHORTER than a screen on purpose: Layer 3's rise ends 70 of
   these 85 units in, and everything after it (erosion, diamond flight, motif
   draw-in) plays on its own clock — so a full 100vh left ~30vh of scroll where
   nothing happened. 85 leaves a 15vh dwell to read the parked copy. Mirrored by
   D2 in initFoldIntegrations. */
.integrations-phase-block[data-phase="3"] { min-height: 85vh; }

/* Content grid: [copy] [ open right space where the weave + logos show ]. */
.integrations-inner {
  position: relative;
  z-index: 1;                 /* above .int-weave, which is a plain (unlayered) sibling */
  /* Took over the clipping .integrations-sticky used to do (see the note there):
     keeps the ±82vh copy phases inside the fold now that the sticky lets the
     weave's faded ends overhang. */
  overflow: hidden;
  height: 100%;
  display: grid;
  grid-template-columns: minmax(280px, 32%) 1fr;
  align-items: center;
  column-gap: clamp(28px, 5vw, 80px);
  padding: 0 var(--fold-inset);
}

/* ---- LEFT: two-phase copy -------------------------------------------------- */
/* Both phases are stacked, full-height, vertically-centred LAYERS that occupy the
   SAME centre. script.js (initFoldIntegrations) drives a scroll-scrubbed vertical
   swap: phase 1 starts centred, then translates up and off while phase 2 rises
   from below into the centre. GSAP owns their transforms (y) — don't set a CSS
   transform on .int-phase or it fights the tween. */
.int-copy {
  grid-column: 1;
  position: relative;
  align-self: stretch;   /* fill the grid row (align-items:center would shrink it) */
  height: 100%;
  text-align: left;
}

.int-phase {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;   /* content sits at viewport centre; y-translate moves it */
  will-change: transform;
}

/* Small label above each phase title ("Layer 1 — The Brain"). Sized well below the
   title so it reads as a kicker, not a second heading, and dimmer than the subtitle
   so the title stays the loudest thing in the column. */
.int-eyebrow {
  margin: 0 0 8px;
  font-family: var(--font-ui);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-muted);
  transition: color 0.45s ease;
}

.int-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-medium);   /* Roboto Medium — the heaviest loaded Roboto weight */
  /* 2.1vw hits 40px at the 1900px width this was tuned at; the 40px cap holds
     it there on wider screens. */
  font-size: clamp(22px, 2.1vw, 40px);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  transition: color 0.45s ease;
}

.int-subtitle {
  margin: 24px 0 0;
  max-width: 360px;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-wide);
  transition: color 0.45s ease;
}

/* Layer 1's copy is capped a touch narrower than the shared 360px — measured by
   eye so its shortened blurb breaks where it should. Layers 2/3 keep 360. */
.int-phase--1 .int-subtitle { max-width: 336px; }

/* Both phases render bright — they don't stack-dim any more; the swap is now a
   vertical translate (phase 1 leaves the screen as phase 2 arrives), so only one
   is near the centre at a time. Titles white, subtitles the hero-subcopy grey. */
.int-title { color: var(--ink); }
.int-subtitle { color: var(--ink-body); }
/* Phase 2 waits below the fold until GSAP lifts it in (fallback if JS is slow). */
.int-phase--2 { transform: translateY(82vh); }

/* ---- Phase 3: Layer 3 / Agentic Workers ------------------------------------ */
/* A plain eyebrow + title + description block, behaving exactly like the other
   phases: GSAP scrubs it up from below, through the centre, and off — the same
   off()/travel() swap geometry as phases 1-2 (see initFoldIntegrations). */
.int-layer3 {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;   /* content sits at viewport centre; y-translate moves it */
  transform: translateY(82vh);   /* waits below (same fallback as .int-phase--2) */
  will-change: transform;
}

/* ---- Logo marks nested in the weave voids ---------------------------------- */
/* An overlay pinned to the weave box; JS drops a .int-logo-mark into each chosen
   void (where four diamond blocks meet). Sits above the weave strokes, below the
   copy column. */
.int-logos {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;                 /* above .int-weave-cells strokes */
}

.int-logo-mark {
  position: absolute;
  /* left/top set inline by JS (box-relative px, already zoom-adjusted). */
  transform: translate(-50%, -50%) scale(0.82);
  width: var(--int-logo-size, 30px);
  height: var(--int-logo-size, 30px);
  opacity: 0;                 /* hidden until phase 2 AND .is-on */
  /* Both the first reveal and the perpetual swap-in/swap-out ride this one
     transition, so it has to feel quick — 0.18s reads as "a mark popped in",
     0.6s+ read as a slow crossfade. Raise for a dreamier shuffle. */
  transition: opacity 0.18s ease, transform 0.18s cubic-bezier(0.4, 0, 0.2, 1);
}

.int-logo-mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* Marks are the full-colour brand SVGs (images/logos-real/). A gentle lift +
     saturation bump keeps them legible on #1F1F1F without washing the colour out —
     down toward brightness(1)/saturate(1) for a flatter, truer set. */
  filter: brightness(1.2) saturate(1.2);
}

/* Phase 2: fade in the marks that are currently ON. Only a subset of the placed
   slots is on at any moment — cycleWeaveLogos in script.js perpetually turns some
   off and relights them at a different free slot, so the field keeps shuffling. */
.is-phase-2 .int-logo-mark.is-on {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

/* ---- RIGHT: static dimmed weave ------------------------------------------- */
/* Box that holds the weave and clips it locally (never overflow-x on html/body).
   No gradient/mask edge — the left edge is the ERODING one, animated per-stroke
   in script.js (buildWeaveErosion). */
.int-weave {
  position: absolute;
  /* Left edge at 39%, right pinned to the screen edge so the pattern stays flush
     right (no gap). Widening the box rebuilds the weave, but the edge logic is
     deterministic + neighbour-based, so it re-forms a correct edge.svg-style ending
     at the new width — it does NOT get "lost". The serrated ending's own position is
     --int-weave-edge-inset, px from THIS left edge, so it moves in lockstep with the
     box and stays in register with the left feather below. */
  /* The box also OVERHANGS the sticky by --int-weave-overhang above and below. Those
     overhangs are EXTRA pattern, not a crop: the vertical mask fade is exactly that
     tall, so each end is permanently faded and bleeds out      and the operator fold below (nothing clips it — see the note on .integrations-sticky). JS
     re-derives the logo band from the box height, so the marks stay in the fold. */
  left: 39%;
  right: 0;
  top: calc(-1 * var(--int-weave-overhang));
  height: calc(100vh + 2 * var(--int-weave-overhang));
  overflow: hidden;
  pointer-events: none;
}

/* Mask wrapper around the pattern SVG only — NOT on .int-weave itself, so the
   sibling .int-logos marks render at full opacity regardless of where they sit
   in the left feather ramp. */
.int-weave-fade {
  position: absolute;
  inset: 0;
  /* Two gradients intersected: the vertical fade (top/bottom, in the overhangs) and
     the left-edge feather into the copy column; right stays flush. The feather runs
     the full ramp from the box edge with no flat transparent hold, so the pattern
     emerges gradually rather than snapping on. --int-weave-feather is the knob. */
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0, #000 var(--int-weave-overhang),
                    #000 calc(100% - var(--int-weave-overhang)), transparent 100%),
    linear-gradient(to right, transparent 0, #000 var(--int-weave-feather));
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to bottom, transparent 0, #000 var(--int-weave-overhang),
                    #000 calc(100% - var(--int-weave-overhang)), transparent 100%),
    linear-gradient(to right, transparent 0, #000 var(--int-weave-feather));
  mask-composite: intersect;
}

/* Real SVG grid (like the hero's buildPatternGrid), NOT a CSS mask — the left
   band's individual <path> strokes undraw/redraw in a loop, which can only be
   done on real strokes. Sized + centred in JS; the -45deg is baked per-cell. */
.int-weave-svg {
  position: absolute;
  left: 50%;
  top: 50%;
  /* scale() zooms the whole field in (larger motif) about the box centre. It's a
     pure compositor transform on the settled SVG — JS never writes this transform
     and all edge/erosion geometry is computed in unscaled coords, so the pattern
     stays internally correct and just renders bigger. */
  transform: translate(-50%, -50%) scale(1.18);
  overflow: visible;
}

/* Resting dim floor, matching the hero (PATTERN_DIM). */
.int-weave-cells path {
  stroke-opacity: 0.16;
}

/* ---- Unified pattern: the hero lattice, once morphed into the fold-2 weave ----
   The hero field rests as OPAQUE pre-mixed greys (#565656 / #454545). When the
   hero->fold-2 morph completes, the scene carries .is-woven and the SAME grid
   flips to the fold-2 weave's white-stroke model — plain white at a low
   stroke-opacity — so decorateWeaveEdge's per-stroke WEAVE_DIM writes compose on
   top exactly as they do on .int-weave-cells. Transitioned so the dim shift eases
   in with the morph rather than snapping. */
.scene-compliance.is-woven .hero-pattern-cells path {
  /* Fold-2 keeps the hero field's exact tone and brightness (was stroke:#fff at 0.168,
     which flashed white then read very dark). No recolor, hero opacity — decorateWeaveEdge
     re-asserts PATTERN_DIM per stroke on complete. */
  stroke-opacity: 1;       /* match the hero front pattern (its dimness is the stroke COLOR, not opacity) */
  /* NO transition on stroke-opacity (2026-08-05). There is no dim shift left to ease —
     fold-2 keeps the hero tone — but a 1s ease made every per-stroke opacity write FADE.
     On a fast scroll past layer 1 the morph snaps its dropped left-edge strokes to
     dashoffset 0 + stroke-opacity 0 in one frame: the dash restored the ink instantly
     while the opacity took a second to fall, so the whole left of the field flashed
     back in and then faded. Drops must vanish on the frame they're dropped. */
}

/* ---- Reduced motion: settled static end-state ----------------------------- */
/* Both phases open + legible, logo marks visible, no pin. */
/* Reduced motion has no scroll swap: show the END state — phase 2 centred, phase 1
   parked above and out of the way. */
.integrations-sticky.is-static .int-phase { opacity: 1; }
.integrations-sticky.is-static .int-phase--1 { transform: translateY(-82vh); }
.integrations-sticky.is-static .int-phase--2 { transform: translateY(0); }
.integrations-sticky.is-static .int-title { color: var(--ink); }
.integrations-sticky.is-static .int-subtitle { color: var(--ink-body); }
.integrations-sticky.is-static .int-logo-mark.is-on {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
/* With phase 3 merged in, reduced motion unrolls the WHOLE scene into normal
   flow (class on the section): no pin, all three layers read top-to-bottom;
   spacers are dropped and the settled weave stays as the right-side texture. */
.scene-integrations.is-static .integrations-sticky { position: static; height: auto; }
.scene-integrations.is-static .integrations-inner {
  height: auto;
  overflow: visible;
  align-items: start;
  padding-top: 12vh;
  padding-bottom: 12vh;
}
.scene-integrations.is-static .int-copy { height: auto; }
.scene-integrations.is-static .int-phase,
.scene-integrations.is-static .int-layer3 {
  position: static;
  transform: none !important;
  padding-top: 0;
  margin-top: 8vh;
}
.scene-integrations.is-static .int-phase--1 { margin-top: 0; }
.scene-integrations.is-static .integrations-phase-block { display: none; }
.scene-integrations.is-static .int-weave { top: 0; height: 100%; }

/* ---- Layer 3 right side: the six agentic workers ------------------------- */
/* Same right-half footprint as .int-weave (but no overhang — the cards live
   inside the viewport). The scrubbed fold timeline staggers .int-agent cards in
   during segment B while six of the weave's own diamond paths fly into them.
   Cards start hidden in CSS so nothing flashes before JS wires the timeline. */
.int-agents {
  position: absolute;
  top: 0;
  left: 39%;
  right: 0;
  height: 100vh;
  z-index: 1;                 /* above the weave strokes, like .integrations-inner */
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* auto rows + align-content:center => the block is only as tall as its content
     and sits centred in the 100vh column, rather than being stretched across it. */
  grid-template-rows: repeat(2, auto);
  align-content: center;
  column-gap: 48px;           /* tuned by eye via the knobs tooltip — keep as flat px */
  row-gap: 85px;
  /* top pad only: with align-content:center it nudges the block clear of the
     fixed header without breaking the vertical centring. */
  padding: 30px clamp(24px, 4vw, 72px) 0;
  pointer-events: none;       /* decorative layer; the copy column keeps focus */
}

/* Each card is a 3-row grid whose FIRST row is a fixed-height art slot sized to
   the TALLEST motif (--art-kmax). The art centres inside that slot, so every
   diamond in a row sits on the same line (diamond = art centre) with the rest
   of the shape following around it; and because the slot height is shared, the
   names all start on the same line too — the shape->text gap varies per motif
   instead. */
.int-agent {
  display: grid;
  grid-template-rows: calc(var(--art-d) * var(--art-kmax)) auto 1fr;
  justify-items: center;
  align-content: start;
  text-align: center;
  opacity: 0;                 /* revealed by the scrubbed timeline (autoAlpha) */
}

/* The agent motif art — white-stroke SVGs on the #1F1F1F field.
   SIZED BY THE DIAMOND, NOT BY THE ARTBOARD. Every motif's central diamond is
   the same 57.98 user units, but the six artboards differ (viewBox 115 -> 172),
   so a shared height would render six different diamonds. Instead `--art-d` is
   the diamond's rendered size — shared by all six, and the size the transported
   weave diamonds have to match — and each motif's `--art-k` (its own viewBox
   height / 57.98) scales the artboard up around it. The shapes therefore come
   out DELIBERATELY different sizes; the diamonds inside them line up. */
.int-agents {
  /* The ONE size knob. Everything else here is a multiplier of it, so the six
     motifs keep their tuned relative sizes at every width. Fluid: 57px at the
     1512 design frame it was tuned at, scaled down ~20% for laptop widths
     (3.0vw = 45px @1512), bounded so it can't collapse on a narrow laptop or
     balloon on a wide monitor. */
  --art-d: clamp(38px, 3.0vw, 56px);
  --art-kmax: 3.45;           /* >= tallest --art-k (2.9612); extra = breathing room in the slot */
}
.int-agent--portal      { --art-k: 2.7417; }
.int-agent--guardians   { --art-k: 1.9857; }
.int-agent--sixth-sense { --art-k: 2.9612; }
.int-agent--atlas       { --art-k: 2.4735; }
.int-agent--vortex      { --art-k: 2.9612; }
.int-agent--neigh-gi    { --art-k: 2.4735; }

/* Per-model tint. One hue per agent, taken 90% of the way from white toward a
   12-step hue wheel (chosen live in the compare harness) — so they read as
   distinct identities but stay pale enough to sit in the monochrome house
   palette. The motif's markup carries stroke="white" as a PRESENTATION
   ATTRIBUTE, which any CSS rule outranks, so this recolours it without
   touching the SVGs. The flying weave diamond inherits it for free:
   measureTransport reads the target .agent-diamond's COMPUTED stroke as its
   landing colour (script.js:1937). */
.int-agent--portal      { --agent-tint: #7cb7dc; }   /* sky */
.int-agent--guardians   { --agent-tint: #dc7c7c; }   /* red */
.int-agent--sixth-sense { --agent-tint: #dcbc7c; }   /* amber */
.int-agent--atlas       { --agent-tint: #7cdc85; }   /* green */
.int-agent--vortex      { --agent-tint: #9c7cdc; }   /* violet */
.int-agent--neigh-gi    { --agent-tint: #dc7cd5; }   /* magenta */

.int-agent-art path,
.int-agent-art rect,
.int-agent-art line,
.int-agent-art polyline,
.int-agent-art circle {
  stroke: var(--agent-tint, #ffffff);
}

.int-agent-art {
  height: calc(var(--art-d) * var(--art-k, 2.6));
  width: auto;
  align-self: center;         /* centre in the shared slot => diamonds align */
}

/* Name/desc mirror the .agent-name/.agent-desc scale, one notch smaller so six
   cards fit one screen. */
.int-agent-name {
  margin: 14px 0 6px;         /* fixed offset below the shared slot — same line for all */
  font-family: var(--font-display);
  font-weight: var(--fw-regular);
  font-size: clamp(15px, 1.25vw, 19px);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-wide);
  color: var(--ink);
}

.int-agent-desc {
  margin: 0;
  max-width: 280px;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-card);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  color: var(--ink-body);
}

/* Reduced motion: the agent grid is part of the settled end-state — unrolled
   into normal flow under the Layer-3 copy, all cards visible, no transport. */
.scene-integrations.is-static .int-agents {
  position: static;
  height: auto;
  padding: 8vh 0 0;
  pointer-events: auto;
}
.scene-integrations.is-static .int-agent { opacity: 1; }

/* ==========================================================================
   Agents list (mobile only)
   Layer 3 now lives inside the fold-2 sticky scene on desktop (.int-layer3 /
   above). This section is the ≤620px fallback: a plain stacked list of
   the same six agents. Hidden above 620px (see the mobile block at the bottom
   of this file, which un-hides it).
   ========================================================================== */
.fold-agents {
  display: none;
  position: relative;
  z-index: 1;
  background: var(--page-bg);
  padding: calc(clamp(80px, 14vh, 168px) + var(--int-weave-overhang))
           var(--fold-inset) clamp(80px, 14vh, 168px);
}

.agents-inner {
  max-width: 1360px;
  margin: 0 auto;
}

/* Kicker above the section heading — mirrors .int-eyebrow in fold-2. */
.agents-eyebrow {
  margin: 0 0 12px;
  font-family: var(--font-ui);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  text-align: center;
  color: var(--ink-muted);
}

/* Section subtitle — mirrors .int-subtitle, centered under the heading. */
.agents-subtitle {
  margin: 20px auto clamp(48px, 8vh, 104px);
  max-width: 720px;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-wide);
  text-align: center;
  color: var(--ink-body);
}

/* Section heading — centered per Figma (node 103:13778). */
.agents-heading {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-eyebrow);
  text-align: center;
  color: var(--ink);
}

/* Plain stacked list: small motif thumb + name/desc. */
.agents-list {
  display: flex;
  flex-direction: column;
  row-gap: clamp(32px, 5vh, 48px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.agent-row {
  display: flex;
  align-items: flex-start;
  gap: 18px;
  text-align: left;
  /* A card, not a bare row — six white-on-black rows in a column read as one
     undifferentiated block on a phone. The hairline + wash give each agent its
     own object, and the tint below gives it its identity. */
  padding: 18px 18px 20px;
  border: 1px solid var(--rule-soft);
  border-radius: var(--r-lg);
  background: rgba(255, 255, 255, 0.028);
}

/* Per-model tint — the SAME six hues the desktop grid uses (.int-agent--*),
   so the mobile list isn't a monochrome downgrade of the same content. The
   motif SVGs carry stroke="white" as a presentation attribute, which any CSS
   rule outranks; script.js inlines the <img> so these can reach the strokes. */
/* --art-k is the SAME per-motif ratio the desktop grid uses (see .int-agent--*
   above): each artboard's viewBox height / 57.98, the shared diamond size. The
   thumb is sized as --art-d * --art-k, so the artboards come out deliberately
   different sizes and the DIAMONDS inside them all render identical. Sizing by a
   flat height instead (what this used to do) gives six different diamonds. */
.agent-row--portal      { --agent-tint: #7cb7dc; --art-k: 2.7417; }   /* sky */
.agent-row--guardians   { --agent-tint: #dc7c7c; --art-k: 1.9857; }   /* red */
.agent-row--sixth-sense { --agent-tint: #dcbc7c; --art-k: 2.9612; }   /* amber */
.agent-row--atlas       { --agent-tint: #7cdc85; --art-k: 2.4735; }   /* green */
.agent-row--vortex      { --agent-tint: #9c7cdc; --art-k: 2.9612; }   /* violet */
.agent-row--neigh-gi    { --agent-tint: #dc7cd5; --art-k: 2.4735; }   /* magenta */

/* Motif thumb beside each row (the same agent art the desktop grid uses).
   Both selectors: it ships as an <img> and becomes an inline <svg> once
   initAgentsMobile() swaps it (so the strokes can be tinted and drawn). */
.agent-thumb {
  flex-shrink: 0;
  width: 54px;
  height: 54px;
  object-fit: contain;
  /* A soft halo in the agent's own colour — the same "lit vector" language as
     fold 3's emblem. Knob: the 6px blur radius. */
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--agent-tint, #fff) 55%, transparent));
}
svg.agent-thumb path,
svg.agent-thumb rect,
svg.agent-thumb line,
svg.agent-thumb polyline,
svg.agent-thumb circle {
  stroke: var(--agent-tint, #ffffff);
}

/* Agent name. */
.agent-name {
  margin: 0 0 8px;
  font-family: var(--font-display);
  font-weight: var(--fw-regular);
  font-size: clamp(18px, 1.55vw, 23px);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-wide);
  color: var(--ink);
}

/* Agent description — centered, constrained width per Figma (~282px). */
.agent-desc {
  margin: 0;
  max-width: 300px;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body-wide);
  color: var(--ink-body);
}

/* ==========================================================================
   Operator fold — fold 4 v5 (Figma node 234:425). Static, monochrome, CENTRED:
   eyebrow, serif headline, intro paragraph, the fold-4 ornament, then two equal
   outlined role cards — each card is a link to pricing with its own "Explore
   Plans" box. v4 (234:402) was the same content left-anchored; v2 (160:50,
   copy column + logo mark) is archived in _archive/.
   ========================================================================== */
.fold-operator {
  position: relative;
  z-index: 1;
  /* TRANSPARENT, not --page-bg: fold-3's chapter-3 row is a WIDE emblem that
     overhangs the bottom of its own section, and an opaque fill here (the same
     colour as body's, so it bought nothing) sliced that overhang off. */
  background: transparent;
  padding: clamp(80px, 12vh, 160px) var(--fold-inset) clamp(80px, 14vh, 168px);
  text-align: center;
}

/* Small uppercase eyebrow above the headline — muted mono grey, wide tracked. */
.operator-eyebrow {
  margin: 0 0 16px;
  font-family: var(--font-ui);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-cozy);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* Serif headline — one line at desktop; the max-width lets it wrap gracefully. */
.operator-heading {
  margin: 0 auto;
  max-width: 660px;
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-wide);
  color: var(--ink);
}

/* Two equal role cards side by side, centred as a pair. Contents are
   LEFT-aligned inside each card (the heading pair above stays centred). */
/* Intro paragraph between the headline and the cards. */
.operator-intro {
  margin: clamp(16px, 2.2vh, 24px) auto 0;
  max-width: 460px;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-body);
  line-height: var(--lh-loose);
  letter-spacing: var(--ls-body-wide);
  color: var(--ink-body);
}

/* The fold-4 ornament — a single decorative weave motif between the copy and
   the cards. The SVG is INLINE in index.html (see the note there) so each
   stroke is addressable: at rest only the central diamond (.op-orn-core) is
   drawn, and the remaining 36 strokes draw themselves on when either role card
   is hovered. Sized by width alone; aspect-ratio keeps the 686x203 proportion,
   so there is no separate height to keep in step. */
.operator-ornament {
  display: block;
  /* clamp: full 571px only on wide screens; 36vw shrinks it sooner on small
     desktops (44vw kept it near the cap down to ~1300px, which read too big);
     320px floor stops it collapsing on narrow windows. */
  width: clamp(320px, 36vw, 571px);
  aspect-ratio: 686 / 203;
  height: auto;
  margin: clamp(30px, 5vh, 54px) auto 0;
  overflow: visible;
  color: var(--ink);   /* idle colour — the accents below only apply on card hover */
  opacity: 1;
  transition: color 0.35s ease;
}
.operator-ornament path {
  stroke: currentColor;
  stroke-width: 3.75;   /* tuned by eye */
  vector-effect: non-scaling-stroke;
}
/* pathLength="1" on every path means dasharray/offset 1 = "the whole stroke",
   so this one rule holds every outer stroke fully undrawn. stroke-opacity is a
   BACKSTOP for renderers that round the dash and leak a dot at rest — it is
   switched INSTANTLY (0s) at the moment the stroke's own draw starts, and on
   the way out only AFTER its undraw finishes, so it never masks the animation.
   --d (set per path in the markup) is that stroke's distance-from-centre delay. */
.op-orn-rest {
  --d: 0s;
  /* --orn-len is that stroke's real length, written by initOperatorOrnament()
     in script.js. The pathLength="1" fallback below only bites with JS off. */
  --orn-len: 1;
  /* --orn-dur is written by applyOperatorOrnamentDraw() — the same draw time for
     every stroke, whatever its length. */
  --orn-dur: 0.5s;
  /* --orn-dash is the same length; both are written by initOperatorOrnament(). */
  --orn-dash: var(--orn-len);
  stroke-dasharray: var(--orn-dash);
  stroke-dashoffset: var(--orn-len);
  stroke-opacity: 0;
  transition:
    stroke-dashoffset var(--orn-dur) ease var(--d),
    stroke-opacity 0s linear calc(var(--d) + var(--orn-dur));
}
/* Strokes chosen by eye (in the pick harness) to stay drawn at rest alongside
   the central diamond — they never undraw, so hover only ADDS the rest. */
.op-orn-keep {
  stroke-dashoffset: 0;
  stroke-opacity: 1;
}
/* The trigger lives on the CARDS, which come AFTER the ornament in the DOM —
   a sibling combinator can only look forwards, so the hover is read on the
   shared parent with :has(). */
.fold-operator:has(.operator-role:hover) .op-orn-rest {
  stroke-dashoffset: 0;
  stroke-opacity: 1;
  transition:
    stroke-dashoffset var(--orn-dur) ease var(--d),
    stroke-opacity 0s linear var(--d);
}
@media (prefers-reduced-motion: reduce) {
  .op-orn-rest { transition: none; }
}

.operator-roles {
  display: grid;
  /* Card width is a single knob — tuned by eye via the width harness. */
  --operator-card-w: 396px;
  grid-template-columns: repeat(2, minmax(0, var(--operator-card-w)));
  justify-content: center;
  gap: 80px;
  margin: clamp(44px, 7vh, 84px) auto 0;
  text-align: left;
}

 /* The whole card is an <a>. Hover lifts the border and the inner button box
    together, so the card reads as one target. */
.operator-role {
  display: flex;
  flex-direction: column;
  padding: 39px 47px;   /* tuned by eye via the card harness — exact px on purpose */
  border: 1px solid var(--rule-role);
  border-radius: var(--r-lg);
  text-decoration: none;
  transition: border-color 0.25s ease, background-color 0.25s ease;
}

/* Each card owns an ACCENT: it tints that card's diamond mark, and — while the
   card is hovered — the whole fold ornament above takes the same colour, so the
   motif reads as belonging to the card you're pointing at. One knob per card. */
.operator-role--vciso { --role-accent: #5d6de5; }   /* tuned by eye */
.operator-role--fde   { --role-accent: #e55d5d; }

.fold-operator:has(.operator-role--vciso:hover) .operator-ornament {
  color: #5d6de5;
}
.fold-operator:has(.operator-role--fde:hover) .operator-ornament {
  color: #e55d5d;
}

/* Hover: the card does NOT fill — only its outline answers, taking that card's
   own accent (the colour of its diamond mark and of the fold ornament above,
   which lights up in the same hue). The face stays transparent so the accent
   is the whole event. */
.operator-role:hover {
  border-color: var(--role-accent);
}

.operator-role-title {
  /* Titles are held to ONE line (white-space: nowrap) — the card width above is
     set to fit the longest of them, "FDE, your vGRC partner". If a longer title
     is ever added, widen --operator-card-w rather than letting it wrap. */
  display: flex; align-items: center; gap: 12px;
  white-space: nowrap;
  margin: 0 0 clamp(18px, 2vh, 26px);
  font-family: var(--font-ui);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-title);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-wide);
  color: var(--ink);
}

/* The card's diamond mark — the same motif as the fold ornament's core, sized
   off the title's own font-size (em) so it tracks the type at every breakpoint.
   Outline only, matching the hairline language of the card and the button. */
.operator-role-mark {
  flex: none;
  color: var(--role-accent);
  width: 1.15em; height: 1.15em;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linejoin: round;
}
.operator-role-body {
  margin: 0 0 auto;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body-wide);
  color: var(--ink-body);
}

/* Per-card link — a hairline outlined button, full card width, label centred.
   The body above takes `margin-bottom:auto`, so the button is pinned to the
   bottom of the card and both buttons line up even when the copy runs to
   different lengths. */
.operator-link {
  display: block;
  box-sizing: border-box;
  /* stretch, not start: the card is a flex column, so `start` on the cross axis
     shrank the pill to its label and left it short of the body text above it. */
  align-self: stretch;
  margin-top: clamp(26px, 3.5vh, 44px);
  padding: 12px 19px;
  border: 1px solid var(--rule-ghost);
  border-radius: var(--r-md);
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-body);
  line-height: var(--lh-cozy);
  letter-spacing: var(--ls-wide);
  /* Rest + hover mirror .nav-demo's label/outline (see the nav block above): a
     step below white on a 42% outline, going pure white on both under the
     cursor. NO fill — like the card itself, the pill answers with its stroke
     only, so nothing on this fold blocks in on hover. */
  color: var(--ink-action);
  text-align: center;
  text-decoration: none;
  transition: border-color 0.25s ease, color 0.25s ease;
}

.operator-role:hover .operator-link {
  color: var(--ink);
  border-color: var(--ink);
}

/* ==========================================================================
   Contact fold (the contact page — "book a strategy call")
   Static two-column panel in the layer 3 mold: left = eyebrow + serif display
   headline + 3-step timeline + mailto; right = the form card. Strictly the
   site's monochrome vocabulary (#1F1F1F field, hairline #575757 borders,
   white/#e0dfdf/45%-white text). The settled state is the CSS default — the
   scroll reveal hides things via gsap.set in JS only, so no-JS and reduced
   motion render the finished section untouched.
   ========================================================================== */
/* On its own page (contact.html, body.page-contact) the single centred stack is
   centred in the VIEWPORT too — min-height 100svh + flex centring. svh (small
   viewport height) not vh: on mobile vh is the URL-bar-collapsed height, which
   overflows while the bar is showing. The fixed 70px nav overlays the top and
   takes no layout space, so the top padding adds it back; the block still grows
   past the viewport when the content is taller (align-items would clip it,
   which is why the padding does the work instead of place-content). */
.page-contact .fold-contact {
  /* Every vertical step below is vh-aware so the whole page fits ONE screen
     at any height — see the note above .contact-inner. */
  --contact-pad-y: clamp(10px, 1.5vh, 64px);
  min-height: 100svh;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-top: calc(var(--nav-h) + var(--contact-pad-y));
  padding-bottom: clamp(14px, 2.4vh, 80px);
}
.page-contact .contact-inner { width: 100%; }

/* The page's flat base colour lives on the body; the fold is transparent so it
   shows through. (The old fixed BACK weave layer — .contact-back / its tile and
   drift keyframes — was removed by request; only the FRONT lattice remains.) */
.page-contact { background: var(--page-bg); }
.page-contact .fold-contact { background: transparent; }
/* No breathing gap before the footer HERE: the lattice lives inside the fold,
   so a margin would leave a bare strip of page-bg between the pattern and the
   footer's hairline. The fold runs edge to edge into the footer instead. */
.page-contact .site-footer { margin-top: 0; }

/* --- the card sits ABOVE the film grain ---------------------------------
   .grain-overlay is a fixed, screen-blended tile at z-index 9. The card must
   clear it, and it can't while its ancestors open stacking contexts at z-index
   1 — every z-index inside those is trapped below 9. So on THIS page the fold
   and its inner wrapper stay position:relative but drop to z-index:auto (no
   context), the lattice keeps its own z-index 0 behind them, and the card takes
   z-index 10. Masking a hole in the grain instead was tried and rejected: the
   hole's edge is visible as a seam over the dark card.
   The nav then has to outrank the card (both would be 10) or the card would
   paint over the bar once the page scrolls. */
.page-contact .fold-contact,
.page-contact .contact-inner { z-index: auto; }
.page-contact .site-nav { z-index: 20; }
.page-contact .contact-card { position: relative; z-index: 10; }

.fold-contact {
  position: relative;
  z-index: 1;
  background: var(--page-bg);
  overflow: hidden;   /* local clip for the oversized weave svg — NEVER on html/body */
  padding: clamp(80px, 14vh, 168px) var(--fold-inset);
}

.contact-inner {
  position: relative;
  z-index: 1;
  max-width: 1360px;
  margin: 0 auto;
  /* One centred column, stacked: copy → points → card. Column (not grid) so each
     child keeps its own natural width and align-items centres them as a group;
     the widths themselves are capped per child below. */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* copy → card: 48px at the 1000px-tall edit stage. Deliberately steeper than
     linear (16vh − 112px) so short screens give this gap up first and the whole
     stack keeps fitting the viewport. */
  row-gap: clamp(10px, 20vh - 138px, 62px);
}

/* --- the front lattice band --- */

/* Covers the WHOLE fold. Full strength down to the form card's top edge, then
   ramps to a faint residue (--band-tail) that carries on to the bottom of the
   section — the weave THINS OUT instead of disappearing (version B's "subtly
   continue"). Tuned by eye: the ramp now runs the WHOLE fold (start = end −
   ramp = 0%), so the weave thins continuously from the top instead of holding
   full strength to the card. Fixed percentages — no longer measured off the
   card. The mask is on THIS element, not .contact-hero-rotate — that square is
   several times the section's height and centred on it, so a gradient there
   would barely register inside the visible area. */
.contact-hero-bg {
  --band-end: 100%;   /* the fade lands at the footer, not before it */
  --band-ramp: 100%;  /* how far above --band-end the fade starts (→ 0%) */
  --band-tail: 0.11;  /* residual strength across the rest of the fold */
  /* The whole lattice sits a step back from the copy — tuned by eye. */
  opacity: 0.7;
  position: absolute;
  inset: 0;           /* top/right/bottom/left 0 — the full section */
  overflow: hidden;   /* local clip for the oversized rotated field */
  z-index: 0;         /* .contact-inner is z-index 1 */
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 0,
    #000 calc(var(--band-end) - var(--band-ramp)),
    rgba(0, 0, 0, var(--band-tail)) var(--band-end),
    rgba(0, 0, 0, var(--band-tail)) 100%);
  mask-image: linear-gradient(to bottom, #000 0,
    #000 calc(var(--band-end) - var(--band-ramp)),
    rgba(0, 0, 0, var(--band-tail)) var(--band-end),
    rgba(0, 0, 0, var(--band-tail)) 100%);
}
/* Centred oversized square — same idiom as .hero-pattern-rotate; the builder
   sets its px size from the band's diagonal. */
.contact-hero-rotate {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
.contact-hero-rotate .hero-pattern-svg { width: 100%; height: 100%; }
/* This page's weave now sits at roughly the hero's own strength (#36353d/#25252b). */
.contact-hero-rotate .hero-pattern-svg path,
.contact-hero-rotate .hero-pattern-svg line,
.contact-hero-rotate .hero-pattern-svg rect { stroke: #37363e; }
.contact-hero-rotate .hero-pattern-svg .pattern-diamond { stroke: #26262c; }

/* --- copy --- */

/* Manrope headline (switched from the hero's Roboto Serif by request). */
.contact-headline {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-h1);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight-lg);
  color: var(--ink);
  /* Same glyph-hugging dark halo as the hero (.hero-subcopy recipe) — five
     stacked black shadows at growing blur, no backing plate, so the type lifts
     off the lattice without a visible box. Here it can live on the element
     itself: unlike the hero headline there's no invert flip to crossfade, so
     nothing re-rasterises mid-animation. */
  text-shadow:
    0 2px 8px rgba(0, 0, 0, 0.62),
    0 4px 18px rgba(0, 0, 0, 0.62),
    0 8px 36px rgba(0, 0, 0, 0.62),
    0 16px 60px rgba(0, 0, 0, 0.62),
    0 28px 90px rgba(0, 0, 0, 0.55);
  /* em, not px — 1em is this element's OWN font-size, which is a clamp() that
     grows with the viewport. A px cap holds still while the type grows, so the
     line count creeps up on wide screens; an em cap scales with it and the
     headline stays 2 lines. ~17em ≈ 27 characters per line for this face. */
  max-width: 17.25em;
  margin-inline: auto;   /* the em cap is narrower than the column — centre it */
}

.contact-subhead {
  margin: clamp(10px, 2vh, 20px) 0 0;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--ink-body);
  /* Hero .hero-subcopy halo, verbatim. */
  text-shadow:
    0 1px 3px rgba(0, 0, 0, 0.62),
    0 2px 8px rgba(0, 0, 0, 0.62),
    0 4px 18px rgba(0, 0, 0, 0.62),
    0 8px 34px rgba(0, 0, 0, 0.62),
    0 16px 54px rgba(0, 0, 0, 0.55);
  max-width: 480px;
  margin-inline: auto;
}


/* --- right column: form card --- */

.contact-card {
  /* The card is the only child with a hard width — everything else centres
     around it. text-align resets to left: labels/fields read left-aligned. */
  width: 100%;
  max-width: 479px;   /* tuned by eye at 1636px */
  box-sizing: border-box;
  text-align: left;
  /* Back to a real card: a hairline ring plus a translucent dark fill, so the
     form sits ON something instead of floating on the weave. The fill is rgba
     (not a flat hex) so the lattice still reads faintly through it. */
  border: 1px solid var(--rule-soft);
  border-radius: var(--r-md);
  /* Dark halo: a tight shadow hugging the border plus a wide one bleeding into
     the lattice, so the card lifts off the weave instead of floating on it. */
  box-shadow:
    0 10px 24px 0 rgba(0, 0, 0, 0.45),
    0 10px 100px 20px rgba(0, 0, 0, 0.5);
  background: rgba(12, 11, 14, 0.47);
  /* 2px of backdrop blur — just enough to soften the lattice reading through
     the fill without turning the card into frosted glass. */
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  padding: clamp(16px, 4.6vh, 46px) clamp(18px, 2.57vw, 42px);
}


/* The form is a touch narrower than the card's text column, by request. The
   width lives on the FIELD (and on the name row), not on the input, so every
   input ends on exactly the same x as Last Name — which used to sit 4% further
   right because 92% of a half-column is not 92% of the whole. */
.contact-field { margin-bottom: clamp(9px, 6vh - 30px, 30px); width: 100%; }
/* First/Last side by side; each keeps its own .contact-field bottom margin. */
.contact-field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 18px;
  width: 100%;
}
/* Inside the row the field is already inside a full-width track — don't re-apply. */
.contact-field-row .contact-field { width: 100%; }

/* Field labels: title case, not the site's all-caps label voice (by request). */
.contact-label {
  display: block;
  margin-bottom: clamp(4px, 0.8vh, 8px);
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--fs-meta);
  letter-spacing: var(--ls-body);
  color: var(--ink-a80);
}

/* First form inputs on the site — authored in the house language: dark
   translucent fill, hairline border, white focus ring (matches the existing
   .btn/.nav-link :focus-visible pattern). */
.contact-input {
  width: 100%;   /* the narrowing lives on .contact-field — see above */
  box-sizing: border-box;
  /* Solid-ish fill so the field reads as a surface against the now-fill-less
     card (the weave shows through the card, not the inputs). */
  background: #17171a;
  border: 1px solid #3d3d3d;
  border-radius: var(--r-sm);
  /* Typed text matches its label exactly — same white at 0.82 — so the pair
     reads as one unit. The placeholder keeps its own dimmer grey below. */
  color: var(--ink-a80);
  font-family: var(--font-ui);
  font-size: var(--fs-body-s);
  padding: clamp(8px, 1.2vh, 12px) 14px;
  transition: border-color 0.3s ease;
}
/* Faint hint text — well below the label's 0.82 so it never reads as a value. */
.contact-input::placeholder { color: var(--ink-a28); }
/* NO offset outline ring on text fields — the detached white frame is heavy on
   a field this size. The BORDER brightening is the focus indicator instead, so
   focus is still visibly signalled (buttons keep their ring: they have no
   border state to lean on). */
.contact-input:focus { border-color: var(--rule-ghost); }
.contact-input:focus-visible { outline: none; }
/* Invalid = brighter white border, monochrome (no red) — the error text below
   the field carries the message. Toggled by initContactForm(). */
.contact-input[aria-invalid="true"] { border-color: rgba(255, 255, 255, 0.85); }

.contact-error {
  margin: 6px 0 0;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  color: var(--ink-a60);
}

/* Engagement picker — real radios, drawn as pills. The <input> is hidden from
   sight but stays in the accessibility tree and keeps arrow-key navigation;
   the <label> is the pill, and :checked styles it via the sibling selector. */
/* Engagement <select> — the .contact-input surface plus a hand-drawn caret
   (appearance:none strips the native arrow, so the SVG below replaces it). */
.contact-select {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  /* Room on the right for the caret, which is painted as a background image
     16px in from the field's right edge. */
  padding-right: 38px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='14' fill='none' stroke='%239a9a9f' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 5.5 5 2.5l3 3M2 8.5l3 3 3-3'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
}
/* The dropdown list itself is OS-drawn — only its colours are ours. */
.contact-select option { background: #17171a; color: var(--ink); }
/* Placeholder state: the select is optional, so :invalid never fires — :has()
   on the checked empty option is what dims "Please select" to placeholder grey. */
.contact-select:has(option[value=""]:checked) { color: var(--ink-a28); }
.contact-select option[value=""] { color: var(--ink-a45); }

/* Submit — composes .btn (pill geometry, type, shadow, focus ring) with the
   resting .btn-solid LOOK but NONE of its hover machinery: .btn-solid:hover
   drives the hero light-theme inversion (scale 1.4, :has() sibling shift,
   initHeroInversion binds to it), which must never fire down here. */
/* Solid white, like the hero's primary pill — but deliberately NOT .btn-solid,
   whose hover drives the hero light-theme inversion. */
/* Button ABOVE the mail-instead line — a column, so the CTA gets its own wide
   pill and the alternative reads as a second option below it. */
.contact-submit-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;   /* the button spans the field width */
  gap: 12px;
  /* Extra air above the button — the form's last field shouldn't butt into it. */
  margin-top: clamp(11px, 8.4vh - 42px, 42px);
  width: 100%;            /* same track as .contact-field */
}
.contact-alt {
  /* Sits under the fineprint, outside the card — .contact-inner's row-gap is
     a flex sibling in .contact-inner, so it must cancel that column's row-gap
     the same way the fineprint does. */
  margin: calc(8px - clamp(10px, 20vh - 138px, 62px)) 0 0;
  text-align: center;
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  line-height: var(--lh-body);
  color: var(--ink-a60);
}
.contact-alt a { color: var(--ink-a80); text-decoration: underline; }
.contact-alt a:hover { color: var(--ink); }

.contact-submit {
  flex: 0 0 auto;
  /* Full-width pill on its own row, matching the inputs' right edge. */
  width: 100%;
  justify-content: center;
  padding-top: clamp(7px, 1vh, 10px);
  padding-bottom: clamp(7px, 1vh, 10px);
  padding-left: 55px;
  padding-right: 46px;
  font-size: var(--fs-ui);
  cursor: pointer;
  /* Rests a step BELOW white and lights up to pure white on hover — the shared
     --white-cta-* pair, same as .nav-cta and .cta-button. It used to rest at #fff
     and darken to #e6e6e6, which reads as the button disabling itself under the
     cursor. See the token comment on :root. */
  background: var(--white-cta-rest);
  border: 0;
  color: var(--ink-on-light);
  font-weight: var(--fw-medium);
  transition: background-color 0.3s ease;
}
.contact-submit:hover { background: var(--white-cta-hover); }

.contact-fineprint {
  /* Lives outside the card now, as a sibling in .contact-inner's flex column —
     so its own top margin has to CANCEL most of that column's row-gap
     (clamp(10px, 20vh - 138px, 62px)) to sit close under the card. */
  margin: calc(14px - clamp(10px, 20vh - 138px, 62px)) 0 0;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-meta);
  color: var(--ink-a45);
  text-align: center;
}

/* --- success state (swapped in by initContactForm after a valid submit) --- */

.contact-success {
  text-align: center;
  padding: clamp(24px, 3vh, 40px) 0;
}
.contact-success-title {
  margin: 0 0 12px;
  font-family: var(--font-ui);
  font-weight: var(--fw-medium);
  font-size: var(--fs-h3);
  color: var(--ink);
}
.contact-success-title:focus { outline: none; }  /* programmatic focus target */
.contact-success-desc {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-body-s);
  line-height: var(--lh-body);
  color: var(--ink-body);
  max-width: 320px;
  margin-inline: auto;
}

@media (max-width: 960px) {
  /* On its own page the contact fold is nothing BUT this content, so the shared
     20px --fold-inset reads as no margin at all next to a 44px headline. Give it
     its own wider side inset (the vertical pads are set separately above). */
  .page-contact .fold-contact {
    padding-left: 28px;
    padding-right: 28px;
  }

  /* Already one column — just tighten the stack. */
  /* Already one column — just tighten the stack. */
  .contact-inner { row-gap: 56px; }
}

@media (max-width: 620px) {
  /* On a phone the desktop's "fit one screen" contract is dropped: the stack
     is taller than the viewport anyway, and squeezing it starved the gap under
     the nav. So no viewport centring — the page starts under the bar with real
     air and simply scrolls.
     Knobs: the 48px top pad (air between the nav and the headline) and the
     32px row-gap (copy → logos → card). */
  .page-contact .fold-contact {
    min-height: 0;
    display: block;
    padding-top: calc(var(--nav-h) + 48px);
    padding-bottom: 48px;
  }
  .contact-inner { row-gap: 32px; }
  /* Extra air between the top section (headline/points/logos) and the card
     itself, beyond the shared row-gap. */
  .contact-card { margin-top: 16px; }

  /* The card's inner spacing is vh-derived on desktop purely to make the stack
     fit one screen (`6vh - 30px` etc.). That contract is gone here, so the
     gaps go back to flat, comfortable px. Knobs: 24px between fields, 8px
     label → input, 28/22px card padding. */
  .contact-card { padding: 28px 22px; }
  .contact-field { margin-bottom: 24px; }
  .contact-label { margin-bottom: 8px; }
  /* Last field → Submit: 24px from the field's own margin + 40px here, so the
     button clearly separates from the form rather than reading as another row. */
  .contact-submit-row { margin-top: 40px; }

  /* iOS Safari AUTO-ZOOMS the page when you focus an input whose font-size is
     under 16px — 15px triggers it. Bumping to exactly 16px on touch widths is
     the fix; the alternative (user-scalable=no on the viewport meta) also kills
     legitimate pinch-zoom, so it's off the table. */
  .contact-input { font-size: var(--fs-ui); }
}

/* ==========================================================================
   Pricing page (pricing.html, body.page-pricing)
   Ported from the root "Pricing — NEIGHPalm" draft and re-set in the house
   language: flat #1F1F1F base with the contact page's fixed far-weave backdrop
   behind everything, hairline #575757 cards, Roboto Serif display, Manrope UI,
   and the shared white CTA tokens. Scoped under .page-pricing / .pricing-* so
   nothing leaks into index.html. Local 960/620 media queries at the end of the
   block, same precedent as the contact page above.
   ========================================================================== */
.page-pricing { background: var(--page-bg); }

/* Content rides above the fixed weave (z-index 0) — same layering as
   .fold-contact on the contact page. */
.pricing-main {
  position: relative;
  z-index: 1;
  /* The fixed nav overlays the top of the page and takes no layout space. */
  /* The two vertical rhythm knobs for the tier block, named so .pricing-tiers
     can subtract them from 100vh below. */
  --pricing-top: clamp(36px, 6vh, 110px);
  --pricing-gap: clamp(32px, 5vh, 88px);
  padding: calc(var(--nav-h) + var(--pricing-top)) var(--fold-inset) 0;
}

/* Headline + cards as one first-screen block. It is a flex COLUMN whose
   min-height is the screen minus the nav and the top padding, and .pricing-tiers
   is the flex:1 child — so the cards absorb whatever is left over and end ~48px
   above the fold instead of leaving hundreds of px of dead space on a tall
   monitor. On a short screen the content overflows it and it does nothing. */
.pricing-first {
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - var(--nav-h) - var(--pricing-top) - 48px);
}
.pricing-first .pricing-tiers { flex: 1; }

/* --- header --- */

.pricing-hero { text-align: center; }

.pricing-headline {
  margin: 0 auto;
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-h1);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight-lg);
  color: var(--ink);
  max-width: 16em;
}

/* --- tier cards --- */

.pricing-tiers {
  /* Tuned by eye at a 1900px viewport: 1280 − 2×46 gaps ÷ 3 = a 396px card. */
  max-width: 1280px;
  margin: var(--pricing-gap) auto 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* Five named-by-position rows, shared by all three cards via subgrid below:
     mark · name · description(+its hairline) · bullets · CTA. The bullets row
     is the 1fr, so it absorbs the height difference (tier 1 has 4 bullets, the
     other two have 6) and every OTHER row — crucially the divider and the
     button — lands at the same y in all three cards. */
  grid-template-rows: auto auto auto 1fr auto;
  /* column gap only — the row rhythm is the children's own margins. */
  column-gap: 46px;
  align-items: stretch;
}

/* SMALL DESKTOP (laptops, 961–1340px). The shared --fold-inset is ~13.5vw, which
   at these widths squeezes the three cards to ~215px each — the copy wraps to
   twice the lines and the CTA is pushed below the fold. Claw the width back from
   the gutters and the column gap; the cards stay three-up, just wider. */
@media (min-width: 961px) and (max-width: 1340px) {
  /* Interpolates from --fold-inset's own ~181px at 1340px down to 48px at
     1024px, so there is NO jump at the breakpoint — the gutter just keeps
     closing as the viewport shrinks, instead of holding at 13.5vw. */
  .pricing-main {
    padding-left: clamp(48px, 42.1vw - 383px, 181px);
    padding-right: clamp(48px, 42.1vw - 383px, 181px);
  }
  .pricing-tiers { column-gap: clamp(24px, 2.6vw, 46px); }
}

/* Same surface as .contact-card: hairline border, 8px radius, a light frost of
   the weave behind. It is a SUBGRID (not a flex column) so its five children sit
   in the PARENT's rows — that is what makes the dividers and the buttons line up
   across cards whose copy wraps to different line counts. */
/* --mark-rgb is the tier's accent as bare CHANNELS, so one value can drive both
   the mark (at full strength) and the card stroke + divider (faded to
   --stroke-a). rgba() needs the channels unbracketed, which is why this is
   "255, 255, 255" and not a hex — see the per-tier overrides below. */
.pricing-tier {
  --mark-rgb: 255, 255, 255;
  /* Tier 1 is the only WHITE card, so it reads brighter than the tinted two at
     the same alpha — it is dialled back to 0.24 here and the accent tiers
     override back up to 0.35 below. */
  --stroke-a: 0.24;
  position: relative;   /* anchor for the card-wide click target on the CTA */
  display: grid;
  grid-row: span 5;
  grid-template-rows: subgrid;
  border: 1px solid rgba(var(--mark-rgb), var(--stroke-a));
  border-radius: var(--r-md);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  background: rgba(7, 6, 9, 0.55);
  padding: clamp(18px, 2.4vh, 36px) clamp(24px, 2.4vw, 36px);
  transition: border-color 0.25s ease, background-color 0.25s ease;
}

/* Tier mark (svg/plan_1..3.svg, inlined in pricing.html).

   Each mark is sized off its central DIAMOND, not off its own total height.
   All three files draw an 80-unit diamond, but in viewBoxes of 86 / 103 / 106 —
   so a shared total height (the obvious approach) renders the diamond at
   41 / 34 / 33px and the tier-1 diamond looks oversized. Instead --mark-h below
   is back-solved per file as DIAMOND * viewBoxHeight / 80, with DIAMOND = 34px,
   which lands the same diamond in all three while the flanking motif is free to
   grow. The row is a fixed 46px (clears the tallest, 45.05) and centred: the
   diamond sits at the vertical midpoint of every viewBox (0.498 / 0.499 / 0.498),
   so align-items:center is what actually lines the three diamonds up. */
.pricing-tier-mark {
  display: flex;
  align-items: center;
  justify-content: center;   /* the marks are three different widths — centre
                                them so the diamonds line up card to card */
  height: 46px;
  margin-bottom: clamp(10px, 1.6vh, 24px);
}
/* DIAMOND(34) × viewBoxHeight / 80 — see the note above. */
.pricing-tier:nth-child(1) .pricing-tier-mark svg { --mark-h: 36.55px; }  /* 86  tall */
.pricing-tier:nth-child(2) .pricing-tier-mark svg { --mark-h: 43.78px; }  /* 103 tall */
.pricing-tier:nth-child(3) .pricing-tier-mark svg { --mark-h: 45.05px; }  /* 106 tall */
/* The accent per tier — white, then indigo, then pink. Set on the CARD (not the
   svg) because the stroke and the divider read it too. */
.pricing-tier:nth-child(2) { --mark-rgb: 125, 132, 227; --stroke-a: 0.35; }   /* #7d84e3 */
.pricing-tier:nth-child(3) { --mark-rgb: 255, 138, 212; --stroke-a: 0.35; }   /* #ff8ad4 */
.pricing-tier-mark svg {
  display: block;
  height: var(--mark-h, 44px);
  width: auto;   /* aspect-ratio from the viewBox — tier 3 is the widest */
  /* The source files are stroked black at width 4; recolour here and pin the
     stroke to a true 2px with vector-effect so it does NOT thin out as each
     mark scales down by a different factor (86 / 103 / 106 tall → 44). Without
     this the three marks would render at 2.05 / 1.71 / 1.66px and read as
     inconsistent weights. */
  stroke: rgb(var(--mark-rgb));
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* Held back at rest so hovering the card has somewhere to go — see the
     .pricing-tier:hover block below. */
  opacity: 0.7;
  transition: opacity 0.25s ease;
}

.pricing-tier-name {
  margin: 0;
  font-family: var(--font-ui);
  font-weight: var(--fw-medium);
  font-size: var(--fs-h3);
  line-height: var(--lh-tight);
  color: var(--ink);
  text-align: center;
}

/* The hairline moved BELOW the description (it used to be its own empty
   .pricing-price-row above, left over from the removed price line) — so the
   mark, name and description read as one centred head block, and the rule
   separates that block from the bullet list. */
.pricing-tier-desc {
  margin: clamp(10px, 1.4vh, 18px) 0 0;
  padding-bottom: clamp(12px, 1.6vh, 22px);
  border-bottom: 1px solid rgba(var(--mark-rgb), var(--stroke-a));
  transition: border-color 0.25s ease;
  font-family: var(--font-ui);
  font-size: var(--fs-body-s);
  line-height: var(--lh-body);
  color: var(--ink-body);
  text-align: center;
}

.pricing-bullets {
  list-style: none;
  margin: clamp(10px, 1.6vh, 22px) 0 0;
  padding: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-meta);
  line-height: var(--lh-body);
  color: var(--ink-body);
}
.pricing-bullets li {
  position: relative;
  padding-left: 24px;
  margin-top: clamp(6px, 0.8vh, 10px);
}
/* Monochrome diamond — same ◆ glyph as the hero's `.hero-reassure-star`, so the
   two lists read as one family. Sized down (the glyph is much heavier than the
   bullet it replaced) and nudged down to sit on the text's optical centre. */
.pricing-bullets li::before {
  content: '◆';
  position: absolute;
  left: 0;
  top: 1px;
  font-size: 9px;
  line-height: 1.9;
  color: var(--ink-a60);
}

/* Composes .btn (pill geometry, type, shadow, focus ring) with the shared
   white-CTA face — same recipe as .contact-submit, and like it deliberately
   NOT .btn-solid (whose hover drives the hero inversion machinery). */
.pricing-tier-cta {
  margin-top: auto;           /* pins to the card's bottom edge */
  padding-top: 12px;
  padding-bottom: 12px;
  justify-content: center;
  /* Deliberately NOT --white-cta-rest: these three sit side by side and the
     hover needs to be obvious, so the resting face is dropped well below white
     and the hover goes to the shared pure-white one. */
  background: #c9c8cc;
  border: 0;
  color: var(--ink-on-light);
  font-weight: var(--fw-medium);
  font-size: var(--fs-ui);
  transition: background-color 0.25s ease;
}
/* margin-top:auto (above) pushes the button down; this pad guarantees a
   minimum gap even in the tallest card, where auto collapses to 0. */
.pricing-bullets { padding-bottom: clamp(12px, 1.8vh, 36px); }

/* THE WHOLE CARD IS THE BUTTON. A stretched pseudo-element on the CTA covers
   the card (which is position:relative for exactly this), so a click anywhere
   follows the link — one <a>, so it stays a single tab stop and keeps its real
   href for screen readers and middle-click. Nothing else in the card is
   interactive, so there is nothing for the overlay to block. */
.pricing-tier-cta::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
@media (hover: hover) and (pointer: fine) {
  .pricing-tier { cursor: pointer; }
  /* Hovering ANYWHERE on the card lights it up: stroke, mark and divider come
     up to full, and the button takes its hover face — so the card reads as one
     control rather than a box with a button in it. */
  .pricing-tier:hover { --stroke-a: 0.75; background: rgba(7, 6, 9, 0.68); }
  .pricing-tier:hover .pricing-tier-mark svg { opacity: 1; }
  .pricing-tier:hover .pricing-tier-cta { background: var(--white-cta-hover); }
}
/* Keyboard parity — focusing the link lights the card the same way. */
.pricing-tier:focus-within { --stroke-a: 0.75; }
.pricing-tier:focus-within .pricing-tier-mark svg { opacity: 1; }

/* --- full comparison table --- */

.pricing-compare {
  max-width: 1360px;
  margin: clamp(64px, 10vh, 110px) auto 0;
}


/* The table keeps a real min-width and the wrapper scrolls sideways on narrow
   screens — squeezing four columns into 380px makes every cell wrap. */
/* NOTE: the sideways scroll is scoped to the MOBILE block at the bottom of this
   file. `overflow-x: auto` computes overflow-y to `auto` too, which would make
   the wrapper the sticky header's scroll container and stop it sticking to the
   viewport — so on desktop the wrapper stays `visible`. */

.pricing-table {
  width: 100%;
  min-width: 640px;
  border-collapse: collapse;
  font-family: var(--font-ui);
  font-size: var(--fs-meta);
  color: var(--ink-body);
}
/* Ruled by COLUMN, not by row: a hairline down the left edge of each plan column
   (so three lines, between the four columns) and nothing between the rows. The
   group rows are colspan=4, so the lines break for each section heading — which
   is what visually groups the blocks now that the headings have no rule. */
.pricing-table th,
.pricing-table td {
  padding: 12px 16px;
  text-align: center;
}
.pricing-table td:not(:first-child) {
  border-left: 1px solid var(--rule-soft);
}
/* …but NOT under the plan names in the header row — the column rules start at
   the first data row, so the header reads as three labels, not three cells. */
.pricing-table thead th { border-left: 0; }
.pricing-table th:first-child,
.pricing-table td:first-child {
  text-align: left;
  width: 40%;
}
/* Without this the three plan columns are auto-sized by their content and the
   widest header ("SOC 2 minded") steals the space — 40 + 3×20 keeps them even. */
.pricing-table th:not(:first-child) { width: 20%; }
/* The header row rides down the long comparison table, parked just under the
   fixed nav. It needs its OWN opaque fill (rows would otherwise scroll through
   it) and a box-shadow rule instead of border-bottom — with border-collapse the
   table owns the borders and they do not travel with a sticky cell. */
.pricing-table thead th {
  position: sticky;
  top: var(--nav-h);
  z-index: 2;
  background: var(--page-bg);
  /* extra top padding = the air between the plan names and the fixed nav they
     park under; it is part of the sticky cell so it travels with the row. */
  padding-top: 34px;
  padding-bottom: 20px;
  box-shadow: inset 0 -1px 0 #575757;
  vertical-align: bottom;
}

.pricing-th-feature {
  font-weight: var(--fw-semibold);
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-a45);
}
.pricing-th-name {
  display: block;
  font-weight: var(--fw-medium);
  font-size: var(--fs-lg);
  color: var(--ink);
}
/* Group rows: section labels inside the table, the same uppercase voice — but
   bigger and brighter than the feature rows, and with NO rule under them: the
   air above each label is what separates the sections now, so the label reads as
   a heading for the rows below rather than the tail of the block above. */
.pricing-group td {
  padding-top: 88px;
  padding-bottom: 34px;   /* air between the heading and its first row */
  font-weight: var(--fw-semibold);
  font-size: var(--fs-xl);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--ink);
  text-align: left;
  border-bottom: 0;
}
/* The first group sits right under the sticky header row — still needs real air
   between "Compare tiers" and the first section label, just less than the gap
   that separates two sections. */
.pricing-table tbody tr:first-child.pricing-group td { padding-top: 56px; }

/* The ✓/— in the cells are TEXT in the markup (so they stay copy-pasteable and
   readable to a screen reader), but the glyph itself is drawn from an SVG mask
   rather than the font — Manrope's ✓ is a thin, slightly lopsided character and
   it rendered differently on Windows. This is a plain two-stroke tick with the
   same round caps as the rest of the site: 16px box, currentColor. */
.pricing-table .yes {
  color: var(--ink);
  font-size: 0;                 /* hide the ✓ character, keep it in the DOM */
}
.pricing-table .yes::before {
  content: '';
  display: inline-block;
  width: 16px;
  height: 16px;
  vertical-align: -3px;
  background: currentColor;
  -webkit-mask: var(--tick) center / contain no-repeat;
  mask: var(--tick) center / contain no-repeat;
  --tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5 6.5 12 13 4.5'/%3E%3C/svg%3E");
}
/* The em dash is fine as type — only the tick needed replacing. */
.pricing-table .no  { color: var(--ink-a28); }
.pricing-table .val { color: var(--ink-body); }

.pricing-note {
  margin: 24px 0 0;
  text-align: center;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-meta);
  color: var(--ink-a45);
}

/* --- closing CTA --- */

.pricing-cta {
  max-width: 1360px;
  margin: clamp(64px, 10vh, 110px) auto;
}
.pricing-cta-box {
  text-align: center;
  border: 0;
  border-radius: var(--r-md);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  background: rgba(7, 6, 9, 0.55);
  padding: clamp(40px, 6vh, 72px) clamp(24px, 4vw, 64px);
}
.pricing-cta-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-h2);
  color: var(--ink);
}
.pricing-cta-sub {
  margin: 16px auto 0;
  font-family: var(--font-ui);
  font-size: var(--fs-body-s);
  line-height: var(--lh-body);
  color: var(--ink-body);
  max-width: 480px;
}
/* Same white-pill recipe as the tier CTAs, inline-sized. */
.pricing-cta-btn {
  margin-top: 28px;
  background: var(--white-cta-rest);
  border: 0;
  color: var(--ink-on-light);
  font-weight: var(--fw-medium);
  font-size: var(--fs-ui);
  transition: background-color 0.3s ease;
}
.pricing-cta-btn:hover { background: var(--white-cta-hover); }

@media (max-width: 960px) {
  /* Same move as .page-contact: the shared --fold-inset is too tight for a
     standalone page at this width. */
  .pricing-main { padding-left: 28px; padding-right: 28px; }
  /* Stacked, so there is nothing to align card-to-card — drop the subgrid and
     go back to a plain flex column (with a real gap between the cards). */
  .pricing-tiers {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    row-gap: 46px;
  }
  .pricing-tier {
    grid-row: auto;
    display: flex;
    flex-direction: column;
  }
}

@media (max-width: 620px) {
  .pricing-main { padding-left: 20px; padding-right: 20px; }
  .pricing-tier { padding: 24px 20px; }
  .pricing-table th,
  .pricing-table td { padding: 10px 12px; }
  /* Sideways scroll only down here — and it costs the sticky header (see the
     note by .pricing-table-scroll), so unstick it at this width. */
  .pricing-table-scroll { overflow-x: auto; }
  .pricing-table thead th { position: static; }
}

/* ==========================================================================
   Final CTA (fold 5 — Figma node 126:15434)
   Dark band, weave field dimly behind, one centered serif line + white CTA.
   The weave markup/build rules are documented at the .fold-cta HTML comment
   and buildCtaWeave() in script.js.
   ========================================================================== */
.fold-cta {
  position: relative;
  z-index: 1;
  background: var(--page-bg);
  overflow: hidden;   /* local clip for the oversized weave svg — NEVER on html/body */
  padding: clamp(150px, 23vh, 250px) 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.cta-weave {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Feather the field into #1F1F1F at top and bottom so the band doesn't
     hard-cut against the contact page and the footer (same idiom as .int-weave). A long,
     gradual fade — extra stops ease the ramp so the weave dissolves softly into
     the field rather than ramping over a short band. */
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0%, rgba(0,0,0,0.35) 22%, rgba(0,0,0,0.85) 40%, #000 50%,
    rgba(0,0,0,0.85) 60%, rgba(0,0,0,0.35) 78%, transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0%, rgba(0,0,0,0.35) 22%, rgba(0,0,0,0.85) 40%, #000 50%,
    rgba(0,0,0,0.85) 60%, rgba(0,0,0,0.35) 78%, transparent 100%);
}

/* Sized + centred by buildStaticWeave(); the -45° is baked per cell. No zoom:
   the motif renders at its native 120px cell size to match the HERO field (NOT
   fold-2's .int-weave-svg, which zooms 1.18× for a larger block). JS never
   writes this transform. */
.cta-weave-svg {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(1);
  overflow: visible;
}

/* Resting dim floor — same level as the fold-2 weave / hero PATTERN_DIM. */
.cta-weave-cells path {
  stroke-opacity: 0.16;
}

.cta-inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Hero-style serif display (Roboto Serif, opsz 8) plus the design's
   capitalize + black halo that lifts the type off the weave (126:15437). */
.cta-headline {
  margin: 0 0 clamp(36px, 6vh, 56px);
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  font-variation-settings: 'opsz' 8, 'wght' 600;
  font-size: clamp(34px, 3.6vw, 54px);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight-lg);
  text-transform: capitalize;
  color: var(--ink);
  max-width: 758px;   /* widened so "Security and GRC," holds one line */
  text-shadow: 0 0 27px #000000;
}

/* White pill on .btn geometry, sized per 126:15441 — and it now behaves EXACTLY
   like the hero's .btn-solid: it stays white and only GROWS on hover, because the
   colour event is the weave behind it taking a new tint (initCtaInversion), swept
   out of this button. It does NOT get the .btn-solid class: initHeroInversion does
   a singular querySelector('.btn-solid') and .hero-cta-buttons:has() is hero-scoped,
   so sharing the class would be *mostly* harmless — but duplicating the two rules
   here keeps fold-5's hover independent of the hero's machinery, which is why this
   button was kept off .btn-solid in the first place. */
.cta-button {
  position: relative;               /* anchors the two shadow layers below */
  background: #ffffff;              /* rests at pure white (like .btn-solid), not the shared brighten token */
  border: 1px solid transparent;    /* height parity, as on .btn-solid */
  color: var(--ink-on-light);
  font-weight: var(--fw-semibold);
  /* 12px 24px, matching .btn's own padding — which is what the contact page's
     .contact-submit uses too. Both then resolve to the SAME 50px box
     (18px text + 12px×2 + 1px border×2). */
  padding: 12px 28px;
  /* Glow halo — a soft white bloom so the primary CTA reads as lit. Held STATIC
     (no breathing pulse): lives on the element's own box-shadow, the dark drop
     shadow stays on the pseudo-elements below, so the crossfade is untouched. */
  box-shadow: 0 0 20px rgba(255,255,255,0.40);
  /* 0.28s, deliberately SHORTER than the tint sweep (CTA_INVERT_D = 0.9s in JS) —
     the pill snaps to size while the colour front is still crossing, so it reads as
     firing the sweep off rather than travelling with it. Same value as .btn-solid. */
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1),
              background-color 0.28s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: transform;
}
/* Two static shadow layers, crossfaded on hover — opacity is a compositor property,
   so each shadow rasters once instead of repainting a 60px blur every frame. */
.cta-button::before,
.cta-button::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  transition: opacity 0.45s ease;
  will-change: opacity;
}
.cta-button::before { box-shadow: var(--shadow-btn); opacity: 1; }
.cta-button::after  { box-shadow: var(--shadow-btn); opacity: 0; }
.cta-button:hover {
  transform: scale(1.08);
  background: var(--white-cta-hover);   /* shared white-CTA hover face */
}

/* The pair, mirroring .hero-cta-buttons (flex row, 24px gap) — centred here rather
   than left-aligned because fold-5 is a centred stack.
   The reveal's entrance lift lives HERE, on the row, and rides a CUSTOM PROPERTY
   rather than GSAP's own `y`. Two reasons: (1) GSAP writes `transform` inline, and
   an inline transform outranks this stylesheet's :hover rules — tweening `y` on a
   button would silently kill its hover grow; (2) putting it on the row means both
   buttons rise together and neither child's own transform has to compose it. */
.cta-buttons {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 24px;
  flex-wrap: wrap;                  /* narrow viewports drop the pair to two rows */
  transform: translateY(var(--cta-rise, 0px));
}
/* Hold the shadow crossfade until the sweep has finished, so nothing heavy paints
   during the compositor-only grow. Hover-OUT reverts immediately (no delay). */
.cta-button:hover::before { opacity: 0; transition: opacity 0.35s ease 0.85s; }
.cta-button:hover::after  { opacity: 1; transition: opacity 0.35s ease 0.85s; }

/* ==========================================================================
   Footer
   Not the Figma placeholder — the site's own monochrome vocabulary: hairline
   top border, brand lockup + tagline left, two link columns right, legal row.
   ========================================================================== */
.site-footer {
  position: relative;
  z-index: 1;
  background: var(--page-bg);
  border-top: 1px solid var(--rule-grey);
  /* Breathing room between the fold-5 CTA band and the footer. Footer, band, and
     body are all #1F1F1F; the hairline top border is what marks the footer. */
  margin-top: clamp(64px, 10vh, 120px);
  padding: clamp(48px, 8vh, 88px) var(--fold-inset) 0;
  font-family: var(--font-ui);
}

.footer-inner {
  max-width: 1360px;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  /* Ceiling kept low on purpose: brand (360px) + nav (~570px) only clears the
     1360px inner width with a modest gap. A 96px gap pushed the total past it and
     wrapped the nav under the brand on every laptop screen. */
  gap: clamp(40px, 3vw, 64px);
}

/* Mark on the left, the text column (wordmark + tagline) to its right. */
.footer-brand { display: flex; }
.footer-lockup {
  display: flex;
  align-items: center;
  gap: 20px;
}
.footer-brand-text {
  display: flex;
  flex-direction: column;
}

/* logo-hq.png is 853x873, cropped tight to its alpha bbox. It is sized to the
   TALLEST footer column (Product / Company / Legal = title + 4 links = 156px).
   Height is HARD-CODED, not 100%: the mark is itself a flex item, so a
   percentage height feeds back into the row it is measuring and blows up to the
   image's intrinsic 873px. If the link columns gain or lose a row, re-measure. */
.footer-logo {
  width: auto;
  height: 156px;
  flex: none;
  display: block;
  object-fit: contain;
}

/* Same serif voice as .nav-wordmark, a step larger. */
.footer-wordmark {
  font-family: var(--font-display);
  font-weight: var(--fw-light);
  font-size: var(--fs-xl);
  font-variation-settings: 'opsz' 8, 'wdth' 100, 'GRAD' 0;
  letter-spacing: var(--ls-wordmark);
  color: var(--ink);
  white-space: nowrap;
}

.footer-tagline {
  margin: 8px 0 0;   /* it now sits under the wordmark inside the lockup */
  font-size: var(--fs-meta);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-meta);
  color: var(--ink-a60);
  /* Narrower than it used to be: the big mark now shares the row with it, and
     the brand block has to stay slim enough for the nav to sit alongside. */
  max-width: 22ch;
}

.footer-nav {
  display: flex;
  /* wrap: three columns (Product/Company/Legal) don't fit a phone width —
     without it the row overflows and gives the whole page a horizontal scroll */
  flex-wrap: wrap;
  gap: clamp(28px, 3.4vw, 64px);
}

.footer-col {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.footer-col-title {
  margin: 0 0 6px;
  font-size: var(--fs-small);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-a45);
}

.footer-link {
  font-size: var(--fs-body-s);
  letter-spacing: var(--ls-meta);
  color: var(--ink-body);
  text-decoration: none;
  transition: color 0.3s ease;
}
.footer-link:hover { color: var(--ink); }
.footer-link:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.8);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Legal row — full-bleed hairline above, quiet small type. */
.footer-legal {
  max-width: 1360px;
  margin: clamp(40px, 6vh, 64px) auto 0;
  padding: 20px 0 28px;
  border-top: 1px solid var(--rule-grey-faint);
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 16px;
}

.footer-copyright {
  margin: 0;
  font-size: var(--fs-small);
  letter-spacing: var(--ls-meta);
  color: var(--ink-a45);
}

.footer-legal-links {
  display: flex;
  gap: 28px;
}

/* The build credit that replaced the duplicate Privacy/Terms pair. The wrapper
   carries the copyright line's muted type so "site by" reads as the same
   register; only the name inside is a .footer-link and picks up its own colour. */
.footer-credit {
  font-size: var(--fs-small);
  letter-spacing: var(--ls-meta);
  color: var(--ink-a45);
}
.footer-legal-links .footer-link {
  font-size: var(--fs-small);
  color: var(--ink-a60);
}
.footer-legal-links .footer-link:hover { color: var(--ink); }

/* Below this the brand + three link columns genuinely cannot share a row, so
   stack them deliberately rather than letting flex-wrap drop the nav on its own
   (which left the nav starting at the brand's left edge with a huge dead gap). */
@media (max-width: 1340px) {
  .footer-inner { flex-direction: column; }
}

@media (max-width: 620px) {
  .footer-inner { flex-direction: column; }
  .footer-nav { gap: 56px; }
  /* Stacked: there is no nav column to match, and a % height against an
     auto-height parent is indefinite — so pin the mark to a fixed size. */
  .footer-logo { height: 96px; }
}

/* ==========================================================================
   MOBILE / NARROW VIEWPORTS
   Everything above this line is the desktop build; this block is the phone and
   small-tablet pass. Deliberately kept in ONE place at the end of the file
   rather than sprinkled per-fold, so the mobile layout can be read as a single
   document and the desktop rules stay untouched (a desktop regression here
   should be impossible — nothing below applies over 960px). The contact and
   pricing pages carry their own small local queries earlier in the file; this
   block is the landing page's.

   Two breakpoints only, reusing the ones the file already had: 960px and 620px.

   What is deliberately NOT changed on mobile:
   - The hero's traveling erosion wave (playErosionWave) still runs. It is the
     hero's whole life and the one motion a touch user can still perceive; the
     pointer FX are already skipped on touch via `finePointer` in script.js.
   - The 120px weave grid / block geometry. The fold-2 weave re-derives its
     edge blocks from its own box on resize (rebuildIntegrationsWeave), so
     re-shaping the .int-weave box below is sufficient — never change CELL_SIZE
     or the block motif to fit a layout.
   ========================================================================== */

/* Hover-capable styling must not stick to a tap. On touch, :hover latches after
   a tap and stays until the next one — so the CTA's grow would leave the button
   enlarged. The JS twins of these (initHeroInversion) are already gated on
   finePointer; these are their CSS halves. */
@media (hover: none) {
  .btn-solid:hover,
  .nav-cta:hover { transform: none; }
  /* Resting face — a latched :hover must not leave a white CTA stuck on its
     brightened state after the tap. .btn-solid is absent on purpose: it is pure
     white in both states, so it has nothing to reset. */
  .nav-cta:hover,
  .cta-button:hover { background: var(--white-cta-rest); }
  .nav-link:hover { color: var(--ink-action); }   /* resting colour — don't stay lit after a tap */
  .nav-demo:hover {                     /* resting outline — don't stay lit after a tap */
    color: var(--ink-action);
    border-color: var(--rule-ghost);
    background: transparent;
    /* Back to the REST frost (1px + ramp), not off: the frost itself is a rest
       property — only the hover's extra 2px is dropped. */
    backdrop-filter: blur(calc(2px + var(--nav-blur, 0) * 0.5px)) saturate(1.05);
    -webkit-backdrop-filter: blur(calc(2px + var(--nav-blur, 0) * 0.5px)) saturate(1.05);
  }
  /* :hover latches after a tap on touch — keep the fine print at its rest grey. */
  .hero-ctas:has(.btn-solid:hover) .hero-reassure { color: var(--ink-quiet); transform: none; }
  .hero-ctas:has(.btn-solid:hover) .hero-reassure-star { color: var(--ink-muted); }

  /* Fold-4 operator cards: rest border/link, and the ornament ignores the
     card-hover colour handoff (on mobile it runs its own accent pendulum). */
  .operator-role:hover { border-color: var(--rule-role); }
  .operator-role:hover .operator-link {
    color: var(--ink-action);
    border-color: var(--rule-ghost);
  }
  .fold-operator:has(.operator-role--vciso:hover) .operator-ornament,
  .fold-operator:has(.operator-role--fde:hover) .operator-ornament { color: var(--ink); }

  /* Buttons / links back to their rest faces after a tap. (Pricing-tier hover
     and the pointer FX in script.js are already gated on hover:hover.) */
  .pricing-cta-btn:hover,
  .contact-submit:hover { background: var(--white-cta-rest); }
  .fold-dock-btn:hover { background: transparent; color: var(--ink-a28); }
  .footer-link:hover,
  .footer-legal-links .footer-link:hover { color: var(--ink-body); }
  .contact-alt a:hover { color: var(--ink-a80); }
}

@media (max-width: 960px) {
  /* The fold-2 copy column has a hard 280px minimum; below ~900px the grid
     starts squeezing the weave to nothing well before it actually overflows.
     Give the copy a proportionally larger share on the way down to the 620px
     stack. */
  .integrations-inner { grid-template-columns: minmax(260px, 42%) 1fr; }

  /* Operator fold: the two role cards stack. */
  .operator-roles { grid-template-columns: minmax(0, var(--operator-card-w)); }
}

@media (max-width: 620px) {
  /* Gutters: 13vw is ~50px of a 390px screen lost per side. Flat 20px instead.
     Single source (see :root at the top of this file) so every fold follows. */
  :root { --fold-inset: 20px; }
  /* Shrink the empty "pattern tail" below the hero (15vh on desktop) so fold-3
     follows the CTAs sooner. The var is declared on .scene-compliance (a closer
     ancestor than :root), so it must be overridden THERE to reach .hero. One
     variable drives the hero height, the pattern wrap, and the depth-echo
     gradient stops, so they all shrink in step. */
  .scene-compliance { --fold1-tail: 6vh; }

  /* Drop the risk-reversal line ("No credit card needed ◆ Unlimited time on
     Free plan") on phones — vertical space under the CTAs is the scarcest
     thing on the hero. */
  .hero-reassure { display: none; }

  /* ---- Nav -------------------------------------------------------------- */
  /* Mobile bar carries ONLY the mark + wordmark, centred (no Log In / CTA — the
     hero's own "Get a Demo" buttons cover the call to action on a phone). With
     the actions gone there's nothing for space-between to push apart, so centre
     the single lockup. */
  /* Override the TOKEN, not .site-nav's height — --nav-h is what the contact
     fold's vertical centring subtracts to account for the fixed bar, so the
     two must not drift apart. .site-nav { height: var(--nav-h) } does the rest. */
  :root { --nav-h: 60px; }
  /* The bar CENTRED its single child here, back when that child was the lone palm
     mark and .nav-actions was hidden — a centred mark read as deliberate. The
     brand is now a left-aligned wordmark on every breakpoint, so drop the
     override and inherit space-between (one child = flush left). */
  .site-nav { padding: 0 var(--fold-inset); }

  /* Once the hero is fully off screen the bar sits over real content, and on a
     phone the copy runs right under it — so the blur lifts from the hero's 0.5px
     whisper. `--nav-blur` is a 0->1 ramp scrubbed by initNavHandoff (script.js),
     so the radius grows WITH the scroll rather than snapping on at a class
     toggle. Mobile only — desktop keeps the Figma chrome. Knobs: the 4px is the
     extra blur at full ramp; the ramp's start/length live in script.js. */
  .site-nav {
    backdrop-filter: blur(calc(0.5px + var(--nav-blur, 0) * 4px));
    -webkit-backdrop-filter: blur(calc(0.5px + var(--nav-blur, 0) * 4px));
  }
  /* Mobile blurs on .site-nav directly (safe here — .nav-actions is hidden), so
     drop the desktop pseudo-layer to avoid stacking two blurs. */
  .site-nav::before { backdrop-filter: none; -webkit-backdrop-filter: none; }
  .nav-wordmark { font-size: var(--fs-ui); }

  /* Mobile nav = wordmark + hamburger; .nav-actions turns into a dropdown
     PANEL under the bar, opened by nav.js toggling .site-nav.is-open.
     (The old rule hid .nav-actions outright — that left pricing/contact/404
     with no navigation at all on a phone.) */
  /* The open sheet is z-index 30, so the bar itself must sit above it to keep
     the X tappable. */
  .site-nav { z-index: 40; }
  /* An element with backdrop-filter becomes a BACKDROP ROOT: its descendants'
     own backdrop-filter can only sample what's inside it — i.e. nothing. So
     while the sheet is open the bar drops its blur and hands it to the panel;
     the bar keeps its solid fill, so nothing reads through it. */
  .site-nav.is-open {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(10, 10, 10, 0.92);
  }
  .nav-burger {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 40px;
    height: 40px;
    padding: 0 8px;
    background: none;
    border: none;
    cursor: pointer;
  }
  .nav-burger span {
    display: block;
    height: 1.5px;
    background: #e4e4e4;
    transition: transform 0.25s ease, opacity 0.2s ease;
  }
  /* Burger -> X while open. The bars are 5px apart plus their own 1.5px height,
     so ±6.5px moves the outer bars onto the middle one before they rotate. */
  .site-nav.is-open .nav-burger span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
  .site-nav.is-open .nav-burger span:nth-child(2) { opacity: 0; }
  .site-nav.is-open .nav-burger span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

  .nav-actions {
    display: none;
    position: fixed;   /* against the viewport, like .site-nav itself */
    top: var(--nav-h);
    left: 0;
    right: 0;
    /* FULL-SCREEN sheet: from under the bar all the way to the bottom edge.
       100svh − nav so the iOS URL bar can't push it past the viewport. */
    /* NOT `bottom: 0`: .site-nav carries a backdrop-filter, which makes it the
       containing block for its fixed descendants — so `bottom` resolves against
       the 60px bar, not the viewport, and the sheet collapsed to nothing. An
       explicit viewport-relative height is immune to that. */
    height: calc(100svh - var(--nav-h));
    /* Must outrank the grain overlay (9) and any page card that claims 10 —
       otherwise those paint straight through the translucent sheet. */
    z-index: 30;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    padding: 24px var(--fold-inset) 32px;
    overflow-y: auto;
    /* Translucent + blurred, not opaque: the page reads through as a soft
       frosted field while the links stay legible on top of it.
       Knobs: the 0.72 alpha (higher = more solid) and the 18px blur radius. */
    background: rgba(10, 10, 10, 0.82);
    -webkit-backdrop-filter: blur(40px) saturate(120%);
    backdrop-filter: blur(40px) saturate(120%);
    border-bottom: 0;
  }
  .site-nav.is-open .nav-actions { display: flex; }
  /* No rules between the items — the sheet is a short list, the padding alone
     separates them. Knobs: the 18px size and the 16px vertical padding. */
  .nav-actions .nav-link {
    padding: 16px 0;
    font-size: var(--fs-lg);
    border-bottom: 0;
  }
  .nav-actions .nav-demo,
  .nav-actions .nav-cta { font-size: 18px; }

  /* Scroll lock while the sheet is open (nav.js sets .nav-open on <html>).
     `position: fixed` on the body was rejected — it loses the scroll position
     and re-fires ScrollTrigger on close.
     Scoped to <html> and to the open state only: a PERSISTENT overflow rule on
     html/body is banned here (see the note at the top of this file — it wedges
     Chromium's repaint of the mutating pattern SVG). */
  html.nav-open,
  html.nav-open body { overflow: hidden; touch-action: none; overscroll-behavior: none; }
  .nav-actions { overscroll-behavior: contain; }
  /* The two pills sit under the links as full-width rows. */
  .nav-actions .nav-demo,
  .nav-actions .nav-cta {
    justify-content: center;
    margin-top: 14px;
  }

  /* ---- Hero ------------------------------------------------------------- */
  /* The 40px clamp floor is a desktop floor — at 360px a 40px serif headline
     breaks to five or six lines and swamps the fold. 30px holds three. Both
     rules must move together: .hero-title-block mirrors the headline's
     font-size so its `em`-based height stays exactly one line-height. */
  .hero-title-block,
  .hero-headline { font-size: clamp(32px, 9vw, 47px); }
  /* Vertically centre the whole hero group (headline + subcopy + CTAs) instead
     of the desktop layout, which reserves a fixed 50vh above the headline to
     land its baseline on screen-centre — on a phone that just dumps an empty
     band at the top and pushes everything low. Collapse that reserved block to
     its content height and let .hero-content's flex column centre the stack. */
  /* Anchor the headline high instead of centring the group: start the stack a
     fixed way down from the top (padding-top) so the title + subcopy sit in the
     upper third, well clear of the bottom-pinned CTAs. Lower the padding-top %
     to raise them further, raise it to drop them. */
  .hero-content { justify-content: flex-start; padding-top: 24vh; }
  .hero-title-block { height: auto; }
  .hero-subcopy {
    font-size: 17px;
    letter-spacing: var(--ls-body);   /* 1.32px is loose at this size and forces breaks */
    margin-top: 28px;
    max-width: 330px;        /* tighter column: fewer, more even lines */
  }
  /* Pin the CTAs to the bottom of the hero, out of the centred flow, so the
     headline + subcopy stay vertically centred while the buttons sit near the
     bottom edge. Absolute against .hero-content (position:relative). left/right 0
     span the full width; the inner button group is max-width:300px + auto margins
     so it still centres. `bottom` is the gap from the hero's bottom edge — raise
     it to lift the buttons, lower it to drop them. */
  .hero-ctas {
    position: absolute;
    left: 0;
    right: 0;
    bottom: clamp(130px, 24vh, 210px);
    margin-top: 0;
    width: 100%;
  }
  /* Stack the two CTAs. .btn keeps white-space:nowrap (the labels must not
     break). The group is capped at 300px and centred, so the buttons read as a
     narrow centred pair rather than full-bleed bars. align-items:stretch keeps
     both the SAME width (the longer label sets it) so they line up as a block;
     it also overrides the desktop `center`, which would shrink each to its text
     width and leave them uneven. Widen/narrow via the 300px max-width knob. */
  .hero-cta-buttons {
    flex-direction: column;
    align-items: stretch;
    gap: 14px;
    width: 100%;
    max-width: 240px;
    margin-inline: auto;
  }
  .hero-cta-buttons .btn { width: 100%; justify-content: center; }

  /* ---- Logo reel ---------------------------------------------------------- */
  /* 71px marks with 50px gaps inside a ~326px content box wrap into a very tall
     block. Step the marks down and tighten the gaps so each row holds 3–4 marks.
     Knobs: the 40px height and the 22px gap. */
  .logo-reel { padding: clamp(28px, 5vh, 56px) 0; }
  .logo-reel-viewport {
    padding: 20px var(--fold-inset);
    gap: 20px;
    overflow: hidden;
  }
  /* ONE row on mobile: the marks no longer wrap — they scroll. The track holds
     the row plus an identical clone, both nowrap, and slides left by exactly
     50% of its own width, so the clone lands where the original started and
     the loop is seamless. Knobs: `40s` (slower = calmer) and the 22px gap.
     Note the track's own `gap: 22px` — it must match the row gap or the seam
     between the two copies shows a wider/narrower space than the rest. */
  .logo-reel-track {
    display: flex;
    width: max-content;
    gap: 22px;
    animation: logo-reel-scroll 40s linear infinite;
  }
  .logo-reel-row {
    gap: 22px;
    flex-wrap: nowrap;
    flex: 0 0 auto;
  }
  .logo-reel-row--clone { display: flex; }
  .logo-reel-row img { height: 56px; }
  @keyframes logo-reel-scroll {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(calc(-50% - 11px), 0, 0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .logo-reel-track { animation: none; }
  }

  /* ---- Fold 3: Compliance Hub — the LIVE (emblem) look ------------------- */
  /* Desktop composes each chapter as a ROW: emblem | gap | copy. At 393px that
     row measures ~500px (122 shape + 63 gap + 273 copy + 40 pad) and simply
     ran off both edges — the emblem was half-cropped on the left and the
     headline ran past the right. Recompose as a centred COLUMN: emblem on top,
     copy under it, everything inside the fold inset.
     Nothing here touches the scrub — the pin and the vertical chapter track are
     unchanged, so scrolling up still reverses for free. */
  .fold-compliance--emblem .fold-chapter,
  .fold-compliance--emblem .fold-chapter:nth-child(even) {
    flex-direction: column;
    justify-content: center;
    align-items: center;
    /* The emblem->copy gap. Was --shape-gap (a horizontal 63px here). */
    gap: clamp(24px, 4.5vh, 40px);
    padding: 0 var(--fold-inset);
    text-align: center;
  }
  /* --shape-w is the single size knob for the artwork; --copy-w is the text
     column. Both are now shares of the phone's width, not of a desktop row. */
  .fold-compliance--emblem .fold-chapter {
    --shape-w: min(210px, 54vw, 26vh);
    --copy-w: 100%;
  }
  .fold-compliance--emblem .fold-chapter-copy { max-width: 340px; }

  /* ---- The emblem STAGE: a fixed layer, outside fold 3's scroll ----------- */
  /* 2026-08-06, user: "just make the shape not a part of fold 3's scroll — make
     it behind it or something". script.js moves all three .fold-emblem hosts out
     of their chapter rows into this element (a sibling of the pinned
     .fold-compliance-pin, inserted at the top of .fold-compliance) and leaves a
     .fold-emblem-slot spacer behind so the copy doesn't shift.

     WHY FIXED: the emblems used to stay centred only because a per-frame
     parkStage() cancelled the pinned track's translate. On iOS the momentum
     fling is composited off the main thread and throttles that JS, so during a
     fling no correction ran and the shape rode up with its copy. A fixed layer
     is held to the viewport by the compositor — it can't move whether JS runs or
     not. It must NOT be inside .fold-compliance-pin: ScrollTrigger transform-pins
     that element, and a transformed ancestor breaks position:fixed. z-index 0
     against the pin's z-index 1 puts the artwork BEHIND the copy. */
  .fold-emblem-stage {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    /* Re-declared here: the hosts no longer inherit from .fold-chapter. */
    --shape-w: min(210px, 54vw, 26vh);
    --glow: #ffffff;
  }
  /* Centred by CSS, not by a tween — GSAP still owns `transform` on the host
     (parkStage only fine-tunes it by the diamond's own offset), so the centring
     is done with left/top + negative margins instead. */
  .fold-emblem-stage .fold-emblem {
    position: absolute;
    left: 50%;
    /* Centred in the space BELOW the fixed nav — the same point parkStage aims
       at (navH + (vh - navH) / 2), so JS has nothing left to correct and the
       shape is already right on the very first frame. */
    top: calc(50% + var(--nav-h) / 2);
    width: var(--shape-w);
    height: var(--shape-w);
    margin: calc(var(--shape-w) / -2) 0 0 calc(var(--shape-w) / -2);
  }
  /* The emblem's ghost in the chapter column: same box, no ink. Keeps the copy
     exactly where it sat when the emblem was still a child of the row. */
  .fold-emblem-slot {
    flex: 0 0 auto;
    width: var(--shape-w);
    height: var(--shape-w);
  }

  /* The copy's own two blocks are width-capped at ~254/272px for the desktop
     column — under a centred phone layout that leaves them off-centre inside a
     wider parent. Let them fill the column and centre with it. */
  .fold-compliance--emblem .fold-heading { align-items: center; }
  .fold-compliance--emblem .fold-heading-line {
    width: 100%;
    font-size: clamp(24px, 7vw, 30px);
    line-height: var(--lh-tight);
  }
  .fold-compliance--emblem .fold-sub {
    width: 100%;
    margin-top: 14px;
    font-size: var(--fs-body-s);
    line-height: var(--lh-body);
  }

  /* ---- Fold 3: retired --alt (tabs + screenshots) look ------------------- */
  /* The stacked content is TALLER than 100vh here, and `justify-content: center`
     on an overflowing flex column splits the overflow evenly top and bottom —
     which slid the heading up UNDER the fixed 60px nav. Pin the stack to the top
     instead and pad past the nav; `min-height` still holds short viewports. */
  .fold-compliance-pin {
    gap: 32px;
    justify-content: flex-start;
    padding-top: 48px;
  }
  /* Desktop pulls the titles UP into the stack and then nudges them DOWN toward
     the image; on mobile the stack is top-aligned, so both moves are wrong and
     the titles end up sitting right on the screenshot. Zero the pull and add a
     real gap of its own (the pin's `gap: 32px` also spaces the carousel/copy,
     so it isn't the knob to turn here). */
  .fold-heading { margin-top: 0; margin-bottom: 20px; }

  /* The screenshot is 4064x2324 shown in a 1239:672 frame — at 350px wide the
     product UI inside is ~2px type, unreadable. Trade width for height and crop
     into the image instead: a taller frame + object-fit:cover on an
     un-overscanned image shows a real, legible slice of the dashboard. */
  /* The frame stops being a picture frame here and becomes the carousel's
     viewport: no border ring, no halo, no fixed ratio — those move onto each
     slide's panel. It keeps its opacity, which is what the fold-3 entrance
     animates. */
  .fold-mockup-frame {
    width: 100vw;
    aspect-ratio: auto;
    border-radius: 0;
    background: none;
    box-shadow: none;
    overflow: visible;
  }
  .fold-mockup-frame::before { display: none; }

  /* Horizontal snap scroller, one slide per viewport width. `scroll-snap-type:
     x mandatory` makes a swipe always settle on a slide; script.js reads the
     settled scrollLeft back to sync the dots + autoplay timer. Scrollbar is
     hidden — the dots are the affordance. */
  .fold-slides {
    display: flex;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* A scroll container clips on BOTH axes, so the panel's 36px white halo was
       being sliced off flat at the track's top/bottom edge. Pad the track by
       more than the bloom radius and the glow lives inside the clip. Vertical
       only — horizontal padding would offset the snap positions. */
    padding: 44px 0;
    margin: -44px 0;
  }
  .fold-slides::-webkit-scrollbar { display: none; }

  .fold-slide {
    flex: 0 0 100%;
    scroll-snap-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    padding: 0 var(--fold-inset);
  }

  /* The panel was absolutely stacked for the desktop crossfade; in a slide it's
     an ordinary block that carries the frame's old look. */
  .fold-mockup-panel {
    position: relative;
    inset: auto;
    width: 100%;
    aspect-ratio: 4 / 3;
    opacity: 1;
    border: 1px solid var(--rule-ghost);
    background: rgba(0, 0, 0, 0.4);
    box-shadow: 0 0 36px 2px rgba(255, 255, 255, 0.16);
  }
  /* ZOOM, not just crop. `object-fit: cover` alone barely crops here (a 1.75:1
     image in a 1.33:1 box loses only ~24% of the width) — the UI stayed 2px tall.
     So the image is sized to 220% of the panel WIDTH with `height: auto` to keep
     its ratio, and slid with left/top: the panel's `overflow: hidden` does the
     cropping. That is a real 2.2x zoom into the source pixels.

     Tuning: `width` is the zoom knob (higher = closer). `left`/`top` are the pan
     knobs, in % of the PANEL's box, and must be negative — 0 shows the image's
     top-left corner, -100% slides it a full panel-width to the left. The desktop
     overscan values (107.46% / -3.58%, Figma 83:637) are fully replaced here. */
  .fold-mockup-panel img {
    width: 220%;
    height: auto;
    left: -36%;
    top: -16%;
    object-fit: fill;
  }
  /* Per-step pans — each targets that screenshot's most legible feature.
     nth-of-type follows the source order in index.html: 1 compliance hub,
     2 risk heatmap, 3 certification journey. */
  .fold-mockup-panel:nth-of-type(2) img { left: -71%; top: -55%; }  /* heatmap grid */
  .fold-mockup-panel:nth-of-type(3) img { left: -60%; top: -48%; }  /* journey track */

  /* script.js moves each description into its own slide, so the desktop
     crossfade stack is left empty here — collapse it. */
  .fold-belowstage { width: 100%; gap: 16px; }
  .fold-desc { display: none; }

  /* Each description now sits under ITS OWN screenshot (static block, not the
     desktop absolute crossfade), so it's always the copy for what you're
     looking at. */
  .fold-column {
    position: static;
    inset: auto;
    opacity: 1;
    max-width: 38ch;
    /* The BLOCK stays centred (auto side margins on a 38ch measure), but the
       LINES inside it are left-aligned — centred body copy gives every line a
       different starting edge, which is hard to read at 3+ lines. */
    margin-inline: auto;
    text-align: left;
    color: var(--ink-a60);
  }
  .fold-column-lead { color: var(--ink); }

  /* The icon dock shrinks to a compact tab row — it stays functional (tapping a
     button scrolls the slider), just tighter so all five fit the phone width. */
  .fold-dock { gap: 4px; padding: 5px; }
  .fold-dock-btn { width: 44px; height: 44px; }
  .fold-dock-btn svg { width: 21px; height: 21px; }
  /* No hover tooltip on touch — the active label would latch after a tap. */
  .fold-dock-label { display: none; }

  /* ---- Layer 3 agents: STACKED, ONE AFTER THE OTHER --------------------- */
  /* The swipe carousel was dropped 2026-08-04: the six models now read as a
     plain vertical column, each one arriving (and drawing itself in) as you
     scroll to it, so nothing has to be discovered by swiping. Contents only —
     no card frame: the motif + its own colour is what separates one from the
     next. */
  .fold-agents .agents-list {
    flex-direction: column;
    row-gap: clamp(44px, 8vh, 72px);
    overflow: visible;
  }
  .fold-agents .agent-row {
    /* A 2-row grid, not a flex column: row 1 is a FIXED art slot sized to the
       tallest motif (--art-kmax), with the art centred in it, so every agent's
       name starts on the same line even though the six artboards are different
       heights — the same trick .int-agent uses on desktop. */
    display: grid;
    grid-template-rows: calc(var(--art-d) * var(--art-kmax)) auto;
    justify-items: center;
    align-content: start;
    text-align: center;
    gap: 18px;
    padding: 0;
    border: 0;
    background: none;
  }
  /* Sized by the DIAMOND (--art-d), not the artboard — see the --art-k note on
     .agent-row--* above. Knob: --art-d is the one size control; every motif
     scales around it and their diamonds stay identical. */
  .fold-agents {
    --art-d: clamp(32px, 10vw, 46px);
    --art-kmax: 2.9612;      /* the tallest --art-k, so the slot fits it exactly */
  }
  .fold-agents .agent-thumb {
    width: auto;
    height: calc(var(--art-d) * var(--art-k));
    align-self: center;
  }
  /* The intro copy sits ONE ROW-GAP above the first motif — same value as the
     gaps between the models, so the column reads as evenly spaced from the top. */
  .fold-agents .agents-subtitle {
    margin-bottom: clamp(44px, 8vh, 72px);
  }

  /* ---- Fold 2: Integrations --------------------------------------------- */
  /* Side-by-side is impossible here: the copy column's 280px minimum alone
     exceeds the content width. Stack it — copy on top, weave as a band below. */
  .integrations-inner {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
    align-content: center;
    row-gap: 0;
  }
  .int-copy {
    grid-column: 1;
    /* MUST be an explicit height. .int-phase children are position:absolute;
       inset:0, so they contribute nothing to the row and it would collapse to
       zero, taking both phases with it. This height is also what the phase
       swap translates within. */
    height: 34vh;
    align-self: start;
    /* Push the copy block down off the nav. The phase swap translates WITHIN this
       box, so padding the box (rather than nudging .int-phase) moves both phases
       together and leaves the swap distances alone. */
    margin-top: 32vh;
    text-align: center;
  }

  /* The weave runs full-bleed BEHIND the copy, so the text competes with linework
     and needs darkening behind it. A radial-gradient panel was the obvious move but
     it's a large soft layer compositing over an animating SVG every frame of the
     phase swap — that's what made the scroll jitter. Stacked text-shadows do the
     same job on the GLYPHS: no extra layer, no per-frame raster, and the halo is
     welded to the letterforms so it can never drift off the text. Three rings of
     the page colour (#1F1F1F) at growing blur radii build up a soft pool.
     Turn the blur px up for a wider halo, add/remove a ring for more or less mass. */
  .int-phase * {
    text-shadow:
      0 0 6px var(--page-bg),
      0 0 14px var(--page-bg),
      0 0 26px var(--page-bg);
  }
  .int-subtitle { max-width: none; margin-inline: auto; }
  /* The weave moves from a right-hand column (inset: 0 0 0 44%) to a full-width
     band across the bottom. The grid is rebuilt against this new box on resize
     by rebuildIntegrationsWeave(), and the edge logic is deterministic, so it
     re-forms a correct edge.svg-style ending at the new shape by itself. */
  /* Feathered on its TOP edge (toward the copy) instead of its left, since the
     band now meets the copy above it rather than a column beside it. */
  /* No serrated LEFT edge here. decorateWeaveEdge always builds that fringe a
     fixed --int-weave-edge-inset from the BOX's left side, so the way to hide
     it is to push the box off-screen to the left — the ragged ending then falls
     outside the viewport and the weave simply bleeds off the edge. The grid is
     rebuilt against this box on resize, so the extra width is real pattern, not
     a stretch. */
  .int-weave {
    inset: 0 0 0 -240px;
    height: auto;
    /* Full-bleed: the weave is the fold's background, with the copy sitting on
       top of it. Only short feathers at the very top and bottom so it doesn't
       start/stop on a hard line as the fold scrolls in and out. */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 12%, #000 90%, transparent 100%);
    mask-image: linear-gradient(to bottom, transparent 0%, #000 12%, #000 90%, transparent 100%);
  }
  .int-logo-mark { --int-logo-size: 24px; }
  /* Marks read hotter at phone size (small + dense on a dark field), so drop the
     desktop lift and hold them just under full opacity. Knobs: the 0.8 opacity
     and the brightness — both go up toward 1 for the desktop look. */
  .int-logo-mark img { filter: brightness(0.95) saturate(1.1); }
  .is-phase-2 .int-logo-mark.is-on { opacity: 0.8; }

  /* The fold's scroll LENGTH, not its frame rate, is what reads as lag on a
     phone: sticky 100vh + 40vh + 100vh means ~2.4 screens of scrolling during
     which the visuals barely move. Roughly halve it — the phase swap still has
     room to read, but the fold releases much sooner. */
  /* 44 + 120 = 164vh, which is EXACTLY the copy's own travel (off()/0.5 =
     1.64 * innerHeight) — so the phases move 1:1 with the scroll rather than
     racing it. Mirrored by D1 in initFoldIntegrations; keep the two in step. */
  .integrations-phase-block { min-height: 120vh; }
  .integrations-phase-block[data-phase="1"] { min-height: 44vh; }

  /* ---- Layer 3 on mobile: OUT of the sticky scene. The scene stays two-phase
     (JS gates segment B on this width too); the phase-3 spacer and the in-scene
     layer are dropped, and the plain .fold-agents list below the scene carries
     the same content instead. */
  .integrations-phase-block[data-phase="3"] { min-height: 0; display: none; }
  .int-layer3 { display: none; }
  .int-agents { display: none; }
  .fold-agents { display: block; }
  /* The base padding-top reserves --int-weave-overhang (150px) on top of its own
     clamp — that reserve exists so the DESKTOP weave, which hangs below the
     sticky scene, has somewhere to bleed. On a phone the weave is inset:0 inside
     the scene and masked at its bottom edge, so nothing hangs down and the
     reserve is pure dead space: 243px of nothing between layer 2 and layer 3 on
     a 664px screen. Drop the reserve, keep a normal fold gap. */
  .fold-agents { padding-top: clamp(56px, 9vh, 88px); }
  /* The scene ends with an unavoidable TAIL: .integrations-sticky is 100vh and
     stops sticking one sticky-height before the scene's bottom, so the last
     ~100vh of the scene is the sticky scrolling away with layer 2's copy already
     gone — a full empty screen between layer 2 and layer 3. Pull layer 3 up into
     that tail so it arrives while the weave is still leaving. Knob: more negative
     = layer 3 arrives sooner / less dead space. Don't exceed ~-60vh or it starts
     colliding with layer 2's copy on its way out. */
  .fold-agents { margin-top: -26vh; }
  .agents-subtitle { margin-bottom: clamp(36px, 6vh, 56px); }

  /* ---- Operator fold ----------------------------------------------------- */
  /* Cards already stack at 960px; just tighten the tail on a phone. On a narrow
     screen the card can no longer be as wide as the longest title, so the
     one-line rule is released here rather than letting it overflow. */
  .operator-role-title { white-space: normal; }
  .fold-operator { padding-bottom: clamp(64px, 11vh, 128px); }

  /* ---- Fold 5: final CTA ------------------------------------------------- */
  /* clamp floor of 150px top AND bottom is 300px of dead band on a phone. */
  .fold-cta { padding: clamp(90px, 12vh, 130px) var(--fold-inset); }
  /* The desktop clamp floors at 34px, sized against a 758px column — at 390px
     that's five cramped lines. 26px holds the three authored <br> lines plus
     one wrap. Knob: the 26px floor / 8vw slope. */
  .cta-headline {
    font-size: clamp(26px, 8vw, 40px);
    letter-spacing: var(--ls-tight-lg);
    margin-bottom: clamp(28px, 5vh, 40px);
  }
  /* The button is content-width on desktop; give it a phone-sized cap so it
     reads as a deliberate pill rather than a stub. */
  .cta-button { padding: 12px 24px; }

  /* ---- Footer ------------------------------------------------------------ */
  .footer-legal { gap: 20px; }
  .footer-legal-links { gap: 18px; }
}

/* ===========================================================================
   SHORT VIEWPORTS — fold 3 fit
   ---------------------------------------------------------------------------
   Height, not width, is the constraint here: a MacBook 14" with DevTools open
   is still 1512 wide, so none of the narrow-width block above applies. The
   stacked fold-3 content is taller than the viewport, and `justify-content:
   center` on an OVERFLOWING flex column splits the overflow evenly top and
   bottom — which slides the heading up under the fixed nav. Same failure mode
   the mobile block documents, so the same fix: pin the stack to the top, clear
   the nav explicitly, and buy back height from the gaps, the bottom padding's
   weave-overhang reserve, and the media cap.
   =========================================================================== */
/* TIER 1 — NOTHING SHRINKS HERE. This tier only changes WHERE the stack sits,
   never how big anything is, so it's safe to apply broadly: the only bug it
   fixes is the heading hiding under the nav, which happens the moment the stack
   is taller than the viewport at any size. */
@media (max-height: 1150px) {
  .fold-compliance-pin {
    justify-content: flex-start;
    padding-top: 96px;                 /* 70px nav + breathing room */
  }
  /* The desktop negative pull exists to lift the titles inside a CENTERED
     stack; with flex-start it just drags them back under the nav. */
  .fold-heading { margin-top: 0; top: 0; }
}

/* TIER 2 — the first tier that actually makes things SMALLER, so it starts well
   below any full-size desktop window. 1000 is set just above the MacBook 14" viewport (1512x982) so the
   laptop IS covered, while leaving every taller monitor untouched. Raise this
   number and you start shrinking large monitors; lower it past 982 and the
   MacBook stops being handled.

   Gated on WIDTH at 1536px — the standard "2XL" desktop breakpoint (Tailwind
   2xl). Everything at or below it is laptop-class (a MacBook 14 is 1512); above
   it is an external desktop display (1920+, 2560, 4K).
   Width, not height, because it is the conventional breakpoint axis AND the only
   one that works here: a 1920x1080 monitor viewport is ~950, SHORTER than the
   MacBook 982, so no height threshold catches one without the other.
   The values inside are still keyed to vh, so the fold keeps responding to a
   resized/short window — width picks the TIER, height scales within it. */
@media (max-width: 1535px) {
  .fold-compliance-pin {
    gap: clamp(20px, 3.4vh, 42px);     /* was a flat 50px */
    /* The weave overhang reserve is what fold-2 needs, not fold-3 — trim it. */
    padding-bottom: calc(clamp(28px, 5vh, 60px) + var(--int-weave-overhang) * 0.4);
    /* Cap the mockup off viewport HEIGHT (normally 56vh) so the biggest block in
       the stack gives first. The 720 tier below takes it further. */
    --fold-media-width: min(66.6vw, calc(51vh * 1239 / 672));
  }
  /* The base clamp is keyed to vw, so on a 1512-wide laptop it sits pinned at
     its 22px ceiling no matter how short the window gets. Re-key it to vh here
     so the type responds to the axis that's actually constrained. */
  .fold-heading-line { font-size: clamp(17px, 2.24vh, 22px); }
  .fold-column       { font-size: clamp(13px, 1.63vh, 16px); }
}

/* WIDE DESKTOP (>= 2XL) — the exact inverse of the laptop tier above, so the two
   never overlap. External monitors only; the MacBook at 1512 is untouched.
   Nudges the titles DOWN toward the image, closing the titles->image gap. Uses
   `top` rather than a margin so it doesn't re-centre the flex stack, and rather
   than `transform` because GSAP owns this element's transform. */
@media (min-width: 1536px) {
  .fold-heading {
    position: relative;
    top: 14px;
  }
}

@media (max-height: 720px) {
  .fold-compliance-pin {
    padding-top: 84px;
    gap: clamp(14px, 2.4vh, 26px);
    padding-bottom: calc(24px + var(--int-weave-overhang) * 0.35);
    --fold-media-width: min(66.6vw, calc(35vh * 1239 / 672));
  }
}


/* ============================================================================
   PAGE-WIDE PATTERN — let the EXISTING sticky pattern (front lattice + its
   .hero-pattern-back echo) keep going past .scene-compliance for the rest of
   the page. No new layer: the same wrap switches sticky -> fixed, and the
   sections after it go transparent so it shows through.
   ========================================================================== */
.hero-pattern-wrap {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  margin-bottom: 0;
  z-index: 0;
}
.fold-agents,
.fold-operator,
.fold-cta,
.site-footer { background: transparent; }

/* ---------------------------------------------------------------------------
   FILM GRAIN. One fixed, pointer-transparent tile repeated over the viewport,
   screen-blended so it ADDS light — overlay/soft-light are backdrop-dependent
   and all but vanish on a near-black page like this one. Its job is texture,
   but also dithering: it breaks up the banding a flat #070609 ground shows
   across a large gradient.

   The noise is a data-URI SVG (feTurbulence, stitchTiles so the tile is
   seamless; saturate 0.95 keeps a trace of chroma; the feFuncA slope/intercept
   pair is a contrast curve on the alpha — 2.35 gives hard specks, few mid-greys).
   The tile is authored at 168px and drawn at 80% (134.4px), which resizes the
   specks with no re-render.

   z-index 9 keeps it UNDER the nav (which is 10). Motion — the "boil" — lives
   in script.js (initGrain); the still tile here is the whole effect without JS.
   ------------------------------------------------------------------------ */
.grain-overlay {
  position: fixed;
  inset: 0;
  z-index: 9;
  pointer-events: none;
  opacity: 0.11;
  mix-blend-mode: screen;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22168%22%20height%3D%22168%22%3E%3Cfilter%20id%3D%22n%22%20x%3D%220%22%20y%3D%220%22%20width%3D%22100%25%22%20height%3D%22100%25%22%3E%3CfeTurbulence%20type%3D%22fractalNoise%22%20baseFrequency%3D%220.480%22%20numOctaves%3D%223%22%20seed%3D%227%22%20stitchTiles%3D%22stitch%22%2F%3E%3CfeColorMatrix%20type%3D%22saturate%22%20values%3D%220.95%22%2F%3E%3CfeComponentTransfer%20result%3D%22n%22%3E%3CfeFuncA%20type%3D%22linear%22%20slope%3D%222.35%22%20intercept%3D%22-0.675%22%2F%3E%3C%2FfeComponentTransfer%3E%3C%2Ffilter%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20filter%3D%22url(%23n)%22%2F%3E%3C%2Fsvg%3E");
  background-size: 134.4px 134.4px;
  background-repeat: repeat;
}

/* ============================================================================
   MOBILE TYPE SCALE  (<= 620px)
   ----------------------------------------------------------------------------
   The site's type is sized with clamp()s keyed to vw. On a 393px phone every one
   of them sits pinned at its FLOOR — floors that were chosen as "smallest still
   OK on a laptop", not as phone sizes. The result read as a shrunken desktop.
   This block re-states the floors for phones only. HARD RULE: nothing below 12px.
   Last in the file, so it wins over the clamps above by source order.
   To retune: change the px here; the desktop clamps above are untouched.
   ========================================================================== */
@media (max-width: 620px) {
  /* ---- Fold 2 / layer 1 + 2 (Integrations copy) ------------------------- */
  .int-eyebrow   { font-size: var(--fs-xs); letter-spacing: var(--ls-wide); }
  .int-title     { font-size: 28px; line-height: var(--lh-tight); }
  .int-subtitle  { font-size: var(--fs-ui); line-height: var(--lh-body); letter-spacing: var(--ls-meta); }
  /* Layer 2's agent rows */
  .int-agent-name { font-size: 17px; }
  .int-agent-desc { font-size: var(--fs-small); }

  /* ---- Fold 2 / layer 3 (Agentic Workers carousel) ---------------------- */
  .agents-eyebrow  { font-size: var(--fs-xs); letter-spacing: var(--ls-wide); }
  .agents-heading    { font-size: 28px; line-height: var(--lh-tight); }
  .agents-subtitle { font-size: var(--fs-ui); line-height: var(--lh-body); }
  .agent-name      { font-size: 20px; }
  .agent-desc      { font-size: var(--fs-body-s); line-height: var(--lh-body); }

  /* ---- Fold 4 Operator --------------------------------------------------- */
  /* No hover on a phone, so the ornament's draw-on-hover never fires: show it
     WHOLE at rest instead (undo .op-orn-rest's undrawn state), and swap the
     card-hover colour handoff for a slow pendulum between the two card accents.
     Cycle: 6s total — hold each accent ~2.2s, crossfade ~0.8s. Knobs: the 6s
     duration and the two hexes (they mirror --role-accent on the cards). */
  .op-orn-rest {
    stroke-dashoffset: 0;
    stroke-opacity: 1;
    transition: none;
  }
  /* 44vw of a phone is a postage stamp — widen it a step, and thin the
     (non-scaling) strokes to suit the smaller motif. Plain white: the accent
     pendulum (orn-accent-swing below, kept for reference) was tried and pulled.
     Knobs: the 66vw width and the 2.5 stroke. */
  .operator-ornament { width: min(240px, 58vw); }
  .operator-ornament path { stroke-width: 1.3; }
  /* Stacked cards need far less air than the desktop's 80px side-by-side gap. */
  .operator-roles { gap: 24px; }
  /* Desktop's 39/47px card padding eats most of a phone's width — tighten it. */
  .operator-role { padding: 22px 22px; }
  @keyframes orn-accent-swing {
    0%, 37%   { color: #5d6de5; }   /* vCISO blue */
    50%, 87%  { color: #e55d5d; }   /* FDE red   */
    100%      { color: #5d6de5; }
  }
  @media (prefers-reduced-motion: reduce) {
    .operator-ornament { animation: none; }
  }
  .operator-eyebrow    { font-size: var(--fs-xs); letter-spacing: var(--ls-wide); }
  .operator-heading      { font-size: 28px; line-height: var(--lh-tight); }
  .operator-intro      { font-size: var(--fs-body-s); line-height: var(--lh-loose); }
  .operator-role-title { font-size: 17px; }
  .operator-role-body  { font-size: var(--fs-body-s); line-height: var(--lh-loose); }
  .operator-link       { font-size: var(--fs-body-s); }

  /* ---- Hero + fold 3 ----------------------------------------------------- */
  .hero-subcopy                            { font-size: 17px; line-height: var(--lh-body); }
  .fold-heading-line                       { font-size: 22px; }
  .fold-column                             { font-size: var(--fs-body-s); line-height: var(--lh-loose); }
  .fold-dock-label,
  .fold-compliance--alt .fold-dock-label   { font-size: var(--fs-small); }
  .fold-compliance--alt .fold-heading-line { font-size: 24px; }
  .fold-compliance--alt .fold-column       { font-size: var(--fs-body-s); }
  .fold-compliance--emblem .fold-heading-line { font-size: 24px; }
  .fold-compliance--emblem .fold-sub       { font-size: var(--fs-body-s); }

  /* ---- Fold 5 CTA -------------------------------------------------------- */

  /* ---- Footer ------------------------------------------------------------ */
  .footer-wordmark   { font-size: 18px; }
  .footer-tagline    { font-size: var(--fs-meta); }
  .footer-col-title  { font-size: var(--fs-small); }
  .footer-link       { font-size: var(--fs-body-s); }
  .footer-copyright,
  .footer-legal-links .footer-link { font-size: var(--fs-small); }

  /* ---- Contact ----------------------------------------------------------- */
  .contact-subhead   { font-size: var(--fs-ui); line-height: var(--lh-body); }
  .contact-label     { font-size: var(--fs-meta); }
  /* 16px is NOT a style choice — iOS Safari auto-zooms the page on focus for
     any input below 16px. Never lower this. (See the note near .contact-input.) */
  .contact-input     { font-size: var(--fs-ui); }
  .contact-error     { font-size: var(--fs-small); }
  .contact-fineprint { font-size: var(--fs-small); }
  .contact-alt       { font-size: var(--fs-meta); }
  .contact-success-desc { font-size: var(--fs-body-s); }

  /* ---- Pricing ----------------------------------------------------------- */
  .pricing-tier-desc     { font-size: var(--fs-body-s); }
  .pricing-bullets       { font-size: var(--fs-meta); }
  .pricing-tier-cta      { font-size: var(--fs-ui); }
  /* On a phone the cards stack, so `margin-top: auto` has no tall sibling to
     stretch against and collapses to ~0 — the button ends up sitting right on
     the last bullet. The desktop min-gap pad tops out at 36px against viewport
     height, which at phone heights is only ~14px. So the gap is set outright on
     the button instead (2026-08-06, "more gap between button and text"). Knob:
     the margin-top — it IS the gap. */
  .pricing-bullets       { padding-bottom: 0; }
  .pricing-tier-cta      { margin-top: 34px; }
  .pricing-table         { font-size: var(--fs-meta); }
  .pricing-th-feature    { font-size: var(--fs-xs); }
  .pricing-note          { font-size: var(--fs-small); }

  /* Comparison table: ALL THREE tiers must fit the screen — no sideways scroll.
     The 640px min-width is what forced the scroll, so it goes; then the feature
     column is capped at 40% and the three tier columns split the rest evenly,
     with tight padding and small type so the ticks stay on one line.
     Knobs: the 40%/20% widths, `padding: 8px 2px`, and the tier name font. */
  .pricing-table-scroll { overflow-x: visible; }
  .pricing-table { min-width: 0; font-size: var(--fs-xs); table-layout: fixed; }
  .pricing-table th,
  .pricing-table td { padding: 8px 2px; }
  .pricing-table th:first-child,
  .pricing-table td:first-child { width: 40%; padding-right: 8px; }
  .pricing-table th:not(:first-child) { width: 20%; }
  .pricing-th-name { font-size: var(--fs-small); }
  .pricing-th-feature { font-size: 10px; letter-spacing: var(--ls-wide); }
  .pricing-table thead th { padding-top: 22px; padding-bottom: 12px; }
  .pricing-table .yes::before { width: 13px; height: 13px; vertical-align: -2px; }
  .pricing-group td { font-size: var(--fs-body-s); padding-top: 48px; padding-bottom: 20px; }
  .pricing-table tbody tr:first-child.pricing-group td { padding-top: 32px; }
  .pricing-cta-sub       { font-size: var(--fs-body-s); }
}

/* PULL-TO-REFRESH CUE (mobile only; created by script.js when the gesture starts
   at scroll 0). Lenis's `syncTouch` holds the page dead still at the top, so
   there is no rubber-band to read the pull against — this is the affordance that
   replaces it. `position: fixed` and pointer-events off: it must never take a
   touch away from the gesture it is reporting on. Knobs: `top` (resting offset,
   sits just under the nav), the `is-armed` colour, and PULL_REFRESH in
   script.js, which decides when `is-armed` lights up. */
.pull-refresh-cue {
  position: fixed;
  top: 8px;
  left: 50%;
  z-index: 200;
  pointer-events: none;
  opacity: 0;
  padding: 6px 14px;
  border-radius: 999px;
  background: rgba(31, 31, 31, 0.82);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.62);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: opacity 0.2s ease, color 0.15s ease, border-color 0.15s ease;
}
.pull-refresh-cue.is-armed {
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.4);
}

/* Chapter-level halo mute — script.js toggles `no-glow` from fold3Shape.glow[i]. */
.fold-compliance--emblem .fold-emblem.no-glow [id='primary shape'] { filter: none; }

/* THE DRAW-IN FLARE (2026-08-06) — a bloom on the primary motif the moment its
   chapter finishes drawing, then gone. It CANNOT ride --emblem-halo: every
   chapter has `glow: false` (fold3Shape.glow), so both halo levels are muted to
   `filter: none` and a --fx-bloom added to a radius nobody renders shows
   nothing. So the flare declares its OWN filter, under a class script.js only
   holds for the length of the tween (initFold3EmblemFx's flareBloom):
     --fx-bloom  radius in px, tweened peak -> 0
     --fx-flare-col  the flare colour ALREADY carrying the tween's alpha,
                 written from JS as plain rgba() (color-mix with a calc()
                 percentage is unreliable in WebKit and killed the whole filter
                 on mobile). Without the fading alpha a 0-blur drop-shadow is a solid
                 tinted copy of the stroke sitting right under it, so the tint
                 stayed at full strength all the way down and then SNAPPED off
                 when the class was dropped (reported 2026-08-06).
     --fx-glow   the flare colour (the chapter's own stroke colour)
   The `.no-glow` copy of the selector is what beats the mute above — same
   classes plus `.is-flaring`, so it wins on specificity, not source order. */
.fold-compliance--emblem .fold-emblem.is-flaring [id='primary shape'],
.fold-compliance--emblem .fold-emblem.is-flaring.no-glow [id='primary shape'] {
  /* THREE stacked shadows, not two: chained drop-shadows COMPOUND (each blurs
     the previous result), so brightness comes from stacking short ones, not
     from a bigger radius — widening alone just spreads the same alpha thinner
     and reads identical (2026-08-06, user: "it didn't do anything"). */
  filter: drop-shadow(0 0 calc(var(--fx-bloom, 0) * 0.35px) var(--fx-flare-col, transparent))
    drop-shadow(0 0 calc(var(--fx-bloom, 0) * 0.9px) var(--fx-flare-col, transparent))
    drop-shadow(0 0 calc(var(--fx-bloom, 0) * 1.8px) var(--fx-flare-col, transparent));
}

