/* ==========================================================================
   SmartMCU — Landing Page v3

   Visual language follows letters.app (measured from the live site, not
   guessed): white base, near-black type, #F5F5F5 panels at radius 48px,
   fully-rounded pill labels, a fixed nav pill that is transparent at the top
   of the page and fills in on scroll, and a dusty-blue gradient hero.

   SmartMCU's blue is kept as the accent — active states, icon chips, and the
   gradient on the active flow card — so the brand still reads.
   ========================================================================== */

/* --- fonts ---------------------------------------------------------------
   Inter 5.3.0 variable (SIL OFL 1.1, see assets/fonts/Inter-OFL.txt), the same
   family Portal v2 ships. The `opsz` cut carries both axes, so display sizes
   pick Inter's tighter display drawing automatically (font-optical-sizing).
   Latin subset only; glyphs outside it (→) fall back to the system font.
   ------------------------------------------------------------------------ */
@font-face {
  font-family: 'Inter';
  src: url('../assets/fonts/Inter-5.3.0-latin-opsz.woff2') format('woff2-variations'),
       url('../assets/fonts/Inter-5.3.0-latin-opsz.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* --- tokens ------------------------------------------------------------- */
:root {
  /* base. Ink and body follow Portal v2 (Apple label colours, flattened to
     solid hex so contrast is predictable): body 5.23:1 on white, 4.69:1 on
     --panel. */
  --ink: #1D1D1F;
  --body: #6C6C70;
  --muted: #868C98;
  --faint: #99A0AE;
  --hairline: #BEC2CA;

  --surface: #ffffff;
  /* Portal v2 --color-bg-grouped. The client-logo band keeps its own
     --klien-bg; see .klien. */
  --panel: #F2F2F7;
  --panel-2: #F9F9FB;

  /* Portal v2 accent and its pressed state; tint = 8% of the accent on white */
  --blue: #105ECB;
  --blue-dark: #0D4BA2;
  --blue-tint: #ECF2FB;
  /* Portal v2 green: large numerals and icon fills only (4.40:1 on white) */
  --green: #248A3D;
  --amber: #E0A000;

  /* Portal v2 mesh (app.css `mesh-band`): navy top-left, bright blue
     top-right, teal low-right, over a blue vertical base. White text on the
     darkest stop is 10.5:1 and on the lightest base stop 5.4:1. */
  --mesh:
    radial-gradient(110% 140% at 0% 0%, #0A3A8C 0%, rgb(10 58 140 / 0) 62%),
    radial-gradient(80% 120% at 100% 0%, #1B66CC 0%, rgb(27 102 204 / 0) 58%),
    radial-gradient(55% 80% at 95% 70%, #17806F 0%, rgb(23 128 111 / 0) 62%),
    linear-gradient(180deg, #0D47A8 0%, #1457BD 48%, #1C61C3 72%, #2468C6 100%);

  --font-display: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: ui-monospace, Menlo, Consolas, monospace;

  /* their nav pill is 1256px at a 1280 viewport; two 586px panels + gap = 1200 */
  --shell: min(94%, 1360px);

  --r-panel: 48px;
  --r-card: 24px;
  --r-chip: 14px;

  --shadow-card: 0 1px 2px rgba(7, 7, 9, .03), 0 6px 16px rgba(7, 7, 9, .04), 0 24px 48px rgba(7, 7, 9, .04);
  --shadow-float: 0 2px 8px rgba(7, 7, 9, .06), 0 18px 44px rgba(7, 7, 9, .10), 0 48px 96px rgba(7, 7, 9, .08);
  --shadow-pill: 0 1px 2px rgba(7, 7, 9, .04), 0 8px 24px rgba(7, 7, 9, .06);
}

/* --- base --------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--surface);
  font-family: var(--font-body);
  font-optical-sizing: auto;
  color: var(--body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img { max-width: 100%; }
a { color: var(--blue); }

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -.03em;
  color: var(--ink);
  margin: 0;
}

:where(a, button):focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 3px;
  border-radius: 6px;
}
/* the ring must not vanish on dark or blue ground */
.hero :where(a, button):focus-visible,
.cta :where(a, button):focus-visible,
.fcard.is-active :where(a, button):focus-visible {
  outline-color: #fff;
}

#page { position: relative; overflow-x: clip; background: var(--surface); }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: 14px 22px; background: var(--ink); color: #fff;
  font: 600 14px var(--font-body); text-decoration: none;
  border-radius: 0 0 12px 0;
}
.skip-link:focus { left: 0; color: #fff; }

/* --- section rhythm ----------------------------------------------------- */
.sec { width: var(--shell); margin-inline: auto; padding: 120px 0; }
.sec--tight { padding: 80px 0; }

/* The measure belongs on the HEADING, not the wrapper: `ch` resolves against
   the element's own font-size, and on a 16px container 22ch is ~240px — which
   is why these headings were wrapping into a narrow column. */
.sec__head { max-width: 900px; margin-inline: auto; text-align: center; }
.sec__head--wide { max-width: 1100px; }
.sec__title {
  font-size: clamp(32px, 3.6vw, 52px);
  line-height: 1.04;
  max-width: 32ch;          /* chars at the heading's own size, not the wrapper's */
  margin-inline: auto;
  text-wrap: balance;
}
.sec__lede {
  margin: 20px auto 0;
  max-width: 78ch;
  font: 400 clamp(15px, 1.05vw, 18px)/1.6 var(--font-body);
  color: var(--body);
  text-align: center;
  text-wrap: pretty;
}

/* inline icon chip embedded mid-headline — letters.app's signature device */
.chip-inline {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: .92em; height: .92em;
  margin: 0 .1em;
  vertical-align: -.08em;
  border-radius: .26em;
  background: var(--surface);
  box-shadow: var(--shadow-pill);
  overflow: hidden;
}
.chip-inline img, .chip-inline svg { width: 62%; height: 62%; display: block; }

/* --- buttons ------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border-radius: 999px;
  text-decoration: none;
  white-space: nowrap;
  font-family: var(--font-body);
  font-weight: 600;
  border: none;
  cursor: pointer;
  transition: background-color .2s ease, color .2s ease, border-color .2s ease, transform .2s ease;
}
.btn--light {
  background: var(--surface); color: var(--ink);
  padding: 15px 26px; font-size: 15px;
  box-shadow: var(--shadow-pill);
}
.btn--light:hover { background: #f2f4f8; color: var(--ink); }

.btn--ghost-light {
  background: rgba(255, 255, 255, .14);
  color: #fff;
  padding: 15px 24px; font-size: 15px;
  border: 1px solid rgba(255, 255, 255, .45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.btn--ghost-light:hover { background: rgba(255, 255, 255, .24); color: #fff; }

/* --- nav ---------------------------------------------------------------- */
/* letters.app: position:fixed, radius 120px, height 64px, and the white fill
   plus blur are absent at scroll 0 and animate in as you scroll. */
.nav-wrap {
  position: fixed;
  inset: 12px 0 auto;
  z-index: 60;
  pointer-events: none;
}

.nav {
  pointer-events: auto;
  width: var(--shell);
  margin-inline: auto;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 10px 0 22px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0);
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  /* TWO shadows, both transparent, matching the solid state's list exactly.
     A transition between shadow lists of different lengths snaps instead of
     interpolating, so the counts have to agree even when one end is invisible. */
  box-shadow: 0 0 0 1px rgba(7, 7, 9, 0), 0 10px 34px rgba(7, 7, 9, 0);
  transition: background-color .35s ease, box-shadow .35s ease, backdrop-filter .35s ease;
}
/* Toggled by js/v3.js once the hero gradient is behind us.

   The hairline ring is not decoration — it is the only thing that makes this
   state visible. Below the hero the page is #ffffff, so an 82%-white pill sits
   on white: the fill has nothing to contrast against and the blur has nothing
   to blur. It was reported as "navbar tidak berubah", and it was very nearly
   true — the sole cue was a 7%-opacity drop shadow. letters.app gets away with
   fill alone because varied content scrolls under its nav; ours needs an edge.

   Drawn with a spread shadow rather than `border`, so the pill's height stays
   exactly 64px and nothing reflows when the class lands. The blur still earns
   its place: the hero mock, the grey client band and the dark CTA all pass
   under it. */
.nav.is-solid {
  background: rgba(255, 255, 255, .82);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  box-shadow: 0 0 0 1px rgba(7, 7, 9, .07), 0 10px 34px rgba(7, 7, 9, .10);
}

.nav__logo { display: flex; align-items: center; text-decoration: none; }
.nav__logo img { height: 26px; width: auto; display: block; }
/* the logo is a dark wordmark; invert it while it sits on the gradient */
.nav:not(.is-solid) .nav__logo img { filter: brightness(0) invert(1); }

.nav__links {
  display: none;
  gap: 28px;
  font: 500 14px var(--font-body);
  white-space: nowrap;
}
.nav__links a { color: rgba(255, 255, 255, .92); text-decoration: none; transition: color .2s ease; }
.nav__links a:hover { color: #fff; }
.nav.is-solid .nav__links a { color: var(--body); }
.nav.is-solid .nav__links a:hover { color: var(--ink); }

.nav__cta {
  display: inline-flex; align-items: center; gap: 7px;
  height: 44px; padding: 0 20px;
  border-radius: 999px;
  background: var(--ink); color: #fff;
  font: 600 14px var(--font-body);
  text-decoration: none;
  transition: background-color .2s ease;
}
.nav__cta:hover { background: #23232b; color: #fff; }

@media (min-width: 860px) { .nav__links { display: flex; } }

/* --- hero --------------------------------------------------------------- */
/* letters.app insets its gradient hero 12px from the viewport and rounds it
   28px at the top, 48px at the bottom, with overflow:hidden so the artwork
   clips against the curve. Measured from the live site. */
.hero-frame { padding: 12px; background: var(--surface); }

.hero {
  position: relative;
  padding: 140px 0 0;
  background: var(--mesh);
  border-radius: 28px 28px 48px 48px;
  overflow: hidden;
  isolation: isolate;
}

/* Hero atmosphere: two identical wave layers offset from each other, each
   translating exactly one pattern period (-50% of a 2000-wide SVG) so the
   loop has no seam. Transform-only -> composited, not repainted. A third
   layer of very fine specks drifts the other way for depth. */
.hero__atmos {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
.hero__atmos-layer {
  position: absolute;
  top: 0; left: 0;
  width: 200%;
  height: 100%;
  display: block;
  will-change: transform;
}
.hero__atmos-layer--a { animation: smcu-flow 38s linear infinite; }
/* the second layer starts half a cycle in and runs slower + flipped, so the
   two never line up into an obvious single wave */
.hero__atmos-layer--b {
  animation: smcu-flow 61s linear infinite;
  animation-delay: -30s;
  transform: scaleY(-1);
  transform-origin: 50% 50%;
  opacity: .7;
}
.hero__atmos-layer--dots {
  width: 200%;
  animation: smcu-flow-slow 84s linear infinite;
  animation-direction: reverse;
}
@keyframes smcu-flow {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
/* the flipped layer needs the flip preserved in both keyframes */
.hero__atmos-layer--b { animation-name: smcu-flow-flip; }
@keyframes smcu-flow-flip {
  from { transform: translate3d(0, 0, 0) scaleY(-1); }
  to   { transform: translate3d(-50%, 0, 0) scaleY(-1); }
}
@keyframes smcu-flow-slow {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

.hero__inner { position: relative; z-index: 1; width: var(--shell); margin-inline: auto; text-align: center; }

.hero__eyebrow {
  display: inline-flex; align-items: center; gap: 9px;
  margin: 0 0 26px;
  padding: 7px 16px 7px 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .18);
  border: 1px solid rgba(255, 255, 255, .38);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  font: 600 12px/1 var(--font-body);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #fff;
}
.hero__eyebrow .dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: #6EE7A8; flex: none;
}

.hero__title {
  color: #fff;
  font-size: clamp(40px, 5.6vw, 76px);
  line-height: 1;
  letter-spacing: -.035em;
  max-width: 20ch;
  margin-inline: auto;
  text-wrap: balance;
}
.hero__title .mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-bottom: .14em;   /* Inter's descenders (g, p) sit lower than Open Runde's */
  margin-right: .2em;
}
.hero__title .mask:last-child { margin-right: 0; }
.w1 { display: inline-block; }

.hero__lede {
  margin: 26px auto 0;
  max-width: 52ch;
  font: 400 clamp(16px, 1.15vw, 20px)/1.55 var(--font-body);
  color: #EAF1F8;   /* solid, not rgba: opacity is not a contrast mechanism */
  text-wrap: pretty;
}
.hero__lede strong { font-weight: 700; color: #fff; }

.hero__actions {
  display: flex; flex-wrap: wrap; gap: 12px;
  justify-content: center;
  margin-top: 32px;
}

/* the product shot sits where letters.app puts its envelope, and parallaxes */
.hero__stage {
  position: relative;
  z-index: 1;
  width: var(--shell);
  margin: 68px auto 0;
  perspective: 1600px;
}
.hero__mock {
  position: relative;
  border-radius: 20px 20px 0 0;
  background: var(--surface);
  box-shadow: var(--shadow-float);
  overflow: hidden;
  transform-origin: 50% 100%;
  will-change: transform;
}
.chrome {
  height: 42px;
  display: flex; align-items: center; gap: 7px;
  padding: 0 16px;
  background: var(--surface);
  border-bottom: 1px solid rgba(7, 7, 9, .07);
}
.chrome i { width: 9px; height: 9px; border-radius: 50%; background: #E4E7EE; flex: none; }
.chrome .url {
  margin-left: 12px; height: 24px; flex: 1; max-width: 340px;
  border-radius: 7px; background: #F3F5FA;
  font: 500 11px/24px var(--font-mono); color: var(--faint);
  padding: 0 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.hero__shot { position: relative; width: 100%; aspect-ratio: 2000 / 1146; background: #F6F8FC; }
.hero__shot img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
}

@media (max-width: 759px) {
  .hero-frame { padding: 8px; }
  .hero { padding-top: 108px; border-radius: 20px 20px 32px 32px; }
  .hero__stage { margin-top: 40px; }
}

/* --- klien band --------------------------------------------------------- */
/* ONE colour knob. The band and both edge fades must never disagree: a
   half-applied change leaves two white smears on a grey band, and every static
   fallback below sets the fades to `display: none` — so that particular
   mistake is invisible in exactly the modes someone would think to test.
   Padding is symmetric because the band is now a VISIBLE surface. `44px 0 8px`
   was white-band arithmetic; on grey it put the logo row 90% of the way down
   its own band, which reads as a bug rather than a decision. */
.klien {
  /* Its own neutral grey, not --panel: the logo plates are flattened onto this
     exact value by tools/logos.py, and a tinted ground like #F2F2F7 does not
     survive lossy WebP (lossless would cost +67 KB above the fold). */
  --klien-bg: #F5F5F5;
  position: relative;
  background: var(--klien-bg);
  padding: 40px 0 36px;
  overflow: hidden;
}
/* The fades are `top: 0; bottom: 0; z-index: 2`, so they span the whole band
   INCLUDING this heading — and the band has no horizontal padding, so at 375px
   the first and last words of the label sat under a fade and washed out. Give
   the heading the shell's measure and lift it over the fades. */
.klien__label {
  position: relative;
  z-index: 3;
  width: var(--shell);
  margin: 0 auto 22px;
  text-align: center;
  font: 600 11px/1 var(--font-body);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--body);
}
/* Proportional, not fixed: a flat 140px each side covered 75% of a 375px
   phone row, so the logos it was meant to soften were the ones it erased. */
.klien__fade {
  position: absolute; top: 0; bottom: 0;
  width: clamp(40px, 10vw, 140px);
  z-index: 2; pointer-events: none;
}
.klien__fade--l { left: 0; background: linear-gradient(90deg, var(--klien-bg), rgba(245, 245, 245, 0)); }
.klien__fade--r { right: 0; background: linear-gradient(270deg, var(--klien-bg), rgba(245, 245, 245, 0)); }

/* ONE spacing knob. It used to be three (mq gap + two slot paddings), which
   is why the row read as too airy: 14 + 24 + 24 = 62px between marks. */
/* The supplied masters are used WHOLE — see tools/logos.py. Every one is a
   1098×550 plate carrying a centred, full-colour mark, so they already share
   one aspect ratio and one optical weight; the supplier balanced them. That
   makes this row two numbers, and nothing per-logo.

   An earlier pass cropped each master to its ink bounds and re-balanced the
   crops by area. It squeezed the three portrait Hermina crests into 40px-wide
   columns with illegible city wordmarks, and it re-derived, worse, a balance
   that was already in the files. Do not reintroduce cropping. */
.mq {
  /* Both clamps share their knees on purpose: 5.8vw hits 72px at 1241px and
     84px at 1448px, and 2.9vw is exactly half of that, so gap/height is pinned
     at 0.500 at EVERY viewport. The old pair drifted 0.563 -> 0.582 -> 0.609,
     i.e. the row got relatively airier as it got bigger, which is backwards.
     The cap is 84px, not 92: 5.5vw only reached 92px above a 1673px monitor,
     so a 1440 laptop rendered 79px and nobody ever saw the 92. 84 arrives on a
     1440 laptop, and at 84px the marks land 34.5-64.8px tall with 14.3%
     DPR2 headroom against the 383px export (92px left only 4.3%). */
  --mq-h: clamp(72px, 5.8vw, 84px);
  --mq-gap: clamp(36px, 2.9vw, 42px);
  /* Columns for the STATIC fallback only — inert while the row is a flex
     marquee. A plain `grid-template-columns` here would not survive the
     cascade: the reduced-motion @media block is the last thing in this file,
     so its `.mq` rule out-orders the phone override at equal specificity, and
     a visitor with BOTH no JS and reduced motion would get four 167px chips on
     a 375px screen. A custom property is set once and read everywhere. */
  --mq-cols: 3;
  display: flex; gap: var(--mq-gap); width: max-content;
  list-style: none; margin: 0; padding: 0;
}
.mq.is-running { will-change: transform; }
.mq li {
  height: var(--mq-h);
  display: inline-flex; align-items: center;
  white-space: nowrap;
}
.mq__slot { width: auto; padding: 0; justify-content: center; }
/* Full colour, no greyscale. `filter: grayscale(1); opacity: .62` was making
   the three Hermina crests all but disappear — they are small, detailed marks
   inside a lot of plate, and desaturating them removed the only thing
   separating crest from ground. Desaturation earns its place when a logo set
   is visually inconsistent; this set is not, because every mark sits on the
   same plate at the same scale. */
/* Width comes from the EXPORT CONSTANT, not from the image. `width: auto` made
   the wrap period depend on load state, and three separate defects followed
   from that: a 404 collapses an alt="" duplicate to 0px while its alt-texted
   original sizes to the alt string, so the three copies stop being congruent
   and the measured cycle is wrong by ~640px; a stale width/height attribute
   silently mis-sets the cycle forever, because measureMq() never re-runs on
   image load; and mid-decode measurement is a race. One calc closes all three
   — every slot is 383/192 of --mq-h whether the image loads, fails, or is
   still decoding. Keep it in step with tools/logos.py, which prints it. */
.mq__slot img {
  height: var(--mq-h);
  width: calc(var(--mq-h) * 383 / 192);
  display: block;
  object-fit: contain;
}

/* ---------- static modes: stop pretending to be a marquee ----------------
   js/v3.js returns early under reduced motion, BEFORE the marquee ticker is
   ever set up, and a failed GSAP CDN drops the `js` class instead. In both
   cases this row is a frozen `width: max-content` strip inside an
   overflow-hidden band — which on a 375px phone shows TWO of eight clients and
   offers no way whatsoever to reach the other six. That is a real content loss,
   not a cosmetic degradation, and it predates the resize.

   So in those modes it stops being a marquee: the sixteen duplicates go, and
   the eight real logos wrap and centre. The edge fades go too — they exist to
   suggest motion continuing past the viewport, and there is none.

   Neither signal costs layout stability. `.reduced` is set by the inline script
   in <head> and the media query needs no script at all, so a reduced-motion
   visitor gets this layout from first paint. `:not(.js)` only fires once the
   CDN has actually failed, where a late reflow is plainly better than six
   hidden clients. */
:root.reduced .mq,
:root:not(.js) .mq,
.mq--static {
  display: grid;
  grid-template-columns: repeat(var(--mq-cols, 4), max-content);
  width: max-content;
  max-width: var(--shell);
  margin-inline: auto;
  justify-content: center;
  gap: 18px var(--mq-gap);
}
:root.reduced .mq li[aria-hidden="true"],
:root:not(.js) .mq li[aria-hidden="true"],
.mq--static li[aria-hidden="true"] { display: none; }

:root.reduced .klien__fade,
:root:not(.js) .klien__fade { display: none; }

@media (prefers-reduced-motion: reduce) {
  .mq {
    display: grid;
    grid-template-columns: repeat(var(--mq-cols, 4), max-content);
    width: max-content;
    max-width: var(--shell);
    margin-inline: auto;
    justify-content: center;
    gap: 18px var(--mq-gap);
  }
  .mq li[aria-hidden="true"] { display: none; }
  .klien__fade { display: none; }
}

/* --- big soft panels (letters.app: #F5F5F5, radius 48px, padding 40px) --- */
.panels {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
  margin-top: 64px;
}
@media (min-width: 900px) { .panels { grid-template-columns: repeat(2, 1fr); } }

.panel {
  position: relative;
  background: var(--panel);
  border-radius: var(--r-panel);
  padding: 40px;
  display: flex;
  flex-direction: column;
  min-height: 520px;
  overflow: hidden;
}

.panel__pill {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 16px;
  border-radius: 999px;
  background: var(--surface);
  font: 500 12px/1 var(--font-body);
  color: var(--ink);
  box-shadow: var(--shadow-pill);
}
.panel__pill svg { width: 14px; height: 14px; flex: none; color: var(--blue); }

.panel__title {
  margin: 26px 0 0;
  font-size: clamp(24px, 1.9vw, 30px);
  line-height: 1.18;
  max-width: 18ch;
}

/* Fixed height + clipping. Previously the layered phones were absolutely
   positioned with a negative bottom, which took them out of flow and let them
   sit on top of the caption below. The stage now owns its own box and the
   caption is never overlapped. */
.panel__stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  margin: 28px calc(-1 * 40px) 22px;   /* bleed to the panel's padding edges */
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.panel__note {
  margin: 0;
  font: 400 15px/1.6 var(--font-body);
  color: var(--body);
  max-width: 52ch;
}
.panel__note strong { font-weight: 700; color: var(--ink); }


/* Layered SVG illustrations. Abstract on purpose: shapes and bars, never
   fabricated readings — they illustrate the idea without claiming to be the
   product's UI. Each layer is a .plx group with its own data-speed, so the
   depth comes from parallax rather than from stacking things over the text.

   The static rotations live on INNER groups: GSAP owns the .plx group's
   transform, and giving it a CSS/attribute transform would corrupt its
   baseline (the trap documented twice in this README). */
.ill {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* --- icon cards (letters.app's 3-up row under the panels) --------------- */
.cards3 {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
  margin-top: 24px;
  list-style: none;
  padding: 0;
}
@media (min-width: 700px) { .cards3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .cards3 { grid-template-columns: repeat(3, 1fr); } }

/* four cards read better 2x2 than as four cramped columns */
.cards4 {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
  margin-top: 64px;
  list-style: none;
  padding: 0;
}
@media (min-width: 760px) { .cards4 { grid-template-columns: repeat(2, 1fr); } }

.card3 {
  background: var(--panel);
  border-radius: var(--r-panel);
  padding: 36px 32px;
}
.card3__chip {
  width: 42px; height: 42px;
  border-radius: var(--r-chip);
  background: var(--surface);
  box-shadow: var(--shadow-pill);
  display: flex; align-items: center; justify-content: center;
  color: var(--blue);
}
.card3__chip svg { width: 19px; height: 19px; }
.card3__title { margin: 26px 0 0; font-size: 18px; line-height: 1.3; letter-spacing: -.02em; }
.card3 p { margin: 10px 0 0; font: 400 15px/1.6 var(--font-body); color: var(--body); }

/* --- ekosistem: product-family bento ------------------------------------
   Three columns: a wide mesh tile (the clinic portal), then the other
   parties, closing on a wide ink tile for the shared dataset, so the two
   emphasised tiles sit on opposite corners. GSAP owns each .eco__item
   (reveal); the hover lift lives on .eco__card so the two transforms never
   meet on one element. */
.eco {
  list-style: none;
  margin: 64px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
}
@media (min-width: 760px) {
  .eco { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .eco__item--wide,
  .eco__item--md-wide { grid-column: span 2; }
}
@media (min-width: 1100px) {
  .eco { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .eco__item--md-wide { grid-column: auto; }
}
.eco__item { display: flex; }
.eco__card {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 300px;
  padding: 34px;
  border-radius: 32px;
  background: var(--panel);
  transition: transform .6s cubic-bezier(.2, .8, .2, 1), box-shadow .6s cubic-bezier(.2, .8, .2, 1);
}
@media (hover: hover) {
  .eco__card:hover { transform: translateY(-4px); box-shadow: var(--shadow-card); }
}
.eco__top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.eco__icon {
  width: 46px; height: 46px; flex: none;
  border-radius: var(--r-chip);
  background: var(--surface);
  box-shadow: var(--shadow-pill);
  display: flex; align-items: center; justify-content: center;
  color: var(--blue);
}
.eco__icon svg { width: 22px; height: 22px; }
.eco__title {
  margin: 0;
  padding-top: 64px;   /* titles share one baseline across a row; slack falls below the copy */
  font-size: clamp(22px, 1.75vw, 28px);
  line-height: 1.15;
  letter-spacing: -.025em;
}
.eco__text { margin: 10px 0 0; max-width: 46ch; font: 400 16px/1.55 var(--font-body); color: var(--body); }

/* the clinic portal: Portal v2's mesh, white type (5.4:1 on the lightest stop) */
.eco__card--main { background: var(--mesh); }
.eco__card--main .eco__title { color: #fff; }
.eco__card--main .eco__text { color: #EAF1F8; }
.eco__card--main .eco__icon,
.eco__card--ink .eco__icon {
  background: rgb(255 255 255 / .14);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .22);
  color: #fff;
}
.eco__pills {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  display: flex; flex-wrap: wrap; gap: 8px;
}
.eco__pills li {
  padding: 8px 14px;
  border-radius: 100px;
  background: rgb(255 255 255 / .14);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .22);
  font: 500 14px/1.2 var(--font-body);
  color: #fff;
}

/* the shared dataset: near-black, the page's other anchor */
.eco__card--ink { background: var(--ink); }
.eco__card--ink .eco__title { color: #fff; }
.eco__card--ink .eco__text { color: #C7C7CC; }   /* 10.6:1 on --ink */

@media (max-width: 759px) {
  .eco { margin-top: 44px; gap: 14px; }
  .eco__card { min-height: 0; padding: 28px; border-radius: 26px; }
  .eco__title { padding-top: 36px; }
}

/* --- stats row ---------------------------------------------------------- */
.stats {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  margin-top: 56px;
  list-style: none;
  padding: 0;
}
@media (min-width: 620px) { .stats { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .stats { grid-template-columns: repeat(4, 1fr); } }

.stat {
  background: var(--panel);
  border-radius: var(--r-card);
  padding: 30px 26px;
}
.stat__n {
  font: 600 clamp(34px, 2.9vw, 50px)/1 var(--font-display);
  letter-spacing: -.035em;
  color: var(--blue);
  white-space: nowrap;
}
.stat__n--green { color: var(--green); }
.stat__l { margin: 14px 0 0; font: 500 14px/1.5 var(--font-body); color: var(--body); }

/* --- comparison table --------------------------------------------------- */
.compare__box {
  margin-top: 48px;
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow-card);
}
.ctable { width: 100%; border-collapse: collapse; table-layout: fixed; text-align: left; }
.ctable thead th {
  padding: 16px 26px;
  background: var(--panel-2);
  font: 600 11px/1 var(--font-body);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--body);
  border-bottom: 1px solid rgba(7, 7, 9, .07);
}
.ctable thead th:nth-child(3) { color: var(--blue); }
.ctable td, .ctable th[scope="row"] {
  padding: 22px 26px;
  border-bottom: 1px solid rgba(7, 7, 9, .05);
  vertical-align: top;
}
.ctable tbody tr:last-child td, .ctable tbody tr:last-child th { border-bottom: none; }
.c-stage { font: 600 clamp(16px, 1vw, 19px)/1.35 var(--font-display); letter-spacing: -.02em; color: var(--ink); }
.c-manual { font: 400 15px/1.6 var(--font-body); color: var(--body); } /* --muted is 3.10:1 on the panel */
.rm {
  background-image: linear-gradient(rgba(180, 67, 58, .65), rgba(180, 67, 58, .65));
  background-repeat: no-repeat;
  background-position: 0 56%;
  background-size: 0% 2px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
.c-smart { font: 500 15px/1.6 var(--font-body); color: var(--ink); }
.c-smart__in { display: flex; gap: 10px; align-items: flex-start; }
.chk {
  flex: none; width: 18px; height: 18px; border-radius: 50%;
  background: var(--green); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; margin-top: 3px;
}

@media (max-width: 759px) {
  .ctable thead { display: none; }
  .ctable, .ctable tbody, .ctable tr, .ctable td, .ctable th[scope="row"] { display: block; width: auto; }
  .ctable tr { padding: 22px 24px; border-bottom: 1px solid rgba(7, 7, 9, .05); }
  .ctable tbody tr:last-child { border-bottom: none; }
  .ctable td, .ctable th[scope="row"] { padding: 0; border-bottom: none; }
  .ctable td { margin-top: 14px; }
  .ctable td::before {
    content: attr(data-label);
    display: block;
    font: 600 10px/1 var(--font-body);
    letter-spacing: .13em; text-transform: uppercase;
    color: var(--body); margin-bottom: 6px;
  }
  .ctable td.c-smart::before { color: var(--blue); }
  .c-smart__in { display: block; }
  .c-smart .chk { display: inline-flex; vertical-align: text-bottom; margin: 0 8px 0 0; }
}
@media (min-width: 760px) {
  /* "Tahap" holds at most 16 characters, so 25% was wasted on it while the
     two prose columns wrapped early. */
  .ctable col.col-stage { width: 19%; }
  .ctable col.col-manual { width: 40.5%; }
  .ctable col.col-smart { width: 40.5%; }
}

/* footnote for the client-reported "2–7 hari" baseline */
.fn-ref { font: 600 .55em var(--font-body); vertical-align: super; line-height: 0; margin-left: .15em; }
.fn-ref a { color: var(--body); text-decoration: none; }
.fn-ref a:hover { color: var(--blue); text-decoration: underline; }
.footnote {
  margin: 20px 0 0;
  font: 400 13px/1.6 var(--font-body);
  color: var(--body);   /* --faint is 2.63:1; this is real prose */
  /* Wider than the ~75ch ideal on purpose: at 13px it otherwise reads as a
     narrow column stranded under a full-width table. */
  max-width: 112ch;
  scroll-margin-top: 100px;
}
.footnote sup { font-weight: 600; }

/* --- faq ---------------------------------------------------------------- */
.faq__list { max-width: 780px; margin: 52px auto 0; }
.faq__item { border-bottom: 1px solid rgba(7, 7, 9, .08); }
.faq__item h3 { margin: 0; font: inherit; letter-spacing: normal; }
.faq__q {
  width: 100%;
  display: flex; justify-content: space-between; align-items: center; gap: 22px;
  padding: 24px 0;
  background: none; border: none; text-align: left; cursor: pointer;
  font: 600 clamp(17px, 1.15vw, 21px)/1.35 var(--font-display);
  letter-spacing: -.02em;
  color: var(--ink);
}
.faq__q-text { flex: 1; }
.faq__icon {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  background: var(--panel);
  display: inline-flex; align-items: center; justify-content: center;
  font: 400 17px var(--font-body); color: var(--ink);
  transition: background-color .2s ease, color .2s ease;
}
.faq__q:hover .faq__icon { background: var(--blue); color: #fff; }
.faq__a { display: none; }
.faq__item.is-open .faq__a { display: block; }
.faq__a p {
  margin: 0; padding: 0 52px 24px 0;
  font: 400 clamp(15px, 1.05vw, 17px)/1.7 var(--font-body);
  color: var(--body);
}

/* --- cta ---------------------------------------------------------------- */
.cta-sec { width: var(--shell); margin-inline: auto; padding: 40px 0 100px; }
.cta {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--r-panel);
  background: var(--mesh);
  padding: clamp(48px, 6vw, 84px) clamp(28px, 5vw, 72px);
  text-align: center;
}

/* Curved lines, same treatment as the hero: two identical layers, each
   translating exactly one pattern period (-50% of a 2000-wide viewBox) so the
   loop has no seam, transform-only so it composites instead of repainting.
   Different periods from the hero's 38s/61s — the two panels are on screen at
   different times and matching them buys nothing, while distinct rhythms stop
   the closing panel reading as a copy of the opening one. No dots layer: the
   CTA carries a headline, a paragraph and a button in the same space the hero
   gives to one line, so the ground under it has to stay quieter. */
.cta__atmos {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
.cta__atmos-layer {
  position: absolute;
  top: 0; left: 0;
  width: 200%;
  height: 100%;
  display: block;
  will-change: transform;
}
.cta__atmos-layer--a { animation: smcu-flow 44s linear infinite; }
.cta__atmos-layer--b {
  animation: smcu-flow-flip 71s linear infinite;
  animation-delay: -35s;
  transform: scaleY(-1);
  transform-origin: 50% 50%;
  opacity: .72;
}

/* the atmos is positioned, so the copy needs its own layer to sit above it —
   a static sibling of a z-index:0 box loses regardless of document order */
.cta__inner { position: relative; z-index: 1; }

.cta h2 {
  color: #fff;
  font-size: clamp(30px, 3.4vw, 50px);
  line-height: 1.06;
  max-width: 24ch;
  margin-inline: auto;
  text-wrap: balance;
}
.cta p {
  margin: 20px auto 0;
  max-width: 52ch;
  font: 400 clamp(15px, 1.1vw, 19px)/1.6 var(--font-body);
  color: #EAF1F8;
  text-wrap: pretty;
}
.cta__actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 30px; }

/* --- legal pages (privacy-policy, terms-of-service) ----------------------
   Reuse every token and component from the landing page; these add only the
   measure and rhythm a body of prose needs. Kept in this stylesheet rather
   than inlined per page so the two legal pages can never drift from the
   site's type scale. */
.legal {
  width: var(--shell);
  margin-inline: auto;
  padding: 150px 0 96px;
}
.legal__inner { max-width: 72ch; }
.legal h1 {
  font-size: clamp(34px, 5vw, 56px);
  line-height: 1.04;
  letter-spacing: -.03em;
  margin: 0 0 14px;
}
.legal__meta {
  margin: 0 0 44px;
  font: 500 13px/1 var(--font-body);
  letter-spacing: .02em;
  color: var(--muted);
}
.legal h2 {
  font-size: clamp(20px, 1.7vw, 26px);
  line-height: 1.22;
  letter-spacing: -.02em;
  margin: 44px 0 12px;
}
.legal h2:first-of-type { margin-top: 0; }
.legal p {
  margin: 0 0 16px;
  font: 400 clamp(15px, 1.05vw, 17px)/1.7 var(--font-body);
  color: var(--body);
  text-wrap: pretty;
}
.legal a:not([class]) { color: var(--blue); text-decoration: underline; text-underline-offset: 2px; }
.legal__back {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: 44px;
  font: 600 15px var(--font-body);
  color: var(--ink); text-decoration: none;
}
.legal__back:hover { text-decoration: underline; }

@media (max-width: 759px) {
  .legal { padding: 118px 0 68px; }
  .legal h2 { margin-top: 34px; }
}

/* --- 404 ------------------------------------------------------------------
   Built on .legal so the type scale, measure and rhythm are the page's own,
   not a second set of numbers that drifts. */
.notfound { display: grid; place-items: center; min-height: 72vh; text-align: center; }
.notfound .legal__inner { max-width: 34rem; }
.notfound__logo { height: 26px; width: auto; display: block; margin: 0 auto 30px; }
.notfound h1 { margin-bottom: 12px; }
.notfound p { margin-bottom: 30px; }
.btn--dark {
  background: var(--ink);
  color: #fff;
}
.btn--dark:hover { background: #23232b; color: #fff; }

/* --- footer ------------------------------------------------------------- */
.footer { background: var(--surface); border-top: 1px solid rgba(7, 7, 9, .07); }
.footer__top {
  width: var(--shell); margin-inline: auto;
  padding: 56px 0 40px;
  display: flex; flex-wrap: wrap; gap: 40px 64px; justify-content: space-between;
}
.footer__brand { flex: 1 1 280px; }
.footer__brand img { height: 26px; width: auto; display: block; }
.footer__brand p { margin: 18px 0 0; font: 400 14px/1.6 var(--font-body); color: var(--body); max-width: 320px; }
.footer__cols { display: flex; flex-wrap: wrap; gap: 40px 64px; }
.footer__col { display: flex; flex-direction: column; gap: 12px; font: 500 14px var(--font-body); }
.footer__col h2 {
  font: 600 11px var(--font-body);
  letter-spacing: .13em; text-transform: uppercase;
  color: var(--body); margin: 0 0 4px;
}
.footer__col a { color: var(--body); text-decoration: none; }
.footer__col a:hover { color: var(--ink); text-decoration: underline; }
.footer__note { color: var(--body); }   /* plain address line; inherits the column type */
.footer__bottom {
  width: var(--shell); margin-inline: auto;
  padding: 20px 0 32px;
  border-top: 1px solid rgba(7, 7, 9, .05);
  display: flex; flex-wrap: wrap; gap: 12px 24px; align-items: center; justify-content: space-between;
  font: 400 13px var(--font-body); color: var(--body);
}
.footer__legal { display: flex; flex-wrap: wrap; gap: 20px; }
.footer__legal a { color: var(--body); text-decoration: none; }
.footer__legal a:hover { color: var(--ink); text-decoration: underline; }

/* ==========================================================================
   FLOW — the scroll-driven horizontal step strip

   Chosen from a three-way design panel: Design A's anchor-left geometry with a
   self-sizing end cap, built on Design B's `position: sticky` window rather
   than ScrollTrigger's `pin` (this codebase already proves sticky, and
   `#page { overflow-x: clip }` exists specifically to keep sticky alive; a pin
   injects a spacer and cannot be reverted without a matchMedia context).

   POLARITY MATTERS: the static 4-up grid below is the REAL markup. The
   horizontal row is an upgrade added by JS via `.flow--linked`. Gating on a
   JS-added class rather than `.js` closes the window where the script tag has
   run but GSAP has not yet arrived — otherwise the strip is live and clipped
   with nothing to drive it.

   Cards must stay NON-INTERACTIVE forever. Zero focusable elements sit inside
   `.flow__viewport`; every control lives in `.flow__nav` outside the clip.
   That is the structural bargain that makes keyboard access work here — the
   day a card gains a link, either the card leaves the clip or off-screen
   tabindex management becomes necessary.
   ========================================================================== */

.flow {
  --card-w: clamp(300px, 38vw, 520px);
  --card-gap: 24px;
  --card-h: clamp(300px, 44vh, 480px);
  --flow-n1: 3;      /* cards - 1: the only knob the card count touches */
  --flow-pace: 1.15; /* scroll distance per card of travel */
  --flow-gutter: max(16px, calc((100% - min(94%, 1256px)) / 2));
}

.flow__travel { position: relative; }
.flow__sticky { position: relative; }

.flow__viewport { position: relative; }

/* ---------- base: static grid. no-JS, reduced motion, short viewports ---- */
.flow__track {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--card-gap);
  width: var(--shell);
  margin-inline: auto;
  padding: 0;
  list-style: none;
}
@media (min-width: 700px) { .flow__track { grid-template-columns: repeat(2, 1fr); } }

.fcard {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--r-panel);
  background: var(--panel);
  padding: 30px 30px 0;
  display: flex;
  flex-direction: column;
  /* authoritative, not advisory: an `auto`-height image blows the sticky
     budget and strands content off the bottom of short viewports */
  height: var(--card-h);
  transition: background-color .18s ease;
}

/* the gradient lives on ::before because a linear-gradient is a
   background-image and therefore does NOT transition; crossfading its opacity
   over a solid background-color keeps text on a real colour the whole time */
.fcard::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(155deg, var(--blue) 0%, #3a6bd0 55%, #4d7cd8 100%); /* tail 4.05:1 with white */
  opacity: 0;
  transition: opacity .35s ease;
}
.fcard.is-active { background-color: var(--blue); }
.fcard.is-active::before { opacity: 1; }

.fcard__num {
  width: 42px; height: 42px;
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
  display: flex; align-items: center; justify-content: center;
  font: 600 15px var(--font-display);
  letter-spacing: -.02em;
  flex: none;
  transition: background-color .35s ease, color .35s ease;
}
.fcard.is-active .fcard__num { background: var(--surface); color: var(--blue); }

.fcard__title {
  margin: 20px 0 0;
  font-size: clamp(19px, 1.4vw, 24px);
  line-height: 1.22;
  max-width: 20ch;
  color: var(--ink);
  transition: color .35s ease;
}
.fcard__step {
  margin: 14px 0 0;
  font: 500 12px/1 var(--font-body);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--body);
  transition: color .35s ease;
}
/* solid #fff, never rgba() — opacity is not a contrast mechanism */
.fcard.is-active .fcard__title { color: #fff; }
.fcard.is-active .fcard__step { color: #fff; }

.fcard__fig {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;      /* lets the image well shrink instead of pushing height */
  margin-top: 22px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
.fcard__fig img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: cover;
  object-position: top center;
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -8px 30px rgba(7, 7, 9, .12);
}
.fcard__fig--phone img {
  width: min(52%, 190px);
  border-radius: 18px 18px 0 0;
}

/* the end cap only exists in linked mode */
.fcard--end { display: none; }

/* ---------- nav: outside the clip, so it is always reachable ------------- */
.flow__nav {
  display: none;
  align-items: center;
  gap: 14px;
  width: var(--shell);
  margin: 24px auto 0;
}
/* inside the sticky block the heading needs its own breathing room */
.flow__head { width: var(--shell); margin: 0 auto 28px; text-align: center; }
.flow__count {
  font: 600 13px var(--font-mono);
  color: var(--body);
  letter-spacing: .04em;
}
.flow__btn {
  width: 48px; height: 48px;
  border-radius: 50%;
  border: none;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: background-color .2s ease, color .2s ease, opacity .2s ease;
}
.flow__btn svg { width: 18px; height: 18px; }
.flow__btn--prev { background: var(--panel); color: var(--ink); }
.flow__btn--prev:hover { background: #e9eaee; }
.flow__btn--next { background: var(--ink); color: #fff; }
.flow__btn--next:hover { background: #23232b; }
.flow__btn[aria-disabled="true"] { opacity: .35; cursor: default; }
.flow__btn[disabled] { opacity: .35; cursor: default; }

/* ---------- linked mode: added by JS once GSAP is actually armed --------- */
/* The heading now lives INSIDE the sticky block, so it stays on screen with
   the cards instead of scrolling up under the fixed nav. `--flow-top` is
   measured in js/v3.js so the whole block sits balanced in the viewport
   rather than hugging the nav with dead space beneath. */
.flow--linked .flow__sticky { position: sticky; top: var(--flow-top, 96px); }

.flow--linked .flow__viewport { overflow: clip; }
@supports not (overflow: clip) {
  /* safe only because nothing inside the viewport is focusable */
  .flow--linked .flow__viewport { overflow: hidden; }
}

.flow--linked .flow__track {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--card-gap);
  width: auto;
  margin-inline: 0;
  /* left gutter only: the end cap's percentage basis must resolve against
     `viewport - gutter` so the strip finishes flush with the window edge */
  padding-left: var(--flow-gutter);
  padding-right: 0;
  will-change: transform;
}
.flow--linked .fcard { flex: 0 0 var(--card-w); }
.flow--linked .fcard--end {
  display: block;
  flex: 0 0 max(280px, calc(100% - var(--card-w) - var(--card-gap)));
  height: var(--card-h);
  background: none;
  padding: 0;
}
.flow--linked .flow__nav { display: flex; }

/* The travel distance must live INSIDE .flow__travel's content box. A sticky
   element is constrained to its parent's content box, so expressing the travel
   as padding-bottom put it outside — the content box was then exactly the
   sticky element's own height, giving zero travel, and the section simply
   scrolled away. (v2's .pinbox worked because its parent's height came from a
   sibling column's real content.) Hence a spacer element, not padding. */
.flow__spacer { display: none; }
.flow--linked .flow__spacer {
  display: block;
  height: calc(var(--flow-n1) * (var(--card-w) + var(--card-gap)) * var(--flow-pace));
}

/* the viewport becomes a keyboard target in linked mode; :where(a, button)
   in the base focus rule does not match a div[tabindex] */
.flow__viewport:focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 6px;
  border-radius: 12px;
}

/* ---------- phones ------------------------------------------------------
   Two modes, same as desktop. The FALLBACK (no JS, reduced motion, a viewport
   too short to pin) is the native horizontal scroller below; `.flow--linked`
   upgrades it to the same sticky strip desktop gets. Deliberately NOT gated on
   `.js` — a phone with no JS still gets the full carousel. v2 hid these four
   screenshots entirely below 760px.                                        */
@media (max-width: 759px) {
  /* Card geometry as TOKENS, so the sticky budget, the end cap and the
     spacer's travel all derive from the same two numbers. The sticky window
     is head + cards, and it has to clear the 64px nav at top:12 and still
     leave the section's bottom edge on screen — hence 46vh, not 62vh. */
  .flow {
    --card-w: 80vw;
    --card-gap: 14px;
    --card-h: clamp(280px, 46vh, 380px);
    --flow-gutter: 16px;
    --flow-pace: 1;
  }

  /* Linked mode transforms the track, so the native scroller must stand
     down — two owners of the horizontal axis is the whole bug. `overflow:
     visible` is what actually stops it being a scroll container; the snap and
     momentum properties below are then inert, but reset anyway so the
     stylesheet says what it means. */
  .flow--linked .flow__track {
    overflow: visible;
    scroll-snap-type: none;
    scroll-padding-inline-start: 0;
    -webkit-overflow-scrolling: auto;
  }
  .flow--linked .fcard { scroll-snap-align: none; }
  /* the strip advances with the page now; the arrows would be a second,
     conflicting way to move it, and they cost 48px of the sticky budget */
  .flow--linked .flow__nav { display: none; }

  .flow__track {
    display: flex;
    flex-wrap: nowrap;
    gap: 14px;
    width: auto;
    margin-inline: 0;
    padding-left: var(--flow-gutter);
    padding-right: var(--flow-gutter);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline-start: var(--flow-gutter);
    overscroll-behavior-x: contain;   /* a swipe past the last card must not go back */
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* scroll-behavior INHERITS. Without this the track picks up
       `html { scroll-behavior: smooth }`, so every programmatic scrollLeft
       write animates and collides with the next one — which is what made the
       coupled strip stutter. Programmatic writes must be instant; goTo() asks
       for smooth explicitly when it wants it. */
    scroll-behavior: auto;
  }

  /* four chips of 143-167px cannot fit a phone; two can */
  /* four chips of 143-167px cannot fit a phone; two can */
  .mq { --mq-cols: 2; }

  .flow__track::-webkit-scrollbar { display: none; }
  .fcard {
    flex: 0 0 var(--card-w);          /* the next card peeks ~20% */
    scroll-snap-align: start;
    height: var(--card-h);
  }
  .flow__nav { display: none; }
}

/* ---------- reduced motion: the grid, all four visible, no coupling ------ */
@media (prefers-reduced-motion: reduce) {
  .fcard, .fcard::before, .fcard__num, .fcard__title, .fcard__step { transition: none; }
}
:root.reduced .fcard,
:root.reduced .fcard::before,
:root.reduced .fcard__num,
:root.reduced .fcard__title,
:root.reduced .fcard__step { transition: none; }

/* ==========================================================================
   Animation start states.

   Only above-the-fold elements are pre-hidden. Everything below the fold gets
   its start state from JS at init, so a blocked CDN can never leave content
   invisible — and js/v3.js drops the `js` class if GSAP never arrives.
   ========================================================================== */
.js:not(.reduced) .nav { opacity: 0; }
.js:not(.reduced) .f1 { opacity: 0; }
.js:not(.reduced) .hero__mock { opacity: 0; }

/* NOT a transform: GSAP folds a CSS translate into its own `y` baseline, and a
   yPercent tween then lands at 110% of element height instead of 0. The masked
   words are moved by GSAP alone.

   The <h1> is the likely LCP element, so its reveal must not wait on the CDN
   forever. GSAP reveals it with an instant .set() ~0.25s in; if that never
   arrives this keyframe reveals it at 1s regardless. Safe against the intro
   because the reveal is a `set`, not a tween — no in-flight opacity to clobber. */
.js:not(.reduced) .hero__title {
  opacity: 0;
  animation: smcu-title-reveal .01s linear 1s forwards;
}
@keyframes smcu-title-reveal { to { opacity: 1; } }

/* ==========================================================================
   Phone spacing. The desktop rhythm (120px section padding, 40px panel
   padding, 520px panel min-height) stacks into a very tall, very empty page
   once everything is one column, so it is compressed here.
   ========================================================================== */
@media (max-width: 759px) {
  .sec { padding: 68px 0; }
  .sec--tight { padding: 52px 0; }

  .sec__lede { margin-top: 16px; }

  .panels { gap: 16px; margin-top: 40px; }
  .panel { padding: 26px 22px; min-height: 0; border-radius: 30px; }
  .panel__stage { margin: 22px -22px 18px; }
  .panel__title { margin-top: 20px; }

  .cards3, .cards4 { gap: 16px; margin-top: 40px; }
  .card3 { padding: 26px 22px; border-radius: 30px; }
  .card3__title { margin-top: 20px; }

  .stats { gap: 14px; margin-top: 36px; }
  .stat { padding: 24px 22px; }

  .compare__box { margin-top: 32px; }
  .faq__list { margin-top: 32px; }
  .cta-sec { padding: 24px 0 68px; }
  .cta { border-radius: 30px; }
  .klien { padding: 28px 0 28px; }   /* symmetric: the band is a visible surface now */

  .footer__top { padding: 40px 0 32px; }
}

/* --- reduced motion ------------------------------------------------------
   Keyed on BOTH signals on purpose: js/v3.js branches on the `reduced` class
   (set from matchMedia before first paint), so the class must produce the same
   result as the media query — and the query alone still covers a JS-disabled
   reduced-motion visitor. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .nav { transition: none; }
  .hero__atmos-layer, .cta__atmos-layer { animation: none; }
}
:root.reduced { scroll-behavior: auto; }
:root.reduced .nav { transition: none; }
:root.reduced .hero__atmos-layer,
:root.reduced .cta__atmos-layer { animation: none; }
