/* Home page — hero, about, programs, cta */

.hero {
	position: relative;
	height: var(--vh-stable, 100svh);
	min-height: 500px;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	padding: 0;

	/* Sizing system: one source of truth per viewport class. Every
	   breakpoint below redefines these variables; .hero__title and
	   .hero__subtitle READ them verbatim.

	   SUBTITLE = 2× SUBTITLE-SMALL, by DEFINITION. Both read off a
	   single --hero-subtitle-small-size variable so it is
	   mathematically impossible for "Accountability Partner Matching"
	   to render any larger than 2× "For Pornography Recovery…". Since
	   subtitle-small is always ≤ 1rem and title is always ≥ 2.8rem,
	   this guarantees the subtitle can never exceed the title size. */
	--hero-title-size: clamp(4.2rem, calc(7 * var(--vh-unit, 1vh)), 10rem);
	--hero-subtitle-small-size: 1rem;
	--hero-subtitle-size: calc(var(--hero-subtitle-small-size) * 2);
}

.hero__bg {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	/* Inverted parallax: the image feels pinned to the viewport while the
	   title + subtitle scroll away faster. We accomplish this by translating
	   the bg DOWN as the user scrolls (canceling out the scroll, so bg
	   appears stationary), while the text above gets a secondary translate
	   UP (making it scroll off faster than the page). */
	scale: 1.04;
	transform-origin: 50% 0%;
	will-change: translate;
}

/* Scroll-driven parallax: the compositor syncs the translate to native scroll
   position, which is the smoothest option on iOS (Lenis is disabled there).
   Multiplier is 0.15 — same gentle magnitude as .image-page parallax, so a
   fast scroll-to-top doesn't feel jarring relative to other sections.
   `translate` is animated independently of `scale` so they compose cleanly. */
@supports (animation-timeline: scroll()) {
	/* Parallax ONLY on the hero bg image — gradients and text scroll at
	   normal page speed so they move in lockstep with the next section
	   that slides up from below. */
	.hero__bg {
		animation: hero-pin linear forwards;
		animation-timeline: scroll(root);
		animation-range: 0 100vh;
	}
	@keyframes hero-pin {
		from { translate: 0 0; }
		to { translate: 0 15vh; }
	}
}

@media (prefers-reduced-motion: reduce) {
	.hero__bg {
		animation: none !important;
		translate: none !important;
	}
}

/* Hero image is preloaded — skip the progressive-image fade-in
   so it doesn't fight the parallax transform. */
.hero__bg progressive-image img {
	opacity: 1;
	transition: none;
}

.hero__top-gradient {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 240px;
	z-index: 1;
	/* Starts as SOLID cream — no hard line between body-cream and gradient
	   on top of the bg image. Then fades to transparent over the remaining
	   length so the trees below stay crisp. */
	background: linear-gradient(
		to bottom,
		var(--bg) 0%,
		var(--bg) 30%,
		rgba(242, 243, 239, 0.7) 55%,
		rgba(242, 243, 239, 0.2) 80%,
		transparent 100%
	);
}

.hero__bottom-gradient {
	position: absolute;
	bottom: 0;
	left: 0;
	right: 0;
	height: clamp(280px, calc(34 * var(--vh-unit, 1vh)), 660px);
	z-index: 1;
	background: linear-gradient(
		to top,
		rgba(45, 74, 62, 0.95) 0%,
		rgba(45, 74, 62, 0.75) 35%,
		rgba(45, 74, 62, 0.35) 65%,
		transparent 100%
	);
}

.hero__top {
	position: relative;
	z-index: 2;
	text-align: center;
	padding-top: 88px;
}

.hero__eyebrow {
	font-family: var(--serif);
	font-size: 0.93rem;
	font-weight: 500;
	letter-spacing: 0.04em;
	color: var(--amber);
	margin-bottom: var(--space-md);
}

.hero__title {
	display: block;
	font-size: var(--hero-title-size);
	font-weight: 300;
	color: var(--ink);
	letter-spacing: -0.04em;
	line-height: 1;
	margin-bottom: 6px;
	text-decoration: none;
}

.hero__title:hover {
	color: var(--ink);
}

.hero__tagline {
	font-size: 1.2rem;
	color: var(--ink);
	opacity: 0.5;
	line-height: 1.6;
}

.hero__bottom {
	position: absolute;
	bottom: 16px;
	left: 0;
	right: 0;
	z-index: 2;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0;
	padding: 0 var(--space-xl);
}

.hero__bottom-line {
	width: 32px;
	height: 1px;
	background: var(--divider);
	margin-bottom: var(--space-lg);
}


.hero__subtitle {
	font-size: var(--hero-subtitle-size);
	font-weight: 300;
	text-align: center;
	line-height: 1.15;
	letter-spacing: -0.02em;
	margin-bottom: 8px;
	color: var(--bg);
	opacity: 0.9;
	mix-blend-mode: difference;
	filter:
		drop-shadow(0 0 18px rgba(128, 128, 128, 0.85))
		drop-shadow(0 0 48px rgba(128, 128, 128, 0.55));
	white-space: nowrap;
}

.hero__subtitle-small {
	font-size: var(--hero-subtitle-small-size);
	color: var(--bg);
	opacity: 0.65;
	mix-blend-mode: difference;
	filter:
		drop-shadow(0 0 14px rgba(128, 128, 128, 0.8))
		drop-shadow(0 0 36px rgba(128, 128, 128, 0.5));
	text-align: center;
	line-height: 1.5;
	margin-bottom: var(--space-lg);
}

.hero__actions {
	display: flex;
	gap: 16px;
	justify-content: center;
}

/* About section */
.about__inner {
	max-width: 680px;
	text-align: center;
}

.about__line {
	width: 32px;
	height: 1px;
	/* Fade baked into the color so `.reveal.in-view > *` opacity:1 doesn't
	   override the intended 40% amber. See note on .text-page__line. */
	background: var(--divider);
	margin: 0 auto var(--space-2xl);
}

.about__actions {
	display: flex;
	gap: 16px;
	justify-content: center;
	margin-top: var(--space-xl);
}

.about__text {
	font-size: 1.65rem;
	font-weight: 300;
	line-height: 1.55;
	color: var(--ink);
	opacity: 0.65;
	letter-spacing: -0.01em;
}

/* Text-page shell (shared across pages) */
.text-page {
	min-height: 100svh;
	min-height: 100svh;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--space-4xl) var(--space-xl);
	background: var(--bg);
	position: relative;
}

.text-page__inner {
	max-width: 580px;
	text-align: center;
}

.text-page__label {
	font-family: var(--serif);
	font-size: 0.95rem;
	font-weight: 500;
	letter-spacing: 0.04em;
	color: var(--secondary);
	margin-bottom: var(--space-lg);
}

.text-page__headline {
	font-size: 3.2rem;
	font-weight: 300;
	line-height: 1.1;
	letter-spacing: -0.03em;
	margin-bottom: var(--space-xl);
}

.text-page__line {
	width: 32px;
	height: 1px;
	background: var(--divider);
	margin: 0 auto var(--space-xl);
}

.text-page__body {
	font-size: 1.1rem;
	line-height: 2;
	color: var(--ink);
	opacity: 0.48;
}

.text-page__body strong {
	opacity: 1;
	color: var(--ink);
}

/* Image pages (full-bleed photo between text) */
.image-page {
	height: var(--vh-stable, 100svh);
	overflow: hidden;
	padding: 0;
}

.image-page img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center center;
}

/* Reveal */
.reveal {
	opacity: 0;
	transform: translateY(32px);
	transition: all 0.9s cubic-bezier(0.23, 1, 0.32, 1);
}

.reveal.in-view {
	opacity: 1;
	transform: translateY(0);
}

/* Programs */
.programs {
	min-height: 100svh;
	min-height: 100svh;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	padding: var(--space-4xl) var(--space-xl);
	background: var(--white);
	position: relative;
}

.programs__header {
	text-align: center;
	margin-bottom: var(--space-3xl);
}

.programs__header h2 {
	margin-top: var(--space-md);
}

.programs__sub {
	font-size: 1rem;
	color: var(--secondary);
	margin-top: var(--space-sm);
}

.programs__grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 24px;
	max-width: 960px;
	width: 100%;
}

.card {
	background: var(--bg);
	border-radius: 20px;
	overflow: hidden;
	border: 1px solid var(--border);
	text-decoration: none;
	color: var(--ink);
	transition: transform 800ms cubic-bezier(0.16, 1, 0.3, 1),
		box-shadow 800ms cubic-bezier(0.16, 1, 0.3, 1);
}

.card:hover {
	transform: translateY(-6px);
	box-shadow: 0 32px 80px rgba(45, 74, 62, 0.1);
	color: var(--ink);
}

.card__hero {
	position: relative;
	height: 200px;
	overflow: hidden;
}

.card__hero progressive-image {
	width: 100%;
	height: 100%;
	transition: transform 800ms cubic-bezier(0.16, 1, 0.3, 1);
}

.card:hover .card__hero progressive-image {
	transform: scale(1.04);
}

.card__gradient {
	position: absolute;
	inset: 0;
	background: linear-gradient(to bottom, transparent 30%, rgba(45, 74, 62, 0.4) 100%);
}

.card__body {
	padding: 20px 20px 24px;
}

.card__meta {
	display: flex;
	align-items: center;
	gap: 6px;
	margin-bottom: 10px;
}

.card__dot {
	width: 3px;
	height: 3px;
	border-radius: 50%;
	background: var(--pencil);
}

.card h3 {
	font-size: 1.15rem;
	margin-bottom: 8px;
}

.card p {
	font-size: 0.82rem;
	color: var(--secondary);
	line-height: 1.7;
}

/* CTA */
.cta {
	min-height: 100svh;
	min-height: 100svh;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--space-4xl) var(--space-xl);
	background: var(--bg);
	position: relative;
}

.cta__inner {
	max-width: 600px;
	text-align: center;
}

.cta h2 {
	font-size: 2.8rem;
	line-height: 1.15;
	margin-bottom: 0;
}

/* The stripped beta ask has no hairline or lede to set the rhythm,
   so the heading itself holds the room before the form. The mark
   above it is the app icon, dressed the way "Start tonight." wore
   it. */
/* The beta box takes the white beat now — the FAQ below it went
   back to cream, so the alternation holds through the swap. */
.cta--beta {
	background: var(--white);
}

.cta--beta h2 {
	margin-bottom: clamp(1.8rem, 4.5vh, 2.75rem);
}

/* The beta status sits between the ask and the field — the pause
   before typing. A full sentence now, not a date, so it reads a size
   up from the base fine print and holds more of its colour; it still
   borrows a little of the heading's bottom margin, but less, and
   hands more air on to the form. */
.cta__fineprint.cta__fineprint--launch {
	margin: calc(-1 * clamp(0.6rem, 1.5vh, 1rem)) 0 2.1rem;
	/* Grown from fine print into the section's second voice — the
	   status is the news here, so it reads at heading scale, clamped
	   so the sentence survives a phone column. */
	font-size: 1.1rem;
	line-height: 1.2;
	letter-spacing: 0.005em;
	color: color-mix(in srgb, var(--secondary) 85%, transparent);
}

/* The mark is now the bare terracotta shell on transparent — no tile,
   so no radius and no shadow to cast. */
.cta__icon {
	width: 76px;
	height: 76px;
	margin: 0 auto clamp(1.4rem, 3vh, 2rem);
	display: block;
	/* The favicon is a solid clay tile now — it wears the app-icon
	   radius (~22%) like every other rendering of the mark. */
	border-radius: 17px;
}

/* The newsletter dropped its hairline; the heading keeps the space
   the line's margins used to hold, and the form rides closer to the
   lede than the stock section put it. */
.cta--news h2 {
	margin-bottom: var(--space-xl);
}

.cta--news .cta__lede {
	margin-bottom: var(--space-xl);
}

.cta__line {
	width: 32px;
	height: 1px;
	background: var(--divider);
	margin: var(--space-xl) auto;
}

.cta__label {
	font-family: var(--serif);
	font-size: 0.95rem;
	font-weight: 500;
	letter-spacing: 0.04em;
	color: var(--secondary);
	margin-bottom: var(--space-lg);
}

.cta__lede {
	color: var(--secondary);
	font-size: 1.05rem;
	line-height: 1.9;
	margin-bottom: var(--space-2xl);
}

.signup {
	display: flex;
	gap: 8px;
	max-width: 440px;
	margin: 0 auto;
}

.signup__input {
	flex: 1;
	padding: 14px 20px;
	border: 1px solid var(--border);
	border-radius: 100px;
	background: transparent;
	font-family: var(--serif);
	font-size: 0.9rem;
	color: var(--ink);
	transition: border-color 0.2s ease;
}

.signup__input::placeholder {
	color: var(--secondary);
	opacity: 0.5;
}

.signup__input:focus {
	outline: none;
	border-color: var(--amber);
}

.signup__btn {
	padding: 14px 28px;
	background: var(--ink);
	color: var(--bg);
	border: none;
	border-radius: 100px;
	font-family: var(--serif);
	font-size: 0.85rem;
	font-weight: 500;
	cursor: pointer;
	transition: all 0.25s ease;
	white-space: nowrap;
}

.signup__btn:hover {
	background: #1e3a30;
	transform: translateY(-1px);
}

.cta__fineprint {
	margin-top: var(--space-lg);
	font-size: 0.8rem;
	color: color-mix(in srgb, var(--secondary) 60%, transparent);
}

.cta__fineprint a {
	color: var(--amber);
	font-weight: 500;
	text-decoration: none;
	transition: color 0.2s ease;
}

.cta__fineprint a:hover {
	color: #a88a35;
}

/* Stats preview */
.home-stat {
	min-height: 100svh;
	min-height: 100svh;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--space-4xl) var(--space-xl);
	background: var(--bg);
}

.home-stat--ink {
	background: var(--ink);
}

.home-stat--ink .home-stat__num {
	color: var(--amber);
}

.home-stat--ink .home-stat__desc {
	color: var(--bg);
}

.home-stat--ink .home-stat__topic {
	color: var(--bg);
	opacity: 0.5;
}

.home-stat--ink .home-stat__cite {
	color: var(--amber);
}

.home-stat--ink .home-stat__link {
	color: var(--bg);
	border-color: rgba(242, 243, 239, 0.2);
}

.home-stat--ink .home-stat__link:hover {
	border-color: rgba(242, 243, 239, 0.4);
	color: var(--bg);
}

/* Experiment (Aug 27): the numbers carry the home stats alone — the
   topic eyebrow and its hairline are parked, not deleted, in case
   they come back. */
.home-stat__topic,
.home-stat__line {
	display: none;
}

.home-stat__inner {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	max-width: 640px;
}

.home-stat__topic {
	font-family: var(--serif);
	font-size: 0.95rem;
	font-weight: 500;
	letter-spacing: 0.04em;
	color: var(--secondary);
	margin-bottom: var(--space-lg);
}

.home-stat__line {
	width: 32px;
	height: 1px;
	background: var(--divider);
	margin-bottom: var(--space-xl);
}

.home-stat__lead {
	font-family: var(--serif);
	font-size: 1.15rem;
	color: var(--ink);
	opacity: 0.7;
	margin-bottom: var(--space-sm);
}

.home-stat__num {
	font-family: var(--serif);
	font-size: clamp(5rem, 13vw, 9rem);
	font-weight: 300;
	line-height: 0.85;
	letter-spacing: -0.04em;
	color: var(--ink);
	font-variant-numeric: tabular-nums;
	margin-bottom: var(--space-xl);
}

.home-stat__desc {
	font-family: var(--serif);
	font-size: 1.15rem;
	line-height: 1.55;
	color: var(--ink);
	opacity: 0.7;
	max-width: 480px;
	margin-bottom: var(--space-md);
}

.home-stat__desc strong {
	font-weight: 700;
	font-size: 1.25em;
	line-height: 1;
	font-variant-numeric: tabular-nums;
}

.home-stat__cite {
	font-style: normal;
	font-family: var(--serif);
	font-size: 0.93rem;
	font-weight: 500;
	letter-spacing: 0.04em;
	color: var(--amber);
	opacity: 0.6;
}

.home-stat__divider {
	width: 32px;
	height: 1px;
	background: var(--divider);
	margin: var(--space-3xl) 0;
}

.home-stat__link {
	margin-top: var(--space-2xl);
}

.home-slider {
	margin: var(--space-xl) 0 var(--space-md);
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--space-md);
	width: 100%;
	max-width: 340px;
}

.home-slider__track {
	position: relative;
	width: 100%;
	padding: 26px 0 10px;
}

.home-slider__input {
	-webkit-appearance: none;
	appearance: none;
	width: 100%;
	height: 2px;
	background: rgba(45, 74, 62, 0.18);
	border-radius: 999px;
	outline: none;
	cursor: pointer;
	position: relative;
	z-index: 2;
	margin: 0;
}

.home-slider__input::-webkit-slider-thumb {
	-webkit-appearance: none;
	appearance: none;
	width: 18px;
	height: 18px;
	border-radius: 50%;
	background: var(--amber);
	border: 2px solid var(--white);
	cursor: pointer;
	box-shadow: 0 2px 10px rgba(201, 168, 76, 0.45);
	transition: transform 0.2s ease;
}

.home-slider__input::-moz-range-thumb {
	width: 18px;
	height: 18px;
	border-radius: 50%;
	background: var(--amber);
	border: 2px solid var(--white);
	cursor: pointer;
	box-shadow: 0 2px 10px rgba(201, 168, 76, 0.45);
	transition: transform 0.2s ease;
}

.home-slider__input:active::-webkit-slider-thumb {
	transform: scale(1.15);
}

.home-slider__tick {
	position: absolute;
	top: 22px;
	bottom: 6px;
	left: calc(4 / 15 * (100% - 18px) + 9px);
	width: 2px;
	background: var(--amber);
	opacity: 0.65;
	border-radius: 999px;
	transform: translateX(-50%);
	pointer-events: none;
	z-index: 1;
}

.home-slider__hint {
	font-family: var(--serif);
	font-size: 0.85rem;
	font-weight: 500;
	letter-spacing: 0.04em;
	color: var(--secondary);
	opacity: 0.6;
	margin: 0;
	text-align: center;
}

.home-slider__avg {
	position: absolute;
	/* Rode at 4px when the label was 0.55rem small caps; the larger
	   natural-case label needs the extra headroom off the track. */
	top: -3px;
	left: calc(4 / 15 * (100% - 18px) + 9px);
	transform: translateX(-50%);
	font-family: var(--serif);
	font-size: 0.85rem;
	font-weight: 500;
	letter-spacing: 0.04em;
	color: var(--amber);
	opacity: 0.85;
	white-space: nowrap;
	pointer-events: none;
}

/* Responsive — hero tuning keyed off aspect-ratio (DPR-independent). See
   memory: project_hero_test_criteria.md for the criteria these values meet
   (title centered in clouds, subtitle ≥ 60% of title, shoulders below line,
   feet above buttons, etc.). */

@media (max-width: 1024px) {
	.programs__grid { grid-template-columns: repeat(2, 1fr); }
}

/* Desktop default — 16/10 (majority MBP/laptop). Padding tuned so title
   center lands in the middle of the cloud + white band. */
@media (min-width: 1024px) and (min-aspect-ratio: 16/10) {
	.hero__top { padding-top: calc(11 * var(--vh-unit, 1vh)); }
}

/* 16/9 and up (typical external monitors) */
@media (min-width: 1024px) and (min-aspect-ratio: 16/9) {
	.hero__top { padding-top: calc(11 * var(--vh-unit, 1vh)); }
}

/* Narrow desktop (aspect < 16/10 — e.g. 3:2, 4:3) */
@media (min-width: 1024px) and (max-aspect-ratio: 16/10) {
	.hero__top { padding-top: calc(11 * var(--vh-unit, 1vh)); }
}

/* Very short hero (≤720px tall) — tighten padding; KEEP the "Coming soon"
   eyebrow since there's usually still sky room for it. */
@media (max-height: 720px) and (min-width: 600px) {
	.hero__top { padding-top: calc(9 * var(--vh-unit, 1vh)); }
}

/* Tall viewports (4K/5K): cloud band grows, push title down proportionally. */
@media (min-height: 2000px) {
	.hero__top { padding-top: calc(13 * var(--vh-unit, 1vh)); }
}

/* Tall portrait tablets (iPad 10/11/mini, Galaxy Tab, Fold open, Tesla
   vertical) — portrait aspect. Cloud band is big, title needs extra
   padding to land in the middle AND bigger font so title_fills_clouds
   stays ≥0.18. 14vh + 8vh pair tuned to keep ratio ≈0.5 and fills ≈0.2
   across the full portrait range. */
@media (min-width: 700px) and (max-aspect-ratio: 1/1) {
	.hero__top { padding-top: calc(14 * var(--vh-unit, 1vh)); }
	.hero {
		/* Cap raised to 16rem so the title keeps filling ≥18% of the
		   cloud band on very tall portraits (1600×2560, 2000×3000). */
		--hero-title-size: clamp(4.2rem, calc(8 * var(--vh-unit, 1vh)), 16rem);
		/* Subtitle stays = 2 × subtitle-small (via the calc). */
	}
}

/* Default ALL-viewport white-band treatment — push bg image down by a few
   vh and shrink its height equally (inset + height: auto) so walker feet
   stay anchored to the container bottom (vs. the old `top: X` that left
   height: 100% and pushed feet off-screen). Cream body shows through
   above; top gradient covers the band with a soft fade into the image. */
.hero__bg {
	inset: calc(4 * var(--vh-unit, 1vh)) 0 0 0;
	height: auto;
}
.hero__top-gradient { top: 0; height: clamp(200px, calc(10 * var(--vh-unit, 1vh)), 360px); }

/* Mid/short-height and narrow viewports — bigger white band. */
@media (max-width: 1024px), (max-height: 900px) {
	.hero__bg { inset: calc(7 * var(--vh-unit, 1vh)) 0 0 0; height: auto; }
	/* Floor lowered from 240px → min(240px, 30vh) so on SHORT viewports
	   (400px tall, 500px tall, etc.) the gradient can't overflow into
	   the trees — it scales down with viewport height. */
	.hero__top-gradient { height: clamp(min(240px, calc(30 * var(--vh-unit, 1vh))), calc(18 * var(--vh-unit, 1vh)), 480px); }
}

@media (max-width: 768px), (max-height: 720px) and (min-width: 600px) and (max-width: 800px) {
	.hero__bg { inset: calc(7 * var(--vh-unit, 1vh)) 0 0 0; height: auto; }
	.hero__top-gradient { height: clamp(min(240px, calc(30 * var(--vh-unit, 1vh))), calc(18 * var(--vh-unit, 1vh)), 460px); }
	.hero__top { padding-top: calc(14 * var(--vh-unit, 1vh)); }
}

/* Short LANDSCAPE DESKTOP (769-1400 wide × ≤760 tall) — placed AFTER
   the short-mobile rule above so its 5vh padding + small inset wins
   the cascade even for viewports that also match max-height: 720.
   Covers 790×530 reported tagline-bleeds-into-trees and similar
   narrow/short landscape windows. */
@media (min-width: 769px) and (max-width: 1400px) and (max-height: 760px) and (min-aspect-ratio: 1/1) {
	.hero__bg { inset: calc(5 * var(--vh-unit, 1vh)) 0 0 0; height: auto; }
	/* Floor scales with vh so the gradient can't bleed into trees on
	   very short viewports (e.g. 998×502). */
	.hero__top-gradient { height: clamp(min(180px, calc(28 * var(--vh-unit, 1vh))), calc(14 * var(--vh-unit, 1vh)), 320px); }
	.hero__top { padding-top: calc(5 * var(--vh-unit, 1vh)); }
	.hero {
		/* Shrink title so title + tagline still fit above the treeline
		   on short-landscape viewports where the cloud band is only
		   ~150–200px tall (e.g. 998×502, 790×530). 4rem cap + 6vh
		   preferred keeps Camino compact without looking anemic. */
		--hero-title-size: clamp(2.8rem, calc(6 * var(--vh-unit, 1vh)), 4rem);
	}
}

/* Short-but-not-super-short landscape (769–1400 wide × 621–760 tall) —
   the Short LANDSCAPE DESKTOP block above was tuned for 998×502 / 790×530
   (very short), which leaves too little cloud space. At 800×659 the
   viewport has more vertical room, so bump padding + title size so the
   mark lands centered rather than jammed near the top. Regression test:
   800x659_short_landscape_window in tests/analyze_hero.py. */
@media (min-width: 769px) and (max-width: 1400px) and (min-height: 621px) and (max-height: 760px) and (min-aspect-ratio: 1/1) {
	.hero__top { padding-top: calc(9 * var(--vh-unit, 1vh)); }
	.hero {
		--hero-title-size: clamp(2.8rem, calc(7 * var(--vh-unit, 1vh)), 5rem);
	}
}

/* Mid-width portrait (~600–800 wide, aspect ≤ 5/6, e.g. 651×828 resized
   Safari window) — the default max-width:768 branch above sets 14vh
   padding, which jams Camino too near the top on taller aspects because
   the cloud band is bigger there. Bump padding + title size so the title
   lands centered in the clouds and fills ≥18%. Regression test:
   651x828_tagline_too_high in tests/analyze_hero.py. */
@media (min-width: 600px) and (max-width: 800px) and (max-aspect-ratio: 5/6) {
	.hero__top { padding-top: calc(24 * var(--vh-unit, 1vh)); }
	.hero {
		--hero-title-size: clamp(4.2rem, calc(9.5 * var(--vh-unit, 1vh)), 12rem);
	}
}

@media (max-width: 480px) {
	.hero__bg { inset: calc(8 * var(--vh-unit, 1vh)) 0 0 0; height: auto; }
	/* On narrow-portrait phones the image's visible SKY extends well
	   below the cream strip (container is taller than image, image is
	   width-fit). The gradient needs to cover the sky-colored strip of
	   the image too — otherwise you see an extra cream-ish "band"
	   continuing past where the gradient ends. Scales with viewport
	   height so the gradient reaches the treeline on short viewports. */
	.hero__top-gradient { height: min(calc(40 * var(--vh-unit, 1vh)), 440px); }
	.hero__top { padding-top: calc(16 * var(--vh-unit, 1vh)); }
}

/* Ultrawide (aspect ≥ 2:1) — the image at this aspect can't show both
   enough sky AND walker feet. Solution: push the image way down with a
   BIG cream strip + tall top-gradient at the top — the strip IS the sky.
   Title lands cleanly above the treeline regardless of how the image
   crops. Placed AFTER the default .hero__bg so it wins the cascade. */
@media (min-aspect-ratio: 2/1) {
	/* Ultrawide: NO cream band. Image fills the hero edge-to-edge.
	   Above the title, a translucent-cream gradient fades over the
	   top of the image (never fully opaque — no "bar" look) giving
	   Camino + tagline enough contrast to read. */
	.hero__bg { inset: 0; height: 100%; }
	.hero__bg progressive-image img { object-position: 47.2% 100%; }
	.hero__top-gradient {
		height: calc(38 * var(--vh-unit, 1vh));
		background: linear-gradient(
			to bottom,
			rgba(242, 243, 239, 0.88) 0%,
			rgba(242, 243, 239, 0.72) 25%,
			rgba(242, 243, 239, 0.4) 55%,
			rgba(242, 243, 239, 0.15) 80%,
			transparent 100%
		);
	}
	.hero__top { padding-top: calc(7 * var(--vh-unit, 1vh)); }
	.hero {
		--hero-title-size: clamp(2.8rem, calc(5 * var(--vh-unit, 1vh)), 5rem);
	}
	/* Diffuse trick on tagline — once the cream gradient thins toward
	   the bottom, the tagline can land over forest; mix-blend-mode +
	   drop-shadow lets it pop against either background. */
	.hero__tagline {
		mix-blend-mode: difference;
		color: var(--bg);
		opacity: 0.9;
		filter:
			drop-shadow(0 0 12px rgba(128, 128, 128, 0.75))
			drop-shadow(0 0 32px rgba(128, 128, 128, 0.45));
	}
}

/* Ultrawide (≥2:1) OR ultra-hotdog (landscape ≤ 360 tall) — at these
   aspects the cream cloud-band is too thin for the big Camino + tagline
   to sit in cleanly. Hide the in-hero title entirely; the nav's
   brand-pill handles "Camino" branding (it morphs into the scroll-up
   chevron on scroll, just like on other pages). Also drop the top
   cream gradient since there's no hero title to frame. */
@media (min-aspect-ratio: 2/1), (orientation: landscape) and (max-height: 360px) {
	.hero__title,
	.hero__eyebrow,
	.hero__tagline,
	.hero__top-gradient {
		display: none;
	}
	.hero__top {
		padding-top: 0;
		min-height: 0;
	}
	/* Image fills the hero top-to-bottom — with the gradient hidden,
	   any cream body inset would read as an un-faded solid band at
	   the top. Setting inset: 0 guarantees there's nothing to see
	   there: image only, then the bottom content. */
	.hero__bg { inset: 0; height: 100%; }
}

/* Extremely high-res displays (4K+ height) — bump title a notch so
   Camino doesn't look lost in a huge viewport. */
@media (min-height: 2000px) and (max-aspect-ratio: 2/1) {
	.hero {
		--hero-title-size: clamp(6rem, calc(8.5 * var(--vh-unit, 1vh)), 14rem);
	}
}

/* Tall portrait (aspect < 3/4 e.g. 430x932 iPhone Pro Max) — title sizing
   stays modest; just nudge padding so title sits in the cloud band. */
@media (max-width: 480px) and (max-aspect-ratio: 3/4) {
	.hero__top { padding-top: calc(14 * var(--vh-unit, 1vh)); }
}

/* Non-fullscreen desktop windows + tablets (769–1199) — the default
   2.1rem floor stretches "Accountability Partner Programs" to 60–70% of
   viewport width, making it feel cramped when the browser isn't
   maximized. Scale with width here so it stays comfortably centered. */
@media (min-width: 769px) and (max-width: 1199px) {
	/* Subtitle = 2 × subtitle-small via the base calc. Keep both at
	   their defaults; shrinking subtitle-small here is how we shrink
	   the subtitle, because the rule is rigid. */
}

@media (max-width: 768px) {
	.hero {
		/* Cap 10rem so tall phones AND 1:3-aspect portraits at width 600
		   (600×1800) can let 9vh breathe and title fills ≥18% of cloud. */
		--hero-title-size: clamp(2.8rem, calc(9 * var(--vh-unit, 1vh)), 10rem);
		/* Shrink subtitle-small on narrow phones — subtitle follows via
		   the 2× rule (so subtitle is 1.8rem ≈ 32px on mobile). */
		--hero-subtitle-small-size: 0.9rem;
	}
	.hero__subtitle {
		padding: 0 8px;
	}
	.hero__tagline { font-size: 0.95rem; }
	.hero__top { padding-top: calc(12 * var(--vh-unit, 1vh)); }
	.hero__bottom { bottom: 16px; padding: 0 var(--space-lg); }
	.hero__bottom-line { margin-bottom: var(--space-md); }
	.hero__subtitle-small { margin-bottom: var(--space-md); }
	.hero__actions { flex-direction: row; justify-content: center; gap: 10px; }

	.text-page__headline { font-size: 2.2rem; }
	.text-page__body { font-size: 1rem; line-height: 1.85; }
	.text-page { min-height: 100svh; padding: var(--space-3xl) var(--space-lg); }

	.about__actions { flex-direction: column; align-items: center; }
	.about__text { font-size: 1.2rem; line-height: 1.5; }

	.image-page { height: var(--vh-stable, 100svh); }
	.man-walking img { object-position: 55% 15%; }

	.programs__grid { grid-template-columns: 1fr; max-width: 100%; }
	.programs { min-height: 100svh; padding: var(--space-3xl) var(--space-lg); }
	.programs__header { margin-bottom: var(--space-xl); }
	.card__hero { height: 220px; }

	.cta h2 { font-size: 1.8rem; }
	.cta { min-height: 100svh; padding: var(--space-3xl) var(--space-lg); }
	.cta p { font-size: 0.95rem; }
	.signup { flex-direction: column; max-width: 320px; }
	.signup__btn { width: 100%; }
}

/* Hotdog landscape phones / ultra-short (≤450px) — hide buttons, compact.
   Must come AFTER the max-width: 768 block so its title + subtitle sizes
   actually win the cascade on viewports that match both. */
@media (orientation: landscape) and (max-height: 450px) {
	/* .hero's 500px min-height would push subtitle/buttons off a 360px
	   viewport. Drop the floor so the hero collapses to viewport height. */
	.hero { min-height: 0; }
	.hero__bottom .hero__actions { display: none; }
	/* subtitle-small stays visible — per design rule, "For Pornography
	   Recovery, Digital Detox, and More" is never hidden. */
	.hero__bottom-line { margin-bottom: 6px; }
	.hero__subtitle { margin-bottom: 0; white-space: normal; padding: 0; }
	/* Push title high so it sits above the tight cloud band on short
	   landscape viewports — 4vh tuned so title has both (a) center-in-
	   clouds and (b) ≥60px clearance above the treeline. */
	.hero__top { padding-top: calc(4 * var(--vh-unit, 1vh)); }
	.hero {
		/* Title bumped to 2.2rem floor so it still fills ≥18% of cloud
		   on very short landscape (360-420 tall). Subtitle small fixed
		   so it's subordinate but readable. */
		--hero-title-size: clamp(2.2rem, calc(8 * var(--vh-unit, 1vh)), 3rem);
		--hero-subtitle-small-size: 0.625rem;  /* subtitle = 2× = 1.25rem */
	}
	.hero__tagline { font-size: clamp(0.7rem, calc(2 * var(--vh-unit, 1vh)), 1rem); }
}

/* ─────────────────────────────────────────────────────────────
   The app — four real screens, stepping down the page in the
   order a night runs. Built on the site's own tokens and reveal
   class, so it inherits the page's type, colour and motion.
   ───────────────────────────────────────────────────────────── */
.screens {
	padding: 9rem 0 10rem;
	background: var(--cream, #f2f3ef);
}

.screens__header { text-align: center; }
.screens__header h2 { margin-top: 0.4rem; }

.screens__strip {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: 2vw;
	width: min(1300px, 92vw);
	margin: 4.5rem auto 0;
	align-items: start;
}

.shot {
	margin: 0;
	display: grid;
	gap: 1rem;
	justify-items: start;
	/* Each screen sits a little lower than the last, so the row reads
	   as a sequence rather than a shelf. */
	transform: translateY(calc(var(--i) * 3.2vh));
}

.shot__num {
	font-family: 'Vollkorn SC', Georgia, serif;
	font-size: 0.62rem;
	letter-spacing: 0.16em;
	color: #b39a4d;
}

/* The device: a plain dark bezel, the same corner radius the phone has. */
.shot__device {
	width: 100%;
	border-radius: 13.5%/6.4%;
	background: linear-gradient(150deg, #2b3a33, #16211c 42%, #0d1512);
	padding: 2.6%;
	box-shadow:
		0 1px 0 rgba(255, 255, 255, 0.22) inset,
		0 0 0 1px rgba(0, 0, 0, 0.35),
		0 34px 60px -34px rgba(13, 26, 20, 0.55);
}

.shot__device img {
	display: block;
	width: 100%;
	border-radius: 11.4%/5.4%;
}

.shot figcaption {
	display: grid;
	gap: 0.2rem;
	font-size: 0.92rem;
	line-height: 1.45;
	color: #616d66;
	max-width: 23ch;
}

.shot figcaption strong {
	font-weight: 500;
	font-size: 1.02rem;
	color: #2d4a3e;
}

@media (max-width: 1000px) {
	.screens__strip { grid-template-columns: repeat(2, 1fr); gap: 3rem 2rem; }
	.shot { transform: none; }
}

@media (max-width: 620px) {
	.screens__strip { grid-template-columns: 1fr; }
	.shot { justify-items: center; text-align: center; }
	.shot figcaption { max-width: 30ch; }
}

/* ─────────────────────────────────────────────────────────────
   Hero, gradients removed: the photograph is the whole surface.

   The cream strip along the top was the page background showing
   through — `.hero__bg` is inset below the nav on small viewports
   and the top gradient used to cover the seam on every size. With
   the gradients gone the seam is bare, so the image runs to the
   very top edge instead.
   ───────────────────────────────────────────────────────────── */
.hero__bg { inset: 0 !important; height: 100% !important; }

/* ─────────────────────────────────────────────────────────────
   The hero line, said in three breaths.

   All three phrases occupy the SAME grid cell, so the line never
   reflows and nothing below it moves — the sizing contract above
   (subtitle = 2× subtitle-small) is untouched. Each one rises in
   out of a soft blur, holds long enough to actually be read, and
   lifts away; the fades overlap slightly so the line is never
   empty. 12s round trip, which is slow on purpose: this is a
   sentence, not a slot machine.
   ───────────────────────────────────────────────────────────── */
.hero__rotator {
	display: grid;
	justify-items: center;
}

.hero__rotator > span {
	grid-area: 1 / 1;
	opacity: 0;
	animation: hero-phrase 12s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}

.hero__rotator > span:nth-child(2) { animation-delay: 4s; }
.hero__rotator > span:nth-child(3) { animation-delay: 8s; }

@keyframes hero-phrase {
	0%   { opacity: 0; transform: translateY(14px); filter: blur(5px); }
	5%   { opacity: 1; transform: none;             filter: blur(0); }
	27%  { opacity: 1; transform: none;             filter: blur(0); }
	33%  { opacity: 0; transform: translateY(-14px); filter: blur(5px); }
	100% { opacity: 0; transform: translateY(14px); filter: blur(5px); }
}

/* Reduced motion gets the whole offer at once, stacked and still —
   the same words, no cycling. */
@media (prefers-reduced-motion: reduce) {
	.hero__rotator { gap: 0.15em; }
	.hero__rotator > span {
		grid-area: auto;
		opacity: 1;
		animation: none;
		filter: none;
		transform: none;
		font-size: 0.62em;
	}
}

/* ─────────────────────────────────────────────────────────────
   The cover treatment, ported from the app.

   `HeroCoverBackdrop` in Cover.swift lays the day's photograph
   under a forest wash and two ramps: a short one at the top, a
   long one climbing out of the bottom, plus a radial pool of the
   same dark centred on the title. Same colour (rgb 0.07/0.13/0.10
   → #12211a) and the same stops, so the site's hero and the app's
   cover are lit identically.

   This is not the cream-to-transparent fade that used to sit here.
   That one bleached the top of the frame; this one just deepens
   the forest that is already in the photograph.
   ───────────────────────────────────────────────────────────── */
.hero__bg::after {
	content: '';
	position: absolute;
	inset: 0;
	pointer-events: none;
	background:
		/* title scrim — a soft pool where the wordmark sits */
		radial-gradient(
			58% 34% at 50% 22%,
			rgba(18, 33, 26, 0.52) 0%,
			rgba(18, 33, 26, 0.28) 45%,
			rgba(18, 33, 26, 0) 100%
		),
		/* the climb out of the bottom */
		linear-gradient(
			to bottom,
			rgba(18, 33, 26, 0) 45%,
			rgba(18, 33, 26, 0.18) 62%,
			rgba(18, 33, 26, 0.42) 78%,
			rgba(18, 33, 26, 0.66) 92%,
			rgba(18, 33, 26, 0.78) 100%
		),
		/* the short ramp at the top */
		linear-gradient(
			to bottom,
			rgba(18, 33, 26, 0.42) 0%,
			rgba(18, 33, 26, 0.26) 14%,
			rgba(18, 33, 26, 0.10) 28%,
			rgba(18, 33, 26, 0) 42%
		),
		/* the flat wash the app puts over the whole frame */
		rgba(18, 33, 26, 0.22);
}

/* On the forest ground the top block reads in cream, the way the
   app's cover title does — the dark-green-on-sky problem the bare
   photograph had. */
.hero__title,
.hero__eyebrow,
.hero__tagline {
	color: #f7f6f1;
}

/* ─────────────────────────────────────────────────────────────
   Hero: three lines, pinned, advanced by the scroll.

   The section is three viewports tall; the stage inside it sticks,
   so the photograph holds still while you travel through it and
   each line owns a third of the distance. Everything else that
   used to live here — eyebrow, wordmark, tagline, buttons — is
   gone, so the only thing on the first screen is the sentence.
   ───────────────────────────────────────────────────────────── */
.hero {
	display: block !important;
	overflow: hidden !important;
	height: calc(100 * var(--vh-unit, 1vh));
	min-height: calc(100 * var(--vh-unit, 1vh));
	padding: 0;
}

.hero__stage {
	position: relative;
	top: 0;
	height: calc(100 * var(--vh-unit, 1vh));
	min-height: calc(100 * var(--vh-unit, 1vh));
	overflow: hidden;
	display: grid;
	place-items: center;
	isolation: isolate;
}

/* The rotator no longer cycles on a timer — the scroll is the timer. */
.hero__rotator {
	position: relative;
	z-index: 2;
	display: grid;
	justify-items: center;
	padding: 0 6vw;
	margin: 0;
	max-width: 100%;
}

.hero__rotator > span {
	grid-area: 1 / 1;
	opacity: 0;
	animation: none !important;
	/* No transition: every value is written per frame from the scroll
	   position, so a transition here would lag the hand. */
	will-change: opacity, transform, filter;
}

/* A hairline that fills as you move through the three — the only
   chrome left, and it doubles as the affordance that says scroll. */
.hero__scroll-cue {
	/* A drawn arrow rather than a bar: a long hairline stem with a chevron on
	   the end, which says "downward" at a glance where a plain 64px tick did
	   not. It doubles as the progress indicator — the lit copy on top is
	   revealed from the top down as the film runs. */
	position: absolute;
	bottom: calc(4 * var(--vh-unit, 1vh));
	left: 50%;
	translate: -50% 0;
	width: 14px;
	height: 132px;
	z-index: 3;
	color: rgba(247, 246, 241, 0.26);
	pointer-events: none;
}

.hero__scroll-cue svg {
	width: 100%;
	height: 132px;
	display: block;
	overflow: visible;
}

.hero__scroll-fill {
	position: absolute;
	inset: 0;
	overflow: hidden;
	height: calc(var(--hero-progress, 0) * 100%);
	color: rgba(247, 246, 241, 0.92);
}

/* Before any scrolling has happened the whole arrow breathes downward, which
   is the part that makes it read as an instruction. It is stopped the moment
   the film starts moving. */
.hero--waiting .hero__scroll-cue {
	animation: hero-hint-bob 2.6s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}

@media (prefers-reduced-motion: reduce) {
	.hero { height: auto; min-height: calc(100 * var(--vh-unit, 1vh)); }
	.hero__stage { position: relative; }
	.hero__rotator { gap: 0.3em; }
	.hero__rotator > span {
		grid-area: auto;
		opacity: 1;
		transform: none;
		filter: none;
		font-size: 0.6em;
	}
	.hero__scroll-cue { display: none; }
}

/* The photograph holds. No push-in, and no parallax translate either —
   the site's hero parallax is meant for a one-screen hero, and inside a
   pinned section it would drift the frame while the words are still
   being read. The picture stays put until all three lines are done. */
.hero__bg {
	/* Held steady through the three lines, then lagged on the way out —
	   `--hero-lag` runs 0 → 9vh across the final stretch of the pin, so the
	   photograph trails the stage as it leaves rather than moving with it.
	   The site's own scroll-timeline parallax is switched off here because
	   it assumes a one-screen hero and would drift the frame mid-sentence. */
	scale: none !important;
	animation: none !important;
	transform: translate3d(0, var(--hero-lag, 0px), 0) scale(1.04) !important;
	will-change: transform;
}

/* The photograph fills its stage edge to edge. The base rule sets
   `inset: 0`, but two later breakpoints inset the top to clear the nav —
   which, with the gradients gone, left a bare cream strip under the bar. */
.hero__stage .hero__bg {
	inset: 0 !important;
	height: 100% !important;
}

/* The last line is the promise, so it is set larger than the two that
   set it up — and because all three share one grid cell, growing it
   moves nothing else on the screen. */
.hero__rotator > span:last-child {
	font-size: 1.32em;
	letter-spacing: -0.025em;
}

@media (max-width: 700px) {
	.hero__rotator > span:last-child { font-size: 1.18em; }
}

/* ─────────────────────────────────────────────────────────────
   The lines, large and low.

   Anchored to the foot of the frame rather than its middle: the
   photograph's road runs into the bottom of the picture, so the
   words sit where the eye already ends up, and the whole upper
   two-thirds of the forest stays uninterrupted.

   `.hero__subtitle` sets `white-space: nowrap` for its own much
   smaller sizing contract — at this scale that would push the
   longest line off both edges, so it wraps here instead.
   ───────────────────────────────────────────────────────────── */
.hero__stage {
	place-items: end center;
}

.hero__rotator {
	width: min(1180px, 92vw);
	padding-bottom: calc(16 * var(--vh-unit, 1vh));
	white-space: normal;
	text-wrap: balance;
	font-size: clamp(2.6rem, 7.4vw, 7.4rem);
	line-height: 0.94;
	letter-spacing: -0.035em;
	/* The drop-shadow glow the small subtitle wears is tuned for small
	   type; at this size it smears, so the scrim under the words does
	   the legibility work instead. */
	filter: none;
	mix-blend-mode: normal;
	color: #f7f6f1;
	opacity: 1;
}

/* A little more weight under the words, so cream type holds on the
   brightest part of the track. */
.hero__stage::after {
	content: '';
	position: absolute;
	inset: auto 0 0 0;
	height: calc(52 * var(--vh-unit, 1vh));
	background: linear-gradient(to top, rgba(18, 33, 26, 0.72), rgba(18, 33, 26, 0) 100%);
	pointer-events: none;
	z-index: 1;
}

.hero__rotator > span:last-child {
	font-size: 1.12em;
}

/* Left over from the video-era hero, where this was a 44px tick. The arrow
   above is 132px and was being clipped to a third of itself by this. */

@media (max-width: 700px) {
	.hero__rotator {
		/* Bigger on the small screen, not smaller: a phone is held close, the
		   words are the whole hero, and 11vw was reading as a caption under a
		   film rather than the film's own line. */
		font-size: clamp(2.3rem, 13.5vw, 4rem);
		/* And sitting higher — a third of the way up rather than a seventh,
		   which clears the home indicator and stops the last line hugging the
		   bottom edge on a tall phone. */
		padding-bottom: calc(24 * var(--vh-unit, 1vh));
	}
	.hero__rotator > span:last-child { font-size: 1.08em; }
}

/* ─────────────────────────────────────────────────────────────
   The hero film.

   Same pinned stage as before — the video fills it exactly the way
   the photograph did, and the scroll drives its timeline instead of
   a set of words. No text over it at all, so the cover wash is
   pulled right back: just enough to keep the nav pills legible
   against a bright meadow, nothing more.
   ───────────────────────────────────────────────────────────── */
.hero__frames {
	position: absolute;
	inset: 0;
	overflow: hidden;
	/* What the dissolve dips through — the app's own forest ground, so the
	   quarter-second between the two shots reads as a held beat rather than
	   a hole in the page. */
	background: #12211a;
}

.hero__frame + .hero__frame {
	/* Hidden until the wipe starts writing its mask. */
	visibility: hidden;
}

.hero__frame {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	opacity: 1;
	/* The dissolve is written per frame by hero-film.js; the drift below is
	   the compositor's own. */
	will-change: opacity, transform;
	transform-origin: 50% 50%;
	backface-visibility: hidden;
}

/* The slow drift.
   A still photograph on a full screen dies without it — but this is a CSS
   animation rather than something the film's clock writes, so it does not
   stop when the film does. It ping-pongs (alternate) instead of looping, so
   it never snaps back to the start, and the two shots pull opposite ways so
   the dissolve has a direction through it. */
@keyframes hero-drift-in {
	from { transform: scale(1.02) translate3d(1.2%, 0.6%, 0); }
	to   { transform: scale(1.15) translate3d(-1.2%, -0.8%, 0); }
}

@keyframes hero-drift-out {
	from { transform: scale(1.15) translate3d(-1.1%, -0.7%, 0); }
	to   { transform: scale(1.02) translate3d(1.1%, 0.7%, 0); }
}

/* 13% of scale across 15s — about 1% a second, which is the point where a
   slow push stops being something you can only measure and becomes something
   you can see. The easing is shallow (0.3/0.7 rather than a true ease-in-out)
   so the ping-pong doesn't park the picture at either end. */
.hero__frame:first-child {
	animation: hero-drift-in 15s cubic-bezier(0.3, 0, 0.7, 1) infinite alternate;
	/* Begun mid-stroke. Without this the opening shot is at the slow end of
	   its easing for the first second or two — exactly the moment the page
	   loads and someone is deciding whether anything here moves at all. */
	animation-delay: -4s;
}

.hero__frame:last-child {
	animation: hero-drift-out 17s cubic-bezier(0.3, 0, 0.7, 1) infinite alternate;
	/* Started part-way in, so the second shot is already moving when it
	   arrives rather than beginning its travel at the cut. */
	animation-delay: -5s;
}

@media (prefers-reduced-motion: reduce) {
	.hero__frame { animation: none !important; transform: scale(1.04); }
}


/* Lighter than the photograph's treatment: the film is the subject. */
.hero__bg::after {
	background:
		linear-gradient(to bottom, rgba(18, 33, 26, 0.34) 0%, rgba(18, 33, 26, 0.10) 18%, rgba(18, 33, 26, 0) 34%),
		linear-gradient(to top, rgba(18, 33, 26, 0.30) 0%, rgba(18, 33, 26, 0) 30%);
}

/* The words are gone, so the bottom scrim goes with them. */
.hero__stage::after { content: none; }

/* The opening statement is a sentence, not a slogan — it needs a reading
   size and a measure, where the three that follow are display type. All
   four still share one grid cell, so nothing reflows between them. */
.hero__line--lede {
	font-size: 0.52em;
	line-height: 1.2;
	letter-spacing: -0.02em;
	max-width: 20ch;
	margin-inline: auto;
}

@media (max-width: 700px) {
	/* The sentences scale with the rest, and keep a narrow measure so they
	   still break into two tidy lines rather than four ragged ones. */
	.hero__line--lede { font-size: 0.58em; max-width: 15ch; }
}

/* A hero whose film has already played out: no pin left to travel, so it
   stands down to one screen and the page carries on. Restored the moment
   the film is replayed from the top. */

/* The payoff line rides higher than the four that lead to it — far enough
   off the foot of the frame to feel like a landing rather than a caption.
   Read per frame by hero-scrub.js, which owns the transform. */
.hero__line--last { --lift: 96px; }

@media (max-width: 700px) {
	.hero__line--last { --lift: 64px; }
}

/* ─────────────────────────────────────────────────────────────
   The scroll hint.

   Appears only after the final line has landed and only if nobody
   has scrolled yet — an answer to "what now", not a permanent
   piece of furniture. It leaves the moment the page is touched.
   ───────────────────────────────────────────────────────────── */
.hero__hint {
	position: absolute;
	left: 50%;
	translate: -50% 0;
	z-index: 3;
	/* On its own plate, in the same smoked glass as the nav pills and the
	   download strip. Every position tried on the bare artwork failed
	   somewhere: under the words is the lit, streaked water (2.8:1 at
	   1920x1080, where the words are large enough to fill the ridge on their
	   own), the foot of the frame is the figure, and the margins read as
	   stray. A cue that brings its own ground is legible wherever it lands,
	   and it says "control" rather than "part of the picture". */
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.42rem 0.85rem 0.42rem 0.95rem;
	border-radius: 999px;
	background: rgba(18, 33, 26, 0.32);
	border: 1px solid rgba(247, 246, 241, 0.16);
	backdrop-filter: blur(12px) saturate(0.85);
	-webkit-backdrop-filter: blur(12px) saturate(0.85);
	box-shadow: 0 12px 30px -26px rgba(11, 22, 16, 0.75);
	color: rgba(247, 246, 241, 0.96);
	opacity: 0;
	transform: translateY(8px);
	transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1), transform 0.9s cubic-bezier(0.16, 1, 0.3, 1);
	pointer-events: none;
}

.hero__hint span {
	font-family: 'Vollkorn SC', Georgia, serif;
	font-size: 0.64rem;
	letter-spacing: 0.22em;
	text-transform: uppercase;
	line-height: 1;
}

.hero__hint svg {
	width: 13px;
	height: 19px;
	animation: hero-hint-bob 2.4s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}

@keyframes hero-hint-bob {
	0%, 100% { transform: translateY(-2px); opacity: 0.8; }
	45%      { transform: translateY(3px); opacity: 1; }
}

/* Shown again — the hint it used to collide with is gone, and with the film
   on the scroll this line finally means something: it is how far through the
   hero you are. */

@media (prefers-reduced-motion: reduce) {
	.hero__hint svg { animation: none; }
}

/* ─────────────────────────────────────────────────────────────
   The two store buttons, arriving with the last line.

   Drawn in the site's own language rather than pasted store
   badges: those are trademarked lockups with their own clear-space
   and colour rules, and an approximation of one looks worse than
   an honest button. Swap in the official artwork from Apple's and
   Google's brand pages when you ship — the markup is ready for it.
   ───────────────────────────────────────────────────────────── */
.hero__stores {
	/* Fixed, not absolute: they arrive with the last line and then stay put
	   at the foot of the window for the rest of the visit, while the hero
	   scrolls away behind them. */
	position: absolute;
	left: 50%;
	top: var(--stores-top, 84vh);
	translate: -50% 0;
	/* Page furniture, so it lives at page level in the markup and sits on the
	   one layer just under the nav (60). Anything lower and a section with a
	   stacking context of its own can paint over it. */
	z-index: 58;
	display: flex;
	gap: 0.7rem;
	opacity: 0;
	transform: translateY(9px);
	pointer-events: none;
}

/* The film writes these per frame now — see hero-film.js. The transition
   below is gone with it: a 0.55s ease cannot be scrubbed. */

/* The badges are artwork, not buttons — and they cannot be recoloured,
   faded or blurred: both vendors' brand rules require the marks to appear
   unmodified. So the softening happens BEHIND them instead. The pair sits
   on a frosted plate of our own, which knocks back the contrast against the
   meadow without touching the artwork, and they are set smaller than before
   so they read as a footnote to the line rather than competing with it. */
.hero__stores {
	/* Minimal, and low. A tight glass strip hugging the bottom edge rather
	   than a floating panel: less plate, less padding, smaller badges, and
	   the two always side by side — including on the phone, where they used
	   to stack into a block that ate a third of the screen. */
	padding: 0.3rem 0.4rem;
	border-radius: 16px;
	background: rgba(18, 33, 26, 0.22);
	border: 1px solid rgba(247, 246, 241, 0.12);
	backdrop-filter: blur(12px) saturate(0.85);
	-webkit-backdrop-filter: blur(12px) saturate(0.85);
	box-shadow: 0 12px 30px -26px rgba(11, 22, 16, 0.75);
	gap: 0.35rem;
}

.store {
	display: inline-flex;
	align-items: center;
	text-decoration: none;
	transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
	/* Clear space, since neither asset carries its own any more: both
	   vendors ask for a margin of at least a tenth of the badge height. */
	padding: 5px 7px;
}

.store::after { display: none; }

.store img {
	display: block;
	height: 30px;
	width: auto;
}

/* Both badges are content-tight now, so they take the same height and the
   pair reads as one row. Google's file shipped with 33% of its height as
   transparent margin baked in; compensating for that by drawing it taller
   is what made it look stretched next to Apple's. The margin was cropped
   off the asset instead, and the clear space both vendors ask for is given
   back as padding on the link below — same guarantee, no distortion. */
.store:last-child img { height: 30px; }



.store:hover { transform: translateY(-2px); }

@media (max-width: 700px) {
	.hero__stores {
		bottom: 10px;
		gap: 0.25rem;
	}
	.store img,
	.store:last-child img { height: 38px; }
}

@media (max-width: 700px) {
	/* Taller strip on the phone, so the hint needs to sit higher again. */
	.hero__hint { bottom: 22%; }
}

/* One badge when we know which phone this is, both when we do not. The strip
   tightens around whichever survives so a single badge does not sit in a
   plate built for two. */
.hero__stores.stores--apple .store:last-child,
.hero__stores.stores--android .store:first-child {
	display: none;
}

.hero__stores.stores--apple,
.hero__stores.stores--android {
	gap: 0;
	padding: 0.3rem 0.45rem;
}

/* The app's own mark at the head of the strip, with its name beneath — so
   the row reads as "this app, from these two places". Set the way it looks
   on an iPhone home screen: rounded square, and a very small system-sans
   label under it rather than the site's display face.

   Alignment: the strip is top-aligned and the mark carries the same 5px of
   top padding the badge links do, so the icon's top edge and the badge
   artwork's top edge land on the same line. The label then hangs below in
   the strip's own padding without pushing the icon out of true. */
.store-mark {
	display: grid;
	justify-items: center;
	gap: 3px;
	padding: 5px 9px 0 5px;
	margin-right: 1px;
	border-right: 1px solid rgba(247, 246, 241, 0.14);
}

.store-mark img {
	display: block;
	/* Same height as the badge artwork beside it. */
	width: 30px;
	height: 30px;
	border-radius: 7px;
}

.store-mark em {
	font-style: normal;
	font-family: -apple-system, 'SF Pro Text', system-ui, sans-serif;
	font-size: 7px;
	font-weight: 500;
	letter-spacing: 0.01em;
	color: rgba(247, 246, 241, 0.85);
	line-height: 1;
}

.hero__stores { align-items: flex-start; }

/* The mark is taller than the badges (the label hangs below the icon), so
   the row box is taller than a badge. Top-aligning the badges too left them
   riding high with dead glass underneath; they centre in the strip instead. */
.hero__stores .store { align-self: center; }

@media (max-width: 700px) {
	.store-mark img { width: 38px; height: 38px; border-radius: 9px; }
	.store-mark em { font-size: 8px; }
	.store-mark { padding: 5px 7px 0 4px; }
}


/* ─────────────────────────────────────────────────────────────
   The hero's light mode.

   The film closes on a pale illustration. Cream type on it measured
   1.2:1 — not dim, unreadable — and the difference blend that keeps
   the type legible over dark photographs turns a mid-tone backdrop
   into a muddy purple rather than clean ink. So over the pale shot
   the hero simply stops blending and sets the type in the forest
   ink instead, at about 12:1.
   ───────────────────────────────────────────────────────────── */
.hero--light .hero__subtitle {
	/* A pale shot: the difference blend turns a bright backdrop to mud, so the
	   closing lines are set plainly, in the forest ink. */
	mix-blend-mode: normal;
	color: #12211a;
	opacity: 1;
	filter: none;
}

/* Unless the shot also asked for shade — then the words sit on the shade and
   go back to cream. The two flags are independent: one says "this picture is
   pale", the other says "this picture has nowhere quiet to set type". */
.hero--light.hero--scrim .hero__subtitle {
	color: var(--bg);
}

/* ─────────────────────────────────────────────────────────────
   The coda.

   Set small and italic under the closing line, at a size that makes
   it an aside rather than a sixth headline. Placed by hero-film.js
   against the same numbers that place the words, so it follows them
   at every viewport.
   ───────────────────────────────────────────────────────────── */
.hero__coda {
	position: absolute;
	top: var(--coda-top, 62%);
	left: 50%;
	z-index: 2;
	font-family: var(--serif);
	font-style: italic;
	font-weight: 300;
	font-size: clamp(1.6rem, 3vw, 3.1rem);
	letter-spacing: 0.01em;
	line-height: 1;
	color: rgba(247, 246, 241, 0.88);
	text-shadow: 0 2px 14px rgba(11, 22, 16, 0.55);
	white-space: nowrap;
	opacity: 0;
	visibility: hidden;
	transform: translate(-50%, 9px);
	pointer-events: none;
}

.hero--light .hero__coda {
	color: rgba(18, 33, 26, 0.82);
	text-shadow: none;
}

.hero--light.hero--scrim .hero__coda {
	color: rgba(247, 246, 241, 0.88);
	text-shadow: 0 2px 14px rgba(11, 22, 16, 0.55);
}

/* A band of shade for illustrations with nowhere quiet to set type.

   Weighted to where the closing lines land and faded out well above and
   below them, so it reads as light falling through the canopy rather than as
   a bar laid over the picture. Only applied to shots that ask for it. */
.hero--scrim .hero__stage::after {
	content: '';
	position: absolute;
	inset: 0;
	/* The video-era rule for this pseudo-element still pins it to 52vh; without
	   resetting that the shade stopped dead partway down and drew a hard seam
	   across the middle of the picture. */
	height: auto;
	z-index: 1;
	pointer-events: none;
	background: linear-gradient(
		to bottom,
		rgba(10, 20, 17, 0) 8%,
		rgba(10, 20, 17, 0.34) 32%,
		rgba(10, 20, 17, 0.68) 56%,
		rgba(10, 20, 17, 0.5) 78%,
		rgba(10, 20, 17, 0.12) 100%
	);
}


/* ─────────────────────────────────────────────────────────────
   Scroll-driven hero.

   The section is three screens tall and the stage sticks to the top
   of the window, so the first two screens of scrolling are spent on
   the film. hero-film.js maps that distance onto the timeline it
   used to play on a clock.

   This block was lost once already, when a neighbouring section was
   cut out by index — without it the hero collapses to one screen,
   the scrub span is zero, and the film pins to its final frame at
   the top of the page, so the opening illustration never appears.
   ───────────────────────────────────────────────────────────── */
.hero {
	height: calc(300 * var(--vh-unit, 1vh)) !important;
	min-height: calc(300 * var(--vh-unit, 1vh)) !important;
	overflow: visible !important;
}

.hero__stage {
	position: sticky;
	top: 0;
	height: calc(100 * var(--vh-unit, 1vh));
	min-height: calc(100 * var(--vh-unit, 1vh));
}

/* Both hang off the window rather than the foot of the section — with the
   stage pinned, the foot of the section is two screens away. */
.hero__stores {
	position: fixed;
	top: auto;
	bottom: 12px;
}

@media (max-width: 700px) {
	.hero__stores { bottom: 10px; }
}


/* An optional dim per shot.

   Photographs need this where the illustrations did not: a sunlit frame has
   specular highlights that no gradient can rescue — the wicker in the opening
   shot measured 1.0:1 against cream type at almost every height, and even a
   heavy scrim only reached 4.4:1. Bringing the whole picture down deals with
   the highlights themselves. Declared per shot as --dim / --sat, keyed by the
   attribute the shot already wears. */
.hero__frame {
	filter: brightness(calc(var(--dim, 1) * var(--dim-scale, 1))) saturate(var(--sat, 1));
}

/* The numbers themselves. These rode inline on each <img> until the server
   grew a CSP — style-src without 'unsafe-inline' drops style attributes
   wholesale, and the hero quietly lost its dark. The crops live here with
   them: same attributes, same reason. */
.hero__frame[data-dimmed] {
	object-position: 50% 52%;
	--dim: 0.56;
	--sat: 0.9;
}

.hero__frame[data-cloud-narrow] {
	object-position: 50% 60%;
}

/* A narrow window crops a photograph to a different slice, and on the opening
   shot that slice is the pale phone back — cream type measured 3.9:1 there
   against 14.5:1 on the desktop crop. The per-shot dim is declared above; the
   breakpoint multiplies it through --dim-scale rather than restating it, so
   one number here serves every photograph that brings its own. */
@media (max-width: 700px) {
	/* Scoped to shots that declare a dim, i.e. the photographs. Applied to
	   .hero__frame at large it also dimmed the illustration that needs no
	   dimming at all, because an undeclared --dim is 1 and 1 x 0.78 is not. */
	.hero__frame[data-dimmed] { --dim-scale: 0.78; }
	/* A shot can ask for a gentler hand on the phone. The closing photograph
	   does: at 0.78 on top of its own 0.72 it came out at 0.56, which is
	   heavier than the picture wants when the crop is already narrow. */
	.hero__frame[data-dimmed="soft"] { --dim-scale: 1; }
}


/* ─────────────────────────────────────────────────────────────
   Where the lines sit.

   Two numbers, both written by hero-film.js: the opening sentences
   are centred in the window, and the closing lines are solved
   against the artwork's own ridge. The fallbacks are what a 900px
   and a 660px window work out to, so a stylesheet-only render is
   still close.

   These rules were lost once, along with the coda's styling and the
   scroll-driven hero block, when a neighbouring section was cut out
   of this file by index. Without them every line falls back to a
   lift of zero and the words sit wherever the flow leaves them.
   ───────────────────────────────────────────────────────────── */
.hero__line { --lift: var(--opening-lift, 56px); }

/* No separate closing lift any more: every line is centred. */

@media (max-width: 700px) {
	.hero__line { --lift: var(--opening-lift, 40px); }
	/* The closing lines take their own number on a phone — see the note in
	   hero-film.js about the cloud. */
	.hero--closing .hero__line { --lift: var(--closing-lift-narrow, var(--opening-lift, 40px)); }
}


/* The word that starts it.

   Bare type, not a plate: an instruction rather than a control, gone within
   the first second of scrolling, sitting directly above the progress arrow
   which carries on alone. (Also lost in that same deletion.) */
.hero__scroll-word {
	position: absolute;
	left: 50%;
	bottom: calc(4 * var(--vh-unit, 1vh) + 148px);
	translate: -50% 0;
	z-index: 3;
	font-family: 'Vollkorn SC', Georgia, serif;
	font-size: 0.62rem;
	letter-spacing: 0.26em;
	text-transform: uppercase;
	color: rgba(247, 246, 241, 0.72);
	text-shadow: 0 2px 12px rgba(11, 22, 16, 0.6);
	pointer-events: none;
}

.hero--light .hero__scroll-word { color: rgba(18, 33, 26, 0.7); text-shadow: none; }

@media (prefers-reduced-motion: reduce) {
	.hero__scroll-word { display: none; }
}

/* ─────────────────────────────────────────────────────────────
   The app, one screen at a time.

   The section is five screens tall with a sticky stage, so the
   page holds still while the phone works through the app — the
   same construction as the hero film, driven by js/screens.js.
   ───────────────────────────────────────────────────────────── */
.screens {
	position: relative;
	height: calc(480 * var(--vh-unit, 1vh));
	background: var(--bg);
}

.screens__stage {
	position: sticky;
	top: 0;
	height: calc(100 * var(--vh-unit, 1vh));
	display: grid;
	place-items: center;
	overflow: hidden;
}

.screens__figure {
	margin: 0;
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: center;
	gap: clamp(2rem, 6vw, 7rem);
	width: min(1100px, 88vw);
}

/* The phone. A real device outline rather than a bare screenshot: the app's
   own screens are edge-to-edge, and without a frame they read as flat images
   of a website instead of something held in a hand. */
.screens__phone {
	position: relative;
	order: 2;
	width: clamp(230px, 24vw, 310px);
	aspect-ratio: 506 / 1100;
	border-radius: clamp(30px, 3.4vw, 44px);
	background: #0b1610;
	padding: 6px;
	box-shadow:
		0 0 0 1px rgba(45, 74, 62, 0.16),
		0 40px 90px -50px rgba(11, 22, 16, 0.6);
}

.screens__shot {
	position: absolute;
	inset: 6px;
	width: calc(100% - 12px);
	height: calc(100% - 12px);
	object-fit: cover;
	object-position: 50% 0;
	border-radius: clamp(25px, 2.9vw, 38px);
	opacity: 0;
	will-change: opacity, transform;
}

.screens__captions {
	position: relative;
	order: 1;
	min-height: 12rem;
	display: grid;
}

/* All five occupy one cell, so nothing reflows as they change over. */
.screens__caption {
	grid-area: 1 / 1;
	margin: 0;
	opacity: 0;
	will-change: opacity, transform;
}

.screens__eyebrow {
	display: block;
	font-family: 'Vollkorn SC', Georgia, serif;
	font-size: 0.97rem;
	letter-spacing: 0.04em;
	color: var(--clay, #cc8f6b);
	margin-bottom: 0.9rem;
}

.screens__title {
	font-family: var(--serif);
	font-weight: 400;
	font-size: clamp(2rem, 3.6vw, 3.1rem);
	line-height: 1.05;
	letter-spacing: -0.02em;
	margin: 0 0 0.9rem;
	color: var(--ink, #2d4a3e);
}

.screens__line {
	font-family: var(--serif);
	font-size: clamp(1.05rem, 1.4vw, 1.3rem);
	line-height: 1.5;
	max-width: 34ch;
	margin: 0;
	color: rgba(45, 74, 62, 0.72);
}

/* Where you are in the run. */
.screens__dots {
	position: absolute;
	right: clamp(14px, 3vw, 40px);
	top: 50%;
	translate: 0 -50%;
	display: grid;
	gap: 10px;
}

.screens__dot {
	width: 6px;
	height: 6px;
	border-radius: 999px;
	background: rgba(45, 74, 62, calc(0.14 + 0.66 * var(--on, 0)));
	scale: calc(0.8 + 0.35 * var(--on, 0));
}

/* Narrow screens.

   The phone is sized by HEIGHT here, not width. Sized by width it came out
   62vw tall inside a 100vh sticky stage, which pushed the caption off the
   bottom of the screen and under the download strip. Height-first, with the
   aspect ratio deriving the width, means the phone and its caption always fit
   the stage together — and the stage keeps clear of the fixed strip. */
@media (max-width: 860px) {
	.screens__stage {
		padding: calc(2 * var(--vh-unit, 1vh)) 0 96px;
		align-content: center;
	}

	.screens__figure {
		grid-template-columns: 1fr;
		justify-items: center;
		align-content: center;
		gap: clamp(1rem, 3vh, 1.8rem);
		text-align: center;
	}

	.screens__phone {
		order: 1;
		height: min(52 * var(--vh-unit, 1vh), 430px);
		width: auto;
		aspect-ratio: 506 / 1100;
	}

	.screens__captions {
		order: 2;
		min-height: 0;
		justify-items: center;
	}

	.screens__eyebrow { margin-bottom: 0.5rem; }
	.screens__title { font-size: clamp(1.6rem, 6.4vw, 2.2rem); margin-bottom: 0.5rem; }
	.screens__line { font-size: 1rem; max-width: 26ch; }
	.screens__dots { display: none; }
}

@media (prefers-reduced-motion: reduce) {
	.screens { height: auto; }
	.screens__stage { position: relative; height: auto; padding: 4rem 0; }
	.screens__shot { opacity: 1 !important; position: relative; inset: auto; }
	.screens__caption { opacity: 1 !important; }
}


/* A gradient at the top of the frame, for closing words that sit high.

   On a phone the closing photograph shows its whole height, so the words go up
   into the sky where there is room — and cream type reads better there than
   ink did, provided something carries it. This is that: weighted to the very
   top and gone by the halfway line, so the crowd, the trees and the city keep
   their light entirely. */
@media (max-width: 700px) {
	.hero--cloud .hero__stage::after {
		content: '';
		position: absolute;
		inset: 0 0 auto 0;
		height: 68%;
		z-index: 1;
		pointer-events: none;
		/* Between the two extremes tried: enough weight where the words
		   actually are (the first third) to carry cream type, then the same
		   long falloff so the treeline and the crowd below keep their light. */
		background: linear-gradient(
			to bottom,
			rgba(10, 20, 17, 0.62) 0%,
			rgba(10, 20, 17, 0.56) 26%,
			rgba(10, 20, 17, 0.36) 52%,
			rgba(10, 20, 17, 0.14) 78%,
			rgba(10, 20, 17, 0) 100%
		);
	}
}


/* ─────────────────────────────────────────────────────────────
   The screens run, driven by CSS rather than JavaScript.

   Technique borrowed from virtues.com: the section declares a
   `view-timeline`, and each screen animates against a slice of it
   with `animation-range`. The browser runs it on the compositor —
   no scroll listener, no rAF loop, and it stays in step during
   momentum scrolling on iOS where a JS loop can fall behind.

   `contain` is the right range here: for a section taller than the
   window it spans exactly the stretch where the section fully
   covers the viewport, which is the pinned stretch.

   js/screens.js is still the fallback and bails out when this is
   supported, so only one of the two ever runs.
   ───────────────────────────────────────────────────────────── */
@supports (animation-timeline: view()) {
	.screens {
		view-timeline: --screens block;
	}

	.screens__shot,
	.screens__caption {
		animation: screens-step linear both;
		animation-timeline: --screens;
	}

	/* Held for the middle of each slice, handing over at the edges. */
	@keyframes screens-step {
		0%   { opacity: 0; transform: translate3d(0, 20px, 0); }
		18%  { opacity: 1; transform: none; }
		82%  { opacity: 1; transform: none; }
		100% { opacity: 0; transform: translate3d(0, -20px, 0); }
	}
	.screens__shot:nth-of-type(1),
	.screens__caption:nth-of-type(1) {
		animation-range: contain 0% contain 20%;
	}
	.screens__shot:nth-of-type(2),
	.screens__caption:nth-of-type(2) {
		animation-range: contain 20% contain 40%;
	}
	.screens__shot:nth-of-type(3),
	.screens__caption:nth-of-type(3) {
		animation-range: contain 40% contain 60%;
	}
	.screens__shot:nth-of-type(4),
	.screens__caption:nth-of-type(4) {
		animation-range: contain 60% contain 80%;
	}
	.screens__shot:nth-of-type(5),
	.screens__caption:nth-of-type(5) {
		animation-range: contain 80% contain 100%;
	}
	/* The first and last hold at the ends rather than fading to nothing,
	   so the run opens and closes on a screen instead of on emptiness. */
	.screens__shot:nth-of-type(1),
	.screens__caption:nth-of-type(1) {
		animation-name: screens-first;
	}

	.screens__shot:nth-of-type(5),
	.screens__caption:nth-of-type(5) {
		animation-name: screens-last;
	}

	@keyframes screens-first {
		0%, 82% { opacity: 1; transform: none; }
		100%    { opacity: 0; transform: translate3d(0, -20px, 0); }
	}

	@keyframes screens-last {
		0%   { opacity: 0; transform: translate3d(0, 20px, 0); }
		18%, 100% { opacity: 1; transform: none; }
	}
}


/* The partner question.

   Same furniture as the statement section it sits beside — hairline, eyebrow,
   display line — with the answer set smaller than the question, so the section
   reads as one beat rather than two competing lines. */
.text-page--partner .text-page__title {
	font-family: var(--serif);
	font-weight: 400;
	font-size: clamp(2rem, 4.4vw, 3.6rem);
	line-height: 1.08;
	letter-spacing: -0.02em;
	margin: 0.6rem 0 1.1rem;
	max-width: 18ch;
	color: var(--ink, #2d4a3e);
}

.text-page--partner .about__text {
	font-size: clamp(1.15rem, 1.7vw, 1.5rem);
	color: rgba(45, 74, 62, 0.68);
	/* Centred under the question. The measure has to be centred as a box too,
	   not just its text — a max-width with no auto margins sits flush left
	   however the text inside it is aligned. */
	max-width: 28ch;
	margin-inline: auto;
	text-align: center;
}

.text-page--partner .text-page__title {
	margin-inline: auto;
	text-align: center;
}


/* The partner question sits still — every entrance it ever had
   (a reveal, then a scroll-scrubbed rise) was removed by request:
   the words are simply there. That includes app.css's site-wide
   scroll-fade, which would otherwise dim the answer line at the
   viewport's edges. */
.text-page--partner .about__text {
	animation: none;
	opacity: 0.68;
}

/* ─────────────────────────────────────────────────────────────
   Your daily ritual on Camino.

   Built like the hero, mechanism for mechanism. The section is
   several screens tall; the stage inside it is sticky at the top
   for exactly one screen, so it can never ride — its travel ends
   at the section's last pixel and the next section simply follows.
   home.js scrubs a film timeline off the scroll position and
   paints the captions per frame — and nothing moves: the only
   instrument is light. A sentence fades in grey, its words are
   read up to full cream by the scroll, it holds fully lit for a
   stretch, and it fades out in place. Scrubbed, so the lit edge
   sits exactly where the hand has read to, rewinds when it
   rewinds, and stops when it stops. The last sentence's timeline
   clamps at the end of the run: it locks, and the remaining
   travel scrolls on past a finished, still stage.

   The phone is the one element with a transition: its screen pages
   sideways with the app's own pager gesture when a beat opens,
   keyed off data-part (0 = the day's cover, then one page per
   sentence). The bezel is a single element — a 6px border with
   overflow:hidden, so the screen area has one definition and the
   clip lands on the border's inner edge. Off-screen rest is 102%,
   not 100%: at 100% the waiting page's edge sits exactly on the
   clip line and subpixel rounding shows it as a cream hairline.

   Film mode exists only under [data-film], which home.js sets when
   motion is allowed. Without JS, or with reduced motion, none of
   this applies: the section is an ordinary static block — cover
   showing, every caption listed, nothing hidden and nothing tall.
   ───────────────────────────────────────────────────────────── */
.parts {
	position: relative;
	background: var(--ink);
	/* app.css pads every <section>; here the padding lives on the stage,
	   and any on the section itself would make the sticky release 96px
	   before the section ends — a visible ride at the film's last beat. */
	padding: 0;
}

.parts__stage {
	display: grid;
	grid-template-rows: auto 1fr auto;
	justify-items: center;
	gap: clamp(2rem, 5vh, 3.5rem);
	padding: clamp(4.5rem, 11vh, 7rem) clamp(1.25rem, 5vw, 3rem) clamp(6rem, 13vh, 8rem);
	overflow: hidden;
}

.parts__head {
	font-family: var(--serif);
	font-weight: 400;
	font-size: clamp(1.9rem, 4vw, 3.2rem);
	line-height: 1.06;
	letter-spacing: -0.02em;
	margin: 0;
	text-align: center;
	color: var(--bg);
}

.parts__columns {
	align-self: center;
	width: min(980px, 100%);
	display: grid;
	grid-template-columns: auto minmax(0, 30ch);
	justify-content: center;
	align-items: center;
	column-gap: clamp(3rem, 7vw, 6rem);
}

.parts__frame {
	position: relative;
	display: grid;
	width: fit-content;
	border: 6px solid #0b1610;
	border-radius: clamp(26px, 2.4vw, 34px);
	isolation: isolate;
	background: #0b1610;
	box-shadow:
		0 0 0 1px rgba(242, 243, 239, 0.1),
		0 44px 80px -40px rgba(4, 10, 7, 0.9);
}

/* The glass carries the clip, the frame carries the border and the
   shadow — separated because they cannot share an element: the
   pages animate on their own compositor layers, and on a Retina
   display mid-turn they escape a plain overflow:hidden radius —
   square corners poking past the bezel. clip-path is honoured by
   the compositor for descendant layers, so the glass uses it (with
   overflow:hidden as the fallback), at the frame's inner radius —
   the outer one minus the 6px bezel, so the corners stay
   concentric. And clip-path on the FRAME would have amputated its
   drop shadow, which is why the clip lives one element in. */
.parts__glass {
	display: grid;
	border-radius: calc(clamp(26px, 2.4vw, 34px) - 6px);
	overflow: hidden;
	clip-path: inset(0 round calc(clamp(26px, 2.4vw, 34px) - 6px));
}

.parts__shot {
	grid-area: 1 / 1;
	display: block;
	height: min(62 * var(--vh-unit, 1vh), 560px);
	width: auto;
}

/* Static base: the cover alone on the glass. */
.parts:not([data-film]) .parts__shot:not(:first-child) {
	display: none;
}

.parts__station {
	display: grid;
	gap: clamp(1.8rem, 4vh, 2.6rem);
}

.parts__caption {
	margin: 0;
}

/* Just the sentence — no step label, no small line under it. */
/* The wordiest sentence sets a size down, so six clauses don't
   tower over the phone the way three words do. Its newlines are the
   only wrap points it accepts: each clause holds one line and may
   run past the caption column into the stage's open air, which
   beats the column shredding the clauses into six shards. */
.parts__caption--long h3 {
	font-size: clamp(1.55rem, 2.7vw, 2.35rem) !important;
	white-space: nowrap;
}

/* Each authored line is a block; on the long caption the lines
   after the first read as a LIST, grouped by one continuous rule
   down their left edge — the quote-bar the app's own chat draws
   beside quoted prompts. Per-line borders stack seamlessly into a
   single line because the blocks are contiguous. The words inside
   still light one by one. */
.parts__line {
	display: block;
}

.parts__caption--long .parts__line + .parts__line {
	border-left: 2px solid rgba(242, 243, 239, 0.35);
	padding-left: 0.75em;
	margin-left: 0.1em;
}

.parts__caption h3 {
	font-family: var(--serif);
	font-weight: 400;
	font-size: clamp(1.7rem, 3vw, 2.6rem);
	line-height: 1.15;
	letter-spacing: -0.01em;
	margin: 0;
	color: var(--bg);
}

.parts__progress {
	display: none;
	width: clamp(64px, 8vw, 96px);
	height: 3px;
	border-radius: 999px;
	background: rgba(242, 243, 239, 0.16);
	overflow: hidden;
}

.parts__progress span {
	display: block;
	width: 100%;
	height: 100%;
	border-radius: inherit;
	background: rgba(242, 243, 239, 0.85);
	transform-origin: left center;
	transform: scaleX(var(--parts-zone, 0));
}

/* ── Film mode ──────────────────────────────────────────────── */
.parts[data-film] {
	height: calc(760 * var(--vh-unit, 1vh));
}

.parts[data-film] .parts__stage {
	position: sticky;
	top: 0;
	height: calc(100 * var(--vh-unit, 1vh));
	/* The heading's row takes all the slack, and the heading centres
	   within it — so on every layout it floats midway between the nav
	   and the phone rather than hugging either. The extra top padding
	   is the balance weight: it cancels the nav's height plus the row
	   gap, which both pull the optical centre upward. */
	grid-template-rows: 1fr auto auto;
	padding-top: clamp(6rem, 14vh, 8.5rem);
}

.parts[data-film] .parts__head {
	align-self: center;
}

/* Captions stack on one cell; every visual property is written per
   frame by home.js, the way the hero's lines are. No transitions —
   a transition here would lag the hand. */
.parts[data-film] .parts__caption {
	grid-area: 1 / 1;
	align-self: center;
	opacity: 0;
	visibility: hidden;
	will-change: opacity;
}

.parts[data-film] .parts__progress {
	display: block;
}

/* The pager. Positions are owned by home.js, which runs the pages
   on a spring in its render loop — see the comment there for why a
   CSS transition was the wrong tool. This base state only covers
   the frames before the first script paint: cover on, the rest
   waiting off right. */
.parts[data-film] .parts__shot {
	transform: translateX(102%);
}

.parts[data-film] .parts__shot:first-child {
	transform: translateX(0);
}

@media (max-width: 820px) {
	.parts__columns {
		grid-template-columns: 1fr;
		justify-items: center;
		row-gap: clamp(0.8rem, 2vh, 1.2rem);
	}
	/* The phone is the section's evidence, so on a handset it gets
	   the screen: everything else — heading, gaps, padding — is
	   trimmed to feed it. Two caps: viewport height for tall
	   phones, viewport width (132vw of height ≈ 61vw of width at
	   the shots' ratio) so it never crowds the edges. */
	.parts__shot {
		height: min(56 * var(--vh-unit, 1vh), 132vw);
	}
	.parts__stage {
		gap: clamp(0.8rem, 2vh, 1.2rem);
		padding: 4.5rem clamp(1.25rem, 5vw, 3rem) 5rem;
	}
	.parts__head {
		font-size: 1.6rem;
	}
	.parts__caption {
		text-align: center;
	}
	.parts__caption h3 {
		font-size: clamp(1.5rem, 6vw, 1.9rem);
	}
	/* The block centres — shrunk to its widest clause — the list
	   stays left-aligned inside it so the quote-bar keeps a left
	   edge to hang from, and the opening line centres over the list
	   it introduces. */
	.parts__caption--long h3 {
		font-size: clamp(1.25rem, 5vw, 1.6rem) !important;
		white-space: normal;
		text-align: left;
		width: fit-content;
		margin-inline: auto;
	}
	.parts__caption--long .parts__line:first-child {
		text-align: center;
	}
	/* The quote-bar itself sits under the Y of the centred opening
	   line, the clauses hanging off it — half the width difference
	   between the widest clause and "Your choice of", eyeballed. */
	.parts__caption--long .parts__line + .parts__line {
		margin-left: 2.4em;
	}
	.parts[data-film] .parts__station {
		min-height: 6rem;
		align-items: center;
	}
	/* The heading floats centred in the slack above the phone — the
	   head row takes it all (1fr) and the heading centres within.
	   The top padding stays outside the centring, so the head lands
	   (padding − row-gap)/2 below the true centre of section-top to
	   phone-top. At the nav-clearing 5.25rem that read as riding
	   low; 2rem is within half a rem of exact and still clears. */
	.parts[data-film] .parts__stage {
		grid-template-rows: 1fr auto auto;
		padding-top: 2rem;
	}
	.parts[data-film] .parts__head {
		align-self: center;
	}
}

/* Short handsets: the same layout cannot hold a 56vh phone, and there
   is no zone above it worth centring in — the heading returns to the
   top-anchored layout. */
@media (max-width: 820px) and (max-height: 700px) {
	.parts__shot {
		height: min(49 * var(--vh-unit, 1vh), 132vw);
	}
	.parts__stage {
		padding-top: 4rem;
		padding-bottom: 4rem;
	}
	.parts[data-film] .parts__stage {
		grid-template-rows: auto 1fr auto;
		padding-top: 4rem;
	}
	.parts[data-film] .parts__head {
		align-self: start;
	}
}

@media (prefers-reduced-motion: reduce) {
	.parts__shot {
		transition: none;
	}
}


/* ─────────────────────────────────────────────────────────────
   Start tonight.

   The page's actual download moment. The strip pinned at the foot
   of the window is a shortcut you can take at any point; this is
   the one place the page asks.

   Cream, between the green three-parts section and the dark
   photograph below, so the run of sections keeps alternating. The
   badges are the vendors' own artwork at a size you can read —
   46px rather than the strip's 30 — and they are the one thing on
   this page that must not be restyled: both vendors require the
   marks unmodified, so the only liberties taken are clear space
   and a lift on hover.
   ───────────────────────────────────────────────────────────── */
.get {
	/* One screen, like every other section on this page — they all measure
	   900px in a 900px window, and this one was 558px, so it read as a strip
	   between two full sections rather than as a stop of its own. Centred
	   rather than padded to height, so it stays composed on a short window. */
	min-height: var(--vh-stable, 100svh);
	display: grid;
	align-content: center;
	padding: clamp(4rem, 10vh, 7rem) clamp(1.25rem, 5vw, 3rem);
	/* A grey a step down from the page cream, mixed from the site's own ink so
	   it stays in the same family rather than reading as a neutral grey
	   dropped in from outside. */
	background: color-mix(in srgb, var(--ink) 8%, var(--bg));
}

.get__inner {
	width: min(640px, 100%);
	margin: 0 auto;
	display: grid;
	justify-items: center;
	text-align: center;
}

.get__icon {
	width: 76px;
	height: 76px;
	border-radius: 18px;
	margin-bottom: clamp(1.4rem, 3vh, 2rem);
	box-shadow: 0 18px 34px -22px rgba(11, 22, 16, 0.45);
}

.get__head {
	font-family: var(--serif);
	font-weight: 400;
	font-size: clamp(2rem, 4.2vw, 3.2rem);
	line-height: 1.06;
	letter-spacing: -0.02em;
	margin: 0 0 clamp(2rem, 5vh, 3rem);
	color: var(--ink, #2d4a3e);
}

.get__stores {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: clamp(0.8rem, 2vw, 1.4rem);
}

/* Clear space, since neither asset carries its own: both vendors ask for a
   margin of at least a tenth of the badge height. */
.get__store {
	display: inline-flex;
	padding: 6px 8px;
	text-decoration: none;
	transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

.get__store::after { display: none; }

.get__store img { display: block; height: 46px; width: auto; }

.get__store:hover { transform: translateY(-3px); }

@media (max-width: 560px) {
	.get__store img { height: 40px; }
}

@media (prefers-reduced-motion: reduce) {
	.get__store { transition: none; }
}


/* (Superseded by the .more grid below — kept only so the class does no harm
   if it reappears.)

   The way on, from the end of the page.

   Five quiet links under the badges rather than a footer of columns: by this
   point the page has said its piece, and anyone still reading wants either the
   app or one more thing to read. Set small, in a row, separated by their own
   spacing — no rules, no headings, nothing that reads as site furniture.

   They sit far enough under the badges that they never compete with them: the
   badges are the ask, these are the alternative to leaving. */
.get__links {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: clamp(1rem, 2.4vw, 2.2rem);
	margin-top: clamp(2.5rem, 6vh, 4rem);
}

.get__links a {
	font-family: var(--serif);
	font-size: clamp(0.95rem, 1.1vw, 1.05rem);
	letter-spacing: 0.01em;
	text-decoration: none;
	color: rgba(45, 74, 62, 0.62);
	padding-bottom: 2px;
	border-bottom: 1px solid transparent;
	transition: color 0.35s ease, border-color 0.35s ease;
}

.get__links a::after { display: none; }

.get__links a:hover {
	color: var(--ink, #2d4a3e);
	border-bottom-color: color-mix(in srgb, var(--ink) 30%, transparent);
}

/* The last one is the one that is not a page about us. */
.get__links a:last-child {
	color: var(--amber, #cc8f6b);
}

.get__links a:last-child:hover {
	border-bottom-color: color-mix(in srgb, var(--amber) 45%, transparent);
}


/* ─────────────────────────────────────────────────────────────
   Learn more about us, and why we did this.

   Built out of `.card` and `.programs__grid` rather than a private
   set of styles, so these are literally the same object as the
   three programs above: same border, same 200px hero with its
   gradient, same meta label, same 800ms lift with the photograph
   easing to 1.04. Two card designs on one page would be two
   vocabularies for one idea.

   Only the section's own padding is defined here.
   ───────────────────────────────────────────────────────────── */
/* The section mirrors `.programs` property for property — flex column, centred
   both ways, the same padding and the same white ground. It was `display: grid`
   with no horizontal padding, which left the 960px grid flush against the left
   edge with a card's width of dead space on the right: the grid was never
   centred, and that is also why the row did not read like the programs row. */
.more {
	min-height: 100svh;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	padding: var(--space-4xl) var(--space-xl);
	background: var(--white);
	position: relative;
}

.more .programs__header h2 {
	max-width: 26ch;
	margin-inline: auto;
}

/* Left-aligned, like the programs cards. Centring the body was tried while
   these still had their own styling; once they became the same card as the
   programs grid it was the one thing left making them read as different. */

/* 3. Room for the download strip.

   The strip is fixed to the foot of the window — 58px on the desktop, 66px on
   a phone, plus its offset — so it sat over the footer's last row: the links
   on the desktop, the badges on a phone. The footer now reserves that height
   plus clear space, so the whole of it can be read with the strip in place. */
.footer {
	padding-bottom: calc(var(--space-xl) + 92px + env(safe-area-inset-bottom)) !important;
}

@media (max-width: 700px) {
	.footer {
		padding-bottom: calc(var(--space-xl) + 108px + env(safe-area-inset-bottom)) !important;
	}
}

@media (prefers-reduced-motion: reduce) {
	.more__card { transition: none; }
}


/* The breakpoints `.programs` has, which `.more` was missing.

   They are written against the `.programs` selector rather than the grid, so
   reusing `.programs__grid` inherited the column counts but not the section's
   padding — at 768px and below the cards here sat in 40px of padding where the
   programs cards sit in 24px, which made them measurably narrower than the
   identical cards further up the page. */
@media (max-width: 768px) {
	.more {
		min-height: 100svh;
		padding: var(--space-3xl) var(--space-lg);
	}

	.more .programs__header {
		margin-bottom: var(--space-xl);
	}
}

/* ─────────────────────────────────────────────────────────────
   FAQ — direction A from the canvas: a quiet accordion on cream.
   Native details/summary carries the interaction, so the section
   ships no JS and the CSP stays as written. */
.faq {
	background: var(--bg);
	/* The house section shape — a full screen, content centered —
	   same as .cta and .home-stat. */
	min-height: 100svh;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--space-4xl) var(--space-lg);
}

.faq__inner {
	width: min(720px, 100%);
}

.faq h2 {
	font-family: var(--serif);
	font-weight: 400;
	font-size: clamp(2rem, 4.2vw, 3rem);
	letter-spacing: -0.02em;
	text-align: center;
	color: var(--ink);
	margin: 0 0 clamp(2.2rem, 5vh, 3.2rem);
}

.faq__item {
	border-top: 1px solid rgba(45, 74, 62, 0.16);
}

.faq__item:last-of-type {
	border-bottom: 1px solid rgba(45, 74, 62, 0.16);
}

.faq__q {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 24px;
	list-style: none;
	cursor: pointer;
	padding: 1.45rem 0.25rem;
	font-family: var(--serif);
	font-size: clamp(1.15rem, 2.1vw, 1.3rem);
	font-weight: 500;
	color: var(--ink);
}

.faq__q::-webkit-details-marker {
	display: none;
}

/* The plus, drawn: two bars; the vertical one folds away open. */
.faq__icon {
	position: relative;
	flex: 0 0 16px;
	width: 16px;
	height: 16px;
}

.faq__icon::before,
.faq__icon::after {
	content: '';
	position: absolute;
	inset: 0;
	margin: auto;
	background: var(--ink);
	transition: transform 0.25s ease;
}

.faq__icon::before { width: 14px; height: 1.5px; }
.faq__icon::after { width: 1.5px; height: 14px; }

.faq__item[open] .faq__icon::after {
	transform: scaleY(0);
}

/* Multi-paragraph answers: every inner paragraph hands back most of
   its bottom padding so the answer reads as one block. */
.faq__a:has(+ .faq__a) {
	padding-bottom: 0.55rem;
}

.faq__a {
	font-family: var(--serif);
	font-size: 1rem;
	line-height: 1.65;
	color: color-mix(in srgb, var(--ink) 68%, transparent);
	/* Padding, not margin: a bottom margin collapses out of the
	   details and the answer lands flush on the rule. */
	margin: 0;
	padding: 0 0.25rem 1.45rem;
}

/* Open, the question hands most of its bottom padding to the answer
   so the pair reads as one unit. */
.faq__item[open] .faq__q {
	padding-bottom: 0.55rem;
}

/* The open is a glide, not a snap: the details content interpolates
   its height where the engine can (Chromium; Safari falls back to
   the fade alone) and the answer arrives through a soft rise. */
.faq__item {
	interpolate-size: allow-keywords;
}

.faq__item::details-content {
	block-size: 0;
	overflow-y: clip;
	opacity: 0;
	transform: translateY(-4px);
	transition: block-size 0.35s cubic-bezier(0.23, 1, 0.32, 1),
		content-visibility 0.35s allow-discrete,
		opacity 0.3s ease,
		transform 0.3s ease;
}

.faq__item[open]::details-content {
	block-size: auto;
	opacity: 1;
	transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
	.faq__item::details-content {
		transition: none;
	}
}

/* The launch line reads as one breath on desktop; a handset splits
   it at the comma instead of wherever the column happens to end. */
.fineprint-break {
	display: none;
}

@media (max-width: 700px) {
	.fineprint-break {
		display: inline;
	}
}

.hero__stores .signup__btn.hero__stores .signup__btn@media (max-width: 700px) {
	/* One line, always: the strip takes what the phone gives, the
	   field flexes, the button never wraps. */
	.hero__stores {
		max-width: calc(100vw - 16px);
	}
				.hero__stores .signup__btn	}


