:root{
  --bg: #08090a;
  --bg-2: #0c0d0f;
  --panel: #121314;
  --panel-2: #17191a;
  --panel-border: #242628;
  --off-cell: #17191a;
  --off-cell-border: #232527;
  --text: #eaf2ec;
  --muted: #8a8a8a;
  --muted-2: #55575a;
  --blue: var(--accent);
  --danger: #ff6b5e;
  --amber: #e0a850;
  /* the site's single accent color — everything that should react to the
     theme swatches in #personalize reads from these two, never --blue
     directly. --accent-rgb is the raw triplet so alpha-blended uses
     (rgba(var(--accent-rgb), .2) etc.) can react too, not just solid fills. */
  --accent-rgb: 82,156,202;
  --accent: rgb(var(--accent-rgb));
  --radius-lg: 20px;
  --radius-md: 12px;
  --radius-sm: 8px;
  /* Fixed 1180px left large empty side margins on anything wider than a
     regular laptop screen — min() scales this with the viewport instead
     (90% of it) up to a 1600px ceiling, so a big external monitor
     actually gets a fuller layout instead of the same 1180px column
     just floating in more empty space. No separate mobile override
     needed: min() already shrinks naturally on narrow viewports the
     same way the old fixed value did (every section's own side padding,
     e.g. `padding:0 24px`, still applies underneath this). */
  --maxw: min(1600px, 90vw);
  --ease: cubic-bezier(.16,.8,.24,1);
}

*{ box-sizing:border-box; }
html{ scroll-behavior:smooth; }
html,body{
  margin:0;
  background: var(--bg);
  color: var(--text);
  font-family:'Inter', ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}
img{ max-width:100%; display:block; }
a{ color:inherit; }
h1,h2,h3,p{ margin:0; }
code{ font-family: ui-monospace, 'SF Mono', Menlo, monospace; background:var(--panel-2); border:1px solid var(--panel-border); border-radius:5px; padding:1px 6px; font-size:0.9em; }

::selection{ background: rgba(var(--accent-rgb),0.35); color:#fff; }

/* ---------- background glow ---------- */
.bg-glow{
  position:fixed; inset:0; z-index:-1; pointer-events:none;
  background:
    radial-gradient(560px 420px at 18% -6%, rgba(var(--accent-rgb),0.16), transparent 60%),
    radial-gradient(640px 480px at 88% 8%, rgba(199,125,210,0.10), transparent 60%),
    var(--bg);
}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  font-family:inherit; font-weight:600; font-size:15px;
  padding:13px 22px; border-radius:999px;
  text-decoration:none; cursor:pointer; border:1px solid transparent;
  transition: transform .18s var(--ease), box-shadow .18s var(--ease), opacity .18s var(--ease), background .18s var(--ease);
  white-space:nowrap;
}
.btn:active{ transform: translateY(1px) scale(0.99); }
.btn-primary{
  background: linear-gradient(180deg,#f5f7f6,#dfe6e2);
  color:#0a0a0a;
  --shiny-base:#0a0a0a; /* .shiny-text can't use currentColor (see its own comment) — this button's real text color, for any .shiny-text nested inside */
  box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset, 0 10px 30px -12px rgba(var(--accent-rgb),0.35);
}
.btn-primary:hover{ transform: translateY(-1px); box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset, 0 14px 34px -10px rgba(var(--accent-rgb),0.5); }
.btn-ghost{
  background: rgba(255,255,255,0.03);
  border-color: var(--panel-border);
  color: var(--text);
}
.btn-ghost:hover{ background: rgba(255,255,255,0.06); border-color:#3a3d40; }
.btn-small{ padding:9px 16px; font-size:13.5px; }
.btn-large{ padding:17px 30px; font-size:17px; }

/* ---------- header ---------- */
.site-header{
  position:fixed; top:0; left:0; right:0; z-index:100;
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  background: rgba(8,9,10,0.55);
  border-bottom:1px solid transparent;
  transition: border-color .25s ease, background .25s ease, backdrop-filter .25s ease, box-shadow .25s ease;
}
/* ---------- glass surface (scrolled header) ----------
   Adapted from react-bits' GlassSurface (Usefulsources/react-bits-main)
   — but deliberately the plain backdrop-filter "fallback" recipe from
   its own CSS, not the SVG feDisplacementMap machinery from its JS.
   That machinery only ever activates behind the source's OWN
   supportsSVGFilters() check, which explicitly returns false for
   Safari and Firefox — and this is a Mac-focused marketing site where
   Safari is a primary expected browser, so most visitors would hit the
   fallback anyway.

   First pass's background was 0.82 opaque — nearly solid, which meant
   backdrop-filter's blur had almost nothing of the actual page showing
   through it to blur, so it read as "a dark bar" rather than "glass."
   Dropped to 0.45 so the blurred content behind it stays genuinely
   visible (which is the entire point of a glass surface), and added a
   diagonal light-sheen gradient layered on top of the tint — a subtle
   highlight catching the "top-left" the way real glass/acrylic panels
   pick up ambient light, rather than a flat tinted rectangle. */
.site-header.scrolled{
  border-bottom-color: var(--panel-border);
  background:
    linear-gradient(155deg, rgba(255,255,255,0.07), rgba(255,255,255,0.01) 40%),
    rgba(10,12,13,0.45);
  backdrop-filter: blur(22px) saturate(200%) brightness(1.15);
  -webkit-backdrop-filter: blur(22px) saturate(200%) brightness(1.15);
  box-shadow:
    inset 0 1px 0 0 rgba(255,255,255,0.14),
    inset 0 -1px 0 0 rgba(255,255,255,0.03),
    0 8px 32px rgba(0,0,0,0.3);
}
.site-header.nav-open{ background: var(--bg); backdrop-filter:none; -webkit-backdrop-filter:none; border-bottom-color: var(--panel-border); }
.header-inner{
  max-width: var(--maxw); margin:0 auto; padding:14px 24px;
  display:flex; align-items:center; gap:24px;
}
.brand{ display:flex; align-items:center; gap:9px; text-decoration:none; font-weight:800; font-size:17px; color:var(--text); margin-right:auto; }
.brand-mark{ width:26px; height:26px; border-radius:7px; }
.site-nav{ display:flex; gap:26px; }
.site-nav a{ text-decoration:none; color:var(--muted); font-size:14px; font-weight:500; transition: color .15s ease; }
.site-nav a:hover{ color: var(--text); }
.header-cta{ margin-left:6px; }

.nav-toggle{ display:none; flex-direction:column; gap:4px; width:32px; height:32px; align-items:center; justify-content:center; background:none; border:none; cursor:pointer; }
.nav-toggle span{ width:18px; height:1.5px; background:var(--text); border-radius:2px; transition: transform .2s ease, opacity .2s ease; }
.nav-toggle.open span:nth-child(1){ transform: translateY(5.5px) rotate(45deg); }
.nav-toggle.open span:nth-child(2){ opacity:0; }
.nav-toggle.open span:nth-child(3){ transform: translateY(-5.5px) rotate(-45deg); }
.mobile-nav{ display:none; flex-direction:column; gap:2px; padding:8px 24px 18px; background: var(--bg); box-shadow: 0 20px 30px -20px rgba(0,0,0,0.6); isolation:isolate; position:relative; z-index:1; }
.mobile-nav a:not(.btn){ padding:12px 4px; text-decoration:none; color:var(--text); font-size:15px; border-top:1px solid var(--panel-border); }
.mobile-nav .btn{ margin-top:10px; }

/* ---------- hero ---------- */
.hero{
  position:relative;
  z-index:0;
  max-width: var(--maxw); margin:0 auto; padding: 168px 24px 90px;
  display:grid; grid-template-columns: 1.05fr 0.95fr; gap:40px; align-items:center;
}
/* ---------- dither background (hero) ----------
   Ported from react-bits' Dither (Usefulsources/react-bits-main) — an
   animated, hand-rolled noise field rendered through an 8x8 ordered
   (Bayer) dither, quantized to a small color count for a retro/
   posterized look, reacting to the cursor. Source renders it as two
   passes (a Three.js scene, then a postprocessing.js Effect reading
   that scene's texture) purely because react-three-fiber/postprocessing
   structure everything as layered passes; this site has zero JS
   dependencies (no three, no r3f, no postprocessing), so setupDither in
   script.js is hand-rolled raw WebGL2 — one shader computing the wave
   noise AND the dither quantization together in a single pass (see the
   comment there for why that's equivalent, not just an approximation).

   z-index:-1 alone needs .hero{position:relative} to contain it, but
   position:relative WITHOUT a non-auto z-index does NOT establish a new
   stacking context — it only makes .hero a containing block for this
   canvas's position:absolute layout, so the canvas's z-index:-1 escaped
   all the way to the document root instead of staying scoped to .hero.
   At the root, it landed BEHIND .bg-glow's fixed, effectively-opaque
   background (var(--bg) as its base layer) — rendering the entire
   effect invisible even though the shader itself was drawing correctly
   the whole time (confirmed live: a positive z-index made it fully
   visible). .hero's own z-index:0 above is what actually fixes this —
   it's what makes position:relative establish a real stacking context,
   containing the canvas's negative z-index within .hero instead of
   letting it escape.

   width:100vw + left:50%/translateX(-50%) is the standard "full-bleed"
   break-out — .hero itself is a max-width, centered container (see
   .hero's own max-width/margin above), so a plain inset:0 canvas was
   confined to that same narrow centered column instead of spanning the
   full page width the user actually wanted. top/bottom still come from
   inset:0 -> 0 0 auto 0 below, so it still only covers .hero's own
   height, just no longer clipped to .hero's horizontal max-width. */
.dither-bg{
  position:absolute; top:0; bottom:0; left:50%;
  width:100vw; height:100%; transform:translateX(-50%);
  z-index:-1;
}
.eyebrow{
  display:inline-block; font-size:12.5px; font-weight:600; letter-spacing:0.02em;
  color: var(--accent); background: rgba(var(--accent-rgb),0.12); border:1px solid rgba(var(--accent-rgb),0.28);
  --shiny-base: rgb(var(--accent-rgb)); /* .shiny-text can't use currentColor (see its own comment) — this badge's real text color, for the .shiny-text nested inside */
  padding:5px 12px; border-radius:999px; margin-bottom:22px;
}
.hero h1{
  font-size: clamp(38px, 5vw, 60px); line-height:1.05; font-weight:650; letter-spacing:-0.02em;
  color: #f6f8f7;
}
/* ---------- variable proximity (hero heading) ----------
   Ported from react-bits' VariableProximity (Usefulsources/react-bits-main)
   — script.js splits the heading into these per-letter spans and animates
   each one's font-variation-settings by distance to the cursor (which,
   once JS runs, is what actually controls the rendered weight for a
   variable font — font-variation-settings wins over the plain
   font-weight above for letters JS has touched). font-weight:650 here is
   just the fallback for the moment before JS runs, kept in sync with
   fromWeight in script.js's setupVariableProximity call so there's no
   flash of a different weight once the split happens. .vp-word keeps a
   word from line-wrapping mid-letter (same reason the source groups
   letters by word instead of flattening the whole string).

   Traced a report of the AI helper heading looking uncentered/overflowing
   to a transient font-loading race, not a layout bug: index.html swaps
   in Inter's variable font asynchronously, and on a slow connection the
   fallback font's metrics can briefly make a word-span run wider before
   the swap corrects it (reproduced once with an artificially narrow
   viewport, self-resolved a moment later with no other change).
   overflow-wrap below is a defensive backstop regardless of cause — it
   only ever activates if a single word is somehow still wider than its
   container, which should never happen in steady state, but costs
   nothing to guarantee. */
.vp-word{ display:inline-block; white-space:nowrap; overflow-wrap:break-word; max-width:100%; }
.vp-letter{ display:inline-block; }
/* visually hidden but still readable by screen readers — holds the
   heading's real, unsplit text since the per-letter spans above are all
   aria-hidden (same standard pattern the source itself uses) */
.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;
}
/* ---------- scroll float (section h2 headings) ----------
   Ported from react-bits' ScrollFloat (Usefulsources/react-bits-main) —
   each letter pops up from below with a squash/stretch overshoot as the
   heading scrolls into view. Reuses the SAME .vp-letter spans
   VariableProximity already splits headings into (script.js's
   buildLetterSpans runs once per heading, shared by both effects) rather
   than re-splitting — one effect drives font-variation-settings, this
   one drives opacity/transform, so they don't touch the same CSS
   properties and can run on the same spans simultaneously. Only h2s get
   the .sf-char class (see script.js's setupScrollFloat call site) — the
   hero h1 is visible above the fold on load, so it has nothing to
   "scroll into view" from.

   --i (each letter's index, set once in JS) drives the stagger via
   calc(), not a per-letter JS timer — cubic-bezier(.34,1.56,.64,1) is a
   standard "back" overshoot approximation for GSAP's ease:'back.inOut(2)'
   without needing GSAP. Stagger 10ms / duration 1s matches the source's
   own animationDuration:1/stagger:0.03 (rounded down further) — a first
   pass at 28ms/0.7s read as a readable left-to-right letter-by-letter
   sweep instead of ScrollFloat's more unified "the whole word floats up
   together" feel.

   transform-origin:50% 0% (top-anchor) matches the source's
   transformOrigin:'50% 0%' — an earlier pass used 50% 100%
   (bottom-anchor), which is NOT an equivalent restyle: scaling is
   anchored at whichever edge transform-origin names, so a bottom anchor
   stretches the letter both upward AND downward from its resting box,
   while a top anchor only ever extends it downward, reading as "rises
   up from below" the way it should.

   translateY/scaleY/scaleX magnitudes are toned down from the source's
   own defaults (yPercent:120/scaleY:2.3/scaleX:0.7) — those numbers are
   tuned for the source demo's huge clamp(1.6rem,8vw,10rem) display type;
   copied onto this site's much smaller ~24-32px h2s, the same relative
   displacement read as an abrupt, oversized "jump" rather than a subtle
   float. Values below keep the same top-anchored "rises from below"
   character at a scale that suits body-sized headings. */
.sf-char{
  opacity:0;
  transform: translateY(45%) scaleY(1.4) scaleX(0.85);
  transform-origin: 50% 0%;
  transition: opacity 1s cubic-bezier(.34,1.56,.64,1), transform 1s cubic-bezier(.34,1.56,.64,1);
  transition-delay: calc(var(--i, 0) * 10ms);
}
.scroll-float-heading.scroll-float-in .sf-char{
  opacity:1;
  transform: translateY(0) scaleY(1) scaleX(1);
}
/* Clips letters mid-rise to the heading's own box instead of letting
   them visibly poke down into the paragraph below — matches the
   source's own .scroll-float{overflow:hidden} on the same element. */
.scroll-float-heading{ overflow:hidden; }
@media (prefers-reduced-motion: reduce){
  .sf-char{ opacity:1; transform:none; transition:none; }
}
.hero-sub{ margin-top:22px; font-size:17px; line-height:1.6; color:var(--muted); max-width:480px; }
.hero-actions{ display:flex; gap:14px; margin-top:32px; flex-wrap:wrap; }
.hero-meta{ margin-top:18px; font-size:13px; color:var(--muted-2); }

/* ---------- mock app window (hero visual) ---------- */
.hero-visual{ position:relative; }
.mock-glow{
  position:absolute; inset:-40px; z-index:-1; border-radius:40px; filter:blur(50px);
  background: radial-gradient(closest-side, rgba(var(--accent-rgb),0.28), transparent 70%);
}
.mock-window{
  background: var(--panel); border:1px solid var(--panel-border); border-radius: var(--radius-lg);
  box-shadow: 0 40px 80px -30px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.02) inset;
  overflow:hidden; transform: perspective(1400px) rotateY(-6deg) rotateX(2deg);
  animation: float 7s ease-in-out infinite;
}
/* real background is --panel, not the --panel-2 the feature cards use —
   ::before's opaque interior cover has to match it or the whole window
   visibly tints to the wrong shade (see .border-glow's --glow-cover). */
.mock-window.border-glow{ --glow-cover: var(--panel); }
@keyframes float{
  0%,100%{ transform: perspective(1400px) rotateY(-6deg) rotateX(2deg) translateY(0); }
  50%{ transform: perspective(1400px) rotateY(-6deg) rotateX(2deg) translateY(-10px); }
}
.mock-app{ display:flex; height: 420px; font-size:11px; }
.mock-sidebar{ width:150px; background: var(--bg-2); border-right:1px solid var(--panel-border); padding:14px 10px; flex-shrink:0; }
.mock-sb-brand{ font-weight:800; font-size:13px; padding:2px 6px 14px; }
.mock-sb-search{ background:var(--off-cell); border:1px solid var(--off-cell-border); border-radius:6px; padding:6px 8px; color:var(--muted-2); font-size:10.5px; margin-bottom:10px; }
.mock-sb-item{ display:flex; align-items:center; gap:7px; padding:6px 7px; border-radius:6px; color:var(--muted); font-size:11px; }
.mock-sb-item.active{ background: var(--panel-2); color:var(--text); }
.mock-sb-dot{ font-size:11px; }
.mock-main{ flex:1; display:flex; flex-direction:column; min-width:0; }
.mock-titlebar{ display:flex; gap:6px; padding:10px 14px; }
.mock-dot{ width:9px; height:9px; border-radius:50%; }
.mock-dot.red{ background:#ff5f57; } .mock-dot.yellow{ background:#febc2e; } .mock-dot.green{ background:#28c840; }
.mock-page{ padding:6px 20px 20px; flex:1; overflow:hidden; }
.mock-page-title{ font-size:19px; font-weight:800; margin-bottom:14px; }

/* heatmap (reused in hero + feature) */
.mock-heatmap{ display:grid; grid-template-columns: repeat(26, 1fr); gap:3px; margin-bottom:16px; }
.mock-heatmap span{ aspect-ratio:1; border-radius:2px; background: var(--off-cell); border:1px solid var(--off-cell-border); }
.mock-heatmap.large{ grid-template-columns: repeat(30, 1fr); gap:4px; }

.mock-row{ display:flex; gap:12px; }
.mock-card{ background: var(--panel-2); border:1px solid var(--panel-border); border-radius: var(--radius-sm); padding:12px 14px; }
.mock-checklist{ flex:1.4; }
.mock-card-title{ font-size:11.5px; font-weight:700; margin-bottom:9px; color:var(--text); }
.mock-check{ display:flex; align-items:center; gap:7px; font-size:11px; color:var(--muted); padding:3px 0; }
.mock-check .box{ width:12px; height:12px; border-radius:3px; border:1.4px solid var(--muted-2); flex-shrink:0; }
.mock-check .box.done{ background: var(--accent); border-color: var(--accent); }
.mock-ring{ flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; position:relative; }
.ring-svg{ width:56px; height:56px; }
.ring-label{ position:absolute; font-size:13px; font-weight:800; display:flex; flex-direction:column; align-items:center; }
.ring-label span{ font-size:8px; font-weight:500; color:var(--muted); text-transform:uppercase; letter-spacing:0.05em; }

/* ---------- strip ---------- */
.strip{
  max-width: var(--maxw); margin:0 auto; padding: 26px 24px 60px;
  display:flex; flex-wrap:wrap; gap:14px 34px; border-top:1px solid var(--panel-border);
}
.strip-item{ font-size:13.5px; color:var(--muted); }
.strip-item strong{ color:var(--text); }

/* ---------- features ---------- */
.features{ max-width: var(--maxw); margin:0 auto; padding: 40px 24px 30px; display:flex; flex-direction:column; gap:130px; }
.feature-row{ display:grid; grid-template-columns: 0.85fr 1.15fr; gap:56px; align-items:center; }
.feature-row.reverse{ grid-template-columns: 1.15fr 0.85fr; }
.feature-row.reverse .feature-copy{ order:2; }
.feature-row.reverse .feature-visual{ order:1; }
.feature-tag{
  display:inline-block; font-size:12px; font-weight:700; letter-spacing:0.06em; text-transform:uppercase;
  margin-bottom:14px;
}
.feature-tag.center{ display:block; text-align:center; }
/* ---------- gradient text (feature-tag labels) ----------
   Ported from react-bits' GradientText (Usefulsources/react-bits-main) —
   the label's text color sweeps through a moving linear-gradient via an
   animated background-position, clipped to the glyph shapes
   (background-clip:text on the inner .feature-tag-text span index.html
   wraps around each label's text, so the gradient clips to just the
   glyphs rather than eating .feature-tag's own layout/spacing rules).
   No Motion (the source only used it to drive a plain looping
   background-position over time — a CSS @keyframes loop does the same
   thing with zero JS). Gradient stops read rgb(var(--accent-rgb))
   directly rather than a hardcoded hex, so the shimmer re-tints along
   with the rest of the page when a theme swatch is picked.

   animation-direction:alternate (ping-ponging 0%<->100% rather than
   looping forward and snapping back to 0%) matches the source's own
   default yoyo:true — and isn't just a style choice: with
   background-size larger than the box, 0% and 100% background-position
   are NOT the same visual frame (100% shows a shifted slice of the
   gradient, not the same slice as 0%), so a one-directional `infinite`
   loop has a visible cut/snap every cycle where it jumps back to 0%.
   Reversing direction instead of resetting position means every frame
   boundary is continuous — the animation is just moving backward
   through frames it already smoothly played forward through, so there's
   nothing to snap. */
.feature-tag-text{
  background-image: linear-gradient(90deg, rgb(var(--accent-rgb)), #fff, rgb(var(--accent-rgb)));
  background-size: 300% 100%;
  background-clip: text; -webkit-background-clip: text;
  color: transparent;
  animation: feature-tag-shine 3.5s linear infinite alternate;
}
@keyframes feature-tag-shine{
  0%{ background-position: 0% 50%; }
  100%{ background-position: 100% 50%; }
}
/* ---------- shiny text (eyebrow badge + hero download button) ----------
   A different character from the gradient-text shimmer above: here the
   text stays its own normal base color almost the whole time, with just
   a narrow bright band sweeping through periodically — like light
   glinting off metal — rather than continuously cycling between two
   hues.

   First pass used currentColor for that base — reasonable-sounding
   (drops onto the eyebrow's accent text and the button's near-black
   text with no variant needed), but wrong: currentColor always resolves
   against the computed color of the SAME element it's used on, and
   this element also sets color:transparent (required for the
   background-clip:text trick). So currentColor here was resolving to
   transparent, not the visually-obvious "real" text color the element
   appears to have before this rule — the gradient's 35%-65% "base"
   stops were genuinely invisible the entire cycle except the instant
   the bright band itself passed through, i.e. real, mostly-invisible
   text, confirmed live via screenshot (the eyebrow and button text
   were unreadable). --shiny-base is set explicitly on each ANCESTOR
   instead (.eyebrow, .btn-primary) with their real color as a plain
   value (not currentColor) and inherits down normally — a custom
   property's value isn't re-evaluated against whatever element
   ultimately consumes it via var(), so this sidesteps the same trap.

   Same background-clip:text + animation-direction:alternate
   seamless-loop pattern as .feature-tag-text (see its comment for why
   alternating avoids a jump-cut every cycle) — the gradient's first and
   last stops are both --shiny-base for the same reason. */
.shiny-text{
  background-image: linear-gradient(100deg, var(--shiny-base, #fff) 35%, rgba(255,255,255,0.92) 50%, var(--shiny-base, #fff) 65%);
  background-size: 300% 100%;
  background-clip: text; -webkit-background-clip: text;
  color: transparent;
  animation: shiny-text-sweep 3.2s linear infinite alternate;
}
@keyframes shiny-text-sweep{
  0%{ background-position: 0% 50%; }
  100%{ background-position: 100% 50%; }
}
.feature-copy h2{ font-size: clamp(24px, 2.6vw, 32px); font-weight:800; letter-spacing:-0.01em; line-height:1.2; margin-bottom:16px; }
.feature-copy p{ font-size:15.5px; line-height:1.65; color:var(--muted); max-width:440px; }
.feature-visual{ position:relative; }
.feature-visual .mock-card{ padding:20px; border-radius: var(--radius-md); box-shadow: 0 30px 60px -30px rgba(0,0,0,0.6); }
/* ---------- tilted card (feature showcase mockups) ----------
   Ported from react-bits' TiltedCard (Usefulsources/react-bits-main) —
   each mockup card tilts in 3D toward the cursor and lifts slightly on
   hover. No Motion/spring physics and deliberately no CSS transition on
   transform either — script.js's setupTiltedCard drives every frame via
   the same per-frame lerp toward a target rotation/scale already
   established for ChromaGrid/ElasticSlider in this codebase (curX +=
   (target-curX)*factor), including the settle back to flat on
   mouseleave (target just becomes 0 and the same lerp eases it there).
   Layering a CSS transition on top of a value that's already being
   rewritten 60x/sec would compound into a laggy, unpredictably-delayed
   result instead of 1:1 cursor tracking — the same class of bug the
   elastic slider hit earlier from stacked transforms, so the lerp is
   left to fully own the timing here. perspective() lives inline in the
   JS-set transform rather than as a property here, so each card gets its
   own vanishing point regardless of how many share a row (two-up
   sections). Skipped the source's caption tooltip and mobile-warning
   banner — these are dense UI mockups already carrying their own
   titles, not standalone photos, so a duplicate caption would be
   redundant. */
.feature-visual .mock-card{ will-change: transform; }

/* ---------- border glow (feature showcase mockups) ----------
   Ported from react-bits' BorderGlow (Usefulsources/react-bits-main).
   Three previous attempts all leaked color into the card's middle
   because they kept reinventing the outer-glow mechanism instead of
   copying the source's actual one: a box-shadow stack, not a blurred
   gradient fill. box-shadow physically can't flood a card's interior —
   it only ever emanates a fixed distance from the element's own
   border, however far its blur-radius says, so there's no "wide patch"
   for a mask to have to contain. This pass ports the real structure:

   - ::before is the crisp colored mesh border: 7 radial-gradient
     positions (cycling through the 3 supplied colors, matching the
     source's own COLOR_MAP) clipped to border-box, with an opaque
     padding-box cover matching the card's real background sitting on
     top of them in the SAME background shorthand — background-clip
     only controls where one layer paints, so it's the opaque cover,
     not the clip alone, that actually hides the gradients across the
     content area (an earlier pass used a *transparent* cover here,
     which does nothing — that was the "color in the middle" bug).
   - ::after is a soft interior tint from the same gradients, blended
     with mix-blend-mode:soft-light (much gentler than screen/lighten)
     and masked into an organic 5-lobe shape via mask-composite —
     intentionally faint; this is what gives real BorderGlow cards a
     soft color cast near the edge without it reading as a solid blob.
   - .edge-light is the actual outer glow, and it is PURE box-shadow —
     no gradient fill at all, just inset+outset shadow layers at
     increasing blur radii, all one hue (--glow-color, from the
     separate glowColor HSL prop, not the colors[] mesh) — this is the
     layer that structurally cannot flood the middle.

   ::before/::after/.edge-light all sit at z-index:-1 (only .edge-light
   is bumped to z-index:1, matching the source exactly) — behind the
   card's own background, but ALSO behind the card's real content
   (.feature-visual .mock-card's own children), so even wherever a
   layer's mask does let color through, it can never paint over
   anything readable; it can only ever show in the gaps around it. */
.border-glow{
  /* isolation:isolate alone creates the stacking context the -1/1
     z-index pseudo-elements rely on — no transform needed (and adding
     one here would fight .mock-window's own perspective/rotate
     transform via the cascade, since both are single-class selectors). */
  position:relative; isolation:isolate;
  --edge-proximity: 0; --cursor-angle: 45deg;
  --edge-sensitivity: 30; --color-sensitivity: calc(var(--edge-sensitivity) + 20);
  --glow-padding: 28px; --cone-spread: 25;
  --glow-c1: #c084fc; --glow-c2: #f472b6; --glow-c3: #38bdf8;
  /* ::before's opaque padding-box cover has to match this element's OWN
     real background exactly (--panel-2 for the feature-showcase cards)
     or the interior visibly tints to whatever the cover color is —
     .mock-window sets this to --panel instead, since that's its real
     background. */
  --glow-cover: var(--panel-2);
}
.border-glow::before,
.border-glow::after,
.border-glow > .edge-light{
  content:''; position:absolute; inset:0; border-radius:inherit;
  pointer-events:none; z-index:-1;
}
.border-glow::before{
  border:1px solid transparent;
  background:
    linear-gradient(var(--glow-cover) 0 100%) padding-box,
    radial-gradient(at 80% 55%, var(--glow-c1) 0, transparent 50%) border-box,
    radial-gradient(at 69% 34%, var(--glow-c2) 0, transparent 50%) border-box,
    radial-gradient(at 8% 6%, var(--glow-c3) 0, transparent 50%) border-box,
    radial-gradient(at 41% 38%, var(--glow-c1) 0, transparent 50%) border-box,
    radial-gradient(at 86% 85%, var(--glow-c2) 0, transparent 50%) border-box,
    radial-gradient(at 82% 18%, var(--glow-c3) 0, transparent 50%) border-box,
    radial-gradient(at 51% 4%, var(--glow-c2) 0, transparent 50%) border-box;
  mask-image: conic-gradient(from var(--cursor-angle) at center, black calc(var(--cone-spread) * 1%), transparent calc((var(--cone-spread) + 15) * 1%), transparent calc((100 - var(--cone-spread) - 15) * 1%), black calc((100 - var(--cone-spread)) * 1%));
  -webkit-mask-image: conic-gradient(from var(--cursor-angle) at center, black calc(var(--cone-spread) * 1%), transparent calc((var(--cone-spread) + 15) * 1%), transparent calc((100 - var(--cone-spread) - 15) * 1%), black calc((100 - var(--cone-spread)) * 1%));
  opacity: calc((var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));
}
.border-glow::after{
  background:
    radial-gradient(at 80% 55%, var(--glow-c1) 0, transparent 50%) padding-box,
    radial-gradient(at 69% 34%, var(--glow-c2) 0, transparent 50%) padding-box,
    radial-gradient(at 8% 6%, var(--glow-c3) 0, transparent 50%) padding-box,
    radial-gradient(at 41% 38%, var(--glow-c1) 0, transparent 50%) padding-box,
    radial-gradient(at 86% 85%, var(--glow-c2) 0, transparent 50%) padding-box,
    radial-gradient(at 82% 18%, var(--glow-c3) 0, transparent 50%) padding-box,
    radial-gradient(at 51% 4%, var(--glow-c2) 0, transparent 50%) padding-box;
  mask-image:
    linear-gradient(to bottom, black, black),
    radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%),
    radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%),
    radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%),
    radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%),
    radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%),
    conic-gradient(from var(--cursor-angle) at center, transparent 5%, black 15%, black 85%, transparent 95%);
  mask-composite: subtract, add, add, add, add, add;
  opacity: calc(0.5 * (var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));
  mix-blend-mode: soft-light;
}
.border-glow > .edge-light{
  inset: calc(var(--glow-padding) * -1);
  z-index:1;
  mask-image: conic-gradient(from var(--cursor-angle) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%);
  -webkit-mask-image: conic-gradient(from var(--cursor-angle) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%);
  opacity: calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity)));
  mix-blend-mode: plus-lighter;
}
.border-glow > .edge-light::before{
  content:''; position:absolute; inset: var(--glow-padding); border-radius:inherit;
  box-shadow:
    inset 0 0 0 1px var(--glow-color, hsl(40deg 80% 80% / 100%)),
    inset 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),
    inset 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),
    inset 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),
    inset 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),
    inset 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),
    inset 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%)),
    0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),
    0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),
    0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),
    0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),
    0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),
    0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%));
}

/* block menu mock */
.mock-blockmenu{ max-width:300px; margin:0 auto; }
.mock-blockmenu-input{ font-size:14px; color:var(--muted); margin-bottom:10px; }
.mock-blockmenu-input .caret{ display:inline-block; width:1.5px; height:13px; background:var(--text); vertical-align:middle; animation: blink 1s step-end infinite; }
@keyframes blink{ 50%{ opacity:0; } }
.mock-blockmenu-row{ display:flex; align-items:center; gap:10px; padding:8px 9px; border-radius:7px; font-size:13px; color:var(--muted); }
.mock-blockmenu-row.hi{ background: rgba(var(--accent-rgb),0.12); color:var(--text); }
.mock-blockmenu-row span{ font-size:14px; }

/* heatmap feature card */
.mock-heatmap-card{ max-width:460px; margin:0 auto; }

/* board + calendar */
.two-up{ display:flex; gap:16px; }
.two-up .mock-card{ flex:1; }
.mock-board{ display:flex; gap:10px; padding:16px !important; }
.board-col{ flex:1; min-width:0; }
.board-col-title{ font-size:10.5px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:0.04em; margin-bottom:8px; }
.board-card{ background: var(--off-cell); border:1px solid var(--off-cell-border); border-radius:6px; padding:8px 9px; font-size:11px; margin-bottom:7px; color:var(--text); }
.board-card.accent{ border-color: rgba(var(--accent-rgb),0.4); box-shadow: 0 0 0 1px rgba(var(--accent-rgb),0.15) inset; }
.board-card.muted{ color:var(--muted); text-decoration:line-through; text-decoration-color:var(--muted-2); }
.mock-calendar{ display:flex; align-items:center; }
.cal-grid{ display:grid; grid-template-columns: repeat(7, 1fr); gap:6px; width:100%; }
.cal-cell{ aspect-ratio:1; display:flex; align-items:center; justify-content:center; font-size:10.5px; border-radius:6px; background:var(--off-cell); border:1px solid var(--off-cell-border); color:var(--muted); position:relative; }
.cal-cell.dim{ opacity:0.35; }
.cal-cell.today{ background:var(--accent); border-color:var(--accent); color:#04121b; font-weight:700; }
.cal-cell.has-dot::after{ content:''; position:absolute; bottom:3px; width:3px; height:3px; border-radius:50%; background:var(--amber); }

/* recipe */
.mock-recipe{ max-width:340px; margin:0 auto; padding:0 !important; overflow:hidden; }
.recipe-photo{ height:130px; background: linear-gradient(135deg, #2c4a5c, #17191a 70%); }
.recipe-body{ padding:16px; }
.macro-row{ display:flex; flex-direction:column; gap:8px; margin:12px 0 14px; }
.macro{ font-size:10.5px; color:var(--muted); display:flex; flex-direction:column; gap:4px; }
.macro-bar{ display:block; height:5px; border-radius:4px; background: var(--off-cell); position:relative; overflow:hidden; }
.macro-bar::after{ content:''; position:absolute; inset:0; width:var(--w); background:var(--c); border-radius:4px; transform-origin:left; transform:scaleX(0); transition: transform 1s var(--ease); }
.in-view .macro-bar::after{ transform:scaleX(1); }
/* messages */
.mock-messages{ max-width:340px; margin:0 auto; display:flex; flex-direction:column; gap:9px; }
.msg-row{ display:flex; align-items:flex-end; gap:7px; }
.msg-row.me{ justify-content:flex-end; }
.msg-bubble{ background:var(--off-cell); border:1px solid var(--off-cell-border); border-radius:14px; padding:9px 13px; font-size:12.5px; max-width:80%; line-height:1.4; }
.msg-bubble.accent{ background: rgba(var(--accent-rgb),0.16); border-color: rgba(var(--accent-rgb),0.35); }
/* profile icons — the actual illustrated PNGs from site/images/
   profileicons/ (messaging.js's real AVATAR_ICON_DEFS set, e.g.
   profileicon18.png="panda", profileicon21.png="redfox"), copied and
   downscaled into website/assets/profileicons/ for this mock. Rendered
   at their native transparent background with no circular frame,
   matching the real app's AVATAR_BORDERLESS:true style — not the
   invented lettered/emoji badges from earlier passes. Both
   participants get one now (not just "them") so the two chosen icons
   both actually show up in the mock. */
.msg-avatar{ width:26px; height:26px; flex-shrink:0; object-fit:contain; }

/* end-to-end encryption */
.mock-encryption{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:14px; padding:46px 24px; text-align:center;
}
.enc-lock-wrap{ position:relative; width:72px; height:72px; display:flex; align-items:center; justify-content:center; }
.enc-lock-glow{
  position:absolute; inset:-16px; border-radius:50%;
  background: radial-gradient(closest-side, rgba(127,191,127,0.35), transparent 70%);
  animation: enc-pulse 2.6s ease-in-out infinite;
}
@keyframes enc-pulse{
  0%,100%{ opacity:0.5; transform:scale(0.9); }
  50%{ opacity:1; transform:scale(1.08); }
}
.enc-lock{ font-size:34px; position:relative; z-index:1; }
.enc-label{ font-weight:700; font-size:14px; }
.enc-sub{ font-size:12.5px; color:var(--muted); max-width:280px; line-height:1.5; margin:0; }

/* ---------- AI helper (Strands) ----------
   No boxed mock-card here — the strands render directly as an ambient
   background behind the row, the same relationship Dither has to the
   hero (see its comment for the z-index:-1 + position:relative/
   z-index:0 containment pattern; same reasoning applies here — .ai-row
   needs a real z-index of its own, not just position:relative, or the
   canvas's negative z-index escapes past it instead of staying scoped
   locally). Overrides .feature-row's normal 2-column grid with a single
   centered column, since there's no separate "visual" slot anymore —
   the background *is* the visual.

   The canvas is bottom-anchored to its own 300px band (not the row's
   full inset:0 height) rather than sitting directly behind the text —
   first pass had the strands' glow crossing right through the heading,
   which looked messy rather than "background." Generous padding-bottom
   on the row reserves the empty space beneath the text for that band
   to occupy, so the glow reads as a wash under/around the copy instead
   of through it. mask-image adds a soft elliptical fade on all four
   edges of that band — the shader's own taper already fades left/right,
   but had no vertical falloff at all, so close to the row's fixed
   height the glow could end in a visible hard rectangular edge instead
   of dissolving into the page; this guarantees a seamless blend
   regardless of the shader's own tuning. */
.feature-row.ai-row{
  position:relative; z-index:0;
  display:block;
  padding:56px 40px 260px;
}
.feature-row.ai-row .feature-copy{ max-width:560px; margin:0 auto; text-align:center; position:relative; z-index:1; }
/* The real, reproducible cause of the "text isn't centered" report (an
   earlier pass wrongly blamed a one-off font-loading race): .feature-copy
   p already carries its own max-width:440px, sized for the normal
   LEFT-aligned feature-rows. That's narrower than this row's 560px
   .feature-copy, and a block element with no auto margins just sits at
   its container's left edge regardless of text-align (text-align only
   centers inline content INSIDE a box, not the block box itself) — the
   paragraph was landing 60px left of the tag/heading above it, exactly
   (560-440)/2. Confirmed by comparing getBoundingClientRect() centers:
   the tag/h2 both centered at x=712.5, the p at 652.5. */
.feature-row.ai-row .feature-copy p{ max-width:none; margin:0 auto; }
.strands-bg{
  position:absolute; left:0; right:0; bottom:0; top:auto;
  height:320px; width:100%;
  z-index:-1;
  /* The ellipse's own radius/stops need to reach fully transparent well
     BEFORE this band's bottom edge, or the canvas's hard rectangular
     boundary shows through as a visible seam right where it meets
     whatever comes after it — the original 75%-radius/82%-stop
     combination didn't clear zero until y≈107% (past the box entirely),
     so the real bottom edge at y=100% was still partway-opaque, i.e. a
     hard cutoff. A first attempt fixed that by shrinking the radius
     (46%) to force zero-opacity comfortably before the edge, but that
     compressed the whole black→transparent ramp into a much narrower
     band (~70px) — the strands shader's own wave animates its peak
     height over time, and moving through that narrow, steep ramp reads
     as an abrupt cut rather than a fade. This version keeps the ramp
     WIDE (radius 100%, stops 3%–35%, ~100px transition) for a genuinely
     gradual fade in both directions, while moving the center down to
     55% so the transparent stop is still reached at y≈90% — comfortably
     inside the box, same guarantee as before, just with room to breathe. */
  mask-image: radial-gradient(ellipse 62% 100% at 50% 55%, black 3%, transparent 35%);
  -webkit-mask-image: radial-gradient(ellipse 62% 100% at 50% 55%, black 3%, transparent 35%);
}

/* trend chart */
.mock-trendchart{ max-width:380px; margin:0 auto; }
.mock-trendchart .mock-card-title{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
.trend-badge{ font-size:10.5px; font-weight:600; color:#7fbf7f; background:rgba(127,191,127,0.12); border:1px solid rgba(127,191,127,0.3); border-radius:999px; padding:3px 8px; }
.trend-svg{ width:100%; height:100px; overflow:visible; }
.trend-area{ fill:url(#trendGrad); opacity:0; transition:opacity .8s var(--ease) .2s; }
.in-view .trend-area{ opacity:1; }
.trend-line{ fill:none; stroke:var(--accent); stroke-width:2; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:400; stroke-dashoffset:400; transition: stroke-dashoffset 1.1s var(--ease); }
.in-view .trend-line{ stroke-dashoffset:0; }
.trend-dot{ fill:var(--panel-2); stroke:var(--accent); stroke-width:1.6; opacity:0; transition:opacity .3s ease .9s; }
.in-view .trend-dot{ opacity:1; }
.trend-axis{ display:flex; justify-content:space-between; font-size:10.5px; color:var(--muted-2); margin-top:4px; }

/* recipe finder */
.mock-recipefinder{ max-width:400px; margin:0 auto; }
.rf-search{ display:flex; align-items:center; gap:7px; font-size:12.5px; color:var(--muted); background:var(--off-cell); border:1px solid var(--off-cell-border); border-radius:7px; padding:8px 10px; margin-bottom:12px; }
.rf-grid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.rf-card{ border-radius:8px; overflow:hidden; background:var(--off-cell); border:1px solid var(--off-cell-border); }
.rf-photo{ display:block; width:100%; height:90px; object-fit:cover; }
.rf-name{ font-size:11px; font-weight:600; padding:7px 8px 2px; }
.rf-kcal{ font-size:10px; color:var(--muted); padding:0 8px 8px; }

/* food log */
.mock-foodlog{ max-width:380px; margin:0 auto; }
.foodlog-row{ display:flex; align-items:center; gap:9px; font-size:12px; padding:6px 0; border-bottom:1px solid var(--panel-border); }
.foodlog-row .fl-icon{ font-size:14px; flex-shrink:0; }
.foodlog-row .fl-name{ flex:1; color:var(--text); }
.foodlog-row .fl-kcal{ color:var(--muted); font-size:11px; }
.foodlog-barcode{ display:flex; gap:7px; margin:12px 0 10px; }
.foodlog-barcode input{ flex:1; font-family:inherit; font-size:11px; color:var(--muted); background:var(--off-cell); border:1px solid var(--off-cell-border); border-radius:6px; padding:8px 9px; min-width:0; }
.cal-btn-mock{ font-family:inherit; font-size:11px; font-weight:600; color:var(--text); background:var(--panel-2); border:1px solid var(--panel-border); border-radius:6px; padding:8px 11px; white-space:nowrap; }
.foodlog-total{ font-size:12px; color:var(--muted); border-top:1px solid var(--panel-border); padding-top:10px; }
.foodlog-total b{ color:var(--text); font-size:14px; }
.foodlog-total span{ margin-left:3px; }

/* finance */
.mock-finance{ display:flex; flex-direction:column; }
.finance-row{ display:flex; align-items:center; gap:10px; font-size:11.5px; padding:7px 0; border-bottom:1px solid var(--panel-border); }
.finance-row .fr-date{ color:var(--muted-2); width:38px; flex-shrink:0; }
.finance-row .fr-desc{ flex:1; color:var(--text); }
.finance-row .fr-amt{ font-weight:600; }
.finance-row .fr-amt.pos{ color:#7fbf7f; }
.finance-row .fr-amt.neg{ color:var(--muted); }

.mock-savings{ display:flex; flex-direction:column; }
.savings-numbers{ font-size:12.5px; color:var(--muted); margin-bottom:8px; }
.savings-numbers b{ color:var(--text); font-size:15px; }
.savings-numbers span{ margin-left:2px; }
.savings-bar-track{ height:7px; border-radius:5px; background:var(--off-cell); border:1px solid var(--off-cell-border); overflow:hidden; }
.savings-bar-fill{ height:100%; border-radius:5px; background:linear-gradient(90deg,var(--accent),#7fbf7f); width:0; transition:width 1s var(--ease) .2s; }
.in-view .savings-bar-fill{ width:var(--w); }
.pie-title{ margin-top:18px; }
.pie-row{ display:flex; align-items:center; gap:18px; }
.pie-chart{
  width:64px; height:64px; border-radius:50%; flex-shrink:0; transform:scale(0); transition:transform .6s var(--ease) .2s;
  background: conic-gradient(var(--accent) 0% 40%, #e0a850 40% 65%, #ff6b5e 65% 85%, #7fbf7f 85% 100%);
  box-shadow: 0 0 0 1px var(--off-cell-border) inset;
}
.in-view .pie-chart{ transform:scale(1); }
.pie-legend{ display:flex; flex-direction:column; gap:5px; font-size:11px; color:var(--muted); }
.pie-legend div{ display:flex; align-items:center; gap:6px; }
.pie-dot{ width:8px; height:8px; border-radius:50%; background:var(--c); flex-shrink:0; }

/* block library grid */
.block-library{
  max-width:900px; margin:70px auto 0; position:relative;
}
.block-library-sub{ margin-top:10px; margin-bottom:36px; }
.block-group{ margin-bottom:22px; position:relative; z-index:1; }
.block-group-title{ font-size:11px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase; color:var(--muted-2); margin-bottom:10px; text-align:center; }
.block-grid{ display:flex; flex-wrap:wrap; gap:9px; justify-content:center; }
.block-chip{ font-size:12.5px; color:var(--muted); background:transparent; border:1px solid var(--panel-border); border-radius:999px; padding:7px 13px; transition:color .15s ease, border-color .15s ease; }
.block-chip:hover{ color:var(--text); border-color:var(--muted-2); }

/* ---------- offline band ---------- */
.offline-band{ padding:80px 24px; background: linear-gradient(180deg, transparent, rgba(var(--accent-rgb),0.05), transparent); border-top:1px solid var(--panel-border); border-bottom:1px solid var(--panel-border); }
.offline-inner{ max-width: var(--maxw); margin:0 auto; display:flex; align-items:center; justify-content:space-between; gap:40px; flex-wrap:wrap; }
.offline-copy h2{ font-size: clamp(24px, 2.6vw, 32px); font-weight:800; letter-spacing:-0.01em; margin-bottom:14px; max-width:520px; }
.offline-copy p{ font-size:15.5px; color:var(--muted); max-width:480px; line-height:1.6; }
.sync-pill{
  display:flex; align-items:center; gap:9px; background:var(--panel); border:1px solid var(--panel-border);
  border-radius:999px; padding:11px 18px; font-size:13px; font-weight:600; flex-shrink:0;
}
.sync-dot{ width:8px; height:8px; border-radius:50%; background:#3ecf7e; box-shadow: 0 0 0 0 rgba(62,207,126,0.5); animation: pulse 2.2s ease-out infinite; }
@keyframes pulse{
  0%{ box-shadow: 0 0 0 0 rgba(62,207,126,0.5); }
  70%{ box-shadow: 0 0 0 8px rgba(62,207,126,0); }
  100%{ box-shadow: 0 0 0 0 rgba(62,207,126,0); }
}

/* ---------- personalize ---------- */
.personalize{ max-width:720px; margin:0 auto; padding:100px 24px; text-align:center; }
.personalize h2{ font-size: clamp(24px, 2.6vw, 32px); font-weight:800; letter-spacing:-0.01em; margin:14px 0 12px; }
.center{ text-align:center; margin-left:auto; margin-right:auto; }
.swatch-row{ display:flex; gap:14px; justify-content:center; margin-top:34px; }
.swatch{
  width:38px; height:38px; border-radius:50%; background:var(--sw); border:2px solid transparent;
  cursor:pointer; box-shadow: 0 0 0 1px rgba(255,255,255,0.08) inset;
  transition: transform .15s var(--ease), box-shadow .15s var(--ease);
}
.swatch:hover{ transform: scale(1.12); }
.swatch.active{ box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px currentColor; color: var(--sw); }

/* ---------- download cta ---------- */
.download-cta{ max-width:640px; margin:0 auto; padding:120px 24px 140px; text-align:center; }
.cta-icon{ width:64px; height:64px; border-radius:16px; margin:0 auto 26px; box-shadow: 0 20px 40px -18px rgba(var(--accent-rgb),0.5); }
.download-cta h2{ font-size: clamp(30px, 4vw, 44px); font-weight:800; letter-spacing:-0.02em; margin-bottom:10px; }
.download-cta p{ font-size:16px; color:var(--muted); margin-bottom:32px; }
.download-meta{ margin-top:16px !important; font-size:12.5px; color:var(--muted-2) !important; }

/* ---------- footer ---------- */
.site-footer{ border-top:1px solid var(--panel-border); padding:36px 24px; }
.footer-inner{ max-width: var(--maxw); margin:0 auto; display:flex; align-items:center; gap:24px; flex-wrap:wrap; }
.footer-nav{ display:flex; gap:22px; margin-right:auto; }
.footer-nav a{ text-decoration:none; color:var(--muted); font-size:13.5px; }
.footer-nav a:hover{ color:var(--text); }
.footer-copy{ font-size:12.5px; color:var(--muted-2); }

/* ---------- reveal-on-scroll ---------- */
.reveal{ opacity:0; transform: translateY(24px); transition: opacity .7s var(--ease), transform .7s var(--ease); transition-delay: var(--delay, 0ms); }
.reveal.in-view{ opacity:1; transform:none; }

@media (prefers-reduced-motion: reduce){
  html{ scroll-behavior:auto; }
  .reveal{ opacity:1; transform:none; transition:none; }
  .mock-window{ animation:none; }
  .sync-dot{ animation:none; }
}

/* ---------- responsive ---------- */
@media (max-width: 980px){
  .hero{ grid-template-columns:1fr; padding-top:130px; text-align:left; }
  .feature-row, .feature-row.reverse{ grid-template-columns:1fr; }
  .feature-row.reverse .feature-copy, .feature-row.reverse .feature-visual{ order:initial; }
  .feature-copy p{ max-width:none; }
  .offline-inner{ flex-direction:column; align-items:flex-start; }
}
@media (max-width: 720px){
  .site-nav, .header-cta{ display:none; }
  .nav-toggle{ display:flex; }
  .mobile-nav.open{ display:flex; }
  .mock-app{ height:340px; }
  .mock-sidebar{ width:110px; }
  .two-up{ flex-direction:column; }
  .features{ gap:90px; }
}
