/* ==========================================================================
   SnapCoach — main stylesheet

   Before adding or changing any @media rule, read ../BREAKPOINTS.md —
   this site's canonical device-width reference and breakpoint list, and
   the verification method for checking a new one is actually safe
   before committing to it (not just a round number picked by eye).
   ========================================================================== */

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg-l1);
  color: var(--text-white);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--fs-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

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

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

h1, h2, h3, p { margin: 0; }

.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--sp-md);
}

@media (min-width: 768px) {
  .container { padding: 0 var(--sp-xl); }
}

section { position: relative; }

/* ---- gradient wordmark / accent text -------------------------------- */
.gradient-text {
  background: var(--gradient-brand);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.script-accent {
  font-family: var(--font-script);
  font-weight: 400;
}

/* ---- ambient background blobs ---------------------------------------- */
.bg-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  opacity: 0.16;
  pointer-events: none;
  z-index: 0;
}

.bg-blob--coral { background: var(--coral); }
.bg-blob--gold { background: var(--gold); }
.bg-blob--violet { background: var(--violet); opacity: 0.12; }

/* ---- nav --------------------------------------------------------------- */
/* Transparent in its normal static flow — no background/blur there. Only
   the fixed/sticky state (.nav--sticky below) gets a background, since
   that's the state where the nav floats directly over scrolling page
   content and needs to stay legible.
   "Smart sticky" behavior, driven from js/main.js's nav scroll-state logic:
   the nav starts as a genuinely normal, in-flow, position:static element
   sitting at the top of the page — while its natural position is still
   on/above the viewport, it just scrolls
   away with the rest of the content like anything else, no fixed layer, no
   hide/show animation at all. Only once you've scrolled past its natural
   position AND then scroll back up does it switch to position:fixed (the
   .nav--sticky class below) and slide into view; scrolling down again while
   sticky hides it (.nav--hidden), scrolling up reveals it again — and
   scrolling all the way back up past its natural position drops
   .nav--sticky entirely, returning it to plain static flow.
   The two states deliberately do NOT share the same top spacing: the
   static state gets a 10px gap (margin-top) above the nav; the sticky
   state is flush against the viewport edge (top:0, no gap) — back to how
   it originally behaved before any top gap was added, on request. */
.nav {
  position: static;
  z-index: 100;
  padding: var(--sp-sm) 0;
  margin-top: 10px;
  transform: translateY(0);
  transition: transform 1s var(--ease-snap);
}

/* Mobile only: 15px more breathing room above the nav content specifically
   (var(--sp-sm) = 16px, so 31px total) — bottom/left/right stay at the
   shared value above. */
@media (max-width: 899px) {
  .nav { padding-top: 31px; }
}

.nav--sticky {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  margin-top: 0;
  /* Top 65%: its own linear fade, 95% opacity at the very top down to
     85% by the 65% mark. Bottom 35%: the same smoothstep curve used
     throughout this rule's history, rescaled to start from 0.85 (where
     the top fade now hands off) instead of the old 0.95 — same relative
     shape, scaled down by a factor of 0.85/0.95, so the two segments
     meet cleanly with no re-brightening blip at the seam.
     --bg-l1 (#0F1117) written out as rgb() so it can carry alpha here. */
  background: linear-gradient(
    to bottom,
    rgba(15, 17, 23, 0.95) 0%,
    rgba(15, 17, 23, 0.85) 65%,
    rgba(15, 17, 23, 0.80) 70%,
    rgba(15, 17, 23, 0.67) 75%,
    rgba(15, 17, 23, 0.49) 81%,
    rgba(15, 17, 23, 0.30) 86%,
    rgba(15, 17, 23, 0.13) 91%,
    rgba(15, 17, 23, 0.03) 96%,
    rgba(15, 17, 23, 0) 100%
  );
}

.nav--hidden {
  transform: translateY(-130%);
}

/* Reserves the exact vertical space the nav occupies in its normal static
   flow (see js/main.js) so that switching it to position:fixed doesn't
   cause the page content below it to jump upward to fill the gap it left.
   Only ever given real height while the nav is in its sticky/fixed state —
   in the static state, the nav is already providing that space itself, so
   this stays collapsed. No transition here: the height change happens
   exactly at the scroll position where the nav's natural edge already
   meets the viewport edge, so there's nothing visible to animate. */
.nav__spacer {
  height: 0;
}

/* Deliberately not using the shared .container here (max-width 1160px,
   centered) — Moto's own nav runs to within 32px of the true viewport edge
   with no width cap, which is what actually gives it that edge-to-edge look
   (not just tighter padding). The rest of the page keeps .container as-is;
   this is nav-only.
   Padding is mobile-first here (24px, matching .container's own mobile
   padding) rather than the fixed 56px this used to be at every width — on
   a phone, .nav__links is hidden (see the 700px breakpoint below), so the
   brand and CTA are the only two things in this grid with the middle
   column collapsed to zero width; 56px of padding on each side left too
   little room for both, so they overflowed their grid columns toward the
   center and collided. 56px only kicks back in at 768px+ (matching
   .container's own breakpoint), where .nav__links is already visible and
   provides real separation between brand and CTA regardless. */
.nav__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-md);
  padding: 0 var(--sp-md);
}

@media (min-width: 768px) {
  .nav__inner { padding: 0 56px; }
}

.nav__brand {
  grid-column: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.375rem;
  color: var(--text-white);
  justify-self: start;
}

.nav__brand img { width: 32px; height: 32px; border-radius: 9px; }

/* grid-column pinned explicitly on this, .nav__brand, and .nav__cta below —
   not just relying on source order — because display:none removes an
   element from grid item generation entirely below the 700px breakpoint,
   which otherwise breaks auto-placement: with only 2 boxes generated
   instead of 3, .nav__cta (the next item in DOM order) collapsed into
   *this* column instead of its own, sizing to its own content and sitting
   right next to the brand rather than flush against the right edge. */
.nav__links {
  grid-column: 2;
  display: none;
  align-items: center;
  gap: var(--sp-lg);
  justify-self: center;
}

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

.nav__link {
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--text-white);
  padding: 6px 0;
  transition: color 0.2s;
}

.nav__link:hover { color: var(--coral-light); }

.nav__link--dud {
  color: var(--text-muted);
  cursor: default;
  pointer-events: none;
}

.nav__cta { grid-column: 3; justify-self: end; }

/* ---- buttons ------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px 28px;
  border-radius: var(--r-button);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-small);
  border: none;
  cursor: pointer;
  white-space: nowrap;
  transition: transform 0.2s var(--ease-snap), box-shadow 0.2s var(--ease-snap), opacity 0.2s;
}

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

.btn--primary {
  background: var(--gradient-brand);
  color: #1a0d0f;
  box-shadow: 0 8px 24px -8px rgba(244, 91, 105, 0.5);
}

.btn--primary:hover { box-shadow: 0 10px 30px -6px rgba(244, 91, 105, 0.65); }

.btn--nav {
  padding: 10px 20px;
  font-size: 0.8125rem;
}

.btn--ghost {
  background: transparent;
  color: var(--text-white);
  border: 1px solid var(--card-stroke);
}

.btn:disabled { opacity: 0.5; cursor: default; }

/* ---- glass + shimmer CTA (nav bar only) --------------------------------- */
/* Real glassmorphism (translucent fill + backdrop-blur) with a rotating
   brand-gradient ring around the border — same mechanism as a "shimmering
   border" trick seen elsewhere on the web: a conic-gradient masked down to
   just the border hairline via the standard content-box/border-box exclude
   double-mask, its angle driven by an animated @property so it can be
   transitioned directly instead of needing an oversized rotating element. */
@property --shimmer-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.btn--glass {
  position: relative;
  background: rgba(248, 250, 252, 0.06);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--text-white);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 12px rgba(255, 255, 255, 0.08);
}

.btn--glass::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(
    from var(--shimmer-angle),
    rgba(244, 91, 105, 0.15),
    var(--coral) 15%,
    var(--gold) 30%,
    rgba(255, 209, 102, 0.15) 45%,
    rgba(244, 91, 105, 0.15) 70%,
    var(--coral) 85%,
    rgba(255, 209, 102, 0.15) 100%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  animation: btn-shimmer-spin 4s linear infinite;
  pointer-events: none;
}

.btn--glass:hover::before,
.btn--glass:focus-visible::before {
  animation-play-state: paused;
}

@keyframes btn-shimmer-spin {
  to { --shimmer-angle: 360deg; }
}

/* ---- glass CTA, static white stroke (hero) ------------------------------- */
/* Same glass recipe as .btn--glass above (translucent fill + backdrop-blur
   + soft inset highlight), sized to match the nav's own CTA exactly
   (.btn--nav's padding/font-size below) rather than the larger base .btn
   sizing. A fully borderless version (tried first) didn't read as glass at
   all — a stroke is what catches the light — so this keeps .btn--glass's
   masked-border trick but swaps the rotating coral/gold conic-gradient for
   a static (no animation, no @property angle) diagonal white-to-transparent
   one. Still the section's only *brand-colored* gradient stays on
   "still you" — this stroke is white, not coral/gold. A standalone rule
   rather than reusing .btn--glass, so the nav's shimmer version is
   untouched. */
.btn--glass-plain {
  position: relative;
  padding: 10px 20px;
  font-size: 0.8125rem;
  background: rgba(248, 250, 252, 0.06);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--text-white);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 12px rgba(255, 255, 255, 0.08);
}

.btn--glass-plain::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1.5px;
  background: linear-gradient(
    135deg,
    rgba(255, 255, 255, 0.55),
    rgba(255, 255, 255, 0.08) 50%,
    rgba(255, 255, 255, 0.4)
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

/* ---- eyebrow / section heads -------------------------------------------- */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-eyebrow);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--coral-light);
  margin-bottom: var(--sp-sm);
}

.eyebrow::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--gradient-brand);
}

.section-head {
  max-width: 640px;
  margin: 0 auto var(--sp-xxl);
  text-align: center;
}

.section-head--left { margin-left: 0; text-align: left; }

.section-title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-h2);
  line-height: 1.1;
  color: var(--text-white);
}

.section-sub {
  margin-top: var(--sp-sm);
  font-size: var(--fs-body-lg);
  color: var(--text-muted);
  line-height: 1.55;
}

/* ---- hero ---------------------------------------------------------------- */
/* .hero-story (index.html) wraps this section and .screens-section
   together — no static CSS of its own here on purpose. Below 1360px it's
   completely inert (a <div> around two <section>s changes nothing). At
   1360px+ it becomes a pinned scroll-scrubbed transition between the two
   sections, but every bit of that (position/inset/z-index/transform on
   .hero-story, .hero, .screens-section, and the hero phone) is applied at
   runtime via gsap.set() inside a ScrollTrigger.matchMedia block in
   js/main.js — deliberately not static CSS, so it reverts to nothing (zero
   leftover inline styles) the instant the viewport crosses back below
   1360px. Read that block for the real mechanics; nothing to add here. */
.hero {
  /* Was 168px, back when .nav was always a fixed overlay and this padding
     was the *entire* space reserved for it (nav floating on top of the
     upper portion). Now that .nav genuinely occupies its own ~91.5px of
     real document flow (in its static "natural" state, or via
     .nav__spacer while sticky — see css/style.css's nav section), that
     part of the old 168px would double up with the nav's own real space.
     76.5px = 168 - 91.5 was the first-pass value from that math; retuned
     down to 46.5px on a direct visual call once the hero section itself
     was in front of us — the nav/hero gap read as too much space at that
     first-pass number, at every breakpoint (there's no responsive
     override here).

     No longer overflow:hidden (removed — was here to hard-clip the
     ambient .bg-blob glows, back when the coral blob bled in from
     top:-120px above .hero's own top edge; both blobs now sit fully
     within bounds so that reason is gone). Keeping it turned out to be
     actively harmful once .hero__glow's box was widened to fix the
     hard-edge bug below it: on a shorter viewport the widened glow box
     pokes a little past .hero's own bottom edge, and overflow:hidden
     was chopping it off right there — trading the old hard line under
     the phone for a new one at the section boundary. Confirmed via
     headless-Chromium measurement across several viewport heights
     before removing this. */
  padding: 46.5px 0 var(--sp-xxxl);
}

/* Tablet through true desktop (600px and up — no upper bound; originally
   capped at 1439px on the assumption desktop wouldn't need it, widened to
   uncapped on request since the user wants the same vertical-centering
   treatment there too): symmetric top/bottom padding alone wasn't the
   actual fix — .hero has no min-height at all, so it only ever wraps
   tightly to its content's own height regardless of padding, and the real
   dead space was further down the page (between the section's
   tight-wrapped bottom and "Why People Love It" below it), not something
   padding tuning could reach. What "centered in the screen" actually
   needs is height to center *within* — .hero gets a min-height filling
   the visible viewport below the nav (91.5px, the nav's own measured
   natural height, same figure referenced in this rule's own history
   above), and centers its content block vertically inside that with flex.
   100dvh (dynamic viewport height) is preferred where supported since
   100vh doesn't account for mobile/tablet Safari's collapsing toolbar;
   the plain 100vh line first is the fallback for browsers without dvh
   support.
   600px is a judgment-call lower bound, not a device-tested one — chosen
   with real margin on both sides: comfortably below the smallest tablet
   actually confirmed (iPad mini portrait at 744px — a ~144px cushion) and
   comfortably above the largest common real phone width in portrait
   (~430px on the biggest iPhones, up to maybe ~480px on the largest
   Android phones/phablets — a ~120px+ cushion), so normal phone rotation
   or slight viewport variance doesn't risk drifting into this range.
   600px also happens to be a conventional phone/tablet breakpoint
   elsewhere (e.g. Material Design's own sm breakpoint), not just a number
   picked in isolation. This range overlaps the existing ≤899px
   mobile-only rules (phone-before-text order, nav padding, right-aligned
   CTA, etc.) on purpose — both apply together there, that's the intent.
   No upper bound now, so this also governs true desktop: past
   --container-max: 1160px the content's own width/height stop changing
   with the viewport, but the section itself still fills and centers
   within whatever viewport height is available, same as at tablet
   widths. */
@media (min-width: 600px) {
  .hero {
    min-height: calc(100vh - 91.5px);
    min-height: calc(100dvh - 91.5px);
    display: flex;
    align-items: center;
    padding-bottom: 46.5px;
  }
}

.hero__grid {
  display: grid;
  gap: var(--sp-xxl);
  align-items: center;
  position: relative;
  z-index: 1;
}

@media (min-width: 900px) {
  .hero__grid { grid-template-columns: 1.05fr 0.95fr; gap: var(--sp-xl); }
}

/* "Coming Soon" and "Take a Look Inside" only — the shared .eyebrow stays
   coral everywhere else (Why/How sections etc.); the dot keeps its
   gradient via .eyebrow::before, untouched. */
.hero .eyebrow,
.screens-section__head .eyebrow { color: var(--text-white); }

/* This section's title/subtitle match .hero__title/.hero__sub's values
   exactly, on request — not just size, every value copied over (weight,
   line-height, letter-spacing, max-width, margin-top). Overrides the
   shared .section-title/.section-sub this head still also carries
   (var(--fs-h2)/var(--fs-body-lg)), scoped to this section only — every
   other .section-title/.section-sub on the page (Why, How It Works,
   etc.) is untouched. */
.screens-section__head .section-title {
  /* var(--fs-hero), reduced 20%, then another 10% on desktop (0.8 × 0.9 =
     0.72) — but that reduction is desktop-only now: mobile/portrait-
     tablet each bump back up from it on request (mobile +10% off 0.72,
     tablet +20% off 0.72), since the wrapping problem that originally
     drove the 0.72 figure was specific to the desktop text column's
     width. Mobile got another +10% on top of that after (0.72 × 1.1 ×
     1.1 = 0.8712). Mobile-first here: this base rule is the mobile
     value, overridden up for tablet then back down for desktop below. */
  font-size: calc(var(--fs-hero) * 0.8712);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.01em;
}

/* Ceiling raised from 899px to 1359px 2026-08-24 (floor already lowered
   768px→700px earlier the same day) — see BREAKPOINTS.md for why 1360px
   is this site's real "desktop" floor, not 900px. This range now covers
   every tablet width, both orientations, not just portrait — the old
   name "portrait tablet" undersold it even before this change, since
   1024-1359px is where tablet *landscape* widths (iPad mini landscape
   1133, iPad Air landscape 1180, iPad Pro 11" landscape 1194, common
   1280×800/1366×768 laptop viewports) also live, and all of them need
   this same stacked, title-width-matched treatment now that the true
   2-column desktop grid only kicks in past 1360px. */
@media (min-width: 700px) and (max-width: 1359px) {
  .screens-section__head .section-title { font-size: calc(var(--fs-hero) * 0.864); }
}

@media (min-width: 1360px) {
  .screens-section__head .section-title { font-size: calc(var(--fs-hero) * 0.72); }
}

.screens-section__head .section-sub {
  margin-top: var(--sp-md);
  font-size: var(--fs-hero-sub);
  max-width: 480px;
  line-height: 1.6;
}

/* Tablet (both orientations, 700-1359px — see BREAKPOINTS.md) only: the
   title ("Simple on purpose") is short enough that it never actually
   wraps at this column's own width (480px) — its real unwrapped text
   width tracks --fs-hero's clamp() as viewport grows, running ~330px at
   700px up to ~426px at 1359px (measured directly in headless Chromium
   at every breakpoint checkpoint in BREAKPOINTS.md's device table). On
   request, the subtitle wraps at that same narrower point instead of
   the full 480px column, so the two visually line up. A single fixed
   max-width can't track a clamp()'d value — CSS has no way to size one
   element off another's live text metrics without JS, which nothing
   else on this page uses for layout — so this is split into two
   sub-bands, each with its own fixed approximation centered on that
   sub-band's actual title-width range (below). Re-measure both if the
   title copy or its font-size multiplier (see .screens-section__head
   .section-title) ever changes.
   History: started as a 768-899px-only rule with a single 355px cap
   (floor lowered to 700px 2026-08-24 to catch iPad mini portrait at
   744px, which the original 768px floor missed). Same day, ceiling
   raised 768px→1359px after a second, more serious bug surfaced: iPad
   Pro 13" portrait (1024px) was hitting the old 900px "desktop"
   2-column grid breakpoint below, whose fixed 828px-wide 3-phone row
   left only ~140-190px for this text column at 900-1250px viewports —
   not just a bad wrap, genuine invisible content clipping (real
   overflow past the section's box, hidden by body's overflow-x:hidden).
   Fix was to raise the 2-column grid's own breakpoint to 1360px
   (verified overflow-free with margin — see .screens-grid below), which
   meant this stacked/matched-width treatment now needed to cover the
   whole 700-1359px range instead of stopping at 899px — and a range
   that wide needed splitting into two bands rather than stretching one
   approximation across a ~330-426px title-width spread. */
@media (min-width: 700px) and (max-width: 1023px) {
  /* Title runs ~330-384px across this sub-band (700-1023px); 357px is
     that range's midpoint. */
  .screens-section__head .section-sub { max-width: 357px; }
}

@media (min-width: 1024px) and (max-width: 1359px) {
  /* Title runs ~385-426px across this sub-band (1024-1359px); 406px is
     that range's midpoint. */
  .screens-section__head .section-sub { max-width: 406px; }
}

.hero__title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-hero);
  line-height: 1.05;
  letter-spacing: -0.01em;
}

/* Same fix as .intro-curtain__tagline .script-accent (see that rule's own
   comment): Fasthand needs its own larger, em-relative size and generous
   line-height or its ascenders/descenders clip — sized here as 1.4em of
   the headline (bigger than the header font, on purpose) rather than
   matching it 1:1. The negative margin cancels the extra line-height back
   out of the surrounding line's flow instead of pushing it apart —
   requires display:inline-block, since vertical margins are ignored on
   plain inline elements. */
.hero__title .script-accent {
  display: inline-block;
  font-size: 1.12em;
  line-height: 1.3;
  padding: 0 0.05em;
  margin: -0.44em 0;
}

.hero__sub {
  margin-top: var(--sp-md);
  font-size: var(--fs-hero-sub);
  color: var(--text-muted);
  max-width: 480px;
  line-height: 1.6;
}

/* Below 900px the hero is stacked single-column (full container width
   available), so the 480px cap above — sized for the narrower text column
   in the two-column desktop layout — just wraps the text early for no
   reason. Let it flow naturally at this size and below. */
@media (max-width: 899px) {
  .hero__sub { max-width: none; }
}

.hero__form {
  margin-top: var(--sp-lg);
  display: flex;
  gap: 12px;
  max-width: 460px;
  flex-wrap: wrap;
}

/* Same reasoning as .hero__sub above: the 460px cap stops the row well
   short of the full container width once the hero is stacked, leaving it
   looking like it "stops halfway." .hero__input already has flex:1 1 220px
   (grow enabled), so removing the cap here is enough on its own for the
   input to stretch and fill the freed-up width, button included. */
@media (max-width: 899px) {
  .hero__form { max-width: none; }
}

/* Mobile only: the CTA sits right-aligned on its row/wrapped line instead
   of left, while the input stays put — a plain auto-margin push rather
   than reversing the whole row (which would also flip the input). */
@media (max-width: 899px) {
  .hero__form .btn--glass-plain { margin-left: auto; }
}

/* Gradient outline (in place of the flat .card-stroke border) to set the
   input apart from the glass CTA beside it — the button's stroke is white,
   the brand gradient only shows up here. Pseudo-elements don't render on
   <input>, so this can't reuse the .btn--glass::before mask trick; the
   standard double-background padding-box/border-box layering does the
   same job on a real form control. Subtle at rest, fuller-strength on
   focus. Padding/font-size sized to land at the same height as
   .btn--glass-plain beside it (both landing around ~39-40px tall) rather
   than the taller base .btn sizing. */
/* display:flex + align-items:center vertically centers the text/placeholder
   reliably — the padding math alone (9px top/bottom, symmetric) should
   center it in theory, but browsers don't always center a line box that
   tall/that font evenly within equal padding; flex alignment sidesteps
   that entirely by centering the actual rendered content box instead. */
.hero__input {
  display: flex;
  align-items: center;
  flex: 1 1 220px;
  min-width: 0;
  padding: 9px 16px;
  border-radius: var(--r-input);
  border: 1.5px solid transparent;
  background:
    linear-gradient(var(--surface-l3), var(--surface-l3)) padding-box,
    linear-gradient(90deg, rgba(244, 91, 105, 0.35), rgba(255, 209, 102, 0.35)) border-box;
  color: var(--text-white);
  font-family: var(--font-body);
  font-size: 0.8125rem;
}

.hero__input::placeholder {
  font-style: italic;
  color: var(--text-dim);
}

.hero__input:focus {
  outline: none;
  background:
    linear-gradient(var(--surface-l3), var(--surface-l3)) padding-box,
    linear-gradient(90deg, var(--coral), var(--gold)) border-box;
  box-shadow: 0 0 0 3px rgba(244, 91, 105, 0.18);
}

.hero__note {
  margin-top: var(--sp-sm);
  font-size: 0.8125rem;
  color: var(--text-dim);
}

.hero__visual { position: relative; }

/* Mobile/small-tablet: phone renders above the hero text instead of after
   it. .hero__grid is already display:grid at every width (it just gains
   explicit columns at 900px, see above) so `order` applies here too — this
   only changes visual order, DOM order is untouched. */
@media (max-width: 899px) {
  .hero__visual { order: -1; }
}

.phone-frame {
  position: relative;
  border-radius: 44px;
  overflow: hidden;
  box-shadow: 0 40px 80px -24px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--card-stroke);
  max-width: 352px; /* was 340px, +15% to 391px, then -10% back down to here */
  margin: 0 auto;
}

.phone-frame img { width: 100%; display: block; }

/* hero's phone-frame image is already phone-shaped with a transparent
   background, so it needs none of the rectangular clip/border/shadow the
   shared .phone-frame adds for plain rectangular screenshots elsewhere. */
.hero__visual .phone-frame {
  border-radius: 0;
  overflow: visible;
  box-shadow: none;
}

/* Right-aligned instead of centered, but only once .hero__grid is
   side-by-side (900px+, same breakpoint .hero__divider below keys off of)
   — gives the phone's right edge a fixed, predictable position (flush with
   the container's own right inner edge) so .hero__divider can end exactly
   there without fragile centered-content math. Mobile/tablet-stacked keeps
   the phone centered as before. */
@media (min-width: 900px) {
  .hero__visual .phone-frame { margin: 0 0 0 auto; }
}

.hero__glow {
  position: absolute;
  /* Widened from -60px and the blur raised from 30px to 90px (matching the
     ambient .bg-blob glow elsewhere in .hero) on purpose: at the old
     values the radial-gradient's own transparent falloff landed close
     enough to this box's edge that the blur couldn't fully soften it,
     leaving a visible hard-edged rectangle behind the phone mockup once
     there was empty space directly below it to show it against (this
     became obvious after the "Take a Look Inside" section moved here).
     The extra inset gives the stronger blur room to feather out to
     nothing before it ever reaches the box boundary. */
  inset: -160px;
  background: radial-gradient(ellipse at center, rgba(244, 91, 105, 0.28), rgba(255, 209, 102, 0.12) 45%, transparent 70%);
  filter: blur(90px);
  z-index: -1;
}

/* Subtle accent line "explaining" the phone mockup's flat bottom crop —
   desktop/tablet only (900px+, matching the point .hero__grid actually
   goes side-by-side; below that the layout is stacked and there's no
   text-left/phone-right pair to anchor it to anyway).
   A child of .hero__visual rather than .hero__grid, and deliberately not
   using .hero__grid's own left:0/right:0/bottom:0 (a shared grid row is
   height-matched via .hero__grid's align-items:center, so "bottom of the
   row" is the *taller* column's bottom — almost always the text stack,
   not the phone — which would float the line well below the phone
   instead of against it). Anchoring to .hero__visual itself instead:
   .hero__visual doesn't stretch to the row's height (align-items:center
   sizes it to its own content), so bottom:0 here is genuinely the phone's
   own bottom edge. right:0 is the phone's right edge too, now that
   .phone-frame is right-aligned above. The left edge has to reach all the
   way back to .hero__grid's left edge (the text column's start), which
   .hero__visual's own box doesn't reach on its own — the negative `left`
   below reconstructs that distance from .hero__grid's own
   grid-template-columns (1.05fr / 0.95fr, var(--sp-xl) gap): at a shared
   fr-basis, column 1's width is column 2's width times (1.05/0.95), so
   width-of-column-1-plus-gap expressed as a negative offset from this
   element's own 100% (= column 2's width) is
   -(100% * 1.05/0.95 + gap). If .hero__grid's column ratio or gap ever
   changes, this multiplier/gap need to change with it.

   Briefly removed 2026-08-24 for reading as a hard cutoff across the
   hero's ambient glow, then restored same day — the actual clipping bug
   was .hero's own overflow:hidden (removed above) plus .hero__glow's
   box being too tight (widened above); this line was never the real
   cause, it's a plain 1px hairline that fades to transparent at both
   ends and sits on z-index:auto, so the glow (z-index:-1, already fixed
   to reach full transparency well clear of any edge) renders behind it
   and continues past it exactly as it does past everything else in the
   hero. Don't remove this again to chase a glow-continuity bug — check
   .hero__glow/.hero's own overflow first. */
.hero__divider { display: none; }

@media (min-width: 900px) {
  .hero__divider {
    display: block;
    position: absolute;
    left: calc(-1 * (100% * 1.10526 + var(--sp-xl)));
    right: 0;
    bottom: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--card-stroke) 15%, var(--card-stroke) 85%, transparent);
  }
}

/* ---- why cards ------------------------------------------------------------ */
.why-grid {
  display: grid;
  gap: var(--sp-md);
  grid-template-columns: repeat(2, 1fr);
  position: relative;
  z-index: 1;
}

@media (min-width: 900px) {
  .why-grid { grid-template-columns: repeat(4, 1fr); }
}

.why-card {
  background: var(--surface-l2);
  border: 1px solid var(--card-stroke);
  border-radius: var(--r-card);
  padding: var(--sp-lg) var(--sp-md);
}

.why-card__icon {
  width: 48px;
  height: 48px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gradient-brand);
  margin-bottom: var(--sp-md);
}

.why-card__icon svg { width: 24px; height: 24px; }

.why-card__title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.0625rem;
  margin-bottom: 8px;
}

.why-card__text {
  font-size: var(--fs-small);
  color: var(--text-muted);
  line-height: 1.55;
}

/* ---- how it works ---------------------------------------------------------- */
#how-it-works {
  scroll-margin-top: 100px;
}

.steps {
  display: grid;
  gap: var(--sp-md);
  grid-template-columns: 1fr;
  position: relative;
  z-index: 1;
}

@media (min-width: 900px) {
  .steps { grid-template-columns: repeat(4, 1fr); }
}

.step {
  position: relative;
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--card-stroke);
}

.step__num {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 2.5rem;
  line-height: 1;
  background: var(--gradient-brand);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  opacity: 0.9;
}

.step__title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.125rem;
  margin: var(--sp-sm) 0 8px;
}

.step__text {
  font-size: var(--fs-small);
  color: var(--text-muted);
  line-height: 1.55;
}

/* ---- prompt-to-preview reveal ------------------------------------------------ */
.reveal {
  display: grid;
  gap: var(--sp-xl);
  align-items: center;
}

@media (min-width: 900px) {
  .reveal { grid-template-columns: 0.9fr 1.1fr; }
}

.reveal__prompt-card {
  background: var(--surface-l3);
  border: 1px solid var(--card-stroke);
  border-radius: var(--r-card);
  padding: var(--sp-lg);
}

.reveal__prompt-label {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--violet-light);
  font-size: var(--fs-small);
  font-weight: 600;
  margin-bottom: var(--sp-sm);
}

.reveal__prompt-text {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.375rem;
  color: var(--text-white);
}

.reveal__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: var(--sp-md) 0;
  color: var(--text-dim);
}

.reveal__stage {
  position: relative;
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.55);
  aspect-ratio: 3 / 4;
  width: 100%;
  max-width: 420px;
  margin: 0 auto;
}

.reveal__stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.reveal__wipe {
  clip-path: inset(0 0 0 100%);
}

.reveal__badge {
  position: absolute;
  bottom: var(--sp-sm);
  left: var(--sp-sm);
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  z-index: 2;
}

/* ---- style gallery rail ----------------------------------------------------- */
.gallery-rail-wrap {
  overflow: hidden;
}

.gallery-rail {
  display: flex;
  gap: var(--sp-md);
  width: max-content;
  padding: 0 var(--sp-md);
}

.gallery-card {
  position: relative;
  width: 260px;
  aspect-ratio: 3 / 4;
  border-radius: var(--r-card);
  overflow: hidden;
  flex-shrink: 0;
  box-shadow: 0 20px 40px -16px rgba(0, 0, 0, 0.5);
}

.gallery-card img { width: 100%; height: 100%; object-fit: cover; }

.gallery-card__label {
  position: absolute;
  left: var(--sp-sm);
  bottom: var(--sp-sm);
  right: var(--sp-sm);
}

.gallery-card__label::before {
  content: '';
  position: absolute;
  inset: -40px -12px -12px -12px;
  background: linear-gradient(to top, rgba(0,0,0,0.75), transparent);
  z-index: 0;
}

.gallery-card__name {
  position: relative;
  z-index: 1;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.0625rem;
}

.gallery-card__desc {
  position: relative;
  z-index: 1;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* ---- product screenshots ----------------------------------------------------- */
/* Same vertical-centering treatment as .hero (see that rule's own history
   comment for the full reasoning on the 91.5px/600px figures) — fills and
   centers within the viewport below the nav when scrolled to, rather than
   just wrapping tightly to its content. This is also the resting state a
   future scroll-pin animation for this section gets built on top of.
   Deliberately NOT using the shared .container class (no max-width/
   centering) — this section runs the full width of the screen, same
   reasoning as .nav__inner going edge-to-edge instead of using .container
   — so it carries its own copy of .container's own responsive side
   padding instead. */
.screens-section {
  padding: var(--sp-xxxl) var(--sp-md);
}

@media (min-width: 768px) {
  /* var(--sp-xl) (40px) + 80px, then -20px = 100px, then trimmed another
     15px each side (85px) to free width for the text column — the
     title ("Simple on purpose") was wrapping to 2 lines by ~23px at its
     current size, and font-size was off the table, so this is the free
     lever: shrinking padding on both sides adds 2x that back to the 1fr
     text column (the phone row's own column is fixed/max-content, so all
     of the freed width flows to text, none of it to the phones). */
  .screens-section { padding-left: calc(var(--sp-xl) + 45px); padding-right: calc(var(--sp-xl) + 45px); }
}

@media (min-width: 600px) {
  .screens-section {
    min-height: calc(100vh - 91.5px);
    min-height: calc(100dvh - 91.5px);
    display: flex;
    align-items: center;
  }
}

/* Mirrors .hero__grid — two columns, vertically centered — but flipped:
   the phones are the dominant (wider) left column here instead of the
   narrower right one .hero__visual gets, and the text is the narrow
   column instead of the dominant one. DOM order is text-then-screens (so
   the mobile/tablet single-column stack below 1360px — see
   BREAKPOINTS.md — already reads text-first, phones-second with zero
   extra CSS); the grid-column pins below are what flip that to
   phones-left/text-right once there's room for two columns side by side
   — same explicit-pinning approach used for .nav__inner's
   items, rather than reordering the DOM itself. Both items also get an
   explicit grid-row:1, not just grid-column — pinning only the column
   and leaving row on auto let the default *sparse* auto-placement put
   them in two separate rows instead of side by side (the text, placed
   first in DOM order, claimed row 1's column 2; sparse placement then
   advances the cursor forward for the next item rather than backfilling
   row 1's now-empty column 1, so the phones landed in row 2 alone) —
   confirmed via computed gridTemplateRows showing two real row tracks
   instead of one. That silently defeated align-items:center too, since
   each item was centering only within its own single-item row. Pinning
   both to row 1 explicitly is what actually keeps them in one shared
   row so they center against each other correctly. */
.screens-grid {
  display: grid;
  /* Mobile/portrait-tablet vertical gap between the stacked text block and
     the phone row below it: --sp-xxl (48px) reduced a little to --sp-xl
     (40px), then another 20% on request (40 × 0.8 = 32px). */
  gap: calc(var(--sp-xl) * 0.8);
  align-items: center;
  /* .screens-section is display:flex (see above, for the hero-style
     vertical centering) — a flex row's lone child sizes to its own
     content-width by default, it doesn't stretch to fill the row. Without
     this, the section going edge-to-edge wouldn't actually hand any of
     that extra width to the text column below (1fr has nothing to
     distribute against if this box never grows past its content). */
  width: 100%;
}

/* Breakpoint raised 900px→1360px 2026-08-24 — see BREAKPOINTS.md. At
   900px this 2-column switch had no real room: the phone row's
   max-content width (828px, fixed regardless of viewport) plus this
   gap (60px) plus the 1fr text column's own minimum left the grid
   demanding more width than the viewport had from 900px up through
   ~1250px, and the browser doesn't clip grid overflow by default — the
   overflow was silently swallowed by body's overflow-x:hidden instead,
   so the text column's content was genuinely, invisibly cut off (real
   bug, not just a bad wrap — found via a real iPad Pro 13" portrait
   Safari screenshot at 1024×1366, where "Simple on purpose" and its
   subtitle both rendered truncated). Verified in headless Chromium
   (grid.scrollWidth vs offsetWidth) that overflow first reaches exactly
   0 at 1250px; 1360px was picked as the real breakpoint to leave
   comfortable margin above that floor, landing the text column at
   ~300px+ instead of ~190px right at the edge. */
@media (min-width: 1360px) {
  /* Back to max-content (not a fixed fr ratio) specifically so the gap
     between phones and the header/subtitle's own width are linked: with
     an fr ratio, shrinking the phone row's internal gap only made the
     phones themselves a little wider (the dominant column's own width
     was fixed by its fr share regardless of what happened inside it) —
     it never gave anything back to the text column. max-content sizes
     the dominant column to exactly what the phone row needs (phones +
     gaps), so a smaller gap shrinks the column itself, and the 1fr text
     column picks up exactly what that frees. */
  /* Gap between the phone row and the text column: var(--sp-xl) (40px)
     + 40px, then -20px on request = 60px. */
  .screens-grid { grid-template-columns: max-content 1fr; gap: calc(var(--sp-xl) + 20px); }
  .screens-section__head { grid-column: 2; grid-row: 1; }
  .screens { grid-column: 1; grid-row: 1; }
}

.screens {
  display: grid;
  gap: var(--sp-lg);
  grid-template-columns: 1fr;
}

@media (min-width: 700px) {
  .screens { grid-template-columns: repeat(3, 1fr); }
}

/* Gap between the 3 phones tightened down at 1360px+ (breakpoint raised
   from 900px 2026-08-24, see the matching note on .screens-grid above)
   from the base 32px — this is also the lever that gives the
   header/subtitle column more room now that the dominant column above
   is max-content (see that rule's comment): a bigger gap here eats into
   that room, a smaller one frees more of it up. var(--sp-sm) (16px) +
   20px on request = 36px. */
@media (min-width: 1360px) {
  .screens { gap: 36px; }
}

/* min-width:0 (grid items default to min-width:auto, which effectively
   means "never shrink below your content's width") is what lets this
   card's own width genuinely follow its 1fr grid track instead of being
   forced wider by the caption text's unwrapped line length (e.g.
   "Gallery — every photo, beautifully kept" as one line) — confirmed via
   a real, reproduced bug: without this, that unwrapped caption width was
   what .screens-grid's dominant column sized itself against, not the
   phone, pushing the whole row past the section's own bounds
   (document.documentElement.scrollWidth genuinely exceeded the viewport
   width). width:100% is belt-and-suspenders on top of the default grid
   stretch behavior — makes it explicit rather than relying on nothing
   overriding the default, in case that default isn't holding in every
   browser. */
.screen-card { text-align: center; min-width: 0; width: 100%; }

/* Same "already phone-shaped, transparent background" reasoning as
   .hero__visual .phone-frame — the shared .phone-frame rounded-card
   treatment (border-radius + overflow:hidden + drop-shadow, meant for
   plain rectangular screenshots) is turned off the same way it is there.
   width:100% so the actual image grows with its column, not just an
   invisible box around a size-capped image — but max-width is back
   (252px: 300px reduced 30% to 210px, then increased 20% back up to
   252px, both on request) rather than none: .screens-grid's dominant
   column above needs a concrete max-content size to measure the phone
   row against, which an unbounded image can't give it (its "natural"
   size would just be the source file's own raw pixel dimensions). */
.screen-card .phone-frame {
  max-width: 252px;
  width: 100%;
  /* Centered by default (mobile/portrait-tablet): .screen-card is full
     section width there (single-column stack), much wider than this
     252px cap, so without margin:auto the phone sat flush against the
     card's left edge instead of centered — confirmed as the actual cause
     of the left-aligned look reported on mobile. Reset to margin:0 at
     900px+ below, where .screen-card's own width is capped to match the
     phone exactly (see that rule's comment) — auto-centering inside an
     already-equal-width box would just reintroduce the sub-pixel
     card/caption mismatch that was fixed earlier. */
  margin: 0 auto;
  border-radius: 0;
  overflow: visible;
  box-shadow: none;
}

/* Breakpoint moved 900px→1360px 2026-08-24, kept in sync with
   .screens-grid's own max-content switch below (see that rule's
   comment) — .screen-card is only exactly phone-width once the grid
   actually caps its dominant column that way, which now happens at
   1360px, not 900px. Leaving this at 900px would have re-broken the
   centering bug described above across the newly-stacked 900-1359px
   range (phone-frame flush-left inside a column much wider than its own
   252px cap). */
@media (min-width: 1360px) {
  .screen-card .phone-frame { margin: 0; }
}

.screen-card__caption {
  margin-top: var(--sp-sm);
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-small);
  color: var(--text-muted);
  /* No max-width on mobile/portrait-tablet — .screen-card is full section
     width there (single-column stack), and .screen-card's own
     text-align:center already centers this full-width paragraph's text
     without needing a width cap. Adding one without margin:auto (same
     mistake .phone-frame had) left the caption's *box* flush left even
     though its text looked centered within that narrower box — visible
     as the caption reading off-center under the (now-centered) phone. */
}

/* Breakpoint moved 900px→1360px 2026-08-24 — same sync reason as
   .phone-frame's margin rule above. */
@media (min-width: 1360px) {
  /* Matches .phone-frame's own max-width exactly (kept in sync through
     every resize since — currently 252px). Not a %, which found a real
     (if small) bug: a percentage max-width is ignored for grid
     *intrinsic*-sizing purposes, so it never actually stopped the
     caption's own unwrapped text width from counting toward
     .screens-grid's max-content dominant column (see that rule above)
     when a caption's unwrapped line was slightly wider than the phone —
     confirmed via measurement, each column was resolving ~5px wider than
     the phone itself as a result. An absolute px cap is what actually
     constrains max-content sizing, so this now genuinely can't exceed
     the phone's width. */
  .screen-card__caption { max-width: 252px; }
}

/* ---- merged CTA + footer panel ---------------------------------------------- */
/* One full-width panel: an organic top edge (via an SVG cap painted in the
   page background color, sitting inside the panel's own box) rather than a
   plain rounded corner, CTA content up top and footer content below,
   separated by full-bleed divider lines instead of being two separate
   cards. Sits as a direct child of <body> (not inside .container) so its
   background spans edge-to-edge; the .container divs nested inside still
   center/constrain the actual content the normal way.

   Background is deliberately background-attachment:fixed (bg pinned to the
   viewport, content scrolls over/reveals it) — degrades gracefully to a
   normal scrolling background on older iOS Safari, which never supported
   fixed attachment reliably; not worth a JS-driven substitute for that. */
.footer-panel {
  position: relative;
  overflow: hidden;
  background-color: var(--surface-l2);
  background-image: radial-gradient(ellipse 900px 520px at 50% 20%, rgba(244, 91, 105, 0.22), transparent 65%);
  background-repeat: no-repeat;
  background-attachment: fixed;
  margin-top: var(--sp-xxxl);
}

/* This panel's own background reads as a visibly bounded card, unlike other
   sections which blend into the page background — the same --sp-md side
   padding used everywhere else reads as noticeably tighter here, so it gets
   a bit more room specifically inside this panel. */
.footer-panel .container {
  padding-left: var(--sp-lg);
  padding-right: var(--sp-lg);
}

@media (min-width: 768px) {
  .footer-panel .container {
    padding-left: var(--sp-xl);
    padding-right: var(--sp-xl);
  }
}

.footer-panel__cap {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 90px;
  display: block;
}

.footer-panel__cap path {
  fill: var(--bg-l1);
}

.footer-panel__inner {
  position: relative;
  z-index: 1;
}

.footer-panel__cta {
  text-align: center;
  padding-top: calc(90px + var(--sp-xl));
  padding-bottom: var(--sp-xxl);
}

.cta-band__title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-h2);
  max-width: 560px;
  margin: 0 auto;
}

.cta-band__form {
  margin: var(--sp-lg) auto 0;
  display: flex;
  gap: 12px;
  max-width: 460px;
  flex-wrap: wrap;
  justify-content: center;
}

.cta-band__form .hero__input { flex: 1 1 220px; }

.footer-divider {
  border-top: 1px solid var(--card-stroke);
  position: relative;
  z-index: 1;
}

.footer__top {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-lg);
  justify-content: space-between;
  align-items: flex-start;
  padding: var(--sp-xl) 0;
}

/* On pages with no CTA block above it (legal pages), footer__top is the
   first thing inside the panel, so it needs clearance under the cap itself.
   This targets a direct child of .footer-panel specifically — on the
   homepage, footer__top sits one level deeper (inside .footer-panel__inner,
   added for the fixed-background fix), so this can never match there. */
.footer-panel > .footer__top {
  padding-top: calc(90px + var(--sp-xl));
}

.footer__brand {
  display: inline-block;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.125rem;
}

.footer__legal-name {
  margin-top: 10px;
  font-size: 0.8125rem;
  color: var(--text-dim);
  line-height: 1.6;
}

.footer__legal-address {
  margin-top: 8px;
  font-size: 0.8125rem;
  color: var(--text-dim);
  line-height: 1.6;
}

.footer__links {
  display: flex;
  gap: var(--sp-lg);
  flex-wrap: wrap;
}

.footer__col h4 {
  font-family: var(--font-head);
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: var(--sp-sm);
}

.footer__col a {
  display: block;
  font-size: var(--fs-small);
  color: var(--text-dim);
  padding: 6px 0;
  transition: color 0.2s;
}

.footer__col a:hover { color: var(--text-white); }

.footer__bottom {
  padding: var(--sp-md) 0;
  font-size: 0.75rem;
  color: var(--text-dim);
}

/* ---- form feedback ------------------------------------------------------------ */
.form-status {
  margin-top: var(--sp-sm);
  font-size: 0.8125rem;
  min-height: 1.2em;
}

.form-status--success { color: var(--success); }
.form-status--error { color: var(--coral-light); }

/* ---- gsap init state (avoid flash before JS runs) ------------------------------ */
[data-reveal] { opacity: 0; }
/* [data-reveal-item] needs its own anti-FOUC rule too — unlike [data-reveal],
   it was never covered by one before, which only went unnoticed because the
   reveal setup used to run immediately on load. Now that it's deferred until
   the intro curtain finishes (see main.js), without this the hero would sit
   fully visible the entire time the iris is opening, then visibly flash to
   hidden and fade back in right as the curtain disappears — this keeps it
   genuinely hidden the whole time until GSAP takes over. */
[data-reveal-item] { opacity: 0; }
/* The hero's own visual — renamed from plain [data-reveal] so main.js's
   generic scroll-triggered loop skips it; its entrance is now driven
   directly by js/intro.js instead, timed to play alongside the iris
   reveal rather than waiting on scroll position at all. */
[data-reveal-hero] { opacity: 0; }
.footer-panel__inner { opacity: 0; }
.no-js [data-reveal],
.no-js [data-reveal-item],
.no-js [data-reveal-hero],
.no-js .footer-panel__inner { opacity: 1; }

/* ---- intro curtain (homepage only, plays once per session) --------------------- */
/* Reveal is an iris: a circular hole at dead center grows outward (driven
   by js/intro.js animating --iris-pct), so the page becomes visible
   starting at the middle and expanding to the corners, curtain vanishing
   last at the extreme edges — rather than the earlier straight-edge wipe.
   The gradient has a wide (12-percentage-point) transparent-to-opaque
   transition band instead of a hard cutoff, so the boundary itself reads
   as a soft, blurred edge rather than a crisp circle — deliberately, for a
   softer/more premium feel than a razor-sharp mask edge would give.
   circle farthest-corner sizing means 100% always exactly reaches the
   viewport's actual farthest corner regardless of aspect ratio, so no
   viewport-dimension math is needed in JS. */
@property --iris-pct {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

.intro-curtain {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: var(--bg-l1);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The mask only exists once this class is added (right as the iris tween
   starts, via its onStart in js/intro.js) — not as a permanent part of
   .intro-curtain above. The gradient's first stop is always "transparent
   0%" regardless of --iris-pct's value, including at 0% itself, so if this
   were always active there'd be a small permanent soft dot at dead center
   from the very first frame — which is exactly what was showing up as a
   faint smudge on the "C" of SnapCoach, since the text sits right at that
   center point. Scoping the mask to only exist once the reveal is actually
   underway removes the dot's only opportunity to render at all. */
.intro-curtain--iris {
  -webkit-mask-image: radial-gradient(
    circle farthest-corner at 50% 50%,
    transparent 0%,
    transparent calc(var(--iris-pct) - 6%),
    black calc(var(--iris-pct) + 6%),
    black 100%
  );
  mask-image: radial-gradient(
    circle farthest-corner at 50% 50%,
    transparent 0%,
    transparent calc(var(--iris-pct) - 6%),
    black calc(var(--iris-pct) + 6%),
    black 100%
  );
}

/* flex column + gap, not margin-top on the tagline — a flex gap sits
   strictly between the two boxes' own edges and has no opinion about what's
   happening inside either one, unlike margin-top which was effectively
   measuring from wherever the tagline's (sometimes inflated) box happened
   to start. This is what makes 22px an accurate, permanent number instead
   of a value fighting against hidden internal leading. */
.intro-curtain__content {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  text-align: center;
  padding: 0 24px;
}

/* Pure vw (no rem floor/ceiling) so the *rendered word* stays at a constant
   ~45% share of viewport width at every screen size alike — desktop,
   tablet, and mobile all get the same ratio, just different absolute
   pixels. (Retuned 2026-08-23 from 8.92vw/~50% to 8.03vw/~45%.) */
.intro-curtain__mark {
  font-family: var(--font-head);
  font-weight: 800;
  font-size: 8.03vw;
  line-height: 1;
  color: var(--text-white);
  opacity: 0;
  white-space: nowrap;
  /* Real bug found and fixed here: a block-level element with width:auto
     stretches to fill its shrink-wrapped parent's width — which, with the
     tagline sibling present, was being set by the *tagline's* own raw
     pre-JS fallback size, not the mark's own text. white-space:nowrap only
     stops wrapping; it does nothing to the block's own width computation.
     width:fit-content locks this element to its own intrinsic content
     width regardless of what its sibling does, which is what every prior
     width measurement (including the ones this session's sizing/flush-match
     math was built on) should have had all along. */
  width: fit-content;
}

/* Starting font-size here is just a pre-JS fallback (briefly visible before
   intro.js measures and overrides it inline) — the real, exactly-flush size
   is computed at runtime in intro.js by measuring the wordmark's actual
   rendered width and scaling this element's font-size to match it exactly.
   A hand-tuned vw constant can only ever approximate that match.
   line-height back to the original (unset/inherited ~1.5) rather than the
   1.6 tried earlier — that inflated the whole line's leading, pushing the
   visible text further from "SnapCoach" above it than intended. Room for
   the script accent's overshoot now comes from its own line-height below,
   scoped to just that one word instead of the whole line. */
.intro-curtain__tagline {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 4.2vw;
  line-height: 1.15;
  color: var(--text-white);
  opacity: 0;
}

/* Fasthand renders visually smaller than Manrope at the same nominal
   font-size (smaller x-height by design), so it gets its own explicit,
   larger multiplier here rather than inheriting the tagline's size as-is —
   biased larger rather than trying to land exactly equal. Its own
   line-height (not the parent's) gives it just enough room for the
   ascender/descender overshoot that was clipping before, without
   inflating the leading around the rest of the line — and since both this
   font-size and this line-height are relative (em), the same ratio of
   headroom scales up automatically as the size grows. */
.intro-curtain__tagline .script-accent {
  font-size: 2.02em;
  line-height: 1.3;
  padding: 0 0.05em;
  /* Vertical margins are ignored on plain inline elements — display:
     inline-block is required for them to take effect at all, which is why
     the negative margin below needs it. The inline-block's own box is
     still generously sized (line-height:1.3, the same value already
     confirmed safe against clipping the Y) — negative margin only cancels
     how much of that box the *surrounding line* counts against it, it
     doesn't clip or shrink anything that actually paints. */
  display: inline-block;
  margin: -0.44em 0;
}

/* ---- legal pages ---------------------------------------------------------------- */
.legal {
  /* Same fix as .hero above, same reasoning: 68.5px = 160 - 91.5 (the
     nav's real measured natural height), preserving the original total
     gap now that the nav takes up real space instead of always floating
     as a fixed overlay on top of it. */
  padding: 68.5px 0 var(--sp-xxxl);
  max-width: 760px;
  margin: 0 auto;
}

.legal h1 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(2rem, 1.6rem + 1.5vw, 2.75rem);
  margin-bottom: var(--sp-xs);
}

.legal .legal__updated {
  color: var(--text-dim);
  font-size: var(--fs-small);
  margin-bottom: var(--sp-xxl);
}

.legal h2 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.375rem;
  margin: var(--sp-xl) 0 var(--sp-sm);
}

.legal p, .legal li {
  color: var(--text-muted);
  font-size: var(--fs-small);
  line-height: 1.7;
}

.legal ul { padding-left: 1.25em; margin: var(--sp-sm) 0; }
.legal li { margin-bottom: 8px; }

.legal a.inline-link {
  color: var(--coral-light);
  text-decoration: underline;
}
