/* =============================================================
   PALARY — PLATFORM PAGE
   Page-specific styles, built section by section per STATUS-platform.md.
   Loaded after tokens/base/components/sections. Reuses existing tokens
   and utility classes (.section, .bg-dark, .container, .grid, .col-N,
   .eyebrow, .h0, .btn) — no invented values.
   ============================================================= */

/* Button radius on this page is 16px, deviating intentionally from the
   index page's --rd-8 (see STATUS-platform.md, "Buttons"). Scoped to this
   page only until the token-vs-override question is settled. */
.platform-page .btn { border-radius: 16px; }

/* ---------- HERO (.hero--split) ----------
   Light section (--color-bg-light), copy left / visual right. A separate
   class from the index page's .hero — the difference is layout structure
   (horizontal split vs. vertical stack), not light vs. dark. Text colors
   below are inherited from .bg-light's cascade (sections.css); no dark-
   context overrides needed here. */
.hero--split .grid { align-items: center; }

.hero--split__copy { display: flex; flex-direction: column; align-items: flex-start; }
.hero--split__copy h1 { margin-block: var(--gap-24); }
.hero--split__subline { color: var(--color-text-inverse-secondary); max-width: 46ch; margin-bottom: var(--gap-40); }
.hero--split__ctas { display: flex; gap: var(--gap-12); flex-wrap: wrap; }

/* Ambient particle background (vanilla-JS canvas port of magicui's
   "Particles" on 21st.dev — js/platform-hero-particles.js does all the
   drawing via a single <canvas>, this file only positions the layer).
   Deliberately a different technique from the earlier, abandoned
   Background Paths attempt (72 individually CSS-animated SVG paths,
   which forced a repaint per path per frame and corrupted paint in
   Chrome) — a canvas is one compositing surface, so there's no
   per-element animation here for the browser to choke on. Same
   pinned-behind-content scaffolding as that attempt though: strictly
   absolute + excluded from this section's auto-height calculation, so
   it can't grow/reflow the hero or push the card stack. */
.hero--split { position: relative; }
.hero--split > .container { position: relative; z-index: 1; }
.hero--split__particles {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.hero--split__particles canvas {
  position: absolute;
  inset: 0;
  display: block;
}

/* overflow:visible at every breakpoint. Originally needed because the
   diagonally-offset middle/back cards bled sideways past the column edge
   at rest (Yoann flagged the resulting crop as a visual bug, 8 Jul).
   Now that the stack is centered (9 Jul) the back layers no longer bleed
   sideways at rest — this is kept because the exiting front card still
   glides right past the stage/viewport edge during the auto-cycle, and
   still relies on html/body's overflow-x:clip (base.css) to avoid
   causing horizontal scroll. */
.hero--split__visual { display: flex; flex-direction: column; overflow: visible; }

/* Card-visual: real art landed 7 Jul (three SVGs in assets/platform/ —
   picture-card-1, logo-card-2 silver, logo-card-3 dark), each already
   containing its own chip/VISA-wordmark/label art, so no overlay text
   is added here. The loop mechanic is Yoann's explicit spec: a flat,
   non-3D overlapping stack, front card auto-cycling to the back.
   Back layers taper down via scale() (9 Jul) so the stack reads as
   depth rather than three same-size cards fanned sideways.
   Stage ratio matches a physical card (85.6 x 53.98mm = 1.586:1). */
.hero--split__card-stage {
  position: relative;
  aspect-ratio: 1.586 / 1;
  width: 100%;
  max-width: 440px;
  /* Centered (not flush-right) in its 40%-wide column at every breakpoint:
     flush-right left ~58px of dead space stranded between the copy block
     and the card at desktop widths, with almost no clearance left for the
     back card's peek-offset before it hit the true viewport edge. Yoann's
     8 Jul ask: even out the left/right space and give the peeking cards
     more room. */
  margin-inline: auto;
  /* Headroom for the back card's upward translate below — it's a paint-
     only transform (position:absolute + inset:0 on the card itself), so
     it isn't accounted for by normal layout and would otherwise paint
     straight over the copy block above at narrow (stacked) widths, where
     the grid's row gap alone isn't enough clearance. Kept in sync with
     --stack-offset-2-y via calc() rather than a second literal. */
  margin-top: calc(-1 * var(--stack-offset-2-y));
  /* Depth offsets and exit distance. Motion-tuning literals, not
     spacing-scale values — same accepted-hardcoded-literal precedent as
     .paths-stack__widget's terminal-chrome colors or .portal-stepper's
     6:5 aspect-ratio. Revisit once real card art lands.
     x is 0 for both back layers (9 Jul, Yoann's explicit ask): centered
     top-peek instead of the earlier diagonal offset — cards taper in
     size via scale() below rather than fanning sideways. Bonus: since
     nothing bleeds past the stage horizontally at rest anymore, this
     also removes the desktop/mobile side-crop the diagonal version had
     at rest (the exit glide still slides right and clips at the
     viewport edge, which is expected/kept). */
  --stack-offset-1-x: 0px;
  --stack-offset-1-y: -28px;
  --stack-offset-2-x: 0px;
  --stack-offset-2-y: -52px;
  --stack-exit-x: 567px;
  --stack-exit-y: -14px;
  /* 3D rendering context for the hover tilt below — no-op on its own,
     only takes effect once a child gets a rotateX/rotateY. */
  perspective: 1000px;
}
.hero--split__card {
  position: absolute;
  inset: 0;
  border-radius: var(--rd-16);
  overflow: hidden;
  opacity: 1;
  transition: transform 1.4s cubic-bezier(0.25, 1, 0.3, 1), z-index 1.4s steps(1, start);
}
.hero--split__card img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero--split__card--pos-front { z-index: 3; transform: translate(0, 0) scale(1); }
.hero--split__card--pos-middle {
  z-index: 2;
  transform: translate(var(--stack-offset-1-x), var(--stack-offset-1-y)) scale(0.96);
  transition-delay: 0.04s;
}
.hero--split__card--pos-back {
  z-index: 1;
  transform: translate(var(--stack-offset-2-x), var(--stack-offset-2-y)) scale(0.92);
  transition-delay: 0.08s;
}
/* Exiting card: opacity stays 1 throughout (Yoann's explicit "no
   disappearing/fading" rule) — it glides off past the section edge,
   safely clipped by html/body's overflow-x: clip (base.css), same as
   any other off-screen exit. z-index jumps to the top immediately
   (steps(1,start)) so it visually passes over, not under, the other
   two cards while sliding out. */
.hero--split__card.is-exiting {
  z-index: 4 !important;
  transform: translate(var(--stack-exit-x), var(--stack-exit-y)) rotate(2deg);
  transition: transform 0.95s cubic-bezier(0.25, 1, 0.4, 1);
  transition-delay: 0s !important;
}
/* Ambient auto-loop, not essential content — same reduced-motion
   philosophy as motion.js elsewhere on this page: freeze at rest
   instead of looping. */
@media (prefers-reduced-motion: reduce) {
  .hero--split__card { transition: none; }
}

/* Hover tilt (7 Jul, Yoann's explicit ask) — front card only, a fixed
   angle, not cursor-tracking. Scoped to :not(.is-exiting) so hovering
   mid-exit doesn't fight the exit glide's own transform. Deliberately
   a faster, separate transition (0.4s) from the base .hero--split__card
   rule's 1.4s — that slower duration is reserved for the auto-cycle's
   position swap and must stay untouched, so this is scoped to the
   :hover state itself rather than added to .hero--split__card--pos-front
   directly (which would also slow down every incoming card's ease into
   place). Gated behind `no-preference` — this is motion regardless of
   trigger, same reduced-motion posture as the auto-loop itself. */
@media (prefers-reduced-motion: no-preference) {
  .hero--split__card-stage:hover .hero--split__card--pos-front:not(.is-exiting) {
    transform: rotateX(3deg) rotateY(-6deg);
    transition: transform 0.4s ease-out;
  }
}

@media (max-width: 1023px) {
  /* Stacked layout only — desktop is side-by-side, no "gap above the
     cards" to fix there. The grid's own row-gap (16-20px depending on
     breakpoint, --grid-gutter) was the only spacing between the CTA row
     and the cards below it — noticeably tighter than the 24px/40px
     rhythm already established above it (h1→subline, subline→CTA).
     Reuses --gap-40 (the same token driving the subline→CTA gap) rather
     than inventing a new value, so the two gaps read as the same beat. */
  .hero--split__ctas { margin-bottom: var(--gap-40); }
}

/* ---------- PROOF + WHAT (merged, two-column) ----------
   Matches Figma node 1415:44285 ("section-platform-intro"): two equal
   (flex:1) columns — "what" statement left, proof stats right — gap-80
   between them, gap-100 vertical section padding. Reuses .tag verbatim
   for "Live in Produktion" (components.css — default variant's colors
   already match this spec exactly, no override) and .stat/.stat__num/
   .unit/.stat__label from sections.css (same markup + initCounters() in
   motion.js). Numbers stay forest-700 (index page uses lime, reserved for
   CTAs, not applicable here).

   Real <h2> for the statement, styled at h3 visual size (28px) —
   deliberately smaller than Portal Stepper's own <h2 class="h2"> (40px)
   directly below, so the two sibling h2s don't visually compete. Not h4:
   h4 (24/18/16 across desktop/tablet/mobile) collapses to the exact same
   pixel value as the support line's --fs-xl (20/18/16) at both the
   tablet and mobile steps — h4 and xl were never designed as a pair
   (the site's real established pairing is h2+text-xl, which always
   stays ordered). h3 (28/22/20) stays ordered above xl (20/18/16) at
   every step and still comfortably below Portal Stepper's h2 (40/28/24).

   Stat numbers scoped down from the shared --fs-h2 (40px) to --fs-h3
   (28px) ONLY in this component: at a 50/50 split the stats-grid gets
   roughly half the container width instead of the full row the original
   Proof Bar had, and "2.000.000+" set at full h2 size doesn't fit three-
   up in that halved space without crowding/overflow — confirmed by
   measuring rendered width, not assumed from the Figma canvas alone.
   --fs-h3 is an existing shared token, not a new hardcoded value.

   Spacing scales proportionally across breakpoints, mirroring the same
   ×0.8-per-step ratio the site's own --section-pad already uses
   (128→100→80): section padding 100→80→64, column/stack gap 80→64→48,
   inner gaps (header/proof/stats) 32→32→24 — the last one unchanged at
   tablet, then dropped a step at mobile, same as the stats-grid's own
   existing 32→24 reduction. */
.proof-what { padding-block: var(--gap-100); }
.proof-what__row {
  display: flex;
  align-items: flex-start;
  gap: var(--gap-80);
}
.proof-what__header {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap-32);
}
.proof-what__lead {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  color: var(--color-text-inverse-primary);
}
.proof-what__support {
  margin: 0;
  max-width: 452px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--fs-xl);
  line-height: var(--lh-xl);
  letter-spacing: var(--ls-xl);
  color: var(--color-text-inverse-secondary);
}
.proof-what__proof {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-32);
}
.proof-what__stats {
  display: flex;
  width: 100%;
  gap: var(--gap-32);
  flex-wrap: wrap;
}
.proof-what .stat {
  /* Natural content width, not flex:1 equal-thirds — Figma's own metadata
     shows the three stat containers at different measured widths
     (157/217/125px @ 1440 canvas), because "2.000.000+" is inherently
     wider than "3". Forcing equal thirds shrinks this stat's box below
     its own text width, and with no wrap the number bleeds into the next
     column — reproduced at 1024px before this fix. */
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-16);
}
.proof-what .stat__value,
.proof-what .stat__value .unit {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  color: var(--forest-700);
  white-space: nowrap;
}
.proof-what .stat__label {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--fs-sm);
  line-height: var(--lh-sm);
  color: var(--color-text-inverse-primary);
}

@media (max-width: 1023px) {
  .proof-what { padding-block: var(--gap-80); }
  .proof-what__row { flex-direction: column; gap: var(--gap-64); }
  .proof-what__proof { width: 100%; }
}
@media (max-width: 767px) {
  .proof-what { padding-block: var(--gap-64); }
  .proof-what__row { gap: var(--gap-48); }
  .proof-what__header,
  .proof-what__proof { gap: var(--gap-24); }
  .proof-what__stats { flex-direction: column; align-items: flex-start; gap: var(--gap-24); }
}

/* ---------- PORTAL FEATURES STEPPER (.portal-stepper) ----------
   Section 3 of 8. Supersedes STATUS-platform.md's "tabbed tour" plan and its
   killed-pattern entries for numbered-stepper/scroll-pinning/scroll-highlight
   — Yoann's 2 July override, scoped to this section only (those entries
   still apply elsewhere on the page unless separately re-approved).

   Deliberately does NOT use the shared .section utility (its padding-block
   would decouple the 400vh box height from the literal 4-segment scroll
   math) or the shared .grid/.col-N utilities (they force-stack every
   [class*="col-"] below 1023px — see base.css:134-137 — which would break
   the pinned two-column layout at tablet width). The 40/60 split is
   hand-rolled flex, scoped to this component.

   top/height account for the sticky .nav (--nav-height, 64px) sitting above
   this section — without the offset the nav would render over the top of
   the pinned content, since both are position: sticky; top: 0. */

.portal-stepper {
  position: relative;
  padding-top: var(--gap-64); /* breathing room below Proof Bar, above the
                                  new section-head (headline/subline were
                                  missing from the build — copy exists
                                  verbatim in palary-produkt-mockup.html) */
  --panel-dur: 0.55s; /* mid-range of the spec's 0.5–0.6s target;
                          component-scoped, only this transition needs it */

  /* Step-indicator roles, cross-referenced against Figma node 1259:25760
     on 3 Jul. Named per-role instead of inlining raw palette refs across
     every selector below — same scoping pattern as --panel-dur above. */
  --stepper-circle-size: 24px;
  --stepper-circle-bg-active: var(--forest-600);
  --stepper-circle-bg-inactive: var(--forest-100);
  --stepper-number-color-active: var(--charcoal-100);
  --stepper-number-color-inactive: var(--forest-600);
  --stepper-line-active: var(--forest-600);
  --stepper-line-inactive: var(--forest-200);
  --stepper-title-color-active: var(--charcoal-900);
  --stepper-desc-color-active: var(--color-text-inverse-secondary);
  --stepper-text-color-inactive: var(--charcoal-200);
}

/* Owns the 400vh scroll-math box that used to live directly on
   .portal-stepper — split out so the new section-head above (headline +
   subline) can sit in normal flow before the pinned region without
   disturbing js/platform-stepper.js's update(), which reads this element's
   offsetHeight/getBoundingClientRect() to derive scroll progress. */
.portal-stepper__scroll {
  position: relative;
  height: 400vh; /* 4 steps × 100vh — js/platform-stepper.js derives scroll
                    progress from this at runtime via offsetHeight, not a
                    hardcoded vh assumption */
  margin-top: var(--gap-32); /* half of the --gap-64 .section-head convention
                                 used elsewhere (e.g. .integration__layout) —
                                 intentionally tighter here since .pin sticks
                                 the stepper/image; the larger gap left too
                                 much dead space once it reaches the sticky
                                 top, per Yoann 3 Jul */
}

.portal-stepper__pin {
  position: sticky;
  top: var(--nav-height);
  height: calc(100vh - var(--nav-height));
  display: flex;
  align-items: center;
  overflow: hidden;
}

.portal-stepper__layout {
  display: flex;
  align-items: center;
  gap: var(--gap-64); /* wider than the sitewide --grid-gutter — more
                          breathing room between the step list and the
                          visual panel than a default content-grid gutter */
  width: 100%;
}

/* ----- Left column: progress line + step list ----- */
/* flex-shrink 1 (not 0) so the layout can absorb `gap` between columns —
   0 0 40%/60% would let basis(40%)+basis(60%)+gap overflow the container
   by exactly the gap width, since flex-shrink:0 forbids the shrink pass
   that would otherwise reconcile it. */
.portal-stepper__left { flex: 0 1 40%; max-width: 40%; }

.portal-stepper__steps {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--gap-48);
}

.portal-stepper__line-track,
.portal-stepper__line-fill {
  position: absolute;
  left: calc((var(--stepper-circle-size) / 2) - (var(--bd-1) / 2)); /* centers on the circle */
  width: var(--bd-1);
  top: 0;   /* set by measureLine()/update() in platform-stepper.js */
  height: 0;
}
.portal-stepper__line-track { background: var(--stepper-line-inactive); }
.portal-stepper__line-fill  { background: var(--stepper-line-active); }

.portal-stepper__step {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--gap-24);
}

.portal-stepper__step-circle {
  flex-shrink: 0;
  width: var(--stepper-circle-size);
  height: var(--stepper-circle-size);
  margin-top: 2px; /* optical alignment with title cap-height */
  border-radius: var(--rd-12); /* 12px on a 24px box == a true circle,
                                   bound to the token rather than 50% so
                                   this reads as an intentional radius
                                   choice, not a coincidence */
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--stepper-circle-bg-inactive);
  transition: background-color var(--dur-default) var(--ease-standard);
}
.portal-stepper__step.is-active .portal-stepper__step-circle {
  background: var(--stepper-circle-bg-active);
}

.portal-stepper__step-num {
  font-family: var(--font-body);
  font-size: 12px; /* Figma spec — no 12px token exists in the type scale,
                       this is the smallest size anywhere on the page */
  line-height: 1;
  font-weight: 400;
  color: var(--stepper-number-color-inactive);
  transition: color var(--dur-default) var(--ease-standard),
              font-weight var(--dur-default) var(--ease-standard);
}
.portal-stepper__step.is-active .portal-stepper__step-num {
  font-weight: 700;
  color: var(--stepper-number-color-active);
}

.portal-stepper__step-text { display: flex; flex-direction: column; gap: var(--gap-12); }

/* Wraps each .portal-stepper__step + its .portal-stepper__step-spacer.
   Plain block, no margin/padding/border of its own on desktop/tablet, so
   it shrink-fits its single visible child (the spacer is display:none
   there) and is visually a no-op — the step itself still reads as the
   flex item inside .portal-stepper__steps. */
.portal-stepper__step-slot { display: block; }

/* Hidden on desktop/tablet. On mobile (see media query below) this becomes
   a real block with a set height — the scroll "track" a sticky card holds
   its position for before the next card covers it. Deliberately a real
   sibling element, NOT padding-bottom on .portal-stepper__step-slot:
   confirmed by direct testing that padding-bottom on a sticky element's
   containing block is not included in the sticky range calculation in
   this browser (computed style correctly reports position:sticky, but the
   element scrolls as if static — silent failure, easy to miss). A real
   element with height (or min-height) works; padding does not. Costly to
   debug twice — don't swap this back to padding-bottom. */
.portal-stepper__step-spacer { display: none; }

/* Duplicate per-step image, hidden on desktop/tablet — shown only in the
   mobile/reduced-motion stacked fallback below, so each step's text and
   visual pair together in document order instead of all 4 screenshots
   dumping after all 4 text blocks. */
.portal-stepper__step-inline-panel { display: none; }

.portal-stepper__step-title {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-xl);
  line-height: var(--lh-xl);
  color: var(--stepper-text-color-inactive);
  transition: color var(--dur-default) var(--ease-standard);
}
.portal-stepper__step-desc {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-sm);
  color: var(--stepper-text-color-inactive);
  transition: color var(--dur-default) var(--ease-standard);
}
.portal-stepper__step.is-active .portal-stepper__step-title {
  color: var(--stepper-title-color-active);
}
.portal-stepper__step.is-active .portal-stepper__step-desc {
  color: var(--stepper-desc-color-active);
}

/* ----- Right column: visual panels -----
   6:5 (737:606) ratio. Panel images are the real white-labeled portal
   screenshots (site/assets/platform/step-*-visual.png, 1474×1212 native —
   1.216 ratio, near-exact match to 6:5's 1.2), not placeholders. No
   tokenized aspect-ratio custom property exists anywhere in the codebase
   (the one precedent, .hero-card-stage, is a hardcoded literal too) —
   hardcoding here is consistent with practice. */
.portal-stepper__right {
  position: relative;
  flex: 0 1 60%; /* see .portal-stepper__left comment re: shrink + gap */
  max-width: 60%;
  aspect-ratio: 6 / 5;
  border-radius: var(--rd-12);
  overflow: hidden;
  background: var(--charcoal-100);
}

.portal-stepper__panel {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(0);
  transition: opacity var(--panel-dur) var(--ease-out),
              transform var(--panel-dur) var(--ease-out);
  pointer-events: none;
}
.portal-stepper__panel-img {
  display: block;
  width: 100%;
  height: 100%;
  /* contain, not cover — the real screenshots (1474×1212, 1.216 ratio) are
     so close to the 6:5 box ratio (1.2) that contain letterboxes by only
     ~1% (imperceptible) while guaranteeing zero cropped content, instead
     of cover's crop-to-fill which was cutting into the bottom of the
     screenshots (table rows, etc). Panel's own background shows through
     the negligible letterbox sliver. */
  object-fit: contain;
  object-position: center;
}
/* Transient class, added then removed by JS in the same frame before
   .is-active, forcing the incoming panel's *starting* position without a
   transition, so the browser then animates .is-entering → .is-active. This
   is what makes entry (slide+fade) and exit (fade only, in place)
   asymmetric from a single .is-active toggle — the outgoing panel never
   gets .is-entering, so its transform never leaves translateY(0). */
.portal-stepper__panel.is-entering {
  transition: none;
  opacity: 0;
  transform: translateY(24px);
}
.portal-stepper__panel.is-active {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* ----- Tablet (768–1023px): 2-col split holds, no override needed -----
   Container narrows (tokens.css breakpoints), 40% left column still wraps
   titles/descriptions comfortably at this width. */

/* ---------- prefers-reduced-motion: fully flat, no pin, no sticky --------
   Applies at ANY viewport width, independent of the mobile block below.
   The pin/scrub (and the mobile sticky-image reveal, same reasoning) IS
   this section's interaction model, not decoration on top of otherwise-
   functional content — so users who've opted out of motion get the
   page's default no-scroll-jacking posture: a plain static list, nothing
   sticky, nothing scroll-linked. */
@media (prefers-reduced-motion: reduce) {
  .portal-stepper__scroll { height: auto; margin-top: 0; }
  .portal-stepper__pin {
    position: static;
    height: auto;
    display: block;
    padding-block: var(--gap-48);
    overflow: visible;
  }
  .portal-stepper__layout { flex-direction: column; align-items: stretch; gap: var(--gap-48); }
  .portal-stepper__left,
  .portal-stepper__right { flex: 1 1 auto; max-width: 100%; }
  .portal-stepper__line-track,
  .portal-stepper__line-fill { display: none; }
  .portal-stepper__steps { gap: var(--gap-32); }
  .portal-stepper__step,
  .portal-stepper__step-circle,
  .portal-stepper__step-title,
  .portal-stepper__step-desc,
  .portal-stepper__panel { transition: none; }
  .portal-stepper__step .portal-stepper__step-circle {
    background: var(--stepper-circle-bg-active);
  }
  .portal-stepper__step .portal-stepper__step-num {
    font-weight: 700;
    color: var(--stepper-number-color-active);
  }
  .portal-stepper__step .portal-stepper__step-title {
    color: var(--stepper-title-color-active);
  }
  .portal-stepper__step .portal-stepper__step-desc {
    color: var(--stepper-desc-color-active);
  }
  .portal-stepper__right { display: none; } /* replaced by the per-step inline panel below */
  .portal-stepper__step-inline-panel {
    display: block; /* overrides the display:none default above */
    position: relative; /* not sticky — reduced motion always gets a flat, static list */
    width: 100%;
    aspect-ratio: 4 / 5; /* taller than the desktop 6/5 — fills more of the
      mobile step's forced full-viewport height, was reading as too much
      empty space around a short wide image */
    border-radius: var(--rd-12);
    overflow: hidden;
    margin-top: var(--gap-16);
  }
  .portal-stepper__step-inline-panel img {
    display: block; width: 100%; height: 100%; object-fit: contain; object-position: center;
  }
}

/* ---------- Mobile (≤767px), motion allowed: stacking-cards reveal -------
   FLAGGED: no dedicated mobile design exists for this component (checked
   site/assets/platform/ — only the 4 desktop portal screenshots). Claude-
   authored, not a locked design — built per Yoann's brief: "the step
   element and the image should change as if they were stacking on each
   other like cards going over on scroll, from down to up."

   Pure CSS, no JS (confirmed against the gsap-scrolltrigger skill that a
   real pin isn't needed here — position:sticky + z-index is the lighter-
   weight native equivalent for a plain "next card covers previous" reveal;
   GSAP's pin is for cases needing scroll-synced transforms, not this).

   Mechanism: each whole step (circle + title + desc + image) is one
   position:sticky card, all sharing the same `top` offset so they land in
   exactly the same spot — and each has a higher z-index than the last, so
   card 2 visually slides up from below and covers card 1 once it reaches
   that shared spot, card 3 covers card 2, etc. `.portal-stepper__step-slot`
   (display:contents everywhere else) becomes each card's real, taller
   containing block here — its height beyond the card itself is the scroll
   "track": how far you scroll while the card stays stuck before the next
   one arrives and covers it. */
@media (max-width: 767px) and (prefers-reduced-motion: no-preference) {
  .portal-stepper__scroll { height: auto; margin-top: 0; }
  .portal-stepper__pin {
    position: static;
    height: auto;
    display: block;
    padding-block: var(--gap-48) 0;
    overflow: visible;
  }
  .portal-stepper__layout { display: block; }
  .portal-stepper__left { max-width: 100%; }
  .portal-stepper__right { display: none; }
  .portal-stepper__line-track,
  .portal-stepper__line-fill { display: none; }
  .portal-stepper__steps { display: block; }

  .portal-stepper__step-slot {
    display: block;
    position: relative;
  }
  /* Real spacer element, not padding-bottom on the slot — confirmed by
     direct testing that padding-bottom on a sticky element's containing
     block is NOT included in the sticky range calculation in this browser
     (position:sticky computed style reports correctly, but the element
     scrolls as if static). min-height/real content height works; padding
     does not. Costly to debug twice — don't swap this back to padding. */
  .portal-stepper__step-spacer {
    display: block;
    height: 45vh; /* pre-JS fallback only — layoutMobileStack() in
                      js/platform-stepper.js overrides this inline to
                      (measured card height + DWELL_BUFFER) on load/resize.
                      Real bug found 3 Jul: a flat 45vh (~380px on a
                      typical phone) is SHORTER than this component's
                      actual card height (~470-490px), which meant the
                      next card started sliding over the current one's
                      image before the current card had even finished
                      sticking — zero dwell, read as the image looking
                      "cropped". The spacer now always exceeds its own
                      card's height by DWELL_BUFFER, guaranteeing a real
                      undisturbed viewing window before the next card's
                      cover-sweep begins. Kept on the last step too (not
                      hidden) — without it, the last card has ~0 scroll
                      room to actually stick/be read: its slot ends almost
                      immediately since there's no dwell and no next slot
                      pulling up into it. The tradeoff is a bit of blank
                      trailing scroll after the section, which Product
                      Paths (next section, not yet built) will absorb into
                      once it exists below this one. */
  }

  .portal-stepper__step {
    position: sticky;
    top: var(--nav-height);
    z-index: 1;
    background: var(--color-bg-light); /* opaque — must fully cover the
                                           card stuck underneath it */
    padding-block: var(--gap-24);
    /* Card content (~470-490px) is shorter than the sticky area below the
       nav on most phones, leaving dead background space under the card
       while it's stuck — flagged by Yoann from a real device. A fixed
       pixel offset would need re-tuning per phone height, so instead the
       card fills the full remaining viewport height and centers its own
       content inside it — adapts to any screen automatically, no
       per-breakpoint values to maintain. */
    min-height: calc(100vh - var(--nav-height));
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* Crossfade: opacity/transform are set inline, per scroll frame, by
       js/platform-stepper.js's updateStackingFade() — continuous and
       scroll-scrubbed, not a fixed-duration CSS transition. A threshold-
       triggered version (add/remove a class, animate over a fixed 300ms)
       was tried first and shipped 9 Jul, but on a real finger-swipe
       (much faster than automated testing) a fixed duration can finish
       before it's perceived as motion — reads as a pop, not a fade. No
       `opacity`/`transform` default here: without JS the cards simply
       stay at the browser default (opacity 1, no transform), a safe
       static fallback rather than content that could get stuck invisible. */
  }
  /* increasing stack order so each later card paints over earlier ones
     once both are technically at the sticky offset at the same time */
  .portal-stepper__step[data-step="1"] { z-index: 2; }
  .portal-stepper__step[data-step="2"] { z-index: 3; }
  .portal-stepper__step[data-step="3"] { z-index: 4; }

  /* No single "active" step once this is a plain scrolling reveal — keep
     every card in the emphasized look instead of a stale dim/bright mix
     that implied a JS-driven state this layout no longer has. */
  .portal-stepper__step-circle { background: var(--stepper-circle-bg-active); }
  .portal-stepper__step-num { font-weight: 700; color: var(--stepper-number-color-active); }
  .portal-stepper__step-title { color: var(--stepper-title-color-active); }
  .portal-stepper__step-desc { color: var(--stepper-desc-color-active); }

  .portal-stepper__step-inline-panel {
    display: block;
    position: relative; /* the whole card is sticky now, not the image alone */
    /* Widened to match its own CARD's width instead of sitting inside the
       narrower text column it's nested under (indented past the circle +
       gap) — set precisely in JS (layoutMobileStack() in
       platform-stepper.js), since the pure-CSS "100vw + margin-left:50% +
       translateX(-50%)" full-bleed trick only self-centers correctly when
       the element's ancestor is itself centered in the viewport (this one
       isn't — confirmed by testing, don't reintroduce it). Deliberately
       does NOT bleed all the way to the viewport edge — the container's
       own side padding stays intact, and going past the card's own width
       let the image overflow past its opaque background, causing visible
       bleed-through between stacked cards. Widening to the card's own
       width still makes the image noticeably bigger/taller than the
       original text-column width, which is most of what closes the gap
       below it — the leftover gap is the intentional dwell spacer. */
    border-radius: var(--rd-12);
    aspect-ratio: 4 / 5; /* taller than the desktop 6/5 — fills more of the
      mobile step's forced full-viewport height, was reading as too much
      empty space around a short wide image */
    overflow: hidden;
    margin-top: var(--gap-16);
    /* Buffer below the image before the card's own bottom edge. Without
       this the next card's opaque background reaches the image almost
       immediately once it starts sliding up (layoutMobileStack() pulls the
       next slot up by exactly this card's measured offsetHeight, so
       whatever's included in that height IS the dwell buffer) — read by
       Yoann as the image getting "cropped" by the incoming card. Adding
       real space here (not just visual polish) is what fixes it; the
       value is picked up automatically by the JS measurement, no constant
       to update there. */
    margin-bottom: var(--gap-32);
    background: var(--charcoal-100);
  }
  .portal-stepper__step-inline-panel img {
    display: block; width: 100%; height: 100%; object-fit: contain; object-position: center;
  }
}

/* ---------- PRODUCT PATHS / ZWEI WEGE (.paths-stack) ----------
   Section 4 of 8. A second, explicit override of the "static build, no
   scroll effects" rule and the "Sticky card stack for integration"
   killed-pattern entry in STATUS-platform.md — Yoann confirmed 3 July, same
   override pattern as the Portal Stepper reversal (still applies everywhere
   else on the page unless separately re-approved).

   Mechanic: two full-width cards pinned over a 300vh track. Card 2
   (API-first, dark) slides up and fully covers card 1 (White-Label, light)
   while both cards' headline/subline/feature-rows cascade in on scroll,
   driven by a --r custom property per element (js/platform-paths.js).

   Deliberately hand-rolled, not .section/.grid/.col-N — same reasoning as
   .portal-stepper: .section's padding-block would decouple the 300vh box
   from the literal scroll math, and .grid > [class*="col-"] force-stacks
   below 1023px, which would break each card's internal two-column layout
   at tablet width.

   top/height account for the sticky .nav (--nav-height) exactly like
   .portal-stepper__pin — without the offset the nav would render over the
   pinned content, since both are position: sticky; top: 0. */

.paths-stack {
  /* Named once, component-scoped — same precedent as .portal-stepper's
     --stepper-circle-size: no token family exists for icon-chip sizing,
     so the literal is named and centralized rather than repeated inline. */
  --paths-feat-ico-size: 48px; /* matches Figma node 1278:36318's icon
                                   container exactly (was 36px, matched to
                                   palary-produkt-mockup.html before this
                                   section had a dedicated Figma frame) */
}

.paths-stack__scroll {
  position: relative;
  height: 240vh; /* pinned scroll distance — js/platform-paths.js derives
                    progress from this at runtime via offsetHeight, not a
                    hardcoded vh assumption, same as .portal-stepper__scroll.
                    Was 300vh: measured every scroll position 6 Jul and found
                    card2's position was pixel-identical from progress 0.88
                    (last feature row finishes revealing) through 1.0
                    (release) — a ~220px dead stretch of scrolling with zero
                    visible change, read as the section "getting stuck."
                    Shortening the total distance compresses that same idle
                    fraction proportionally without touching the phase
                    constants in platform-paths.js (SLIDE_END/FEAT2_END etc.
                    — the choreography itself is unchanged, just faster to
                    scroll through end to end). */
}

/* .paths-stack__head lives OUTSIDE this element now (see platform.html) —
   it scrolls away normally before the pin takes over, it isn't part of
   the sticky box. This is a deliberate change, 6 Jul: Figma node
   1278:36318 lays the header (196px) + 72px gap + API card (670px) out
   in plain vertical flow, never pinned into one screen — that's ~938px
   before nav-height even before real content padding, already taller
   than a common 900px laptop viewport. A `position: sticky` element can
   never reveal more of itself than fits in the viewport while it's stuck
   (scrolling the page doesn't pan a too-tall sticky box — verified this
   directly: the excess only flashes into view for an instant as the
   section releases, not a real, readable view) — so header+gap+card could
   never all be guaranteed visible together in one pinned box on a short
   screen, only the card can be. Yoann's call: let the headline scroll
   past like normal content, pin only the card area. */
.paths-stack__view {
  position: sticky;
  top: var(--nav-height);
  /* clamp(), not a flat 100vh - --nav-height: filling the whole viewport
     height left ~86-138px of dead space above/below the centered card on
     a typical 900px screen (card content is only 560-664px tall) — Yoann
     flagged this 6 Jul as an "arrive at the section, see nothing yet"
     moment. 720px is the floor (API card's ~664px + ~2*--gap-24
     breathing room) so the card never overlaps/clips on short screens;
     820px is the ceiling so the pin doesn't keep stretching to fill a
     tall monitor and re-inflate the same dead space — between those two,
     100vh - --nav-height still drives the value on the common range. */
  height: clamp(720px, calc(100vh - var(--nav-height)), 820px);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.paths-stack__head { padding-top: var(--gap-64); }
.paths-stack__head .h2 { max-width: 640px; }
.paths-stack__support {
  max-width: 452px;
  margin-top: var(--gap-12);
  color: var(--color-text-inverse-secondary);
}

.paths-stack__stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}

/* No fixed height, no overflow:hidden on the card itself — only
   .paths-stack__view (the pinned viewport) clips, which is what hides
   card 2 while it's off-stage below the fold. Reintroducing a fixed
   height or overflow:hidden here reproduces the crop bug the reference
   build already hit and fixed once (see the build spec's own "don't
   reintroduce" list). */
.paths-stack__card {
  position: absolute;
  left: 50%;
  top: 50%;
  /* Same width formula as .container (calc(100% - 2*--side-padding), capped
     at --container) — matches Figma node 1278:36318 exactly: the card frame
     and the header frame share the identical inset from the section edge.
     Replaces the old min(1240px, calc(100% - 48px)) formula, which drifted
     out of alignment with the section headline above it. */
  width: calc(100% - 2 * var(--side-padding));
  max-width: var(--container);
  min-height: min(560px, 66vh); /* no exact token for this literal — same
                                    class of unavoidable hardcoded value as
                                    .portal-stepper__right's 6:5 aspect-ratio.
                                    Safe to leave as a floor, not a cap, now
                                    that .paths-stack__view's height formula
                                    guarantees the stage always has room for
                                    the card's real content — see the
                                    comment there. */
  border-radius: var(--rd-12); /* reference prototype uses 20px; no --rd-20
                                   token exists (--rd-12 cards, --rd-16
                                   section containers) — snap to --rd-12 per
                                   STATUS-platform.md's own resolved note */
  padding: var(--gap-40) var(--gap-48);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap-64); /* matches palary-produkt-mockup.html's .type-block
                          column gap exactly (64px) — the established value
                          for this two-column card shape, not a new pick */
  align-items: start; /* was center — with feat-list taller than a bare
                          headline+subline, centering pushed the shorter
                          column's content down out of alignment with the
                          top of the other column. Top-align matches how
                          the mockup's own .type-block reads (both columns
                          start together). */
  transform: translate(-50%, -50%) translateY(var(--ty, 0px)) scale(var(--sc, 1));
}
.paths-stack__card--wl {
  background: var(--color-bg-light-pure);
  z-index: 1;
}
.paths-stack__card--api {
  background: var(--color-bg-brand); /* forest-800, per Figma node's own
    color/brand fill — not charcoal; COWORK.md caps the page at one
    charcoal-dark section (Hero/Proof Bar/Portal Stepper are all light), and
    forest-800 is a separate allowed beat, same as Integration's use of it. */
  z-index: 2;
}
.paths-stack__card--api .paths-stack__widget-col { order: -1; }

/* First use of a container query in this codebase — the API widget's own
   horizontal/vertical flip needs to react to its own column width, not the
   viewport (it lives inside roughly a quarter of the page width, squeezes
   well before any window-width media query would fire). See
   .paths-stack__widget-body below.

   display:flex + justify-content:center + align-self:stretch — same
   symmetric-padding treatment as .paths-stack__copy/.paths-stack__feat-list:
   without it, the widget (shorter than the card's full content height)
   shrink-wraps and sits pinned to the top of the row under the grid's
   align-items:start, showing more empty space below it than above. */
.paths-stack__widget-col {
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-self: stretch;
}

/* Vertically centers the merged headline block within the column's row
   height (set by the taller feat-list/feat-list+link column next to it) —
   matches Figma node 1278:36318 exactly (card-content is `justify-content:
   center` on both cards). align-self: stretch is scoped to .paths-stack__copy
   only, so the grid's own align-items: start (documented above, fixes a real
   cross-column misalignment bug during the scroll cascade) still governs
   .paths-stack__feat-list unaffected. */
.paths-stack__copy {
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-self: stretch;
}
.paths-stack__card--api .paths-stack__copy {
  gap: var(--gap-28); /* matches Figma's card-content gap exactly (28px
                          between headline block, dev-docs link, feat-list) */
}

/* Headline + subline merged into a single H4 copy block, zero spacing
   between them (just a <br>), differentiated only by color via the
   .paths-stack__headline-payoff span — matches Figma node 1278:36318
   exactly (one text node, both runs at H4 size, color is the only
   distinction). Was two separate elements (h3 + p) at --fs-h3 with a
   16px/24px margin gap; Figma confirms this component doesn't use the H3
   scale at all. */
.paths-stack__headline {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  opacity: var(--r, 0);
  transform: translateY(calc((1 - var(--r, 0)) * 16px));
}
.paths-stack__card--wl .paths-stack__headline { color: var(--color-text-inverse-primary); }
.paths-stack__card--api .paths-stack__headline { color: var(--color-text-primary); }

.paths-stack__headline-payoff { font-weight: 500; }
.paths-stack__card--wl .paths-stack__headline-payoff { color: var(--color-text-inverse-secondary); }
/* forest-300 used directly — matches the reference's own mapping (this
   subline and the feat-label below share the same muted-on-brand role),
   same precedent as Proof Bar's direct --forest-700 use where no semantic
   alias exists yet. Flagged as tech debt in STATUS: the --color-text-
   inverse-* naming is confusing enough that it's worth a second look on
   the next section that mixes a light and dark card. */
.paths-stack__card--api .paths-stack__headline-payoff { color: var(--forest-300); }

/* Dev-docs link — resolves the "known gap, not resolved" item in
   STATUS-platform.md: the reference's dev-docs link was left out of the
   original port pending an explicit yes/no from Yoann. Matches Figma node
   1278:36318's card-link exactly: rgba(255,255,255,0.04) fill,
   rgba(255,255,255,0.1) border, --rd-12, --gap-12 padding, lime text via
   the new --color-link-on-dark token (added to tokens.css, mirroring the
   --tag-bg-* on-dark/on-brand/on-accent pattern per STATUS's own flagged
   plan — only the on-dark variant is built since it's the only consumer). */
.paths-stack__link {
  display: inline-flex;
  align-items: center;
  gap: 4px; /* no --gap-4 token exists (smallest is --gap-12) — same class of
               accepted hardcoded literal as the widget's terminal-chrome
               values above */
  width: fit-content;
  padding: var(--gap-12);
  border-radius: var(--rd-12);
  background: rgba(255, 255, 255, 0.04);
  border: var(--bd-1) solid rgba(255, 255, 255, 0.1);
  color: var(--color-link-on-dark);
  font-family: var(--font-body);
  font-size: var(--fs-text);
  line-height: var(--lh-text);
  font-weight: 600;
  text-decoration: none;
  transition: background var(--dur-default) var(--ease-standard);
  opacity: var(--r, 0);
  transform: translateY(calc((1 - var(--r, 0)) * 16px));
}
.paths-stack__link:hover,
.paths-stack__link:focus-visible { background: rgba(255, 255, 255, 0.08); }
.paths-stack__link svg { flex-shrink: 0; }

/* gap:16px matches Figma node 1278:36318's feat-list gap exactly (was
   --gap-12 — a palary-produkt-mockup.html-based pick made before this
   section had a dedicated Figma frame; the frame is now the source of
   truth for this exact value). Static and independent of scroll/animation
   state: the reference relied solely on each row's own animated margin-top
   for spacing, which meant the resting gap held only via that transient
   value and collapsed to 0 in the mobile/reduced-motion fallback, where
   it gets reset. */
/* align-self: stretch + justify-content: center — same treatment as
   .paths-stack__copy above, for the same reason: without it this column
   shrink-wraps to its own content and sits pinned to the top of the row
   (grid's align-items: start), leaving all the card's leftover vertical
   space below it instead of split evenly — the padding is symmetric
   (40px top/bottom) but the visible content wasn't. */
.paths-stack__feat-list {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-self: stretch;
  gap: var(--gap-16);
}

.paths-stack__feat-row {
  display: flex;
  gap: var(--gap-24); /* icon-to-text gap — matches Figma exactly (was
                          --gap-12, a mockup-based guess) */
  align-items: flex-start;
  border-radius: var(--rd-16); /* matches Figma exactly (was --rd-12) */
  padding: var(--gap-28); /* matches Figma exactly (was --gap-16, a
                              mockup-based guess made before this section
                              had a dedicated Figma frame) */
  opacity: var(--r, 0);
  transform: translateY(calc(var(--r, 0) * -18px)) scale(calc(1 - (1 - var(--r, 0)) * 0.06));
  /* Negative pull-in during entry only, resolving to 0 at rest (r=1) — the
     "stacking" cascade feel now comes purely from this transient overlap,
     not from a permanent extra margin on top of the static gap above. */
  margin-top: calc((1 - var(--r, 0)) * -8px);
  transform-origin: top center;
}
.paths-stack__card--wl .paths-stack__feat-row {
  border: var(--bd-1) solid var(--color-border-light);
  box-shadow: var(--shadow-xs);
}
.paths-stack__card--api .paths-stack__feat-row {
  background: color-mix(in srgb, var(--color-text-primary) 6%, transparent);
  border: var(--bd-1) solid color-mix(in srgb, var(--color-text-primary) 10%, transparent);
}

.paths-stack__feat-ico {
  width: var(--paths-feat-ico-size);
  height: var(--paths-feat-ico-size);
  border-radius: var(--rd-8);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.paths-stack__card--wl .paths-stack__feat-ico {
  background: var(--forest-100);
  color: var(--charcoal-900);
}
/* Icon-chip lime is a deliberate exception confirmed 14 Jul 2026, not the
   generic accent-color rule — Yoann's explicit call for this component,
   distinct from the earlier decision (see git history) to avoid lime here. */
.paths-stack__card--api .paths-stack__feat-ico {
  background: var(--color-bg-accent);
  color: var(--forest-700);
}

.paths-stack__feat-label {
  font-family: var(--font-display);
  font-size: var(--fs-eyebrow); /* mockup's own feat-label is 10px, smaller
                                    than any token in the type scale — snapped
                                    to --fs-eyebrow (12px), the smallest real
                                    token, same "snap drift to existing
                                    tokens" practice STATUS-platform.md
                                    already applied to eyebrow/tag/label
                                    sizing sitewide rather than introducing a
                                    new one-off size */
  line-height: var(--lh-eyebrow);
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  margin-bottom: var(--gap-12); /* was 4px — no token that small; --gap-12
                                    keeps this on-scale, close enough to the
                                    mockup's intent without inventing a
                                    literal */
}
.paths-stack__card--wl .paths-stack__feat-label { color: var(--color-text-inverse-secondary); }
.paths-stack__card--api .paths-stack__feat-label { color: var(--forest-300); }

.paths-stack__feat-text {
  font-size: var(--fs-sm);
  line-height: var(--lh-sm);
  font-weight: 500;
}
.paths-stack__card--wl .paths-stack__feat-text { color: var(--color-text-inverse-primary); }
.paths-stack__card--api .paths-stack__feat-text { color: var(--color-text-primary); }

/* ----- API demo widget — ported from the reference's own resolved,
   tested artifact. Inner "terminal chrome" (borders, bar chrome, status
   dots, muted labels) has no matching semantic token family anywhere in
   tokens.css — same class of accepted hardcoded literal as
   .portal-stepper__right's 6:5 aspect-ratio. Colors that DO have an exact
   token match are bound to it instead of left as raw hex. ----- */
.paths-stack__widget {
  background: var(--charcoal-900);
  border-radius: 14px;
  overflow: hidden;
  border: var(--bd-1) solid #2A2A29; /* terminal-chrome border, no token match */
  width: 100%;
  max-width: 780px;
  opacity: var(--r, 0);
  transform: translateY(calc((1 - var(--r, 0)) * 16px));
}
.paths-stack__widget-bar {
  background: #1E1E1D; /* terminal-chrome bar, no token match */
  border-bottom: var(--bd-1) solid #2A2A29;
  padding: 11px 16px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.paths-stack__widget-bar i {
  width: 10px; height: 10px; border-radius: 50%;
  background: #3A3A38; /* terminal-chrome dot, no token match */
  display: block; flex-shrink: 0;
}
.paths-stack__widget-bar-label {
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
  font-size: 11px;
  color: #6B6B6A; /* terminal-chrome muted label, no token match */
  margin-left: 10px;
}
.paths-stack__widget-body { display: grid; grid-template-columns: 1fr 1fr; }
/* API widget's horizontal→vertical flip: reacts to its own column width via
   @container (see .paths-stack__widget-col above), not the viewport — the
   widget sits inside roughly a quarter of the page width, squeezed well
   before any window-width media query would fire. */
@container (max-width: 460px) {
  .paths-stack__widget-body { grid-template-columns: 1fr; }
  .paths-stack__widget-pane { border-right: none; border-bottom: var(--bd-1) solid #2A2A29; }
  .paths-stack__widget-pane:last-child { border-bottom: none; }
}
/* viewport fallback for browsers without container-query support */
@media (max-width: 600px) {
  .paths-stack__widget-body { grid-template-columns: 1fr; }
  .paths-stack__widget-pane { border-right: none; border-bottom: var(--bd-1) solid #2A2A29; }
  .paths-stack__widget-pane:last-child { border-bottom: none; }
}
.paths-stack__widget-pane { padding: 18px 20px; border-right: var(--bd-1) solid #2A2A29; }
.paths-stack__widget-pane:last-child { border-right: none; }
.paths-stack__widget-pane-label {
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: #6B6B6A;
  margin-bottom: 12px;
}
.paths-stack__widget-pre {
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
  font-size: 11px;
  line-height: 1.7;
  color: var(--charcoal-200);
  white-space: pre-wrap;
  word-break: break-word;
  min-height: 120px;
}
.paths-stack__widget-pre .k { color: var(--forest-300); }
.paths-stack__widget-pre .s { color: var(--text-accent); }
.paths-stack__widget-pre .n { color: var(--lime-400); }
.paths-stack__widget-pre .c { color: #6B6B6A; }
.paths-stack__widget-pre .b { color: var(--charcoal-100); }
.paths-stack__widget-endpoint-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
.paths-stack__widget-method {
  background: var(--forest-700);
  color: var(--text-accent);
  font-family: ui-monospace, monospace;
  font-size: 9.5px;
  font-weight: 600;
  padding: 3px 7px;
  border-radius: 5px;
  letter-spacing: var(--tracking-label);
  flex-shrink: 0;
}
.paths-stack__widget-path {
  font-family: ui-monospace, monospace;
  font-size: 10.5px;
  color: var(--charcoal-200);
  opacity: .6;
}
.paths-stack__widget-status-row { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; min-height: 18px; }
.paths-stack__widget-status-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: #333332; /* terminal-chrome idle dot, no token match */
  flex-shrink: 0;
  transition: background var(--dur-default) var(--ease-standard);
}
.paths-stack__widget-status-dot.is-ok { background: #28C840; } /* success green — no site token, one-off */
.paths-stack__widget-status-label {
  font-size: 10.5px;
  font-weight: 600;
  color: #6B6B6A;
  letter-spacing: var(--tracking-label);
  transition: color var(--dur-default) var(--ease-standard);
}
.paths-stack__widget-status-label.is-ok { color: #28C840; }
.paths-stack__widget-resp-pre { transition: opacity var(--dur-default) ease; min-height: 100px; opacity: 0; }
.paths-stack__widget-resp-pre.is-shown { opacity: 1; }
.paths-stack__widget-security-bar {
  background: var(--forest-900); /* exact match — #0F1918 */
  border-top: var(--bd-1) solid #2A2A29;
  padding: 13px 20px;
  display: flex;
  align-items: flex-start;
  gap: 9px;
}
.paths-stack__widget-security-bar svg { flex-shrink: 0; margin-top: 1px; }
.paths-stack__widget-security-text {
  font-size: 11px;
  font-weight: 500;
  color: var(--forest-300);
  line-height: 1.5;
}
.paths-stack__widget-security-text strong { color: var(--text-accent); font-weight: 600; }
.paths-stack__widget-sending { display: flex; align-items: center; gap: 6px; margin-top: 14px; opacity: 0; transition: opacity var(--dur-default) ease; }
.paths-stack__widget-sending.is-shown { opacity: 1; }
.paths-stack__widget-send-pulse {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--text-accent);
  animation: pathsWidgetPulse 1s ease-in-out infinite;
}
@keyframes pathsWidgetPulse { 0%, 100% { opacity: .3; transform: scale(.8); } 50% { opacity: 1; transform: scale(1); } }
.paths-stack__widget-send-text { font-size: 10.5px; color: var(--forest-300); font-family: ui-monospace, monospace; }

/* ---------- prefers-reduced-motion: fully flat, no pin, no scroll-scrub --
   Same posture as .portal-stepper — the pin/scrub IS this section's
   interaction model, so opting out of motion returns to the page's plain
   default: static, nothing scroll-linked. The API widget's own request/
   response cycle keeps running (js/platform-paths.js) — it's a functional
   product demo, not ambient scroll motion, same precedent as motion.js's
   own reduced-motion philosophy. */
@media (prefers-reduced-motion: reduce) {
  .paths-stack__scroll { height: auto; }
  .paths-stack__view { position: static; height: auto; overflow: visible; }
  /* padding-block's bottom side is 0, not var(--gap-48) — this white section
     (.paths-stack is bg-white) is immediately followed by Build vs Buy
     (bg-forest). Equal top/bottom padding left a strip of white between the
     last (dark) API card's rounded bottom corners and the next dark
     section, reading as a stray visual seam. Top padding is kept for
     breathing room below the headline/subline. */
  .paths-stack__stage { display: flex; flex-direction: column; gap: var(--gap-24); padding-block: var(--gap-48) 0; }
  .paths-stack__card {
    position: static;
    transform: none;
    width: 100%;
    max-width: none;
    margin: 0;
  }
  .paths-stack__headline,
  .paths-stack__link,
  .paths-stack__feat-row,
  .paths-stack__widget { opacity: 1; transform: none; margin-top: 0; }
}

/* ---------- Mobile (≤860px, per the approved build spec) ----------
   No pin, no scroll-scrub, no sticky: cards render as plain stacked blocks,
   full scroll freedom throughout. Reverted 10 Jul — a sticky dwell-then-
   cover mechanic (mirroring .portal-stepper's mobile stacking-cards) was
   tried here and shipped briefly, but real-device testing showed BOTH
   cards' full, untrimmed content (esp. the API card with its widget) is
   taller than a phone's visible sticky area — so each card froze in place
   ("blocked") with its bottom cropped for a long stretch of scroll before
   handing off. That's a fundamental content-height mismatch, not a tunable
   timing bug: a `position: sticky` box can never show more of itself than
   fits the screen it's stuck to, and reads as broken when it can't.
   Reuses the site's actual established reveal mechanism (data-reveal/
   data-reveal-group + initReveal() in motion.js, GSAP ScrollTrigger,
   already loaded on this page and already used by the Proof Bar) for a
   simple fade+rise per card — not the legacy mockup's plain-
   IntersectionObserver .reveal script, which was never carried into the
   real site. This section's own breakpoint (860px) intentionally differs
   from .portal-stepper's 767px: a two-column-per-card layout needs more
   room before it gets cramped than the Stepper's simpler 40/60 list. */
@media (max-width: 860px) {
  .paths-stack__scroll { height: auto; }
  .paths-stack__view { position: static; height: auto; overflow: visible; }
  /* padding-block's bottom side is 0, not var(--gap-32) — this white section
     (.paths-stack is bg-white) is immediately followed by Build vs Buy
     (bg-forest). Equal top/bottom padding left a strip of white between the
     last (dark) API card's rounded bottom corners and the next dark
     section, reading as a stray visual seam — the "corner radius at the end
     of the section" issue Yoann flagged 10 Jul. Top padding is kept for
     breathing room below the headline/subline. */
  .paths-stack__stage { display: flex; flex-direction: column; gap: var(--gap-24); padding-block: var(--gap-32) 0; }
  .paths-stack__card {
    position: static;
    transform: none;
    width: 100%;
    max-width: none;
    margin: 0;
    grid-template-columns: 1fr;
    padding: var(--gap-24);
  }
  /* Mobile order: headline+subline+feature cards first, widget last — Yoann's
     explicit call. Desktop keeps widget-col first (order:-1) since it's the
     visual anchor sliding in from the left; mobile reads top-to-bottom as
     copy first, demo widget as a supporting visual after the pitch. */
  .paths-stack__card--api .paths-stack__widget-col { order: 1; }
  .paths-stack__card--api .paths-stack__copy { order: 0; }
  /* API is the last card before Build vs Buy's own dark section starts
     immediately below (see the padding-block fix above) — squaring off its
     bottom corners lets its dark background run straight into the next
     section with no rounded edge breaking the seam. Top corners keep
     --rd-12, matching every other card on the page. */
  .paths-stack__card--api {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    /* Once the section-end gap and the rounded corner were both removed,
       the card's normal --gap-24 top/bottom padding read as too tight —
       top against the White-Label card above it, bottom against the flush
       edge into Build vs Buy — nothing left to soften either stop. Bumped
       both to --gap-40 for an even, balanced feel, matching this card's own
       DESKTOP vertical padding rhythm (padding: var(--gap-40) var(--gap-48)
       above), not a new value. */
    padding-top: var(--gap-40);
    padding-bottom: var(--gap-40);
  }
  .paths-stack__headline,
  .paths-stack__link,
  .paths-stack__feat-row,
  .paths-stack__widget { opacity: 1; transform: none; margin-top: 0; }
}

/* ---------- BUILD VS BUY (.build-vs-buy) ----------
   Section 5 of 8. Built against Figma node 1259:26792 (updated 6 Jul), which
   supersedes palary-produkt-mockup.html's older single-"Gregor" person-card in
   favor of a 3-avatar team panel. Uses the shared .section/.bg-forest/.grid/
   .col-N utilities throughout (no scroll-pin math to protect, unlike Portal
   Stepper/Product Paths), so mobile stacking below 1023px is inherited for
   free from the sitewide grid default — no bespoke breakpoint needed here. */

/* Confirmed exception, same tier as the Portal Stepper/Product Paths
   overrides: plain lime text, no pill. Overrides the sitewide
   .bg-forest .eyebrow pill (sections.css) for this instance only — every
   other eyebrow on the page keeps the pill. Needs the .bg-forest prefix to
   match that rule's specificity (0,2,0); platform.css loading after
   sections.css then wins the tie on source order. */
.bg-forest .build-vs-buy__eyebrow {
  background: none;
  padding: 0;
  color: var(--text-accent);
  margin-bottom: var(--gap-16);
}
.build-vs-buy__copy .h2 { margin-bottom: var(--gap-16); }
.build-vs-buy__body { margin-bottom: var(--gap-48); color: var(--forest-300); font-weight: 400; }
.build-vs-buy__resolve { font-weight: 400; }
.build-vs-buy__resolve strong { font-weight: inherit; color: var(--text-accent); }

.build-vs-buy__card {
  align-items: stretch; /* grid default, stated explicitly: right panel stretches
                            to match the left column's height, per Figma's own
                            self-stretch on card-col-visual */
  border-radius: var(--rd-16);
  /* Figma's own shadow here (0px 2px 4px rgba(24,24,24,.06)) is a near-exact
     match for --shadow-sm (0 2px 8px 0 rgba(24,24,23,.06)) — drift, not a new
     standard, same pattern as the rest of this page's token audit. */
  box-shadow: var(--shadow-sm);
}

.build-vs-buy__panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-radius: var(--rd-16);
  padding: var(--gap-24);
  /* No token matches this shadow — every other shadow in tokens.css is
     y-offset only; this one has a real x-offset. Hardcoded literal, same
     precedent as the Product Paths API-widget chrome colors. Reappears here
     after STATUS-platform.md's 2 Jul "no shadow" note, which predates this
     nested-panel structure. */
  box-shadow: 12px 16px 13px rgba(19, 19, 19, 0.21);
}
.build-vs-buy__panel-headline,
.build-vs-buy__panel-text { margin-bottom: var(--gap-28); }

/* ----- Avatar stack: new, generically-named, reusable component ----- */
.avatar-stack { display: flex; align-items: center; margin-bottom: var(--gap-40); }
.avatar-stack__item {
  --avatar-stack-size: 69px; /* no token for this — same precedent as
    .portal-stepper's --stepper-circle-size / .paths-stack's --paths-feat-ico-size.
    Figma node 1259:26803 exports the frame at 69px (border included, border-box) — confirmed via get_design_context. */
  display: block;
  flex-shrink: 0;
  width: var(--avatar-stack-size);
  height: var(--avatar-stack-size);
  border-radius: 50%;
  overflow: hidden;
  /* 4px hardcoded — no --bd-4 token exists (only --bd-1/--bd-3); matches Figma's
     border-4 exactly. Color binds to the panel's own bg so the avatar blends
     into the surface it sits on — same "blend in, don't stand out" principle
     as the earlier avatar-border audit note, re-targeted now that the avatars
     sit on the nested panel (--color-bg-brand) rather than directly on the
     section bg. */
  border: 4px solid var(--color-bg-brand);
  background: var(--color-bg-brand);
}
/* -14.14px is Figma's own literal export value (node 1313:30170/72, mr-[-14.14px]
   on the Team photo frames row) — not a rounded fraction of the avatar size. */
.avatar-stack__item:not(:first-child) { margin-left: -14.14px; }
.avatar-stack__item img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* .btn is display:inline-flex (auto width) by default, but .build-vs-buy__panel's
   flex-column would still stretch it full-width via the parent's default
   align-items:stretch — opt out so the button sizes to its own content,
   matching every other .btn--primary usage on the page. */
.build-vs-buy__cta { align-self: flex-start; }

/* ----- Marquee: new, generically-named, reusable component. Distinct from
   .herofan__marquee, which is a one-off mobile-only hero-fan fallback tightly
   coupled to that section's JS — not something to extend here. Ported from
   palary-produkt-mockup.html's .pm-track/.pmscroll mechanic (pure CSS, no
   JS), with colors snapped to real tokens instead of the mockup's local
   hardcoded values, per Yoann's "animate it now" call this session. ----- */
.marquee {
  overflow: hidden;
  margin-top: var(--gap-64); /* Figma's own root-level gap between the card and this row */
  padding-top: var(--gap-20); /* Figma's exact feature-list padding-top */
  border-top: var(--bd-1) solid var(--forest-500);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.marquee__track {
  display: flex;
  width: max-content;
  animation: build-vs-buy-marquee 26s linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
.marquee__group {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: var(--gap-40); /* Figma's flex gap between every item/dot (41px) — 1px drift, snap to the token */
  margin-right: var(--gap-40); /* seam gap to the next (duplicated) group — must equal the intra-group gap, and must apply to both groups equally so each half of .marquee__track stays the same width and the -50% loop keyframe stays exact */
}
.marquee__item {
  font-size: var(--fs-xl);
  line-height: var(--lh-xl);
  font-weight: 400;
  color: var(--color-text-on-forest);
  white-space: nowrap;
}
.marquee__dot { font-size: var(--fs-xl); line-height: var(--lh-xl); color: var(--forest-300); }

@keyframes build-vs-buy-marquee {
  to { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  /* Matches Figma's own static rendering exactly — the feature-list row is
     already what this component looks like at rest. */
  .marquee { overflow: visible; -webkit-mask-image: none; mask-image: none; }
  .marquee__track { animation: none; width: auto; }
  .marquee__group[aria-hidden="true"] { display: none; }
  .marquee__group { flex-wrap: wrap; }
}

/* ---------- CTA (.platform-cta) ----------
   Section 6 of 8. Built against Figma node 1310:87324. Originally shipped as
   a flat --color-bg-accent fill per Figma's literal flat lime rect; Yoann's
   6 Jul live review called it out as reading flat next to the index page's
   .final-cta and asked for the same radial gradient — superseding that
   earlier "deliberate flat fill" note. Now shares the exact gradient formula
   with .final-cta (sections.css) so both lime CTA cards read as one system.
   No scroll-pin math to protect, so — same as Build vs
   Buy — this uses .section/.bg-white/.container directly; the card itself is
   hand-rolled flex (not .grid/.col-N) because the split is asymmetric
   (flexible copy column + a capped-width image column), which the site's
   fractional grid columns can't express.
   Column gap corrected to --gap-48 (was --gap-24, Figma's literal spec) to
   match the established text/visual two-column convention elsewhere on the
   site — Integration's .integration__layout (index page, also a text-left/
   visual-right pair) uses the same --gap-48, not its Figma value either.
   Image column corrected from a fixed 558px basis to flex:1 1 0 capped at
   max-width:558px — at this page's real .container width (1280px at 1440
   viewport, vs Figma's 1395px canvas) a truly fixed 558px pinned the copy
   column down to 442px against the image's 558px, an imbalance Figma's own
   557/558 split never had. Flexing both and capping the image restores that
   near-50/50 balance at real widths while still hitting Figma's exact 558px
   once the viewport is wide enough for both columns to reach it. */
.platform-cta__card {
  display: flex;
  align-items: center;
  gap: var(--gap-48);
  background: radial-gradient(135% 130% at 50% 0%,
    var(--lime-300) 0%, var(--lime-400) 48%, var(--lime-500) 100%);
  border-radius: var(--rd-16);
  /* Vertical padding pulled in from Figma's literal --pd-80: at this page's
     real container width the image column renders narrower (and so
     shorter) than Figma's 558px canvas slot, so the untouched 80px figure
     read as excess air above/below the image once Yoann reviewed it live.
     --gap-64 (64px, already in the token set) tightens that without
     inventing a new pixel value. Horizontal stays --pd-128, matching Figma. */
  padding: var(--gap-64) var(--pd-128);
}
.platform-cta__copy {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-28);
  color: var(--color-text-inverse-primary);
}
.platform-cta__copy .h2,
.platform-cta__copy .text-xl { color: var(--color-text-inverse-primary); }
.platform-cta__visual {
  --cta-visual-radius: 20px; /* no token match (rd-16 is the nearest, but that's
    coincidental — same "don't bind to a numerically-close token" precedent as
    the 2 Jul audit's API card-visual-inner/FAQ-list-container note) */
  flex: 1 1 0;
  max-width: 558px;
  aspect-ratio: 558 / 398;
  border-radius: var(--cta-visual-radius);
  overflow: hidden;
}
.platform-cta__visual img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Stack below the sitewide 1023px breakpoint, same treatment as the index
   page's own .final-cta at this width: single centered column, copy above
   visual, padding steps down from the desktop 80/128 figure. */
@media (max-width: 1023px) {
  .platform-cta__card {
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: var(--pd-100) var(--pd-40);
  }
  .platform-cta__copy { align-items: center; }
  .platform-cta__visual { flex: 0 1 auto; width: 100%; max-width: 558px; }
}
@media (max-width: 480px) {
  .platform-cta__card { padding: var(--pd-80) var(--pd-20); }
}

/* ====================== 7. FAQ ======================
   Reuses sections.css's .faq__* component wholesale — markup, styling, and
   motion.js's initFaq() (one-open-at-a-time + GSAP height tween) all come
   from the shared files already loaded on this page, unchanged. Two
   page-scoped deviations only, both confirmed against Figma node
   1259:26843: question text sits at the h4 scale here (24px/30px) instead
   of the index page's --fs-xl (20px/32px) pick for the same class; the
   dev-docs answer link needs underline affordance since the sitewide `a`
   reset removes text-decoration and no --color-link-* on-light variant
   exists yet (only on-dark, built for Product Paths) — no new color
   invented, just the affordance. sections.css itself is untouched, so the
   index page's own FAQ instance is unaffected by either override. */
.platform-page .faq__q {
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
}
.platform-page .faq__a a {
  text-decoration: underline;
  text-underline-offset: 2px;
}
