/* Navarro POOL: see DESIGN.md (tokens, layout, the opening scene). Light only, one accent, sentence case. */

/* Plus Jakarta Sans, self-hosted (DESIGN.md → Typography; OFL): one variable file per subset covers 400–700.
   latin includes every Spanish accent, ñ, ¿ and ¡; latin-ext is only fetched if a page needs it. */
@font-face {
  font-family: "Plus Jakarta Sans"; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url(/assets/fonts/plus-jakarta-sans-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Plus Jakarta Sans"; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url(/assets/fonts/plus-jakarta-sans-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --font: "Plus Jakarta Sans", system-ui, sans-serif;
  --bg: #FFFFFF;
  --mist: #F2F7F9;
  --mist-2: #E6EFF3;
  --border: #E2EAEE;
  --border-strong: #CFDBE1;
  --ink: #0F1E26;
  --ink-2: #536570;
  --accent: #0A6A93;
  --accent-hover: #085678;
  --accent-tint: #E6F2F7;
  --water: #29B6D9;
  --star: #E3A008;
  --ph: linear-gradient(160deg, #F2F7F9 0%, #E6EFF3 55%, #DDF1F7 100%);

  --max: 1200px;
  --gutter: clamp(20px, 5vw, 56px);
  --flow: clamp(88px, 12vw, 168px);
  --bar-h: 72px;
  --quote-bar: 0px; /* the phone quote bar's height (it shows below 900px), incl. the iPhone home-indicator area */
  --edge: calc(var(--gutter) + max(0px, (100vw - var(--max)) / 2)); /* from the screen edge to the content column */
  --ease: cubic-bezier(.2, .7, .2, 1);
}
@media (max-width: 899px) { :root { --bar-h: 64px; --quote-bar: calc(76px + env(safe-area-inset-bottom)); } }

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
main section[id] { scroll-margin-top: var(--bar-h); } /* jumps (nav, EN/ES switch) land below the fixed top bar */
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 400 17px/1.7 var(--font);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, h2, h3 { margin: 0; text-wrap: balance; letter-spacing: -0.025em; }
p { margin: 0; text-wrap: pretty; }
a { color: var(--accent); }
img, video, svg { display: block; }
.wrap { max-width: var(--max); margin: 0 auto; padding: 0 var(--gutter); }
.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; }
.skip { position: absolute; left: 12px; top: -60px; z-index: 50; background: var(--ink); color: #fff; padding: 10px 16px; border-radius: 999px; font-weight: 600; text-decoration: none; }
.skip:focus { top: 12px; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 52px; padding: 0 24px; border-radius: 999px; border: 0;
  font: 600 16px/1 var(--font); text-decoration: none; white-space: nowrap; cursor: pointer;
  transition: background-color .2s, border-color .2s, color .2s;
}
.btn svg { width: 20px; height: 20px; flex: none; }
.btn-primary { background: var(--accent); color: #fff; box-shadow: 0 10px 24px -12px rgba(10, 106, 147, .7); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-line { background: #fff; color: var(--ink); border: 1px solid var(--border-strong); }
.btn-line:hover { border-color: var(--ink-2); }
.btn-sm { min-height: 44px; padding: 0 18px; font-size: 15px; }

/* ---------- top bar ---------- */
.bar { position: fixed; inset: 0 0 auto; z-index: 30; transition: background-color .3s, box-shadow .3s; }
.bar.is-scrolled { background: rgba(255, 255, 255, .88); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); box-shadow: 0 1px 0 var(--border); }
.bar-in { height: var(--bar-h); display: flex; align-items: center; gap: 24px; }
/* his own logo, straight from the truck decal (DESIGN.md → Logo) */
.brand { display: inline-flex; flex: none; border-radius: 10px; }
.brand img { display: block; height: 52px; width: auto; border-radius: 10px; }
@media (max-width: 899px) { .brand img { height: 48px; } }
.nav { display: none; gap: 28px; margin-left: auto; font-size: 15px; font-weight: 500; }
.nav a { color: var(--ink-2); text-decoration: none; transition: color .2s; }
.nav a:hover { color: var(--ink); }
.bar-actions { display: flex; align-items: center; gap: 12px; margin-left: auto; }
.bar .btn-primary { display: none; }
@media (min-width: 900px) { .bar .btn-primary { display: inline-flex; } }
@media (min-width: 1100px) { .nav { display: flex; } .bar-actions { margin-left: 8px; } }

/* EN | ES: both labels always visible; the current one is a white chip */
.lang { display: inline-flex; padding: 3px; border-radius: 999px; background: var(--mist); border: 1px solid var(--border); }
.lang a {
  display: inline-grid; place-items: center; min-width: 44px; min-height: 44px; padding: 0 12px; border-radius: 999px;
  font: 600 13px/1 var(--font); color: var(--ink-2); text-decoration: none; transition: color .2s, background-color .2s;
}
.lang a:hover { color: var(--ink); }
.lang a[aria-current="true"] { background: #fff; color: var(--ink); box-shadow: 0 1px 2px rgba(15, 30, 38, .12); }

/* ---------- 1. opening scene ---------- */
.clip { position: relative; }
.clip img, .clip video { width: 100%; height: 100%; object-fit: cover; }
.clip video { position: absolute; inset: 0; opacity: 0; transition: opacity .35s; } /* only created with motion */
.clip.has-clip video { opacity: 1; }
.clip-note { margin: 0; text-align: center; font: 500 13px/1.4 var(--font); color: var(--ink-2); }
.pill { align-self: flex-start; display: inline-block; font: 600 14px/1.3 var(--font); color: var(--accent); background: var(--accent-tint); padding: 8px 14px; border-radius: 999px; }
.h1 { font-size: clamp(40px, 4.8vw, 64px); line-height: 1.03; font-weight: 700; letter-spacing: -0.035em; }
.h1 em, .h2 em { font-style: normal; color: var(--accent); }
.h2 { font-size: clamp(32px, 3.8vw, 50px); line-height: 1.06; font-weight: 700; }
.lead { font-size: clamp(17px, 1.4vw, 19px); color: var(--ink-2); max-width: 46ch; }
.ctas { display: flex; flex-wrap: wrap; gap: 12px; }
.facts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px; font-size: 15px; color: var(--ink-2); }
/* ★ 4.8 · 72 reviews, wherever it appears (hero facts, reviews close, contact rows) */
.rating { display: inline-flex; align-items: center; gap: 6px; }
.rating svg, .contact-rows .star { width: 16px; height: 16px; color: var(--star); }
:is(.facts, .reviews-close, .contact-rows) b { color: var(--ink); font-weight: 600; }
.clip picture { display: contents; }
.clip source { display: none; }
.review { margin: 0; font-size: clamp(18px, 1.6vw, 21px); font-weight: 500; line-height: 1.4; max-width: 32ch; }
.review footer { margin-top: 8px; font-size: 14px; font-weight: 500; color: var(--ink-2); }
.beat { display: flex; flex-direction: column; gap: 20px; }
/* the hero's buttons + facts (user, 2026-10-14): with motion one fixed row under the beats for the whole scene, so
   "Request a quote" never moves or fades while the beats change above it; without motion it sits under the hero text */
.scene-cta { display: flex; flex-direction: column; gap: 20px; margin-top: -36px; } /* 20px under the hero, not the 56px between beats */

/* static / reduced-motion / no-JS: a before-after pair, then the four beats as normal blocks */
.scene { position: relative; padding: calc(var(--bar-h) + 32px) 0 0; }
.stage { position: relative; display: grid; gap: 40px; }
.clip { width: min(calc(100% - 2 * var(--gutter)), 760px); margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.clip img { aspect-ratio: 3 / 4; height: auto; border-radius: 28px; background: var(--ph); }
html.motion .clip img { height: 100%; border-radius: 0; }
html.motion .clip .after { display: none; }
/* until the clip paints (slow download), or without it (Save-Data, failed load): the real after photo fades in over
   the before photo from beat 2; once the clip paints it takes over at the right frame. Hidden (so not downloaded)
   until then; main.js starts loading it early when the clip is late. */
html.motion .clip:not(.has-clip).show-after .after { display: block; position: absolute; inset: 0; animation: after-in .8s var(--ease) both; }
@keyframes after-in { from { opacity: 0; } to { opacity: 1; } }
html.motion .clip-note {
  position: absolute; z-index: 2; right: 20px; bottom: 16px; max-width: 340px; text-align: left; font-size: 12px;
  background: rgba(255, 255, 255, .88); padding: 6px 12px; border-radius: 14px;
}
.beat-box { display: grid; gap: 56px; }
.when-motion { display: none; }
/* the recreation note only on the last, clean beat, so the real before photo isn't doubted (user, 2026-10-15) */
html.motion .clip.at-end:not(.no-clip) ~ .clip-note .when-motion { display: inline; }
/* the promise on the first screen (user, 2026-10-15): a small real "after" photo of the same pool, until the clip itself
   clears (beat 2); motion only, since without motion the before/after pair already shows it */
.clip-after { display: none; }
html.motion .clip-after {
  display: block; position: absolute; z-index: 2; margin: 0; right: 20px; bottom: 64px; width: clamp(108px, 11vw, 168px);
  border: 3px solid #fff; border-radius: 18px; overflow: hidden; transition: opacity .5s var(--ease), transform .5s var(--ease);
}
html.motion .clip-after img { width: 100%; height: auto; aspect-ratio: 3 / 4; object-fit: cover; }
html.motion .clip-after figcaption {
  position: absolute; left: 8px; bottom: 8px; padding: 5px 9px; border-radius: 999px;
  background: rgba(255, 255, 255, .9); font: 600 12px/1 var(--font); color: var(--ink);
}
html.motion .clip.show-after ~ .clip-after { opacity: 0; transform: translateY(8px); }
.rail { display: none; }

/* motion: one pinned 100svh stage, scroll scrubs the clip, beats crossfade */
html.motion .scene { height: 250vh; padding: 0; } /* 1.5 screens of scroll (user, 2026-10-15: was 420vh, too long) */
html.motion :is(.stage, .reel-pin) { position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden; } /* the pinned screens */
html.motion .stage { display: block; }
html.motion .clip {
  position: absolute; top: var(--bar-h); right: 0; bottom: 0; width: auto; aspect-ratio: 3 / 4; max-width: 60vw; margin: 0;
  display: block; background: var(--ph); overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 22%), linear-gradient(to top, transparent 0, #000 14%), linear-gradient(to bottom, transparent 0, #000 5%);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(to right, transparent 0, #000 22%), linear-gradient(to top, transparent 0, #000 14%), linear-gradient(to bottom, transparent 0, #000 5%);
  mask-composite: intersect;
}
html.motion .beats { position: absolute; inset: var(--bar-h) 0 0; display: flex; align-items: center; }
html.motion .beats > .wrap { position: relative; width: 100%; }
html.motion .beat-box {
  position: relative; display: grid; grid-template-rows: minmax(0, 1fr) auto; row-gap: 28px;
  width: min(520px, 44vw); height: min(500px, calc(100svh - var(--bar-h) - 40px));
}
/* Beat change (user, 2026-10-14): the new beat fades in where the old one was (no slide). The old one's headline
   letters sink one after another with a small splash where they go under (main.js → splash); its other text
   drops and fades with them. While a beat is on screen its headline letters ride a slow wave. */
/* the beats share the top row and sit right above the fixed buttons (row 2) */
html.motion .beat { grid-area: 1 / 1; align-self: end; opacity: 0; }
html.motion .scene-cta { grid-area: 2 / 1; margin: 0; }
html.motion .beat .ctas { display: none; } /* the last beat's own button: the fixed row already has it */
/* only the incoming beat fades (after the old one has sunk); a beat that stops leaving is already gone */
html.motion .beat.on { opacity: 1; transition: opacity .45s var(--ease) .85s; }
html.motion .beat.leaving { opacity: 1; transition: none; }
html.motion .beat.leaving :is(.pill, .lead, .ctas, .facts, .review) {
  opacity: 0; transform: translateY(18px); transition: opacity .3s ease-in, transform .3s ease-in;
}

/* headline letters (split by main.js; the heading keeps its full text as aria-label). --x is the letter's horizontal
   position (0–1), so the wave runs left to right and letters stacked in a column ride it together. The sink and the
   splashes are in main.js (clip-path waterline + canvas). */
.w { display: inline-block; white-space: nowrap; }
.l { display: inline-block; }
html.motion :is(.beat.on, .beat.leaving) .l { will-change: transform; } /* only the letters that move */
html.motion :where(.beat.on, .flood-text) .l { animation: wave 2.4s ease-in-out calc(var(--x, 0) * -1.7s) infinite; } /* :where keeps the pause rules stronger */
html.motion .scene.away .beat.on .l { animation-name: none; will-change: auto; } /* off screen (main.js adds .away): stop */
@keyframes wave {
  0%, 100% { transform: translateY(0) rotate(0); }
  25% { transform: translateY(-0.13em) rotate(-3deg); }
  50% { transform: translateY(0) rotate(0); }
  75% { transform: translateY(0.08em) rotate(2.5deg); }
}
.splash-layer { position: absolute; pointer-events: none; z-index: 3; }
html.motion .rail { display: block; position: absolute; left: calc(var(--gutter) - 30px); top: 50%; width: 2px; height: 220px; margin-top: -110px; background: var(--border); border-radius: 2px; }
html.motion .rail i { position: absolute; inset: 0 0 auto; height: calc(var(--p, 0) * 100%); background: var(--accent); border-radius: 2px; }

/* phones and tall portrait screens: clip on top fading into white, text below it on white */
@media (max-width: 899px) {
  .beat .ctas, .scene-cta .ctas { display: none; } /* the quote bar at the bottom is the fixed call to action */
  /* phones: a calm crossfade between beats instead of the sink and splash (user, 2026-10-15) */
  html.motion .beat { transition: opacity .25s ease-in; }
  html.motion .beat.on { transition: opacity .4s var(--ease) .2s; }
}
@media (max-width: 899px), (max-aspect-ratio: 4/5) {
  .h1 { font-size: 31px; }
  .h2 { font-size: 29px; }
  .lead { font-size: 16px; line-height: 1.55; }
  .review { font-size: 18px; }
  .beat { gap: 12px; }
  .scene-cta { margin-top: -44px; }
  html.motion .beat-box { row-gap: 12px; }
  html.motion .stage { --clip-h: 57svh; }
  html.motion .clip {
    top: var(--bar-h); left: 0; right: 0; bottom: auto; width: 100%; height: var(--clip-h); max-width: none; aspect-ratio: auto;
    -webkit-mask-image: linear-gradient(to top, transparent 0, #000 40%); mask-image: linear-gradient(to top, transparent 0, #000 40%);
  }
  html.motion .clip-note { top: calc(var(--bar-h) + 10px); bottom: auto; right: 10px; max-width: calc(100% - 20px); font-size: 11px; }
  html.motion .clip-after { top: calc(var(--bar-h) + 62px); bottom: auto; right: 10px; width: 92px; border-radius: 14px; }
  html.motion .clip-after figcaption { left: 6px; bottom: 6px; font-size: 11px; padding: 4px 8px; }
  html.motion .beats { inset: calc(var(--bar-h) + var(--clip-h) - 28px) 0 calc(var(--quote-bar) + 12px); align-items: stretch; }
  html.motion .beat-box { width: 100%; height: 100%; }
  html.motion .beat { align-self: start; }
  html.motion .rail { display: none; }
}
/* short phones: a bit more room for the text, and the pinned facts keep to one line (hours are in the footer and quote) */
@media (max-width: 899px) and (max-height: 700px) {
  html.motion .stage { --clip-h: 45svh; }
  html.motion .scene-cta .facts > span:not(.rating) { display: none; }
}
/* landscape phones, big ones too (reviewer, stage 5): too short for the clip on top or the desktop text, so the clip
   sits on the right and compact beat text on the left, above the quote bar (or the buttons from 900px); the pill and
   the hours step aside */
@media (orientation: landscape) and (max-height: 500px) {
  html.motion .scene :is(.h1, .h2) { font-size: 24px; }
  html.motion .scene .lead { font-size: 14px; line-height: 1.45; }
  html.motion .scene .review { font-size: 15px; }
  html.motion .scene .pill { display: none; }
  html.motion .clip {
    top: var(--bar-h); left: auto; right: 0; bottom: 0; width: 44vw; height: auto; aspect-ratio: auto; max-width: none;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 22%), linear-gradient(to top, transparent 0, #000 18%);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(to right, transparent 0, #000 22%), linear-gradient(to top, transparent 0, #000 18%);
    mask-composite: intersect;
  }
  html.motion .clip-note { top: auto; bottom: 88px; right: 10px; max-width: 40vw; }
  html.motion .clip-after { top: calc(var(--bar-h) + 10px); bottom: auto; right: 10px; width: 72px; border-radius: 12px; }
  html.motion .beats { inset: var(--bar-h) 0 var(--quote-bar); align-items: stretch; }
  html.motion .beat-box { width: calc(56vw - var(--gutter) - 16px); height: 100%; padding: 10px 0 12px; row-gap: 10px; }
  html.motion .beat { align-self: end; gap: 8px; }
  html.motion .scene-cta .facts > span:not(.rating) { display: none; }
  html.motion .rail { display: none; }
}
@media (orientation: landscape) and (max-height: 340px) {
  html.motion .scene :is(.h1, .h2) { font-size: 22px; } /* the smallest landscape phones (568×320) */
  html.motion .scene .lead { font-size: 13px; line-height: 1.4; }
  html.motion .beat { gap: 6px; }
}
@media (orientation: landscape) and (max-height: 500px) and (min-width: 900px) {
  html.motion .scene-cta { gap: 10px; } /* buttons + rating in the short side column */
}
/* very short portrait phones (320×568): the location pill steps aside so the hero fits (the lead names Miami-Dade) */
@media (max-width: 899px) and (orientation: portrait) and (max-height: 600px) {
  html.motion .scene .pill { display: none; }
}
@media (min-width: 600px) and (max-aspect-ratio: 4/5) {
  html.motion .stage { --clip-h: 62svh; }
  html.motion .beat { align-self: center; }
}

/* ---------- chapters ---------- */
.chapter { padding-top: var(--flow); }
.chapter .lead { margin-top: 20px; }
.label { font: 600 15px/1 var(--font); color: var(--accent); margin-bottom: 14px; }
.h3 { font-size: clamp(20px, 2vw, 24px); font-weight: 700; line-height: 1.25; letter-spacing: -0.015em; }

/* ---------- 2. your weekly visit: sticky heading beside a timeline whose line fills as you read ---------- */
.weekly-grid { display: grid; gap: 48px; }

/* scenery: a photo area whose edges fade into the page; .media-inner is 16% taller so parallax never shows an edge */
.scenery { position: relative; overflow: hidden; background: var(--ph); }
.media-inner { position: absolute; inset: -8% 0; }
.media-inner img { width: 100%; height: 100%; object-fit: cover; }
/* his finished pool (p14): desktop runs off the left edge of the screen and fades to the right and bottom;
   phones run edge to edge and fade at the bottom */
.weekly-scene {
  margin: 32px calc(-1 * var(--gutter)) 0; height: 58vw; max-height: 420px;
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent); mask-image: linear-gradient(to bottom, #000 62%, transparent);
}
.weekly-scene img { object-position: 72% 55%; }
@media (min-width: 900px) {
  .weekly-scene {
    margin: 36px 0 0 calc(-1 * var(--edge));
    height: clamp(220px, 44vh, 440px); max-height: none;
    -webkit-mask-image: linear-gradient(to right, #000 68%, transparent), linear-gradient(to bottom, #000 70%, transparent), linear-gradient(to top, #000 88%, transparent);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(to right, #000 68%, transparent), linear-gradient(to bottom, #000 70%, transparent), linear-gradient(to top, #000 88%, transparent);
    mask-composite: intersect;
  }
  .weekly-grid { grid-template-columns: 5fr 7fr; gap: clamp(40px, 7vw, 104px); align-items: start; }
  .weekly-intro { position: sticky; top: calc(var(--bar-h) + 48px); }
}
.timeline { --progress: 1; position: relative; list-style: none; margin: 0; padding: 6px 0 0; }
html.motion .timeline { --progress: 0; }
.timeline::before, .timeline::after {
  content: ""; position: absolute; left: 19px; top: 30px; bottom: 30px; width: 2px; border-radius: 2px;
}
.timeline::before { background: var(--border); }
.timeline::after { background: var(--accent); transform-origin: top; transform: scaleY(var(--progress)); }
.step { position: relative; padding: 0 0 clamp(40px, 5vw, 60px) 72px; }
.step:last-child { padding-bottom: 0; }
.step-n {
  position: absolute; left: 0; top: 0; z-index: 1; display: grid; place-items: center;
  width: 40px; height: 40px; border-radius: 999px; background: var(--bg);
  border: 2px solid var(--accent); color: var(--accent); font: 700 16px/1 var(--font);
  transition: background-color .3s, color .3s, border-color .3s;
}
html.motion .step:not(.done) .step-n { border-color: var(--border-strong); color: var(--ink-2); }
.step.done .step-n { background: var(--accent); color: #fff; }
.step .h3 { padding-top: 6px; }
.step p { margin-top: 6px; color: var(--ink-2); max-width: 46ch; }
.weekly-close { margin-top: clamp(40px, 5vw, 56px); padding-top: 24px; border-top: 1px solid var(--border); font-weight: 500; max-width: 52ch; }
.weekly-close a { font-weight: 600; text-underline-offset: 3px; }
@media (max-width: 899px) {
  .step { padding-left: 60px; }
  .timeline::before, .timeline::after { left: 17px; }
  .step-n { width: 36px; height: 36px; font-size: 15px; }
}

/* ---------- 3. repairs & equipment: text steps scroll beside one pinned photo (his installs) that crossfades.
   Phones, reduced motion and no-JS: each step has its own photo fading into white above its text. ---------- */
.scenes { display: grid; margin-top: clamp(16px, 3vw, 40px); }
.scenes-text { padding: 0 var(--gutter); max-width: var(--max); margin: 0 auto; width: 100%; }
.scene-step { padding: 40px 0 16px; }
.scene-step h3 { font-size: clamp(26px, 2.8vw, 34px); font-weight: 700; line-height: 1.15; letter-spacing: -0.02em; }
.scene-step > p { margin-top: 12px; color: var(--ink-2); max-width: 46ch; }
.scene-step .review { margin-top: 22px; font-size: clamp(17px, 1.4vw, 19px); max-width: 36ch; }
.scene-mobile {
  height: 62vw; max-height: 440px; margin: 0 calc(-1 * var(--gutter)) 20px;
  -webkit-mask-image: linear-gradient(to top, transparent 0, #000 32%); mask-image: linear-gradient(to top, transparent 0, #000 32%);
}
.scene-mobile img { object-position: 50% 62%; }
.scenes-media { display: none; }
.repairs-more { padding-top: clamp(32px, 4vw, 48px); max-width: 46ch; }
/* hairline lists (.rows: plain items; .hair-rows: label left, value right) */
:is(.rows, .hair-rows) { list-style: none; margin: 0; padding: 0; }
:is(.rows, .hair-rows) li { padding: 14px 0; border-top: 1px solid var(--border); font-weight: 500; }
:is(.rows, .hair-rows) li:last-child { border-bottom: 1px solid var(--border); }
.more-link { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; margin-top: 18px; font-weight: 600; text-decoration: none; }
.more-link svg { width: 18px; height: 18px; transition: transform .2s; }
.more-link:hover svg { transform: translateX(4px); }
@media (min-width: 900px) {
  .scene-mobile {
    height: 380px; max-height: none; margin: 0 0 24px; max-width: 560px;
    -webkit-mask-image: linear-gradient(to top, transparent 0, #000 30%), linear-gradient(to left, transparent 0, #000 22%), linear-gradient(to bottom, transparent 0, #000 10%);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(to top, transparent 0, #000 30%), linear-gradient(to left, transparent 0, #000 22%), linear-gradient(to bottom, transparent 0, #000 10%);
    mask-composite: intersect;
  }
  html.motion .scenes { grid-template-columns: 1fr 1fr; }
  html.motion .scenes-text {
    padding: 0 clamp(32px, 5vw, 72px) 0 var(--edge);
    max-width: none; margin: 0;
  }
  html.motion .scene-step { min-height: 80vh; display: flex; flex-direction: column; justify-content: center; padding: 48px 0; }
  html.motion .scene-mobile { display: none; }
  html.motion .scenes-media { display: block; position: sticky; top: var(--bar-h); height: calc(100vh - var(--bar-h)); }
  .scene-img {
    position: absolute; inset: 0; opacity: 0; transition: opacity .9s var(--ease);
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 24%), linear-gradient(to bottom, transparent 0, #000 7%), linear-gradient(to top, transparent 0, #000 10%);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(to right, transparent 0, #000 24%), linear-gradient(to bottom, transparent 0, #000 7%), linear-gradient(to top, transparent 0, #000 10%);
    mask-composite: intersect;
  }
  .scene-img.is-active { opacity: 1; }
  .scene-img img { object-position: 50% 60%; }
}

/* ---------- 4. pools we keep clear (user, 2026-10-14): with motion the section pins, the heading sits large in the
   middle of the screen, and six of his pools slide from right to left in front of it, spaced wide and staggered,
   covering it as they pass. Reduced motion / no JS: heading above a swipe strip that snaps. ---------- */
.reel-title { text-align: center; padding: 0 var(--gutter); }
.reel-h { font-size: clamp(40px, 7.2vw, 120px); line-height: 1; letter-spacing: -0.04em; }
.reel-sticky { margin-top: clamp(28px, 4vw, 48px); overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.reel-sticky::-webkit-scrollbar { display: none; }
.reel-sticky:focus-visible { outline-offset: -3px; }
.reel-track { display: flex; gap: 12px; list-style: none; margin: 0; padding: 0 var(--gutter); width: max-content; }
.reel-item { position: relative; flex: 0 0 auto; width: min(78vw, 420px); aspect-ratio: 3 / 4; margin: 0; border-radius: 28px; overflow: hidden; background: var(--ph); scroll-snap-align: center; }
.reel-item img { width: 100%; height: 100%; object-fit: cover; }
.reel-count { position: absolute; left: 14px; bottom: 14px; font: 600 12px/1 var(--font); color: var(--ink); background: rgba(255, 255, 255, .88); padding: 7px 11px; border-radius: 999px; }

html.motion .reel { height: calc(100vh + var(--scroll-len, 0px)); }
html.motion .reel-title {
  position: absolute; inset: var(--bar-h) 0 0; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
}
html.motion .reel-sticky {
  position: absolute; inset: var(--bar-h) 0 0; z-index: 2; margin: 0; overflow: visible; scroll-snap-type: none; pointer-events: none;
}
html.motion .reel-track {
  position: absolute; left: 0; top: 0; height: 100%; align-items: center; padding: 0; gap: 18vw;
  transform: translate3d(var(--reel-x, 100vw), 0, 0); will-change: transform;
}
html.motion .reel-item { width: auto; height: 60vh; height: 60svh; border-radius: 24px; }
html.motion .reel-item:nth-child(odd) { top: -9vh; }
html.motion .reel-item:nth-child(even) { top: 10vh; }
html.motion .reel-count { left: 18px; bottom: 18px; font-size: 13px; }
@media (max-width: 899px) {
  html.motion .reel-track { gap: 30vw; }
  html.motion .reel-item { height: 46svh; border-radius: 22px; }
  html.motion .reel-item:nth-child(odd) { top: -13svh; }
  html.motion .reel-item:nth-child(even) { top: 13svh; }
}

/* ---------- 4b. the flood (user, 2026-10-14): after the reel stops with the last photo centred, scrolling raises water
   from the bottom until it fills the screen, with "And more." in the middle; then the pin releases and the water runs
   on into a band that fades to the white page (the next section). The water: a moving wave surface (two layers) and
   pool-floor light (caustics.png from research/tools/caustics.py, two tiles drifting against each other so the web of
   light shifts like a real pool), bright pool blue, never dark. The light fades out with depth, so the body's bottom
   colour meets the tail with no seam. --flood (0–1) and --flood-text (0–1) come from main.js. ---------- */
.flood { display: none; }
html.motion .flood {
  display: block; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  transform: translate3d(0, calc((1 - var(--flood, 0)) * (100% + 70px)), 0); will-change: transform;
}
.flood-body {
  position: absolute; inset: 0; overflow: hidden;
  background: linear-gradient(to bottom, #4CC7E6 0%, #1A94C2 36%, #1585B4 100%); /* ≥3.5:1 behind "And more." */
}
/* the light layer holds the depth fade still while the two tiles drift underneath it */
.flood-light {
  position: absolute; inset: 0; overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, .7) 45%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, .7) 45%, transparent 100%);
}
.flood-light::before, .flood-light::after { content: ""; position: absolute; top: 0; left: 0; }
/* the pool-light texture is only fetched once the reel is two screens away (main.js adds .near) */
.reel.near .flood-light::before, .reel.near .flood-light::after { background: url(/assets/img/caustics.png) repeat; }
/* each layer is one tile larger than the screen and drifts exactly one tile per loop, so the loop is seamless */
.flood-light::before {
  width: calc(100% + 460px); height: calc(100% + 460px); background-size: 460px; opacity: .42;
  animation: drift-a 26s linear infinite;
}
.flood-light::after {
  width: calc(100% + 720px); height: calc(100% + 720px); background-size: 720px; opacity: .26;
  animation: drift-b 38s linear infinite;
}
/* the water only moves while it's on screen (main.js adds .wet once it starts rising); off screen the loops stop
   outright, which also frees their layers, and restart unseen as the water comes up */
.reel-pin:not(.wet) .flood-wave, .reel-pin:not(.wet) .flood-light::before, .reel-pin:not(.wet) .flood-light::after,
.reel-pin:not(.wet) .flood-text .l { animation-name: none; }
@keyframes drift-a { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-460px, -460px, 0); } }
@keyframes drift-b { from { transform: translate3d(-720px, 0, 0); } to { transform: translate3d(0, -720px, 0); } }
.flood-wave { position: absolute; left: 0; width: 200%; height: 70px; bottom: 100%; margin-bottom: -1px; }
.flood-wave path { fill: #4CC7E6; }
.flood-wave.back { height: 64px; opacity: .55; animation: swell 9s linear infinite reverse; }
.flood-wave.back path { fill: #8ADDF0; }
.flood-wave.front { animation: swell 6s linear infinite; }
@keyframes swell { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.flood-text { display: none; }
html.motion .flood-text {
  display: flex; position: absolute; inset: var(--bar-h) 0 0; z-index: 4; align-items: center; justify-content: center;
  margin: 0; color: #fff; text-align: center; pointer-events: none;
  font-size: clamp(48px, 8vw, 132px); line-height: 1; font-weight: 700; letter-spacing: -0.04em;
  opacity: var(--flood-text, 0); transform: translateY(calc((1 - var(--flood-text, 0)) * 40px));
  text-shadow: 0 2px 24px rgba(10, 70, 100, .35), 0 1px 3px rgba(10, 60, 90, .3);
}
/* after the pin: the water runs on and fades into the white page. Reduced motion / no JS: a still water band with
   "And more." that fades to white on both sides. */
.flood-tail {
  display: flex; align-items: center; justify-content: center; min-height: 60vh; margin-top: clamp(40px, 6vw, 80px);
  background: linear-gradient(to bottom, #fff 0%, #3FBCE0 30%, #1E9ECB 55%, #fff 100%);
}
.flood-tail-text { margin: 0; color: #fff; font-size: clamp(44px, 7vw, 110px); line-height: 1; font-weight: 700; letter-spacing: -0.04em; }
/* the tail continues the risen water only once main.js runs; before that the static band (with its words) shows */
html.motion.js-ready .flood-tail { min-height: 70vh; margin-top: 0; background: linear-gradient(to bottom, #1585B4 0%, #3FBCE0 34%, #A8E2F2 68%, #fff 100%); }
html.motion.js-ready .flood-tail-text { display: none; }

/* ---------- 5. reviews: big word-for-word quotes down the page, alternating sides, no boxes. With motion each quote
   floats: it drifts side to side as it rises, like something floating up through water (main.js sets translate/rotate,
   which stack with the reveal's transform), and its words fill from soft grey (#748791, 3.7:1) to ink as it reaches the
   middle of the screen, at reading pace. Reduced motion / no JS: still, ink from the start. ---------- */
html.motion .reviews { padding-top: calc(var(--flow) * .5); overflow-x: clip; } /* the water band above already gives the pause; clip keeps the drifting quotes from widening the page */
.quotes { display: grid; gap: clamp(64px, 8vw, 120px); margin-top: clamp(56px, 7vw, 104px); }
.quote { margin: 0; width: min(100%, 760px); }
.quote:nth-child(even) { margin-left: auto; }
.stars { display: flex; gap: 3px; margin-bottom: 18px; color: var(--star); }
.stars svg { width: 102px; height: 18px; } /* the #i-stars sprite: five 18px stars, 3px apart */
.quote blockquote { margin: 0; }
.quote p {
  margin: 0; font-size: clamp(22px, 2.4vw, 32px); line-height: 1.28; font-weight: 500; letter-spacing: -0.015em;
  text-indent: -0.42em; text-wrap: pretty; /* the opening quote mark hangs, so the words line up with the stars */
}
.quote figcaption { margin-top: 18px; font-size: 15px; font-weight: 500; color: var(--ink-2); }
html.motion .quote p.fill .qw { color: #748791; transition: color .35s var(--ease); }
html.motion .quote p.fill .qw.on { color: var(--ink); }
.reviews-close {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 24px;
  margin: clamp(72px, 9vw, 128px) 0 0; padding-top: 14px; border-top: 1px solid var(--border); font-size: 15px; color: var(--ink-2);
}
.reviews-close .more-link { margin-top: 0; }
@media (max-width: 899px) {
  .quote:nth-child(even) { width: auto; margin-left: 12%; }
}
/* "Translated from …" note under a quote: its own line on phones instead of breaking mid-phrase */
@media (max-width: 599px) {
  .tr { display: block; }
  .tr .sep { display: none; }
}

/* ---------- 6. meet Frank: his pool at night (p12, user's pick; no photo of Frank yet) runs off the left edge and fades
   into the page beside his story; light parallax. With motion the pool lights come up as the photo rises into view (a
   dark layer that fades with scroll; only once JS runs, so it never stays dim). Credential with a seal, facts as
   hairline rows. ---------- */
.frank-grid { display: grid; gap: 36px; }
.frank-scene {
  margin: 0 calc(-1 * var(--gutter)); height: 74vw; max-height: 460px;
  -webkit-mask-image: linear-gradient(to bottom, #000 66%, transparent); mask-image: linear-gradient(to bottom, #000 66%, transparent);
}
.frank-scene img { object-position: 30% 60%; } /* keeps the spa and its waterfall in frame */
html.motion.js-ready .frank-scene::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; background: #06202C;
  opacity: calc((1 - var(--lights, 0)) * .58); /* main.js: 0 as the photo enters, 1 once its top is 25% from the top */
}
.frank-body { margin: 20px 0 0; max-width: 44ch; }
.credential { display: flex; align-items: center; gap: 10px; margin: 28px 0 0; font-weight: 600; }
.credential svg { flex: none; width: 24px; height: 24px; color: var(--accent); }
/* facts as hairline rows: label left (ink-2), value right (shared with the contact rows in 7) */
.hair-rows li { display: flex; justify-content: space-between; align-items: center; gap: 16px; min-height: 52px; padding: 4px 0; text-align: right; }
.hair-rows span { color: var(--ink-2); font-weight: 400; text-align: left; }
.frank-facts { margin-top: 24px; max-width: 420px; }
.frank .more-link { margin-top: 12px; }
@media (min-width: 900px) {
  .frank-grid { grid-template-columns: 7fr 5fr; gap: clamp(40px, 6vw, 96px); align-items: center; }
  .frank-scene {
    margin: 0 0 0 calc(-1 * var(--edge));
    height: clamp(380px, 70vh, 660px); max-height: none;
    -webkit-mask-image: linear-gradient(to right, #000 62%, transparent), linear-gradient(to bottom, #000 76%, transparent), linear-gradient(to top, #000 86%, transparent);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(to right, #000 62%, transparent), linear-gradient(to bottom, #000 76%, transparent), linear-gradient(to top, #000 86%, transparent);
    mask-composite: intersect;
  }
}

/* ---------- 7. request a quote: two steps write a WhatsApp message, shown as a chat bubble before it opens
   (wa.me link in the page's language). No backend, nothing stored. The need chips are a native radio group (arrow
   keys work) styled as pills. Contact details on hairline rows beside it (below it on phones). ---------- */
.request { padding-bottom: var(--flow); }
.request-grid { display: grid; gap: 56px; margin-top: clamp(36px, 5vw, 56px); }
.qb { display: grid; gap: 32px; max-width: 680px; }
.qb-step { min-width: 0; margin: 0; padding: 0; border: 0; }
.qb-q { display: block; float: none; padding: 0; margin: 0 0 14px; font-size: 20px; font-weight: 700; letter-spacing: -0.02em; }
.chips { display: flex; flex-wrap: wrap; gap: 10px; }
.chip {
  position: relative; display: inline-flex; align-items: center; min-height: 44px; padding: 0 18px; border-radius: 999px;
  border: 1px solid var(--border-strong); font: 600 15px/1 var(--font); color: var(--ink); cursor: pointer; user-select: none;
  transition: border-color .2s, background-color .2s, color .2s;
}
.chip:hover { border-color: var(--ink-2); }
.chip input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.chip:has(input:checked) { background: var(--accent); border-color: var(--accent); color: #fff; }
.chip:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 3px; }
#qb-area {
  width: min(100%, 360px); height: 52px; padding: 0 20px; border: 1px solid transparent; border-radius: 16px;
  background: var(--mist); font: 500 16px var(--font); color: var(--ink); transition: background-color .2s, border-color .2s, box-shadow .2s;
}
#qb-area::placeholder { color: var(--ink-2); opacity: 1; }
#qb-area:focus { outline: none; background: #fff; border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-tint); }
.qb-msg { max-width: 520px; padding: 16px 22px 18px; border-radius: 22px 22px 22px 6px; background: var(--mist); font-size: 16px; line-height: 1.5; }
.qb-msg small { display: block; margin-bottom: 6px; font-size: 13px; font-weight: 600; color: var(--ink-2); }
.qb .ctas { margin-top: -8px; }
.qb-small { margin: -18px 0 0; font-size: 14px; color: var(--ink-2); }
.contact-rows a { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; font-weight: 600; text-decoration: none; }
.contact-rows a:hover { text-decoration: underline; text-underline-offset: 3px; }
.contact-rows a svg:not(.star) { width: 16px; height: 16px; }
@media (min-width: 900px) {
  .request-grid { grid-template-columns: minmax(0, 1fr) minmax(300px, 360px); gap: clamp(40px, 5vw, 80px); align-items: start; }
}
.site-foot { border-top: 1px solid var(--border); padding: 32px 0 120px; font-size: 14px; color: var(--ink-2); }
.site-foot a { color: var(--ink-2); }
@media (min-width: 900px) { .site-foot { padding-bottom: 40px; } }

/* ---------- phone quote bar ---------- */
.phonebar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; display: flex; gap: 10px;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  background: rgba(255, 255, 255, .9); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  box-shadow: 0 -1px 0 var(--border);
}
.phonebar .btn-primary { flex: 1; min-height: 52px; }
.phonebar .btn-line { flex: none; width: 52px; min-height: 52px; padding: 0; }
@media (min-width: 900px) { .phonebar { display: none; } }

/* ---------- reveal (later sections) ---------- */
html.motion [data-reveal] { transition: opacity .8s var(--ease), transform .8s var(--ease); transition-delay: calc(var(--d, 0) * 90ms); }
html.motion.js-ready [data-reveal]:not(.in) { opacity: 0; transform: translateY(22px); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition: none !important; }
}
