/* ==========================================================================
   Cloche — marketing site
   One green, two themes, type does the work.
   ========================================================================== */

/* ---------- Tokens ---------- */

:root {
  --ink: #141414;
  --paper: #FFFFFF;
  --paper-2: #F5F5F5;
  --line: #DBDBDB;
  --muted: #536371;
  --accent: #1F8F4E;
  --accent-lift: #35B06B;
  --accent-soft: rgba(31, 143, 78, 0.12);
  --cream: #FDFAF5;

  --glow: rgba(31, 143, 78, 0.18);
  --nav-bg: rgba(255, 255, 255, 0.72);
  --card-bg: var(--paper);
  --bezel: #101010;

  --font-display: "Inter Tight", "Inter", system-ui, sans-serif;
  --font-body: "Inter", system-ui, sans-serif;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  color-scheme: light;
}

:root[data-theme="dark"] {
  --ink: #F5F5F5;
  --paper: #000000;
  --paper-2: #1C1C1C;
  --line: #2E2E2E;
  --muted: #9DA9B1;
  --accent: #35B06B;
  --accent-lift: #35B06B;
  --accent-soft: rgba(53, 176, 107, 0.14);
  --cream: #1C1C1C;

  --glow: rgba(53, 176, 107, 0.22);
  --nav-bg: rgba(0, 0, 0, 0.6);
  --card-bg: #0E0E0E;
  --bezel: #262626;

  color-scheme: dark;
}

/* No-JS fallback: honour the OS preference when the inline script never ran */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --ink: #F5F5F5;
    --paper: #000000;
    --paper-2: #1C1C1C;
    --line: #2E2E2E;
    --muted: #9DA9B1;
    --accent: #35B06B;
    --accent-lift: #35B06B;
    --accent-soft: rgba(53, 176, 107, 0.14);
    --cream: #1C1C1C;
    --glow: rgba(53, 176, 107, 0.22);
    --nav-bg: rgba(0, 0, 0, 0.6);
    --card-bg: #0E0E0E;
    --bezel: #262626;
    color-scheme: dark;
  }
}

/* ---------- Base ---------- */

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

/* No scrollbar-gutter here: `stable` reserves a gutter even where scrollbars
   are overlays and take no space (macOS), which narrows the in-flow content
   while the loader's position: fixed overlay stays centred on the full
   viewport — putting the mark half a scrollbar off the phone's indicator.
   The loader survives a scroll-lock release instead by measuring, below. */
html { scroll-behavior: smooth; }

body {
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
}

/* Nothing may scroll the body sideways. clip (not hidden) so no scroll
   container is created and position: sticky keeps working. The decorative
   device glows intentionally bleed past the viewport and get cut here. */
html, body {
  overflow-x: hidden;
  overflow-x: clip;
}

img, video { max-width: 100%; display: block; }

a { color: var(--accent); text-decoration: none; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

.container {
  width: min(1120px, 100% - 48px);
  margin-inline: auto;
}

.section { padding: clamp(88px, 12vh, 150px) 0; }

/* Anchor targets land below the fixed nav */
[id] { scroll-margin-top: 76px; }

/* ---------- Type ---------- */

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1.05;
  text-wrap: balance;
}

h1 { font-size: clamp(2.8rem, 7.2vw, 5.2rem); }
h2 { font-size: clamp(2rem, 4.6vw, 3.4rem); }
h3 { font-size: clamp(1.3rem, 2.2vw, 1.7rem); line-height: 1.15; }

.kicker {
  color: var(--accent);
  font-weight: 600;
  font-size: 0.95rem;
  letter-spacing: 0.02em;
  margin-bottom: 14px;
}

.section-sub {
  color: var(--muted);
  font-size: 1.1rem;
  max-width: 34em;
  margin-top: 18px;
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  border-radius: 999px;
  transition: transform 0.2s var(--ease-out), background 0.2s, border-color 0.2s;
  will-change: transform;
}

.btn-primary {
  background: var(--accent);
  color: #fff;
  padding: 10px 20px;
}
.btn-primary:hover { background: var(--accent-lift); }
:root[data-theme="dark"] .btn-primary { color: #06130B; }

.btn-sm { font-size: 0.92rem; padding: 8px 18px; }

.btn-store {
  border: 1px solid var(--line);
  background: var(--card-bg);
  color: var(--ink);
  padding: 12px 22px;
  line-height: 1.15;
}
.btn-store:hover { border-color: var(--accent); }
.btn-store svg { width: 26px; height: 26px; flex: none; }
.btn-store small {
  display: block;
  font-weight: 400;
  font-size: 0.72rem;
  color: var(--muted);
}

/* ---------- Nav ---------- */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 50;
  padding: 10px 0;
  transition: background 0.3s, border-color 0.3s;
  border-bottom: 1px solid transparent;
}
.nav.scrolled {
  background: var(--nav-bg);
  -webkit-backdrop-filter: blur(18px);
  backdrop-filter: blur(18px);
  border-bottom-color: var(--line);
}

.nav-inner {
  width: min(1220px, 100% - 40px);
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.wordmark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.45rem;
  letter-spacing: -0.03em;
  color: var(--ink);
}

.nav-pill {
  display: flex;
  gap: 4px;
  padding: 5px;
  border-radius: 999px;
  border: 1px solid transparent;
  transition: background 0.3s, border-color 0.3s;
}
.nav.scrolled .nav-pill {
  background: var(--accent-soft);
  border-color: transparent;
}
.nav-pill a {
  color: var(--ink);
  font-size: 0.92rem;
  font-weight: 500;
  padding: 6px 14px;
  border-radius: 999px;
}
.nav-pill a:hover { background: var(--accent-soft); color: var(--accent); }

.nav-actions { display: flex; align-items: center; gap: 12px; }

.theme-toggle {
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: border-color 0.2s;
}
.theme-toggle:hover { border-color: var(--accent); }
.theme-toggle svg { width: 18px; height: 18px; }
.icon-moon { display: none; }
:root[data-theme="dark"] .icon-moon { display: block; }
:root[data-theme="dark"] .icon-sun { display: none; }

/* ---------- Device frame ---------- */

.device {
  position: relative;
  width: min(300px, 76vw);
  background: var(--bezel);
  border-radius: 44px;
  padding: 10px;
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.18),
    0 30px 70px -20px rgba(0, 0, 0, 0.35);
}
.device-small { width: min(250px, 68vw); }

.device-screen {
  position: relative;
  aspect-ratio: 1206 / 2622; /* native screenshot ratio — height rules, width follows */
  border-radius: 34px;
  overflow: hidden;
  background: #000;
}
.device-screen img,
.device-screen video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.device-glow {
  position: absolute;
  inset: -18%;
  background: radial-gradient(closest-side, var(--glow), transparent 72%);
  z-index: -1;
  pointer-events: none;
}

/* The glow is anchored to .hero-device, not to .device, so it does not ride
   the loader's zoom — it would sit at the phone's final spot, marking the
   destination with a green smudge while the phone is still on its way in.
   Hold it back and bring it up as the phone arrives. */
html.is-loading .hero-device .device-glow { opacity: 0; }
.hero-device .device-glow { transition: opacity 0.37s ease; } /* 0.55s / 1.5 */

/* ---------- Hero ---------- */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  padding: 130px 0 70px;
  overflow: hidden;
}

.hero-inner {
  width: min(1120px, 100% - 48px);
  margin-inline: auto;
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  align-items: center;
  gap: clamp(32px, 5vw, 72px);
}

.hero-sub {
  font-size: clamp(1.15rem, 2vw, 1.4rem);
  font-weight: 500;
  margin-top: 22px;
  max-width: 24em;
}

.hero-detail {
  color: var(--muted);
  margin-top: 16px;
  max-width: 32em;
}

.hero-ctas {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 34px;
}

.hero-device {
  position: relative;
  justify-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}
.hero-device .device {
  /* Never taller than the viewport allows (the toggle below needs room too) */
  width: min(300px, 76vw, calc((100svh - 280px) * 0.52));
}

/* The loader hands over with the feed in its pulled-down pose; dropping
   .is-arriving lets it settle up as the mark retracts. The 13% matches the
   app's own pull depth, so the mark sits in the gap it opened. */
.swap-screen { background: var(--paper); }

/* The loader hands over with the feed already pulled down; dropping
   .is-arriving lets it settle up once the phone has landed. The 13% matches
   the app's own pull depth, so the mark sits in the gap it opened.

   Entering the pose is a starting state, not an animation — transition: none
   so it lands instantly behind the opaque overlay. Left to transition, the
   feed slides visibly DOWN during the reveal and then back up, which reads as
   a double move. (The hold-off before it settles is sequenced in JS.) */
html.is-arriving .swap-screen .swap-img {
  transform: translateY(13%);
  transition: none;
}

/* The in-phone refresh beat: the same sprite at the app's own indicator
   geometry — x 50%, y 10.51% of the screen, 8.96% of its width. */
.screen-cloche {
  position: absolute;
  left: 50%; top: 10.51%;
  width: 8.96%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background: url("assets/cloche-refresh-sheet.png") 0% 0% / 600% 600% no-repeat;
  opacity: 0;
  transition: opacity 0.15s ease;
  pointer-events: none;
}
.screen-cloche.is-on { opacity: 1; }

/* Animated gradient mesh — three soft green blobs drifting very slowly */
.mesh { position: absolute; inset: 0; pointer-events: none; }
.mesh-blob {
  position: absolute;
  width: 55vw; height: 55vw;
  max-width: 720px; max-height: 720px;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--glow), transparent 70%);
  opacity: 0.7;
}
.mesh-a { top: -18%; left: -12%; animation: drift-a 26s ease-in-out infinite alternate; }
.mesh-b { bottom: -22%; right: -10%; animation: drift-b 32s ease-in-out infinite alternate; }
.mesh-c { top: 30%; left: 45%; opacity: 0.4; animation: drift-a 40s ease-in-out infinite alternate-reverse; }

@keyframes drift-a { from { transform: translate(0, 0); } to { transform: translate(9%, 6%); } }
@keyframes drift-b { from { transform: translate(0, 0); } to { transform: translate(-8%, -7%); } }

/* Split-word rise: each word sits in an overflow mask and rises into view */
.split-words .w { display: inline-block; overflow: hidden; vertical-align: bottom; }
.split-words .wi {
  display: inline-block;
  transform: translateY(110%);
  animation: word-rise 0.8s var(--ease-out) forwards;
  animation-delay: var(--d, 0s);
}
@keyframes word-rise { to { transform: translateY(0); } }

/* ---------- Marquee ---------- */

.marquee {
  overflow: hidden;
  border-block: 1px solid var(--line);
  padding: 18px 0;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.marquee-track {
  display: flex;
  width: max-content;
  color: var(--muted);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.05rem;
  white-space: nowrap;
  animation: marquee 36s linear infinite;
}
/* Two identical groups, each ending in its own gap, make -50% loop cleanly */
.marquee-group { display: flex; gap: 40px; padding-right: 40px; }
@keyframes marquee { to { transform: translateX(-50%); } }

/* ---------- Scroll gallery ---------- */

/* The rail is the scroll container. Its inline padding matches .container's
   gutter so the first card starts on the same line as the heading above it,
   while later cards still bleed off both edges. Percentages resolve against
   this element's own width, which is the full content width — unlike vw, that
   already excludes the scrollbar gutter. */
.gallery-viewport {
  --gutter: max(24px, calc((100% - 1120px) / 2));
  margin-top: 44px;
  /* No padding-right: Chrome drops a scroll container's end padding from
     scrollWidth, so it would buy no trailing space and would only shrink the
     base the spacer below measures against. The spacer supplies that edge. */
  padding: 4px 0 8px var(--gutter);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-padding-inline-start: var(--gutter);
  overscroll-behavior-x: contain;
  /* No visible scrollbar — the arrows and the peeking next card say enough */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.gallery-viewport::-webkit-scrollbar { display: none; }
.gallery-viewport:focus-visible { outline-offset: -2px; }

.gallery-track {
  list-style: none;
  display: flex;
  gap: 20px;
  align-items: stretch; /* equal-height cards, so the links line up */
}

/* Trailing gutter, as a flex item — the only kind of end space a scroll
   container actually counts. Without it the last card can never come to rest
   against the same gutter the first one starts on; it just jams into the
   viewport edge.
   The width has to equal --gutter, but a percentage here resolves against the
   track's content box (viewport width minus the leading gutter), not the
   viewport. Given base B = W - G and G = (W - 1120) / 2, the identity
   G = B - 1120 falls out — hence 1120, less the 20px flex gap that already
   sits in front of this item. */
.gallery-track::after {
  content: "";
  flex: 0 0 max(4px, calc(100% - 1140px));
}

.gallery-card {
  flex: 0 0 clamp(230px, 26vw, 340px);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
}

/* Tile first, phone inside it, bleeding off the bottom edge */
.gallery-shot {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: 22px;
  overflow: hidden;
  background: var(--cream);
  border: 1px solid var(--line);
}
:root[data-theme="dark"] .gallery-shot { background: var(--card-bg); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .gallery-shot { background: var(--card-bg); }
}

.gallery-phone {
  position: absolute;
  left: 50%;
  top: 11%;
  width: 62%;
  transform: translateX(-50%);
  background: var(--bezel);
  border-radius: 26px;
  padding: 6px;
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.18),
    0 24px 50px -20px rgba(0, 0, 0, 0.35);
}
.gallery-phone .device-screen { border-radius: 21px; }

/* Apple's caption shape: bold lead-in, the rest in muted grey */
.gallery-caption {
  margin-top: 22px;
  color: var(--muted);
  font-size: 1.02rem;
  line-height: 1.45;
}
.gallery-caption b { color: var(--ink); font-weight: 600; }

/* Pushed to the bottom of the card so every link sits on one line across the
   rail, however many lines the caption above it runs to. */
.gallery-link {
  align-self: flex-start;
  margin-top: auto;
  padding-top: 14px;
  font-weight: 500;
  font-size: 0.98rem;
}
.gallery-link span { display: inline-block; transition: transform 0.2s var(--ease-out); }
.gallery-link:hover span { transform: translateX(3px); }

.gallery-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  margin-top: 34px;
}
.gallery-hint { color: var(--muted); font-size: 0.9rem; }

.gallery-nav { display: flex; gap: 10px; }

.gallery-btn {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card-bg);
  color: var(--ink);
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s, opacity 0.2s;
}
.gallery-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.gallery-btn:disabled { opacity: 0.35; cursor: default; }
.gallery-btn svg { width: 18px; height: 18px; }

/* JS turns the arrows on; without it the rail is still a normal scroller and
   dead buttons would be worse than none. */
.gallery-nav { display: none; }
html.js .gallery-nav { display: flex; }

/* Pointer-drag. Snapping is switched off for the duration: with it on, every
   scrollLeft the drag writes gets yanked back to the nearest snap point and
   the card judders under the cursor. Dropping .is-dragging on release lets
   the snap re-engage, which is what lands the throw on a card. */
.gallery-viewport.is-dragging {
  cursor: grabbing;
  scroll-snap-type: none;
  user-select: none;
}
.gallery-viewport.is-dragging a { pointer-events: none; }

/* ---------- Full-screen map scroll ---------- */

.mapscroll {
  /* Tall on purpose: the height ABOVE the viewport is the scrub track the pins
     land over — 55svh of it here, so the whole set rains down in about half a
     screen of scrolling (a quarter of the 220svh this started at). */
  height: 155svh;
  position: relative;
}

.map-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  overflow: clip;
  background: var(--paper-2);
}

/* Cover the viewport at the base image's own ratio (1206 x 1820), whichever
   axis rules, so the pins' percentage positions always land on the same spot
   of the map. The zoom is applied on top by JS — pins are children, so they
   ride it and stay welded to their suburbs. */
.map-stage {
  position: absolute;
  left: 50%;
  top: 0;
  width: max(100vw, calc(100vh * 0.7012)); /* svh fallback for older Safari */
  width: max(100vw, calc(100svh * 0.7012));
  aspect-ratio: 1206 / 1720;
  transform: translateX(-50%);
  /* The pin band's centre on the image. JS keeps this point mid-viewport on
     short, wide screens (the image is far taller than a landscape viewport),
     and the zoom pivots on it so the anchor never drifts. */
  transform-origin: 50% 39%;
  will-change: transform;
}

.map-base {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Outer element owns the position (tip of the teardrop = the suburb),
   inner .pin-fall owns the drop, so the two transforms never fight. */
.map-pin {
  position: absolute;
  transform: translate(-50%, -100%);
  pointer-events: none;
}
.map-pin svg {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 5px 9px rgba(0, 0, 0, 0.25));
}
.pin-fall { will-change: transform, opacity; }

/* Legibility for the titles and the nav over a busy map */
.map-scrim {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
}
.map-scrim-top {
  top: 0;
  height: 36svh;
  background: linear-gradient(to bottom, var(--paper), transparent);
  opacity: 0.9;
}
.map-scrim-bottom {
  bottom: 0;
  height: 22svh;
  background: linear-gradient(to top, var(--paper), transparent);
  opacity: 0.75;
}

.map-titles {
  position: absolute;
  top: max(96px, 11svh);
  left: 0;
  right: 0;
  text-align: center;
  z-index: 5;
  pointer-events: none;
  padding-inline: 24px;
}
.map-note { color: var(--muted); margin-top: 12px; font-size: 0.98rem; }

/* The app's own summary pill, counting up as the pins land */
.map-chip {
  position: absolute;
  left: 50%;
  bottom: max(30px, 6svh);
  transform: translate(-50%, 12px);
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--nav-bg);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border: 1px solid var(--line);
  font-weight: 500;
  font-size: 0.98rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.4s ease, transform 0.4s var(--ease-out);
  z-index: 6;
}
.map-chip.is-on { opacity: 1; transform: translate(-50%, 0); }
.map-chip b { font-weight: 700; }
.map-chip-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

/* ---------- Two sides ---------- */

.tabs {
  display: inline-flex;
  gap: 4px;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card-bg);
}
.tab {
  font: inherit;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: 999px;
  padding: 9px 20px;
  cursor: pointer;
  transition: background 0.25s, color 0.25s;
}
.tab.is-active { background: var(--accent); color: #fff; }
:root[data-theme="dark"] .tab.is-active { color: #06130B; }

/* Static two-column copy — the interactive toggle lives in the hero */
.sides-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-top: 48px;
}

.side-col {
  border: 1px solid var(--line);
  border-radius: 18px;
  background: var(--card-bg);
  padding: 34px 30px;
}
.side-tag {
  color: var(--accent);
  font-weight: 600;
  font-size: 0.9rem;
  margin-bottom: 12px;
}
.side-col h3 { margin-bottom: 16px; }
.side-col p { color: var(--muted); max-width: 34em; }

.ticks { list-style: none; margin-top: 24px; display: grid; gap: 12px; }
.ticks li { padding-left: 30px; position: relative; }
.ticks li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.36em;
  width: 17px; height: 17px;
  border-radius: 50%;
  background: var(--accent-soft);
  /* inline tick, tinted per-theme via the two rules below */
}
.ticks li::after {
  content: "";
  position: absolute;
  left: 4px; top: calc(0.36em + 4px);
  width: 9px; height: 5px;
  border-left: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: rotate(-45deg);
}

/* Crossfading screenshot pairs (hero tabs + pinned scroll share the
   mechanics). Inactive panes go visibility: hidden after the fade so they
   leave the accessibility tree, not just the paint. */
.swap-img, .beat-img {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 0.45s ease,
    transform 0.45s var(--ease-out),
    visibility 0s linear 0.45s;
}
.swap-img.is-active, .beat-img.is-active {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

/* Only the hero pair's transform is part of the loader's choreography (the
   feed settling up), so it runs on the same 1.5x clock — 0.45s / 1.5. The
   opacity crossfade is an interaction, not choreography, and keeps its pace. */
.swap-img {
  transition:
    opacity 0.45s ease,
    transform 0.3s var(--ease-out),
    visibility 0s linear 0.45s;
}

/* ---------- Pinned feature scroll ---------- */

.pinned-layout {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: clamp(40px, 8vw, 110px);
  margin-top: 30px;
}

.pinned-device {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  align-items: center;
}
.pinned-device .device {
  width: min(300px, 76vw, calc((100svh - 140px) * 0.5));
}

.beat {
  min-height: 88svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  max-width: 26em;
}
.beat h3 { margin-bottom: 16px; }
.beat p { color: var(--muted); }
.beat p + p { margin-top: 12px; }

/* ---------- Cloche delight beat ---------- */

.cloche-beat {
  /* Tall on purpose: the extra height is the scrub track for the video */
  height: 260svh;
  background: var(--cream);
}
:root[data-theme="dark"] .cloche-beat { background: var(--paper-2); }

.cloche-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
}

.cloche-inner {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: clamp(36px, 6vw, 90px);
}
.cloche-copy p { color: var(--muted); margin-top: 18px; max-width: 26em; }

/* There is no dark recording of the pull-to-refresh clip, so dark theme flips
   the light one (flat UI content, no photos): invert + hue-rotate keeps the
   cloche green while whites become blacks.

   Scoped to .has-video — set in JS once frames are actually decodable — so the
   poster is never inverted. The poster is a real dark capture swapped in by JS,
   which is what reduced-motion and load-failure users see. */
:root[data-theme="dark"] .cloche-beat video.has-video { filter: invert(1) hue-rotate(180deg); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .cloche-beat video.has-video { filter: invert(1) hue-rotate(180deg); }
}

/* Geometry measured off the 1206x2622 frame: the island spans x 415-790,
   y 42-151. The screen box shares that ratio, so percentages land exactly.
   Inflated ~2px per side (kept centred on 50%) to swallow the island's
   antialiased edge, which would otherwise invert into a pale halo. */
.island-patch {
  position: absolute;
  left: 34.2%;
  top: 1.50%;
  width: 31.6%;
  height: 4.40%;
  border-radius: 999px;
  background: #000;
  opacity: 0;
  pointer-events: none;
}
:root[data-theme="dark"] video.has-video ~ .island-patch { opacity: 1; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) video.has-video ~ .island-patch { opacity: 1; }
}

/* ---------- Feature grid ---------- */

.feature-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-top: 48px;
}

.card {
  position: relative;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: var(--card-bg);
  padding: 28px 26px;
  overflow: hidden;
}
/* Cursor-following glow, driven by --mx/--my set in JS */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(280px circle at var(--mx, 50%) var(--my, 50%), var(--accent-soft), transparent 70%);
  opacity: 0;
  transition: opacity 0.35s;
  pointer-events: none;
}
.card:hover::before { opacity: 1; }

.card svg { width: 26px; height: 26px; color: var(--accent); margin-bottom: 18px; }
.card h3 { font-size: 1.12rem; margin-bottom: 10px; }
.card p { color: var(--muted); font-size: 0.98rem; }

/* ---------- Stats band ---------- */

.stats {
  background: #050505;
  color: #F5F5F5;
  padding: clamp(80px, 11vh, 130px) 0;
}

.stats-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 32px;
  text-align: center;
}
.stat dd {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.6rem, 6vw, 4.4rem);
  letter-spacing: -0.03em;
  color: #35B06B;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.stat dt { color: #9DA9B1; margin-top: 10px; font-size: 0.98rem; }
.stats-note { text-align: center; color: #6E7B84; margin-top: 44px; font-size: 0.92rem; }

/* ---------- CTA + footer ---------- */

.cta { text-align: center; }
.cta h2 { font-size: clamp(2.4rem, 6vw, 4.6rem); }
.cta-ctas { justify-content: center; margin-top: 38px; }

.footer { border-top: 1px solid var(--line); padding: 34px 0; }
.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.footer p { color: var(--muted); font-size: 0.9rem; }
.footer nav { display: flex; gap: 22px; }
.footer nav a { color: var(--muted); font-size: 0.9rem; }
.footer nav a:hover { color: var(--accent); }

/* ---------- Scroll reveals ---------- */
/* Hidden state only applies when JS is running AND motion is allowed,
   so content is never trapped invisible. */

@media (prefers-reduced-motion: no-preference) {
  html.js [data-reveal],
  html.js [data-reveal-children] > * {
    opacity: 0;
    transform: translateY(24px);
    filter: blur(8px);
    transition:
      opacity 0.9s var(--ease-out),
      transform 0.9s var(--ease-out),
      filter 0.9s var(--ease-out);
    transition-delay: var(--d, 0s);
  }
  html.js [data-reveal].in,
  html.js [data-reveal-children].in > * {
    opacity: 1;
    transform: none;
    filter: none;
  }

  /* While the preloader holds the page, reveals wait with it: the observer
     runs under the overlay and hands out .in early, but nothing moves until
     .is-loading drops. Placed after the .in rule on purpose — same
     specificity, later wins. */
  html.js.is-loading [data-reveal],
  html.js.is-loading [data-reveal-children] > * {
    opacity: 0;
    transform: translateY(24px);
    filter: blur(8px);
    transition: none;
  }
  /* The split-word rise pauses inside its delay too, so the stagger replays
     intact once the loader hands over. */
  html.js.is-loading .split-words .wi { animation-play-state: paused; }
}

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .mesh-blob, .marquee-track { animation: none; }
  .split-words .wi { transform: none; animation: none; }
  .swap-img, .beat-img { transition: none; }
  .btn, .gallery-link span { transition: none; }
  .cloche-beat { height: auto; padding: clamp(88px, 12vh, 150px) 0; }
  .cloche-sticky { position: static; height: auto; }
  /* One static screen: the finished map, pins already landed (their default
     state — the drop is only ever applied inline by JS) */
  .mapscroll { height: auto; }
  .map-sticky { position: static; }
  .map-chip { opacity: 1; transform: translate(-50%, 0); transition: none; }
}

/* ---------- Responsive ---------- */

@media (max-width: 960px) {
  .feature-grid { grid-template-columns: repeat(2, 1fr); }
  .stats-row { grid-template-columns: repeat(2, 1fr); row-gap: 48px; }
}

@media (max-width: 860px) {
  .nav-pill { display: none; }

  /* Phone first on small screens. The whole opening beat is a refresh
     happening on a phone, so the phone has to be wholly on screen when the
     page settles — with the copy stacked underneath it rather than above,
     where it pushed the phone past the fold. */
  .hero {
    min-height: auto;
    align-items: flex-start;
    padding: 84px 0 64px;
  }
  .hero-inner { grid-template-columns: 1fr; text-align: left; }
  .hero-device { order: -1; margin-top: 0; }
  /* Sized off the viewport so the phone AND its toggle always fit the first
     screen: 0.46 is the screen's 1206/2622 ratio (width from height), and the
     250px covers the nav, the hero's top padding, the toggle and its gap. */
  .hero-device .device { width: min(240px, 62vw, calc((100svh - 250px) * 0.46)); }
  .hero-copy { margin-top: 34px; }

  .sides-columns { grid-template-columns: 1fr; }

  /* Pinned scroll on small screens: the phone shrinks and pins near the top,
     the copy scrolls beneath it */
  .pinned-layout { grid-template-columns: 1fr; gap: 0; }
  .pinned-device {
    top: 58px;
    height: auto;
    justify-content: center;
    z-index: 1;
    padding: 10px 0;
    background: var(--paper);
  }
  .pinned-device .device { width: min(160px, 42vw, calc(40svh * 0.46)); border-radius: 30px; padding: 7px; }
  .pinned-device .device-screen { border-radius: 24px; }
  /* Copy sits low in each beat so it clears the pinned phone above it */
  .beat {
    min-height: 60svh;
    margin-inline: auto;
    text-align: center;
    justify-content: flex-end;
    padding-bottom: 7svh;
  }

  .cloche-inner { grid-template-columns: 1fr; text-align: center; }
  .cloche-device { justify-self: center; }
}

@media (max-width: 560px) {
  .feature-grid { grid-template-columns: 1fr; }

  /* One card at a time, with the next one just showing */
  .gallery-card { flex-basis: 74vw; }
  .gallery-caption { font-size: 0.96rem; }
  .gallery-hint { display: none; }
  .gallery-foot { justify-content: flex-end; }

  .btn-store { flex: 1 1 auto; justify-content: center; }
  .hero-tabs .tab { padding: 8px 14px; font-size: 0.88rem; }
}
