/* CocoBuddy landing page - design prototype.
   Palette and geometry mirror the app's Home screen. */

@font-face {
  font-family: "Momo Trust Display";
  src: url("../fonts/MomoTrustDisplay-Regular.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Momo Trust Sans";
  src: url("../fonts/MomoTrustSans-Semibold.woff2") format("woff2");
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: "Momo Trust Sans";
  src: url("../fonts/MomoTrustSans-Bold.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}

:root {
  /* palette */
  --cream: #FFF7ED;
  --navy: #373A60;
  --muted: #5C5F86;
  --yellow: #FFE875;
  --tone-blue: #98CDFF;
  --tone-pink: #FDBBE9;
  --tone-purple: #B3B0FF;
  --tone-orange: #FFB379;
  --tone-mint: #CBF4E9;

  /* motion */
  --cloud-speed: 1;
  /* Why: the cloud loop distance is one sky width in px, set by JS.
     Percentages of a resizing parent are what made the clouds jump. */
  --track-w: 100vw;

  /* geometry - fluid on viewport height so one screen fits without scrolling.
     Why: every height-relative value is declared in vh here and re-declared in
     svh inside the @supports block below. A custom property accepts any token
     stream at parse time, so a plain "fallback first" pair does NOT protect
     against an engine that cannot resolve svh - the bad value would still win
     and every property built on it would fall back to its initial value. The
     @supports gate is the only construction that actually degrades. */
  /* Why: --sky-base is the art scale (sun, clouds, wordmark); --sky-h is the
     band's own height. Round 11 splits them so the phone band can close up
     under the wordmark without the wordmark or the sky art shrinking with it.
     The band is now exactly the wordmark's box plus one small strip below. */
  --sky-base: clamp(120px, 20vh, 190px);
  --sun-w: calc(var(--sky-base) * 0.9);
  --cloud-back-w: calc(var(--sky-base) * 1.25);
  --cloud-front-w: calc(var(--sky-base) * 0.85);
  --cloud-back-x: -100px;
  --cloud-front-x: calc(var(--track-w) * 0.72);
  /* the wordmark art is 760 x 554 (ratio 1.3718). The 1.3705 below was
     computed from the original 2922 x 2132 source and is kept: the two
     ratios differ by 0.15 px at the widest the art ever renders. */
  --wordmark-w: min(calc(var(--sky-base) * 0.98), 58vw);
  --wordmark-h: calc(var(--wordmark-w) / 1.3705);
  --wordmark-top: calc(var(--sky-base) * 0.09);
  /* Why: the strip of sky under the wordmark. It is the last thing that
     gives on a 667pt phone, where the pill needs its 12px above the footer
     and the footer padding is already at its floor. */
  --sky-below: clamp(8px, 1.4vh, 12px);
  --sky-h: calc(var(--wordmark-top) + var(--wordmark-h) + var(--sky-below));

  --tagline-size: clamp(18px, 3vh, 30px);
  --explain-size: clamp(12px, 1.8vh, 16px);

  --card-w: min(300px, 80vw, 37vh);
  /* Why: the card is taller than round 3 (0.68) so it uses the free space
     under the pill. The question text shrank ~15% at the same time, so the
     text block still sits comfortably inside the reserved button row. */
  --card-h: calc(var(--card-w) * 0.74);
  --card-r: calc(var(--card-w) * 0.115);
  --card-border: 3.5px;
  --btn: 52px;
  /* Why: the shuffle ring reads as part of the card's own outline, so it
     carries the card's border weight at every size. */
  --btn-border: var(--card-border);
  --q-size: calc(var(--card-w) * 0.0867);
  --card-pad: calc(var(--card-w) * 0.085);
  /* Why: the question breathes at 10% of the card width on each side. */
  --card-pad-x: calc(var(--card-w) * 0.10);
  /* the reserved shuffle row: the button plus its 12px top offset */
  --card-row: calc(var(--btn) + 12px);
  --peek-x: max(10px, calc(var(--card-w) * 0.05));
  /* Why: keyed off the card height, not the width, so the slice of the peek
     card that shows stays the same fraction of a card when the ratio moves. */
  --peek-y: calc(var(--card-h) * 0.066);
  --buddy-w: calc(var(--card-w) * 0.56);

  --pill-size: clamp(13px, 2vh, 16px);
  /* Why: the footer is back on the first screen, so the three gaps are the
     only budget the layout has left - the wordmark, buddy, card and text all
     keep their round-9 sizes. These are the round-9 gaps scaled to about 43%,
     which is what the footer plus a 12px breathing strip above it costs at
     375x812. They keep their round-9 ratio (roughly 1 : 2.8 : 1.7 for
     tagline-explain : explain-deck : deck-pill), so the deck still gets the
     most air on both sides.
     Why the floors carry most of the work: a 667pt screen has MORE slack to
     absorb than an 812pt one, so a pure svh ramp cannot serve both. The floor
     sets the short-phone value and the svh term only starts to lift the gaps
     once there is real height to spend; the ceiling stops them ballooning on a
     tall desktop window, where the leftover slack stays under the pill. */
  --gap: clamp(25px, 3.08vh, 28px);
  --explain-gap: clamp(10px, 1.4vh, 14px);
  --deck-gap: clamp(38px, 4.83vh, 46px);
}

/* Why: svh is the better unit on a phone (it ignores the collapsing toolbar,
   so the layout does not jump), but it must never be the only definition. */
@supports (height: 1svh) {
  :root {
    --sky-base: clamp(120px, 20svh, 190px);
    --sky-below: clamp(8px, 1.4svh, 12px);
    --tagline-size: clamp(18px, 3svh, 30px);
    --explain-size: clamp(12px, 1.8svh, 16px);
    --card-w: min(300px, 80vw, 37svh);
    --pill-size: clamp(13px, 2svh, 16px);
    --gap: clamp(25px, 3.08svh, 28px);
    --explain-gap: clamp(10px, 1.4svh, 14px);
    --deck-gap: clamp(38px, 4.83svh, 46px);
  }
}

* { box-sizing: border-box; }

html {
  overflow-x: hidden;
  overscroll-behavior: none;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--cream);
}

/* Why: the whole page is one screen, footer included, so it must not scroll
   at all when everything fits - a page that scrolls by a few px fights the
   card drag, and the card wins. The lock is the default; JS adds .can-scroll
   on <html> only when the content really is taller than the viewport. */
body {
  font-family: "Momo Trust Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: var(--navy);
  -webkit-text-size-adjust: 100%;
}

html.js body {
  /* Why: --vvh is window.visualViewport.height in px, set by landing.js on
     every measurement. On an external-app launch (a tap from Messages) iOS
     reports a taller viewport at first paint than the one it settles on once
     the toolbars animate in; sizing to svh there lays the page out to the
     wrong height and the visual viewport ends up panned down inside it. The
     visual viewport is the only height that is true in that window. The
     svh/vh declarations stay as the fallback for a browser with no
     visualViewport. */
  height: 100vh;
  height: var(--vvh, 100vh);
  overflow: hidden;
}

@supports (height: 1svh) {
  html.js body {
    height: 100svh;
    height: var(--vvh, 100svh);
  }
}

html.can-scroll body {
  height: auto;
  min-height: 100vh;
  min-height: 100svh;
  overflow: auto;
}

/* Why: the escape hatch. While locked, body clips at the viewport, so no
   measurement taken through it can ever report content taller than one
   screen - the lock would be measuring itself. JS adds this class for the
   duration of one synchronous read, so the natural content height is what
   decides whether the lock comes off. */
html.measuring body {
  height: auto !important;
  min-height: 0 !important;
  overflow: visible !important;
}

/* Why: the offset recovery. A locked page that is somehow scrolled cannot
   scroll itself back - overflow:hidden means there is no scroll offset to
   write to. This drops the lock for the duration of one synchronous reset,
   so scrollTo(0, 0) has something to act on, and then the lock returns. */
html.unpinning,
html.unpinning body {
  height: auto !important;
  min-height: 0 !important;
  overflow: auto !important;
}

.page {
  /* Why: the page tracks the same height as the locked body, or the footer
     would be pushed past the bottom of a body sized to the visual viewport. */
  min-height: 100vh;
  min-height: var(--vvh, 100vh);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Why: the flying card is allowed to leave the deck now, so the page is
     what keeps it from growing the document. Both axes are clipped: the page
     box is content-sized, so nothing real is cut, but a card flying sideways
     or downward can never add scrollWidth or scrollHeight. */
  overflow: clip;
  /* Why: the safe-area insets are zero while viewport-fit=cover is off
     (index.html), but the page still carries them so restoring the meta later
     needs no layout change. Everything inside it - the content column, the
     deck and the footer - is inset once, here, and can never sit under a
     notch or clip at a rounded corner. The sky is the one band that cancels
     these insets again (see .sky). */
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

@supports (height: 1svh) {
  .page {
    min-height: 100svh;
    min-height: var(--vvh, 100svh);
  }
}


img {
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
}

/* Why: screen-reader-only. The one live region that announces the new
   question lives outside the rotating cards, so it is never aria-hidden
   mid-rotation and never gets a populated-then-revealed live region. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
/* Why: without the script the buddy and shuffle buttons do nothing, so they
   are shipped disabled and the script enables them. They keep their look. */
.buddy:disabled, .shuffle:disabled { cursor: default; opacity: 1; }

/* ---------- hero ---------- */

/* Why: the hero sizes to its own content now. Any min-height here would push
   the footer past the bottom of the 100svh page and force a scroll, which is
   exactly what round 10 is undoing. The footer's margin-top: auto is what
   parks it at the bottom of the screen instead. */
.hero {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* ---------- sky ---------- */

.sky {
  position: relative;
  /* Why: the sky must run to the physical device edges while the readable
     content stays inside the safe area. align-self: stretch is what makes the
     margin box fill .page's content box (the column centres its items, so a
     plain width: auto would shrink-to-fit); the negative margins then push the
     band back out under each inset, whether or not the two are equal. It lands
     exactly on .page's padding box, which is where its overflow clips, so no
     horizontal overflow is created. */
  align-self: stretch;
  width: auto;
  margin-inline: calc(-1 * env(safe-area-inset-left, 0px)) calc(-1 * env(safe-area-inset-right, 0px));
  height: var(--sky-h);
  overflow: hidden;
  flex: none;
}

.sky-art {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.sun {
  position: absolute;
  width: var(--sun-w);
  height: auto;
  top: -26px;
  right: -40px;
  z-index: 1;
}

.wordmark {
  /* Why: centred by auto margins between left:0 and right:0, not by
     translateX(-50%). A transform is the one thing on this element that any
     other rule, animation shorthand or class toggle can overwrite, and when
     it is lost the art lands at the sky's left edge at its natural 760px
     width and is clipped. Auto margins cannot be lost that way. */
  position: absolute;
  z-index: 5;
  top: var(--wordmark-top);
  left: 0;
  right: 0;
  margin-inline: auto;
  display: block;
  width: var(--wordmark-w);
  height: auto;
  /* belt and braces: never taller than the sky that clips it */
  max-width: 100%;
  max-height: calc(var(--sky-h) - var(--wordmark-top) - 4px);
  object-fit: contain;
}

.cloud-track {
  position: absolute;
  left: 0;
  width: calc(var(--track-w) * 2);
  display: flex;
  z-index: 0;
}

.cloud-slot {
  flex: 0 0 var(--track-w);
  position: relative;
  height: 1px;
}

.cloud-slot img {
  position: absolute;
  top: 0;
  height: auto;
}

.cloud-track--back {
  top: 6px;
  animation: drift-right calc(420s / var(--cloud-speed)) linear infinite;
}
.cloud-track--back img {
  width: var(--cloud-back-w);
  left: var(--cloud-back-x);
}

.cloud-track--front {
  top: calc(var(--sky-base) * 0.46);
  animation: drift-left calc(280s / var(--cloud-speed)) linear infinite;
}
.cloud-track--front img {
  width: var(--cloud-front-w);
  left: var(--cloud-front-x);
}

/* Why: on a phone the band (--sky-h) is shorter than the art scale
   (--sky-base) - round 11 closed the band up under the wordmark but left the
   clouds sized and placed off the art scale, so the front cloud hung out of
   the band's bottom edge. Below the desktop breakpoint both tracks are
   re-anchored to the BAND instead: the front cloud is parked a fixed 8px
   clear of the band's bottom, and the back cloud keeps its old proportion of
   the band, which lets its top clip off the band - which is what it did on
   desktop too, just less. Desktop (>=720px) is untouched: there --sky-h is
   --sky-base and the original placement is already right.
   The two heights are the art's own aspect ratios: CloudFront is 493x262,
   CloudBack is 732x320. */
@media (max-width: 719px) {
  :root {
    --cloud-front-h: calc(var(--cloud-front-w) / 1.8817);
    --cloud-back-h: calc(var(--cloud-back-w) / 2.2875);
  }
  .cloud-track--front {
    top: max(0px, calc(var(--sky-h) - var(--cloud-front-h) - 8px));
  }
  .cloud-track--back {
    top: calc(var(--sky-h) * 0.59 - var(--cloud-back-h));
  }
}

/* Why: the loop travels exactly one track width, so the second copy lands
   where the first started and the wrap is seamless at any size. */
@keyframes drift-right {
  from { transform: translateX(calc(-1 * var(--track-w))); }
  to   { transform: translateX(0); }
}
@keyframes drift-left {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(-1 * var(--track-w))); }
}

/* JS drops this class for one frame on resize so the animation restarts
   from a known phase instead of jumping. */
.clouds-paused .cloud-track { animation: none !important; }

/* ---------- content column ---------- */

.content {
  width: 100%;
  max-width: 480px;
  padding: 0 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.tagline {
  font-family: "Momo Trust Display", Georgia, serif;
  font-weight: 400;
  font-size: var(--tagline-size);
  line-height: 1.18;
  color: var(--navy);
  margin: clamp(6px, 1.5vh, 15px) 0 0;
  max-width: 22ch;
  text-wrap: pretty;
  text-wrap: balance;
}

.explain {
  font-family: "Momo Trust Sans", sans-serif;
  font-weight: 600;
  font-size: var(--explain-size);
  line-height: 1.35;
  color: var(--muted);
  /* Why: balanced wrapping keeps "forever." off a line of its own. The
     wider measure gives the balancer two usable lines to work with. */
  margin: var(--explain-gap) 0 0;
  max-width: 40ch;
  text-wrap: pretty;
  text-wrap: balance;
}

/* ---------- buddy + deck ---------- */

.deck-wrap {
  position: relative;
  margin-top: var(--deck-gap);
  width: var(--card-w);
  /* the buddy's visible top two thirds sit above the card */
  padding-top: calc(var(--buddy-w) * 1.024 * 0.64);
}

.buddy {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: var(--buddy-w);
  height: calc(var(--buddy-w) * 1.024);
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  cursor: pointer;
  z-index: 1;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
}
.buddy:focus { outline: none; }
.buddy:focus-visible {
  outline: 3px solid var(--navy);
  outline-offset: 3px;
  border-radius: 12px;
}

.buddy-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: auto;
  opacity: 0;
  transition: opacity 220ms ease;
  pointer-events: none;
}
.buddy-img.is-on { opacity: 1; }

.deck {
  position: relative;
  width: var(--card-w);
  height: calc(var(--card-h) + var(--peek-y));
  z-index: 2;
  /* Why: overflow-clip-margin is not implemented in Safari, so the previous
     "clip plus 60vw margin" cut the flying card off at the deck's edge there
     while Chromium showed it leave. The page clips instead (see .page). */
  overflow: visible;
}

.card {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--card-w);
  height: var(--card-h);
  border-radius: var(--card-r);
  border: var(--card-border) solid var(--navy);
  background: var(--tone-blue);
  /* Why: the text block is centred in the height that is left under the
     reserved shuffle row. The 12px bottom keeps a longest-prompt line off the
     card's inside edge; landing.js steps the size down if it still overflows. */
  padding: var(--card-row) var(--card-pad-x) 12px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: left;
  /* Why: the drag works in every direction, so the browser must never
     claim the gesture for page scrolling. */
  touch-action: none;
  will-change: transform;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.card[data-tone="blue"]   { background: var(--tone-blue); }
.card[data-tone="pink"]   { background: var(--tone-pink); }
.card[data-tone="purple"] { background: var(--tone-purple); }
.card[data-tone="orange"] { background: var(--tone-orange); }
.card[data-tone="mint"]   { background: var(--tone-mint); }

/* fully opaque so the buddy's legs never show through.
   Why: the two cards swap these classes, they are never rebuilt, so the
   stacking order comes from the class and never from DOM order. */
.card--peek {
  z-index: 1;
  opacity: 1;
  transform: translate(var(--peek-x), var(--peek-y)) scale(0.965);
  /* the back card is scenery: nothing on it is clickable or draggable */
  pointer-events: none;
}

/* Why: the spare is a ghost, exactly as in the app. It rests one further
   peek step back and fully transparent, so a deck at rest reads as two cards.
   The drag fades it in and lifts it into the peek slot in proportion to the
   pull, so the slot is never empty when a card leaves. */
.card--spare {
  z-index: 0;
  opacity: 0;
  transform: translate(calc(var(--peek-x) * 2), calc(var(--peek-y) * 2)) scale(0.93);
  pointer-events: none;
}

.card--front {
  z-index: 2;
  cursor: grab;
  pointer-events: auto;
}
.card--front:active { cursor: grabbing; }
/* the flying card must not swallow a tap meant for the new front card */
.card--leaving { pointer-events: none; }

.card-q {
  font-family: "Momo Trust Display", Georgia, serif;
  font-weight: 400;
  font-size: var(--q-size);
  line-height: 1.15;
  color: var(--navy);
  margin: 0;
}

.shuffle {
  position: absolute;
  top: 12px;
  right: 12px;
  width: var(--btn);
  height: var(--btn);
  border-radius: 50%;
  background: var(--yellow);
  border: var(--btn-border) solid var(--navy);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.shuffle svg { width: calc(var(--btn) * 0.46); height: calc(var(--btn) * 0.46); }
.shuffle:focus { outline: none; }
.shuffle:focus-visible {
  outline: 3px solid var(--navy);
  outline-offset: 3px;
}

:focus-visible {
  outline: 3px solid var(--navy);
  outline-offset: 3px;
}

/* ---------- primary pill ---------- */

.pill {
  margin: var(--gap) 0 0;
  background: var(--navy);
  color: var(--cream);
  font-family: "Momo Trust Sans", sans-serif;
  font-weight: 700;
  font-size: var(--pill-size);
  padding: calc(var(--pill-size) * 0.85) calc(var(--pill-size) * 1.6);
  border-radius: 999px;
  white-space: nowrap;
}

/* Why: the launch-day badge takes the pill's slot. Apple: any height at or
   above 40px, proportions unchanged (so width: auto), clear space of a
   quarter of the badge height on every side, never recolored or animated.
   40px was the floor and it read small next to the rest of the page - the
   pill grows with the viewport and a fixed badge did not, so on a wide
   window it shrank against everything around it. 48px on phones, 60px in
   the desktop grid, which is where reference landing pages sit. Clear space
   holds at both: 48px needs 12px and the tightest --gap above is 12px with
   the footer's 12px padding below; 60px needs 15px and the landscape band
   that owns the desktop grid runs --gap 16px above, a 1fr spacer plus 20px
   of footer padding below, and a 28px column gap to the deck. */
.store {
  margin: var(--gap) 0 0;
  display: inline-block;
  line-height: 0;
}
.store img {
  display: block;
  height: 48px;
  width: auto;
}

/* ---------- footer ---------- */

.footer {
  /* Why: the last flex item in the one-screen column, pushed to the bottom
     edge. Whatever height the hero does not use lands between the pill and
     here. Round 11 pays for the wider gaps above out of this padding. */
  margin-top: auto;
  width: 100%;
  max-width: 480px;
  padding: 12px 16px 16px;
  text-align: center;
}

.links {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  /* Why: 6px between the two phone rows as well as between the items. */
  gap: 6px;
  font-size: clamp(12px, 1.85svh, 14px);
}

/* Why: on a phone the family line does not fit on the links row, so it takes
   a full-width flex line of its own and everything else wraps below it. The
   inline separator only exists for the desktop one-row layout. */
.family { flex: 0 0 100%; }
.family-dot { display: none; }

.links a {
  color: var(--navy);
  text-decoration: none;
  font-weight: 600;
}
.links a:hover,
.links a:focus-visible { text-decoration: underline; }
.dot { color: var(--muted); }

/* Why: on a phone the family line sits on a row of its own above the nav
   links, where full navy made it read as the loudest thing in the footer.
   The muted navy drops it back without changing its size or link behaviour
   (5.76:1 on the cream, so it still clears 4.5:1). Desktop keeps it inline
   in link colour. */
@media (max-width: 719px) {
  .links a.family { color: var(--muted); }
}

/* ---------- narrow phones ---------- */

/* Why: the 18px floor only applies where the card is wide enough to hold
   the question under the reserved button row. */
@media (min-width: 360px) {
  :root { --q-size: max(20px, calc(var(--card-w) * 0.0867)); }
}

/* Why: the band between the landscape mode (<=560px tall) and a modern phone
   is the one portrait size where the round-7 deck does not fit on its own.
   The last ~10px comes out of the explain-to-deck gap, which is the first
   thing to give up, so a 320x568 phone still shows the whole page. */
@media (min-height: 561px) and (max-height: 600px) {
  :root {
    --deck-gap: 21px;
    /* Why: round 10's wider gaps are funded by slack this band does not have -
       at 320x568 the pill already sits at the fold. The three gaps keep their
       round-9 values here, so this size is unchanged. */
    --gap: clamp(12px, 2.4vh, 28px);
    --explain-gap: clamp(12px, 1.7vh, 18px);
  }
}

@supports (height: 1svh) {
  @media (min-height: 561px) and (max-height: 600px) {
    :root {
      --gap: clamp(12px, 2.4svh, 28px);
      --explain-gap: clamp(12px, 1.7svh, 18px);
    }
  }
}

/* ---------- desktop ---------- */

@media (min-width: 720px) {
  :root {
    /* Why: the bigger deck (card 330, buddy 56%) eats ~75px more height than
       round 6. The room comes from the sky band and the explain-to-deck gap,
       in that order, so 1280x800 still fits on one screen. */
    /* Why: the desktop band keeps its round-10 height - the tagline gap
       there is already right, so only the phone band closes up. */
    --sky-base: clamp(140px, 17vh, 190px);
    --sky-h: var(--sky-base);
    /* Why: 1.3 made the art 167px tall inside a 176px sky that already
       offsets it by 9%, so the bottom of the wordmark was clipped. At 1.15
       the art ends at 93% of the sky height at every size. */
    --wordmark-w: min(calc(var(--sky-base) * 1.15), 30vw);
    --tagline-size: clamp(22px, 3.4vh, 32px);
    --explain-size: clamp(14px, 1.9vh, 16px);
    --card-w: min(360px, 41.5vh);
    --q-size: max(23px, calc(var(--card-w) * 0.0904));
    /* Why: round 11 widens the two gaps that read as cramped - the explain
       line to the buddy (+20px) and the deck to the pill (+16px). The room
       comes out of the slack that used to pool under the pill. */
    --deck-gap: clamp(35px, 4.4vh, 39px);
    --gap: clamp(31px, 3.9vh, 34px);
    --btn: 58px;
  }
  .explain { max-width: 56ch; }
  .tagline { max-width: 24ch; }

  /* Why: a laptop window is wide enough that 48px reads small - desktop
     references sit around 60px, and --gap is 31px here so the space above is
     already clear. Below, the footer's own 12px of padding is the only gap
     guaranteed when the column has no slack to pool, so 3px makes up the 15px
     a 60px badge needs. */
  .store { margin-bottom: 3px; }
  .store img { height: 60px; }

  /* Why: on desktop the family line fits inline with the links, so it gives
     up its own row and its separator appears. The footer drops its 480px
     measure here - that cap is what forced the one row to wrap back to two. */
  .footer { max-width: none; }
  .family { flex: 0 1 auto; }
  .family-dot { display: inline; }
}

@supports (height: 1svh) {
  @media (min-width: 720px) {
    :root {
      --sky-base: clamp(140px, 17svh, 190px);
      --tagline-size: clamp(22px, 3.4svh, 32px);
      --explain-size: clamp(14px, 1.9svh, 16px);
      --card-w: min(360px, 41.5svh);
      --deck-gap: clamp(35px, 4.4svh, 39px);
      --gap: clamp(31px, 3.9svh, 34px);
    }
  }
}

/* ---------- short viewports (landscape phone, small windows) ---------- */

/* Why: below ~560px tall there is no height left to scale into. Everything
   height-relative collapses while the text does not, and the layout piles up
   on itself. This mode stops scaling by viewport height entirely, uses fixed
   phone-sized values, and lets the page scroll instead of trying to fit. */
@media (max-height: 560px) {
  /* Why: unconditional. The scroll lock exists so a fitted page cannot end up
     scrolled; here the page genuinely does not fit, so the lock would clip
     content that has nowhere else to go. */
  html.js body {
    height: auto;
    min-height: 100vh;
    min-height: 100svh;
    overflow: auto;
  }

  :root {
    --sky-base: 96px;
    --sky-h: 96px;
    --wordmark-w: 150px;
    --wordmark-top: 3px;

    --tagline-size: 22px;
    --explain-size: 14px;

    --card-w: 240px;
    --q-size: 21px;
    --buddy-w: 110px;
    /* Why: --btn-border is deliberately not overridden here - the shuffle
       ring matches the card border at every size, landscape included. */
    --btn: 46px;

    --pill-size: 14px;
    --gap: 16px;
    --explain-gap: 10px;
    --deck-gap: 18px;
  }

  .tagline { margin-top: 8px; }

  .footer {
    padding: 20px 16px 28px;
  }
  .links { font-size: 13px; }
}

/* two columns once there is width to put the text beside the deck */
@media (max-height: 560px) and (min-width: 640px) {
  .content {
    max-width: 820px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    /* Why: the deck spans every row, so without the two 1fr spacers the
       leftover height is shared out between the text rows and the left
       column drifts apart. The spacers take it instead and the text stays
       a block, centred against the deck. */
    grid-template-rows: 1fr auto auto auto 1fr;
    column-gap: 28px;
    align-items: center;
    justify-items: start;
    text-align: left;
  }
  .tagline  { grid-column: 1; grid-row: 2; max-width: 20ch; margin-top: 0; }
  .explain  { grid-column: 1; grid-row: 3; max-width: 42ch; }
  .pill, .store { grid-column: 1; grid-row: 4; }
  /* Why: the badge reads too small once the page is this wide - desktop
     references sit around 60px. Clear space still holds: --gap 16px above,
     the 1fr spacer plus the footer's 20px padding below, 28px to the deck. */
  .store img { height: 60px; }
  .deck-wrap {
    grid-column: 2;
    grid-row: 1 / -1;
    margin-top: 0;
    justify-self: end;
  }
}

/* ---------- reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .cloud-track--back,
  .cloud-track--front {
    animation: none;
  }
  .cloud-track--back { transform: translateX(calc(-1 * var(--track-w))); }
  .cloud-track--front { transform: translateX(0); }
  .buddy-img { transition: none; }
  .card { transition: none !important; }
}
