/* permanentlightingboise.com — landing page + quiz.
   One committed dark theme: night is when the product exists, so the page is
   shot at night. Photography carries the design; type and one warm accent do
   the rest. No frameworks, no external requests, no gradients-as-decoration.

   Design tokens are the whole system. If a colour is not here it is a bug. */

@font-face {
  font-family: "Bricolage";
  src: url("/static/fonts/bricolage-latin.woff2") format("woff2");
  /* 600-800, because that is every weight the page asks for: 800 for h1, 700
     for the other display type, 600 for the testimonial quote. The shipped
     file is the variable font instanced to that range (fontTools
     `varLib.instancer wght=600:800`), 77KB -> 69KB, with the optical-size axis
     left whole for the `font-optical-sizing: auto` on the headings. */
  font-weight: 600 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* The palette, ratified 2026-08-30 (design-director §R; owner's go). The §R
     names are the identity — ground/surface/line, cream, muted, amber. Hex
     here; the same colours restated in oklch in the @supports block below,
     which is the encoding of record. (The two blocks are exact conversions of
     each other, verified 2026-08-30 — this was a naming ratification, not a
     repaint.) */
  --ground: #0a0e16;
  --surface: #111726;
  --line: #232c3f;
  --cream: #f2efe7;
  --muted: #9ba4b5;
  --amber: #f5b95a;
  --amber-hi: #ffcb77;
  --amber-ink: #1a1206;
  /* Working aliases the sheet was written in. The §R names above are
     canonical; these exist so a hundred use sites did not have to churn on
     ratification day. New rules may use either; the colour is the same. */
  --text: var(--cream);
  --accent: var(--amber);
  --accent-hi: var(--amber-hi);
  --accent-ink: var(--amber-ink);
  /* Quiz-surface semantics only (area check, field errors). The landing page
     shows neither hue since 2026-08-30: the one-appearance green (the season
     countdown dot) was retired to amber per §R. */
  --good: #7bd88f;
  --bad: #ff8a80;
  --radius: 14px;        /* surfaces: cards, inputs, photos, option cards */
  --radius-pill: 999px;  /* THE button geometry (§R: one geometry, the pill) */

  /* The six-token type ramp (§R). Every font-size in this file is one of
     these; a seventh size is a bug. Floor: nothing renders below the caption
     token at any viewport. */
  --fs-caption: 0.8rem;
  --fs-small: 0.9rem;
  --fs-body: 1.05rem;
  --fs-h3: 1.3rem;
  --fs-h2: clamp(1.5rem, 5vw, 2.1rem);
  --fs-h1: clamp(2.1rem, 8vw, 3.4rem);

  --display: "Bricolage", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
             Roboto, "Helvetica Neue", Arial, sans-serif;
  --body: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;

  --gap: clamp(38px, 6vw, 72px);
  --pad: clamp(18px, 5vw, 28px);
  --col: 680px; /* the one content column (change 3, 2026-08-30) */
}

/* The ratified encoding (§R). Inside @supports rather than double-declared,
   because custom properties cannot use the invalid-value fallback trick: an
   engine without oklch() would happily store the token string and then fail
   at computed-value time in every var() site, painting nothing. Pre-oklch
   webviews keep the hex block above — the identical colours. */
@supports (color: oklch(82.3% 0.131 77)) {
  :root {
    --ground: oklch(16.4% 0.018 264);
    --surface: oklch(20.7% 0.031 267);
    --line: oklch(29.4% 0.037 265);
    --cream: oklch(95.2% 0.011 90);
    --muted: oklch(71.7% 0.027 263);
    --amber: oklch(82.3% 0.131 77);
    --amber-hi: oklch(87.1% 0.118 78);
    --amber-ink: oklch(18.9% 0.026 78);
  }
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

html {
  background: var(--ground);
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font: var(--fs-body)/1.6 var(--body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

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

/* Content column. Photography goes full-bleed; words never do. */
.wrap {
  width: 100%;
  max-width: var(--col);
  margin: 0 auto;
  padding-left: var(--pad);
  padding-right: var(--pad);
}

h1, h2, h3 {
  font-family: var(--display);
  font-optical-sizing: auto;
  text-wrap: balance;
  letter-spacing: -0.02em;
  margin: 0 0 14px;
}
h1 { font-size: var(--fs-h1); font-weight: 800; line-height: 1.04; }
h2 { font-size: var(--fs-h2); font-weight: 700; line-height: 1.12; }
h3 { font-size: var(--fs-h3); font-weight: 700; line-height: 1.25; margin-bottom: 6px; }

p { margin: 0 0 16px; }
.sub { color: var(--muted); margin-bottom: 24px; }
.note { color: var(--muted); font-size: var(--fs-small); margin: 0; }
.caption { color: var(--muted); font-size: var(--fs-small); margin: 12px 0 0; }
.micro { color: var(--muted); font-size: var(--fs-caption); }
.good { color: var(--good); }

.eyebrow {
  margin: 0 0 12px;
  font-size: var(--fs-caption);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

/* Screen-reader-only, still focusable (the slider's range input). */
.vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

[tabindex="-1"]:focus { outline: none; }

a { color: var(--accent); }
a:focus-visible, button:focus-visible, input:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Motion budget: scene crossfade, slider handle, quiz progress. Nothing else
   moves on its own. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* horizontal photo strip: edge fade as a swipe affordance */
.strip {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 18px, #000 calc(100% - 30px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 18px, #000 calc(100% - 30px), transparent 100%);
}

/* ====================================================================== hero */

.hero {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: flex-end;
  min-height: clamp(460px, 66svh, 620px);
  padding: 56px 0 clamp(22px, 5vw, 40px);
  overflow: hidden;
}
.hero-photo, .hero-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
}
.hero-photo img { object-fit: cover; object-position: 50% 35%; }

/* The photo melts into the page: transparent at the top, ground at the bottom,
   with enough weight behind the copy to hold contrast on any frame.

   The mid stops used to run 0.66 / 0.90 / 0.97 at 42% / 62% / 80%, and on a
   phone the 0.90 stop landed on the roofline — the one thing a cold visitor
   has to understand in three seconds was painted out at 7/255. The dark half
   of the ramp is now pushed down the frame, below the house and onto the
   ground line where it is doing its actual job (melting the photo into the
   page), and the copy carries its own contrast floor via text-shadow rather
   than borrowing it from the scrim. */
.hero-fade {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(10, 14, 22, 0.34) 0%,
    rgba(10, 14, 22, 0.26) 24%,
    rgba(10, 14, 22, 0.42) 52%,
    rgba(10, 14, 22, 0.62) 72%,
    rgba(10, 14, 22, 0.88) 88%,
    var(--ground) 100%
  );
}
/* Type sits on a photograph, so it carries its own floor of contrast rather
   than trusting whichever frame the crop lands on. The microtrust list and the
   scheduling line are in here too: they used to sit on a 0.97 scrim that did
   the work for them, and the lightened ramp hands that job back to the type. */
.hero-body { position: relative; }
/* Ramp note (2026-08-30): the old clamp bottomed at 0.66rem, which put the
   hero eyebrow at ~0.71rem on a 390px phone — below the ratified microcopy
   floor. It sits on the caption token now, like every other eyebrow. */
.hero-body .eyebrow { font-size: var(--fs-caption); letter-spacing: 0.12em; color: var(--accent-hi); font-weight: 700; }
/* The wordmark, above the geo tag (V-53 / J3-04). Quieter than the eyebrow on
   purpose: it identifies, it does not sell. Muted rather than accent so the
   gold in the first viewport stays the button's. */
.hero-brand {
  font-family: var(--display);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--text);
  margin-bottom: 6px;
}
.hero-body .hero-brand,
.hero-body .eyebrow,
.hero-body h1,
.hero-body .lede,
.hero-body .hero-offer,
.hero-body .hero-ask-label,
.hero-body .microtrust,
.hero-body .hero-disclosure,
.hero-body .season { text-shadow: 0 1px 14px rgba(10, 14, 22, 0.85); }

/* 16 CFR 255.5 in the fold, under the hook that quotes a compensated
   homeowner (P3-004). The treatment `.reel-disclosure` established: quiet, but
   quiet is as far as it goes — larger than the page's smallest type, on the
   muted token rather than a dimmed one, in flowing text, never behind a tap.
   No `opacity`, for the same reason that block carries none: a disclosure that
   is dimmed is a disclosure argued about later. It sits over a photograph, so
   it takes the hero's text-shadow like every other line in the fold. The
   negative top margin pulls it against the subhead it belongs to, ahead of the
   season line's own spacing. */
.hero-disclosure {
  margin: -8px 0 16px;
  max-width: 42ch;
  font-size: var(--fs-caption);
  line-height: 1.5;
  color: var(--muted);
}

/* ------------------------------------------------------- the fold's ask
   Question five, in the fold (2026-08-30). One ask, three answers: the label
   is the question and the pills are its options, so the group reads as a
   single choice rather than as three buttons.

   Geometry is the ratified pill (§R: one button geometry) — these extend
   `.btn`, they do not invent a shape. What they do not take is the filled
   amber: a solid gold pill is the page's ONE promise-carrying CTA treatment
   and it still belongs to "Show me my house" on the three surfaces that carry
   that string. An answer is not a promise, so these are outlined in the same
   amber over a dark ground, which is also the `.opt` semantic (choose one)
   in the pill's geometry. */
.hero-ask { margin: 10px 0 0; }
/* Small, because the h1 is the fold's voice and this is the fold's question —
   the ramp's small token, the weight that says "read me", and the amber the
   eyebrow already uses so the ask reads as one object with its pills. */
.hero-ask-label {
  margin: 0 0 8px;
  font-size: var(--fs-small);
  font-weight: 700;
  line-height: 1.35;
  color: var(--text);
}
/* Single column, top to bottom: the one form-shape finding that replicates
   (CXL, n=702, single-column completes 15.4s faster), and at 390px a row of
   three would wrap into ragged pill fragments anyway. */
/* One column, capped to the lede's measure.

   A two-up grid was tried first and photographed: it saves 44px of fold, and it
   buys them by wrapping "Front of the house" and "Front and sides" onto two
   ragged lines each at 390px, which is the width the paid traffic arrives on.
   The saving was real and the ask stopped reading as an ask, so the answer is
   the column. What the column costs is recorded on the offer band in
   index.html: its `section_seen` mark no longer clears the first viewport, so
   fold-exit reports one section rather than two from this deploy on.

   The cap is the second half: the old hero button filled the column at every
   width and one gold pill can carry that, but three translucent ones
   photographed at 1440 as smeared glass panels across most of the screen. A
   choice reads as a choice at a thumb's width. */
.hero-choices {
  display: grid;
  gap: 8px;
  max-width: 26rem;
}
.btn-choice {
  /* 46px rather than the 52px `.btn` sets. Three answers stacked cost three
     times whatever this is, and the fold has the h1, the lede, the countdown,
     the reassurance line and the microtrust list to hold as well; 46 is still
     above the 44px tap-target floor every platform guideline names. */
  min-height: 46px;
  padding: 12px 20px;
  text-align: left;
  line-height: 1.3;
  color: var(--accent-hi);
  /* Amber over its own dark ground, in that order: the tint plus the 2px rim is
     what says "press me" at arm's length on a photograph, and the first draft —
     a hairline rim over a plain dark fill — photographed as three inert
     outlines. The dark layer underneath is not optional: with only the tint,
     the lit roofline behind the middle pill came through it. Still not the
     filled pill, which is the promise's treatment; these are answers. */
  background:
    linear-gradient(rgba(245, 185, 90, 0.16), rgba(245, 185, 90, 0.16)),
    rgba(10, 14, 22, 0.66);
  border: 2px solid var(--accent);
  font-size: var(--fs-small);
  font-weight: 700;
  backdrop-filter: blur(2px);
}
@media (hover: hover) {
  .btn-choice:hover {
    background:
      linear-gradient(rgba(245, 185, 90, 0.3), rgba(245, 185, 90, 0.3)),
      rgba(10, 14, 22, 0.66);
  }
}
.btn-choice:active {
  background:
    linear-gradient(rgba(245, 185, 90, 0.4), rgba(245, 185, 90, 0.4)),
    rgba(10, 14, 22, 0.66);
}

/* What the ask actually buys, in the one place a visitor who never scrolls
   will read it. The offer itself is named at #preview, four screens down. */
.hero-offer {
  margin: 10px 0 0;
  font-size: var(--fs-small);
  line-height: 1.45;
  color: var(--text);
}

.lede { font-size: var(--fs-body); color: var(--text); margin-bottom: 18px; max-width: 42ch; }

.callline { margin: 6px 0 0; font-size: var(--fs-small); color: var(--muted); }
/* The phone number is a conversion action, not decoration: it gets a real
   thumb target even though it sits inside a sentence. */
.callline a {
  display: inline-block;
  padding: 13px 6px;
  color: var(--text);
  text-underline-offset: 3px;
}
.callline a:hover { color: var(--accent); }
.callline-big { font-size: var(--fs-body); margin-top: 10px; }

.microtrust {
  list-style: none;
  display: grid;
  gap: 5px;
  margin: 16px 0 0;
  padding: 10px 0 0;
  border-top: 1px solid var(--line);
  font-size: var(--fs-caption);
  line-height: 1.4;
  color: var(--muted);
}
/* Stacked with a leading marker, not an inline row with separators between the
   items. The row worked while these were two-word spec tags; CRO item 9 made
   each one a phrase, every phrase took its own line at 390px, and the trailing
   separator was then stranded at the end of a line with nothing after it. A
   marker that leads its own item cannot be orphaned by a wrap. */
.microtrust li { position: relative; padding-left: 14px; }
.microtrust li::before {
  content: "·";
  position: absolute;
  left: 3px;
  color: var(--accent);
  font-weight: 700;
}

/* Above the button now, not under the microtrust rule: at 0.9rem below a
   horizontal rule the only honest urgency on the page read as fine print
   (CRO item 13). Bottom margin instead of top, since it leads into the CTA. */
.season {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 16px;
  font-size: var(--fs-small);
  color: var(--text);
}
/* Amber since 2026-08-30 (§R): the dot was the page's only green — a
   one-appearance hue on a surface whose identity is ink, cream and amber.
   Urgency is what the pulse says; the colour now says it in the house accent. */
.season-dot {
  flex: none;
  margin-top: 7px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--amber);
  box-shadow: 0 0 10px rgba(245, 185, 90, 0.9);
  animation: pulse 2.6s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* =================================================================== bands */

.band { padding: var(--gap) 0; }
.band-final { padding-bottom: clamp(40px, 8vw, 72px); }

/* §1b — the off-season offer, directly under the hero. Prominent by position
   and by a single hairline of accent, not by shouting: it is the one band on
   the page carrying a number, and a number in a coloured box reads as an ad
   inside the ad. The rule above and below sets it off from the photography on
   either side without adding a surface. */
.band-offer {
  padding-top: clamp(26px, 5vw, 40px);
  padding-bottom: clamp(26px, 5vw, 40px);
  border-top: 1px solid rgba(245, 185, 90, 0.28);
  border-bottom: 1px solid rgba(245, 185, 90, 0.28);
  background: rgba(245, 185, 90, 0.05);
}
.band-offer h2 { margin-bottom: 10px; }
/* The band's button came off on 2026-08-28 (one ask per viewport), so the
   sub-head now runs into the deadline line directly and takes the button's
   old bottom margin with it. */
.band-offer .sub { margin-bottom: 14px; }

/* §1c — the proof strip, hoisted to the cliff (2026-08-30).
   It sits directly under the offer band and has to read as the answer to it,
   not as the start of the next chapter: tight padding top and bottom, no
   heading, no rule of its own. The grid is photo-left / words-right at every
   width, because the pairing IS the proof — her face and her sentence in one
   glance, at the depth a visitor who is about to leave has actually reached.
   Deliberately quieter than `.reel-quote`, which is the display-type version
   of the same words 4,800px down: this one supports the offer above it and
   must not out-shout the $500. */
.band-proof {
  padding-top: clamp(22px, 4vw, 30px);
  padding-bottom: clamp(22px, 4vw, 30px);
}
.proof {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 16px;
  row-gap: 6px;
  max-width: 46ch;
}
/* The frame is 720x1280 of TWO people at a doorway — Diane in the left third,
   the canvasser in the right. `object-fit: cover` cannot reach that: at any box
   wider than 9:16 it crops vertically and shows the full width, so every
   object-position lands his half-face on the edge of a 124px box, which reads
   as a mistake rather than as context. The window is therefore taken by hand:
   the box clips, and the photograph inside it is oversized and offset onto her.
   Numbers, so they can be re-derived rather than nudged — at 175% width the
   image is 217x386 in a 124x155 box, and the translate puts the visible window
   at roughly x 43-455 / y 152-666 of the source, which is her, her doorway, and
   none of him. */
.proof-shot {
  grid-row: 1 / 3;
  display: block;
  position: relative;
  overflow: hidden;
  width: clamp(100px, 27vw, 124px);
  aspect-ratio: 4 / 5;
  border-radius: var(--radius);
  background: var(--surface);
}
.proof-photo {
  position: absolute;
  top: 0;
  left: 0;
  width: 175%;
  /* The page-wide `img { max-width: 100% }` would clamp that 175% straight back
     to the box and hand back the uncropped frame — silently, since the result
     is a perfectly reasonable-looking photograph of the wrong thing. */
  max-width: none;
  height: auto;
  transform: translate(-6%, -12%);
}
.proof blockquote {
  grid-column: 2;
  align-self: end;
  margin: 0;
  font-size: var(--fs-body);
  line-height: 1.45;
  font-weight: 600;
  text-wrap: pretty;
}
.proof blockquote::before { content: "\201C"; }
.proof blockquote::after { content: "\201D"; }
.proof figcaption {
  grid-column: 2;
  align-self: start;
  color: var(--muted);
  font-size: var(--fs-small);
}
/* 16 CFR 255.5, on the same terms as `.reel-disclosure`: the muted token
   rather than a dimmed one, above the page's smallest type, in flowing text,
   never behind a tap. It follows the attribution with nothing in between. */
.proof-disclosure {
  margin: 14px 0 0;
  max-width: 46ch;
  color: var(--muted);
  font-size: var(--fs-caption);
  line-height: 1.5;
}

/* §2 — native scroll-snap strip, no JS, no lightbox. */
.strip {
  list-style: none;
  display: flex;
  gap: 12px;
  margin: 28px 0 0;
  padding: 0 var(--pad);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.strip::-webkit-scrollbar { display: none; }
.strip li {
  flex: 0 0 60vw;
  max-width: 380px;
  scroll-snap-align: center;
}
.strip img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--surface);
}

/* §3 — the one section that breaks the column: full-bleed stage, chips on it. */
.scenes { padding: var(--gap) 0; }
.stage {
  position: relative;
  margin: 28px auto 20px;
  width: min(100%, calc(46vh * 1.6));
  aspect-ratio: 16 / 10;
  max-height: 46vh;
  overflow: hidden;
  background: var(--surface);
  isolation: isolate;
}
.stage-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 200ms ease;
}
.stage-img.is-on { opacity: 1; }
.stage-fade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(10, 14, 22, 0) 40%, rgba(10, 14, 22, 0.88) 88%, var(--ground) 100%);
}
.stage-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  padding: 14px var(--pad) 0;
}
.stage-chips::-webkit-scrollbar { display: none; }
.stage-chips .chip { flex: 0 0 auto; }

/* The caption changes with the frame, and the frame changes on a timer, so the
   box has to be tall enough for the longest line at the narrowest width or the
   whole section below it walks up and down every few seconds. Two lines at
   390px is the measured worst case. */
.scene-caption {
  max-width: 36ch;
  margin: 14px auto 20px;
  padding: 0 var(--pad);
  min-height: 2.9em;
  text-align: center;
  font-size: var(--fs-small);
  line-height: 1.45;
  color: var(--text);
}

/* §4 — numbered steps, no cards. */
.steps { list-style: none; margin: 0; padding: 0; counter-reset: step; }
.steps li {
  counter-increment: step;
  position: relative;
  padding: 0 0 24px 52px;
}
.steps li::before {
  content: counter(step);
  position: absolute;
  left: 0; top: 0;
  width: 36px; height: 36px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--accent);
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--fs-small);
}
.steps li:last-child { padding-bottom: 0; }
.steps p { color: var(--muted); margin: 0; }

.panel {
  margin: 32px 0 0;
  padding: 18px 20px;
  border-left: 3px solid var(--accent);
  background: var(--surface);
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--text);
  font-size: var(--fs-body);
}

/* §5 — before/after. `--pos` is set by app.js; CSS owns the resting state. */
.slider {
  position: relative;
  margin: 26px 0 0;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  touch-action: pan-y;
  cursor: ew-resize;
  user-select: none;
  -webkit-user-select: none;
}
.slider-base { width: 100%; }
.slider-top {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  clip-path: inset(0 calc(100% - var(--pos, 50%)) 0 0);
}
.slider-handle {
  position: absolute;
  top: 0; bottom: 0;
  left: var(--pos, 50%);
  width: 2px;
  margin-left: -1px;
  background: rgba(242, 239, 231, 0.85);
  pointer-events: none;
}
.slider-handle .grip {
  position: absolute;
  top: 50%; left: 50%;
  width: 44px; height: 44px;
  margin: -22px 0 0 -22px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
}
.slider-handle .grip::before,
.slider-handle .grip::after {
  content: "";
  position: absolute;
  top: 50%;
  width: 0; height: 0;
  margin-top: -5px;
  border: 5px solid transparent;
}
.slider-handle .grip::before { left: 8px; border-right-color: var(--accent-ink); }
.slider-handle .grip::after { right: 8px; border-left-color: var(--accent-ink); }
.slider-tag {
  position: absolute;
  top: 10px;
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  background: rgba(10, 14, 22, 0.72);
  color: var(--text);
  font-size: var(--fs-caption);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.slider-tag-l { left: 10px; }
.slider-tag-r { right: 10px; }

#preview .btn-accent { margin-top: 28px; }

/* §6 — the same eave, twice: off in daylight, then lit. */
.day-photo {
  width: 100%;
  margin-top: 24px;
  aspect-ratio: 5 / 4;
  object-fit: cover;
  object-position: 50% 42%;
  border-radius: var(--radius);
  background: var(--surface);
}
.turn {
  margin: 30px 0 14px;
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--fs-h3);
  letter-spacing: -0.02em;
}
.turn-photo {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--surface);
}

/* §7 — the only card grid on the page. */
.cards {
  list-style: none;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin: 26px 0 0;
  padding: 0;
}
.cards li {
  padding: 18px 16px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
/* Card titles stay a rung below h3 on purpose: five accent-coloured lines in
   one grid at 1.3rem would shout. Body-token display type, like .opt strong. */
.cards h3 { color: var(--accent); font-size: var(--fs-body); }
.cards p { margin: 0; color: var(--muted); font-size: var(--fs-small); line-height: 1.5; }

/* §8 — testimonial video. A vertical phone clip, so the card is sized the way
   §3's stage is: viewport height decides the width, never the other way round,
   which is what keeps a 9:16 frame off the bottom of a short landscape screen
   and stops it towering over the 680px column on a desktop. The poster is the
   frame; the accent disc is the only affordance. */
.reel { margin-top: 26px; }
.reel-frame {
  position: relative;
  width: min(100%, calc(58vh * 9 / 16), 300px);
  margin: 0 auto;
  aspect-ratio: 9 / 16;
  max-height: 58vh;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
/* Three layers in one frame, bottom to top: the lazy poster image, the video,
   the tap disc. The video is transparent until it has frames, so the poster
   below shows through at rest and the real clip paints over it on play — which
   is what lets the markup drop `poster=` and let the frame load lazily like
   every other image this far down the page. `position` on the video is what
   puts it above the absolutely-positioned image; without it a positioned
   element would paint over a static one whatever the source order says. */
.reel-poster {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.reel-video {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: transparent;
}

/* Two people talking, so two colours: the homeowner in the accent, the man
   asking the questions in body text. The names come from the WebVTT voice tags
   (`<v Diane>`), which both engines expose to `::cue(v[voice=...])`. */
::cue(v[voice="Diane"]) { color: var(--accent); }
::cue(v[voice="Jordan"]) { color: var(--text); }

/* Covers the frame, so the whole poster is the tap target. Hidden until app.js
   wires it: with no JS the native controls underneath stay reachable. */
.reel-play {
  position: absolute;
  inset: 0;
  z-index: 2; /* above the video, which had to be raised over the poster image */
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: linear-gradient(180deg, rgba(10, 14, 22, 0) 45%, rgba(10, 14, 22, 0.55) 100%);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.reel-glyph {
  display: grid;
  place-items: center;
  width: 64px; height: 64px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
  transition: background 150ms ease;
}
.reel-glyph::after {
  content: "";
  margin-left: 5px; /* optical centre: a triangle's mass sits left of its box */
  width: 0; height: 0;
  border: 11px solid transparent;
  border-left-color: var(--accent-ink);
  border-right-width: 0;
}
.reel-play:hover .reel-glyph { background: var(--accent-hi); }
/* Muted autoplay in progress (`.is-muted-running`, set by app.js): the whole
   frame is still one button, but its promise is sound, not playback — a play
   triangle over a clip that is already moving reads as a bug (owner report,
   2026-08-30). The triangle yields to a quiet pill naming the actual offer. */
.reel-sound-tag {
  display: none;
  position: absolute;
  left: 14px;
  bottom: 14px;
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  background: rgba(10, 14, 22, 0.72);
  color: var(--cream);
  font-size: var(--fs-caption);
  font-weight: 600;
}
.reel.is-muted-running .reel-glyph { display: none; }
.reel.is-muted-running .reel-sound-tag { display: inline-block; }
.reel.is-muted-running .reel-play { background: none; } /* the scrim exists to
   hold the triangle's contrast; behind one small pill it just dims her */
/* The clip failed to load. Keep the poster, stop offering a tap that cannot
   work, and say nothing the visitor has to interpret. */
.reel-play[disabled] { cursor: default; }
.reel-play[disabled] .reel-glyph { opacity: 0.3; box-shadow: none; }

.reel-quote {
  margin: 20px auto 0;
  max-width: 34ch;
  text-align: center;
}
.reel-quote blockquote {
  margin: 0;
  font-family: var(--display);
  font-weight: 600;
  font-size: var(--fs-h3);
  line-height: 1.35;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.reel-quote blockquote::before { content: "\201C"; }
.reel-quote blockquote::after { content: "\201D"; }
.reel-quote figcaption {
  margin-top: 8px;
  color: var(--muted);
  font-size: var(--fs-small);
}
/* 16 CFR 255.5: the material connection is disclosed near the endorsement.
   It closes the section rather than sitting against the clip, and it is quiet
   — but quiet is as far as it goes. Larger than the smallest type the page
   already ships (.eyebrow, .progress-label), on the muted token rather than a
   dimmed one, in flowing text, never behind a tap. */
.reel-disclosure {
  margin: 22px auto 0;
  max-width: 44ch;
  text-align: center;
  color: var(--muted);
  font-size: var(--fs-caption);
  line-height: 1.5;
}

/* §9 — FAQ. */
.faq { border-bottom: 1px solid var(--line); }
.faq:first-of-type { border-top: 1px solid var(--line); }
.faq summary {
  list-style: none;
  cursor: pointer;
  padding: 18px 34px 18px 0;
  position: relative;
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--fs-body);
  letter-spacing: -0.01em;
  min-height: 48px;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "";
  position: absolute;
  right: 6px; top: 26px;
  width: 9px; height: 9px;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: rotate(45deg);
  transform-origin: center;
}
.faq[open] summary::after { transform: rotate(-135deg); top: 30px; }
.faq p { margin: 0 0 20px; color: var(--muted); }

/* ================================================================== footer */

.foot {
  padding: clamp(36px, 7vw, 60px) 0 calc(96px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-caption);
}
.foot p { margin: 0 0 10px; overflow-wrap: anywhere; }
.foot-phone {
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--fs-h3);
  letter-spacing: -0.02em;
}
.foot-phone a {
  display: inline-block;
  padding: 10px 2px;
  color: var(--text);
  text-decoration: none;
}
.foot-phone a:hover { color: var(--accent); }
.foot-links { margin-bottom: 2px; }
.foot-links a { display: inline-block; padding: 12px 2px; }
/* V-54 / J2-29 (partial), 2026-08-28. This paragraph is the page's material
   disclosure — who designs, who installs, and that the preview and the visit
   are free with no obligation — and it was rendered as the dimmest text on the
   page at #9aa3b5, which is what a footer does with a line it hopes nobody
   reads. A disclosure the FTC would call clear and conspicuous cannot be the
   quietest thing on the surface it appears on, so it is the muted body colour
   at body-copy size now: still a footer, no longer fine print.

   The other half of the finding — a business mailing address and a contact
   phone in the footer — is not here: neither exists to publish yet, and both
   are the owner's to supply. */
.foot-fine { font-size: var(--fs-small); line-height: 1.6; color: var(--muted); }

.foot a, .consent a, .legal a, .callline a {
  color: var(--muted);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: rgba(242, 239, 231, 0.24);
  text-underline-offset: 3px;
  border-radius: 4px;
}
.consent a, .callline a { color: var(--text); }
.foot a:hover, .consent a:hover, .legal a:hover, .callline a:hover {
  color: var(--accent);
  text-decoration-color: currentColor;
}

/* ============================================================= sticky bar */

.stickybar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 40;
  display: flex;
  gap: 10px;
  align-items: stretch;
  min-height: 56px;
  padding: 10px var(--pad) calc(10px + env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--line);
}
/* V-29 / J1-20 / J2-28, 2026-08-28. The bar is opaque and fixed, so anything
   the document ends underneath it is content no amount of scrolling reveals —
   at full scroll the footer's fine print (the "we are not the installer"
   disclosure, of all lines) and the FTC endorsement disclosure sat behind it on
   both captured viewports.

   The bar's own height, given back to the page as room to end in: 56px of
   minimum bar plus its 10px top padding, plus whatever the device's home
   indicator takes. `.landing` rather than `body`, because the quiz hides the
   bar (`body.is-quiz .stickybar`) and a screen that never has a bar over it
   should not carry a bar's worth of dead space.

   What this does NOT claim to fix is the bar passing over mid-page content
   while a visitor scrolls: a fixed element covers what it is over, that is what
   fixed means, and the audit's half-viewport frames are slices of a scroll
   rather than places a reader stops. The permanent occlusion is the end of the
   document, and the end of the document is what moves. */
.landing { padding-bottom: calc(66px + env(safe-area-inset-bottom)); }

.sticky-cta { flex: 1 1 auto; min-height: 52px; font-size: var(--fs-body); }
.sticky-call {
  flex: 0 0 52px;
  width: 52px;
  min-height: 52px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--ground);
  color: var(--accent);
  text-decoration: none;
}
.sticky-call:hover { border-color: var(--accent); color: var(--accent-hi); }
body.is-quiz .stickybar { display: none; }

/* ============================================================== quiz nudge */
/* Question one as a bottom sheet (experiment quiz-nudge). Above the sticky bar
   in the stack because it replaces it while it is up, and it slides rather than
   appears so it reads as a sheet the visitor can push back down. */

.nudge {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 45;
  max-width: 560px;
  margin: 0 auto;
  padding: 18px var(--pad) calc(18px + env(safe-area-inset-bottom));
  background: var(--surface);
  border: 1px solid var(--line);
  border-bottom: 0;
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -20px 44px rgba(0, 0, 0, 0.5);
  transform: translateY(100%);
  transition: transform 280ms cubic-bezier(0.2, 0.9, 0.3, 1);
}
.nudge.is-on { transform: none; }
.nudge-title { font-family: var(--display); font-size: var(--fs-h3); font-weight: 700; }
.nudge-sub {
  margin-top: 4px;
  color: var(--muted);
  font-size: var(--fs-small);
  line-height: 1.45;
}
.nudge-options { display: grid; gap: 10px; margin-top: 14px; }
/* V-58 / J1-24, 2026-08-28: the sheet's button is the page's primary CTA, in
   the page's primary treatment. It was `.opt` — an outlined ghost button on a
   dark panel, the quietest ask in the funnel on the one surface that interrupts
   a reader to make it. The `.opt` rule stays for any list-shaped option the
   sheet grows back. */
.nudge-options .opt { min-height: 52px; padding: 14px 16px; text-align: center; }
.nudge-cta { width: 100%; min-height: 52px; font-size: var(--fs-body); }
.nudge-close {
  position: absolute;
  top: 10px; right: 10px;
  width: 40px; height: 40px;
  display: grid;
  place-items: center;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 50%;
  font: inherit;
  font-size: var(--fs-h3); /* the × glyph — icon, nearest rung */
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
}
.nudge-close:hover, .nudge-close:focus-visible { color: var(--text); }
body.is-quiz .nudge { display: none; }
/* The sheet takes the sticky bar's place while it is up, rather than sitting
   on top of it — two bars at the bottom of a phone is one bar too many. */
body.is-nudged .stickybar { display: none; }

/* ==================================================================== quiz */

.quiz { display: block; }
.quiz-inner {
  max-width: 560px;
  margin: 0 auto;
  padding: 22px var(--pad) calc(48px + env(safe-area-inset-bottom));
}

.screen { display: none; }
/* The 2026-08-28 visual audit carried a residual note that this container is
   declared `transition: all` and should name its properties. It is not: the
   entrance is the `screen-in` animation below, which already animates exactly
   two properties, and nothing in this file declares a transition on `.screen`.
   The note is a computed-style reading — `getComputedStyle(el).transition`
   returns "all 0s ease 0s" on an element with no transition declared at all.
   Left as-is on purpose; do not add a no-op `transition: opacity` to satisfy
   a note whose premise the source does not carry. */
.screen.is-active { display: block; animation: screen-in 200ms ease both; }
@keyframes screen-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
.quiz h2 { margin-bottom: 10px; }

.progress { margin: 0 0 26px; }
.progress-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  width: 15%;
  border-radius: var(--radius-pill);
  background: var(--accent);
  transition: width 200ms ease;
}
.progress-label {
  margin: 8px 0 0;
  font-size: var(--fs-caption);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

.options { display: grid; gap: 12px; }
.options-grid { grid-template-columns: 1fr 1fr; }

.opt {
  display: block;
  width: 100%;
  min-height: 56px;
  padding: 16px;
  text-align: left;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: border-color 150ms ease, background 150ms ease;
}
/* V-39 / J1-14 / J3-15, 2026-08-28. `:focus-visible` used to take the accent
   border, which is the same gold outline `.opt-cta` wears to mark the
   recommended path — so the keyboard ring was indistinguishable from a picked
   answer, one option per enrichment screen rendered as pre-chosen, and on the
   needs_review exit BOTH cards carried a gold outline at once so neither read
   as the recommendation. Focus keeps the offset outline the base rules give it
   (it is 2px, gold, and outside the box, so it can never be confused with a
   border); selection keeps the border. `:hover` goes behind `(hover: hover)`
   for the reason the chips did: a touch browser leaves it stuck on the last
   thing tapped. */
@media (hover: hover) {
  .opt:hover { border-color: var(--accent); }
}
.opt:active { background: rgba(245, 185, 90, 0.12); border-color: var(--accent); }
.opt-wide { grid-column: 1 / -1; }
.opt-cta { border-color: var(--accent); background: rgba(245, 185, 90, 0.09); }
.opt strong { display: block; font-size: var(--fs-body); }
.opt span {
  display: block;
  margin-top: 5px;
  font-weight: 400;
  font-size: var(--fs-small);
  line-height: 1.45;
  color: var(--muted);
}

/* The terms line under the address field on screen one. Quiet, in the register
   the operators' survey found (design/OPERATOR-CAPTURE-PATTERNS-2026-08-27.md
   section 2.2): a plain paragraph, no panel, no border, no icon. What it does
   NOT copy is the industry's 10px type and its inherit-coloured links, which
   are the two properties the industry's own standard forbids and the FTC has
   already litigated. It inherits `.hint`'s size, which is the same size as the
   field labels around it. */
.hint-quiet a { color: var(--text); text-decoration: underline; }

/* The service-area answer, on the same screen as the address that produced it.
   Neutral by default, because "we will check this one" is not bad news and must
   not be dressed as an error; `is-good` is the confirmed-in-area state. */
.area-verdict {
  margin: 0 0 16px;
  padding: 12px 14px;
  border-radius: 10px;
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.45;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
}
.area-verdict.is-good {
  color: var(--good);
  background: rgba(123, 216, 143, 0.10);
  border-color: rgba(123, 216, 143, 0.32);
}

.points { margin: 0 0 4px; padding-left: 20px; color: var(--muted); }
.points li { margin-bottom: 8px; }

.check {
  width: 56px; height: 56px;
  margin: 0 0 18px;
  display: grid;
  place-items: center;
  font-size: var(--fs-h2); /* the ✓ glyph — icon, nearest rung */
  color: var(--accent-ink);
  background: var(--good);
  border-radius: 50%;
}

/* =================================================================== forms */

/* J1-01: the line on question one that tells the progress counter above it what
   it means. It answers the label, so it is weighted like the sub it follows
   rather than like fine print, and it takes the sub's bottom margin so the two
   read as one block above the field. */
.q1-reassure {
  margin: -14px 0 24px;
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--muted);
}

.field { display: block; margin: 0 0 16px; border: 0; padding: 0; }
.field > span, .field legend {
  display: block;
  padding: 0;
  margin: 0 0 6px;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--text);
}
.field em { font-style: normal; font-weight: 400; color: var(--muted); }

/* Why a field is asked, and what happens to the answer (CRO items 67, 75). It
   sits outside the label and is wired up with `aria-describedby`, so the
   field's accessible name stays "Home address" rather than growing a sentence.
   The field it belongs to gives up its own bottom margin so the two read as one
   block instead of two. */
.field:has(+ .hint) { margin-bottom: 6px; }
.hint {
  margin: 0 0 16px;
  font-size: var(--fs-caption);
  line-height: 1.45;
  color: var(--muted);
}

input[type="text"], input[type="tel"], input[type="email"] {
  width: 100%;
  min-height: 52px;
  padding: 14px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font: inherit;
  /* The body token is 1.05rem = 16.8px. It must never drop below 16px: a
     smaller input font is an iOS zoom-on-focus, which is why this was a
     literal 16px before the ramp. */
  font-size: var(--fs-body);
  transition: border-color 150ms ease;
}
input::placeholder { color: #6c7590; }
input[type="text"]:hover, input[type="tel"]:hover, input[type="email"]:hover {
  border-color: #33405a;
}
input[aria-invalid="true"] { border-color: var(--bad); }

.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  flex: 1 1 auto;
  min-height: 48px;
  padding: 12px 16px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  font: inherit;
  font-weight: 600;
  font-size: var(--fs-small);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: border-color 150ms ease, background 150ms ease, color 150ms ease;
}
.chip[aria-pressed="true"] {
  background: var(--text);
  border-color: var(--text);
  color: var(--ground);
}
/* V-38 / J1-10 / J3-09, 2026-08-28 — and this is the root cause, not the
   symptom the judges described. Selected is cream, everywhere, for every chip
   group. What the appointment form showed instead was "Weekend" cream and
   "Evening" gold on the same screen, so the last chip tapped read as a second
   submit button sitting beside the real one.
   The gold was never a second selected state: it is `:hover` on a selected
   chip, and a touch browser leaves `:hover` stuck on the last thing tapped
   until something else is. So the "two treatments" were one treatment plus a
   pointer style being applied to a device with no pointer.
   Two changes, because `(hover: hover)` alone was not enough. Behind the query,
   a phone stops inventing a pointer state at all. And the gold fill on a
   SELECTED chip is deleted outright rather than merely gated, because a mouse
   is no more entitled to a second selected treatment than a finger is: gold is
   the primary CTA's colour and a chip that is already picked has nothing left
   to say by turning into one. The chip's own re-verification frame is a desktop
   Chromium, where `(hover: hover)` is true — so a gate hidden behind the query
   would have gone on rendering the defect in every capture the judges read.
   What survives is the border cue on an UNSELECTED chip, which is feedback
   about what a click would do and cannot be mistaken for what has been done. */
@media (hover: hover) {
  .chip:not([aria-pressed="true"]):hover { border-color: var(--accent); }
}

/* ----------------------------------------------- the two-day slot offer
   One heading per day, the openings under it, and the way out under both.
   The chips are the same chips as everywhere else on purpose: a slot is a
   choice of the same weight as "Weekend", and a second treatment for it would
   read as a second kind of control on a form that already has a submit.

   `flex: 0 0 auto` is the one difference. A time reads as a time — "5 PM" —
   and the stretched `flex: 1 1 auto` of the preference chips would blow four
   short labels out to full width, so a list of four openings would look like
   four buttons rather than one row of times. */
.slot-offer .hint { margin-top: -2px; }
.slot-day + .slot-day { margin-top: 16px; }
.slot-day-name {
  margin: 0 0 8px;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--muted);
}
.slot-day .chips .chip { flex: 0 0 auto; min-width: 88px; text-align: center; }
.slot-escape {
  display: block;
  margin-top: 14px;
  padding: 0;
  text-align: left;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.consent {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  margin: 20px 0 16px;
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--text);
  cursor: pointer;
}
.consent:has(input:checked) { border-color: var(--accent); }
.consent input {
  flex: none;
  width: 22px; height: 22px;
  margin: 1px 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* The tap-is-consent disclosure on the capture screen (owner ruling
   2026-08-27). No box, so no panel and no border either: the operators' survey
   found the converged form is a quiet paragraph of fine print sitting directly
   above the button it names
   (design/OPERATOR-CAPTURE-PATTERNS-2026-08-27.md section 2.2). Muted, but not
   small — this is the same size as `.hint`, because the two properties the
   FTC has actually litigated are 10px type and links that do not look like
   links, and both links here are underlined in body colour. */
.consent-line {
  margin: 0 0 14px;
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--muted);
}
.consent-line a { color: var(--text); text-decoration: underline; }

/* ================================================================= buttons */

.btn {
  display: block;
  width: 100%;
  min-height: 52px;
  /* 24px sides since the pill (§R change 2, 2026-08-30): a label inside a
     999px radius needs more shoulder than it needed inside 14px corners. */
  padding: 15px 24px;
  border: 0;
  border-radius: var(--radius-pill); /* §R: ONE button geometry, the pill */
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 700;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: background 150ms ease;
}
/* The accent hue drifts with scroll position (gold high on screen, warm ember
   low), iMessage-style. --cta-h is set per button from app.js; the fallback
   keeps the static brand amber when JS is absent. */
.btn-accent {
  background: hsl(var(--cta-h, 38) 88% 66%);
  color: var(--accent-ink);
  transition: background 120ms linear;
}
.btn-accent:hover { background: hsl(var(--cta-h, 38) 92% 72%); }
.btn-accent:disabled { opacity: 0.45; cursor: not-allowed; background: hsl(var(--cta-h, 38) 88% 66%); }
.btn-link {
  margin-top: 10px;
  background: transparent;
  color: var(--muted);
  font-weight: 600;
  font-size: var(--fs-small);
  min-height: 48px;
}
.btn-link:hover { color: var(--text); }
form .btn-accent { margin-top: 4px; }

/* ----------------------------------------------------- the finish line
   Consent, the error line and the submit are one block, and on a phone that
   block sticks to the bottom of the viewport.

   Measured on a 390x664 in-app browser — the geometry Instagram and Facebook
   actually hand us: the callback form put its submit at 661 px of an 830 px
   document and the appointment form at 952 px of 1,122 px, so on both the last
   tap, and the consent box that gates it, sat below the fold at the exact
   moment the progress bar read 100% (FRACTIONAL-GAINS-2026-08-26 row 3 / QF-2).
   The appointment form cannot be trimmed into 664 px — six fields, two chip
   rows and a TCPA disclosure do not fit — so the finish line follows the
   visitor instead.

   `position: sticky` engages only when the block would otherwise fall below the
   fold: a form that already fits renders exactly as before. The error line
   lives inside the block on purpose, so a failed or timed-out submit (QF-8) is
   never announced off-screen. */
.form-actions { position: relative; margin-top: 18px; }
.form-actions .consent { margin-top: 0; }
.form-actions .consent-line { margin-top: 0; }
/* V-42 / J1-12, 2026-08-28. The row put Back and the submit side by side at
   1:2, which made the two highest-value taps in the funnel — "Get my free
   preview" at step 2 and "Book my visit" at step 8 — about 230px wide and
   right-aligned, while the low-stakes "Continue" and "Keep going" on step one
   are full-width buttons. The submit got smaller as the ask got bigger, and a
   third of the finish line was spent on the way out.
   Column-reverse rather than a reordered DOM: Back stays first in source, where
   it belongs for a keyboard and for a screen reader walking the form, and the
   submit renders above it. Back is a text link under the button now, which is
   what it always was in style; it was only its position in a two-up row that
   gave it a button's weight. */
.action-row {
  display: flex;
  flex-direction: column-reverse;
  gap: 2px;
}
/* Beats `form .btn-accent`'s 4 px, which would knock the button off the top of
   the block it is stacked in. */
form .action-row .btn { margin-top: 0; }
.action-row .btn-accent { width: 100%; }
.action-row .btn-link { min-height: 44px; }

@media (max-width: 700px) {
  .form-actions {
    position: sticky;
    bottom: 0;
    z-index: 3;
    padding-bottom: calc(8px + env(safe-area-inset-bottom));
    background: var(--ground);
  }
  /* The only cue that content passes underneath. Over the flat ground colour
     it is invisible while the block sits in normal flow. */
  .form-actions::before {
    content: "";
    position: absolute;
    left: 0; right: 0; top: -20px;
    height: 20px;
    background: linear-gradient(to top, var(--ground), rgba(10, 14, 22, 0));
    pointer-events: none;
  }
  /* Every pixel taken out of the fields is a pixel the sticky block does not
     have to cover. Contact screens only: the question screens are not tight. */
  .screen[data-screen="appointment"] .field,
  .screen[data-screen="callback"] .field { margin-bottom: 12px; }
  .screen[data-screen="appointment"] .field:has(+ .hint),
  .screen[data-screen="callback"] .field:has(+ .hint) { margin-bottom: 5px; }
  .screen[data-screen="appointment"] .hint,
  .screen[data-screen="callback"] .hint { margin-bottom: 12px; }
  .screen[data-screen="appointment"] .sub,
  .screen[data-screen="callback"] .sub { margin-bottom: 16px; }
}

/* Keyboard up on a phone leaves a band around 364 px tall. The block still has
   to fit the disclosure and the button inside it, so it gets compact rather
   than clipped — the caption token (0.8rem = 12.8px) is the floor the consent
   text is allowed to reach, the same floor the whole ramp obeys (§R). The
   compaction is spacing and line-height now; the sizes stay on the ramp. */
@media (max-width: 700px) and (max-height: 520px) {
  .form-actions { margin-top: 10px; }
  .form-actions .consent {
    gap: 10px;
    padding: 10px;
    font-size: var(--fs-caption);
    line-height: 1.4;
    margin-bottom: 10px;
  }
  .form-actions .consent-line {
    font-size: var(--fs-caption);
    line-height: 1.4;
    margin-bottom: 10px;
  }
  .action-row .btn { min-height: 46px; font-size: var(--fs-body); }
  /* The stack costs the sticky finish-line block a row it did not use to have
     (V-42), and this is the viewport where that block was already the tightest
     thing on the screen — the 390x664 in-app browser QF-2 was measured on. Back
     gives the height back: it is a text link, it is the way out rather than the
     way on, and 40px still clears the 24px WCAG 2.5.8 minimum with room. */
  .action-row .btn-link { min-height: 40px; font-size: var(--fs-small); }
}

.err {
  margin: 6px 0 12px;
  padding: 10px 12px;
  color: var(--bad);
  background: rgba(255, 138, 128, 0.10);
  border: 1px solid rgba(255, 138, 128, 0.30);
  border-radius: 10px;
  font-size: var(--fs-small);
  font-weight: 600;
}

/* One field, one message, directly under the field it belongs to (CRO item
   77). Built by `wireFieldErrors` in app.js, not written into the markup, so
   the slot and the field list cannot drift apart.

   Deliberately quieter than `.err`: no panel, no border. The boxed form-level
   line is for the things that belong to no field — the consent box, a chip
   nobody picked, whatever the server said — and it stays the loud one. This is
   a note under a field, in the same place and the same size as the hint that
   explains the field, so a form with an error is the same shape as a form
   without one. */
.field-err {
  margin: 0 0 14px;
  color: var(--bad);
  /* Errors round UP at the 0.85 tie (§R mapping): a message asking for a fix
     should not be quieter than the hint that explains the field. */
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.4;
}
/* The field gives up its own bottom margin so the message reads as part of the
   field rather than as a line floating between two of them — the same trick
   `.field:has(+ .hint)` already plays.

   The slot moved above the hint on 2026-08-28 (V-41 / J1-09), so the pair of
   rules moved with it: the hint no longer precedes the message, and the message
   now has a hint under it to leave room for. A hidden slot has the UA's
   `display: none` and contributes neither margin nor box, so an error-free
   field still renders exactly as it did. */
.field:has(+ .field-err) { margin-bottom: 6px; }
.field-err:has(+ .hint) { margin-bottom: 6px; }

/* And the field itself says so, for the visitor who is scrolling past the
   message looking for the box to fix. */
.field input[aria-invalid="true"] { border-color: var(--bad); }
.field input[aria-invalid="true"]:focus {
  border-color: var(--bad);
  box-shadow: 0 0 0 3px rgba(255, 138, 128, 0.18);
}

/* ============================================================ wider screens */

@media (min-width: 701px) {
  .hero-photo img { object-position: 50% 50%; }
  .strip li { flex: 0 0 42vw; }
  .stage { aspect-ratio: 16 / 8; }
  /* Full-bleed rails still start on the text column's left edge, so the first
     photo and the first chip line up under the heading above them. */
  .strip, .stage-chips {
    padding-left: max(var(--pad), calc((100% - var(--col)) / 2));
    padding-right: max(var(--pad), calc((100% - var(--col)) / 2));
  }
}

@media (max-width: 430px) {
  .cards { grid-template-columns: 1fr; }
}

/* ============================================================= legal pages */

.legal { padding-top: 32px; padding-bottom: 64px; }
/* Heading-role wins over nearest-number (§R mapping): a legal page's h1 is a
   section head in the page system, not a landing hero — it sits on the h2 rung,
   and its h2s on h3. */
.legal h1 { font-size: var(--fs-h2); margin-bottom: 4px; }
.legal .sub { margin-top: 0; }
.legal h2 { font-size: var(--fs-h3); margin-top: 30px; }
.legal p { line-height: 1.6; }
.legal .foot-link { margin-top: 36px; }


/* ------------------------------------------------- micro-interactions pass */
.btn, .chip, .opt { transition: transform 120ms ease, box-shadow 150ms ease, background 150ms ease, border-color 150ms ease, color 150ms ease; }
.btn:active, .chip:active, .opt:active { transform: scale(0.985); }
/* Both behind `(hover: hover)` since 2026-08-28 (V-38 / V-39): the second one
   restated `.opt:hover` after the base rule and would have put the sticky
   touch-hover border straight back. */
@media (hover: hover) {
  .btn-accent:hover { box-shadow: 0 6px 22px rgba(245, 185, 90, 0.28); }
  .opt:hover { border-color: var(--accent); }
}
input:focus-visible, .btn:focus-visible, .chip:focus-visible, .opt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.field input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(245, 185, 90, 0.16); }

/* progress bar eases between steps */
.progress-fill { transition: width 320ms cubic-bezier(0.3, 0.8, 0.3, 1); }

/* sticky bar slides up when it arrives */
.stickybar { animation: sticky-in 260ms cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes sticky-in { from { transform: translateY(100%); } to { transform: none; } }

/* hero photo: a very slow drift, alive but never noticed directly */
.hero-photo img { animation: hero-drift 26s ease-in-out infinite alternate; }
@keyframes hero-drift { from { transform: scale(1); } to { transform: scale(1.045); } }

/* faq chevron eases */
.faq summary::after { transition: transform 220ms ease; }

/* thanks check pops */
.check { animation: check-pop 380ms cubic-bezier(0.2, 1.4, 0.4, 1) both; }
@keyframes check-pop { from { transform: scale(0.4); opacity: 0; } to { transform: none; opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .screen.is-active, .stickybar, .hero-photo img, .check { animation: none; }
  /* The sheet still arrives, it just does not travel to get there. */
  .nudge { transition: none; }
}


/* desktop: sticky bar hugs the content column, not the full viewport */
@media (min-width: 701px) {
  .stickybar {
    padding-left: max(var(--pad), calc((100% - var(--col)) / 2));
    padding-right: max(var(--pad), calc((100% - var(--col)) / 2));
  }
}

/* ====================================================== the desktop treatment
   J3-01 / J3-02, 2026-08-29. Above the tablet breakpoint the page was the phone
   layout stretched: a full-width yellow bar with a ~680px pill inside it
   repainting over the content column at every scroll depth, and a proof rail
   that started at the text column's left edge and ran off the right of the
   viewport with the third house cut in half. Both are the same mistake —
   geometry written for a 390px column and handed a 1440px one.

   1024px rather than 701px is deliberate: a 768px tablet in portrait is still
   a thumb device and wants the phone's bar under it, and a rail with a peek is
   the right affordance at that width. This is the mouse-and-monitor tier. */
@media (min-width: 1024px) {
  /* J3-01. A button, not a bar: fixed to the corner the thumb never has to
     reach, sized to its own label, out of the content column entirely so it
     stops repainting over what is being read. The label, the arrow and the call
     target are unchanged — this is where the ask sits, not what it says. */
  .stickybar {
    left: auto;
    right: 28px;
    bottom: 28px;
    width: auto;
    max-width: min(440px, calc(100vw - 56px));
    padding: 10px;
    gap: 10px;
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    box-shadow: 0 10px 34px rgba(6, 9, 15, 0.55);
  }
  .sticky-cta {
    flex: 0 1 auto;
    padding-left: 28px;
    padding-right: 28px;
    border-radius: var(--radius-pill); /* inherits the one geometry from .btn;
       restated so the corner treatment cannot drift from its frame */
    white-space: nowrap;
  }
  /* End-of-document clearance, restored 2026-08-30 (§R change 2, band05). The
     J3-01 `padding-bottom: 0` reintroduced the V-29 class of permanent
     occlusion at 1024–1300px, where the corner pill's left edge reaches into
     the content column: at full scroll the footer's last lines sat behind it.
     The pill stands 28px off the floor plus 72px tall — 112px lets the
     document end clear of it at every desktop width. */
  .landing { padding-bottom: 112px; }

  /* J3-02. The proof section is the trust core of the page and it was the one
     visibly broken composition on a monitor. A grid rather than a rail: five
     photographs, three across and two across, inside a bounded centred column,
     with no clipped house and no dead margin. The scroll rail, its snap and its
     edge mask are phone affordances and come off with it — a grid that keeps a
     mask fades its own outer columns for no reason. */
  .strip {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 16px;
    /* The one column (§R change 3, 2026-08-30). 1120px was the page's sole
       second container logic — a left-anchored heading over a wider centred
       band, and the band the corner pill sat on at 1440 (band05). The 3+2
       grid keeps its shape; it just obeys the column everything else obeys. */
    max-width: var(--col);
    margin: 28px auto 0;
    padding-left: var(--pad);
    padding-right: var(--pad);
    overflow-x: visible;
    scroll-snap-type: none;
    -webkit-mask-image: none;
    mask-image: none;
  }
  .strip li {
    grid-column: span 3;
    max-width: none;
  }
  /* Re-cut 2026-08-30 (design-director verdict on §R change 3): the 3+2 cut
     inside the one column shrank the proof photographs to ~197px — contract-
     correct, product-regressive on the page's trust core. Five photos two
     across cut as a full-width lead plus a 2×2 quad: the lead runs the whole
     column (~630px, the largest a proof photo has ever rendered here) and the
     quad holds ~310px — no orphan, no ragged edge, first slot stays the
     CLEAN-graded hero per R-0830-1. */
  .strip li:first-child { grid-column: span 6; }
}
