/* ══════════════════════════════════════════════════════════════════════════
   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);
}
.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 ─────────────────────────────────────────────────────────── */
.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); }
.header-in { display: flex; align-items: center; gap: var(--ra-space-6); padding-block: var(--ra-space-4); }
/* ── 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; }
.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). */
.hero { background: var(--ra-site-hero-image-background); overflow: hidden; }
/* 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. */
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"]),
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); }
.progress .track { display: flex; gap: var(--ra-space-2); flex: 1; }
.progress .track i {
  position: relative; height: 3px; flex: 1; background: var(--ra-stone-200);
  border-radius: var(--ra-radius-full); overflow: hidden;
}
/* The fill sweeps left to right rather than appearing, so progress reads as
   movement forward. */
.progress .track i::after {
  content: ""; position: absolute; inset: 0; background: var(--ra-brand);
  transform: scaleX(0); transform-origin: left;
  /* --ra-site-motion-slow has never existed, so this was silently running on
     the 360ms fallback: a hardcoded duration smuggled past the token check by
     a var() fallback. The kit's durations are fast 160, base 240, reveal 520,
     hero 650. A progress fill is an ordinary state change, which is what base
     is for and what the other thirteen transitions in this file use. */
  transition: transform var(--ra-site-motion-base) var(--ra-site-ease-standard);
}
.progress .track i.done::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) { .progress .track i::after { 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. */
.q-screen { display: grid; gap: var(--ra-space-5); max-width: 40rem; }
.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; }
/* 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); }
.q-option {
  display: flex; align-items: center; gap: var(--ra-space-4);
  width: 100%; min-height: 72px; text-align: left;
  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); }

.q-option-text { display: grid; gap: 2px; min-width: 0; }
.q-option-label { font-weight: var(--ra-fw-label); }
.q-option-help { font-size: var(--ra-text-small); color: var(--ra-text-muted); }

.q-icon { flex: none; display: grid; place-items: center; width: 44px; height: 44px; color: var(--ra-brand); }
.q-icon svg { width: 28px; height: 28px; }
.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. */
.q-input {
  width: 100%; max-width: 22rem;
  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; }
.q-error { margin: var(--ra-space-2) 0 0; color: var(--ra-error-600); font-size: var(--ra-text-small); }

/* 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; }

/* ── 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; }
}
