/* ═══════════════════════════════════════════════════════════════════════════════════════════
   Pawcast landing — PAGE LAYER
   Everything a *component* does lives in assets/components.css (mirror of the DS).
   This file only holds page chrome (announce, header, hero, sticky bar) and the organisms that
   are specific to this landing page. Never hardcode a value that has a token.

   R1 · body is the floor. --font-size-small and --font-size-label do not appear in this file.
        --font-size-fine appears exactly twice, both on the allow-list (announcement micro copy,
        legal/© bar — the latter comes from .pc-footer__bar in components.css).
   R4 · reuse: buttons are .pc-btn, chips are .pc-badge/.pc-label, icon-only controls are
        .pc-icon-btn, ticks/stars/chevrons are Nucleo symbols from the sprite.
   ═══════════════════════════════════════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }                   /* anchor jumps when Lenis is OFF (reduced-motion) */
:where(section, [id]) { scroll-margin-top: 7rem; }  /* clear the sticky header on anchor jumps */
/* Lenis: when it drives the scroll it adds .lenis.lenis-smooth to <html>. Kill native smooth then,
   or the two fight and scrolling only "catches up" on release. */
html.lenis, html.lenis body { height: auto; }
html.lenis { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }

body {
  margin: 0; background: var(--color-bg); color: var(--color-text);
  font-family: var(--font-family-body); font-weight: 600;
  font-size: var(--font-size-body); line-height: var(--line-height-body);
  -webkit-font-smoothing: antialiased;
  transition: background var(--duration-base) var(--ease-standard), color var(--duration-base) var(--ease-standard);
}
img { max-width: 100%; display: block; }
h1, h2, h3, h4 { font-family: var(--font-family-display); font-weight: 700; margin: 0; line-height: 1.05;
  text-wrap: balance; }   /* no orphan word dangling on the last line of a display heading */
p { margin: 0; }
a { color: inherit; }
strong { font-weight: 800; color: inherit; }
s { color: var(--color-text-muted); }
/* selection carries the brand too — Fetch behind ink is the Button pairing, AAA-large by rule */
::selection { background: var(--color-primary); color: var(--color-text-on-primary); }

/* em highlight inside a heading = TEXT COLOUR only, never an underline.
   On a Forest band it switches to Sunbeam (Fetch on Forest is unreadable). */
h1 em, h2 em, .h1 em, .h2 em { font-style: normal; color: var(--color-primary); }
.pc-on-dark h1 em, .pc-on-dark h2 em { color: var(--color-accent); }

/* ---- a11y utilities ---- */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.skiplink { position: absolute; left: var(--space-16); top: -100%; z-index: 200;
  background: var(--color-surface); color: var(--color-text); font-weight: 800;
  padding: var(--space-12) var(--space-20); border-radius: var(--radius-md);
  border: var(--border-medium) solid var(--color-border); text-decoration: none;
  transition: top var(--duration-base) var(--ease-standard); }
.skiplink:focus { top: var(--space-16); }

/* ---- section rhythm + text roles ----
   Airy on purpose (ref: Happly's section rhythm; SURFACES.md says landing = space-64…96): big
   breath BETWEEN sections, generous but tighter air INSIDE them — bold blocks need clear water. */
.section { padding-block: var(--space-96); }
.section--tight { padding-block: var(--space-80); }
/* full-viewport sections: the section fills the browser height MINUS the sticky header
   (--header-h = the header's real box: rung-S control + its block padding), content centres, and
   in splits the MEDIA column is sized from that same available height via its own aspect ratio —
   the image is as tall as the screen allows and the text takes the rest. */
:root { --header-h: calc(var(--control-h-s) + 2 * var(--space-12)); }
@media (min-width: 820px) {
  .section--fullh { display: grid; align-items: center; min-height: calc(100svh - var(--header-h)); }
  .section--fullh > .pc-container { width: 100%; }
  /* width = (available height − meta/padding slack) × aspect, capped so huge monitors don't
     balloon it. phone frame = 9/13; appshot = 750/1624. */
  .section--fullh .phone { max-width: min(34rem, calc((100svh - var(--header-h) - 12rem) * 0.6923)); }
  .section--fullh .appshot { max-width: min(28rem, calc((100svh - var(--header-h) - 7rem) * 0.4613)); }
}
.h1 { font-size: var(--font-size-h1); line-height: var(--line-height-h1); letter-spacing: var(--letter-spacing-h1); }
.h2 { font-size: var(--font-size-h2); line-height: var(--line-height-h2); letter-spacing: var(--letter-spacing-h2); }
.h3 { font-size: var(--font-size-h3); line-height: var(--line-height-h3); letter-spacing: var(--letter-spacing-h3); }
.h4 { font-size: var(--font-size-h4); line-height: var(--line-height-h4); letter-spacing: var(--letter-spacing-h4); }
.lead { font-size: var(--font-size-lead); line-height: var(--line-height-lead); color: var(--color-text-muted); }
.muted { color: var(--color-text-muted); }
.center { text-align: center; }
/* Measure guard: no reading paragraph runs past a comfortable line length on a 2560 canvas */
.pc-split__body p, .specs__note, .footer__tagline { max-width: 60ch; }
/* Split headlines take the H3 metrics: the copy deck's split headlines run 45–52 characters and
   a half-width column at full H2 size forces an awkward 3rd line. One step down = 2 clean lines,
   and it reads as correct hierarchy under the full-width centred section heads. */
.pc-split__body .h2 { font-size: var(--font-size-h3); line-height: var(--line-height-h3);
  letter-spacing: var(--letter-spacing-h3); }
/* the two text-heaviest splits (#math, #setup) also widen their TEXT column to 7/5 — at 50/50
   their 47–52-char headlines still ran to a 3rd line even at H3. Media is height-sized anyway,
   so the width belongs to the words. --wideleft: text col first; --wideright: text col second. */
@media (min-width: 820px) {
  .pc-split--wideleft { grid-template-columns: 7fr 5fr; }
  .pc-split--wideright { grid-template-columns: 4.5fr 7.5fr; }   /* #setup's 52-char headline needs the extra half-column */
}
.closer { margin-top: var(--space-8); }
/* on a Forest band the PAGE text roles collapse to cream (the DS classes are handled by
   .pc-on-dark rules in components.css — don't duplicate them here) */
.pc-on-dark .lead, .pc-on-dark .muted { color: var(--color-text-on-dark); opacity: .88; }

/* ---- full-bleed colour band ---- */
/* box-shadow + clip-path instead of a 100vw pseudo-element: 100vw includes the scrollbar and
   caused a hairline of horizontal scroll on Windows. */
.band { --band-bg: var(--color-surface); background: var(--band-bg);
  box-shadow: 0 0 0 100vmax var(--band-bg); clip-path: inset(0 -100vmax); }
/* #memory: soft mint break between the cream stretches (user ask). Ink/checks are ~12:1 on it;
   the muted lead is large text, 6:1 ≥ the 4.5 AAA-large bar. */
.band--memory { --band-bg: var(--color-park-soft); }
/* #pillars: soft peach — same separation move, warmer family (the cards stay cream on top) */
.band--pillars { --band-bg: var(--color-fetch-soft); }

/* ═══ 1 · ANNOUNCEMENT BAR ═══ */
.announce { background: var(--color-secondary); color: var(--color-text-on-dark); overflow: hidden;
  border-bottom: var(--border-medium) solid var(--color-border); }
.announce__marquee { display: flex; width: max-content; animation: pc-marquee 40s linear infinite; }
.announce:hover .announce__marquee { animation-play-state: paused; }
.announce__track { display: flex; flex: none; margin: 0; padding: var(--space-8) 0; list-style: none; }
/* --font-size-fine — allow-list #4 (announcement-bar micro copy) */
.announce__item { white-space: nowrap; padding-inline: var(--space-16); font-weight: 800;
  font-size: var(--font-size-fine); line-height: var(--line-height-fine); text-transform: uppercase;
  letter-spacing: .06em; display: inline-flex; align-items: center; gap: var(--space-12); }
/* long rule-lines between the three text groups, so they read as separate thoughts */
.announce__sep { display: inline-block; width: clamp(2.5rem, 4vw, 4.5rem); height: 2px;
  background: currentColor; opacity: .45; }
/* the kit link is the ONE thing in the bar that should pop — Sunbeam on Forest (7.2:1, bold) */
.announce .announce__cta { color: var(--color-accent); font-weight: 800; }
@keyframes pc-marquee { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .announce__marquee { animation: none; } }

/* ═══ SITE HEADER ═══ */
.site-header { position: sticky; top: 0; z-index: 50; width: 100%;
  background: transparent; border-bottom: var(--border-medium) solid transparent;
  transition: background var(--duration-base) var(--ease-standard),
              border-color var(--duration-base) var(--ease-standard),
              box-shadow var(--duration-base) var(--ease-standard); }
.site-header.scrolled { background: var(--color-surface); border-bottom-color: var(--color-border);
  box-shadow: 0 6px 18px -10px var(--shadow-ambient-light); }
.site-header__inner { display: flex; align-items: center; gap: var(--space-24); padding-block: var(--space-12); }
/* the wordmark is a link, so the UA underline has to go too — "no underline" is the whole rule,
   and it would surface the moment anyone puts text next to the logo */
.site-header__logo { display: inline-flex; flex: none; text-decoration: none; }
/* .pc-logo (DS mirror) hardcodes the old wordmark's 541/107 ratio — tuckr-text.png is 355/150. */
.tuckr-logo { aspect-ratio: 355 / 150; }
.site-header__menu { flex: 1; display: flex; justify-content: center; gap: var(--space-32); font-weight: 700; }
.site-header__actions { flex: none; display: flex; align-items: center; gap: var(--space-16); }
.nav-toggle { display: none; }   /* hamburger — mobile only */
/* NO cream override for the unscrolled state — at scroll 0 the header sits between the announce
   bar and the hero, over the CREAM page bg (it only overlaps the hero once it sticks, and by then
   .scrolled has given it a surface bg). Cream links here were invisible on load — user-caught. */

/* ═══ MOBILE NAV ═══ full-screen overlay */
.mobile-nav { position: fixed; inset: 0; z-index: 100; background: var(--color-bg);
  display: flex; flex-direction: column; padding: var(--space-16) 0 var(--space-48);
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: opacity var(--duration-base) var(--ease-standard),
              transform var(--duration-base) var(--ease-standard), visibility var(--duration-base); }
.mobile-nav.open { opacity: 1; visibility: visible; transform: none; }
.mobile-nav__top { display: flex; align-items: center; justify-content: space-between;
  padding-inline: var(--container-gutter); }
.mobile-nav__links { display: flex; flex-direction: column; gap: var(--space-24);
  padding: var(--space-48) var(--container-gutter) 0; }
.mobile-nav__links a:not(.pc-btn) { font-family: var(--font-family-display); font-weight: 600;
  font-size: var(--font-size-h3); line-height: 1.1; letter-spacing: var(--letter-spacing-h3);
  color: var(--color-text); text-decoration: none;
  transition: color var(--duration-base) var(--ease-standard); }
.mobile-nav__links a:not(.pc-btn):hover { color: var(--color-primary); }
.mobile-nav__links .pc-btn { margin-top: var(--space-16); align-self: flex-start; }
body.nav-locked { overflow: hidden; }
@media (prefers-reduced-motion: reduce) { .mobile-nav { transform: none; } }

@media (max-width: 820px) {
  .site-header__menu { display: none; }
  .header-cta { display: none; }        /* header CTA moves into the overlay */
  .nav-toggle { display: inline-grid; }
}
@media (min-width: 821px) { .mobile-nav { display: none; } }

/* ═══ 2 · HERO ═══ full-bleed poster; the Forest fallback keeps it on-brand until media lands */
.hero { position: relative; isolation: isolate; width: 100%;
  min-height: 100vh; min-height: 100svh; display: grid; place-items: center;
  padding-block: var(--space-80); overflow: hidden; background: var(--color-secondary); }
.hero__bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  object-position: center 35%; z-index: -2; filter: brightness(1.28) saturate(1.06); }
/* SANCTIONED LITERALS (here + the text-shadow below): rgba washes of ink-950 #17110c over the
   hero PHOTO. A photo doesn't flip with the theme, so its legibility scrim must not either — a
   token would wrongly invite theming it. The only raw values in the page layer, on purpose. */
.hero__scrim { position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgba(23,17,12,.45) 0%, rgba(23,17,12,.6) 50%, rgba(23,17,12,.78) 100%); }
/* nudge the stack up by ~the announce+header height so it optically centres in the VISIBLE
   viewport (the hero box is 100svh but starts below the bar + sticky header) */
.hero__inner { width: 100%; text-align: center; margin-bottom: 5rem; }
.hero__stack { display: grid; justify-items: center; gap: 0; color: var(--color-text-on-dark); }
.hero h1, .hero__sub, .hero__note { text-shadow: 0 1px 14px rgba(23,17,12,.45); }
/* Hero display: its own clamp (~112px @1440, under the global display ceiling) so the two-sentence
   headline sits on two lines with the CTA still above the fold. */
.hero h1 { font-size: clamp(2rem, 0.6479rem + 7.2113vw, 9.6rem); line-height: 1.08;
  letter-spacing: var(--letter-spacing-display); max-width: 22ch; color: var(--color-text-on-dark); }
.hero__sub { font-size: var(--font-size-lead); line-height: var(--line-height-lead);
  max-width: 52ch; margin-top: var(--space-32); text-wrap: pretty; color: var(--color-text-on-dark); }
.hero__cta { justify-content: center; margin-top: var(--space-40); }
.hero__note { margin-top: var(--space-20); color: var(--color-text-on-dark); }
/* $99 — the key number pops in Sunbeam (large bold on the ink scrim ⇒ AAA-large) */
.hero__note strong { color: var(--color-accent); font-size: 1.3em; }

/* ═══ 3 · TRUST BULLETS ═══ Sunbeam tint band — taller and warmer so the three promises land
   right after the hero instead of reading as a hairline (user-requested pop). Ink on sun-soft
   is ~13:1; the green tick is a decorative affordance, the text carries the meaning. */
.trustband { --band-bg: var(--color-sun-soft); border-block: var(--border-medium) solid var(--color-border); }
.trustrow { list-style: none; margin: 0; display: flex; flex-wrap: wrap;
  justify-content: center; gap: var(--space-24) var(--space-64); padding-block: var(--space-32); }
.trustrow li { display: flex; align-items: center; gap: var(--space-12); font-weight: 800; }
.trustrow .ico { color: var(--color-success); }

/* ═══ 4 · STAT BAR ═══ */
.statbar { display: grid; grid-template-columns: repeat(2, 1fr);
  border: var(--border-bold) solid var(--color-border); border-radius: var(--radius-xl);
  overflow: hidden; background: var(--color-surface); }
@media (min-width: 860px) { .statbar { grid-template-columns: repeat(4, 1fr); } }
.statbar__cell { padding: var(--space-48) var(--space-32); display: grid; gap: var(--space-8);
  justify-items: start; align-content: start;
  border-top: var(--border-medium) solid var(--color-border-subtle);
  border-left: var(--border-medium) solid var(--color-border-subtle);
  transition: background var(--duration-base) var(--ease-standard); }
.statbar__cell .iconchip { margin-bottom: var(--space-12); }
.statbar__cell:hover { background: var(--color-border-subtle); }
.statbar__cell:nth-child(-n+2) { border-top: 0; }
@media (max-width: 859px) { .statbar__cell:nth-child(odd) { border-left: 0; } }
@media (min-width: 860px) { .statbar__cell:nth-child(-n+4) { border-top: 0; }
  .statbar__cell:nth-child(4n+1) { border-left: 0; } }
.statbar__cell b { display: flex; align-items: center; gap: var(--space-8);
  font-family: var(--font-family-display); font-size: var(--font-size-h2); line-height: 1;
  letter-spacing: var(--letter-spacing-h2); }
.statbar__star { color: var(--color-primary); }
/* R1: a stat key is BODY size — its label-ness is weight + caps + tracking + colour, never a
   smaller step. Colour is --color-text-muted (8:1), NOT Fetch: Fetch on cream measures 2.76:1,
   and this is real text, not a graphic. Same choice as .pc-spec__k and .mathchart__title. */
.statbar__k { font-weight: 800; text-transform: uppercase; letter-spacing: .08em; color: var(--color-text-muted); }
.statbar__sub { color: var(--color-text-muted); }

/* ---- play disc — shared by the reel strip and the phone mockup (one atom, R4) ---- */
.playdisc { position: absolute; inset: 0; margin: auto; width: var(--control-h-m); height: var(--control-h-m);
  border-radius: var(--radius-pill); background: var(--color-surface); color: var(--color-text);
  border: var(--border-bold) solid var(--color-border); display: grid; place-items: center;
  transition: transform var(--duration-slow) var(--ease-in-out); }
.playdisc .ico { width: 45%; height: 45%; vertical-align: 0; }

/* ═══ 5 · REEL STRIP ═══ sticker energy: each card sits at a slight tilt and straightens on
   hover — "moves like zoomies". `rotate` is its own property so it never fights the lift. */
/* Ten recaps in ONE horizontal scroll-snap track (was a 4-up auto-fit grid). The grid could not
   hold ten 9:16 cards: auto-fit would either wrap them into rows of leftovers or crush them to
   ~120px each. A scroller keeps every card at a readable width and behaves identically on a phone
   and a desktop — no second row that only exists above 768px, which is how the pre-DS homepage
   quietly hid five of its ten. Bleeds to the viewport edge so the last card is visibly cut off:
   that overflow IS the affordance telling you there is more to drag. */
.reelstrip { list-style: none; margin: 0; padding: 0 0 var(--space-16);
  display: flex; gap: var(--space-24);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
  /* Cancel .pc-container's inset so the track runs edge to edge, then re-pad the content so the
     first card still lines up with the headline above it. It MUST be --container-gutter and not a
     --space-* step: the gutter is clamp(20px, 5vw, 120px) and space-32 bottoms out at 24px, so at
     390px the track hung 4px past the container on each side and gave the whole page a horizontal
     scrollbar. Bleed exactly as far as the gutter you are cancelling. */
  margin-inline: calc(var(--container-gutter) * -1); padding-inline: var(--container-gutter);
  scrollbar-width: thin; scrollbar-color: var(--color-border-subtle) transparent; }
.reelstrip::-webkit-scrollbar { height: 6px; }
.reelstrip::-webkit-scrollbar-track { background: transparent; }
.reelstrip::-webkit-scrollbar-thumb { background: var(--color-border-subtle); border-radius: var(--radius-pill); }
.reel { flex: 0 0 clamp(13rem, 42vw, 15rem); scroll-snap-align: start;
  display: grid; gap: var(--space-12); }
/* The tilt needs breathing room or the rotated corners clip against the scroll container. */
.reelstrip { padding-block: var(--space-8) var(--space-20); }
.reel__frame { position: relative; aspect-ratio: 9/16; border-radius: var(--radius-xl);
  border: var(--border-bold) solid var(--color-border); overflow: hidden;
  background: linear-gradient(155deg, var(--color-primary), var(--color-accent));
  box-shadow: var(--elevation-card);
  transition: transform var(--duration-slow) var(--ease-in-out), rotate var(--duration-slow) var(--ease-in-out); }
.reel:nth-child(odd) .reel__frame { rotate: -1.4deg; }
.reel:nth-child(even) .reel__frame { rotate: 1.4deg; }
.reel:hover .reel__frame { transform: translateY(-6px); rotate: 0deg; }
/* ── Card chrome scales with the CARD, not the page (2026-08-10, user: "nhỏ lại để không dính
   tầm nhìn video") ──────────────────────────────────────────────────────────────────────────
   The tag was a real .pc-badge and the meta real body text, so both rode the DS control ladder
   and type ramp: a 42–58px pill and 20–32px type over a 300px-wide thumbnail. Between them they
   covered roughly a third of the frame — you were reading the label instead of watching the clip.
   Same call as the phone mockup below: this is chrome drawn ON a video, i.e. depicted UI, so it
   scales off the frame (`cqw`) and holds its proportion whether a card is 260px or 300px wide.
   Kept generous enough to stay legible over the scrim; the point is to get out of the way, not
   to disappear. */
.reel__frame { container-type: inline-size; }
/* ×2 on the type (user call 2026-08-10, after the first pass went too far the other way):
   3.4 → 6.8cqw, i.e. ~21px on a 313px card. The PADDING is deliberately NOT doubled — at 2.2/6cqw
   the pill would have run past half the card width. Doubling the label while keeping the pill
   snug is what buys the size back without giving the frame away. */
.reel__tag { position: absolute; top: 3.5cqw; left: 3.5cqw; z-index: 2;
  padding: 1.3cqw 3.4cqw; border-radius: var(--radius-pill);
  background: var(--color-sun-soft); color: var(--color-text);
  font-family: var(--font-family-body); font-weight: 800; font-size: 6.8cqw;
  letter-spacing: .01em; white-space: nowrap; }
/* the disc is an affordance, not a target — 15cqw ≈ 45px on a 300px card (was a 58px control) */
.reel__frame > .playdisc { width: 15cqw; height: 15cqw; border-width: 1.1cqw; }
.reel:hover .playdisc { transform: scale(1.08); }
/* at 9.2cqw a name like "Pepper's Thursday" no longer fits on one line beside the duration, so the
   row wraps instead of squeezing the duration off the card; the scrim below covers both lines. */
.reel__meta { position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
  gap: 0 3cqw; padding: 4.5cqw 4.5cqw 4cqw; line-height: 1.15;
  font-family: var(--font-family-body); font-weight: 800; font-size: 9.2cqw; }
.reel__meta span { flex: none; font-weight: 700; font-size: 8.4cqw; }
@media (prefers-reduced-motion: reduce) {
  .reel__frame { rotate: 0deg; }
  .reel:hover .reel__frame, .reel:hover .playdisc { transform: none; }
}

/* ═══ 6 · THE MATH ═══ chart card */
.mathchart { margin: 0; display: grid; gap: var(--space-24); background: var(--color-surface);
  border: var(--border-bold) solid var(--color-border); border-radius: var(--radius-xl);
  padding: var(--space-40); box-shadow: var(--elevation-card); }
.mathchart__title { font-weight: 800; text-transform: uppercase; letter-spacing: .08em;
  color: var(--color-text-muted); }
.mathchart__row { display: grid; grid-template-columns: 1fr; gap: var(--space-8); align-items: center; }
@media (min-width: 560px) { .mathchart__row { grid-template-columns: auto 1fr auto; gap: var(--space-16); } }
.mathchart__k { font-weight: 800; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
.mathchart__bar { display: block; height: 1.6rem; border-radius: var(--radius-pill);
  background: var(--color-border-subtle); border: var(--border-medium) solid var(--color-border);
  overflow: hidden; }
.mathchart__bar > span { display: block; height: 100%; background: var(--color-success); }
.mathchart__row--dark .mathchart__bar > span { background: var(--color-primary); }
.mathchart__v { font-family: var(--font-family-display); font-size: var(--font-size-h3); line-height: 1;
  letter-spacing: var(--letter-spacing-h3); min-width: 2.5ch; text-align: right; }
.mathchart__note { padding-top: var(--space-16);
  border-top: var(--border-thin) solid var(--color-border-subtle); color: var(--color-text-muted); }
/* the 91% is INK, not Fetch — Fetch on surface is 2.6:1 and this sits inside a sentence.
   The display face + size do the shouting; the colour stays readable. */
.mathchart__note strong { color: var(--color-text); font-family: var(--font-family-display);
  font-size: 1.5em; line-height: 1; }

/* ═══ 7 · THE OUTPUT ═══ Forest keepsake band + phone mockup */
.keepsake { background: var(--color-secondary); color: var(--color-text-on-dark); }
.phone { max-width: 380px; width: 100%; margin-inline: auto;
  border: var(--border-bold) solid var(--color-border); border-radius: var(--radius-xl);
  background: var(--color-surface); color: var(--color-text);
  box-shadow: var(--elevation-card); overflow: hidden; }
.phone__frame { position: relative; aspect-ratio: 9/16;
  background: linear-gradient(160deg, var(--color-primary), var(--color-accent));
  /* the phone is its own sizing world — see .phone__ui */
  container-type: inline-size; }

/* ═══ THE PHONE'S INNER UI ═══ rebuilt 2026-08-10 to the pre-DS structure and, more importantly,
   to the pre-DS SCALE. Read this before "fixing" the units below.

   What was wrong: the badge was a real `.pc-badge` and the caption real body text, so both sat on
   the DS type ramp and control ladder — a 42–58px pill and 20–32px copy inside a ~300px phone. The
   pill spanned most of the screen width, shoved "Pawcast" out from under it, and the caption wrapped
   across the play disc. Four absolutely-positioned children were all competing for two corners.

   Why em/cqw and not DS tokens: this is a PICTURE OF AN APP, not page furniture. The text inside it
   is depicted UI, at the same layer of reality as the text baked into assets/media/app/*.webp — and
   nobody would enlarge the type inside a screenshot to satisfy the page's type scale. R1 ("body is
   the floor") governs what the reader reads; it does not govern what the reader is looking at a
   picture of. So the chrome scales with the DEVICE (`cqw` off .phone__frame), which is why it holds
   its proportions whether the split column is 260px or 380px wide. Same reasoning as .appshot,
   which dodges this entirely by being a flat image.
   Ratios are the pre-DS ones (12 / 9 / 11.5 / 10px at a 340px frame), expressed as cqw. */
/* Sizes are the pre-DS ratios EXACTLY: 12 / 9 / 11.5 / 10px at a 340px frame = 3.5 / 2.65 / 3.4 /
   2.95 cqw. First pass used 4 / 3 / 3.9 / 3.4 — ~15% over, and the user called it (2026-08-10).
   Don't nudge these back up: the whole point is that the reader watches the clip, not the chrome. */
.phone__ui { position: absolute; inset: 0; z-index: 2; pointer-events: none;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 5.5cqw 4cqw 4.5cqw; font-family: var(--font-family-body); }
.phone__ui-top { display: flex; align-items: center; justify-content: space-between;
  gap: 3cqw; }
.phone__brand { font-size: 3.5cqw; font-weight: 800; letter-spacing: .01em;
  color: var(--color-text-on-dark); }
.phone__tag { flex: none; padding: .9cqw 2.4cqw; border-radius: var(--radius-pill);
  background: var(--color-accent); color: var(--color-text-on-primary);
  font-size: 2.65cqw; font-weight: 800; letter-spacing: .04em; white-space: nowrap; }
/* the bottom strip carries its own scrim, so the frame-wide ::after only has to lift the top row */
.phone__ui-bottom { margin: 0 -4cqw -4.5cqw; padding: 8cqw 4cqw 4.5cqw;
  background: linear-gradient(0deg, rgba(23,17,12,.78) 0%, rgba(23,17,12,.45) 55%, transparent 100%); }
.phone__caption { margin: 0 0 1.4cqw; font-size: 3.4cqw; font-weight: 600; line-height: 1.45;
  color: var(--color-text-on-dark); text-wrap: pretty; }
.phone__dur { font-size: 2.95cqw; font-weight: 800; color: var(--color-accent); }
/* the old mockup had no play disc at all (the video just autoplayed). Keep it — the poster needs an
   affordance before the loop loads — but at 13cqw it reads as a hint, not a button. */
.phone__frame > .playdisc { width: 13cqw; height: 13cqw; border-width: 1cqw; }
/* All three lines (name · what was in it · the share row) sit at ONE size, one step under body —
   user call 2026-08-10. This is a deliberate R1 exception and the third allow-listed use of
   --font-size-fine on the page (with the announcement bar and the legal row): the block is a
   CAPTION for the mockup beside it, not a reading paragraph, and at body size it competed with the
   section's own lead copy for attention two columns away. Using the token rather than a magic
   number keeps it auditable — grep --font-size-fine and you find all three.
   Hierarchy inside the block is then family + colour, exactly as R1 prescribes: the recap name
   keeps Fredoka, the rest is body weight in muted ink. Same size, different voice. */
.phone__meta { padding: var(--space-20); display: grid; gap: var(--space-4);
  font-size: var(--font-size-fine); line-height: 1.45; }
.phone__meta b { font-family: var(--font-family-display); font-size: inherit; }
.phone__meta small { font-size: inherit; color: var(--color-text-muted); }
.phone__share { margin-top: var(--space-12); padding-top: var(--space-12);
  border-top: var(--border-thin) solid var(--color-border-subtle);
  font-size: inherit; font-weight: 800; color: var(--color-text-muted); }

/* ═══ 8 / 11 · APP SCREENSHOT FRAME ═══ */
/* the gradient is the placeholder: assets/media/app/* isn't shot yet, so a missing file reads as
   an intentional frame instead of a broken-image hole. The real screenshot covers it. */
.appshot { border: var(--border-bold) solid var(--color-border); border-radius: var(--radius-xl);
  overflow: hidden; box-shadow: var(--elevation-card);
  background: linear-gradient(165deg, var(--color-accent), var(--color-primary));
  max-width: 320px; width: 100%; margin-inline: auto; aspect-ratio: 750/1626; }
.appshot img,
.appshot__vid { width: 100%; height: 100%; object-fit: cover; display: block; }
/* the frame is 750/1626 and the videos are 786/1704 — same ratio to 4 decimal places, so `cover`
   crops nothing. Stated because it is the kind of thing a re-export quietly breaks. */

/* ═══ 9 · THE DIARY ═══ day-1 → day-1,095, bookended by the two markers */
/* TRIPTYCH: day-1 screen · copy · day-1,095 screen. Rebuilt 2026-08-10 — see index.html.
   Columns are top-aligned (`align-items: start`), matching the pre-DS build: the two "Before" /
   "After 3 years" labels then sit on one line and the size difference shows by the right frame
   running FURTHER DOWN. Centring them instead staggered the labels and read as two loose objects
   rather than one comparison. */
.diary { display: grid; gap: var(--space-40); justify-items: center; text-align: center; }
@media (min-width: 1000px) {
  /* symmetric now that the frames match — the asymmetric 0.82/1.9/1 existed to absorb two
     different frame widths and left the whole band visibly off-centre. */
  .diary { grid-template-columns: minmax(0, 1fr) minmax(0, 2.1fr) minmax(0, 1fr);
    align-items: start; gap: var(--space-48); }
  .diary__mid { grid-column: 2; }
}
.diary__mid { display: grid; gap: var(--space-40); justify-items: center; }
.diary__side { display: grid; gap: var(--space-16); justify-items: center; margin: 0; }
.diary__screen { width: 100%; overflow: hidden; aspect-ratio: 750/1626;
  border-radius: var(--radius-xl); border: var(--border-bold) solid var(--color-border);
  box-shadow: var(--elevation-card); background: var(--color-surface); }
.diary__screen img { display: block; width: 100%; height: 100%; object-fit: cover;
  object-position: top; }
/* ONE size for both frames (user call 2026-08-10). I had them at 11rem / 14rem on the theory that
   the size gap "was" the three years. It wasn't — it just read as two mismatched screenshots, and
   it broke the comparison: when the frames differ you can't tell whether the right screen holds
   more entries or is simply bigger. Equal frames put the difference where it belongs, in the
   CONTENT (one entry vs a full week). Also fixes the alignment axis — two identical frames
   top-aligned either side of the copy is a grid; two different ones was a guess. */
.diary__screen--sm,
.diary__screen--lg { max-width: min(13rem, 58vw); }
.diary__marker { display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: var(--space-12); font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
.diary__stamp { display: grid; gap: var(--space-4); font-weight: 800;
  text-transform: uppercase; letter-spacing: .08em; }
.diary__stamp em { font-family: var(--font-family-display); font-style: italic; font-weight: 600;
  text-transform: none; letter-spacing: 0; font-size: var(--font-size-h4);
  line-height: var(--line-height-h4); }
.diary__stamp--accent { color: var(--color-accent); }
/* the ladder — one heading, four lines, each a step brighter than the last */
/* H4, not H2: the ladder used to own a full-width centred column. Inside the triptych's middle
   column H2 wrapped EVERY rung onto two lines ("Day 365 is / irreplaceable.") and the four-beat
   rhythm the copy is built on collapsed. One rung, one line — that is the whole device. */
.diary__ladder { display: grid; gap: var(--space-8); font-size: var(--font-size-h4);
  line-height: var(--line-height-h4); letter-spacing: var(--letter-spacing-h4); max-width: 30ch; }
.diary__ladder span:nth-child(1) { opacity: .75; }
.diary__ladder span:nth-child(2) { opacity: .85; }
.diary__ladder span:nth-child(4) { color: var(--color-accent); }
.diary__copy { display: grid; gap: var(--space-16); max-width: 56ch; }
.diary__coda { font-family: var(--font-family-display); font-weight: 600;
  font-size: var(--font-size-h4); line-height: var(--line-height-h4); color: var(--color-accent);
  margin-top: var(--space-16); }
.diary__quote { max-width: 46ch; text-align: left; }

/* ═══ 10 · THE COMPLETE KIT ═══ */
.kit { display: grid; gap: var(--space-48); align-items: start; }
@media (min-width: 980px) { .kit { grid-template-columns: 5fr 6fr; } }
/* Thumbs sit in a vertical rail LEFT of the shot at desktop, as on both live references
   (pawcastcam.com/#order and the ads LP's .fo-gallery). Below 900px they fall back to a row under
   the shot, which is the only thing that fits a phone. DOM order stays shot-then-thumbs: the
   thumbs are a control FOR the shot, so a screen reader should meet the thing before its switcher. */
.kit__media { display: grid; gap: var(--space-16); }
.kit__body { display: grid; gap: var(--space-32); background: var(--color-surface);
  border: var(--border-bold) solid var(--color-border); border-radius: var(--radius-xl);
  padding: var(--space-48); box-shadow: var(--elevation-card); }
.kitshot { display: grid; gap: var(--space-12); }
/* 1:1, not 5:4 — three of the five shots are the 1000×1000 showcase infographics and a 5:4
   frame cropped 20% off the top and bottom of their text. Square fits them natively, so
   object-fit:cover crops nothing at all; the flatlay and the collar shot were re-cut to match. */
.kitshot__frame { position: relative; aspect-ratio: 1; border-radius: var(--radius-xl);
  border: var(--border-bold) solid var(--color-border); overflow: hidden;
  background: linear-gradient(150deg, var(--color-primary), var(--color-accent));
  box-shadow: var(--elevation-card); }
/* the sale tag lives on the PHOTO as a tilted sticker — off the price row, where it was clutter */
.kitshot__label { font-weight: 700; }
.kitshot__hint { color: var(--color-text-muted); }
/* ── OFFER COLUMN ── mirrors the live references' .fo-* structure (pawcastcam.com/#order and the
   ads LP's .founder-offer). Replaces a 4-row price table: same four facts, none of the reveal. */
.kit__badge { justify-self: start; }
.kit__bens { gap: var(--space-12); }
/* the 6-months-free line is the one people actually weigh — lit, not just listed */
.kit__bens--hi { background: var(--color-sun-soft); border-radius: var(--radius-lg);
  padding: var(--space-12) var(--space-16); margin-inline: calc(var(--space-16) * -1); }
.kit__kith { margin-bottom: calc(var(--space-16) * -1); }

/* Four component tiles. Each carries its value ON the picture — that is the section's whole move:
   four things you can see, one price underneath. */
.kitparts { list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-12); }
.kitpart { display: grid; gap: var(--space-4); }
.kitpart__img { position: relative; aspect-ratio: 1; overflow: hidden;
  border-radius: var(--radius-lg); border: var(--border-medium) solid var(--color-border);
  background: var(--color-surface); }
.kitpart__img img { width: 100%; height: 100%; object-fit: cover; }
/* the badge sits on the tile, so it is chrome on a picture — sized off the tile, like the reel and
   phone chrome, or it would be a 42px control on a 90px tile */
.kitpart__img { container-type: inline-size; }
.kitpart__val { position: absolute; top: 5cqw; left: 5cqw; z-index: 1;
  padding: 1.5cqw 4cqw; border-radius: var(--radius-pill);
  background: var(--color-park-soft); color: var(--color-text);
  font-family: var(--font-family-body); font-weight: 800; font-size: 11cqw; white-space: nowrap; }
.kitpart__val s { color: var(--color-text-muted); font-weight: 600; }
/* R1 exception, same reasoning as the phone caption: these are tile labels under a picture, not
   reading copy, and at body size four of them wrapped to three lines each. */
.kitpart__n { font-weight: 800; font-size: var(--font-size-fine); line-height: 1.25; }
.kitpart__k { color: var(--color-text-muted); font-size: var(--font-size-fine); line-height: 1.25; }
@media (max-width: 620px) { .kitparts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* Price reveal — struck total, the real number, and the discount as a tilted sticker pill. */
.pricereveal { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-16); flex-wrap: wrap;
  background: var(--color-sun-soft); border: var(--border-medium) solid var(--color-border);
  border-radius: var(--radius-lg); padding: var(--space-16) var(--space-20); }
.pricereveal__row { display: flex; align-items: baseline; gap: var(--space-12); flex-wrap: wrap; }
.pricereveal__row b { font-family: var(--font-family-display); font-weight: 700;
  font-size: var(--font-size-h2); line-height: 1; letter-spacing: var(--letter-spacing-h2); }
.pricereveal__row span { color: var(--color-text-muted); }
.pricereveal__pill { flex: none; display: grid; justify-items: center; rotate: -4deg;
  background: var(--color-primary); color: var(--color-text-on-primary);
  border: var(--border-medium) solid var(--color-border); border-radius: var(--radius-md);
  padding: var(--space-8) var(--space-12); line-height: 1; }
.pricereveal__pill b { font-family: var(--font-family-display); font-size: var(--font-size-h4); }
.pricereveal__pill small { font-size: inherit; font-weight: 800; text-transform: uppercase;
  letter-spacing: .08em; }
.kit__save { margin-top: calc(var(--space-24) * -1); }
.kit__restate { display: flex; align-items: baseline; gap: var(--space-12); flex-wrap: wrap; }
.kit__restate b { font-family: var(--font-family-display); font-weight: 700;
  font-size: var(--font-size-h3); line-height: 1; letter-spacing: var(--letter-spacing-h3); }
.kit__fine { margin-top: calc(var(--space-24) * -1); }

.kitthumbs { list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(3, 1fr); gap: var(--space-12); }
.kitthumb { width: 100%; aspect-ratio: 1; padding: 0; cursor: pointer; border-radius: var(--radius-lg);
  border: var(--border-medium) solid var(--color-border-subtle);
  background: linear-gradient(150deg, var(--color-accent), var(--color-primary));
  transition: border-color var(--duration-base) var(--ease-standard),
              transform var(--duration-base) var(--ease-standard); }
/* placeholder variety so four thumbs don't read as one repeated tile (real shots replace these) */
.kitthumbs li:nth-child(2) .kitthumb { background: linear-gradient(210deg, var(--color-primary), var(--color-accent)); }
.kitthumbs li:nth-child(3) .kitthumb { background: linear-gradient(330deg, var(--color-accent), var(--color-primary)); }
.kitthumbs li:nth-child(4) .kitthumb { background: linear-gradient(30deg, var(--color-primary), var(--color-accent)); }
.kitthumb:hover { transform: translateY(-3px); }
.kitthumb.is-active { border: var(--border-bold) solid var(--color-border); }
.kitthumb:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--color-focus); }
@media (prefers-reduced-motion: reduce) { .kitthumb:hover { transform: none; } }
/* "Free" as green TEXT is only 3.11:1 on cream — Park green is a graphic colour, never type.
   The DS answer is the soft-tint pattern: park tint behind ink text. Reads green, stays AAA. */
/* price stack — the old price sits directly ABOVE the new one (user-requested), struck and muted;
   no rules, no badges: $99 is the only loud thing in the block. */

/* ---- icon chip — shared decorative icon-in-a-tile (stat bar, pillars). Rung-S box, so it lines
   up with chips/avatars; NOT a control (no hover), just Nucleo in a soft tint. ---- */
.iconchip { display: grid; place-items: center; flex: none;
  width: var(--control-h-s); height: var(--control-h-s); border-radius: var(--radius-lg);
  background: var(--color-fetch-soft); color: var(--color-text);
  border: var(--border-medium) solid var(--color-border); }
.iconchip svg { width: 50%; height: 50%; }
.iconchip--lg { border-radius: var(--radius-lg); }

/* ═══ 13 · THREE PILLARS ═══ */
.grid3 { display: grid; grid-template-columns: 1fr; gap: var(--space-32); }
@media (min-width: 860px) { .grid3 { grid-template-columns: repeat(3, 1fr); } }
.pillar__tag { font-style: italic; font-weight: 700; color: var(--color-text); margin-top: auto; }

/* ═══ 14 · SPEC LIST ═══ */
.specs__note { margin-top: var(--space-32); color: var(--color-text-muted); }

/* ═══ 15 · TESTIMONIALS ═══ scroll-dealt card stack (user spec, riffing on componentry's
   orbit-card-stack): the six quotes start as one fanned pile and DEAL OUT into a 3×2 grid as you
   scroll — progress drives the interpolation (main.js sets --tx/--ty/--rot per card). Once fully
   spread (.is-spread), hovering a card lifts it above its neighbours. Fallbacks: no JS, mobile
   (<820px) or reduced-motion = the plain responsive grid below, so the quotes are never locked
   behind the effect. */
.orbit { display: grid; gap: var(--space-32);
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.orbit .pc-quote { box-shadow: var(--elevation-card); }
.orbit.is-ready { display: block; position: relative; }
.orbit.is-ready .pc-quote { position: absolute; left: 0; top: 0; margin: 0;
  transform: translate3d(var(--tx, 0px), var(--ty, 0px), 0) rotate(var(--rot, 0deg));
  will-change: transform;
  /* equal-height cards (JS): the quote stretches, attribution pins to the bottom edge */
  grid-template-rows: auto 1fr auto; align-content: stretch; }
/* transitions only once dealt — while the scroll is scrubbing, direct writes must not lag */
.orbit.is-spread .pc-quote { transition: transform var(--duration-base) var(--ease-standard),
  box-shadow var(--duration-base) var(--ease-standard); }
.orbit.is-spread .pc-quote:hover { z-index: 10;
  transform: translate3d(var(--tx, 0px), calc(var(--ty, 0px) - 10px), 0) rotate(0deg) scale(1.03);
  box-shadow: var(--elevation-card), 0 18px 40px -18px var(--shadow-ambient-light); }
.orbit__hint { text-align: center; margin-top: var(--space-32); color: var(--color-text-muted); }

/* ═══ 16 · FAQ ═══ */
.faqwrap { max-width: 90ch; margin-inline: auto; display: grid; gap: var(--space-24); }
/* R1: a group heading is BODY size — caps + tracking + muted carry the hierarchy */
.faqwrap__group { font-family: var(--font-family-body); font-weight: 800;
  font-size: var(--font-size-body); line-height: var(--line-height-body);
  text-transform: uppercase; letter-spacing: .08em; color: var(--color-text-muted); }
.faqwrap__group:not(:first-child) { margin-top: var(--space-32); }

/* ═══ 17 · FINAL CTA ═══ the punch line pops in Sunbeam (large bold on Forest = 7.2:1).
   .cta-hero = the hero pattern again: full viewport (minus header), video bg + the same
   sanctioned ink scrim, Forest fallback until the media lands. */
.pc-cta-band strong { color: var(--color-accent); }
.pc-cta-band .cta-band__note { margin-top: var(--space-20); }
.cta-hero { position: relative; isolation: isolate; overflow: hidden;
  display: grid; align-items: center; min-height: calc(100svh - var(--header-h)); }
.cta-hero__bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  z-index: -2; }
.cta-hero__scrim { position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgba(23,17,12,.5) 0%, rgba(23,17,12,.62) 55%, rgba(23,17,12,.75) 100%); }

/* ═══ 18 · FOOTER ═══ full-viewport close (Happly-style): tagline + nav up top, the wordmark
   blown up to the full container width as the page's final image, legal bar at the bottom. */
footer.pc-footer { min-height: calc(100svh - var(--header-h)); display: grid; }
.footer__grid { display: grid; align-content: space-between; gap: var(--space-64); }
.footer__top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-24) var(--space-48);
  justify-content: space-between; }
.footer__tagline { font-family: var(--font-family-display); font-weight: 600;
  font-size: var(--font-size-h3); line-height: var(--line-height-h3); }
.footer__nav { display: flex; flex-wrap: wrap; gap: var(--space-24); }
.footer__markwrap { position: relative; display: block; }
.footer__mark { width: 100%; height: auto; display: block; color: var(--color-text); }
.footer__legal { display: flex; gap: var(--space-24); }

/* ═══ STICKERS ═══ /pawcast-gen-icon bold-sticker art (chunky ink outlines, flat warm palette) —
   the brand's decorative layer. Placement law: ONE sticker per section, sitting right by the
   headline; the hero gets the dog + cat pair. They FLOW above the eyebrow (never absolute inside
   text columns → no collisions at any viewport); only the hero pair floats. Decorative only:
   alt="", pointer-events none. Keep it sparse — more of these reads phèn, not playful. */
.sticker { pointer-events: none; user-select: none; height: auto; }
/* the hero pair is BIG and slapped ON the h1 like a real sticker (user call). Anchor = the
   HEADLINE BOX itself (.hero__h1wrap position:relative — a stable containing block, unlike the
   animating reveal stack): %-offsets track the text, so they kiss the type at every viewport
   (vw/% of the SECTION drifted off the h1 on tall 2K screens). Negative offsets hang them over
   the box corners; the drop shadow lifts them off the letters. */
.hero__h1wrap { position: relative; display: inline-block; }
.hero__sticker { position: absolute; z-index: 1; width: clamp(5rem, 10.5vw, 12rem);
  filter: drop-shadow(0 4px 10px rgba(23,17,12,.35)); }   /* same sanctioned ink wash as the scrim */
.hero__sticker--dog { left: -5%; top: -16%; rotate: -10deg; }
/* the cat hangs mostly OFF the corner — line 2 spans the full box, so a deep offset sat right
   on top of "dog." (user-caught, nudged out twice); the dog's side is fine, line 1 is narrower */
.hero__sticker--cat { right: -15%; bottom: -34%; rotate: 9deg; }
/* near-headline decor — first child of the sec-head / split body, so it rides the same grid gap */
.deco-head { width: clamp(4rem, 5.6vw, 6.2rem); rotate: -7deg; }
.deco-head--r { rotate: 7deg; }
.pc-sec-head--center .deco-head { margin-inline: auto; }
.diary__sticker { width: clamp(3.6rem, 5.2vw, 5.6rem); rotate: 9deg; }
/* three BIG stickers slapped ON the giant wordmark (user call — playful over polite): they sit
   over the letterforms; pointer-events:none keeps the home link under them clickable */
.footer__deco { position: absolute; right: 3%; top: -8%; width: clamp(5.5rem, 11vw, 11.5rem); rotate: 14deg; }      /* bone — on the "t" */
.footer__deco--heart { right: auto; left: 3%; top: -6%; width: clamp(5rem, 9.5vw, 10rem); rotate: -12deg; }
.footer__deco--paw { right: auto; left: 33%; top: 38%; width: clamp(4.5rem, 8.5vw, 9rem); rotate: 8deg; }           /* paw — in the "w" (glyph spans ~30–50% of the mark) */
/* narrow desktop (≤1280): the hero stack is tighter, so the cat's drop is shallower or it lands
   on the sub (caught at 1024×768) */
@media (max-width: 1280px) { .hero__sticker--cat { bottom: -10%; right: -13%; } }
/* below 900px the headline wraps taller and the corners run out of safe air (the cat landed on
   the sub at tablet) — the pair only ships where it has room; the deco-heads carry the vibe */
@media (max-width: 900px) { .hero__sticker { display: none; } }

/* (sticky order bar removed 2026-07-30 — user call; the header CTA + section CTAs carry it) */

/* ---- reduced motion: kill every animation, but never hide content ---- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important;
    scroll-behavior: auto !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   ORDER POPUP — the 3-step funnel (email → shipping address → Shopify hand-off).
   Behaviour lives in assets/js/order.js; this is the SHELL ONLY. Every control inside is a DS
   component (.pc-btn / .pc-input / .pc-select / .pc-badge), so nothing below restyles a button or
   a field — that was the whole point of the port: the pre-DS popup hard-coded a golden-hour
   palette (--jet/--lime/--warm) and drew its own button, which is exactly what R4 forbids.
   Follows the DS Modal spec (COMPONENTS.md 174:3): surface + 4px ink border + radius-xl +
   elevation-card, space-32 padding, scrim on ink, enter = fade + scale from .96.
   R1 · there is NOT ONE font-size in this block. Sub-body hierarchy is colour + weight only —
   labels go to 800, hints/fine print to --color-text-muted, errors to --color-danger. Shrinking
   the type would have been the easy way to fit the form and is the one thing the DS bans.
   ═════════════════════════════════════════════════════════════════════════════════════════════ */
.pawo{position:fixed;inset:0;z-index:10000;display:none;align-items:center;justify-content:center;
  padding:var(--space-20)}
.pawo.is-open{display:flex}
.pawo-backdrop{position:absolute;inset:0;background:rgba(42,31,24,.55)}
.pawo-panel{position:relative;width:min(34rem,100%);max-height:calc(100vh - var(--space-40));
  overflow-y:auto;background:var(--color-surface);color:var(--color-text);
  border:var(--border-bold) solid var(--color-border);border-radius:var(--radius-xl);
  box-shadow:var(--elevation-card);padding:var(--space-32);
  animation:pawoPop var(--duration-base) var(--ease-emphasized)}
@keyframes pawoPop{from{transform:scale(.96);opacity:0}to{transform:none;opacity:1}}

/* Close — DS Modal law: a BARE xmark. No button chrome, no divider under the header.
   Icon-only affordances rotate rather than flip a label (MOTION: it has no label to flip). */
.pawo-close{position:absolute;top:var(--space-20);right:var(--space-20);display:grid;place-items:center;
  width:1.2rem;height:1.2rem;padding:0;border:0;background:none;color:var(--color-text);cursor:pointer;
  transition:transform var(--duration-base) var(--ease-standard)}
.pawo-close svg{width:100%;height:100%;fill:currentColor}
.pawo-close:hover{transform:rotate(90deg)}

/* Step meter — three rungs, filled to the current step. */
.pawo-steps{display:flex;gap:var(--space-8);margin:0 0 var(--space-24)}
.pawo-steps .d{height:.2rem;flex:1;border-radius:var(--radius-pill);background:var(--color-border-subtle);
  transition:background var(--duration-base) var(--ease-standard)}
.pawo-steps .d.on{background:var(--color-primary)}

.pawo-panel .pc-badge{margin-bottom:var(--space-12)}
.pawo-title{margin:0 0 var(--space-8)}
.pawo-sub{margin:0 0 var(--space-24)}
.pawo-panel .pc-btn{width:100%;margin-top:var(--space-8)}
.pawo-back{margin-bottom:var(--space-16);width:auto}

/* Fields — spacing + label only; the box itself is .pc-input. */
.pawo-field{position:relative;margin-bottom:var(--space-16)}
.pawo-field.row{display:flex;gap:var(--space-16)}
.pawo-field.row > div{flex:1;position:relative}
.pawo label{display:block;margin:0 0 var(--space-8);font-weight:800;color:var(--color-text)}
.pawo label .opt{font-weight:400;color:var(--color-text-muted)}
.pawo-err{margin:var(--space-8) 0 0;min-height:1px;font-weight:600;color:var(--color-danger)}
.pawo-fine{margin:var(--space-16) 0 0;text-align:center;color:var(--color-text-muted)}

/* Address + state autocomplete — same dropdown for the mock and the real Places path. */
.pawo-ac{position:absolute;left:0;right:0;top:calc(100% + var(--space-4));z-index:20;display:none;
  overflow:hidden;background:var(--color-surface);border:var(--border-medium) solid var(--color-border);
  border-radius:var(--radius-lg);box-shadow:var(--elevation-card)}
.pawo-ac.open{display:block}
.pawo-ac .it{display:flex;gap:var(--space-12);align-items:flex-start;cursor:pointer;
  padding:var(--space-12) var(--space-16);
  border-bottom:var(--border-thin) solid var(--color-border-subtle)}
.pawo-ac .it:last-child{border-bottom:0}
.pawo-ac .it:hover,.pawo-ac .it.active{background:var(--color-fetch-soft)}
.pawo-ac .pin{flex:0 0 auto;color:var(--color-primary)}
.pawo-ac .l1{font-weight:700}
.pawo-ac .l2{color:var(--color-text-muted)}
.pawo-ac .ft{padding:var(--space-8) var(--space-16);text-align:right;color:var(--color-text-muted);
  background:var(--color-bg)}

/* Hand-off state — the only screen the customer sees while the Shopify cart is being built. */
.pawo-hand{text-align:center}
.pawo-hand .ic{display:grid;place-items:center;width:3rem;height:3rem;margin:0 auto var(--space-16);
  background:var(--color-sun-soft);border:var(--border-medium) solid var(--color-border);
  border-radius:var(--radius-pill)}
.pawo-spin{display:inline-block;width:.8rem;height:.8rem;border-radius:var(--radius-pill);
  border:var(--border-medium) solid var(--color-border-subtle);border-top-color:var(--color-primary);
  animation:pawoSpin .7s linear infinite}
@keyframes pawoSpin{to{transform:rotate(360deg)}}
body.pawo-lock{overflow:hidden}

@media (max-width:480px){
  .pawo-panel{padding:var(--space-24) var(--space-20)}
  .pawo-field.row{flex-direction:column;gap:var(--space-16)}
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   REAL MEDIA — the slots the DS demo left as token gradients.
   Every frame KEEPS its gradient underneath: it is the load/decode state and the fallback if a
   file ever 404s, so a missing image degrades to an intentional Fetch→Sunbeam panel rather than a
   broken-image icon. That is why the images are painted over the top rather than replacing it.
   ═════════════════════════════════════════════════════════════════════════════════════════════ */

/* NOTE ON THE SELECTORS BELOW — they are child selectors on purpose, not tidiness.
   The DS page CSS already carries `.phone__frame > :not(.playdisc){position:relative}`, which is
   specificity (0,2,0) because :not() takes its argument's weight. A plain `.phone__poster` rule is
   (0,1,0) and LOSES to it, so the poster and the loop stayed in flow: the phone rendered as two
   stacked full-size images and the 9/13 aspect ratio blew out. Writing these as
   `.phone__frame > .phone__poster` ties at (0,2,0) and wins on source order. Don't "simplify". */

/* Reel cards — an explicit three-rung z-index ladder: media 0 · scrim 1 · chrome 2.
   Spelled out because the obvious shortcut is wrong in both directions. A blanket
   `.reel__frame > :not(.reel__poster):not(.reel__vid){position:relative}` (which is what the
   phone frame does) OVERRIDES the DS's own `position:absolute` on .reel__tag / .playdisc /
   .reel__meta — they drop into flow and the label and play disc slide to the top of the card.
   And leaving the scrim to source order doesn't work either: ::after generates as the LAST child,
   so with everything on z-index:auto the scrim paints over the label it is supposed to be under.
   Only the ladder gets both right, and it touches no `position` the DS already set. */
.reel__frame > .reel__poster,
.reel__frame > .reel__vid { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0; }
.reel__frame > .reel__vid { opacity: 0; transition: opacity var(--duration-slow) var(--ease-standard); }
.reel__frame > .reel__vid.is-playing { opacity: 1; }
/* Scrim + meta re-audit. The DS page CSS sets .reel__meta to --color-text-on-primary (BLACK ink)
   and its comment says why: the label sat on the Fetch/Sunbeam gradient, where black is AAA-large.
   That comment also names this exact moment — "when real video lands … add a poster with a light
   bottom area (or re-audit)". A light bottom isn't something you can promise about real pet
   footage, so this is the re-audit: dark scrim + cream label. Black ink over the scrim would have
   been the worse of the two failures — dark on dark. Cream on rgba(23,17,12,.78) clears AAA over
   any frame, which is the point of scrimming instead of hoping. */
.reel__frame::after { content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background: linear-gradient(to top, rgba(23,17,12,.82) 0%, rgba(23,17,12,.40) 42%, transparent 68%); }
.reel__frame > .reel__tag,
.reel__frame > .playdisc,
.reel__frame > .reel__meta { z-index: 2; }
.reel__meta { color: var(--color-text-on-dark); }

/* Phone mockup — media layer 0, top scrim 1, .phone__ui 2 (set in the §7 block above).
   The bottom scrim lives on .phone__ui-bottom, not here: it has to sit UNDER the caption but OVER
   the video, and a frame-wide ::after generates last, so a single gradient covering both ends would
   paint over the very text it exists to support. Splitting it also lets the bottom strip carry the
   pre-DS gradient shape verbatim. This one only lifts the top row (brand + tag). */
.phone__frame > .phone__poster,
.phone__frame > .phone__vid { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: inherit; z-index: 0; }
.phone__frame > .phone__vid { opacity: 0; transition: opacity var(--duration-slow) var(--ease-standard); }
.phone__frame > .phone__vid.is-playing { opacity: 1; }
.phone__frame::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; z-index: 1;
  background: linear-gradient(to bottom, rgba(23,17,12,.52) 0%, rgba(23,17,12,.14) 18%, transparent 34%); }
.phone__frame > .playdisc { z-index: 2; }

/* Kit gallery — z-index ladder, NOT a blanket `> :not(#kitShotImg){position:relative}`.
   That blanket rule is (0,3,0) and it silently overrode `position:absolute` on .kitshot__tag and
   .kitshot__sale, dropping both badges into normal flow: they landed side by side on one line,
   overlapping, and the left one truncated mid-word ("$99 · complete k"). I first "fixed" that by
   moving the sale badge to bottom-right, which did nothing, because a `bottom` on a
   position:relative element just nudges it. Third time this exact selector shape has bitten on
   this page (reels, phone, here) — if you find yourself writing `> :not(...)` to lift chrome above
   an image, give the image z-index:0 and the chrome z-index:1 instead. */
#kitShotImg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  z-index: 0; }
.kitshot__tag,
.kitthumb { overflow: hidden; }
.kitthumb img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* NOTE: this block MUST stay below the .kitthumbs/.kitthumb base rules. It overrides
   grid-template-columns at the SAME specificity, so above them the base repeat(5, 1fr)
   won inside the media query — the rail rendered as a row, the `auto` column ate the width,
   and the shot collapsed to a thumbnail with the caption in a one-character column. */
@media (min-width: 900px) {
  /* STICKY, matching the reference's `.fo-gallery{position:sticky;top:88px}`: the right column is
     roughly twice as tall as the gallery, so without this the product images scroll away while the
     reader is still working through the kit list and the price. Offset clears the sticky header.
     Requirements, in case this ever stops working: .kit must keep `align-items: start` (a stretched
     grid item fills the row and has nothing to stick within), and no ancestor may set `overflow`
     or `clip-path` — which is why #kit is NOT a .band (that class clips at ±100vmax). */
  .kit__media { grid-template-columns: auto minmax(0, 1fr); column-gap: var(--space-16);
    align-items: start;
    position: sticky; top: calc(var(--header-h) + var(--space-24));
    /* Cap the shot so the WHOLE gallery fits on screen while it is pinned. The shot is 1:1 and
       sized from its column, so on a wide-but-short window (a 1440×800 laptop) it grew taller than
       the viewport and sticky pinned something you could only ever see the top of. Same trick
       .section--fullh .phone uses above: derive the max width from the available HEIGHT, since a
       square's height is its width. 7rem covers the label, the hint and the gaps under it. */
    --kit-avail: calc(100svh - var(--header-h) - var(--space-24) - 7rem); }
  .kitshot { max-width: min(100%, var(--kit-avail)); }
  .kitshot { grid-column: 2; grid-row: 1; }
  .kitthumbs { grid-column: 1; grid-row: 1; grid-template-columns: 1fr;
    grid-auto-rows: max-content; align-content: start; }
  .kitthumb { width: 3.6rem; }
  .kitshot__hint { grid-column: 2; grid-row: 2; }
}

/* How-it-works cards — the shot sits between the step numeral and the heading. */
.step__shot { margin: 0; overflow: hidden; border-radius: var(--radius-lg);
  border: var(--border-medium) solid var(--color-border); aspect-ratio: 4/3; }
.step__shot img { display: block; width: 100%; height: 100%; object-fit: cover; }


/* Testimonials — the two owner photos, side by side under the section head. */
.whoshots { list-style: none; margin: 0 0 var(--space-40); padding: 0; display: grid;
  gap: var(--space-24); grid-template-columns: repeat(2, minmax(0, 1fr)); }
.whoshot { display: grid; gap: var(--space-12); justify-items: start; text-align: left; }
.whoshot img { width: 100%; aspect-ratio: 4/3; object-fit: cover;
  border-radius: var(--radius-xl); border: var(--border-bold) solid var(--color-border);
  box-shadow: var(--elevation-card); }
.whoshot__who { color: var(--color-text-muted); }
.whoshot__who b { color: var(--color-text); }
@media (max-width: 640px) { .whoshots { grid-template-columns: minmax(0, 1fr); } }
