/* ============================================================
   UWU INVITES v2 — Apple-class, theme-switchable
   Default: dark (premium MacBook Pro feel).
   [data-theme="light"]: warm-paper light feel (uwuinvites.com).
   ============================================================ */

/* Google Fonts (Inter + Instrument Serif) loaded via <link> in
   home-v2.html head with preconnect — was @import here which forced
   serial CSS-then-font fetching. Moving to HTML head + preconnect
   parallelizes the DNS/TLS handshake with CSS download. */

:root,
[data-theme="dark"] {
  --ink: #f5f5f7;
  --ink-2: #c7c7cc;
  /* Brighter than the prior #86868b (5.04:1) and #6e6e73 (4.43:1 —
     below AA's 4.5:1 floor). #a1a1a6 ≈ 6.07:1; #8e8e93 ≈ 5.05:1.
     Both pass WCAG AA for normal text on the --paper #000 background;
     supporting copy reads less dim while staying on-brand. */
  --ink-3: #a1a1a6;
  --ink-4: #8e8e93;
  --hair: rgba(255,255,255,0.10);
  --hair-2: rgba(255,255,255,0.06);
  --paper: #000000;
  --paper-2: #0d0d0f;
  --paper-3: #131316;
  --surface: rgba(255,255,255,0.025);
  --surface-2: rgba(255,255,255,0.05);
  --surface-strong: rgba(255,255,255,0.08);

  --accent: #c9a66b;
  --accent-2: #d4b27a;
  --accent-ink: #c9a66b;
  --accent-glow: rgba(201,166,107,0.10);

  --nav-bg: rgba(0,0,0,0.5);
  --nav-bg-scrolled: rgba(0,0,0,0.72);
  --nav-border: rgba(255,255,255,0.08);

  --shadow-card: 0 30px 60px -20px rgba(0,0,0,0.7), 0 8px 20px -10px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06);
  --shadow-hero: 0 50px 80px -20px rgba(0,0,0,0.8), 0 16px 30px -10px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.06);
}

[data-theme="light"] {
  --ink: #1d1d1f;
  --ink-2: #424245;
  --ink-3: #6e6e73;
  --ink-4: #86868b;
  --hair: #d2d2d7;
  --hair-2: #e8e8ed;
  --paper: #fbfaf7;
  --paper-2: #f4f1ec;
  --paper-3: #efeae3;
  --surface: #ffffff;
  --surface-2: #f8f6f2;
  --surface-strong: #ffffff;

  --accent: #b48066;
  --accent-2: #c89a82;
  --accent-ink: #7a3a32;
  --accent-glow: rgba(180,128,102,0.14);

  --nav-bg: rgba(251,250,247,0.6);
  --nav-bg-scrolled: rgba(251,250,247,0.82);
  --nav-border: rgba(0,0,0,0.06);

  --shadow-card: 0 30px 60px -28px rgba(70,40,30,0.22), 0 8px 20px -10px rgba(70,40,30,0.10), 0 0 0 1px rgba(0,0,0,0.03);
  --shadow-hero: 0 50px 80px -28px rgba(70,40,30,0.28), 0 16px 30px -10px rgba(70,40,30,0.14), 0 0 0 1px rgba(0,0,0,0.03);
}

:root {
  --ink-on-dark: #f5f5f7;
  --ink-on-dark-2: #a1a1a6;
  --hair-on-dark: rgba(255,255,255,0.10);
  --paper-dark: #000000;
  --paper-dark-2: #0a0a0a;

  --r-1: 8px;
  --r-2: 14px;
  --r-3: 22px;
  --r-4: 32px;

  --t-mono: "SF Mono", ui-monospace, Menlo, monospace;
  --t-sans: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Display",
    "Helvetica Neue", Arial, sans-serif;
  --t-serif: "Instrument Serif", "Times New Roman", serif;

  --pad-x: clamp(20px, 4vw, 64px);
  --w-readable: 720px;
  --w-frame: 1280px;
  --w-wide: 1440px;
}

* {
  box-sizing: border-box;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

html, body { margin: 0; padding: 0; }

/* Skip-link CSS removed Round 4.7 — see bundle-v2.jsx for context.
   The .skip-link rules are kept out of the bundle so a stray
   element with that class never silently re-introduces the popup
   the operator reported. */

html {
  scroll-behavior: smooth;
  background: var(--paper);
  transition: background 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

body {
  font-family: var(--t-sans);
  font-size: 17px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
  letter-spacing: -0.01em;
  overflow-x: hidden;
  transition:
    background 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    color 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

button { font-family: inherit; border: 0; cursor: pointer; background: transparent; color: inherit; letter-spacing: inherit; }
a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; }
::selection { background: var(--accent); color: #0a0a0a; }

/* Apple type scale */
.eyebrow { font-size: 13px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
.eyebrow-accent { font-size: 13px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent-ink); }

.h-hero {
  font-family: var(--t-sans);
  font-size: clamp(64px, 12vw, 168px);
  line-height: 0.92;
  letter-spacing: -0.05em;
  /* Anchor weight 500 — pairs with .hero-stack-item weight 700 italic
     accent for visible emphasis hierarchy within a single typeface
     (single-voice principle, see hero-stack-item rule). Was 600 in
     the previous build. */
  font-weight: 500;
  margin: 0;
}

.h-display {
  font-family: var(--t-sans);
  font-size: clamp(48px, 8.5vw, 112px);
  line-height: 1;
  letter-spacing: -0.04em;
  font-weight: 600;
  margin: 0;
}

.h-1 { font-size: clamp(40px, 6vw, 72px); line-height: 1.04; letter-spacing: -0.035em; font-weight: 600; margin: 0; }
.h-2 { font-size: clamp(32px, 4vw, 52px); line-height: 1.08; letter-spacing: -0.03em; font-weight: 600; margin: 0; }
.h-3 { font-size: clamp(22px, 2vw, 28px); line-height: 1.18; letter-spacing: -0.02em; font-weight: 600; margin: 0; }

.serif { font-family: var(--t-serif); font-weight: 400; letter-spacing: -0.01em; }
.italic { font-style: italic; }

.lede {
  font-size: clamp(18px, 1.6vw, 22px);
  line-height: 1.45;
  color: var(--ink-2);
  font-weight: 400;
  max-width: 32ch;
}

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 46px;
  padding: 0 22px;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.005em;
  touch-action: manipulation;
  transition:
    transform 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    background 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    border-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    box-shadow 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  white-space: nowrap;
}

[data-theme="dark"] .btn-primary { background: var(--ink); color: #0a0a0a; }
[data-theme="dark"] .btn-primary:hover { background: #ffffff; transform: translateY(-1px); }
[data-theme="light"] .btn-primary { background: #1d1d1f; color: #ffffff; }
[data-theme="light"] .btn-primary:hover { background: #000000; transform: translateY(-1px); }

[data-theme="dark"] .btn-ghost { background: transparent; color: var(--ink); border: 1px solid rgba(255,255,255,0.18); }
[data-theme="dark"] .btn-ghost:hover { border-color: var(--ink); background: rgba(255,255,255,0.04); }
[data-theme="light"] .btn-ghost { background: transparent; color: var(--ink); border: 1px solid rgba(0,0,0,0.16); }
[data-theme="light"] .btn-ghost:hover { border-color: var(--ink); background: rgba(0,0,0,0.03); }

.btn-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  font-weight: 500;
  color: var(--accent-ink);
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
  cursor: pointer;
  touch-action: manipulation;
}
.btn-link:hover { border-color: currentColor; }
.btn-link svg { transition: transform 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
.btn-link:hover svg { transform: translateX(3px); }

/* Keyboard focus rings — `:focus-visible` only fires for keyboard
   navigation, not mouse clicks. Outline + offset stays outside the
   pill radius so it reads cleanly. Color matches accent so the ring
   ties to brand vocabulary rather than browser-default blue.
   WCAG 2.4.7 — Focus Visible. */
.btn:focus-visible,
.btn-link:focus-visible,
.sp-cta-primary:focus-visible,
.sp-cta-secondary:focus-visible,
.sticky-cta-mobile__btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Sections */
.section { padding: clamp(80px, 12vh, 160px) var(--pad-x); }
.section-tight { padding: clamp(64px, 9vh, 120px) var(--pad-x); }
.section-paper-2 { background: var(--paper-2); transition: background .6s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
.section-paper-3 { background: var(--paper-3); transition: background .6s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
.section-dark { background: #000000; color: #f5f5f7; }
.section-dark .eyebrow { color: var(--ink-on-dark-2); }
.section-dark .lede { color: var(--ink-on-dark-2); }

.frame { max-width: var(--w-frame); margin: 0 auto; }
.frame-wide { max-width: var(--w-wide); margin: 0 auto; }
.frame-readable { max-width: var(--w-readable); margin: 0 auto; }

image-slot {
  --slot-bg: var(--paper-2);
  --slot-fg: var(--ink-3);
  --slot-border-color: var(--hair-2);
  --slot-border-width: 0px;
}

.row { display: flex; align-items: center; gap: 12px; }
.stack { display: flex; flex-direction: column; }
.center { display: flex; align-items: center; justify-content: center; }
.muted { color: var(--ink-3); }
.hairline { height: 1px; background: var(--hair); border: 0; }
.hairline-dark { height: 1px; background: var(--hair-on-dark); border: 0; }

/* Reveal */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.9s cubic-bezier(0.2, 0.8, 0.2, 1),
    transform 0.9s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.reveal.in { opacity: 1; transform: none; }
.reveal-d1 { transition-delay: 0.08s; }
.reveal-d2 { transition-delay: 0.16s; }
.reveal-d3 { transition-delay: 0.24s; }
.reveal-d4 { transition-delay: 0.32s; }
.reveal-d5 { transition-delay: 0.40s; }

/* Reduced-motion guard for the foundational .reveal class. Per-
   component PRM blocks (cookie, sticky-cta, heroSlide, showcase-word,
   ai-cursor) already exist, but the base .reveal was the last
   un-guarded animation on the page. Vestibular users now see all
   reveal-class content (hero h1, lede, CTAs, feature cards) at full
   opacity without translateY motion. */
@media (prefers-reduced-motion: reduce) {
  .reveal,
  .reveal.in {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* Scrollbars */
@media (hover: hover) {
  ::-webkit-scrollbar { width: 10px; height: 10px; }
  ::-webkit-scrollbar-thumb { background: var(--surface-strong); border-radius: 999px; }
  ::-webkit-scrollbar-track { background: transparent; }
}

@media (max-width: 720px) {
  body { font-size: 16px; }
  .btn { height: 44px; padding: 0 18px; }
}

/* ============== v2-only ============== */

/* Crossfade word-stack hero — 3 items, 9s loop, opacity-driven.
   Each item: fade-in 0%→5% (0.45s), visible plateau 5%→30% (2.25s),
   fade-out 30%→40% (0.9s), invisible 40%→100%. Items staggered by
   3s (33% of cycle), so as item N fades out at 30-40% of its cycle,
   item N+1 fades in at 0-5% of its cycle — windows overlap.
   Item 1 uses negative animation-delay (-0.45s) so at first paint
   it's already at 5% (visible plateau) — no blank on page load. */
.hero-stack {
  position: relative;
  display: block;
  overflow: visible;
  width: 100%;
  min-height: 1.3em;
}
.hero-stack-inner {
  display: block;
}
.hero-stack-item {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  /* SINGLE-VOICE HEADLINE — emphasis via weight + italic + color,
     NOT typeface swap. Anchor line ".h-hero" stays Inter weight 500;
     rotating phrases here are Inter weight 700 italic in accent
     color. Same typeface, multiple emphasis vectors. Apple iPhone-
     Pro pattern: "Titanium. So strong. So light. So Pro." — never
     mixes typefaces in one H1.
     SIZE: 0.75em of the H1 anchor — sub-hierarchy below the static
     line, prevents the gold italic phrase from overflowing on
     desktop viewports and creates a clear "anchor + accent" reading
     order. Was 1em (same size as anchor) which on >=1440px viewports
     made the longest phrase "diceritakan kata demi kata." bleed
     past the right edge. */
  font-style: italic;
  font-family: inherit;
  font-weight: 700;
  font-size: 0.75em;
  color: var(--accent);
  letter-spacing: -0.04em;
  white-space: nowrap;
  opacity: 0;
  /* Initial state matches keyframe 0% — subtle 8px slide-up entry,
     no jarring 40px jump (which previously paired with the upward
     exit and caused overlap with the static H1 line above). */
  transform: translateY(8px);
  animation: heroSlide 9s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite both;
}
/* Symmetric 3s delays — every item has the same N→N+1 gap, so the
   transitions are visually identical. Slide-in/out + opacity is
   driven by heroSlide keyframe; first-paint shows item 1 already
   in its visible plateau. */
.hero-stack-item:nth-child(1) { animation-delay: 0s; }
.hero-stack-item:nth-child(2) { animation-delay: 3s; }
.hero-stack-item:nth-child(3) { animation-delay: 6s; }

/* ============================================================
   Social-proof section — between Features and RSVP. Theme-aware:
   uses --paper-2 for the section background so it tonally
   separates from the main --paper background without breaking
   light-mode. */

.social-proof-section {
  background: var(--paper-2);
  padding: 80px var(--pad-x);
  transition: background 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.social-proof-container {
  max-width: 800px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 48px;
}

/* Layer 1 — metrics */
.sp-eyebrow {
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--ink-3);
  text-align: center;
  margin: 0 0 32px;
  text-transform: uppercase;
  font-weight: 500;
}
.sp-metric-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  text-align: center;
}
.sp-metric-item {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.sp-metric-number {
  font-size: clamp(40px, 8vw, 72px);
  font-weight: 700;
  color: var(--accent);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
/* Hierarchy: hero claim larger than supporting claims so the eye
   lands on the highest-trust signal first. Apple spec-section
   pattern — one dominant stat anchors the row. */
.sp-metric-number--hero { font-size: clamp(56px, 11vw, 104px); }
.sp-metric-number--sub  { font-size: clamp(34px, 6vw, 56px); }
.sp-metric-label {
  font-size: 13px;
  color: var(--ink-3);
  line-height: 1.4;
}

/* Divider */
.sp-divider {
  height: 1px;
  background: var(--hair);
}

/* Layer 2 — founder quote */
.sp-founder { display: flex; justify-content: center; }
.sp-founder-content { max-width: 560px; text-align: center; }
.sp-founder-quote {
  font-size: 18px;
  line-height: 1.6;
  color: var(--ink-2);
  font-style: italic;
  font-family: var(--t-serif);
  font-weight: 400;
  margin: 0 0 20px;
}
.sp-founder-attribution {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: center;
}
.sp-founder-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.sp-founder-role {
  font-size: 12px;
  color: var(--ink-3);
  letter-spacing: 0.04em;
}

/* Layer 3 — invitation card */
.sp-invitation {
  text-align: center;
  padding: 40px 24px;
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: 20px;
}
.sp-invitation-eyebrow {
  font-size: 11px;
  letter-spacing: 0.18em;
  color: var(--accent-ink);
  margin: 0 0 20px;
  text-transform: uppercase;
  font-weight: 600;
}
.sp-invitation-heading {
  font-size: clamp(24px, 5vw, 40px);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
  margin: 0 0 16px;
  letter-spacing: -0.02em;
}
.sp-invitation-heading em {
  font-style: italic;
  color: var(--accent);
  font-weight: 400;
  font-family: var(--t-serif);
}
.sp-invitation-body {
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink-2);
  max-width: 480px;
  margin: 0 auto 32px;
}
.sp-invitation-cta {
  display: flex;
  gap: 16px;
  justify-content: center;
  flex-wrap: wrap;
  margin-bottom: 16px;
}
.sp-cta-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 12px 28px;
  background: var(--ink);
  color: var(--paper);
  border-radius: 999px;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  touch-action: manipulation;
  transition:
    transform 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    opacity 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.sp-cta-primary:hover {
  transform: translateY(-1px);
  opacity: 0.95;
}
.sp-cta-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 12px 28px;
  background: transparent;
  color: var(--ink-2);
  border: 1px solid var(--hair);
  border-radius: 999px;
  font-size: 15px;
  text-decoration: none;
  touch-action: manipulation;
  transition:
    border-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.sp-cta-secondary:hover {
  border-color: var(--ink-3);
  color: var(--ink);
}
.sp-invitation-hint {
  font-size: 12px;
  color: var(--ink-3);
  margin: 0;
}

@media (max-width: 480px) {
  .social-proof-section {
    padding: 56px var(--pad-x);
  }
  .sp-metric-grid { gap: 12px; }
  .sp-metric-number { font-size: clamp(28px, 9vw, 40px); }
  .sp-metric-label { font-size: 11px; }
  .sp-eyebrow { font-size: 11px; letter-spacing: 0.18em; }
  .sp-founder-quote { font-size: 16px; }
  .sp-invitation { padding: 28px 16px; }
  .sp-invitation-cta { flex-direction: column; align-items: stretch; }
  .sp-cta-primary,
  .sp-cta-secondary { width: 100%; }
}

/* ============================================================
   Cookie consent banner — UU PDP compliance. Fixed bottom, dark
   tonal layer, copper primary button. z-index 9999 sits above
   everything; the banner intentionally takes priority over the
   StickyCTA (z-index 50) so the consent decision is made before
   the user engages other UI. */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 9999;
  background: rgba(0, 0, 0, 0.97);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  transform: translateY(100%);
  opacity: 0;
  pointer-events: none;
  transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
              opacity 0.4s ease-out;
}
.cookie-banner--visible {
  transform: translateY(0);
  opacity: 1;
  pointer-events: auto;
}
.cookie-banner__content {
  max-width: 960px;
  margin: 0 auto;
  padding: 16px 24px calc(16px + env(safe-area-inset-bottom, 0px));
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}
.cookie-banner__text { flex: 1; min-width: 200px; }
.cookie-banner__text p {
  font-size: 13px;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.65);
  margin: 0;
}
.cookie-banner__link {
  color: #c9a66b;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.cookie-banner__link:hover { color: #d4b27a; }
.cookie-banner__detail {
  width: 100%;
  padding: 12px 0;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.cookie-banner__detail-text {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.5);
  margin: 0 0 6px;
  line-height: 1.5;
}
.cookie-banner__actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-shrink: 0;
}
.cookie-banner__btn {
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  border-radius: 8px;
  padding: 10px 16px;
  cursor: pointer;
  border: none;
  touch-action: manipulation;
  transition:
    background 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    border-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  white-space: nowrap;
  min-height: 44px;
}
.cookie-banner__btn--primary {
  background: #c9a66b;
  color: #1a1000;
}
.cookie-banner__btn--primary:hover {
  background: #d4b27a;
}
.cookie-banner__btn--secondary {
  background: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.78);
  border: 1px solid rgba(255, 255, 255, 0.12);
}
.cookie-banner__btn--secondary:hover {
  background: rgba(255, 255, 255, 0.14);
  color: #ffffff;
}
/* Mobile: banner pads its own bottom by the StickyCTA height so the
   two bars stack instead of overlapping. ~76px is the visual height
   of the sticky CTA's button + padding + safe-area. */
@media (max-width: 768px) {
  .cookie-banner__content {
    padding: 12px 16px;
    padding-bottom: calc(12px + 76px + env(safe-area-inset-bottom, 0px));
  }
  .cookie-banner__actions {
    width: 100%;
    justify-content: flex-end;
  }
}
@media (prefers-reduced-motion: reduce) {
  .cookie-banner { transition: none; }
}

/* Mobile-only bottom sticky CTA. Hidden on desktop entirely.
   Visibility is React-state-controlled by .visible class — the
   transform+opacity transition gives a smooth reveal when the
   user scrolls past 300px and hides cleanly near footer. */
.sticky-cta-mobile { display: none; }
@media (max-width: 768px) {
  .sticky-cta-mobile {
    display: block;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 50;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
    background: rgba(0, 0, 0, 0.92);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
    border-top: 1px solid var(--hair);
    transform: translateY(100%);
    opacity: 0;
    pointer-events: none;
    transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94),
                opacity 0.3s ease-out;
  }
  .sticky-cta-mobile.visible {
    transform: translateY(0);
    opacity: 1;
    pointer-events: auto;
  }
  .sticky-cta-mobile__btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 48px;
    padding: 12px 24px;
    background: #ffffff;
    color: #0a0a0a;
    border-radius: 12px;
    font-weight: 600;
    font-size: 15px;
    letter-spacing: -0.01em;
    text-decoration: none;
  }
  [data-theme="light"] .sticky-cta-mobile {
    background: rgba(251, 250, 247, 0.92);
    border-top: 1px solid var(--hair);
  }
  [data-theme="light"] .sticky-cta-mobile__btn {
    background: #1d1d1f;
    color: #ffffff;
  }
}
@media (prefers-reduced-motion: reduce) {
  .sticky-cta-mobile { transition: none; }
}

/* Mobile rotating-text font cap — h-hero inline override is
   clamp(40px, 10vw, 127px), so 10vw ≈ 39px on a 390px viewport.
   Italic serif + 25-char phrases ("diceritakan dengan indah") +
   `white-space: nowrap` on .hero-stack-item caused horizontal
   overflow on phones. Shrink the rotating line specifically below
   480px so it fits on one line without overflowing the viewport. */
@media (max-width: 480px) {
  .hero-stack-item {
    font-size: clamp(22px, 6.5vw, 34px);
  }
}

/* 3 items @ 9s with 3s stagger = 33.3% slot each. Visible window must
   fit ≤33% to prevent overlap; previously 12-80% (68%) caused 3+s of
   simultaneous on-screen text. New: visible 5-28% (~2.07s), exit
   complete at 33%, then held off-screen for the remainder of cycle.

   EXIT BUG FIX: prior keyframe translated UP by -40px on exit,
   causing the rotating phrase to drift INTO the static line above
   ("Cerita cinta kalian,") during the fade-out window — visible
   ghost-text collision on mobile viewports where line height is
   compressed. Apple iPad-Pro pattern uses pure fade (no translate
   on exit) — matched here. Subtle 8px slide remains on entry so
   the phrase still "arrives" with character. */
@keyframes heroSlide {
  0%   { opacity: 0; transform: translateY(8px); }
  5%   { opacity: 1; transform: translateY(0); }
  28%  { opacity: 1; transform: translateY(0); }
  33%  { opacity: 0; transform: translateY(0); }
  34%  { opacity: 0; transform: translateY(8px); }
  100% { opacity: 0; transform: translateY(8px); }
}

@media (prefers-reduced-motion: reduce) {
  @keyframes heroSlide {
    0%       { opacity: 0; transform: none; }
    5%, 28%  { opacity: 1; transform: none; }
    33%      { opacity: 0; transform: none; }
    34%, 100% { opacity: 0; transform: none; }
  }
}

/* Zanura parent-company logo in footer — two assets shipped:
   zanura-light.png (light-colored, visible on dark surfaces) and
   zanura-dark.png (dark-colored, visible on light surfaces).
   Display swap is CSS-only — prior version set inline `display` in
   JSX which beat the !important override (inline styles always win
   over stylesheet rules regardless of !important). Now defaults are
   set here, [data-theme="light"] flips them. */
.zanura-mark--on-dark { display: block; }
.zanura-mark--on-light { display: none; }
[data-theme="light"] .zanura-mark--on-dark { display: none; }
[data-theme="light"] .zanura-mark--on-light { display: block; }

/* Showcase Hawa & Adam name overlay — on mobile the sticky-div uses
   gridTemplateRows: "190px 1fr 140px", so its geometric center sits
   ABOVE the phone frame's visual center (header row 190 > footer row
   140). The flex-center on .showcase-name-overlay aligns to the
   sticky-div, not the phone. Visual fix: pull the overlay up so it
   sits visually centered on the phone chassis area on mobile. */
@media (max-width: 720px) {
  .showcase-name-overlay {
    /* Subtle nudge up — was -32px which went too far (user feedback
       "naiknya terlalu banyak"). 10px brings the overlay slightly
       above the geometric center of the sticky-div, just enough to
       align with the phone chassis center without over-correcting. */
    transform: translateY(-10px);
  }
}

/* Showcase device-transition heading — allow word spans to wrap
   naturally on narrow viewports. Without this, the inline default
   keeps the whole phrase on one line and the right edge cuts off
   at small viewports. */
.showcase-word { word-break: keep-all; overflow-wrap: break-word; }
@media (max-width: 480px) {
  /* Shrink the heading itself on phones so it fits without breaking
     mid-word. Targets the parent <h2> inside the showcase intro
     block — h-2 ships at clamp(32px, 4vw, 52px); the floor was
     too tall at 390px viewports. */
  .showcase-word {
    font-size: clamp(22px, 6vw, 30px);
  }
  /* Cap h-2 inside the showcase section on phones — both the intro
     ("Dari satu undangan, ke seluruh hari kalian.") and the
     device-transition headings inherit from .h-2 which floors at
     32px. At 390px viewports with italic serif + 38+ chars, that
     overflowed the right edge. Section-scoped so it doesn't leak
     into other .h-2 usages elsewhere on the page. */
  #showcase .h-2 {
    font-size: clamp(22px, 6vw, 30px);
    word-break: keep-all;
    overflow-wrap: break-word;
    /* Defensive: ensure padding doesn't add to width (some browsers
       still default to content-box on h2) and the element can shrink
       below its content width when inside flex/grid ancestors. */
    box-sizing: border-box;
    min-width: 0;
    max-width: 100%;
  }
  /* Same guards on the absolute-positioned wrapper that holds both
     showcase H2s — `position: absolute; left: 0; right: 0` fills the
     parent grid cell, which can stretch wider than the viewport
     under certain sticky-scroll layouts. */
  #showcase [class="h-2"] + p,
  #showcase .eyebrow-accent,
  #showcase .eyebrow {
    max-width: 100%;
    box-sizing: border-box;
  }
}

/* WA FAB lift when the mobile sticky CTA is visible — body class
   toggled by StickyCTA. !important is required because the FAB's
   bottom is set inline (would otherwise win the cascade). */
body.sticky-cta-active .wa-float-btn {
  bottom: 92px !important;
  transition: bottom 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

/* Mobile: keep WA FAB at the WCAG 44×44 floor (was 40×40 — below
   the AC tap target rule). On 360px Samsung A24 (UWU primary target)
   the 4px difference is the line between "tappable on first try" and
   "fat-thumb misses". Earlier attempt that added `padding-right: 56px`
   on .sp-invitation + .founder-section-wrapper broke the visual
   centering of those sections (reverted) — the FAB now sits at the
   right edge gracefully at 44px instead. */
@media (max-width: 480px) {
  .wa-float-btn {
    width: 44px !important;
    height: 44px !important;
    right: 16px !important;
    /* Lift the FAB above content edge by default on phones so it
       doesn't sit over the right edge of readable text. */
    bottom: calc(80px + env(safe-area-inset-bottom, 16px)) !important;
  }
  .wa-float-btn svg {
    width: 20px;
    height: 20px;
  }
}

/* Showcase device-transition heading — words light up in sequence as
   the user scrolls through the section. Default state is dimmed
   (ink-3); when scroll progress crosses each word's threshold, JSX
   adds the .showcase-word--lit class and the word transitions to
   the accent color. */
.showcase-word {
  color: var(--ink-3);
  transition: color 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  display: inline;
}
.showcase-word--lit {
  color: var(--accent);
}
@media (prefers-reduced-motion: reduce) {
  .showcase-word { transition: none; }
}

/* Showcase template-switcher "+N lainnya" discovery chip. Sits at
   the end of the tab strip; visually distinct (no pill background)
   so it reads as a link, not another tab. Uses theme tokens so it
   works in both light and dark modes. */
.showcase-more-chip {
  display: inline-flex;
  align-items: center;
  padding: 6px 14px;
  border-radius: 999px;
  border: 1px solid var(--hair);
  color: var(--ink-3);
  font-size: 14px;
  font-family: var(--t-sans);
  letter-spacing: 0.01em;
  text-decoration: none;
  transition:
    color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    border-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    background 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  white-space: nowrap;
}
.showcase-more-chip:hover {
  color: var(--ink);
  border-color: var(--ink-3);
  background: rgba(255,255,255,0.04);
}

/* Dual CTA group below template switcher. Centered, wraps on narrow
   viewports so the secondary link drops to its own line if needed. */
.showcase-cta-group {
  display: flex;
  align-items: center;
  gap: 24px;
  justify-content: center;
  flex-wrap: wrap;
}
.showcase-cta-secondary {
  color: var(--ink-3);
}
.showcase-cta-secondary:hover {
  color: var(--ink-2);
}

/* ============================================================
   AI card typewriter demo — replaces the static stagger reveal.
   All visuals use theme tokens so it adapts to light + dark modes.
   Cursor + thinking-dots use copper accent for the AI identity.
   ============================================================ */
.ai-demo-container {
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: 16px;
  padding: 18px;
  width: 100%;
  max-width: 320px;
  box-sizing: border-box;
  font-family: var(--t-sans);
}

.ai-input-bubble {
  background: rgba(201, 166, 107, 0.14);
  border: 1px solid rgba(201, 166, 107, 0.28);
  border-radius: 12px 12px 12px 4px;
  padding: 11px 13px;
  margin-bottom: 12px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink);
  min-height: 44px;
}
.ai-cursor {
  display: inline-block;
  width: 2px;
  height: 1em;
  background: var(--accent);
  margin-left: 1px;
  vertical-align: text-bottom;
  animation: aiCursorBlink 0.7s step-end infinite;
}
@keyframes aiCursorBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

.ai-thinking {
  display: flex;
  gap: 4px;
  padding: 6px 4px 10px;
  opacity: 0;
  transition: opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.ai-dot-pulse {
  width: 6px;
  height: 6px;
  background: rgba(201, 166, 107, 0.7);
  border-radius: 50%;
  animation: aiDotPulse 1s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
@keyframes aiDotPulse {
  0%, 100% { opacity: 0.3; transform: scale(0.8); }
  50%      { opacity: 1;   transform: scale(1); }
}

.ai-header {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  color: var(--ink-3);
  letter-spacing: 0.02em;
  margin-bottom: 10px;
}
.ai-dot {
  width: 8px;
  height: 8px;
  background: var(--accent);
  border-radius: 50%;
}

.ai-lang-card {
  border: 1px solid var(--hair);
  background: var(--surface);
  border-radius: 8px;
  padding: 8px 10px;
  margin-bottom: 6px;
  opacity: 0;
  transition: opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.ai-lang-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 3px;
}
.ai-flag { font-size: 13px; }
.ai-lang-name {
  font-size: 10px;
  color: var(--ink-3);
  letter-spacing: 0.02em;
}
.ai-lang-output {
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--ink-2);
  margin: 0;
  min-height: 17px;
}
.ai-lang-output[dir="rtl"] {
  text-align: right;
  font-family: "Noto Naskh Arabic", "Amiri", "Times New Roman", serif;
}

.ai-cta-btn {
  display: block;
  width: 100%;
  padding: 10px 14px;
  background: #993C1D;
  color: #ffffff;
  border: none;
  border-radius: 8px;
  font-family: var(--t-sans);
  font-size: 12.5px;
  font-weight: 600;
  text-align: center;
  margin-top: 6px;
  opacity: 0;
  transition: opacity 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

@media (max-width: 768px) {
  /* Mobile sizing for the AI demo. Shows the same 2 langs as desktop
     (Indonesia + Arab) — the JSX renders only those two, so no mobile
     hide rule is needed. Latin + Arabic RTL distinction is enough to
     communicate "AI translator across languages" without crowding. */
  .ai-demo-container {
    padding: 12px;
    max-width: 100%;
  }
  .ai-input-bubble {
    font-size: 12px;
    padding: 9px 11px;
    margin-bottom: 8px;
    min-height: 36px;
  }
  .ai-header {
    margin-bottom: 8px;
  }
  .ai-thinking {
    padding: 4px 4px 6px;
  }
  .ai-lang-card {
    padding: 6px 8px;
    margin-bottom: 4px;
  }
  .ai-lang-meta {
    margin-bottom: 2px;
  }
  .ai-flag { font-size: 12px; }
  .ai-lang-name { font-size: 11px; }
  .ai-lang-output {
    font-size: 10.5px;
    line-height: 1.4;
    min-height: 14px;
  }
  .ai-cta-btn {
    padding: 8px 12px;
    font-size: 11.5px;
    margin-top: 4px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ai-cursor { animation: none; opacity: 1; }
  .ai-dot-pulse { animation: none; opacity: 0.7; }
  /* Note: typewriter loop itself still runs but at instant speed
     would feel jumpy; the React component checks reduced-motion
     internally is out of scope here — cursor + dots stop at least. */
}

/* Scroll-margin-top on #features so anchor links + scrollIntoView
   leave clearance for the fixed 48-64px navbar. Belt-and-suspenders:
   apply same offset to #showcase and #pricing too. */
#features,
#showcase,
#pricing,
#faq,
#rsvp {
  scroll-margin-top: 80px;
}

/* Feature card illustration container — standardized height across
   all 7 highlights so the layout reads as a single rhythmic series
   rather than mismatched columns. Desktop uses 380px (vs the prior
   60vh which was tall on big screens); tablet 260px; phone 220px.
   Overflow: hidden clips any visual that exceeds the box — visuals
   should be designed to fit within these bounds. */
.feat-card-visual {
  height: 380px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Hero on mobile is now content-driven (JSX sets `minHeight: auto` +
   padding 100px/60px for breathing room). No CSS cap needed — the
   prior `max-height: 100dvh; overflow: hidden` fought with the
   content-driven height and could clip CTAs on tall content. */

/* ─── Hero photo backdrop ───────────────────────────────────────────
   Two <picture> elements (one per theme) sit at z:0 absolute-filled
   inside section#top. Theme-aware visibility via [data-theme] on
   <html>. A gradient overlay (z:1) keeps headline area legible while
   letting the photo bloom at the bottom of the hero. */
.hero-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* contain image rendering to this layer so any sub-pixel rounding
     doesn't bleed onto the overlay/content */
  overflow: hidden;
}
.hero-bg-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Mobile default: 55% lifts the envelope + wax-seal composition
     into the visible viewport behind the hero text (was 65% which
     pushed the subject too far down and made it crop under the
     trust badge). Desktop override at @media min-width: 769px. */
  object-position: center 55%;
  display: block;
}
/* Default page boot has data-theme="dark" — light backdrop hidden
   until user toggles. Both pictures stay in the DOM so theme swap is
   instant; the lazy `loading` on light keeps its bytes off the LCP
   path on first paint. */
.hero-bg--light { display: none; }
[data-theme="light"] .hero-bg--dark { display: none; }
[data-theme="light"] .hero-bg--light { display: block; }

.hero-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* Mobile dark — Apple-class composition tightened: solid hugs the
     content stack (0-60%), fast transition 60-72%, then bottom 28%
     fully transparent so the envelope + wax-seal photography reads
     at full opacity. Prior gradient (0.80 → 0.25 → 0.05 across
     65-100%) kept the photo washed out — user feedback: "amplopnya
     jadi kurang terlihat". The trust badge sits in the 70-80% zone
     under low overlay (~0.10-0.30) and stays readable via the
     text-shadow rules below. */
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 1) 0%,
    rgba(0, 0, 0, 1) 60%,
    rgba(0, 0, 0, 0.30) 72%,
    rgba(0, 0, 0, 0) 85%,
    rgba(0, 0, 0, 0) 100%
  );
}
[data-theme="light"] .hero-overlay {
  /* Cream/ivory matches --paper at data-theme=light (#fbfaf7). Same
     composition as dark — solid 0-60%, fast transition 60-72%, then
     bottom 28% fully transparent so the cream envelope, wax-seal,
     marble surface, and baby's-breath sprigs all read at full
     fidelity. */
  background: linear-gradient(
    to bottom,
    rgba(251, 250, 247, 1) 0%,
    rgba(251, 250, 247, 1) 60%,
    rgba(251, 250, 247, 0.30) 72%,
    rgba(251, 250, 247, 0) 85%,
    rgba(251, 250, 247, 0) 100%
  );
}

/* Desktop — photo composition is landscape, subject sits slightly
   higher so object-position moves up. Overlay also tightens its
   solid band to keep the upper third anchored for the centered
   headline (vs mobile's flex-start layout). */
@media (min-width: 769px) {
  .hero-bg-img {
    object-position: center 60%;
  }
  .hero-overlay {
    background: linear-gradient(
      to bottom,
      #000000 0%,
      #000000 20%,
      rgba(0, 0, 0, 0.8) 50%,
      rgba(0, 0, 0, 0.4) 80%,
      rgba(0, 0, 0, 0.5) 100%
    );
  }
  [data-theme="light"] .hero-overlay {
    background: linear-gradient(
      to bottom,
      rgba(251, 250, 247, 0.95) 0%,
      rgba(251, 250, 247, 0.85) 30%,
      rgba(251, 250, 247, 0.5) 60%,
      rgba(251, 250, 247, 0.2) 85%,
      rgba(251, 250, 247, 0.05) 100%
    );
  }
}

/* Hero text legibility on mobile — the overlay was opened up
   substantially (default rule above) so the photo could breathe
   through. Headline/eyebrow/lede/trust now sit on top of a
   semi-transparent gradient; a subtle text-shadow brings contrast
   back to a comfortable reading range without recoating the photo
   in opacity. Buttons are skipped — they have their own opaque
   backgrounds and the shadow would muddy their edge. */
@media (max-width: 768px) {
  #top .eyebrow-accent,
  #top .h-hero,
  #top .hero-stack-item,
  #top .lede,
  #top .reveal p {
    text-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
  }
  [data-theme="light"] #top .eyebrow-accent,
  [data-theme="light"] #top .h-hero,
  [data-theme="light"] #top .hero-stack-item,
  [data-theme="light"] #top .lede,
  [data-theme="light"] #top .reveal p {
    text-shadow: 0 1px 15px rgba(255, 255, 255, 0.6);
  }
}

/* Reduced-motion respect: photo backdrop is static (no animation),
   so nothing to disable here — kept as a documentation anchor for
   any future entrance animation that may be added. */

/* Showcase frame centering safety (mobile): the morph's fitScale
   transform with `transform-origin: center center` already centers
   the device, and the grid parent has `justify-content: center`.
   These extra rules are defensive — they enforce the same intent
   in case any inline transform or position drift creeps back. */
@media (max-width: 768px) {
  #showcase {
    text-align: center;
  }
  #showcase .device-frame,
  #showcase [class*="phoneFrame"],
  #showcase [class*="deviceFrame"] {
    margin-left: auto !important;
    margin-right: auto !important;
  }
  /* Outro slot text + CTA defensive centering. Inline styles on the
     outro wrapper already set alignItems:center + textAlign:center,
     but the .lede class ships maxWidth: 32ch which on 360-414px
     viewports leaves the paragraph at ~320px — narrower than the
     viewport but the auto margin only centers if the parent flex
     allows shrinking. These rules pin the centering at the CSS
     layer so any future inline-style refactor doesn't regress it. */
  #showcase .lede {
    max-width: 100%;
    margin-left: auto !important;
    margin-right: auto !important;
    padding-left: 8px;
    padding-right: 8px;
    overflow-wrap: anywhere;
    /* Allow the paragraph to break long words rather than overflow
       the right edge (the "Tampilan sama..." line was cut on 360px). */
    word-break: normal;
  }
  #showcase .showcase-cta-group {
    justify-content: center !important;
    flex-wrap: wrap;
  }
  /* Tab strip — multi-purpose mobile fix.

     Centering: already in a flex parent with alignItems:center, but
     the strip itself is content-width. Margin auto guarantees the
     strip stays centered if a future render shifts the parent flex.

     Wrap + compact padding (THIS is the actual fix for the long-running
     "chassis off-center on mobile" bug): the strip is `display: flex,
     gap: 4, padding: 4` with `flex-wrap: nowrap` default. With 3 tabs
     (Eclipse/Lentera/Bahari ~115px each) + a "+7 lainnya" chip,
     min-content of the strip = ~392px. The outro slot wraps it with
     `padding: 0 24px` → outro min-content = 48 + 392 = 440px.

     Outro is row 3 of the sticky container's grid (display: grid,
     gridTemplateRows: ..., implicit single auto column). Auto column
     sizes to max-content of widest item. So outro's 442 expanded the
     grid column to 442, making ALL three grid rows (header, stage,
     outro) 442 wide — wider than the 414 viewport. stageRef centered
     in the 442 stage slot → offsetLeft (442-260)/2 = 91 → after scale
     the chassis visually sat 14px right of viewport center.

     `flex-wrap: wrap` drops the strip's min-content to single-button
     width (~115px). Compact button padding (14px horizontal vs 20
     desktop) keeps the strip on ONE row for the common case at
     360-414px viewport, so the pill aesthetic is preserved. Wrap is
     fallback for narrower viewports or future label changes. */
  #showcase [role="tablist"] {
    margin-left: auto;
    margin-right: auto;
    flex-wrap: wrap !important;
    justify-content: center !important;
    max-width: 100%;
  }
  #showcase [role="tablist"] > button {
    padding: 6px 14px !important;
  }
  #showcase .showcase-more-chip {
    padding: 4px 10px !important;
  }
}

/* Apple highlights pattern (mobile): all feature card visuals share
   the same fixed bounding box so every card in the carousel has the
   same total height. Bumped 280→320 so Tanda Kasih (3 wishlist
   cards), Check-in (3 method tiles + 3 guests), and Analytics
   (summary + 4 funnel rows + device pill) fit without clipping at
   top/bottom. AI now also fits all 4 language cards in compact form
   (see mobile rules above) — no internal scroll, no hidden langs. */
@media (max-width: 768px) {
  .feat-card-visual {
    height: 320px;
    max-height: 320px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* AI mobile previously needed overflow-y: auto because cards 3-4
     were hidden but the residual stack still overflowed 280px. With
     the 320 height + compact lang-card sizing above, all 4 fit
     without scroll — removed the auto override to keep the same
     centered layout as the other visuals. */

  /* Tanda Kasih (gift): hide the 3rd wishlist card on mobile —
     Amplop Digital + Stand Mixer reads as the full feature story
     ("emerald + digital methods"); Rice Cooker was visual filler
     that pushed the illustration past the 320px window. Visible at
     desktop; hidden only at ≤768. */
  [data-anim="gift"] .feat-anim-step-2 {
    display: none !important;
  }
}
@media (max-width: 480px) {
  /* Small mobile gets a slightly smaller visual region so total card
     height stays under viewport on iPhone SE / Samsung A24. */
  .feat-card-visual {
    height: 300px;
    max-height: 300px;
  }
}

/* Mobile carousel cards: fixed total height so the swipe reads as a
   uniform grid. Text block ~140-160px + visual region (320/300) =
   consistent total per card. Bumped 460→500 / 420→460 to match the
   new visual heights. */
@media (max-width: 768px) {
  .feat-card-mobile {
    height: 500px;
  }
}
@media (max-width: 480px) {
  .feat-card-mobile {
    height: 460px;
  }
}

/* ─── Feature card scroll-triggered animations ─────────────────
   Each FeatureVisual wraps with `data-visible` (toggled true by
   IntersectionObserver in bundle-v2.jsx at threshold 0.3). CSS
   animations gate on that attribute. Items start invisible
   (opacity 0, translateY 14px) and reveal on staggered delay. */

[data-anim] .feat-anim-item {
  opacity: 0;
  transform: translateY(24px);
}
[data-anim][data-visible="true"] .feat-anim-item {
  animation: featReveal 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
}
[data-anim][data-visible="true"] .feat-anim-step-0 { animation-delay: 0.05s; }
[data-anim][data-visible="true"] .feat-anim-step-1 { animation-delay: 0.20s; }
[data-anim][data-visible="true"] .feat-anim-step-2 { animation-delay: 0.35s; }
[data-anim][data-visible="true"] .feat-anim-step-3 { animation-delay: 0.50s; }
[data-anim][data-visible="true"] .feat-anim-step-4 { animation-delay: 0.65s; }
[data-anim][data-visible="true"] .feat-anim-step-5 { animation-delay: 0.80s; }
[data-anim][data-visible="true"] .feat-anim-step-6 { animation-delay: 0.95s; }

@keyframes featReveal {
  to { opacity: 1; transform: translateY(0); }
}

/* 02 EDIT — "Mengedit ✓" badge pulses + caret cursor blinks so the
   illustration reads as "live edit happening right now". User
   feedback: the prior scale(1.06) pulse was too subtle on the 7px
   badge. Bumped to scale(1.18) + wider opacity swing for clear
   "breathing" feel. Cursor blink is added on top — the vertical
   caret inside the gold-tinted edit field now blinks at typewriter
   speed (step-end 0.9s) like a real text input. */
[data-anim="edit"][data-visible="true"] .feat-edit-pulse {
  animation: featEditPulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) 0.6s infinite;
}
@keyframes featEditPulse {
  0%, 100% { transform: scale(1);    opacity: 1;    }
  50%      { transform: scale(1.18); opacity: 0.78; }
}

[data-anim="edit"][data-visible="true"] .feat-edit-cursor {
  animation: featEditCursor 0.9s step-end infinite;
}
@keyframes featEditCursor {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0; }
}

/* 03 COLLAB — 3 cursors continuously drift around the document.
   Different paths + durations make the motion feel ambient, not
   choreographed. Animation starts on visibility. */
[data-anim="collab"] .feat-collab-cursor {
  transform-origin: top left;
  animation-play-state: paused;
}
[data-anim="collab"][data-visible="true"] .feat-collab-cursor--hawa {
  animation: featCursorHawa 5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  animation-play-state: running;
}
[data-anim="collab"][data-visible="true"] .feat-collab-cursor--adam {
  animation: featCursorAdam 6s cubic-bezier(0.4, 0, 0.6, 1) 0.5s infinite;
  animation-play-state: running;
}
[data-anim="collab"][data-visible="true"] .feat-collab-cursor--wo {
  animation: featCursorWO 5.5s cubic-bezier(0.4, 0, 0.6, 1) 1s infinite;
  animation-play-state: running;
}
@keyframes featCursorHawa {
  0%, 100% { transform: translate(0, 0); }
  35%      { transform: translate(-44px, 30px); }
  70%      { transform: translate(-16px, 56px); }
}
@keyframes featCursorAdam {
  0%, 100% { transform: translate(0, 0); }
  40%      { transform: translate(50px, -32px); }
  75%      { transform: translate(22px, -52px); }
}
@keyframes featCursorWO {
  0%, 100% { transform: translate(0, 0); }
  30%      { transform: translate(-52px, -20px); }
  65%      { transform: translate(-28px, -46px); }
}

/* 05 GIFT — progress bars fill from 0 to their target percentage.
   Inline width on the element is the target (72% / 40%). The
   animation goes from transform: scaleX(0) → scaleX(1), so the
   final rendered width = inline width × scale 1 = target %.

   Why scaleX instead of animating `width`: animating to
   `var(--bar-pct)` inside a keyframe was unreliable — Safari/iOS
   inconsistencies left bars at width 0 in the carousel context
   even when data-visible flipped to true. ScaleX is a transform
   (GPU-accelerated, no layout), works identically across browsers,
   and the bar's container has overflow:hidden so partial scales
   never visually leak. */
[data-anim="gift"] .feat-gift-bar {
  transform: scaleX(0);
  transform-origin: left center;
}
[data-anim="gift"][data-visible="true"] .feat-gift-bar {
  animation: featGiftFill 1.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.6s forwards;
}
@keyframes featGiftFill {
  to { transform: scaleX(1); }
}

/* 06 CHECK-IN — "Belum" badge pulses gently as if waiting to scan */
[data-anim="checkin"][data-visible="true"] .feat-checkin-pulse {
  animation: featCheckinPulse 2.2s cubic-bezier(0.45, 0, 0.55, 1) 0.8s infinite;
}
@keyframes featCheckinPulse {
  0%, 100% { opacity: 1;   }
  50%      { opacity: 0.55; }
}

/* 07 ANALYTICS — bars grow from 0 to their target with stagger.
   Same scaleX approach as gift bars above — inline width is the
   target percentage, transform animates the visual reveal. */
[data-anim="analytics"] .feat-analytics-bar {
  transform: scaleX(0);
  transform-origin: left center;
}
[data-anim="analytics"][data-visible="true"] .feat-analytics-bar {
  animation: featAnalyticsBar 1.1s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
}
[data-anim="analytics"][data-visible="true"] .feat-anim-step-0 .feat-analytics-bar { animation-delay: 0.35s; }
[data-anim="analytics"][data-visible="true"] .feat-anim-step-1 .feat-analytics-bar { animation-delay: 0.55s; }
[data-anim="analytics"][data-visible="true"] .feat-anim-step-2 .feat-analytics-bar { animation-delay: 0.75s; }
[data-anim="analytics"][data-visible="true"] .feat-anim-step-3 .feat-analytics-bar { animation-delay: 0.95s; }
@keyframes featAnalyticsBar {
  to { transform: scaleX(1); }
}

/* Reduced-motion: skip all transforms + heavy looped animations.
   Items remain visible (no reveal animation), bars instantly at full
   width, cursors stay put. Static fallback that preserves info. */
@media (prefers-reduced-motion: reduce) {
  [data-anim] .feat-anim-item,
  [data-anim][data-visible="true"] .feat-anim-item {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
  [data-anim="collab"][data-visible="true"] .feat-collab-cursor--hawa,
  [data-anim="collab"][data-visible="true"] .feat-collab-cursor--adam,
  [data-anim="collab"][data-visible="true"] .feat-collab-cursor--wo,
  [data-anim="edit"][data-visible="true"] .feat-edit-pulse,
  [data-anim="edit"][data-visible="true"] .feat-edit-cursor,
  [data-anim="checkin"][data-visible="true"] .feat-checkin-pulse {
    animation: none !important;
  }
  [data-anim="gift"] .feat-gift-bar,
  [data-anim="analytics"] .feat-analytics-bar {
    width: var(--bar-pct) !important;
    animation: none !important;
  }
}

/* color story */
.cstory-card {
  position: relative;
  width: 100%;
  aspect-ratio: 9 / 13;
  border-radius: 22px;
  overflow: hidden;
  box-shadow: var(--shadow-hero);
  transition: transform .6s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.cstory-pip {
  width: 28px; height: 28px; border-radius: 999px;
  border: 1px solid var(--hair);
  cursor: pointer;
  touch-action: manipulation;
  transition:
    transform .2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    box-shadow .2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  position: relative;
}
.cstory-pip[aria-pressed="true"] {
  transform: scale(1.18);
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px var(--ink);
}
.cstory-pip:hover { transform: scale(1.12); }

/* spec stat */
.stat-num {
  font-family: var(--t-sans);
  font-size: clamp(72px, 11vw, 144px);
  line-height: 0.95;
  letter-spacing: -0.05em;
  font-weight: 600;
  color: var(--ink);
}

/* .theme-toggle block removed — the FAB was moved into Nav (see
   bundle-v2.jsx Nav component, theme toggle button). The old CSS
   class was unused and sat at z:90 conflicting with the WhatsApp FAB.
   If you need a standalone theme toggle in the future, prefer a
   namespaced class (e.g. .uwu-fab-theme) to avoid latent conflicts. */
