:root {
  --text: #1d1d1f;
  --text-secondary: #6e6e73;
  --text-muted: #86868b;
  --bg: #ffffff;
  --bg-subtle: #f5f5f7;
  --bg-deep: #e5e5ea;
  --border: #d2d2d7;
  --accent: #0071e3;
  --accent-hover: #0077ed;
  --wrap: 1100px;
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --text: #f5f5f7;
    --text-secondary: #a1a1a6;
    --text-muted: #86868b;
    --bg: #000000;
    --bg-subtle: #1d1d1f;
    --bg-deep: #000000;
    --border: #424245;
    --accent: #2997ff;
    --accent-hover: #55aeff;
  }
}

* { box-sizing: border-box; }

/* Scroll-reveal via IntersectionObserver (see script near end of body).
   animation-timeline: view() would be the more elegant, JS-free tool here,
   but despite being standards-track it isn't actually firing in the browser
   this site is tested in — so IntersectionObserver it is, the
   universally-supported option. Trying to pre-trigger the observer by a
   fixed pixel margin so the transition "arrives" exactly as the element
   reaches the viewport turned out to be unworkable — trackpad flicks,
   mouse-wheel steps and keyboard paging all cover wildly different
   distances per second, so no fixed margin is right for all of them.
   Instead the trigger point is left alone (fires once meaningfully
   visible) and the motion itself is made slow and large enough (650ms,
   18px) to read clearly as an animation no matter when exactly it starts.
   Degrades to "always visible" if JS is unavailable, since .reveal only
   gets the hidden state once JS confirms it can reveal it. */
.js .reveal {
  opacity: 0;
  transform: translateY(32px);
  transition: opacity 1200ms var(--ease-out), transform 1200ms var(--ease-out);
}

.js .reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .js .reveal {
    transform: none;
    transition: opacity 300ms ease;
  }
}

/* Grids of items (feature tiles, review cards): the grid is the observed
   target (.reveal-group), children stagger in via nth-child delays. */
.js .reveal-group .feature-tile,
.js .reveal-group .review-card {
  opacity: 0;
  transform: translateY(32px);
  transition: opacity 1200ms var(--ease-out), transform 1200ms var(--ease-out);
}

.js .reveal-group.is-visible .feature-tile,
.js .reveal-group.is-visible .review-card {
  opacity: 1;
  transform: none;
}

.reveal-group .feature-tile:nth-child(2),
.reveal-group .review-card:nth-child(2) { transition-delay: 160ms; }
.reveal-group .feature-tile:nth-child(3),
.reveal-group .review-card:nth-child(3) { transition-delay: 320ms; }
.reveal-group .feature-tile:nth-child(4),
.reveal-group .review-card:nth-child(4) { transition-delay: 480ms; }
.reveal-group .feature-tile:nth-child(5),
.reveal-group .review-card:nth-child(5) { transition-delay: 640ms; }
.reveal-group .feature-tile:nth-child(6),
.reveal-group .review-card:nth-child(6) { transition-delay: 800ms; }
.reveal-group .review-card:nth-child(7) { transition-delay: 960ms; }
.reveal-group .review-card:nth-child(8) { transition-delay: 1120ms; }
.reveal-group .review-card:nth-child(9) { transition-delay: 1280ms; }
.reveal-group .review-card:nth-child(10) { transition-delay: 1440ms; }

/* Review stars: pop in left-to-right once their card is revealed — the
   delay is deliberately well past the card's own fade so the stars read as
   a small flourish after the card has already arrived. */
.js .review-card .star {
  display: inline-block;
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 450ms var(--ease-out), transform 450ms var(--ease-out);
  transition-delay: 1200ms;
}

.js .reveal-group.is-visible .review-card .star {
  opacity: 1;
  transform: scale(1);
}

.review-card .star:nth-child(2) { transition-delay: 1300ms; }
.review-card .star:nth-child(3) { transition-delay: 1400ms; }
.review-card .star:nth-child(4) { transition-delay: 1500ms; }
.review-card .star:nth-child(5) { transition-delay: 1600ms; }

@media (prefers-reduced-motion: reduce) {
  .js .reveal-group .feature-tile,
  .js .reveal-group .review-card,
  .js .review-card .star {
    transform: none;
    transition: opacity 300ms ease;
    transition-delay: 0ms;
  }
}

html { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: var(--text);
  background: var(--bg);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  font-optical-sizing: auto;
}

.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 24px;
}

h1, h2, h3, h4 {
  font-weight: 600;
  letter-spacing: -0.015em;
  margin: 0 0 16px;
}

h1 { font-size: clamp(42px, 7vw, 72px); font-weight: 700; letter-spacing: -0.03em; line-height: 1.05; }
h2 { font-size: clamp(30px, 4vw, 44px); letter-spacing: -0.02em; }
h4 { font-size: 16px; margin-bottom: 4px; letter-spacing: 0; }

p { color: var(--text-secondary); margin: 0 0 16px; }

a { color: inherit; text-decoration: none; }

/* Header */
.site-header {
  border-bottom: 1px solid transparent;
  position: sticky;
  top: 0;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  z-index: 10;
  transition: border-color 200ms ease, box-shadow 200ms ease;
}

/* Scroll state toggled by a passive scroll listener (see script near end of
   body) — a state-indication cue, not decoration, so it ships ungated by
   .js: with JS absent the header simply keeps its default border. */
.site-header.scrolled {
  border-bottom-color: var(--border);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 52px;
}

.logo { font-weight: 600; font-size: 15px; }

.site-header nav a {
  margin-left: 28px;
  font-size: 13px;
  color: var(--text-secondary);
}

.site-header nav a:hover { color: var(--text); }

.site-header nav a.lang-switch {
  color: var(--text);
  font-weight: 600;
  border: 1px solid var(--border);
  border-radius: 980px;
  padding: 4px 11px;
}

.site-header nav a.lang-switch:hover { border-color: var(--text); }

/* Hero */
.hero {
  padding: 120px 0 80px;
  text-align: center;
}

/* First-paint entrance: staggered fade + rise, no JS needed. Fires once, on
   initial render, via @starting-style — the right tool since this is a
   rare/first-time view (page load), not a repeated interaction. */
.hero .eyebrow,
.hero h1,
.hero-sub,
.hero-actions {
  opacity: 1;
  transform: none;
  transition: opacity 1100ms var(--ease-out), transform 1100ms var(--ease-out);
}

.hero .eyebrow { transition-delay: 0ms; }
.hero h1 { transition-delay: 120ms; }
.hero-sub { transition-delay: 240ms; }
.hero-actions { transition-delay: 360ms; }

@media (prefers-reduced-motion: no-preference) {
  @starting-style {
    .hero .eyebrow,
    .hero h1,
    .hero-sub,
    .hero-actions {
      opacity: 0;
      transform: translateY(14px);
    }
  }
}

.eyebrow .no-caps {
  text-transform: none;
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
  margin-bottom: 16px;
}

.hero-sub {
  max-width: 560px;
  margin: 0 auto 36px;
  font-size: 19px;
}

.hero-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 24px;
  flex-wrap: wrap;
}

.btn {
  display: inline-block;
  padding: 12px 24px;
  border-radius: 980px;
  font-weight: 500;
  font-size: 16px;
  transition: background 0.2s ease, opacity 0.2s ease, transform 120ms ease;
}

.btn:active { transform: scale(0.97); }

.btn-primary {
  background: var(--accent);
  color: #fff;
}

.btn-primary:hover { background: var(--accent-hover); }

.btn-text {
  color: var(--accent);
  padding: 12px 4px;
}

.btn-text:hover { color: var(--accent-hover); }

@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover:not(:active) { transform: translateY(-1px); }
}

/* Stats */
.stats {
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  text-align: center;
  padding: 40px 0;
}

.stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 24px;
  border-left: 1px solid var(--border);
}

.stat:first-child { border-left: none; }

.stat-number {
  margin: 0;
  font-size: clamp(30px, 5vw, 56px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.025em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.stat-label {
  margin: 0;
  max-width: 12rem;
  font-size: clamp(12px, 1.5vw, 15px);
  font-weight: 500;
  color: var(--text-muted);
}

/* App sections */
.app-section {
  padding: 100px 0;
  overflow: hidden;
}

.app-section.alt {
  background: var(--bg-subtle);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}

.app-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 64px;
  align-items: center;
}

.app-grid.reverse .app-copy { order: 2; }
.app-grid.reverse .app-shots { order: 1; }

.app-title-row {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 8px;
}

.app-icon-sm {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  display: block;
}

.app-title-row h2 { margin: 0; }

.app-tagline {
  font-size: 19px;
  color: var(--text);
  font-weight: 500;
  margin-bottom: 16px;
}

.app-description {
  font-size: 16px;
  max-width: 46ch;
}

.feature-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  margin: 32px 0 36px;
}

.feature-tile h4 { color: var(--text); }

.feature-tile p {
  font-size: 14px;
  margin: 0;
}

.feature-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin-bottom: 12px;
  color: var(--accent);
}

.feature-icon svg {
  width: 24px;
  height: 24px;
}

.store-badge {
  display: inline-block;
  transition: transform 150ms ease;
}
.store-badge img { height: 44px; width: auto; display: block; }

.store-badge:active { transform: scale(0.96); }

@media (hover: hover) and (pointer: fine) {
  .store-badge:hover:not(:active) { transform: scale(1.03); }
}

.app-shots {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  align-items: center;
}

.app-shots img {
  width: 100%;
  border-radius: 24px;
  box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.25);
  display: block;
}

.app-shots img.raised {
  transform: translateY(-28px);
}

/* Concept stats (illustrative, not live) — gray "tile" callout, rockettrace-style */
.concept-stats {
  margin-top: 56px;
  padding: 48px 32px;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: 20px;
  text-align: center;
  transition: opacity 600ms var(--ease-out), transform 600ms var(--ease-out);
}

/* The box starts [hidden] and is revealed by JS once real data loads — a
   genuine mount event, so @starting-style is the right (JS-free) tool for
   its entrance rather than wiring up another .reveal observer. */
@media (prefers-reduced-motion: no-preference) {
  @starting-style {
    .concept-stats:not([hidden]) {
      opacity: 0;
      transform: translateY(12px) scale(0.98);
    }
  }
}

.concept-badge {
  display: inline-block;
  margin: 0 0 16px;
  padding: 4px 12px;
  border-radius: 980px;
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.concept-title {
  margin: 0 auto 36px;
  max-width: 26ch;
  font-size: clamp(22px, 3.4vw, 30px);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.2;
  color: var(--text);
}

.concept-stats-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  max-width: 720px;
  margin: 0 auto;
}

.concept-stat {
  padding: 0 16px;
  border-left: 1px solid var(--border);
}

.concept-stat:first-child { border-left: none; }

.concept-stat-number {
  margin: 0;
  font-size: clamp(24px, 4vw, 34px);
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.concept-stat-label {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--text-muted);
}

.concept-caption {
  margin: 20px 0 0;
  font-size: 11px;
  line-height: 1.4;
  color: var(--text-muted);
  opacity: 0.7;
  white-space: nowrap;
}

/* Reviews */
.reviews {
  background: #000;
  color: #f5f5f7;
  padding: 100px 0;
  text-align: center;
}

.reviews .eyebrow-dark {
  color: #2997ff;
}

.reviews-title {
  color: #f5f5f7;
  margin-bottom: 48px;
}

/* Reviews are a horizontally scrolling carousel (up to 10 cards once live
   data replaces the static ones) rather than a grid, since a grid can't
   hold a variable, potentially-large count without wrapping awkwardly. */
.review-carousel {
  position: relative;
}

.review-grid {
  list-style: none;
  margin: 0;
  padding: 4px 4px 12px;
  display: flex;
  gap: 20px;
  text-align: left;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.review-grid::-webkit-scrollbar {
  display: none;
}

/* Soft edge fade instead of a hard cut on a partially visible card — the
   web equivalent of the native-app carousel feel is a mask-image gradient.
   The carousel now loops infinitely (see assets/js/review-carousel.js), so
   there's always more content in both directions — both edges fade
   permanently once the track actually overflows, rather than being toggled
   per scroll position. */
.review-grid.fade-edges {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 64px, #000 calc(100% - 64px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 64px, #000 calc(100% - 64px), transparent);
}

.review-card {
  background: #1c1c1e;
  border: 1px solid #333336;
  border-radius: 20px;
  padding: 28px;
  display: flex;
  flex-direction: column;
  flex: 0 0 320px;
  max-width: 85vw;
  /* "start" snapped each card flush to the left edge with peek-space only
     on the right — on mobile, where the card nearly fills the viewport,
     that reads as "cut off on the left" rather than the centered,
     peek-on-both-sides native-carousel look. The infinite loop means
     there's always a neighbor on both sides to center against, even for
     the "first"/"last" real card. */
  scroll-snap-align: center;
}

/* Round, semi-transparent prev/next controls — only shown once JS detects
   the carousel actually overflows (see script near end of body), so they
   never sit there uselessly on a short, non-scrolling set of cards. */
.review-nav {
  display: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid #333336;
  background: rgba(28, 28, 30, 0.85);
  color: #f5f5f7;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: background 150ms ease, opacity 150ms ease, transform 150ms ease;
}

.review-nav.is-active {
  display: block;
}

.review-nav:hover {
  background: rgba(28, 28, 30, 1);
}

.review-nav:active {
  transform: translateY(-50%) scale(0.94);
}

.review-nav-prev { left: -36px; }
.review-nav-next { right: -36px; }

/* Mobile-only floating back-to-top button — desktop keeps the sticky
   header's nav links as its own "jump back up" shortcut, but those
   collapse away on mobile (see .site-header nav a:not(:last-child)),
   leaving a long page with no quick way back to the top. */
.back-to-top {
  display: none;
  position: fixed;
  right: 16px;
  bottom: 16px;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  color: var(--text);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  cursor: pointer;
  z-index: 20;
}

.back-to-top:active { scale: 0.92; }

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

  .back-to-top {
    display: flex;
    opacity: 0;
    transform: translateY(8px);
    pointer-events: none;
    transition: opacity 200ms ease, transform 200ms ease, scale 100ms ease-out;
  }

  .back-to-top.is-visible {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }

  @media (prefers-reduced-motion: reduce) {
    .back-to-top {
      transition: opacity 150ms ease;
      transform: none;
    }
  }
}

.review-stars {
  color: #ffb340;
  letter-spacing: 2px;
  font-size: 15px;
  margin-bottom: 16px;
}

/* Live-fetched reviews can be 4-star, not just 5 — show the honest count
   instead of always filling five stars. */
.review-stars .star.is-empty {
  color: #86868b;
}

.review-card blockquote {
  margin: 0 0 24px;
  font-size: 17px;
  font-weight: 500;
  line-height: 1.5;
  color: #f5f5f7;
  flex: 1;
}

.review-byline {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-top: 20px;
  border-top: 1px solid #333336;
}

.review-byline img {
  width: 32px;
  height: 32px;
  border-radius: 8px;
}

.review-byline div {
  display: flex;
  flex-direction: column;
}

.review-author {
  font-size: 14px;
  font-weight: 600;
  color: #f5f5f7;
}

.review-app {
  font-size: 12px;
  color: #86868b;
}

/* About */
.about {
  padding: 110px 0 120px;
  text-align: center;
}

.about p {
  max-width: 620px;
  margin: 0 auto;
  font-size: 18px;
}

/* Footer */
.site-footer {
  background: var(--bg-subtle);
  border-top: 1px solid var(--border);
  padding: 56px 0 28px;
}

.footer-top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 40px;
  padding-bottom: 32px;
}

.footer-brand .logo {
  font-size: 15px;
}

.footer-tagline {
  margin: 8px 0 0;
  max-width: 220px;
  font-size: 14px;
  color: var(--text-secondary);
}

.footer-col { min-width: 160px; }

.footer-legal { max-width: 260px; }

.footer-heading {
  margin: 0 0 16px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

.footer-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.footer-list a {
  font-size: 14px;
  color: var(--text-secondary);
}

.footer-list a:hover { color: var(--text); }

.footer-text {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-muted);
}

.footer-text a { color: var(--text-muted); }
.footer-text a:hover { color: var(--text); }

.footer-copyright {
  margin: 0;
  padding-top: 24px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-muted);
}

@media (max-width: 860px) {
  .app-grid, .app-grid.reverse {
    grid-template-columns: 1fr;
    gap: 40px;
  }
  .app-grid.reverse .app-copy { order: 1; }
  .app-grid.reverse .app-shots { order: 2; }
  .app-description { max-width: none; }
  .app-shots { grid-template-columns: repeat(4, 1fr); }
  .site-header nav a:not(:last-child) { display: none; }
}

@media (max-width: 640px) {
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  /* A 3rd item in a 2-column grid lands in column 1, flush left under the
     first stat — span it across both columns so it sits centered instead. */
  .stats-grid .stat:last-child:nth-child(odd) {
    grid-column: 1 / -1;
    border-left: none;
    padding-top: 16px;
    margin-top: 16px;
    border-top: 1px solid var(--border);
  }
  .feature-grid { grid-template-columns: 1fr; }
  .hero { padding: 88px 0 56px; }
  .app-section { padding: 72px 0; }
  /* Keep the same 4-up, staggered-height arrangement as desktop, just
     smaller — a flat 2x2 grid with no offset reads as flat and dull. */
  .app-shots { grid-template-columns: repeat(4, 1fr); gap: 8px; }
  .app-shots img { border-radius: 14px; }
  .app-shots img.raised { transform: translateY(-14px); }
  /* The scroll track bleeds out to the true screen edge on mobile (.wrap's
     side padding, and .review-grid's own padding, would otherwise hold the
     next/previous card's peek short of the actual edge, making it look
     like it's emerging from within the padding instead of sliding in from
     off-screen). The card itself stays narrower than the viewport though —
     making it full width too would grow it noticeably wider than before,
     which reads as a size change, not just "edge-to-edge scrolling". No
     fade mask here: arrows are already hidden on mobile, swiping alone
     drives the carousel, and a flat-cut peek (no fade) is itself a
     deliberate "there's more" cue many native carousels use as-is. */
  .review-carousel { margin: 0 -24px; }
  .review-grid { padding-left: 0; padding-right: 0; }
  .review-card { flex-basis: 82%; max-width: 82%; }
  .review-grid.fade-edges {
    -webkit-mask-image: none;
    mask-image: none;
  }
  .reviews { padding: 72px 0; }
  .concept-stats-grid { grid-template-columns: 1fr; gap: 16px; }
  .concept-stat { border-left: none; padding: 16px 0 0; border-top: 1px solid var(--border); }
  .concept-stat:first-child { border-top: none; padding-top: 0; }
  .concept-caption { font-size: 9.5px; }
}

/* Translucent materials (header, back-to-top, carousel arrows) fall back to
   solid surfaces for people who asked the system for less transparency, and
   get a defined border when more contrast is requested. */
@media (prefers-reduced-transparency: reduce) {
  .site-header,
  .back-to-top,
  .review-nav {
    background: var(--bg);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

@media (prefers-contrast: more) {
  .site-header { border-bottom-color: var(--text-secondary); }
  .back-to-top,
  .review-nav { border-color: var(--text-secondary); }
}
