/* ══════════════════════════════════════════════════════════════════════════
   RENOVATION ALLY — CONSUMER WEBSITE LAYER

   Built entirely on the brand kit's own tokens. Every colour, font, size,
   radius, shadow, duration and easing below is a `--ra-*` variable from
   `brand/tokens.css`, which is generated from `tokens.json` and must not be
   hand-edited. `node web/token-check.mjs` fails on any literal in this file.

   Authority, per 08-consumer-website-handoff/START-HERE-WEBSITE.md:
     brand/tokens.css              colours and type. Frozen.
     brand/website-layer-tokens.css  website-only spacing and motion. Frozen.
     this file                     composition, which the handoff governs.

   Direction, from the handoff: "Quiet architecture-gallery polish. White or
   off-white generous negative space, restrained calm green accents, strong
   Bricolage typography, outstanding photoreal renovation imagery, hairline
   dividers where needed." Open and image-led, NOT a wall of pale-green cards.
   ══════════════════════════════════════════════════════════════════════════ */

@import url("brand/tokens.css");
@import url("brand/website-layer-tokens.css");

/* ── BASE ───────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--ra-bg);
  color: var(--ra-text);
  font-family: var(--ra-font-ui);
  font-size: var(--ra-text-body);
  line-height: var(--ra-lh-body);
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}
img, svg, video { max-width: 100%; display: block; }
a { color: var(--ra-link); }
a:hover { color: var(--ra-link-hover); }

/* The kit's focus ring, everywhere, with no exceptions for decoration. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: none;
  box-shadow: var(--ra-focus-ring);
  border-radius: var(--ra-radius-sm);
}

/* ── LAYOUT ─────────────────────────────────────────────────────────────── */
.wrap {
  width: min(var(--ra-site-content-max), 100%);
  margin-inline: auto;
  padding-inline: var(--ra-site-gutter);
}
.wrap-narrow { width: min(760px, 100%); margin-inline: auto; padding-inline: var(--ra-site-gutter); }
.section { padding-block: var(--ra-site-section-space); }
.section-tight { padding-block: calc(var(--ra-site-section-space) * .6); }

/* ── SECTION RHYTHM ────────────────────────────────────────────────────────
   The space above lives inside each section, so two stacked sections both
   contribute and the boundary between them gets TWICE what either one asked
   for. Measured across all eleven informational pages at 1440, 768 and 390,
   every boundary came back identical: 202px, 108px, 96px. Band change or not,
   dense section or a single line, the number never moved. That is not quiet
   architectural spacing, it is one constant applied everywhere, and on a short
   page it reads as a hole.

   So the boundary now depends on what it separates. Two sections on the SAME
   surface collapse to 1.4 units, which is still generous and no longer a void.
   A boundary where the surface CHANGES keeps the full two, because a colour
   edge needs room on both sides to read as deliberate rather than as a seam.

   `.section-plan` is excluded: the planner sets its own asymmetric padding and
   sits after a JavaScript-disabled fallback section, so collapsing against a
   sibling that is usually not even displayed would be meaningless.
   ─────────────────────────────────────────────────────────────────────────── */
:where(.section, .section-tight):not(.band-paper):not(.band-dark)
  + :where(.section, .section-tight):not(.band-paper):not(.band-dark):not(.section-plan):not(.section-attached),
.section.band-paper + .section.band-paper,
.section.band-dark + .section.band-dark {
  padding-top: calc(var(--ra-site-section-space) * .4);
}

/* A note that OPENS a page qualifies the heading above it, so it is part of
   that opening rather than a section in its own right. It drops its top
   padding entirely and lets the heading block's bottom padding carry the gap,
   which stops a two-line caveat from sitting in the middle of 280px of white. */
.section-attached { padding-top: 0; }
.band-paper { background: var(--ra-surface-alt); }
.band-dark { background: var(--ra-offblack); }
.stack { display: grid; gap: var(--ra-space-4); }
.stack-lg { display: grid; gap: var(--ra-space-8); }
.hr { border: 0; border-top: 1px solid var(--ra-border); margin: 0; }

/* ── TYPE ───────────────────────────────────────────────────────────────── */
h1, h2, h3, h4 {
  font-family: var(--ra-font-display);
  color: var(--ra-heading);
  margin: 0;
  text-wrap: balance;
}
.display { font-size: var(--ra-text-display); line-height: var(--ra-lh-display);
  letter-spacing: var(--ra-ls-display); font-weight: var(--ra-fw-display); }
h1 { font-size: var(--ra-text-h1); line-height: var(--ra-lh-h1);
  letter-spacing: var(--ra-ls-h1); font-weight: var(--ra-fw-h1); }
h2 { font-size: var(--ra-text-h2); line-height: var(--ra-lh-h2);
  letter-spacing: var(--ra-ls-h2); font-weight: var(--ra-fw-h2); }
h3 { font-size: var(--ra-text-h3); line-height: var(--ra-lh-h3);
  letter-spacing: var(--ra-ls-h3); font-weight: var(--ra-fw-h3); }
h4 { font-size: var(--ra-text-h4); line-height: var(--ra-lh-h4);
  letter-spacing: var(--ra-ls-h4); font-weight: var(--ra-fw-h4); }

/* Copy measure of 55 to 70 characters, per the UI overrides. */
p { margin: 0; max-width: var(--ra-site-copy-measure); text-wrap: pretty; }
.lede { font-size: var(--ra-text-body-lg); line-height: var(--ra-lh-body-lg);
  letter-spacing: var(--ra-ls-body-lg); color: var(--ra-text-muted); max-width: 58ch; }
.small { font-size: var(--ra-text-small); line-height: var(--ra-lh-small); color: var(--ra-text-muted); }
.caption { font-size: var(--ra-text-caption); line-height: var(--ra-lh-caption);
  letter-spacing: var(--ra-ls-caption); color: var(--ra-text-muted); }
.eyebrow {
  display: inline-flex; align-items: center; gap: var(--ra-space-2);
  font-family: var(--ra-font-ui); font-size: var(--ra-text-eyebrow);
  line-height: var(--ra-lh-eyebrow); letter-spacing: var(--ra-ls-eyebrow);
  font-weight: var(--ra-fw-eyebrow); text-transform: uppercase; color: var(--ra-text-muted);
}
.eyebrow .pip { width: 6px; height: 6px; border-radius: var(--ra-radius-full);
  background: var(--ra-brand); flex: none; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* A skip link that never appears is not a skip link. On focus it becomes a real
   button in the top-left, above everything. */
a.sr-only:focus {
  position: fixed; top: var(--ra-space-4); left: var(--ra-space-4);
  width: auto; height: auto; z-index: 999; clip-path: none; white-space: normal;
  padding: var(--ra-space-3) var(--ra-space-5);
  background: var(--ra-cta); color: var(--ra-cta-text);
  border-radius: var(--ra-radius-full); text-decoration: none;
  font-weight: var(--ra-fw-button); box-shadow: var(--ra-shadow-lg);
}
[hidden] { display: none !important; }

/* ══ BUTTONS AND INTERACTIVE CONTROLS ══════════════════════════════════
   One system, seven roles, every state considered.

     .btn-primary     the one action on a view, filled calm green
     .btn-secondary   the alternative, outlined
     .btn-ghost       low emphasis on a tinted or dark surface
     .btn-text        a link that behaves like a button, with an arrow
     .btn-icon        square, icon only, always labelled
     .btn-lg          a campaign CTA
     .btn-sm          a compact navigation action

   SHAPE. Pill for anything that reads as an action, so it is never mistaken for
   an input or a card. Icon buttons are circles. Inputs and cards keep the kit's
   10 and 16px corners, which is what keeps the pill meaningful rather than a
   global radius change.

   MOTION. Three responses, none decorative:
     hover    background and border shift, the arrow moves 3px
     press    compresses to .97 for ~110ms, no bounce
     focus    the kit's own ring, never removed
   The primary never LIFTS on hover. A button that moves away from the cursor is
   a gimmick; one that deepens is a response. */

.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--ra-space-2);
  min-height: 48px; padding: var(--ra-space-3) var(--ra-space-6);
  font-family: var(--ra-font-ui); font-size: var(--ra-text-button);
  line-height: var(--ra-lh-button); letter-spacing: var(--ra-ls-button);
  font-weight: var(--ra-fw-button);
  border-radius: var(--ra-radius-full);
  border: 1px solid transparent;
  text-decoration: none; cursor: pointer;
  /* Named properties, never `all`: transitioning everything animates layout. */
  transition: background-color var(--ra-site-motion-fast) var(--ra-site-ease-standard),
              border-color var(--ra-site-motion-fast) var(--ra-site-ease-standard),
              color var(--ra-site-motion-fast) var(--ra-site-ease-standard),
              transform 110ms var(--ra-site-ease-standard);
  -webkit-tap-highlight-color: transparent;
}
.btn:active { transform: scale(.97); }

.btn-primary { background: var(--ra-cta); color: var(--ra-cta-text); border-color: var(--ra-cta); }
.btn-primary:hover { background: var(--ra-cta-hover); border-color: var(--ra-cta-hover); color: var(--ra-cta-text); }

.btn-secondary { background: transparent; color: var(--ra-secondary); border-color: var(--ra-border-input); }
.btn-secondary:hover { background: var(--ra-surface-tint); border-color: var(--ra-secondary); color: var(--ra-secondary); }

.btn-ghost { background: transparent; color: var(--ra-text); border-color: transparent; }
.btn-ghost:hover { background: var(--ra-surface-tint); }

/* A text button: no box until it is touched, so a page can carry several. */
.btn-text {
  background: none; border: 0; padding: var(--ra-space-2) 0; min-height: 44px;
  color: var(--ra-link); border-radius: var(--ra-radius-sm);
  /* ── THE UNDERLINE BELONGS TO THIS BUTTON (2026-10-28) ────────────────────
     Josh, on the approved-application screen: hovering "Not you? Start a new
     application" drew "a strange approximately 1px black line across the
     middle of the screen over the green Renovation Ally area."

     It did, and it was this rule. The ::after below is the sliding underline:
     position absolute, left 0, right 0. With no positioned ancestor its
     containing block is the INITIAL containing block, so left:0 and right:0
     mean the edges of the PAGE, not the edges of the button, and bottom is
     measured from the page too. Hovering set scaleX(1) and painted a 1px rule
     from one side of the viewport to the other, at whatever height the page
     happened to put it. On the approval card nothing between the button and
     the root is positioned, which is why that screen showed it and others did
     not: elsewhere some ancestor happened to be relative and quietly caught
     the underline.

     One word fixes it. The pseudo-element is scoped to the element it
     underlines, which is what it always meant. */
  position: relative;
}
.btn-text::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: var(--ra-space-2);
  height: 1px; background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.btn-text:hover::after, .btn-text:focus-visible::after { transform: scaleX(1); }
.btn-text:active { transform: none; }

.btn-icon {
  width: 48px; height: 48px; min-height: 48px; padding: 0;
  border-radius: var(--ra-radius-full); border-color: var(--ra-border-input);
  background: var(--ra-surface); color: var(--ra-text);
}
.btn-icon:hover { background: var(--ra-surface-tint); border-color: var(--ra-secondary); }
.btn-icon svg { width: 20px; height: 20px; }

.btn-lg { min-height: 56px; padding: var(--ra-space-4) var(--ra-space-8); font-size: var(--ra-text-body-lg); }
.btn-sm { min-height: 44px; padding: var(--ra-space-2) var(--ra-space-5); font-size: var(--ra-text-small); }

/* On a dark surface the action inverts: the action is the highest-contrast
   element on its surface, and that rule survives where the colour cannot. */
.on-dark .btn-primary, .btn-primary.on-dark {
  background: var(--ra-white); border-color: var(--ra-white); color: var(--ra-offblack);
}
.on-dark .btn-primary:hover { background: var(--ra-matcha-300); border-color: var(--ra-matcha-300); }
.on-dark .btn-secondary { color: var(--ra-white); border-color: var(--ra-border-input); }
.on-dark .btn-secondary:hover { background: var(--ra-green-800); border-color: var(--ra-white); }

/* DISABLED: unavailable without relying on colour alone. */
.btn[disabled], .btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; transform: none; }
.btn-primary[disabled]:hover { background: var(--ra-cta); border-color: var(--ra-cta); }
.btn-secondary[disabled]:hover { background: transparent; border-color: var(--ra-border-input); }

/* LOADING: the label stays put so the button does not resize, and a spinner
   appears beside it. `aria-busy` is what a screen reader reads. */
.btn[aria-busy="true"] { cursor: progress; }
.btn[aria-busy="true"] .btn-spinner { display: block; }
.btn-spinner {
  display: none; width: 16px; height: 16px; flex: none;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: var(--ra-radius-full);
  animation: btn-spin 620ms linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

/* SUCCESS: held briefly before the view changes, so the action is acknowledged. */
.btn[data-state="done"] { background: var(--ra-success-600); border-color: var(--ra-success-600); color: var(--ra-white); }
.btn[data-state="done"] .btn-spinner { display: none; }

/* THE ARROW. 3px is enough to feel; more reads as a bounce. */
.btn .arrow { display: inline-block;
  transition: transform var(--ra-site-motion-fast) var(--ra-site-ease-standard); }
.btn:hover .arrow, .btn:focus-visible .arrow { transform: translateX(3px); }

/* TOUCH: a hover state must not stick to a finger, and a tap that shrinks the
   target under the thumb feels broken. */
@media (hover: none) {
  .btn-primary:hover { background: var(--ra-cta); border-color: var(--ra-cta); }
  .btn-secondary:hover { background: transparent; border-color: var(--ra-border-input); }
  .btn-ghost:hover, .btn-icon:hover { background: transparent; }
  .btn:active { transform: none; }
  .btn-primary:active { background: var(--ra-cta-hover); }
  .btn-text::after { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .btn, .btn .arrow, .btn-text::after { transition-duration: 1ms; }
  .btn:active { transform: none; }
  .btn-spinner { animation-duration: 1.4s; }
}

.btn-row { display: flex; gap: var(--ra-space-3); flex-wrap: wrap; align-items: center; }

.link-plain { background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
  color: var(--ra-link); text-decoration: underline; text-underline-offset: 4px;
  text-decoration-thickness: 1px; }
.link-plain:hover { color: var(--ra-link-hover); }

/* ── SITE SHELL ─────────────────────────────────────────────────────────── */

/* ── THE STICKY HEADER'S HEIGHT, DECLARED HERE AND MEASURED AT RUNTIME ──────
   A SITE token, not a brand one. `brand/tokens.css` is hash-protected by
   web/brand-integrity.mjs, and rightly: a header's height is a fact about this
   shell, not part of the kit, so adding it there would have meant re-stamping a
   brand hash for something that is not brand.

   The value here is the default, and the only one that matters is the single
   frame before `find-a-professional.html` publishes the header's measured
   height over it. Declared rather than left to a `var()` fallback because
   web/token-check refuses a reference to a token nothing defines, and it was
   right to: a token that exists only at runtime is invisible to every reader of
   the CSS, and a fallback is how that stops looking like a bug. */
:root { --ra-header-h: 4.5rem; }

.site-header { position: sticky; top: 0; z-index: 100; background: var(--ra-bg);
  border-bottom: 1px solid transparent;
  transition: border-color var(--ra-site-motion-fast) linear; }
.site-header.is-stuck { border-bottom-color: var(--ra-border); }
/* `flex-wrap` so the header REFLOWS instead of overflowing. At 375px with text
   at 200%, "Get quote" is 185px wide and the row came to 399px against a 375px
   viewport: the whole page scrolled sideways by 24px, on every page, which is
   the two-dimensional scrolling WCAG reflow exists to prevent.

   Wrapping costs nothing at ordinary sizes, because a flex row only wraps when
   its contents do not fit. `min-width: 0` lets the action shrink before it
   wraps, so the common case is still one row. */
.header-in { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ra-space-6); padding-block: var(--ra-space-4); }
.header-in > * { min-width: 0; }
/* ── THE HEADER ACTION NEVER WRAPS ──────────────────────────────────────────
   Found by web/wrap-check.mjs on SEVEN pages at 320 and 360: "Find one" broke
   across two lines inside its pill and made the button 58px tall instead of 48.
   The same fault Josh reported on the contractor header, on the homeowner site,
   everywhere, and nobody had noticed because nothing overflows and no page
   scrolls: the control just gets taller.

   A flex item shrinks below the width of its own words by default. `nowrap`
   alone would overflow instead; `flex: none` alone would overflow the row. The
   pair is the rule: it may give, never past its own longest word. */
.nav-actions .btn { flex: 0 1 auto; min-width: max-content; white-space: nowrap; }
/* The supplied logo artwork, placed and never altered. */
.logo { display: inline-flex; align-items: center; flex: none; }
.logo img { height: 34px; width: auto; }
@media (max-width: 480px) { .logo img { height: 28px; } }
.nav-links { display: flex; gap: var(--ra-space-5); margin-left: auto; align-items: center; }
/* 44px tall, because an audit found four nav links at 21px. A text link on a
   touch laptop is still a touch target. */
.nav-links a {
  position: relative; display: inline-flex; align-items: center;
  min-height: 44px; padding-inline: var(--ra-space-2);
  font-size: var(--ra-text-label); font-weight: var(--ra-fw-label);
  color: var(--ra-text); text-decoration: none;
  transition: color var(--ra-site-motion-fast) var(--ra-site-ease-standard);
}
/* A hairline that grows from the centre. Hover and the active page use the same
   indicator, so "where I am" and "where I would go" read as one language. */
.nav-links a::after {
  content: ""; position: absolute; left: var(--ra-space-2); right: var(--ra-space-2);
  bottom: 6px; height: 2px; border-radius: var(--ra-radius-full);  /* off-scale: a 2px rule nudged optically under a baseline, not spacing between things */
  background: var(--ra-brand); transform: scaleX(0);
  transition: transform var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.nav-links a:hover { color: var(--ra-brand); }
.nav-links a:hover::after { transform: scaleX(1); }
/* THE ACTIVE PAGE. The audit found no indicator at all. */
.nav-links a[aria-current="page"] { color: var(--ra-brand); }
.nav-links a[aria-current="page"]::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) { .nav-links a::after { transition: none; } }
.nav-actions { display: flex; gap: var(--ra-space-3); align-items: center; }
/* ── THE HEADER IS A LOGO, ONE ACTION, AND A MENU ───────────────────────────
   Josh, 2026-10-24: "We don't need all the buttons in the corner, all the
   stuff. I just want to keep it super simple."

   The inline links are gone at EVERY width and the menu button is shown at
   every width instead. Not removed: "How it works" and the project pages would
   then be reachable only from the footer, and a page you can only get to by
   scrolling past everything is most of the way to not existing. One button
   holds them, which is simple and orphans nothing. */
.nav-burger { display: grid; width: 44px; height: 44px; place-items: center;
  background: none; border: 1px solid var(--ra-border-input); border-radius: var(--ra-radius-md);
  cursor: pointer; color: var(--ra-text); margin-left: auto; }
.nav-burger svg { width: 20px; height: 20px; }
.nav-links { display: none; }
@media (max-width: 900px) {
  .nav-actions .btn { padding-inline: var(--ra-space-4); }
}
/* The label gives way before the button does: hiding the action to save space
   is the wrong trade on the page we are paid for.

   ── 420 WAS THE SECOND GUESSED NUMBER IN THIS HEADER (2026-10-25) ──────────
   Josh photographed "Start getting leads" running off the right of a 430px
   phone. The nav LINK's breakpoint was 400 and this one was 420, so at 430
   both kept their long labels and the row needed 437px of a 430px viewport.

   Fixing the link alone did not fix the header, because the button's label is
   a separate pair with a separate number, and the sweep still failed at 430.
   Two independent magic numbers in one row is two chances for a band to break
   and no way to reason about either in isolation.

   ── AND ONLY THIS ONE WAS LOAD-BEARING ────────────────────────────────────
   I changed the LINK's 400 first and the sweep still failed at 430. Then I
   changed this one and it passed. Planting each separately afterwards settled
   which mattered: with the button's label short there is room for the link's
   long one even at 430, so the link's number was never the fault and putting
   it to 900 would have truncated "I'm a homeowner" on every tablet to fix a
   phone. It is back at 400 and this is the number that moved.

   520 rather than 900, because the sweep says both long labels need about
   481px: below 520 the button says "Get leads", above it there is genuinely
   room for the full phrase. Measured, not chosen for feel. */
/* ── 3px AT 320, AND WHERE IT CAME FROM (2026-10-25) ────────────────────────
   Renaming the header action to Josh's wording made the narrow label "Get quote"
   where it had been "Find one": one character wider, and the planner page then
   scrolled sideways by exactly 3px at 320. `audit-planner` measures
   `documentElement.scrollWidth - innerWidth` on every step, so it caught it on
   all four.

   The label is not shortened back. "Quote" alone loses the verb on the only
   action the homeowner page has, and 3px is a padding problem, not a copy
   problem. 340 rather than 320 so the smallest phones share one rule. */
@media (max-width: 340px) {
  .nav-actions .btn { padding-inline: var(--ra-space-3); }
  .header-in { gap: var(--ra-space-2); }
}

.btn-label-short { display: none; }
@media (max-width: 520px) {
  .nav-actions .btn-label-long { display: none; }
  .nav-actions .btn-label-short { display: inline; }
}

/* The audit measured opacity 1 and transform none at both 80ms and 480ms: the
   sheet simply appeared. It now fades and rises, and its links arrive just
   behind it, which is what makes a menu feel built rather than toggled. */
.nav-sheet { position: fixed; inset: 0; z-index: 500; background: var(--ra-bg);
  padding: var(--ra-space-6) var(--ra-site-gutter); display: grid; align-content: start;
  gap: var(--ra-space-5); overflow-y: auto;
  opacity: 0; transform: translateY(-8px);
  transition: opacity var(--ra-site-motion-base) var(--ra-site-ease-standard),
              transform var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.nav-sheet.is-open { opacity: 1; transform: none; }
.nav-sheet[hidden] { display: none !important; }
.nav-sheet a:not(.btn) {
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--ra-site-motion-base) var(--ra-site-ease-enter),
              transform var(--ra-site-motion-base) var(--ra-site-ease-enter);
}
.nav-sheet.is-open a:not(.btn) { opacity: 1; transform: none; }
.nav-sheet.is-open a:nth-of-type(1) { transition-delay: 40ms; }
.nav-sheet.is-open a:nth-of-type(2) { transition-delay: 70ms; }
.nav-sheet.is-open a:nth-of-type(3) { transition-delay: 100ms; }
.nav-sheet.is-open a:nth-of-type(4) { transition-delay: 130ms; }
.nav-sheet.is-open a:nth-of-type(5) { transition-delay: 160ms; }
.nav-sheet.is-open a:nth-of-type(6) { transition-delay: 190ms; }
@media (prefers-reduced-motion: reduce) {
  .nav-sheet, .nav-sheet a:not(.btn) { transition: none; opacity: 1; transform: none; }
}
.nav-sheet-top { display: flex; align-items: center; justify-content: space-between;
  margin-bottom: var(--ra-space-4); }
.nav-sheet a:not(.btn) { font-family: var(--ra-font-display); font-size: var(--ra-text-h3);
  font-weight: var(--ra-fw-h3); letter-spacing: var(--ra-ls-h3); color: var(--ra-heading);
  text-decoration: none; }

.site-footer { border-top: 1px solid var(--ra-border); background: var(--ra-surface-alt); }
.footer-in { padding-block: var(--ra-space-16); display: grid; gap: var(--ra-space-10); }
@media (min-width: 760px) { .footer-in { grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: var(--ra-space-8); } }
.site-footer h4 { font-family: var(--ra-font-ui); font-size: var(--ra-text-caption);
  letter-spacing: var(--ra-ls-caption); text-transform: uppercase; color: var(--ra-text-muted);
  font-weight: var(--ra-fw-caption); margin-bottom: var(--ra-space-4); }
.site-footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--ra-space-3); }
.site-footer a { font-size: var(--ra-text-small); color: var(--ra-text-muted); text-decoration: none; }

/* ══ TAP TARGETS ON A TOUCH WIDTH ══════════════════════════════
   Found by sweeping all fourteen public pages at four widths: every footer and
   nav link rendered **17px tall** on a phone, against the 24px minimum in WCAG
   2.5.8. Twenty-eight findings, all the same fault.

   WCAG's own exception is for a link INLINE IN A SENTENCE, and the sweep honours
   it: a link inside a paragraph with text either side is skipped. A footer list
   is not that. Each item is its own line, they are stacked a few pixels apart,
   and 17px is where a thumb hits the wrong one.

   Padding rather than a larger font, so nothing about the typography changes:
   the target grows, the design does not. Negative margin on the list cancels the
   extra outer space so the footer does not get taller than it was, which was
   measured, because the signup page fits its laptop viewport by 2px and a
   taller footer would undo that.

   Touch widths only. On a desktop a 17px link under a mouse is fine and the
   extra padding would loosen footers that are already correct. */
@media (max-width: 767px) {
  .site-footer a, .footer-inline a {
    display: inline-block;
    padding-block: var(--ra-space-1);
  }
  .site-footer ul { margin-block: calc(-1 * var(--ra-space-1)); }

  /* The header's switch link ("I'm a homeowner", "Back to the contractor
     page") on the contractor surfaces. Block-level already, so it only wants
     the padding. */
  .nav-switch { padding-block: var(--ra-space-1); }

  /* A link that is the ONLY thing in its paragraph is not a link in a
     sentence, whatever its markup says, so WCAG's inline exception does not
     cover it. Checked rather than assumed: the homepage's "All questions and
     answers" sits in a <p> whose entire text is "All questions and answers".
     `:only-child` is exactly that condition, so a real inline link in real
     prose is untouched. */
  p > a:only-child {
    display: inline-block;
    padding-block: var(--ra-space-1);
  }
}
.site-footer a:hover { color: var(--ra-text); }
.footer-legal { border-top: 1px solid var(--ra-border); padding-block: var(--ra-space-6); }

/* ── HERO, PATTERN A: content left, supplied 3D composition right ───────
   Founder-directed in START-HERE-WEBSITE.md. The canvas is set to the image's
   own sampled background so there is no visible rectangle where the art sits:
   the PNG is opaque, not a cutout, and its corners measure rgb(249,246,241). */
/* ── CLIPS SIDEWAYS, NOT DOWNWARDS ──────────────────────────────
   This was `overflow: hidden`, and Josh's screenshot shows what that costs: the
   hero's "What are you planning?" picker opens downward and its list was cut
   off at the section's bottom edge, with the next section starting underneath
   the truncated options.

   THE HORIZONTAL CLIP IS LOAD-BEARING AND WAS CHECKED. Removing it entirely
   gives 144px of horizontal overflow at 1440x900, because the hero art is
   deliberately drawn past the right edge of the viewport, which is Josh's own
   earlier ask recorded forty lines below: "So the frame RUNS PAST the viewport
   on the right". The section is what crops it.

   `overflow-x: hidden` would NOT have worked. When one axis is `hidden` the
   other computes to `auto` rather than `visible`, so the list would still be
   trapped, now inside a scroll container.

   `overflow-x: clip` is the one that does. `clip` creates no scroll container,
   and the spec allows it to pair with `overflow-y: visible`, which `hidden`
   does not. So the art is still cropped sideways and the dropdown is free to
   hang below the section. Measured in Chromium and WebKit: horizontal overflow
   stays 0 and the list is no longer truncated. */
.hero { background: var(--ra-site-hero-image-background); overflow-x: clip; overflow-y: visible; }
/* MOBILE ART DIRECTION, not a shrunk desktop. Josh: "mobile placement is not
   optimized. The bathroom image needs to be at the top first, and then the hero
   below." So the art leads, full width and bleeding both edges, and the copy
   follows underneath it. */
.hero-grid { display: grid; gap: var(--ra-space-6); align-items: center;
  padding-block: 0 0; }
.hero-art { order: -1; }
@media (max-width: 999px) {
  .hero-art {
    margin-inline: calc(-1 * var(--ra-site-gutter));
    width: calc(100% + (2 * var(--ra-site-gutter)));
  }
  /* The mobile source is a 4:3 crop of the room, which at full width is tall
     enough to push the primary action off a 768px screen: measured at 932px on
     an 844px fold. Capping the height keeps the action reachable on every
     phone and tablet. `cover` trims ceiling and floor, not fixtures, which was
     checked rather than assumed. */
  .hero-art img {
    width: 100%;
    /* Josh: "a little bit smaller and a little bit lower on mobile, so we can
       see more of it." Shorter frame (36svh rather than 44), and the crop window
       sits higher in the source so the ROOM sits lower in the frame, with the
       floor and the base of the vanity kept rather than trimmed. */
    aspect-ratio: 16 / 10;
    max-height: 36svh;
    object-fit: cover;
    object-position: center 34%;
  }
  .hero-copy { padding-bottom: var(--ra-space-12); }
}
@media (min-width: 1000px) {
  /* ONE LAYER, NOT TWO COLUMNS.
     Josh wanted the bathroom bigger and slightly off the edge. Widening a right
     hand column did that, but it cut straight through the vanity, which the
     imagery spec forbids.
     The master already contains the answer: its left third is empty ivory. So
     the art spans the WHOLE hero and the copy sits on top of that empty third.
     The room itself is far larger, nothing is cropped on the right, and the only
     thing behind the headline is flat background at #F9F6F1. */
  .hero-grid {
    width: 100%; max-width: none;
    padding-left: max(var(--ra-site-gutter),
                      calc((100vw - var(--ra-site-content-max)) / 2 + var(--ra-site-gutter)));
    padding-right: 0;
    grid-template-columns: minmax(0, 33rem) minmax(0, 1fr);
    gap: var(--ra-space-8);
    padding-block: var(--ra-space-12);
    align-items: center;
  }
  .hero-copy { grid-column: 1; grid-row: 1; position: relative; z-index: 1; }
  /* A SOFT FADE, and only because it is measurably necessary. With the room
     spanning the whole hero, the worst 24px tile behind the headline measured
     3.3:1 against the ink at 1920px, which fails AA. A mean reading said 11:1
     and hid it, which is why the check reports the worst tile.
     The fade is the hero's own background colour, opaque where the copy sits
     and gone by the time the room begins, so over the ivory it is invisible and
     over the glass it lifts the contrast back above 4.5:1. */
  /* ── THE FADE HAS NOTHING LEFT TO DO ───────────────────────────────────
     It existed because the art ran the full width and the headline sat on top
     of it: the worst 24px tile behind the ink measured 3.3:1, which fails AA.
     With the art in the second column there is nothing behind the headline but
     flat background, so the gradient would only be dimming the room for no
     reason. `contrast-check.mjs` is what decides this, not an opinion, and it
     is in check-all. */
  .hero-art::after { content: none; }
  .hero-art {
    position: relative;
    /* ── THE SECOND COLUMN, NOT THE WHOLE HERO ───────────────────────────
       It spanned both columns and started at x=0, with the copy laid over its
       empty left third and a gradient painted across that third to hold the
       headline's contrast up. That is what "too far to the left" describes:
       the picture began at the edge of the screen behind the words.

       It starts at the copy column's right edge now. The room is smaller
       because its frame is, the gradient has nothing left to correct because
       there is no art behind the headline, and the only thing that changed
       about the right-hand side is that it runs off it. */
    grid-column: 2 / -1; grid-row: 1; order: 0;
    /* ── OFF THE RIGHT EDGE, NOT TUCKED IN BEHIND IT ─────────────────────
       An earlier pass read "too big for its frame" as "pull the right edge in",
       which left a band of flat ivory between the room and the edge of the
       screen. The room then read as a picture sitting on the page rather than
       as the page.

       Josh, 2026-10-24: "It's too far to the left. It needs to be off the edge
       to the right, and then maybe you can shrink it a little bit."

       So the frame RUNS PAST the viewport on the right, and the room is scaled
       down inside it. The two together are the ask: smaller room, and its right
       side carried off the screen instead of stopping short of it. */
    /* Past the right edge of the viewport, by the gutter plus a little, so the
       room is carried off the screen rather than stopping short of it. */
    /* Past the right edge of the viewport, by a measured amount, so the room is
       carried off the screen rather than stopping short of it. */
    width: calc(100% + max(var(--ra-site-gutter),
                      (100vw - var(--ra-site-content-max)) / 2 + var(--ra-site-gutter))
                     + var(--ra-space-6));
    pointer-events: none;
  }
  /* Shorter too, by the same hand: the corner needs room above it, not only
     beside it. `cover` rather than `fill` so the ratio is honoured while the
     box is no longer exactly the source ratio. */
  .hero-art img {
    width: 100%;
    height: auto;
    /* ── NARROWER THAN THE SOURCE, ON PURPOSE ──────────────────────────
       The master's LEFT THIRD IS EMPTY IVORY. It was drawn that way so the art
       could span the whole hero with the copy over that empty third. With the
       art in the second column, holding the source's 16:9 showed that empty
       third as a band of nothing between the headline and the room.

       A narrower frame plus `cover` anchored right crops the empty third away
       and leaves the room. Nothing of the room is lost on the left: what gets
       cut is the part of the picture that has nothing in it. */
    aspect-ratio: 1.34 / 1;
    /* `cover` with the content anchored LEFT. The master's left third is empty
       ivory and the room lives in its right two thirds, so anchoring left and
       letting the frame overrun the viewport is what carries the room off the
       right edge. Anchoring right would crop the room's left wall instead. */
    object-fit: cover;
    object-position: right center;
  }
}
.hero-copy { display: grid; gap: var(--ra-space-6); }
/* 16ch broke "What will your new bathroom cost?" across three lines with one
   word on the last. `balance` evens the lines out instead of leaving an orphan,
   and the slightly wider cap lets it settle at two. */
.hero-copy h1 { max-width: 22ch; text-wrap: balance; }
.hero-art { position: relative; }
/* Natural width first, as the spec instructs, rather than object-fit: cover,
   which would crop the right-hand room silhouette. */
.hero-art img { width: 100%; height: auto; }
/* A soft mask on the BOTTOM EDGE ONLY. Measured against the hero canvas, the
   top, left and right edges differ by at most 3/255 and are invisible, but the
   floor shadow reaches the bottom edge and steps by up to 22/255, which reads as
   a rectangle. The fade covers the last 9% of the height, which is floor and
   shadow: no fixture, no room silhouette and no right-hand wall is touched.
   The colour is irrelevant to a mask, only its alpha, so it uses a token. */
.hero-art img {
  -webkit-mask-image: linear-gradient(to bottom, var(--ra-offblack) 91%, transparent 100%);
  mask-image: linear-gradient(to bottom, var(--ra-offblack) 91%, transparent 100%);
}

/* ── NOT FLEX ───────────────────────────────────────────────────────────────
   It was `display: flex; gap: space-2`, which makes every text node and the
   link their own flex item with a gap between them. "Explore ideas first." then
   rendered as "Explore ideas first ." with the full stop floating away from the
   sentence. A paragraph of prose is a paragraph. */
.hero-note { margin: 0; }

/* ── DUAL INTENT, equal legitimacy ──────────────────────────────────────
   The handoff: "friendly equal-legitimacy messaging; do not treat exploration
   as a hidden refusal." So both routes are the same size and weight. */
.paths { display: grid; gap: var(--ra-space-4); }
@media (min-width: 760px) { .paths { grid-template-columns: 1fr 1fr; gap: var(--ra-space-6); } }
.path {
  display: grid; gap: var(--ra-space-3); align-content: start;
  padding: var(--ra-space-8); background: var(--ra-surface);
  border: 1px solid var(--ra-border); border-radius: var(--ra-radius-lg);
  text-decoration: none; color: inherit;
  transition: border-color var(--ra-site-motion-base) var(--ra-site-ease-standard),
              box-shadow var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.path:hover { border-color: var(--ra-brand); box-shadow: var(--ra-shadow-md); }
.path h3 { color: var(--ra-heading); }
.path .go { color: var(--ra-brand); font-weight: var(--ra-fw-label); font-size: var(--ra-text-label); }

/* ── NUMBERED PROCESS, hairlines rather than boxes ──────────────────────── */
.steps { display: grid; max-width: 900px; }
.step { display: grid; gap: var(--ra-space-3) var(--ra-space-8);
  padding-block: var(--ra-space-10); border-top: 1px solid var(--ra-border); }
.steps .step:last-child { border-bottom: 1px solid var(--ra-border); }
@media (min-width: 860px) { .step { grid-template-columns: 4rem 1fr 1.1fr; align-items: start; } }
.step-n { font-family: var(--ra-font-display); font-size: var(--ra-text-h2);
  font-weight: var(--ra-fw-h2); color: var(--ra-early-600); line-height: 1; }
.step-media { border-radius: var(--ra-radius-md); overflow: hidden; }
/* A third column of quiet reassurance rather than a third image. */
.step-aside { color: var(--ra-text-muted); }
@media (min-width: 860px) { .steps-wide .step { grid-template-columns: 4rem 1fr 18rem; } }

/* The tiles now hold photography. A gentle scale on hover, clipped by the
   rounded container, is the whole interaction: no overlay, no caption slide. */
.tile-media img { width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; }

/* ── OPTION TILES, image-led ────────────────────────────────────────────── */
.tiles { display: grid; gap: var(--ra-space-6); }
@media (min-width: 760px) { .tiles { grid-template-columns: repeat(3, 1fr); } }
.tile { display: grid; gap: var(--ra-space-4); text-decoration: none; color: inherit; }
/* ── THE WHITE BAR UNDER TWO OF THREE PHOTOS ────────────────────────────────
   The tiles sit in a grid with equal row heights, and `.tile-media` is the
   first row of each tile, so it STRETCHED to whatever height was left over.
   The image inside keeps its 4:3 ratio, so on the two tiles whose heading fits
   on one line the box grew 17px taller than the photo and those 17px showed as
   a pale bar of the container's own background. The third tile's heading wraps
   to two lines, which used up the slack, which is why only two of the three had
   it and the bug looked arbitrary.

   `align-self: start` stops the box stretching, so it is exactly as tall as the
   photo on every tile. The leftover space goes to the text row instead, which
   is where it was always meant to go. Stretching the IMAGE to fill instead
   would have fixed the bar and left the three photos at different heights. */
.tile-media { border-radius: var(--ra-radius-lg); overflow: hidden; background: var(--ra-stone-100); align-self: start; }
.tile-media img, .tile-media .ph { transition: transform var(--ra-site-motion-base) var(--ra-site-ease-standard); }
/* The card is no longer a link, so the image zoom follows the two links inside
   it rather than a hover on the card. Pointing at the card does nothing now,
   which is correct: there is nothing to click on the card itself. */
.tile:focus-within .tile-media img, .tile-actions a:hover ~ .tile-media img { transform: scale(1.02); }
.tile:hover .tile-media img, .tile:hover .tile-media .ph { transform: scale(1.02); }
/* A plain two-up grid for short benefit blocks. `auto-fit` rather than a
   breakpoint, for the reason the portal's fact grid needed it: this sits inside
   a content column, and a viewport query would size it by the window instead of
   by the space it actually has. */
.benefits { display: grid; gap: var(--ra-space-6) var(--ra-space-8); grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }

.tile-actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--ra-space-3) var(--ra-space-5);
  margin-top: var(--ra-space-2);
}
.tile .go { color: var(--ra-brand); font-weight: var(--ra-fw-label); font-size: var(--ra-text-label); }

/* ── FAQ, native disclosure so it works without script ──────────────────── */
/* The container is now the full content width on every page so sections share a
   left edge. Reading measure is set on the CONTENT instead, which is what the UI
   overrides ask for: an open layout with 55 to 70 characters of copy. */
.faq { display: grid; max-width: 820px; }
.faq details { border-top: 1px solid var(--ra-border); }
.faq details:last-of-type { border-bottom: 1px solid var(--ra-border); }
.faq summary { display: flex; justify-content: space-between; align-items: center;
  gap: var(--ra-space-5); padding-block: var(--ra-space-6); cursor: pointer;
  font-family: var(--ra-font-display); font-size: var(--ra-text-h4);
  font-weight: var(--ra-fw-h4); color: var(--ra-heading); list-style: none; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; font-family: var(--ra-font-ui); font-size: 1.4em;  /* off-scale: a glyph sized relative to its own line, so it tracks the summary text */
  color: var(--ra-text-muted); line-height: 1; flex: none; }
.faq details[open] summary::after { content: "\2212"; }
/* The audit measured the FAQ reaching full height within 70ms: a native snap.
   A `grid-template-rows: 0fr → 1fr` transition cannot fix it on its own, because
   a closed <details> does not render its content at all, so there is no start
   state to animate from. `::details-content` is the modern answer where it
   exists; `web/faq.mjs` animates the height where it does not, and the native
   snap remains the floor when neither runs. */
.faq .answer > :first-child { padding-bottom: var(--ra-space-6); }
.faq summary::after { transition: transform var(--ra-site-motion-base) var(--ra-site-ease-standard); }
.faq details[open] summary::after { transform: rotate(180deg); }

@supports selector(details::details-content) {
  .faq details::details-content {
    block-size: 0; overflow: hidden;
    transition: block-size var(--ra-site-motion-base) var(--ra-site-ease-standard),
                content-visibility var(--ra-site-motion-base) allow-discrete;
  }
  .faq details[open]::details-content { block-size: auto; }
  /* `auto` is only animatable when keyword interpolation is enabled. */
  .faq details { interpolate-size: allow-keywords; }
}

/* The JavaScript path sets an explicit height, so it needs no keyword support. */
.faq details[data-animating] .answer { overflow: hidden; }

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

/* ── IMAGE PLACEHOLDERS ─────────────────────────────────────────────────
   Named source placeholders with a stated requirement, per the imagery spec:
   "where missing, keep named source placeholders and a small asset requirement
   list rather than inventing low-quality stock photos." */
.ph { position: relative; background: var(--ra-stone-100);
  background-image: repeating-linear-gradient(135deg, var(--ra-stone-200) 0 10px, transparent 10px 20px);
  display: grid; place-items: end start; }
/* The rule that printed `data-asset` on the page is DELETED. It put an internal
   production note ("ASSET · Photoreal walk-in shower, 4:3 at 1200x900") on two
   live consumer pages. Nothing may render a brief to a customer, so the ability
   to do it is gone rather than merely unused. */
.ph-16x9 { aspect-ratio: 16 / 9; } .ph-4x3 { aspect-ratio: 4 / 3; }
.ph-3x2 { aspect-ratio: 3 / 2; } .ph-1x1 { aspect-ratio: 1 / 1; } .ph-4x5 { aspect-ratio: 4 / 5; }

/* ── MOTION ─────────────────────────────────────────────────────────────
   THE REVEAL IS GATED ON `html.js`. An audit of the live site found 13 sections
   at opacity 0 with JavaScript disabled, because `.reveal` is written into the
   markup and the stylesheet hid it unconditionally. A tiny inline script in
   <head> adds `js` before first paint, so with no script the class never
   applies and every section is simply visible. */
.js .reveal {
  opacity: 0; transform: translateY(10px);
  transition: opacity var(--ra-site-motion-reveal) var(--ra-site-ease-enter),
              transform var(--ra-site-motion-reveal) var(--ra-site-ease-enter);
  will-change: opacity, transform;
}
.js .reveal.is-in { opacity: 1; transform: none; will-change: auto; }
.js .reveal-hero { transition-duration: var(--ra-site-motion-hero); }
/* A stagger measured in tens of milliseconds, never per word. */
.js .reveal[data-delay="1"] { transition-delay: 70ms; }
.js .reveal[data-delay="2"] { transition-delay: 140ms; }
.js .reveal[data-delay="3"] { transition-delay: 210ms; }

@media (prefers-reduced-motion: reduce) {
  .js .reveal, .js .reveal[data-delay] {
    opacity: 1 !important; transform: none !important;
    transition: none !important; transition-delay: 0ms !important;
  }
}

/* ── FORMS ──────────────────────────────────────────────────────────────
   Per the UI overrides: whole target clickable, a true semantic input, a strong
   selected border AND an indicator, state and error clear without relying on
   colour alone, every interactive state preserved. */
.field { display: grid; gap: var(--ra-space-2); }
.field > label, .field > .field-label {
  font-size: var(--ra-text-label); line-height: var(--ra-lh-label);
  font-weight: var(--ra-fw-label); color: var(--ra-text);
}
.field .hint { font-size: var(--ra-text-small); color: var(--ra-text-muted); }
/* An allowlist of input types left `type="password"` rendering as a bare
   browser box next to a styled email field on the portal sign-in page, and it
   would have done the same to the next type anybody added. Inverted: every
   input is a text field unless it is one of the kinds that is deliberately
   something else. The radios inside `.choice` are excluded here and hidden
   deliberately further down. */
/* ── `.otp-box` IS EXCLUDED, NOT OVERRIDDEN ────────────────────────────────
   The four-digit code boxes are fixed-width squares. Styling them by class
   alone does NOT work and I found that out by looking at the rendered page:
   this selector carries nine attribute selectors, so its specificity is far
   above a single class, and `width: 100%` won. Each box filled the row and
   the four of them overflowed the viewport sideways.

   Fighting it with ten chained classes would be worse than the problem. The
   exclusion list above is this stylesheet's existing answer to "this input is
   deliberately a different kind of thing", so the code boxes join it. */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="color"]):not([type="image"]):not(.otp-box),
select, textarea {
  width: 100%; min-height: 48px; font: inherit; color: var(--ra-text);
  background: var(--ra-surface); padding: var(--ra-space-3) var(--ra-space-4);
  border: 1px solid var(--ra-border-input); border-radius: var(--ra-radius-md);
  transition: border-color var(--ra-site-motion-fast) var(--ra-site-ease-standard);
}
input:hover, select:hover, textarea:hover { border-color: var(--ra-secondary); }
input[aria-invalid="true"], select[aria-invalid="true"] { border-color: var(--ra-error-600); border-width: 2px; }

/* Choice cards: a real radio, visually hidden but focusable, with the whole
   card as the label. The indicator is a shape as well as a colour. */
.choices { display: grid; gap: var(--ra-space-3); }
@media (min-width: 680px) { .choices.cols-2 { grid-template-columns: 1fr 1fr; } }
@media (min-width: 860px) { .choices.cols-3 { grid-template-columns: repeat(3, 1fr); } }
.choice {
  position: relative; display: grid; grid-template-columns: 22px 1fr;
  gap: var(--ra-space-2) var(--ra-space-4); align-items: start;
  padding: var(--ra-space-4) var(--ra-space-5); cursor: pointer;
  background: var(--ra-surface); border: 1px solid var(--ra-border-input);
  border-radius: var(--ra-radius-md); min-height: 48px;
  transition: border-color var(--ra-site-motion-fast) var(--ra-site-ease-standard),
              background-color var(--ra-site-motion-fast) var(--ra-site-ease-standard);
}
.choice input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.choice .box {
  grid-column: 1; grid-row: 1; width: 20px; height: 20px; margin-top: 2px;
  border: 1px solid var(--ra-border-input); background: var(--ra-surface);
  display: grid; place-items: center; flex: none;
}
.choice[data-type="radio"] .box { border-radius: var(--ra-radius-full); }
.choice[data-type="check"] .box { border-radius: var(--ra-radius-xs); }
.choice .box::after { content: ""; width: 10px; height: 10px; transform: scale(0);
  background: var(--ra-selected-border);
  transition: transform var(--ra-site-motion-fast) var(--ra-site-ease-standard); }
.choice[data-type="radio"] .box::after { border-radius: var(--ra-radius-full); }
.choice .t { grid-column: 2; grid-row: 1; font-weight: var(--ra-fw-label); }
.choice .d { grid-column: 2; grid-row: 2; font-size: var(--ra-text-small); color: var(--ra-text-muted); }
.choice:hover { border-color: var(--ra-secondary); }
.choice:has(input:checked) { border-color: var(--ra-selected-border); border-width: 2px;
  background: var(--ra-selected-bg); padding: calc(var(--ra-space-4) - 1px) calc(var(--ra-space-5) - 1px); }
.choice:has(input:checked) .box { border-color: var(--ra-selected-border); border-width: 2px; }
.choice:has(input:checked) .box::after { transform: scale(1); }
.choice:has(input:focus-visible) { box-shadow: var(--ra-focus-ring); }

/* Status: conveyed with text and an icon, never colour alone. */
.notice { display: grid; gap: var(--ra-space-2); padding: var(--ra-space-5); max-width: 820px;
  border-radius: var(--ra-radius-md); border: 1px solid; }
/* Any heading in a notice, not one tag: the level is semantic and follows the
   notice's position on the page, while the size stays the same either way. */
.notice :is(h2, h3) { font-size: var(--ra-text-h4); }
.notice-error { background: var(--ra-error-100); border-color: var(--ra-error-600); color: var(--ra-error-600); }
.notice-error :is(h2, h3), .notice-error li, .notice-error p { color: var(--ra-error-600); }
.notice-success { background: var(--ra-success-100); border-color: var(--ra-success-600); }
.notice-info { background: var(--ra-info-100); border-color: var(--ra-info-600); }
.notice-warning { background: var(--ra-warning-100); border-color: var(--ra-warning-700); }
.notice ul { margin: 0; padding-left: var(--ra-space-5); font-size: var(--ra-text-small); }

/* Progress: a step count in words as well as a bar. */
.progress { display: flex; align-items: center; gap: var(--ra-space-3);
  font-size: var(--ra-text-caption); color: var(--ra-text-muted); }
/* ── ONE CONTINUOUS FILL, NOT THREE SEGMENTS ──────────────────────
   This was three equal segments, each filling via a scaleX on ::after when it
   got a `.done` class, under a label reading "Stage 1 of 3". Josh called the
   three-stage framing deceiving, because the questionnaire inside stage one was
   separately printing "Step 4 of 12" and the lede promised "Three short steps".

   The segments are gone and the track holds a single fill whose width is the
   real fraction through the whole journey: every question, then the review,
   then the details. find-a-professional.html is the only page that uses this
   component and nothing used the `.done` segment pattern any more, so this
   replaces it rather than sitting beside it.

   A width transition rather than a transform, because the fill has to stop at
   an arbitrary percentage rather than at the end of a fixed segment. */
.progress .track {
  flex: 1; block-size: 3px; overflow: hidden;
  background: var(--ra-stone-200);
  border-radius: var(--ra-radius-full);
}
.progress .track i {
  display: block; block-size: 100%; inline-size: 0;
  background: var(--ra-brand);
  border-radius: var(--ra-radius-full);
  transition: inline-size var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
@media (prefers-reduced-motion: reduce) { .progress .track i { transition: none; } }

/* The audit measured step 2 at opacity 1 and transform none at both 60ms and
   480ms. A step now arrives from the direction you travelled, which is the one
   piece of motion in a form that carries meaning: forward or back. */
.form-step { display: grid; gap: var(--ra-space-6); }
.js .form-step[data-enter] { animation: step-in var(--ra-site-motion-base) var(--ra-site-ease-enter) both; }
.js .form-step[data-enter="back"] { animation-name: step-in-back; }
@keyframes step-in { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; transform: none; } }
@keyframes step-in-back { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .js .form-step[data-enter] { animation: none; }
}
.form-nav { display: flex; gap: var(--ra-space-3); flex-wrap: wrap; align-items: center;
  padding-top: var(--ra-space-2); }

/* The confirmation recap: what was asked for, on hairlines. */
.recap { margin: 0; display: grid; }
.recap-row { display: grid; gap: var(--ra-space-1) var(--ra-space-6);
  padding-block: var(--ra-space-4); border-top: 1px solid var(--ra-border); }
.recap-row:last-child { border-bottom: 1px solid var(--ra-border); }
@media (min-width: 560px) { .recap-row { grid-template-columns: 10rem 1fr; align-items: baseline; } }
.recap dt { font-size: var(--ra-text-small); color: var(--ra-text-muted); }
.recap dd { margin: 0; font-weight: var(--ra-fw-label); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE PLANNER
   Four tools and a summary, over one plan object. Layout is a rail plus a
   column on desktop and a single column on phones, because a progress rail
   beside a form at 390px leaves neither enough room.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The no-JavaScript block is the page until the planner has actually rendered.
   It is hidden on `js` so there is no flash, and plan.html puts it back if the
   module never arrives, so a failed script shows real content rather than a
   permanent loading state. */
html.js #plan-fallback { display: none; }
html.js #plan-app:empty::before {
  content: "Opening your planner";
  display: block;
  padding: var(--ra-space-12) 0;
  color: var(--ra-text-muted);
  font-size: var(--ra-text-small);
}

.section-plan { padding-block: var(--ra-space-10) var(--ra-space-16); }

/* ── A HEADING FOCUSED BY SCRIPT SHOWS NO RING, ANYWHERE (2026-10-25) ────────
   The heading is focused on every view change so a screen reader lands on it.
   A full-width focus box around a heading reads as a text field to everyone
   else, and the live region already announces the change, so the ring is
   suppressed for this programmatic focus.

   This used to name `.plan-main` only, and the same pattern had since been used
   in three more places: the signup wizard focuses each step's `.su-h2`, the
   signup outcome focuses its `h3`, and the order wizard focuses
   `.pt-step-heading`. So Josh saw a green box appear around "Where you work"
   and around "What should we look for?" and asked for it to go: "I just want to
   get rid of the outline thing that we've been seeing all throughout the site."

   ── WHY THIS IS NOT `outline: none` ON EVERYTHING ──────────────────────────
   That ring is the focus indicator and it is how somebody using a keyboard
   knows where they are. Removing it outright would fail `a11y-check.mjs` and
   make the forms unusable without a mouse, which is a worse bug than the one
   being fixed.

   `[tabindex="-1"]` is the whole safety argument: an element with it is NOT
   reachable by keyboard, so no keyboard user can ever arrive there by tabbing.
   The only way focus lands on it is a script putting it there, which is exactly
   the case where the ring is noise. Anything a person can tab to keeps its
   ring, untouched.

   Scoped rather than applied to every `[tabindex="-1"]`, because the portal also
   gives a focused LEAD panel that attribute and a ring around the thing somebody
   just acted on is useful there.

   ── THE NAV SHEET IS ON THE LIST TOO (2026-10-25) ─────────────────────────
   Opening the mobile menu moves focus into it, or a keyboard user is left
   behind it on the burger. It used to focus the first LINK, which is
   tab-reachable, so the ring was drawn around "How it works": the green outline
   Josh reported for the third time. `shell.mjs` focuses the sheet itself now,
   and the sheet needs the same suppression a heading gets, for the same reason:
   nothing can tab to it, so the only focus it ever receives is a script's. */
:where(h1, h2, h3, h4)[tabindex="-1"]:focus,
.nav-sheet[tabindex="-1"]:focus { outline: none; box-shadow: none; }

.plan-layout { display: grid; gap: var(--ra-space-8); }
.plan-main { min-width: 0; }
@media (min-width: 1000px) {
  .plan-layout:not([data-view="start"]) {
    grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    gap: var(--ra-space-12);
    align-items: start;
  }
  .plan-rail { position: sticky; top: var(--ra-space-20); }
}

/* ── the step rail ──────────────────────────────────────────────────────── */

.plan-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--ra-space-1); }
.plan-step a {
  display: grid; grid-template-columns: 1.75rem 1fr; align-items: center;
  gap: var(--ra-space-3);
  padding: var(--ra-space-3);
  border-radius: var(--ra-radius-md);
  text-decoration: none; color: var(--ra-text);
  transition: background var(--ra-dur-fast) var(--ra-ease-standard);
}
.plan-step a:hover { background: var(--ra-surface-tint); }
.plan-step-num {
  display: grid; place-items: center;
  width: 1.75rem; height: 1.75rem;
  border-radius: var(--ra-radius-full);
  border: 1px solid var(--ra-border-input);
  font-size: var(--ra-text-caption); font-variant-numeric: tabular-nums;
}
.plan-step-label { font-size: var(--ra-text-small); }
.plan-step[data-state="done"] .plan-step-num {
  background: var(--ra-selected-bg); border-color: var(--ra-selected-border); color: var(--ra-brand);
}
.plan-step[data-state="here"] a { background: var(--ra-surface-tint); }
.plan-step[data-state="here"] .plan-step-num {
  background: var(--ra-cta); border-color: var(--ra-cta); color: var(--ra-cta-text);
}
.plan-step[data-state="here"] .plan-step-label { font-weight: var(--ra-fw-label); }
.plan-rail-note { margin-top: var(--ra-space-4); }

@media (max-width: 999px) {
  .plan-steps { grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0; }
  .plan-step a { grid-template-columns: 1fr; justify-items: center; text-align: center; gap: var(--ra-space-1); padding: var(--ra-space-2); }
  .plan-step-label { font-size: var(--ra-text-caption); }
  .plan-rail { border-bottom: 1px solid var(--ra-border); padding-bottom: var(--ra-space-4); }
}

/* ── the start cards ────────────────────────────────────────────────────── */

.plan-cards { display: grid; gap: var(--ra-space-4); }
@media (min-width: 700px) { .plan-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.plan-card {
  display: block; padding: var(--ra-space-6);
  border: 1px solid var(--ra-border); border-radius: var(--ra-radius-lg);
  background: var(--ra-surface); text-decoration: none; color: var(--ra-text);
  transition: border-color var(--ra-dur-fast) var(--ra-ease-standard),
              transform var(--ra-dur-fast) var(--ra-ease-standard);
}
.plan-card:hover { border-color: var(--ra-secondary); transform: translateY(-2px); }
.plan-card[data-done="true"] { border-color: var(--ra-selected-border); background: var(--ra-selected-bg); }
.plan-card-num {
  display: inline-grid; place-items: center;
  width: 1.5rem; height: 1.5rem; margin-bottom: var(--ra-space-3);
  border-radius: var(--ra-radius-full); background: var(--ra-surface-tint);
  font-size: var(--ra-text-caption); font-variant-numeric: tabular-nums;
}
.plan-card :is(h2, h3) { margin: 0 0 var(--ra-space-2); font-size: var(--ra-text-h4); }

/* ── questions ──────────────────────────────────────────────────────────── */

.plan-q { border: 0; padding: 0; margin: 0 0 var(--ra-space-10); }
.plan-q legend { padding: 0; }
.plan-q legend h3 { margin: 0 0 var(--ra-space-2); }
.plan-q-help { color: var(--ra-text-muted); margin: 0 0 var(--ra-space-4); max-width: var(--ra-site-copy-measure); }
.plan-q-count { margin-top: var(--ra-space-3); }

.choices-swatch .choice { grid-template-columns: auto 1fr auto; }
.swatch { grid-column: 3; grid-row: 1 / span 2; display: flex; gap: 2px; align-self: center; }
.swatch i {
  display: block; width: 1.25rem; height: 2.25rem;
  border-radius: var(--ra-radius-xs);
  border: 1px solid var(--ra-border);
}
.swatch i:first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.swatch i:last-child { border-top-left-radius: 0; border-bottom-left-radius: 0; }

/* ── derived output and estimates ───────────────────────────────────────── */

.plan-output {
  padding: var(--ra-space-6);
  border-left: 3px solid var(--ra-selected-border);
  background: var(--ra-surface-tint);
  border-radius: var(--ra-radius-md);
}
.plan-output h3 { margin: 0 0 var(--ra-space-2); font-size: var(--ra-text-h4); }
.plan-output p { margin: 0 0 var(--ra-space-2); }
.plan-output p:last-child { margin-bottom: 0; }

.plan-estimate {
  padding: var(--ra-space-8);
  border: 1px solid var(--ra-border);
  border-radius: var(--ra-radius-lg);
  background: var(--ra-surface);
}
.plan-estimate-range {
  margin: var(--ra-space-2) 0 0;
  font-family: var(--ra-font-display);
  font-size: var(--ra-text-h1);
  line-height: 1.05;
  color: var(--ra-heading);
  font-variant-numeric: tabular-nums;
}
.plan-estimate > .small { color: var(--ra-text-muted); }

.plan-lines { margin: var(--ra-space-6) 0; display: grid; gap: var(--ra-space-2); }
.plan-lines > div { display: flex; justify-content: space-between; gap: var(--ra-space-4); }
.plan-lines dt, .plan-lines dd { margin: 0; font-size: var(--ra-text-small); }
.plan-lines dd { font-variant-numeric: tabular-nums; }
.plan-lines-total {
  border-top: 1px solid var(--ra-border);
  padding-top: var(--ra-space-2);
  font-weight: var(--ra-fw-label);
}
.plan-lines-total dt, .plan-lines-total dd { font-size: var(--ra-text-body); }

.plan-details { border-top: 1px solid var(--ra-border); }
.plan-details summary {
  padding: var(--ra-space-4) 0; cursor: pointer;
  font-size: var(--ra-text-small); font-weight: var(--ra-fw-label);
}
.plan-details summary:focus-visible { box-shadow: var(--ra-focus-ring); border-radius: var(--ra-radius-xs); }
.plan-details-body { padding-bottom: var(--ra-space-4); }
.plan-details-body p { margin: 0 0 var(--ra-space-2); }
.plan-sources { margin: var(--ra-space-2) 0 0; padding-left: var(--ra-space-5); }
.plan-sources li { margin-bottom: var(--ra-space-1); color: var(--ra-text-muted); }

/* ── materials comparison ───────────────────────────────────────────────── */

.plan-compare { margin-bottom: var(--ra-space-12); }
.plan-compare h3 { margin: 0 0 var(--ra-space-2); }
.plan-compare-grid { display: grid; gap: var(--ra-space-4); }
@media (min-width: 760px) { .plan-compare-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1200px) { .plan-compare-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.plan-option {
  display: flex; flex-direction: column; gap: var(--ra-space-3);
  padding: var(--ra-space-5);
  border: 1px solid var(--ra-border); border-radius: var(--ra-radius-md);
  background: var(--ra-surface);
}
.plan-option[data-on="true"] { border-color: var(--ra-selected-border); border-width: 2px; padding: calc(var(--ra-space-5) - 1px); }
.plan-option h4 { margin: 0; font-size: var(--ra-text-h4); }
.plan-option-cost { margin: 0; display: flex; align-items: center; gap: var(--ra-space-2); font-size: var(--ra-text-caption); color: var(--ra-text-muted); }
.plan-cost-pips { display: inline-flex; gap: 2px; }
.plan-cost-pips i { width: 6px; height: 6px; border-radius: var(--ra-radius-full); background: var(--ra-stone-200); }
.plan-cost-pips i[data-fill="true"] { background: var(--ra-accent); }
.plan-option-facts { margin: 0; display: grid; gap: var(--ra-space-2); flex: 1; }
.plan-option-facts dt { margin: 0; font-size: var(--ra-text-caption); color: var(--ra-text-muted); text-transform: uppercase; letter-spacing: 0.06em  /* off-scale: label tracking, one value for every uppercase caption; the kit names no tracking scale */; }
.plan-option-facts dd { margin: 0; font-size: var(--ra-text-small); }
.plan-option .btn { align-self: start; }

/* ── readiness and process ──────────────────────────────────────────────── */

.plan-blockers { margin: var(--ra-space-2) 0 0; padding-left: var(--ra-space-5); }
.plan-blockers li { margin-bottom: var(--ra-space-2); }
.plan-process { margin-top: var(--ra-space-12); }
.plan-stages { margin: var(--ra-space-4) 0 0; padding-left: var(--ra-space-6); display: grid; gap: var(--ra-space-5); }
.plan-stages h4 { margin: 0 0 var(--ra-space-1); font-size: var(--ra-text-h4); }
.plan-stages p { margin: 0; max-width: var(--ra-site-copy-measure); }

/* ── the summary sheet ──────────────────────────────────────────────────── */

.plan-sheet {
  border: 1px solid var(--ra-border); border-radius: var(--ra-radius-lg);
  background: var(--ra-surface); overflow: hidden;
}
.plan-sheet-head { padding: var(--ra-space-6); border-bottom: 1px solid var(--ra-border); background: var(--ra-surface-tint); }
.plan-sheet-head h3 { margin: var(--ra-space-2) 0; }
.plan-sheet-body { margin: 0; padding: var(--ra-space-6); display: grid; gap: var(--ra-space-4); }
.plan-sheet-body > div { display: grid; gap: var(--ra-space-1); }
@media (min-width: 700px) { .plan-sheet-body > div { grid-template-columns: 11rem minmax(0, 1fr); gap: var(--ra-space-5); } }
.plan-sheet-body dt { margin: 0; font-size: var(--ra-text-caption); color: var(--ra-text-muted); text-transform: uppercase; letter-spacing: 0.06em  /* off-scale: label tracking, one value for every uppercase caption; the kit names no tracking scale */; }
.plan-sheet-body dd { margin: 0; }

.plan-measure { display: grid; gap: var(--ra-space-4); }
@media (min-width: 560px) { .plan-measure { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.plan-actions { display: flex; flex-wrap: wrap; gap: var(--ra-space-3); }

.plan-offer {
  padding: var(--ra-space-8);
  border-radius: var(--ra-radius-lg);
  background: var(--ra-selected-bg);
  border: 1px solid var(--ra-selected-border);
}
.plan-offer h3 { margin: 0 0 var(--ra-space-2); }
.plan-offer .btn-row { margin-top: var(--ra-space-5); }
.plan-offer-quiet { background: var(--ra-surface-tint); border-color: var(--ra-border); }
.plan-prompt { color: var(--ra-text-muted); margin-bottom: var(--ra-space-4); }
.plan-footer { margin-top: var(--ra-space-8); padding-top: var(--ra-space-6); border-top: 1px solid var(--ra-border); }
.plan-brief { white-space: pre-wrap; }
.plan-storage-warning { margin-bottom: var(--ra-space-4); }

/* ── printing, which is the save-as-PDF route until email is connected ──── */

@media print {
  .site-header, .site-footer, .no-print, .plan-rail, #plan-fallback { display: none !important; }
  .section-plan { padding: 0; }
  .plan-layout { display: block; }
  .plan-sheet { border: 1px solid var(--ra-border); break-inside: avoid; }
  .plan-sheet-head { background: none; }
  .plan-summary > .stack > .lede { display: none; }
  a[href^="#"]::after { content: ""; }
}

/* ── the planner's glyphs ───────────────────────────────────────────────────
   One stroke weight, one grid, inheriting the text colour, so a selected card
   tints its own icon. Every glyph is decorative: the label beside it carries
   the meaning, which is why they are all aria-hidden. */

.plan-icon {
  display: inline-grid;
  place-items: center;
  flex: none;
  color: var(--ra-stone-600);
  transition: color var(--ra-dur-fast) var(--ra-ease-standard),
              background var(--ra-dur-fast) var(--ra-ease-standard);
}
.plan-icon svg { width: 24px; height: 24px; display: block; }

/* In a choice card the glyph gets a container, because an unbacked 24px stroke
   beside a checkbox and two lines of text reads as a smudge rather than as a
   picture of anything. At 44px with a tint it is legible at arm's length on a
   phone, which is the only test that counts. */
.choice-icon .plan-icon {
  width: 44px;
  height: 44px;
  border-radius: var(--ra-radius-md);
  background: var(--ra-surface-tint);
}
.choice-icon .plan-icon svg { width: 26px; height: 26px; }

/* On a heading a bare 20px stroke next to 24px type reads as a speck, so the
   question glyph gets the same container the cards use, one step smaller, and
   acts as a section marker rather than as decoration. */
.plan-q legend h3,
.plan-stages h4 { display: flex; align-items: center; gap: var(--ra-space-3); }
.plan-q legend h3 .plan-icon {
  width: 36px;
  height: 36px;
  border-radius: var(--ra-radius-md);
  background: var(--ra-selected-bg);
  color: var(--ra-brand);
}
.plan-q legend h3 .plan-icon svg { width: 22px; height: 22px; }
.plan-stages h4 .plan-icon { color: var(--ra-accent); }
.plan-stages h4 .plan-icon svg { width: 22px; height: 22px; }

/* Inside a button the glyph takes the button's own colour, not the muted grey. */
.btn .plan-icon { color: currentColor; margin-right: calc(-1 * var(--ra-space-1)); }
.btn .plan-icon svg { width: 18px; height: 18px; }

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

/* ═══════════════════════════════════════════════════════════════════════════
   THE FOUR CARD SHAPES, from the planner asset sheet
   ═══════════════════════════════════════════════════════════════════════════

   card         tall media card, icon badge and control over the media
   row          icon left, text, thumbnail right
   row-swatch   text and swatches left, thumbnail right
   tile         media fills the card, label beneath

   The media panel holds a PHOTOGRAPH when one exists and the GLYPH on a tint
   when it does not, at the same aspect ratio either way, so a file landing
   changes the picture and never the layout.
   ═══════════════════════════════════════════════════════════════════════════ */

.plan-media {
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: var(--ra-radius-md);
  background: var(--ra-surface-tint);
}
.plan-media[data-media="photo"] { background: var(--ra-stone-100); }
.plan-media .plan-thumb { width: 100%; height: 100%; object-fit: cover; }
/* With no photograph the glyph carries the panel, so it is drawn much larger
   than it would be beside a label. */
.plan-media[data-media="glyph"] .plan-icon { width: auto; height: auto; background: none; color: var(--ra-stone-400); }
.plan-media[data-media="glyph"] .plan-icon svg { width: 44%; height: auto; min-width: 34px; }
.choice:hover .plan-media[data-media="glyph"] .plan-icon { color: var(--ra-secondary); }
.choice:has(input:checked) .plan-media[data-media="glyph"] .plan-icon { color: var(--ra-brand); }

/* The icon badge: a white tile over the top-left of a photograph. */
.plan-badge {
  display: inline-grid;
  place-items: center;
  width: 44px; height: 44px;
  flex: none;
  border-radius: var(--ra-radius-md);
  background: var(--ra-surface-tint);
  color: var(--ra-stone-600);
  transition: color var(--ra-dur-fast) var(--ra-ease-standard),
              background var(--ra-dur-fast) var(--ra-ease-standard);
}
.plan-badge .plan-icon { background: none; color: inherit; width: auto; height: auto; }
.plan-badge .plan-icon svg { width: 26px; height: 26px; }
.choice:hover .plan-badge { color: var(--ra-secondary); }
.choice:has(input:checked) .plan-badge { color: var(--ra-brand); background: var(--ra-white); }

/* ── card: the project type ─────────────────────────────────────────────── */

/* `stretch`, not `start`: five cards whose descriptions run to a different
   number of lines were five different heights in one row, which reads as five
   different components rather than one choice. The third row absorbs the slack
   so the media panels all align along the top. */
/* `auto-fit` with a minimum produced four cards and one orphan at the width the
   planner actually has, which reads as a mistake rather than as a wrap. The
   column count is set explicitly instead, so every breakpoint is a shape
   somebody chose: five in a row when there is genuinely room, three and two when
   there is not, one on a phone. */
.choices-card { align-items: stretch; }
@media (min-width: 640px) { .choices-card { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1240px) { .choices-card { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.choice-card {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto auto 1fr;
  gap: var(--ra-space-3);
  position: relative;
  padding: var(--ra-space-3);
}
.choice-card .plan-media { grid-row: 1; width: 100%; }
/* Over the media, not beside it, which is what the asset sheet shows. */
.choice-card .plan-badge { position: absolute; top: var(--ra-space-5); left: var(--ra-space-5); background: var(--ra-white); }
.choice-card .box { position: absolute; top: var(--ra-space-5); right: var(--ra-space-5); background: var(--ra-white); }
.choice-card .t { grid-row: 2; grid-column: 1; }
.choice-card .d { grid-row: 3; grid-column: 1; }

/* ── row: the day-to-day priorities and the mood ────────────────────────── */

.choice-row {
  grid-template-columns: auto auto 1fr;
  column-gap: var(--ra-space-4);
  align-items: center;
}
.choice-row .plan-badge { grid-column: 2; grid-row: 1 / span 2; }
.choice-row .t { grid-column: 3; grid-row: 1; }
.choice-row .d { grid-column: 3; grid-row: 2; }
.choice-row .choice-aside { grid-column: 4; grid-row: 1 / span 2; width: 104px; }
.choice-row .choice-aside .plan-thumb { aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--ra-radius-md); }

/* The mood cards carry swatches instead of a leading glyph. */
.choice-row-swatch { grid-template-columns: auto 1fr; }
.choice-row-swatch .t { grid-column: 2; grid-row: 1; }
.choice-row-swatch .d { grid-column: 2; grid-row: 2; }
.choice-row-swatch .swatch { grid-column: 2; grid-row: 3; margin-top: var(--ra-space-2); }
.choice-row-swatch .choice-aside { grid-column: 3; grid-row: 1 / span 3; }

@media (min-width: 700px) {
  .choices-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .choices-row-swatch { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 599px) {
  /* A 104px aside plus a control plus two lines of text leaves the label about
     eleven characters wide, so the aside is dropped rather than squeezed. The
     glyph and the words carry the card at this width. */
  .choice-row .choice-aside, .choice-row-swatch .choice-aside { display: none; }
}

/* ── tile: the fitting finish ───────────────────────────────────────────── */

.choices-tile { align-items: stretch; }
@media (min-width: 520px) { .choices-tile { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 860px) { .choices-tile { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.choice-tile {
  grid-template-columns: 1fr;
  grid-template-rows: auto auto 1fr;
  gap: var(--ra-space-3);
  position: relative;
  padding: var(--ra-space-3);
  text-align: center;
}
/* Every child needs an EXPLICIT row and column here. The base `.choice` rules
   put `.t` and `.d` in column 2, which in a one-column tile created an implicit
   second column and dropped the label on top of the swatch. */
.choice-tile .plan-media { grid-row: 1; grid-column: 1; width: 100%; }
/* The control sits over the finish panel, which can be near-white (polished
   chrome) or near-black (matte black). A white circle on light metal vanished
   entirely, so it carries its own ring and a soft shadow and reads on anything. */
.choice-tile .box {
  position: absolute; top: var(--ra-space-5); left: var(--ra-space-5);
  background: var(--ra-white);
  box-shadow: 0 0 0 2px var(--ra-white), var(--ra-shadow-sm);
}
.choice-card .box { box-shadow: 0 0 0 2px var(--ra-white), var(--ra-shadow-sm); }
.choice-tile .t { grid-row: 2; grid-column: 1; }
.choice-tile .d { grid-row: 3; grid-column: 1; }

/* A swatch filling the media panel, for the finishes, which have no glyph. */
.plan-media[data-media="swatch"] { display: flex; gap: 0; }
.plan-media[data-media="swatch"] i { display: block; flex: 1 1 0; height: 100%; }

/* ── the material comparison thumbnails ─────────────────────────────────── */

.plan-thumb { display: block; width: 100%; height: auto; border-radius: var(--ra-radius-sm); }
.plan-option-thumb { aspect-ratio: 1 / 1; object-fit: cover; margin-bottom: var(--ra-space-1); }

.plan-concept-note {
  color: var(--ra-text-muted);
  padding: var(--ra-space-3) 0;
  border-top: 1px solid var(--ra-border);
}

/* The same note under a three-up grid describes all three images, but it is a
   <p>, so the copy measure cut its rule off a third of the way across and the
   line read as a caption belonging to the first tile. Directly after a grid it
   spans the grid. In the planner, where it sits in a narrow column, it does
   not match this and keeps the measure. */
.tiles + .plan-concept-note { max-width: none; }

/* ── saved in plan ──────────────────────────────────────────────────────────
   The asset sheet's fourth card state. Not a disabled choice card: there is no
   control in it at all, because a control somebody cannot operate is worse than
   none. The tick is a statement and "Change this" is the way back. */

.plan-saved {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--ra-space-5);
  padding: var(--ra-space-4);
  border: 1px solid var(--ra-selected-border);
  border-radius: var(--ra-radius-lg);
  background: var(--ra-selected-bg);
}
.plan-saved-media { display: block; width: 104px; }
.plan-saved-media .plan-media { width: 100%; background: var(--ra-white); }
.plan-saved-body h4 { margin: 0 0 var(--ra-space-1); font-size: var(--ra-text-h4); }
.plan-saved-body p { margin: 0 0 var(--ra-space-2); }
.plan-saved-tick {
  display: inline-grid;
  place-items: center;
  width: 36px; height: 36px;
  border-radius: var(--ra-radius-full);
  background: var(--ra-cta);
  color: var(--ra-cta-text);
}
.plan-saved-tick .plan-icon { background: none; color: inherit; width: auto; height: auto; }
.plan-saved-tick .plan-icon svg { width: 20px; height: 20px; }

@media (max-width: 599px) {
  .plan-saved { grid-template-columns: auto 1fr; gap: var(--ra-space-4); }
  .plan-saved-media { grid-column: 1 / -1; width: 100%; }
  .plan-saved-media .plan-media { aspect-ratio: 16 / 9; }
}

@media print {
  .plan-saved-tick { display: none; }
  .plan-saved { background: none; }
}

/* ── tool meta and answered-already rows ────────────────────────────────── */

.plan-tool-meta {
  display: flex; flex-wrap: wrap; gap: var(--ra-space-2);
  align-items: center;
  color: var(--ra-text-muted);
}
.plan-tool-meta a { color: var(--ra-link); }

.plan-answered {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--ra-space-4);
  padding: var(--ra-space-3) var(--ra-space-4);
  border: 1px solid var(--ra-border);
  border-radius: var(--ra-radius-md);
  background: var(--ra-surface-tint);
}
.plan-answered .plan-thumb { width: 72px; aspect-ratio: 2 / 1; object-fit: cover; border-radius: var(--ra-radius-sm); }
.plan-answered p { margin: 0; }
.plan-answered-label { font-weight: var(--ra-fw-label); }

.plan-lines-bands > div { padding: var(--ra-space-2) 0; border-bottom: 1px solid var(--ra-border); }
.plan-lines-bands > div:last-child { border-bottom: 0; }
.plan-lines-bands dt { font-weight: var(--ra-fw-label); }
.plan-lines-bands dd { font-variant-numeric: tabular-nums; }

/* An escape option ("I am not sure yet") is a real answer, so it gets a panel
   of its own weight rather than an empty hole in the row. */
.plan-media[data-media="escape"] { background: var(--ra-surface-tint); border: 1px dashed var(--ra-border-input); }
.plan-media[data-media="escape"] .plan-icon { background: none; color: var(--ra-stone-400); width: auto; height: auto; }
.plan-media[data-media="escape"] .plan-icon svg { width: 34%; height: auto; }

/* ── the materials comparison, as five disclosures ──────────────────────── */

details.plan-compare {
  border: 1px solid var(--ra-border);
  border-radius: var(--ra-radius-lg);
  background: var(--ra-surface);
  margin-bottom: var(--ra-space-4);
  overflow: hidden;
}
details.plan-compare > summary {
  padding: var(--ra-space-5);
  cursor: pointer;
  list-style: none;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--ra-space-4);
}
details.plan-compare > summary::-webkit-details-marker { display: none; }
details.plan-compare > summary::after {
  content: "";
  width: 10px; height: 10px;
  border-right: 2px solid var(--ra-stone-600);
  border-bottom: 2px solid var(--ra-stone-600);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--ra-dur-fast) var(--ra-ease-standard);
}
details.plan-compare[open] > summary::after { transform: rotate(225deg) translate(-2px, -2px); }
details.plan-compare > summary:focus-visible { box-shadow: var(--ra-focus-ring); }
.plan-compare-title { display: grid; gap: var(--ra-space-1); min-width: 0; }
.plan-compare-h { font-family: var(--ra-font-display); font-size: var(--ra-text-h4); color: var(--ra-heading); }
.plan-compare-chosen { color: var(--ra-text-muted); }
.plan-compare-body { padding: 0 var(--ra-space-5) var(--ra-space-5); }
.plan-compare-body > .btn-ghost { margin-top: var(--ra-space-4); }

@media (prefers-reduced-motion: reduce) {
  details.plan-compare > summary::after { transition: none; }
}

/* ── finishes, drawn by how they hold light ─────────────────────────────────
   Not photography, and not pretending to be. A flat chip cannot separate
   brushed nickel from polished chrome, because what differs is the highlight,
   not the colour. These three treatments render that difference until the real
   fixture photographs arrive, at which point srcFor() takes over. */

.plan-finish { background: var(--finish-tone); position: relative; overflow: hidden; }

/* Polished: a hard mirror highlight and a dark reflected band under it. */
.plan-finish[data-treatment="polished"] {
  background-image:
    linear-gradient(105deg,
      color-mix(in srgb, var(--ra-white) 78%, transparent) 0%,
      color-mix(in srgb, var(--ra-white) 10%, transparent) 26%,
      color-mix(in srgb, var(--ra-offblack) 34%, transparent) 46%,
      color-mix(in srgb, var(--ra-white) 62%, transparent) 62%,
      color-mix(in srgb, var(--ra-offblack) 18%, transparent) 86%,
      color-mix(in srgb, var(--ra-white) 40%, transparent) 100%);
}

/* Brushed: a soft directional sheen along a fine grain. */
.plan-finish[data-treatment="brushed"] {
  background-image:
    repeating-linear-gradient(100deg,
      color-mix(in srgb, var(--ra-white) 16%, transparent) 0 1px,
      transparent 1px 3px),
    linear-gradient(100deg,
      color-mix(in srgb, var(--ra-white) 34%, transparent) 0%,
      color-mix(in srgb, var(--ra-offblack) 10%, transparent) 52%,
      color-mix(in srgb, var(--ra-white) 20%, transparent) 100%);
}

/* Matte: no highlight at all, which is the whole point of it. */
.plan-finish[data-treatment="matte"] {
  background-image:
    linear-gradient(100deg,
      color-mix(in srgb, var(--ra-white) 7%, transparent) 0%,
      transparent 60%);
}

.plan-care, .plan-note {
  grid-column: 1;
  font-size: var(--ra-text-caption);
  color: var(--ra-text-muted);
  text-align: left;
}
.plan-care { padding-top: var(--ra-space-2); border-top: 1px solid var(--ra-border); }
.plan-footnote {
  margin-top: var(--ra-space-4);
  padding-top: var(--ra-space-4);
  border-top: 1px solid var(--ra-border);
  color: var(--ra-text-muted);
  max-width: var(--ra-site-copy-measure);
}


/* ── tile media ─────────────────────────────────────────────────────────────
   A real image where one exists, and a quiet panel where one does not. The
   panel says nothing: an absent image is a plainer card, never a visible todo. */

.tile-img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; }
.tile-ph {
  aspect-ratio: 4 / 3;
  background:
    radial-gradient(120% 90% at 25% 20%, color-mix(in srgb, var(--ra-matcha-100) 85%, transparent), transparent 62%),
    var(--ra-stone-100);
}


/* ── anchored sections clear the sticky header ──────────────────────────────
   The homepage accessibility card links to bathroom-renovations.html#accessibility.
   Without this the browser scrolls the heading exactly to the top of the
   viewport, which is underneath the sticky header, so the section a reader
   clicked through for is the one thing they cannot see. */
:target,
.section[id],
.section-tight[id] { scroll-margin-top: calc(var(--ra-space-20) + var(--ra-space-4)); }

/* A measurement note that is an error must look and read like one. */
[data-measure-note][data-tone="error"] { color: var(--ra-error-600); }
[data-measure-note][data-tone="ok"] { color: var(--ra-success-600); }
.input[aria-invalid="true"] { border-color: var(--ra-error-600); }

/* A ticked answer that has outgrown the project type. Not an error: the answer
   is fine, the scope is the thing that has to change. */
.plan-conflict {
  margin: var(--ra-space-5) 0;
  padding: var(--ra-space-5);
  border: 1px solid var(--ra-warning-700);
  border-radius: var(--ra-radius-md);
  background: var(--ra-warning-100);
}
.plan-conflict h4 { margin: 0 0 var(--ra-space-2); font-size: var(--ra-text-label); }
.plan-conflict p { margin: 0 0 var(--ra-space-3); }
.plan-conflict-list { margin: 0 0 var(--ra-space-3); padding-left: var(--ra-space-5); }
.plan-conflict-list li { margin-bottom: var(--ra-space-2); font-size: var(--ra-text-small); }
.plan-conflict .btn-row { margin-top: var(--ra-space-4); gap: var(--ra-space-3); flex-wrap: wrap; }

/* ── the six bathroom renders ──────────────────────────────────────────────
   4:3 with an explicit width and height, so the space is reserved and nothing
   below the image moves when it lands. */
.room-img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--ra-stone-100);
}

/* A page hero with a supporting image. One column on a phone, two where there
   is room, with the copy first in both so reading order never depends on width. */
.page-hero-split { display: grid; gap: var(--ra-space-8); align-items: center; }
@media (min-width: 900px) {
  .page-hero-split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--ra-space-12); }
}
.page-hero-media { min-width: 0; }
.page-hero-media .room-img { border-radius: var(--ra-radius-lg); }

/* ── THE HERO PROJECT SELECTOR ──────────────────────────────────────────────
   Four links that open the questionnaire with a choice already made. They are
   list items so a screen reader announces how many there are, and the list is
   labelled by the question above it.

   Each row is a 44px-minimum target with the padding on the link, so the whole
   row is tappable rather than just the words. The hover and focus treatments
   do not change the box, so nothing below a row moves when it is pointed at. */
.hero-choices { margin-top: var(--ra-space-6); }
.hero-choices-q {
  font-family: var(--ra-font-ui);
  font-size: var(--ra-text-body);
  font-weight: var(--ra-fw-label);
  color: var(--ra-text);
  margin: 0 0 var(--ra-space-3);
}
/* ── NAMED hero-choice, NOT choice ──────────────────────────────────────────
   `.choice` already exists: the introduction form and the planner both use it
   for their radio cards, with a `.box`, a `.t` and a `.d` inside. Adding hero
   rules to that name rewrote the planner's radio cards too, and the planner's
   style step overflowed its column by 87px at 1024. A new component needs a new
   name, and a name that is already in the stylesheet is not a new name. */
.hero-choice-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--ra-space-2); max-width: 30rem; }
.hero-choice {
  display: flex; align-items: center; justify-content: space-between; gap: var(--ra-space-4);
  min-height: 52px;
  padding: var(--ra-space-3) var(--ra-space-4);
  background: var(--ra-surface);
  border: 1px solid var(--ra-border);
  border-radius: var(--ra-radius-lg);
  color: var(--ra-text);
  text-decoration: none;
  font-weight: var(--ra-fw-label);
  transition: border-color var(--ra-site-motion-base) var(--ra-site-ease-standard),
              background var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.hero-choice:hover { border-color: var(--ra-brand); background: var(--ra-surface-tint); color: var(--ra-text); }
.hero-choice-arrow {
  color: var(--ra-brand); flex: none;
  transition: transform var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.hero-choice:hover .hero-choice-arrow { transform: translateX(3px); }
/* Pressed feedback, the same as the buttons have. The audit caught its absence:
   a 52px row that is clearly a control should acknowledge being pressed. The
   scale is small enough not to shift the rows around it. */
.hero-choice:active { transform: scale(.995); }
@media (prefers-reduced-motion: reduce) {
  .hero-choice, .hero-choice-arrow { transition: none; }
  .hero-choice:active { transform: none; }
  .hero-choice:hover .hero-choice-arrow { transform: none; }
}

/* ── THE QUESTIONNAIRE, ONE QUESTION AT A TIME ──────────────────────────────
   Rendered by web/intake/intake.mjs. Big targets, because a question answered
   in one click should be hard to miss, and the whole row is the control rather
   than a 13px radio inside it. */
/* ── THE PAGE INTRO STEPS ASIDE ONCE SOMEBODY IS ANSWERING ────────────
   Josh: "it doesn't fit on the screen. It's still cut off at the bottom."
   Measured at 390x844 before this: the document was 1116px against an 844px
   viewport, and the last option and the Back/Continue row were both below the
   fold. The breakdown was eyebrow 16 + h1 75 + lede 79 = 170px of intro, plus a
   148px footer, carried above every question.

   The intro is not deleted, it is hidden from question two onward, which is the
   same thing the contractor signup does with `.su-intro-lede.is-read`. On
   question one it is still the thing that explains the page.

   `display: none` rather than a height animation: this fires on a step change
   that already moves the question in, and two motions at once on a small screen
   reads as a jump. */
/* ── THE TITLE STAYS, THE LEDE GOES ─────────────────────────────────────────
   This hid the whole intro block, heading and all, on the first answer. Josh:
   "'Tell us about the project' needs to stay the whole time because after I do
   the first one, it goes away, which makes everything move, which looks weird."

   The reason it was hidden is still real: the lede is three lines and the form
   has to fit a laptop without scrolling. So the lede goes, which is the part
   that has already been read, and the eyebrow and the heading stay, which is
   what stops the page moving under them. */
body.q-started #intake-intro .lede { display: none; }

/* ── THE FOOTER IS NOT PART OF A QUESTION ───────────────────────────────────
   Josh, four separate times across two sessions: "I have to scroll for it...
   ideally I don't have to scroll for anything. You just make the sizing fit."

   Measured on the worst step, five options at 1366x768: the question itself
   ended at exactly 768px and the document was 1065. The entire 297px overflow
   was the site footer, 152px of it plus the 145px of page spacing above it.
   The form already fitted. The footer was what made it scroll.

   So on the question stage the footer is out. It returns for the review and
   the consent step, which is where its privacy link actually matters, and
   which is why this is scoped to `.q-intake-step` rather than to `.q-started`:
   `q-started` is true on the consent step too. */
body.q-intake-step .site-footer { display: none; }

/* The consent step measured 470px of overflow at 1366x768, so it needs the same
   treatment plus a denser consent block. The privacy link moved INTO the
   consent group when the footer went, because that is the one link that has to
   be reachable exactly where somebody is being asked to agree. */
body.q-details-step .site-footer { display: none; }
@media (max-height: 920px) {
  body.q-details-step .choice .d { font-size: var(--ra-text-caption); }
  body.q-details-step .choices { gap: var(--ra-space-2); }
  body.q-details-step .field { gap: 4px; }
  body.q-details-step #intake-intro { display: none; }
}

/* ── A SHORT SCREEN: LESS INTRO, AND THE ACTION ALWAYS REACHABLE ────────
   Question ONE is the worst case, because the intro is still up. Measured
   before this, with four options:

     1280x720   nav 156px below the fold
     390x844    nav  76px below the fold
     375x667    nav 280px below the fold

   which is exactly the screenshot Josh sent, with the fourth option half cut
   off at the bottom.

   TWO CHANGES, and the second is the one that matters.

   The intro is reduced rather than removed on arrival: the eyebrow and the lede
   go, the headline stays smaller. The headline is what tells somebody what this
   page is; the lede repeats what the questions will show them anyway.

   AND THE NAV STICKS. Arithmetic settles this one: at 375x667 the header is
   81px and the footer 148, leaving 438px, and a four-option question is 507px
   on its own. No amount of trimming fits that, so the honest goal is the one
   the contractor signup already uses: the action is always on screen and the
   options scroll under it. A page that scrolls with Continue visible does not
   read as cut off; a page that hides Continue does.

   The backdrop is on the CONTAINER, not on the buttons. Putting it on a button
   is what made "Create my account" white on white earlier today.

   900px, not 820. At 820 the rule missed 390x844, which is an iPhone 14, and
   that was the one size where the nav was still off screen after everything
   else. A breakpoint that excludes the commonest phone is the wrong
   breakpoint. */
@media (max-height: 900px) {
  #intake-intro .eyebrow, #intake-intro .lede { display: none; }
  #intake-intro h1 { font-size: var(--ra-text-h3); }
  main .section:has(#enquiry) { padding-block: var(--ra-space-5); }
  .q-nav {
    position: sticky; bottom: 0; z-index: 2;
    padding-block: var(--ra-space-3);
    padding-block-end: calc(var(--ra-space-3) + env(safe-area-inset-bottom, 0px));
    background: var(--ra-surface);
    border-top: 1px solid var(--ra-border);
  }
}

/* And the footer compacts on this page at short heights, for the same reason and
   in the same way as the contractor signup. The disclaimer is the only thing
   unique to it, so that is what is kept. */
@media (max-height: 820px) {
  body.q-started .site-footer { padding-block: var(--ra-space-3); }
  body.q-started .site-footer .wrap { gap: var(--ra-space-2); }
  body.q-started .site-footer > .wrap > span { display: none; }
  body.q-started .site-footer p { margin: 0; font-size: var(--ra-text-caption); }
}

/* ── CENTRED, BECAUSE JOSH ASKED FOR IT AND IT READS BETTER HERE ────────
   "I want it more cenerted and the question they are being asked to be more
   centered, it needs to be easier to understand and go through."

   The column was `max-width: 40rem` with no `margin-inline`, so on a wide
   screen the whole questionnaire sat against the left edge with a third of the
   page empty beside it, which is what his screenshot shows.

   The COLUMN is centred and the PROMPT is centred with it. The options are left
   aligned inside their rows on purpose: a list of choices is read down the left
   edge, and centred option text makes a column of ragged starts that is harder
   to scan, which would work against "easier to understand and go through". */
.q-screen {
  display: grid; gap: var(--ra-space-5);
  max-width: 40rem; margin-inline: auto;
}
.q-screen .q-prompt, .q-screen .q-help { text-align: center; }
.q-step {
  margin: 0; font-size: var(--ra-text-caption); font-weight: var(--ra-fw-label);
  letter-spacing: 0.06em  /* off-scale: label tracking, one value for every uppercase caption; the kit names no tracking scale */; text-transform: uppercase; color: var(--ra-text-muted);
}
.q-prompt {
  font-family: var(--ra-font-display); font-size: var(--ra-text-h2);
  line-height: var(--ra-lh-h2); letter-spacing: -0.02em  /* off-scale: display tracking, keeps a large heading from looking loose */;
  color: var(--ra-heading); margin: 0; text-wrap: balance;
}
/* ── outline AND box-shadow, because the ring in this kit is a SHADOW ────────
   This cleared `outline` only, and the kit's focus ring is
   `box-shadow: var(--ra-focus-ring)`, not an outline. So the question heading
   went on drawing a green box after every tap, which `web/ring-check.mjs` found
   on its first run: a FOURTH instance of the thing Josh reported three times,
   in a place nobody had looked.

   Same reasoning as the headings and the nav sheet: `.q-prompt` carries
   `tabindex="-1"`, is focused by the questionnaire so a screen reader lands on
   the new question, and cannot be reached by tabbing. The only focus it ever
   takes is a script's. */
.q-prompt:focus { outline: none; box-shadow: none; }

/* ── NOTHING THAT TAKES FOCUS ENDS UP UNDER THE STICKY HEADER ───────────────
   Focusing a heading makes the browser scroll it to the TOP of the viewport,
   which is behind `position: sticky`. Josh saw it on the result: "the large
   heading was partially hidden above/behind the sticky header."

   `--ra-header-h` is published by the page from the header's measured height,
   so this tracks the real element through both breakpoints, a font load and an
   orientation change. The fallback only matters for the single frame before
   the first measurement lands. */
.q-prompt,
#q-stop,
#q-warn,
.review-h,
.form-step h1,
#done-routed h1,
#done-held h1 {
  scroll-margin-top: calc(var(--ra-header-h) + var(--ra-space-4));
}
/* The explicit ring that used to be here is gone, and it is the fourth copy of
   the same mistake: `.pt-step-heading:focus-visible` in portal.css was the
   third. An element carrying `tabindex="-1"` cannot be reached by tabbing, so a
   focus-visible ring on it can only ever be shown to somebody who did not ask
   for one. Deleted rather than overridden, because an override is a second rule
   arguing with the first forever. */
.q-help { margin: 0; color: var(--ra-text-muted); font-size: var(--ra-text-small); }

.q-options { display: grid; gap: var(--ra-space-3); }
/* ── CENTRED, BECAUSE EVERYTHING ABOVE THEM IS ─────────────────────────────
   `.q-screen .q-prompt` and `.q-help` are centred, and these were not, so each
   screen centred its question and then left-aligned its answers. Josh: "All of
   these selectors might as well be centered if the other ones are centered."

   `justify-content` centres the icon-and-label pair as a group, so a card with
   an icon and a card without one both read as deliberate. That matters here
   because 35 of the 47 options have no artwork yet (FR7). */
.q-option {
  display: flex; align-items: center; justify-content: center; gap: var(--ra-space-4);
  width: 100%; min-height: 72px; text-align: center;
  padding: var(--ra-space-4);
  background: var(--ra-surface);
  border: 1px solid var(--ra-border);
  border-radius: var(--ra-radius-lg);
  color: var(--ra-text);
  font: inherit; cursor: pointer;
  transition: border-color var(--ra-site-motion-base) var(--ra-site-ease-standard),
              background var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.q-option:hover { border-color: var(--ra-secondary); background: var(--ra-surface-tint); }
/* The chosen state changes the border COLOUR and width together with a
   compensating inset, so selecting an answer does not move the rows below it. */
.q-option.is-chosen {
  border-color: var(--ra-selected-border); border-width: 2px;
  padding: calc(var(--ra-space-4) - 1px);
  background: var(--ra-selected-bg);
}
.q-option:active { transform: scale(.995); }

/* ── THE ICON COLUMN, SO ICONS LINE UP DOWN THE SCREEN ──────────────────────
   Josh: "Individual icon/text clusters are centred separately, so icons sit at
   different horizontal positions from one card to the next."

   Measured on the budget screen at 1440, the six icons sat at x = 645, 641,
   637, 635, 638 and 666: the icon and label were centred AS A PAIR, so a longer
   label pushed the icon left and the column wandered by 31px down six rows.

   The fix keeps the pair centred, which is Josh's earlier decision and still
   right, and makes it a two-column grid of a FIXED icon width. The group is
   centred; the icon column inside it is not. So every icon on a screen sits at
   the same x, the text left-aligns against a common edge, and nothing about the
   centred composition changes.

   `max-width` with `margin-inline: auto` is what centres it. Without the cap a
   1fr text column would stretch the inner grid to the full card and the whole
   thing would read as left-aligned. */
.q-option-inner {
  display: grid; grid-template-columns: var(--q-icon-col, 44px) minmax(0, 1fr);
  align-items: center; gap: var(--ra-space-4);
  /* ── A SHARED WIDTH, NOT `max-content` ────────────────────────────────────
     `max-width: max-content` was the first attempt and it did not work: it
     makes each row's inner block as wide as ITS OWN content, so centring the
     block still moves the icon whenever the label length changes. Measured
     after that change, the icons still wandered 43px down the condition screen
     and 55px down timing. The fix looked right and changed nothing, which is
     why it was measured rather than eyeballed.

     A SHARED width is what makes a column. Every row's inner block is now the
     same width and centred, so the icon column lands at the same x on every
     row while the composition stays centred, which was Josh's earlier call and
     is still right.

     `min(100%, 26rem)` so it never exceeds a narrow card. 26rem holds the
     longest label on the form, "As soon as a contractor is available", beside a
     44px icon without wrapping. */
  width: min(100%, 26rem); margin-inline: auto; min-width: 0;
}
.q-option-text { display: grid; gap: 2px; min-width: 0; justify-items: start; text-align: left; }
.q-option-label { font-weight: var(--ra-fw-label); }
.q-option-help { font-size: var(--ra-text-small); color: var(--ra-text-muted); }

/* ── A SELECTION MARK THAT DOES NOT MOVE ANYTHING ───────────────────────────
   Always in the markup, so selecting an answer changes a fill and never the
   layout: adding the element on selection would reflow the card at the exact
   moment somebody is looking at it.

   A tick, drawn in CSS, in a consistent corner. It is NOT the only signal: the
   border colour and width change too, because colour alone must not encode
   state. */
.q-option { position: relative; }
.q-option-mark {
  position: absolute; inset-block-start: var(--ra-space-3); inset-inline-end: var(--ra-space-3);
  width: 20px; height: 20px; border-radius: var(--ra-radius-full);
  border: 1.5px solid var(--ra-border); background: var(--ra-surface);
  display: grid; place-items: center;
  transition: background var(--ra-site-motion-base) var(--ra-site-ease-standard),
              border-color var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.q-option-mark::after {
  content: ""; width: 9px; height: 5px;
  /* ── `--ra-surface`, WHICH IS CORRECT IN BOTH THEMES ─────────────────────
     This referenced an "on-selected" token that does not exist, with a white
     hardcoded fallback. web/token-check caught it in all three dists.

     The fallback was the dangerous part: it would have RENDERED correctly in
     light mode and been invisible in dark, where the selected fill is
     matcha-300 and the tick would have been white on pale green. A hardcoded
     fallback is how an undefined token stops looking like a bug.

     The token is not named literally here, because token-check reads this file
     as text and a name in a comment is a reference as far as it is concerned.
     Same shape as Tailwind scanning comments for class names.

     `--ra-surface` is the inverse of `--ra-selected-border` by construction:
     light mode pairs white surface with a green-900 fill, dark mode pairs a
     green-900 surface with a matcha-300 fill. One token, both themes, no
     fallback. */
  border-inline-start: 2px solid var(--ra-surface);
  border-block-end: 2px solid var(--ra-surface);
  transform: rotate(-45deg) scale(0); transform-origin: center;
  transition: transform var(--ra-site-motion-base) var(--ra-site-ease-standard);
  margin-block-start: -2px;
}
.q-option.is-chosen .q-option-mark {
  background: var(--ra-selected-border); border-color: var(--ra-selected-border);
}
.q-option.is-chosen .q-option-mark::after { transform: rotate(-45deg) scale(1); }
@media (prefers-reduced-motion: reduce) {
  .q-option-mark, .q-option-mark::after { transition: none; }
}

/* ── SIX ANSWERS AS A GRID OF SQUARES ───────────────────────────────────────
   Josh: "three-column by two-row grid of square cards on desktop, rather than
   six full-width rows", for `extras` and `budget`.

   Driven by `grid: true` on the question, not by counting options, so a seventh
   answer does not silently change the layout. `auto-fit` is deliberately NOT
   used: three columns is the decision, and auto-fit would quietly become four
   on a wide screen.

   ROW-MAJOR, which is the default grid flow, so visual order and keyboard order
   are the same. No carousel, no hidden seventh state, no sideways scrolling:
   six answers are six complete hit targets. */
@media (min-width: 860px) {
  .q-options.is-grid { grid-template-columns: repeat(3, 1fr); }
  .q-options.is-grid .q-option {
    aspect-ratio: 1 / 1;
    /* The icon above the label rather than beside it: a square card has the
       height for it, and stacking is what makes the six read as a set. */
    flex-direction: column; justify-content: center;
    padding: var(--ra-space-5);
  }
  .q-options.is-grid .q-option-inner {
    grid-template-columns: 1fr; justify-items: center; gap: var(--ra-space-3);
  }
  .q-options.is-grid .q-option-text { justify-items: center; text-align: center; }
  /* Bigger inside a square card, which has the room. Still the same asset. */
  .q-options.is-grid .q-icon { width: 56px; height: 56px; }
  .q-options.is-grid .q-icon :is(svg, img) { width: 40px; height: 40px; }
}

/* ── A SQUARE CARD IS AS TALL AS IT IS WIDE, WHICH A LAPTOP CANNOT AFFORD ────
 *
 * Josh, on the budget question: "I have to scroll for it." He was still right
 * after the last attempt at this. Measured on the live site 2026-10-27, with
 * the last answer's distance BEHIND the sticky action bar:
 *
 *     1280x800   budget 79px     extras fits
 *     1366x768   budget 93px     extras 19px
 *      375x812   budget 28px
 *
 * `aspect-ratio: 1 / 1` on three columns is the cause. At 1280px wide each card
 * is about 380px across, so it is also 380px TALL, and two rows of that is
 * 760px before the heading, the help paragraph and the bar. It cannot fit in
 * 800px, and no amount of tuning the icon size changes that: the height is
 * derived from the width.
 *
 * So the squareness is capped rather than the content shrunk. The cards stay a
 * grid and stay the same shape on a screen with the room; on a laptop they
 * become shorter rectangles, which is the same design one notch down.
 *
 * Not a `max-height` on the OPTION alone: the inner padding has to come in with
 * it or the icon and a two-line label overflow the shorter box.
 *
 * This is NOT the icon enlargement from earlier in this file. These two
 * screens are `.is-grid` and that rule is `:not(.is-grid)`; measured with it
 * and without it and these numbers are identical either way. It was already
 * like this.
 */
@media (min-width: 860px) and (max-height: 920px) {
  .q-options.is-grid .q-option {
    /* Width still comes from the grid; this only stops the height following it
       past what the screen has. 200px holds a 56px icon, the gap and two lines
       of label with the reduced padding below. */
    max-block-size: min(19vh, 168px);
    padding: var(--ra-space-3);
  }
  .q-options.is-grid .q-option-inner { gap: var(--ra-space-2); }
}

/* ── THE TABLET STEP ────────────────────────────────────────────────────────
   Three columns only where the labels comfortably fit. At 768 the content
   column is about 620px, so a third of it is ~195px, and "$25,000 to $40,000"
   does not sit on one line in a card that narrow. Two columns, three rows. */
@media (min-width: 560px) and (max-width: 859px) {
  .q-options.is-grid { grid-template-columns: repeat(2, 1fr); }
  .q-options.is-grid .q-option { flex-direction: column; justify-content: center; min-height: 132px; }
  .q-options.is-grid .q-option-inner { grid-template-columns: 1fr; justify-items: center; gap: var(--ra-space-2); }
  .q-options.is-grid .q-option-text { justify-items: center; text-align: center; }
}

/* ── THE PHONE: TWO COLUMNS, THREE ROWS ─────────────────────────────────────
   Josh: "On smaller phones use a readable responsive layout: normally two
   columns by three rows, with a one-column fallback when long text or enlarged
   text needs it. Do not force three tiny columns on a 375px screen to imitate
   desktop geometry."

   NOT square here. A 375px screen gives each card about 168px, and
   "$25,000 to $40,000" in a 168px square would have to shrink the text to fit,
   which the brief rules out: "Do not shrink text just to force a square." So
   the cards keep a minimum height and take whatever height the label needs.

   16px text and a 44px minimum target are floors, not suggestions. */
@media (max-width: 559px) {
  /* ── TWO COLUMNS, FALLING TO ONE WHEN THE TEXT GROWS ─────────────────────
     `minmax(10rem, ...)` is the whole mechanism, and the unit is the point.

     Text-only zoom scales `rem` and does NOT change the viewport width, so no
     width media query can see it: at 200% text a `repeat(2, 1fr)` grid keeps
     two columns and the label clips or shrinks, which the brief rules out.

     With a rem floor the browser does the arithmetic. At a 16px root a 375px
     phone has about 343px of content, two 160px tracks fit, and the result is
     the two-by-three the brief asks for. At a 24px root the floor becomes
     240px, two no longer fit, and it collapses to ONE column of natural-height
     cards by itself. No query, no breakpoint to maintain, and it is correct at
     every intermediate size rather than only at the two I happened to test.

     `auto-fit` is safe HERE and refused on desktop: the cap on a phone is the
     screen, so it can never quietly become three. */
  .q-options.is-grid {
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    gap: var(--ra-space-2);
  }
  .q-options.is-grid .q-option {
    flex-direction: column; justify-content: center;
    min-height: 112px; padding: var(--ra-space-3);
    aspect-ratio: auto;
  }
  .q-options.is-grid .q-option-inner {
    grid-template-columns: 1fr; justify-items: center; gap: var(--ra-space-2);
  }
  .q-options.is-grid .q-option-text { justify-items: center; text-align: center; }
  .q-options.is-grid .q-option-label { font-size: 1rem; line-height: 1.25; }
  .q-options.is-grid .q-icon { width: 36px; height: 36px; }
  .q-options.is-grid .q-icon :is(svg, img) { width: 26px; height: 26px; }
  /* The mark moves in, because a 168px card has less corner to spare. */
  .q-options.is-grid .q-option-mark { inset-block-start: 6px; inset-inline-end: 6px; }
}


.q-icon { flex: none; display: grid; place-items: center; width: 44px; height: 44px; color: var(--ra-brand); }
.q-icon :is(svg, img) { width: 28px; height: 28px; }

/* ── QUESTION ONE'S ARTWORK IS LARGER, AND ONLY QUESTION ONE'S ──────────────
   The three project illustrations each carry two objects and the relationship
   between them: a tub, an arrow and a walk-in. Rendered at 28px beside the
   icons they replace, the composition was legible but dense; at 40px it reads
   immediately. The brief allows exactly this: "compact project illustrations
   for question 1, simple icons for later questions."

   Scoped to the project screen so nothing else changes size. Every other
   question keeps a 28px icon, which is right for a single-idea glyph and keeps
   the rows the height they already are. */
/* ── AND ONLY WHERE THERE IS ROOM FOR IT ─────────────────────────────────
   Enlarging these made each of the four rows taller, and web/audit-form.mjs
   caught the consequence immediately: at 430x932 the Back and Continue row went
   off screen, so the form read as cut off at the bottom.

   A phone has four rows, a heading, a helper and a nav to fit in one viewport,
   and the row height is worth more there than the illustration is: the label is
   doing the work on a narrow screen anyway. So the enlargement applies from
   768px up, and a phone keeps the 28px icon it already had, which is the one
   size that cannot regress because it is what shipped. */
/* ── EVERY ILLUSTRATED ANSWER IS 40px ON A WIDE SCREEN ──────────────────────
   This was scoped to the project question alone. The rest of the new family is
   just as detailed: a building whose storeys must be told apart, a room corner
   with a crack in it, a plan with a fixture in a named position. Photographed
   in the real form at 28px, the four floor icons were nearly indistinguishable
   at a glance, which is the thing the brief asks to be tested rather than
   assumed.

   The brief allows it: simple icons at 28-40px, detailed illustrations at
   40-64px "where the existing card layout accommodates them".

   TWO SCOPES, both load-bearing:

   `min-width: 768px` because enlarging these on a phone is what pushed the Back
   and Continue row off screen at 430x932 last time. A phone has four rows, a
   heading, a helper and a nav to fit in one viewport, and there the row height
   is worth more than the illustration.

   `:not(.is-grid)` because the extras and budget grids already set their own
   icon size per breakpoint inside a square card, and two rules fighting over
   the same icon is how one of them silently wins. */
/* ── AND ONLY WHERE THERE IS ROOM FOR IT ────────────────────────────────────
   The first version of this rule was `@media (min-width: 768px)` alone, and it
   put the last answer on the five-option shower question BEHIND the sticky
   action bar: 35px at 1280x800, 16px at 1366x768. Measured with the rule and
   again without it, and `shower_goal` fits without and does not fit with, so
   it was this.

   Width was the wrong question. Twelve extra pixels per row is 60px on a five
   option screen, and whether that fits is about HEIGHT. A 1440x900 laptop has
   the room and a 1366x768 one does not.

   `web/check-all.sh` could not have caught it: a sticky element hides content
   without lengthening the document, so `scrollHeight - innerHeight` is 0 and
   every overflow metric reads clean. `web/live-check.mjs` walks all thirteen
   screens and asks whether the last answer is above the bar, which is the only
   question that sees it, and it runs after a deploy rather than in the suite. */
@media (min-width: 768px) {
  /* Four answers or fewer: the extra height is affordable at any laptop size.
     `:has(.q-option:nth-child(5))` is "this list has a fifth answer", so the
     negation is "at most four". */
  .q-options:not(.is-grid):not(:has(.q-option:nth-child(5))) .q-icon { width: 56px; height: 56px; }
  .q-options:not(.is-grid):not(:has(.q-option:nth-child(5))) .q-icon :is(svg, img) { width: 40px; height: 40px; }
}
/* ── AND NOT ON FIVE, AT ANY HEIGHT ─────────────────────────────────────────
   There was a `@media (min-width: 768px) and (min-height: 880px)` here that
   gave the five-option screens the big icons on a tall laptop. Measured at
   1440x900, the tallest laptop anybody here uses, and the five-option shower
   question was STILL 19px behind the bar. There is no height at which five
   rows of 56px and a three-line help paragraph fit above a sticky action bar,
   so the height query was a threshold hunting for a value that does not exist.

   Five-option screens keep the 44px icon. A small icon is worse than a big one
   and better than an answer nobody can see. `shower_goal` and `timing` are the
   two affected; on a phone every screen is 24 to 28px anyway, so this changes
   nothing about what Josh sees on the device he reported from. */
.q-img {
  flex: none; width: 72px; height: 54px; overflow: hidden;
  border-radius: var(--ra-radius-md); background: var(--ra-stone-100);
}
.q-img img { width: 100%; height: 100%; object-fit: cover; }

.q-nav { display: flex; justify-content: space-between; align-items: center; gap: var(--ra-space-4); }

@media (prefers-reduced-motion: reduce) {
  .q-option { transition: none; }
  .q-option:active { transform: none; }
}

/* The two questionnaire screens that take a value rather than a choice. */
/* `max-width` without `margin-inline: auto` left a 22rem field against the left
   edge of a 40rem centred card. Josh: "The postal code fill-in needs to be
   centered. It looks weird here." The same class is the municipality select. */
.q-input {
  width: 100%; max-width: 22rem; margin-inline: auto; display: block;
  min-height: 52px;
  padding: var(--ra-space-3) var(--ra-space-4);
  font: inherit; font-size: var(--ra-text-body);
  background: var(--ra-surface);
  border: 1px solid var(--ra-border-input);
  border-radius: var(--ra-radius-md);
  color: var(--ra-text);
}
.q-input[aria-invalid="true"] { border-color: var(--ra-error-600); border-width: 2px; }
/* ── THE ERROR SITS UNDER ITS INPUT, AND ONLY UNDER ITS INPUT ───────────────
   Josh: "Centre the postal validation message directly beneath its input."

   The input is `max-width: 22rem; margin-inline: auto`, so it is a 352px box in
   the middle of a 640px column. The error was a plain sibling paragraph with no
   width of its own, so it spanned the WHOLE column and left-aligned: the message
   started 144px to the left of the field it was about.

   Same width, same centring, same axis. `min-height` reserves the line so the
   questionnaire does not jump by one line-height when a message appears and
   disappears: on a short screen that shift moves the Continue button under the
   cursor mid-click. */
.q-error {
  margin: var(--ra-space-2) auto 0;
  width: 100%; max-width: 22rem;
  text-align: center;
  color: var(--ra-error-600); font-size: var(--ra-text-small);
}
.field:has(.q-input) { justify-items: center; }
/* Reserved only where an error can appear, so other fields are unaffected. */
.q-screen .field > .q-error[hidden] { display: block; visibility: hidden; min-height: 1lh; }

/* The review screen: what the contractor receives, before it is sent. */
.review { display: grid; gap: 0; margin: 0; }
.review > div {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--ra-space-2) var(--ra-space-5);
  padding: var(--ra-space-4) 0;
  border-bottom: 1px solid var(--ra-border);
}
.review > div:last-child { border-bottom: 0; }
.review dt { margin: 0; font-size: var(--ra-text-small); color: var(--ra-text-muted); }
.review dd {
  margin: 0; font-weight: var(--ra-fw-label); overflow-wrap: anywhere;
  /* The value takes the room it needs and Change sits at the end of the row.
     `start` rather than `center` so a value that wraps to three lines keeps the
     control beside its first line instead of floating halfway down. */
  display: flex; align-items: start; justify-content: space-between;
  gap: var(--ra-space-4);
}
.review-value { min-width: 0; }

/* ── THE PER-ROW CHANGE CONTROL ─────────────────────────────────────────────
   A link in appearance, a button in behaviour, because it changes state rather
   than navigating. `flex: none` so it is never squeezed by a long value, and a
   44px target because it is a real control on a phone.

   The accessible name is set on the element and names the QUESTION. Twelve
   buttons all announcing "Change" would tell somebody nothing about which row
   they are on, which is the whole difficulty this screen had before. */
.review-change {
  flex: none; align-self: start;
  min-height: 44px; padding: var(--ra-space-2) var(--ra-space-3);
  margin-block-start: calc(var(--ra-space-2) * -1);
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: var(--ra-text-small); font-weight: var(--ra-fw-label);
  color: var(--ra-brand); text-decoration: underline; text-underline-offset: 3px;
  border-radius: var(--ra-radius-sm);
}
.review-change:hover { color: var(--ra-secondary); }

/* Where a required answer is missing after a branch change, so the review never
   looks complete when it is not. */
.review > div.is-missing dd .review-value { color: var(--ra-error-600); }

/* A brief, non-essential acknowledgement that THIS row is the one that moved.
   Colour is not the only signal: focus moves to the row's own control. */
.review > div.is-just-changed { background: var(--ra-selected-bg); }
@media (prefers-reduced-motion: reduce) {
  .review > div.is-just-changed { background: none; }
}

/* ── A LANDING PAGE'S LEGAL LINKS, IN ONE ROW ───────────────────────────────
   The site footer lays its links out in columns under headings. A landing page
   carries three and no headings, so the column grid left them stacked in a
   single narrow strip with a lot of empty space beside it. */
/* `.site-footer ul` sets display:grid and is MORE SPECIFIC than a bare class,
   so the first version of this rule lost and the three links stacked in a
   narrow strip exactly as before. Scoped the same way, so it is specificity
   that decides rather than source order. */
.site-footer ul.footer-inline {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--ra-space-4);
}
.site-footer ul.footer-inline a { color: inherit; }

/* ── FIND PROS IN ANY TOWN ──────────────────────────────────────────────────
   HomeBuddy's equivalent is a grid of states. We have eight municipalities, so
   the grid is wider per item and narrower overall, and it stays a list: these
   are links to a prefilled form, not a set of options to weigh up. */
.towns {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--ra-space-3);
  grid-template-columns: 1fr;
}
/* Two up from the narrowest phone: eight towns stacked one per row is a
   screen and a half of scrolling for a list somebody is scanning for one word. */
.towns { grid-template-columns: 1fr 1fr; }
@media (max-width: 359px) { .towns { grid-template-columns: 1fr; } }
@media (min-width: 900px) { .towns { grid-template-columns: repeat(4, 1fr); } }
.town {
  display: flex; align-items: center; justify-content: space-between; gap: var(--ra-space-3);
  /* 44px, like every other target on the site: P12 was a 32px row. */
  min-height: 44px;
  padding: var(--ra-space-3) var(--ra-space-4);
  border: 1px solid var(--ra-border); border-radius: var(--ra-radius-md);
  background: var(--ra-surface); color: var(--ra-heading);
  text-decoration: none; font-weight: var(--ra-fw-label);
}
.town:hover { border-color: var(--ra-cta); }
.town-arrow { color: var(--ra-text-muted); transition: transform var(--ra-site-motion-reveal) var(--ra-site-ease-enter); }
.town:hover .town-arrow { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) { .town-arrow { transition: none; } }

/* ── THE HERO'S ONE FORM ────────────────────────────────────────────────────
   Four full-width project links became one select and one button, because four
   choices above the fold read as four decisions and the point is that there is
   one thing to do here. It has to be the most obvious thing in the hero. */
.hero-start { display: grid; gap: var(--ra-space-3); margin-top: var(--ra-space-6); max-width: 34rem; }
.hero-start-label { font-weight: var(--ra-fw-label); color: var(--ra-heading); }
.hero-start-row { display: grid; gap: var(--ra-space-3); }
@media (min-width: 560px) {
  /* The select takes the room it needs and the button keeps its own width, so
     "Let's find out" never wraps. A wrapped call to action is the thing this
     whole change is meant to stop. */
  .hero-start-row { grid-template-columns: minmax(0, 1fr) auto; align-items: stretch; }
}
.hero-start-select {
  /* 48px, above the 44px target minimum, because this is the control the page
     exists for and it should feel like it. */
  min-height: 48px;
  padding: 0 var(--ra-space-4);
  font: inherit; font-size: var(--ra-text-body);
  color: var(--ra-heading);
  background: var(--ra-surface);
  border: 1px solid var(--ra-border-input);
  border-radius: var(--ra-radius-md);
  /* The native arrow, for the no-JavaScript fallback. When picker.mjs runs it
     hides this select entirely and draws .ra-picker instead; see below. */
}
.hero-start-select:focus-visible { outline: 2px solid var(--ra-cta); outline-offset: 2px; }
.hero-start-go { min-height: 48px; white-space: nowrap; }

/* The menu is the only route to these pages now, so its links need the same
   hover feedback the inline nav had. Without it a cursor over a link looks the
   same as a cursor over the panel. */
.nav-sheet a:not(.btn):hover { color: var(--ra-brand); }

/* ── THE WAY ACROSS ─────────────────────────────────────────────────────────
   One small link at the foot of each site pointing at the other. Deliberately
   quiet: most visitors are on the right site already, and the few who are not
   need to find it rather than be offered it. */
.footer-switch { padding-block: var(--ra-space-4) 0; }
.footer-switch-link {
  font-size: var(--ra-text-caption);
  color: inherit; opacity: .75; text-decoration: none;
}
.footer-switch-link:hover { opacity: 1; text-decoration: underline; }


/* ── OUR OWN PROJECT SELECTOR ───────────────────────────────────────────────
   Josh, 2026-10-25, screenshot of it open on his phone: "Can you make this
   selection more branded."

   A native select's OPEN state is drawn by the operating system and CSS does
   not reach it, so on iOS the most prominent control on the site handed over
   to a grey sheet in the system font. This is the replacement. The native
   select is still in the markup and still carries the value; with JavaScript
   off it is what you get, which is the right fallback.

   Sized to match .hero-start-select exactly, so the page does not move when
   the enhancement runs. */
.ra-picker { position: relative; display: grid; min-width: 0; }

.ra-picker-button {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--ra-space-3);
  width: 100%;
  min-height: 48px;
  padding: 0 var(--ra-space-4);
  font: inherit; font-size: var(--ra-text-body);
  text-align: left;
  color: var(--ra-heading);
  background: var(--ra-surface);
  border: 1px solid var(--ra-border-input);
  border-radius: var(--ra-radius-md);
  cursor: pointer;
}
.ra-picker-button:hover { border-color: var(--ra-cta); }
.ra-picker-button:focus-visible { outline: 2px solid var(--ra-cta); outline-offset: 2px; }
/* The chevron turns, so the control says which way it is going. */
.ra-picker-chev {
  flex: none; width: 20px; height: 20px; color: var(--ra-text-muted);
  transition: transform var(--ra-site-motion-fast) var(--ra-site-ease-standard);
}
.ra-picker-button[aria-expanded="true"] .ra-picker-chev { transform: rotate(180deg); }
/* The chosen value must not wrap the button onto two lines on a narrow phone. */
.ra-picker-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ra-picker-list {
  position: absolute; z-index: 20;
  top: calc(100% + var(--ra-space-2)); left: 0; right: 0;
  margin: 0; padding: var(--ra-space-2);
  list-style: none;
  background: var(--ra-surface);
  border: 1px solid var(--ra-border-input);
  border-radius: var(--ra-radius-md);
  box-shadow: var(--ra-shadow-lg);
  /* Taller than the viewport is possible on a short landscape phone, and a
     popup that cannot be scrolled is a popup with options nobody can reach. */
  max-height: min(60vh, 20rem); overflow-y: auto;
}
.ra-picker-option {
  padding: var(--ra-space-3);
  border-radius: var(--ra-radius-sm);
  cursor: pointer;
  /* Above the 44px target minimum, the same as the button it came from. */
  min-height: 44px; display: flex; align-items: center;
}
/* `is-active` is the keyboard's position and `aria-selected` is the chosen
   value. They are different things and looked identical in the first version,
   which made arrowing through the list look like it was choosing as it went. */
.ra-picker-option.is-active { background: var(--ra-surface-alt); }
.ra-picker-option[aria-selected="true"] {
  background: var(--ra-selected-bg);
  color: var(--ra-brand);
  font-weight: var(--ra-fw-label);
}
.ra-picker-option[aria-selected="true"].is-active { box-shadow: inset 0 0 0 1px var(--ra-cta); }

/* Opened upwards, when the button is close enough to the bottom of the screen
   that a downward popup would run past it. See picker.mjs: the hero sits near
   the fold on a phone and the last option was only reachable by scrolling the
   page, which on iOS closes the popup. */
.ra-picker.is-up .ra-picker-list {
  top: auto;
  bottom: calc(100% + var(--ra-space-2));
}

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

/* ── THE QUESTION WITH FIVE ANSWERS IS THE ONE THAT HAS TO FIT ──────────────
   With the footer out, every step fitted except the five-option one, which
   still overflowed 145px at 1366x768 and 15px at 1440x900. Five cards at 78px
   with a 44px icon in each is simply more than a 768px laptop has.

   Measured rather than guessed: the icon box is the biggest single saving at
   five cards, because it sets the card height on its own. 44px of icon plus
   32px of padding is a 76px card before the label is considered.

   Both tiers are DENSITY only. Nothing is hidden, nothing is cut, and the
   44x44 touch target survives where it matters, because these heights are
   laptops rather than phones.

   AT THE END OF THE FILE ON PURPOSE. These were first written above the
   `.q-option` base rule and changed nothing at all: a media query carries no
   extra specificity, so a later rule of equal specificity wins whatever the
   viewport is. The measured overflow was byte-identical before and after,
   which is what said the rules were inert rather than wrong. */
@media (max-height: 920px) {
  .q-options { gap: var(--ra-space-2); }
  .form-step { gap: var(--ra-space-4); }
}
@media (max-height: 840px) {
  .q-option { min-height: 56px; padding: var(--ra-space-2) var(--ra-space-4); }
  .q-icon { width: 36px; height: 36px; }
  .q-icon :is(svg, img) { width: 24px; height: 24px; }
}


/* 19px short at 1366x768, which is the commonest laptop on this list. The
   question is two lines of `--ra-text-h2` there; one step down the type scale
   pays for the last of it and still reads as the heading of the screen. */
@media (max-height: 780px) {
  .q-prompt { font-size: var(--ra-text-h3); line-height: var(--ra-lh-h3, 1.22); }
}

/* ── THE REVIEW AND THE CONSENT, WHICH CANNOT SIMPLY BE SHRUNK ──────────────
   Stage one got down to zero overflow at every laptop size by losing the
   footer and tightening the cards. Stages two and three cannot do the same:
   the review lists every answer the person gave, which is twelve rows on a
   full run, and the consent step is three fields plus three unbundled consents
   that are not allowed to be collapsed into one.

   So the honest fix is not to pretend they fit. It is to make sure the ACTION
   is never what you have to scroll to find, which is the actual complaint
   underneath "I have to scroll": the button pins to the bottom of the viewport
   and the content scrolls under it. Same pattern as `.q-nav` on stage one and
   the same pattern Josh accepted on the order modal.

   Measured before and after at 1280x800, 1366x768 and 1440x900. */
body.q-review-step .site-footer { display: none; }
@media (max-height: 920px) {
  body.q-review-step .form-nav,
  body.q-details-step .form-nav {
    position: sticky; bottom: 0; z-index: 2;
    padding-block: var(--ra-space-3);
    padding-block-end: calc(var(--ra-space-3) + env(safe-area-inset-bottom, 0px));
    background: var(--ra-surface);
    border-top: 1px solid var(--ra-border);
  }
  /* The review rows carry a label and a value each. At these heights the label
     does not need its own line. */
  body.q-review-step #intake-intro { display: none; }
  body.q-review-step .review > div { padding-block: var(--ra-space-2); }
  body.q-review-step .review dt { font-size: var(--ra-text-caption); }
}

/* The consent step, measured part by part at 1366x768: the three consents are
   347px of the 972px total, the three fields are 257, and the gaps are 112.
   Nothing here can be removed. The consents are deliberately unbundled and
   each one's description is what makes it informed, so these are density
   changes only, and the submit stays pinned by the rule above. */
@media (max-height: 920px) {
  body.q-details-step .form-step { gap: var(--ra-space-3); }
  body.q-details-step .q-input,
  body.q-details-step input[type="text"],
  body.q-details-step input[type="tel"],
  body.q-details-step input[type="email"] { min-height: 44px; }
  body.q-details-step .choice .d { line-height: 1.35; }
  body.q-details-step > .section p.small,
  body.q-details-step p.small { font-size: var(--ra-text-caption); margin-block-start: 0; }
}

/* ── A STICKY BAR MUST NOT BE ABLE TO HIDE AN ANSWER ────────────────────────
   `.q-nav` goes sticky at short heights so the action is always reachable. On
   the budget question that put the last two options UNDERNEATH it: 96px of them
   at 1280x800, 68px at 1366x768, 133px at 1440x900.

   The page did not scroll, so every overflow measurement said the screen
   fitted. `scrollHeight - innerHeight` cannot see content hidden behind a
   sticky element, which is why thirteen screens measured clean and the
   thirteenth was wrong. It took rendering all of them and looking.

   Two things, because the copy fix alone would leave the mechanism in place for
   the next long question: the options reserve the bar's height so nothing can
   ever sit behind it, and a six-option screen goes one density step tighter so
   it still fits once that space is reserved. */
@media (max-height: 920px) {
  .q-nav { --q-nav-h: 4.5rem; }
  .q-options { padding-block-end: 0; }
  .q-screen:has(.q-nav) .q-options { scroll-margin-block-end: var(--q-nav-h); }
  /* ── LIST SCREENS ONLY, AND `:not(.is-grid)` IS LOAD-BEARING ─────────────
     Six answers is the most any question asks, and a six-row LIST needs this
     tightening to clear the sticky bar.

     `max-height: 920px` is also true of 1440x900, which is the desktop width
     the grid screens are designed for, and `:has(.q-option:nth-child(6))`
     scores (0,4,0) against the grid rules' (0,3,0). So without the `:not()`
     these would win on the desktop grid and shrink its icons from 56px back to
     30px inside a square card built for them: the grid would have looked
     half-finished for a reason nothing in the grid CSS could explain.

     The grid screens set their own density per breakpoint and are excluded. */
  .q-options:not(.is-grid):has(.q-option:nth-child(6)) { gap: 6px; }
  .q-options:not(.is-grid):has(.q-option:nth-child(6)) .q-option { min-height: 48px; padding-block: 4px; }
  .q-options:not(.is-grid):has(.q-option:nth-child(6)) .q-icon { width: 30px; height: 30px; }
  .q-options:not(.is-grid):has(.q-option:nth-child(6)) .q-icon :is(svg, img) { width: 22px; height: 22px; }
}

/* ── SIX CARDS ON A SHORT PHONE ─────────────────────────────────────────────
   Appended after the `max-width: 559px` block on purpose: same specificity, so
   the later rule wins, and keeping it here means the block above can be read
   without this exception tangled into it.

   The budget question is six cards in two columns, three rows of 112px, under a
   prompt and a two-sentence help paragraph. On a 375x812 phone the last row sat
   28px behind the sticky action bar. Measured, not guessed, and only on the
   short phone: a 390x844 one fits, which is why this carries a height.

   84px is still nearly twice the 44px minimum target the brief sets, so the
   floor that matters is untouched. 96px left 8px of the last row behind the
   bar, which is a sliver and still a sliver somebody cannot tap. */
@media (max-width: 559px) and (max-height: 820px) {
  .q-options.is-grid .q-option { min-height: 84px; }
}

/* ── THE EXTRACTED ARTWORK IS A RASTER IN THE SAME BOX ──────────────────────
   The seventeen intake icons are Josh's original ChatGPT artwork, lifted out
   of the source sheets rather than redrawn, so they are PNGs rather than inline
   SVG. They render inside the same `.q-icon` box, and the six sizing rules
   above now say `:is(svg, img)` so there is one set of sizes rather than two
   that can disagree.

   `object-fit: contain` is what keeps the proportions: the aspect ratios differ
   on purpose, the project illustrations being wide and the damage corners
   nearly square, so the sizing rules give each one a square box and the image
   fits inside it at its own shape instead of being stretched to fill. */
/* ── THE RASTER TAKES THE SAME SIZES THE SVGS TOOK ──────────────────────────
   The six sizing rules above say `:is(svg, img)`, so there is one set of sizes
   for both and no second set to disagree with it.

   A `width: 100%` override to make the raster fill its whole box was tried and
   removed. It did nothing: every one of those rules is inside a
   `.q-options:not(.is-grid):not(:has(.q-option:nth-child(5)))` selector, which
   scores (0,5,1) against this rule's (0,1,1), so the image kept drawing at 40px
   in a 56px box and the browser was right. Matching that specificity would mean
   six more overrides, one per breakpoint, for a modest gain in apparent size,
   and six rules that can drift apart is how the first icon regression happened.
   Trimming the extraction canvas from ten source pixels to two recovered most
   of it with no CSS at all. */
.q-icon img { object-fit: contain; }

/* ══════════════════════════════════════════════════════════════════════════
   THE FOUR-SQUARE CODE INPUT
   ══════════════════════════════════════════════════════════════════════════

   Josh asked for four digits in a four-square input. Built from the same
   tokens as every other field on the site, so it reads as part of this form
   rather than as a widget dropped into it.

   ── THE WIDTH OVERRIDE IS NOT OPTIONAL ─────────────────────────────────────

   The global input rule above sets `width: 100%` on every input that is not a
   checkbox, radio or button, which is right for a name field and would make
   each of these four boxes fill the row and stack. `flex: none` plus an
   explicit width is what keeps them square and on one line.

   ── WHY THE BOXES ARE 56px AND NOT 48 ──────────────────────────────────────

   48px is the minimum touch target used everywhere else on the site. These sit
   in a row of four with gaps, a thumb reaches them at an angle rather than
   straight on, and a mistyped digit here costs the whole code. 56 is still
   comfortable at 320px: 4 x 56 + 3 x 12 gaps = 260px inside a 320px viewport
   with the page's own gutters. */
/* ── LEFT, WITH EVERYTHING ELSE ON THE PAGE ────────────────────────────────
   My first version centred the boxes, the buttons and the captions, which
   looked reasonable in isolation and wrong on the page: the heading and the
   lede above them are left-aligned, so the panel split into a left-aligned top
   half and a centred bottom half with the boxes starting 200px to the right of
   the sentence that introduces them. Found by looking at it.

   The confirmation panels this sits next to are left-aligned too, so this is
   consistency with the page rather than a preference. */
.otp-boxes {
  display: flex;
  gap: var(--ra-space-3);
  margin-block: var(--ra-space-5);
}

.otp-box {
  /* Excluded from the global input rule above, so this declares everything:
     the generic rule supplies no background, border or colour here. */
  width: 56px;
  flex: none;
  min-height: 56px;
  height: 56px;
  padding: 0;
  color: var(--ra-text);
  background: var(--ra-surface);
  border-style: solid;
  border-color: var(--ra-border-input);
  transition: border-color var(--ra-site-motion-fast) var(--ra-site-ease-standard);
  text-align: center;
  /* Tabular, so "1" and "8" occupy the same width and the row does not shift
     as somebody types. */
  font-variant-numeric: tabular-nums;
  font-size: var(--ra-text-h3);
  line-height: 1;
  font-weight: var(--ra-fw-h3);
  font-family: var(--ra-font-display);
  border-width: 2px;
  border-radius: var(--ra-radius-md);
  /* iOS zooms the page when a focused input's text is under 16px. The display
     size here is well over that, so this is belt and braces against a future
     smaller size. */
  -webkit-text-size-adjust: 100%;
}

/* A filled box is visibly done, so progress through the four is legible
   without counting. Border rather than background, to stay within the
   contrast the rest of the form uses. */
.otp-box[data-filled="true"] { border-color: var(--ra-secondary); }

.otp-box:focus-visible {
  /* The site's focus ring, not the browser's, and it must be visible against
     the thicker border these carry. */
  outline: 3px solid var(--ra-focus-ring);
  outline-offset: 2px;
  border-color: var(--ra-secondary);
}

/* Every box shows the error, because the code is wrong as a whole and
   highlighting one digit would imply we know which one. */
.otp-boxes[data-state="wrong"] .otp-box { border-color: var(--ra-error-600); }

.otp-msg {
  min-height: 1.5rem;         /* reserved, so the row does not jump when a message appears */
  margin: 0;
  font-size: var(--ra-text-small);
  line-height: var(--ra-lh-caption);
  color: var(--ra-text-muted);
}
.otp-msg[data-tone="error"] { color: var(--ra-error-600); font-weight: var(--ra-fw-label); }
.otp-msg[data-tone="ok"] { color: var(--ra-green-800); font-weight: var(--ra-fw-label); }

#otp-foot {
  color: var(--ra-text-muted);
  min-height: 1.25rem;
}

/* ── 320px, WHICH IS THE NARROWEST VIEWPORT WE SUPPORT ────────────────────
   Four 56px boxes plus three 12px gaps is 260px. Inside the page gutters that
   fits, but barely, and a user-agent font scale can push it over. Below 360
   the boxes give a little and the gap tightens rather than the row
   overflowing, which is the reflow rule the rest of this stylesheet follows. */
@media (max-width: 360px) {
  .otp-boxes { gap: var(--ra-space-2); }
  .otp-box { width: 48px; min-height: 48px; height: 48px; font-size: var(--ra-text-h4); }
}
