/* PetList marketing landing page (PET-205) */

:root {
  --purple-deep: #1B163F;
  --purple-secondary: #2D1B69;
  --coral: #E0637A;
  --lavender: #F3EFFA;
  --lavender-card: #FFFFFF;
  --on-dark: #FFFFFF;
  --on-dark-secondary: #C9C3E0;
  --text: #1B163F;
  --text-secondary: #55506E;
  --border: #E2DEEE;

  --font-display: "Fraunces", Georgia, serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 32px;
  --space-5: 48px;
  --space-6: 64px;
  --space-7: 96px;

  --radius-card: 12px;
  --radius-pill: 999px;

  --ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--lavender);
  color: var(--text);
  font-family: var(--font-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* Defensive page-level overflow guard. The feature gallery handles its own
   horizontal scrolling on narrow screens; the document itself never should. */
html, body {
  overflow-x: hidden;
}

@supports (overflow-x: clip) {
  html, body {
    overflow-x: clip;
  }
}

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

a { color: inherit; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.container {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--space-3);
}

/* ---------- Scroll-linked fade-up (PET-205 round 3, item 3) ---------- */
/* The single, consistent entrance effect for everything from "What's
   inside" onward (the showcase strip, upcoming features, store availability, footer)
   -- replacing the previous round's per-line text stagger and 92%->100%
   scale-up, both cited as reading as "wonky." One treatment: fades in and
   slides up from translateY(28px), continuously tied to scroll position
   as the element enters the viewport rather than a one-shot threshold
   trigger that plays once and disconnects.

   Native CSS Scroll-Driven Animations (animation-timeline: view()) drive
   this where supported -- zero JS, runs on the compositor thread, and is
   what the ticket asked to prefer.

   `animation-range: cover 0% cover 38%` -- deliberately `cover`, not
   `entry`. `entry` was tried first and measured to be wrong: it's scoped
   to the ELEMENT's own height, and for anything much shorter than the
   viewport (a two-line heading, a single card) that phase only covers
   ~50-90px of scroll -- technically still scroll-linked, but the reveal
   happens in 1-2 scroll ticks and reads exactly like a threshold snap.
   `cover` is scoped to the *viewport's* height instead (0% = element's
   leading edge first touches the viewport's far edge; 100% = it's fully
   exited the near edge), which gives real scroll distance to work with
   regardless of the element's own size. The 38% endpoint was tuned by
   recording real scroll traces (see the verification notes for this
   round): it lands full opacity with the element's own top edge at
   roughly 50-55% down the viewport -- matching the ticket's target -- and
   climbs through several distinct intermediate opacity values across a
   ~250-300px scroll distance on the way there, confirmed genuinely
   continuous rather than a jump.

   `both` fill mode holds the 0% frame before the range starts and the
   100% frame after it ends -- so nothing animates back out as the element
   continues scrolling up past this range (the no-exit-animation rule from
   prior rounds), and scrolling back UP into the range *does* reverse it,
   which is correct for something genuinely scroll-LINKED rather than a
   reveal-once lock.

   Where animation-timeline: view() isn't supported, the inline script
   computes the identical progress from a scroll listener and drives the
   same opacity/transform via inline styles -- see "Scroll-linked fade-up"
   in the script. The @supports gate below is what keeps the two from
   fighting: only one of them is ever actually setting opacity/transform
   on a given element. */
.scroll-fade {
  opacity: 0;
  transform: translateY(28px);
  will-change: opacity, transform;
}

@supports (animation-timeline: view()) {
  .scroll-fade {
    animation: scroll-fade-in linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 38%;
  }
}

@keyframes scroll-fade-in {
  from { opacity: 0; transform: translateY(28px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ---------- Per-line text reveal (hero headline only) ---------- */
/* Kept for the hero's own on-load headline reveal, which is explicitly
   unchanged this round (the hero's load-sequence animation is separate
   from scroll-triggered entrances, see the ticket). Removed everywhere
   else it was used (the two section titles) in favor of .scroll-fade
   above -- text-splitting was cited as the main source of jank and isn't
   worth the complexity for anything but the hero's single showcase moment.
   Lines can't be targeted in CSS, so the inline script wraps each word in
   .line-word, measures which words share an offsetTop, and stamps each
   with its line index as --line. .has-split is added by the script, so a
   no-JS (or failed-JS) visitor keeps the plain whole-element fade
   underneath and never sees permanently hidden text. */
.has-split {
  animation: none !important;
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
}

.has-split .line-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.5em);
  transition: opacity 0.5s var(--ease-standard), transform 0.5s var(--ease-standard);
  transition-delay: calc(var(--line, 0) * 90ms);
}

.has-split.is-revealed .line-word {
  opacity: 1;
  transform: translateY(0);
}

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

.hero {
  position: relative;
  background: var(--purple-deep);
  overflow: hidden;
  /* Compensates for .site-header now being position:fixed (out of flow) --
     92px = the header's own resting-state height (20px + 56px logo + 20px). */
  padding-top: 92px;
  /* The +40 clears the wave divider (round 9, item 8), which is absolutely
     positioned at the bottom edge and up to 80px tall -- without it the
     trust bar would sit partly inside the wave. */
  padding-bottom: calc(var(--space-7) + 40px);
}

/* Round 9: every offset in this block is now viewport-relative (vw) rather
   than a percentage of .hero's own height, and that is a layout-stability
   fix, not a cosmetic one.

   These blobs used to be inset: -20% ... with height: 140%, both resolved
   against the hero's height. The hero's height is not stable across a page
   load -- webfonts swap in, text re-wraps, and the section settles a few
   pixels taller or shorter. Each of those re-resolved the blobs' top
   offsets and moved them, and because they are large VISIBLE elements the
   browser scored every one as a layout shift. Measured at 768px, where the
   single-column hero is tallest and reflows most: 0.00778 of that width's
   0.00869 CLS came from this one element alone.

   Sized and positioned in vw, they no longer depend on the hero's content
   height at all, so a font swap cannot move them. They are decorative,
   blurred by 80px and pointer-events:none, so anchoring them to the
   viewport rather than the section costs nothing visually. */
.hero-ambient {
  position: absolute;
  top: 0;
  left: -10%;
  right: -10%;
  bottom: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

.hero-ambient::before,
.hero-ambient::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.28;
  animation: drift-a 26s ease-in-out infinite alternate;
}

.hero-ambient::before {
  width: 46vw;
  height: 46vw;
  max-width: 620px;
  max-height: 620px;
  background: var(--coral);
  top: -4vw;
  left: 58%;
}

.hero-ambient::after {
  width: 38vw;
  height: 38vw;
  max-width: 520px;
  max-height: 520px;
  background: var(--purple-secondary);
  top: 26vw;
  left: 4%;
  animation-name: drift-b;
  animation-duration: 32s;
}

@keyframes drift-a {
  from { transform: translate(0, 0) scale(1); }
  to   { transform: translate(-4%, 5%) scale(1.06); }
}

@keyframes drift-b {
  from { transform: translate(0, 0) scale(1); }
  to   { transform: translate(5%, -4%) scale(1.08); }
}

/* ---------- Persistent site header (PET-205 follow-up item 5) ---------- */
/* Fixed so it survives past the hero; transparent/large at rest, shrinks
   and gains a blurred backdrop once scrolled past the hero, so it stays
   legible over both the lavender showcase and purple store availability sections
   below. position:fixed keeps it out of document flow, so its own size
   change on scroll can never shift any other content -- CLS stays 0. */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  padding: 20px 0;
  background: transparent;
  opacity: 0;
  animation: fade-in 0.5s var(--ease-standard) forwards;
  transition: padding 0.3s var(--ease-standard), background-color 0.3s var(--ease-standard), box-shadow 0.3s var(--ease-standard);
}

.site-header.is-scrolled {
  padding: 10px 0;
  background: rgba(27, 22, 63, 0.82);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18);
}

.site-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

/* The approved dark wordmark sits on a compact white surface over the header. */
.site-header .brand {
  display: inline-flex;
  align-items: center;
  padding: 7px 10px;
  border-radius: 12px;
  background: #fff;
  text-decoration: none;
  transition: padding 0.3s var(--ease-standard);
}

.site-header.is-scrolled .brand {
  padding: 5px 8px;
}

.site-header .brand-image {
  height: 40px;
  width: auto;
  display: block;
  transition: height 0.3s var(--ease-standard);
}

.site-header.is-scrolled .brand-image {
  height: 34px;
}

/* ---------- Trust badge (PET-205 round 9, item 2) ---------- */
/* A <p>, not a button or link -- it states something, it doesn't do
   anything. Deliberately quiet beside the wordmark. --on-dark-secondary
   on --purple-deep measures 10.03:1.

   white-space:nowrap makes this the one element in the header that can
   force horizontal overflow, which is why it gets its own step-down at
   359px rather than being allowed to wrap to two lines. */
.trust-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: var(--on-dark-secondary);
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  opacity: 0;
  animation: fade-in 0.5s var(--ease-standard) forwards;
  animation-delay: 0.06s;
}

.trust-badge-glyph {
  font-size: 0.85em;
  line-height: 1;
}

@media (max-width: 519px) {
  .site-header .brand { padding: 6px 8px; }
  .site-header.is-scrolled .brand { padding: 5px 7px; }
  .site-header .brand-image { height: 27px; }
  .site-header.is-scrolled .brand-image { height: 25px; }
}

@media (max-width: 359px) {
  .site-header .brand { padding: 5px 6px; }
  .site-header .brand-image { height: 23px; }
  .site-header.is-scrolled .brand-image { height: 22px; }
  .trust-badge { font-size: 0.64rem; padding: 5px 10px; }
}

.hero-body {
  position: relative;
  z-index: 1;
  display: grid;
  gap: var(--space-6);
  align-items: center;
}

@media (min-width: 860px) {
  .hero-body {
    grid-template-columns: 1.05fr 0.95fr;
    gap: var(--space-5);
  }
}

.hero-copy {
  text-align: center;
}

@media (min-width: 860px) {
  .hero-copy { text-align: left; }
}

.hero-headline {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(2.25rem, 5.6vw, 3.75rem);
  line-height: 1.08;
  letter-spacing: -0.01em;
  color: var(--on-dark);
  margin: 0 0 var(--space-3);
  opacity: 0;
  transform: translateY(14px);
  animation: fade-up 0.55s var(--ease-standard) forwards;
  animation-delay: 0.12s;
}

/* ---------- Copy emphasis (PET-205 round 9, item 3) ---------- */
/* Coral used as EMPHASIS WITHIN A SENTENCE, which is a deliberate,
   scoped exception to this page's otherwise CTA-only coral rule -- that
   rule still governs buttons, fills and lines everywhere else.

   Two constraints, both load-bearing for the headline's per-line reveal
   (see splitIntoLines in index.html), neither obvious from looking at it:

   1. .hero-em must stay position:static. The line grouping reads each
      word's offsetTop, which is measured against the nearest POSITIONED
      ancestor -- giving this span position:relative would put its words on
      a different offset origin and scramble the grouping.
   2. .hero-em must stay plain inline. display:inline-block would stop the
      browser breaking lines inside it, changing the line count outright.

   So: colour only. Nothing here may add positioning or change display. */
.hero-em {
  color: var(--coral);
}

.pitch-em {
  color: var(--coral);
}

.hero-pitch {
  font-size: clamp(1.05rem, 2vw, 1.25rem);
  color: var(--on-dark-secondary);
  margin: 0 0 var(--space-4);
  /* .pitch-break below is what guarantees the two sentences sit on
     separate lines, so this cap only needs to be wide enough that neither
     sentence wraps *within* itself. Re-measured for round 9's copy:
     "Organize your pet's life with ease." is 35 characters and
     "One payment. No subscriptions. Ever." is 36 -- both inside this cap,
     so 40ch still holds and did not need widening. */
  max-width: 40ch;
  margin-left: auto;
  margin-right: auto;
  opacity: 0;
  transform: translateY(14px);
  animation: fade-up 0.55s var(--ease-standard) forwards;
  animation-delay: 0.22s;
}

/* Forces the tagline's two sentences onto their own lines at every width,
   instead of letting them reflow into a ragged mid-sentence break. Uses a
   real space in the markup so the sentences stay separated for screen
   readers and for text selection/copy. */
.pitch-break {
  display: block;
  height: 0;
  overflow: hidden;
}

/* On the narrowest phones the longer sentence overruns its line by a few
   pixels and orphans its last word onto a third line. Nudging the type
   down there keeps the tagline to a clean two lines.

   Re-measured for round 9's copy: with the nudge the pitch is 2 lines at
   every width from 320 up; without it, 320 and 340 go to 3 lines while 355
   and 359 are already fine. So the break moved from ~355px to ~341-355px
   when the copy changed, and this rule is still load-bearing at the narrow
   end rather than dead code -- but it now has more slack than it did.
   360px and up break cleanly at the sentence boundary and are untouched. */
@media (max-width: 359px) {
  .hero-pitch {
    font-size: 0.92rem;
  }
}

@media (min-width: 860px) {
  .hero-pitch { margin-left: 0; margin-right: 0; }
}

.hero-cta {
  opacity: 0;
  transform: translateY(14px);
  animation: fade-up 0.55s var(--ease-standard) forwards;
  animation-delay: 0.3s;
}

/* ---------- Bottom trust bar (round 9, item 7) ---------- */
/* auto-fit + minmax reflows 1 -> 2 -> 4 columns continuously across
   320-1440 with no breakpoints of its own, which is why it's used here
   rather than another media query.

   Separated from the vet card above by a hairline rather than being carded
   itself: this hero already states "you only pay once" in the subhead and
   this bar's second item, so the treatment stays quiet
   on purpose. */
.trust-bar {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: var(--space-4) 0 0;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-2) var(--space-3);
  opacity: 0;
  transform: translateY(14px);
  animation: fade-up 0.55s var(--ease-standard) forwards;
  animation-delay: 0.64s;
}

.trust-item {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: 10px;
  align-items: center;
}

.trust-icon {
  grid-row: span 2;
  width: 22px;
  height: 22px;
  fill: none;
  stroke: var(--coral);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.trust-label {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--on-dark);
  line-height: 1.3;
}

.trust-desc {
  font-size: 0.76rem;
  color: var(--on-dark-secondary);
  line-height: 1.35;
}

@keyframes fade-in {
  to { opacity: 1; }
}

@keyframes fade-up {
  to { opacity: 1; transform: translateY(0); }
}

/* Store CTAs. Apple's badge remains the official unmodified artwork; the
   Google Play link uses the site's own compact black CTA treatment so the
   two platforms have equal visual weight without adding another remote asset. */
.store-badges {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

@media (min-width: 520px) {
  .store-badges {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
  }
}

@media (min-width: 860px) {
  .hero-cta .store-badges {
    justify-content: flex-start;
  }
}

.store-badge-link {
  flex: none;
  transition: transform 0.18s var(--ease-standard), filter 0.18s var(--ease-standard);
}

.store-badge-link:hover,
.store-badge-link:focus-visible {
  transform: translateY(-1px);
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.35));
}

.appstore-badge {
  display: inline-block;
  line-height: 0;
  border-radius: 8px;
}

.appstore-badge img {
  height: 54px;
  width: auto;
  display: block;
}

.google-play-badge {
  box-sizing: border-box;
  min-width: 176px;
  height: 54px;
  padding: 6px 14px 6px 11px;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  border: 1px solid rgba(255, 255, 255, 0.72);
  border-radius: 8px;
  background: #000;
  color: #fff;
  text-decoration: none;
  font-family: var(--font-body);
}

.google-play-mark {
  width: 29px;
  height: 29px;
  flex: none;
  fill: currentColor;
}

.google-play-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1;
}

.google-play-kicker {
  font-size: 0.57rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.google-play-name {
  margin-top: 3px;
  font-size: 1.12rem;
  font-weight: 500;
  letter-spacing: -0.01em;
}

/* ---------- Hero store screenshot ---------- */

.hero-visual {
  display: flex;
  justify-content: center;
  min-width: 0;
}

.store-shot {
  margin: 0;
  overflow: hidden;
  background: #15103D;
  border-radius: 28px;
  box-shadow: 0 24px 54px -18px rgba(0, 0, 0, 0.42);
}

.store-shot img {
  display: block;
  width: 100%;
  height: auto;
}

.store-shot--hero {
  width: min(78vw, 320px);
  opacity: 0;
  transform: translateY(24px) scale(0.96);
  animation: store-shot-in 0.75s var(--ease-standard) forwards;
  animation-delay: 0.38s;
}

@keyframes store-shot-in {
  to { opacity: 1; transform: translateY(0) scale(1); }
}

/* ---------- Wave divider (round 9, item 8) ---------- */
/* bottom:-1px rather than 0 kills the sub-pixel hairline seam that shows
   between the SVG's own bottom edge and .showcase's background at
   fractional device pixel ratios. Fill comes from the token rather than a
   literal in the markup so the two can't drift apart. */
.hero-wave {
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: clamp(36px, 5vw, 80px);
  display: block;
  z-index: 2;
  pointer-events: none;
}

.hero-wave path {
  fill: var(--lavender);
}

/* ---------- Hero extras: feature row (round 9 item 4; round 11 folded the
   former standalone vet-ready card into this row as its 4th item) ---------- */
/* Deliberately a sibling of .hero-body rather than content inside
   .hero-copy -- see the comment at the markup. */
.hero-extras {
  position: relative;
  z-index: 1;
  margin-top: var(--space-5);
}

.feature-row {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* 2x2 on phones, 4-across from 700px -- unchanged shape from round 10,
     still exactly 4 items so no grid-column-count math changed, only what
     fills the 4th slot. Two 124px columns fit at 320px; round 11's taller
     square-ish card (see .feature-chip below) only grows vertically, so
     that horizontal fit is undisturbed. */
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-2);
  opacity: 0;
  transform: translateY(14px);
  animation: fade-up 0.55s var(--ease-standard) forwards;
  animation-delay: 0.52s;
}

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

/* Round 11: square-ish proportions (was wider/shorter) now that
   .feature-desc is always-visible static content rather than a hover
   reveal -- the card needs real in-flow room for wrapped subtitle text,
   not just an icon and a one-line label. Height is deliberately left to
   `auto` rather than pinned via `aspect-ratio`: at the narrowest 2-column
   width (320px) the longest subtitle ("Generate a vet-ready summary in one
   tap") wraps to 3 lines, and a hard-locked ratio would either clip that
   text or force it to overflow the card's painted background -- letting
   the box grow with its content is what guarantees it never does either,
   at the cost of "square" being an approximate proportion rather than a
   literal 1:1 one. Generous padding is what gets it there in practice. */
.feature-chip {
  position: relative;
  width: 100%;
  /* Caps the card so it doesn't stretch edge-to-edge with its grid track at
     wide viewports -- at 1120px (this page's container max-width) an
     unconstrained 4-across track is ~256px wide against a card whose
     content wants roughly 180-200px, which reads as wide/flat again
     exactly the shape this round moved away from. Centered within its
     track via margin:auto rather than shrinking the track itself, so the
     2-column mobile layout (where this cap never binds) is untouched. */
  max-width: 200px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 22px 14px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius-card);
  background: rgba(255, 255, 255, 0.06);
  color: var(--on-dark);
  font-family: inherit;
  text-align: center;
}

.feature-icon {
  width: 24px;
  height: 24px;
  stroke: var(--coral);
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.feature-label {
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.25;
}

/* Static feature subtitle, visible without interaction. */
.feature-desc {
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.35;
  color: var(--on-dark-secondary);
  text-align: center;
}

/* ---------- Showcase ---------- */

.showcase {
  background: var(--lavender);
  padding: var(--space-7) 0;
}

.section-heading {
  text-align: center;
  max-width: 640px;
  margin: 0 auto var(--space-6);
}

.section-eyebrow {
  font-weight: 600;
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--coral);
  margin: 0 0 var(--space-1);
}

.section-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.75rem, 3.2vw, 2.5rem);
  color: var(--purple-deep);
  margin: 0;
  line-height: 1.15;
}

.store-gallery {
  display: grid;
  grid-template-columns: repeat(8, minmax(0, 1fr));
  justify-content: center;
  align-items: start;
  gap: clamp(16px, 1.5vw, 22px);
}

.store-shot-card {
  width: 100%;
  grid-column: span 2;
}

/* Center the three cards in the second row of the four-column gallery. */
.store-shot-card:nth-child(5) {
  grid-column: 2 / span 2;
}

@media (min-width: 761px) and (max-width: 1199px) {
  .store-gallery {
    grid-template-columns: repeat(2, minmax(0, 320px));
  }

  .store-shot-card,
  .store-shot-card:nth-child(5) {
    grid-column: auto;
  }
}

@media (max-width: 760px) {
  .showcase .container {
    padding-left: 0;
    padding-right: 0;
  }

  .showcase .section-heading {
    padding-left: var(--space-3);
    padding-right: var(--space-3);
  }

  .store-gallery {
    display: flex;
    justify-content: flex-start;
    gap: 16px;
    overflow-x: auto;
    padding: 0 var(--space-3) 18px;
    scroll-padding-inline: var(--space-3);
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    overscroll-behavior-inline: contain;
    -webkit-overflow-scrolling: touch;
  }

  .store-gallery::-webkit-scrollbar {
    display: none;
  }

  .store-shot-card {
    flex: 0 0 min(78vw, 320px);
    scroll-snap-align: center;
  }
}

/* ---------- Upcoming features (PET-205 follow-up item 6) ---------- */
/* Deliberately lighter/flatter than the showcase strip above: no phone
   frames, no screenshots (none exist for unbuilt features), a plain bordered
   list rather than shadowed cards, muted (not coral) eyebrow -- this section
   should read as "here's where we're headed," not compete with the real,
   shipping feature showcase. */

.upcoming {
  background: var(--lavender);
  padding: var(--space-6) 0 var(--space-7);
}

.upcoming .section-eyebrow {
  color: var(--text-secondary);
}

.upcoming-list {
  list-style: none;
  margin: var(--space-4) auto 0;
  padding: 0;
  max-width: 640px;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.upcoming-item {
  background: var(--lavender-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: 14px 20px;
  text-align: left;
  /* .scroll-fade (see its own comment) provides the resting state and the
     JS-fallback path for browsers without animation-timeline: view()
     support. Where that IS supported, the tumble-in override below (round
     11) replaces the plain fade for this list specifically. */
}

/* ---------- "More on the way" tumble-in (round 11, item 4) ---------- */
/* A rotate + translateY + opacity entrance, replacing the plain
   .scroll-fade look for this one list -- reuses that same mechanism
   (animation-timeline: view(), same cover-0%-to-38% window, same JS
   fallback for browsers that lack it) rather than inventing a parallel
   scroll-trigger, per the ticket. Only `animation-name` is overridden here;
   `animation-timing-function: linear`, `animation-fill-mode: both` and
   `animation-timeline: view()` all still come from the base .scroll-fade
   rule, so this is additive, not a fork of it.

   The "overshoot and settle" read comes from the keyframe STOPS, not from
   a spring timing-function -- there isn't one for scroll-linked animations,
   since `animation-timing-function` here is `linear` (interpolating
   evenly between whatever stops are defined). Placing a stop past the
   resting rotation/position (55%) and another just short of it in the
   other direction (80%) before the final 100% is what reads as a card
   tumbling in and settling, entirely through non-uniform keyframe values
   rather than easing math.

   Tried and kept over the plain-fade fallback: on-review this reads as a
   genuine "tumble," not jank, at every width tested (320-1920px) -- no
   layout shift risk either, since `transform: rotate()/translateY()` never
   participates in layout (same reason .scroll-fade's own translateY
   doesn't count against CLS elsewhere on this page), only the shape of the
   motion is new.

   Staggered per-item via a small shift to each item's OWN
   animation-range, on top of (not instead of) the natural cascade the
   comment above used to describe -- five items 60-70px apart already
   enter their individual cover-0%-38% windows a little later than their
   predecessor, but with a short list mostly visible in one viewport that
   gap alone reads as barely-there. Nudging each subsequent item's window
   a further 4 percentage points later makes the stagger the ticket asked
   for ("so they don't all land simultaneously") actually read as
   deliberate rather than incidental. */
@supports (animation-timeline: view()) {
  .upcoming-item.scroll-fade {
    animation-name: upcoming-tumble-in;
    transform-origin: 50% 100%;
  }

  .upcoming-item.scroll-fade:nth-child(2) { animation-range: cover 4% cover 42%; }
  .upcoming-item.scroll-fade:nth-child(3) { animation-range: cover 8% cover 46%; }
  .upcoming-item.scroll-fade:nth-child(4) { animation-range: cover 12% cover 50%; }
  .upcoming-item.scroll-fade:nth-child(5) { animation-range: cover 16% cover 54%; }
}

@keyframes upcoming-tumble-in {
  0%   { opacity: 0; transform: translateY(48px) rotate(-7deg); }
  55%  { opacity: 1; transform: translateY(-8px) rotate(2deg); }
  80%  { transform: translateY(3px) rotate(-0.8deg); }
  100% { opacity: 1; transform: translateY(0) rotate(0deg); }
}

.upcoming-label {
  display: block;
  font-weight: 600;
  font-size: 1rem;
  color: var(--purple-deep);
  margin-bottom: 2px;
}

.upcoming-desc {
  display: block;
  font-size: 0.9rem;
  color: var(--text-secondary);
}

/* ---------- Store availability ---------- */

.store-availability {
  background: var(--purple-secondary);
  padding: var(--space-6) 0;
}

.store-availability-inner {
  max-width: 720px;
  margin: 0 auto;
  text-align: center;
}

.store-availability-eyebrow {
  margin: 0 0 var(--space-1);
  color: var(--coral);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.store-availability-title {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  color: var(--on-dark);
}

.store-availability-body {
  max-width: 56ch;
  margin: 0 auto;
  color: var(--on-dark-secondary);
}

.store-availability-badges {
  margin-top: var(--space-4);
}

/* ---------- Footer ---------- */

.site-footer {
  background: var(--purple-deep);
  color: var(--on-dark-secondary);
  padding: var(--space-4) 0;
  font-size: 0.85rem;
}

.footer-inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: center;
  text-align: center;
}

/* The footer is the last element on the page, so .scroll-fade's shared
   `cover 0% cover 38%` range is unreachable for it: `cover`'s endpoint
   requires room to keep scrolling well past the element, and there's
   nothing below the footer to scroll into -- max scroll is reached with
   the footer barely past cover 0%, so it would sit stuck at partial
   opacity forever once a reader hits the bottom of the page. Overridden
   here (more specific selector wins over .scroll-fade) to `entry`, whose
   endpoint is bounded to the footer's own modest height and therefore
   always reachable regardless of what's below it. */
.footer-inner.scroll-fade {
  animation-range: entry 0% entry 90%;
}

@media (min-width: 640px) {
  .footer-inner {
    flex-direction: row;
    justify-content: space-between;
    text-align: left;
  }
}

.footer-links {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  justify-content: center;
}

.footer-links a {
  color: var(--on-dark-secondary);
  text-decoration: none;
}

.footer-links a:hover,
.footer-links a:focus-visible {
  color: var(--on-dark);
  text-decoration: underline;
}

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

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

  .site-header,
  .hero-headline,
  .hero-pitch,
  .hero-cta,
  .store-shot--hero,
  .trust-badge,
  .hero-extras,
  .feature-row,
  .trust-bar {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  .site-header,
  .site-header .brand,
  .site-header .brand-image {
    transition: none !important;
  }

  .hero-ambient::before,
  .hero-ambient::after {
    animation: none !important;
  }


  .scroll-fade,
  .has-split .line-word {
    animation: none !important;
    transition: none !important;
    opacity: 1 !important;
    transform: none !important;
  }




  .store-badge-link {
    transition: none;
  }
}
