/*
 * washmates site styles (Version B of the owner's Claude Design prototype).
 *
 * Load order: tokens.css (the design-system tokens, copied VERBATIM from
 * .scratch/washmates-mvp/design/project/ds/washmates/tokens.css; never edit it here) then this file.
 * Fonts (Archivo Black + DM Sans) are self-hosted in public/fonts/; the @font-face rules and
 * preloads are inlined in each page's <head> by BaseLayout from src/utils/fonts.ts.
 *
 * Sections:
 *   1. Base          page ground, links, focus ring (from the design's <helmet> block)
 *   2. Components    wm-* classes, copied from ds/washmates/components/bundle.css
 *   3. Layout        header, hero, services, FAQ, contact band, footer, legal pages
 *   4. Quote form    the quote form, 4 steps on /quote since issue 16 (QuoteForm.dc.html)
 *   5. Cookie banner
 *   6. Quote page   /quote, the wizard on its own page (washmates-mvp issue 15, prototype B)
 *   7. Quote bar    the landing page's phone sticky "Get my free quote" bar (issue 22)
 *   8. No Moss Guarantee  the guarantee's pill, footer badge and inline link (owner 2026-10-20)
 *   9. Offer ticket  the time-limited offer's ticket, its explanation line and quote chip tag (washmates-mvp 56)
 *
 * Breakpoints (mobile-first; the design gives 390px and 1440px artboards only):
 *   < 640px     B-Mobile, 16px gutters
 *   640-1023px  in-between: mobile structure, 32px gutters, roomier cards
 *   >= 1024px   B-Desktop structure, 40px gutters (the full 80px lands at 1280px, where the
 *               design's 72px hero headline and full header fit without crowding)
 *   >= 1480px   content stops at 1320px and centres; backgrounds stay full width
 *               (washmates-mvp issue 21, the side margins of chilternsurface.co.uk)
 */

/* ============================================================================================
   1. Base
   ============================================================================================ */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  /* Same ground as the design artboards, so overscroll never flashes the teal :root tokens. */
  background: #f5faf9;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  font-family: var(--font-sans);
  background: var(--surface);
  color: var(--ink);
}

main {
  flex-grow: 1;
}

img {
  max-width: 100%;
}

a {
  color: var(--aqua-ink);
}

a:hover {
  color: var(--ink);
}

a:focus-visible,
button:focus-visible,
select:focus-visible,
input:focus-visible,
summary:focus-visible,
[tabindex='-1']:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Headings focused by script (step changes) only show a ring for keyboard users. */
[tabindex='-1']:focus:not(:focus-visible) {
  outline: none;
}

summary {
  cursor: pointer;
  list-style: none;
}

summary::-webkit-details-marker {
  display: none;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: 16px;
  top: -100px;
  z-index: 100;
  padding: 12px 16px;
  background: var(--surface-raised);
  color: var(--ink);
  border: 2px solid var(--aqua);
  border-radius: var(--radius-md);
}

.skip-link:focus {
  top: 12px;
}

/* A plain <button> that reads as an inline link (e.g. "Cookies" in the footer). */
.link-button {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

/* `font: inherit` above would otherwise beat the type class on the same element. */
.link-button.caption {
  font: var(--text-caption);
}

/* ---------- Spacing standard (washmates-seo 16) ----------
 *
 * One flow rule and one supporting-line rule, so a new guide, post or section needs no spacing CSS
 * of its own. Both live in the `flow` layer (order declared in tokens.css): they beat the margin
 * resets in the `prose` and `reset` layers whatever those resets' specificity, and any unlayered
 * component rule can still change a container's display (a grid on desktop, a row) without a fight.
 *
 * `.flow`: a vertical stack. Its children sit var(--flow-gap) apart; set --flow-gap on the
 *   container (--flow-prose by default). Children carry no margins of their own.
 *
 * `[data-support]`: a supporting line under the thing it belongs to (a note under a button, a
 *   caption or source line under a table or image, a VAT line under a price, chips under their
 *   intro, the hours line under a list). It sits exactly --gap-support (12px) below the element
 *   before it: inside a `.flow` its margin takes back the difference from the stack's gap; in a
 *   plain block it is the margin itself. Its bottom margin is always 0. Never set a margin on one
 *   by hand. Markdown captions are
 *   marked at build time (src/utils/rehypeMarkSupport.ts); e2e/spacing.spec.ts measures every one
 *   on every page in the sitemap.
 *
 * Edge: a `[data-support]` element must not itself be a `.flow` (it would read its own --flow-gap).
 * Lay its children out with a plain `gap` instead, as `.service-areas` does.
 */
@layer flow {
  .flow {
    display: flex;
    flex-direction: column;
    gap: var(--flow-gap, var(--flow-prose));
  }

  /* Both block margins: the line owns the space above it and none below, so a UA <p> margin (or
     any other default) can never push the next block down (the home FAQ answers have no reset). */
  [data-support] {
    margin-block: var(--gap-support) 0;
  }

  .flow > [data-support] {
    margin-block: calc(var(--gap-support) - var(--flow-gap, var(--flow-prose))) 0;
  }
}

/* ============================================================================================
   2. Components (from ds/washmates/components/bundle.css; the font @import is replaced by the
      <link> in each page's <head>)
   ============================================================================================ */

/* Logo */
.wm-logo { display: inline-flex; align-items: center; gap: var(--space-3); color: var(--ink); }
.wm-logo--stacked { flex-direction: column; gap: var(--space-2); text-align: center; }
.wm-mark-roof { stroke: var(--ink); }
.wm-mark-drop { fill: var(--aqua); }
.wm-mark-spark { fill: var(--sun); }
.wm-word { font-family: var(--font-display); line-height: 0.9; letter-spacing: -0.02em; color: var(--ink); }
.wm-word-accent { color: var(--aqua-ink); }

/* Button */
.wm-btn {
  font-family: var(--font-sans); font-weight: 700; font-size: 16px; line-height: 20px;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: 48px; padding: 0 var(--space-6); border-radius: var(--radius-md);
  border: 2px solid transparent; cursor: pointer; text-decoration: none;
}
.wm-btn--lg { min-height: 56px; font-size: 18px; padding: 0 var(--space-8); }
.wm-btn--block { display: flex; width: 100%; }
.wm-btn--primary { background: var(--aqua); color: var(--on-aqua); }
.wm-btn--primary:hover { filter: brightness(1.06); }
.wm-btn--secondary { background: transparent; color: var(--ink); border-color: var(--aqua); }
.wm-btn--secondary:hover { background: var(--surface-sunken); }
.wm-btn--ghost { background: transparent; color: var(--aqua-ink); text-decoration: underline; text-underline-offset: 3px; }
.wm-btn:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
.wm-btn:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }

/* Chip */
.wm-chip {
  font-family: var(--font-sans); font-weight: 700; font-size: 15px; line-height: 20px;
  display: inline-flex; align-items: center; min-height: 40px; padding: 0 18px;
  border: 2px solid var(--aqua); border-radius: var(--radius-pill);
  background: transparent; color: var(--ink);
}
button.wm-chip { cursor: pointer; min-height: 44px; }
button.wm-chip:hover { background: var(--surface-sunken); }
.wm-chip--selected, button.wm-chip--selected:hover { background: var(--aqua); color: var(--on-aqua); }
.wm-chip:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* Card */
.wm-card {
  background: var(--surface-raised); color: var(--ink);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-6); display: flex; flex-direction: column; gap: var(--space-3);
  font-family: var(--font-sans); font-size: 16px; line-height: 24px;
}
.wm-card-title { margin: 0; font-family: var(--font-sans); font-size: 22px; line-height: 28px; font-weight: 700; }

/* TextField */
.wm-field { display: flex; flex-direction: column; gap: var(--space-2); font-family: var(--font-sans); }
.wm-field-label { font-size: 15px; line-height: 20px; font-weight: 700; color: var(--ink); }
.wm-field-input {
  font-family: var(--font-sans); font-size: 16px; line-height: 24px; color: var(--ink);
  background: var(--surface-raised); min-height: 48px; padding: 0 var(--space-4);
  border: 2px solid var(--border-strong); border-radius: var(--radius-sm);
}
.wm-field-input::placeholder { color: var(--ink-muted); }
.wm-field-input:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; border-color: var(--aqua); }
.wm-field-hint { margin: 0; font-size: 13px; line-height: 18px; font-weight: 500; color: var(--ink-muted); }
.wm-field--error .wm-field-input { border-color: var(--danger); }
.wm-field--error .wm-field-hint { color: var(--danger); }

/* Site additions in the same style: min-width 0 stops long values widening grid cells. */
.wm-field-input {
  width: 100%;
  min-width: 0;
}

/* ============================================================================================
   3. Layout
   ============================================================================================ */

/* Side gutter, reused by every full-width block. */
:root {
  --gutter: 16px;
}

@media (min-width: 640px) {
  :root {
    --gutter: 32px;
  }
}

@media (min-width: 1024px) {
  :root {
    --gutter: 40px;
    /* The pinned desktop header (washmates-seo 14). Fixed heights, so the sticky offset and the
       anchor offset are plain CSS with no JS measuring: the contact strip (36px + its 1px
       border) scrolls away, the bar (88px) plus the header's 1px bottom border stays pinned. */
    --contact-strip-h: 37px;
    --site-header-bar-h: 88px;
    --site-header-pinned-h: calc(var(--site-header-bar-h) + 1px);
  }

  /* Every in-page jump (#faq, #services, #footer-contact, a guide's TOC links) lands below the
     pinned header bar; a target's own scroll-margin-top still adds its bit of air on top. */
  html {
    scroll-padding-top: var(--site-header-pinned-h);
  }
}

/* From 1280px the gutter is 80px until the content reaches 1320px (at 1480px); past that the
   gutter grows instead, so wide screens get even side margins while every background still
   runs edge to edge (washmates-mvp issue 21). */
@media (min-width: 1280px) {
  :root {
    --gutter: max(80px, calc((100vw - 1320px) / 2));
  }
}

/* Logo sizes. The component sets the word size inline; here it is a class so it can change
   with the breakpoint (mark = round(size x 1.6), as in bundle.js). */
.wm-logo--s24 .wm-word { font-size: 24px; }
.wm-logo--s24 .wm-mark { width: 38px; height: 38px; }
.wm-logo--s28 .wm-word { font-size: 28px; }
.wm-logo--s28 .wm-mark { width: 45px; height: 45px; }

/* ---------- Header ---------- */

.site-header {
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
}

.site-header__bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px var(--gutter);
}

.site-header__home {
  display: flex;
  flex-grow: 1;
  text-decoration: none;
}

/* 48px square outline buttons (the menu) from the mobile artboard. */
.icon-button {
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  color: var(--ink);
  cursor: pointer;
}

.icon-button:hover {
  color: var(--ink);
  background: var(--surface-sunken);
}

/* The menu icon swaps to a cross while the menu is open. */
.icon-button .icon-close,
.icon-button[aria-expanded='true'] .icon-menu {
  display: none;
}

.icon-button[aria-expanded='true'] .icon-close {
  display: block;
}

.site-nav,
.site-header__cta {
  display: none;
}

/* Mobile disclosure menu: pushes the page down rather than covering it. */
.mobile-menu {
  border-top: 1px solid var(--border);
}

.mobile-menu ul {
  list-style: none;
  margin: 0;
  padding: 8px 0;
}

.mobile-menu a {
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: 0 var(--gutter);
  color: var(--ink);
  text-decoration: none;
}

.mobile-menu a:hover {
  background: var(--surface-sunken);
}

/* washmates-seo 01: the service pages, indented under "Services" in the phone menu (non-home pages). */
.mobile-menu__sub {
  padding: 0 0 4px !important;
}

.mobile-menu__sub a {
  padding-left: calc(var(--gutter) + 16px);
  color: var(--ink-muted);
}

/* The desktop Services disclosure (non-home pages): hidden with the rest of .site-nav on phones. */
.site-nav__menu {
  position: relative;
}

.site-nav__menu summary {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}

.site-nav__menu summary::-webkit-details-marker {
  display: none;
}

.site-nav__menu summary:hover {
  color: var(--aqua-ink);
}

.site-nav__menu summary:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 2px;
}

.site-nav__menu[open] summary svg {
  transform: rotate(180deg);
}

.site-nav__list {
  position: absolute;
  top: calc(100% + 12px);
  left: -16px;
  z-index: 50;
  min-width: 280px;
  margin: 0;
  padding: 8px 0;
  list-style: none;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(15, 59, 58, 0.12);
}

.site-nav .site-nav__list a {
  display: block;
  padding: 10px 16px;
}

.site-nav .site-nav__list a:hover {
  background: var(--surface-sunken);
}

@media (min-width: 1024px) {
  /*
   * Pinned desktop header (washmates-seo 14). The whole <header> is sticky with a negative top
   * equal to the contact strip's height, so the strip scrolls away and the bar stops at the very
   * top of the viewport. It stays in the flow (no fixed positioning, no spacer), so nothing shifts
   * when it pins. It sits above page content (z 30) but under the cookie sheet (50); the Services
   * list's own z-index works inside this stacking context. body/html have no overflow set, which
   * would break sticky.
   */
  .site-header {
    position: sticky;
    top: calc(-1 * var(--contact-strip-h));
    z-index: 30;
  }

  .site-header__bar {
    gap: 24px;
    /* An exact height (20px padding + the 48px quote button), matching --site-header-bar-h. */
    height: var(--site-header-bar-h);
    padding: 20px var(--gutter);
  }

  .site-header__home {
    flex-grow: 0;
  }

  /* Header logo is size 24 on the mobile artboard, 28 on desktop. */
  .site-header .wm-logo--s24 .wm-word { font-size: 28px; }
  .site-header .wm-logo--s24 .wm-mark { width: 45px; height: 45px; }

  .site-nav {
    display: flex;
    gap: 32px;
    flex-grow: 1;
  }

  .site-nav a {
    color: var(--ink);
    text-decoration: none;
  }

  .site-nav a:hover {
    color: var(--aqua-ink);
  }

  .site-header__cta {
    display: inline-flex;
  }

  .site-header__menu,
  .mobile-menu {
    display: none !important;
  }
}

@media (min-width: 1280px) {
  .site-header__bar {
    gap: 48px;
  }
}

/* Once pinned, a soft shadow fades in under the bar (over the scroll that takes the strip away),
   so it reads as floating over the page. Scroll-driven, so no JS; browsers without scroll
   timelines keep the header's 1px bottom border alone. */
@supports (animation-timeline: scroll()) {
  @media (min-width: 1024px) {
    .site-header {
      animation: site-header-pinned linear both;
      animation-timeline: scroll(root block);
      animation-range: var(--contact-strip-h) calc(var(--contact-strip-h) + 24px);
    }
  }
}

@keyframes site-header-pinned {
  from {
    box-shadow: 0 0 0 rgba(15, 59, 58, 0);
  }

  to {
    box-shadow: 0 4px 16px rgba(15, 59, 58, 0.08);
  }
}

/* ---------- Contact strip (washmates-seo 13) ----------
   Above the header bar on every page but /quote (ContactStrip.astro). Phones: the number as text,
   email and WhatsApp as icon links, no hours (the footer has them). From 640px the email address
   shows as text; from 1024px the hours too, and everything sits at the right like the prototype.
   Links are at least 44px square below 1024px so they stay easy to tap. */

/* Same white as the bar (owner pick, 2026-10-09); the hairline keeps it a row of its own. */
.contact-strip {
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
  color: var(--ink-muted);
}

.contact-strip__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 0 var(--gutter);
  min-height: 44px;
}

.contact-strip__group {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.contact-strip__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  min-width: 44px;
  color: var(--ink-muted);
  text-decoration: none;
  white-space: nowrap;
}

.contact-strip__link:hover {
  color: var(--aqua-ink);
}

.contact-strip__link svg {
  flex-shrink: 0;
}

/* The number is the strip's one bold item. */
.contact-strip__phone {
  justify-content: flex-start;
  color: var(--ink);
  font-weight: 700;
}

/* The WhatsApp glyph in WhatsApp's dark teal-green: #25D366 is 1.7:1 on the light strip, below the
   3:1 a graphic needs; #128C7E is 4.1:1 on the white strip. */
.wa-glyph {
  color: #128c7e;
}

.contact-strip__email-text,
.contact-strip__hours {
  display: none;
}

@media (min-width: 640px) {
  .contact-strip__email-text {
    display: inline;
  }

  .contact-strip__group {
    gap: var(--space-4);
  }

  .contact-strip__email {
    min-width: 0;
  }
}

@media (min-width: 1024px) {
  /* An exact height (plus the strip's 1px border = --contact-strip-h), so the pinned header's
     negative top matches it. */
  .contact-strip__inner {
    justify-content: flex-end;
    gap: var(--space-6);
    min-height: 0;
    height: calc(var(--contact-strip-h) - 1px);
  }

  .contact-strip__group {
    gap: var(--space-6);
  }

  /* Icon links drop to a 24px target (WCAG 2.2 minimum) so the gaps match the text items. */
  .contact-strip__link {
    min-height: 36px;
    min-width: 24px;
  }

  .contact-strip__hours {
    display: inline;
    white-space: nowrap;
  }
}

/* ---------- Hero (#quote) ---------- */

.hero {
  --flow-gap: 20px;
  padding: 28px var(--gutter) 48px;
  display: flex;
  flex-direction: column;
  gap: var(--flow-gap);
}

/* On phones the copy column dissolves into the hero's flex column; on desktop it is a real column
   again. The order: badge, title, lead, then the services list, the button and the trust row
   (issue 22). Issue 30 moved the "why we ask" box's reasons onto /quote (.qf-why).
   .hero__copy is a `.flow` and the trust row is `data-support` (washmates-seo 16): the row sits
   --gap-support under the button at every width, whatever the hero's own gap (--flow-gap, inherited
   from .hero on phones and set on .hero__copy on desktop). */
.hero__copy {
  display: contents;
}

.hero__title { order: 1; margin: 0; }
.hero__lead { order: 2; margin: 0; color: var(--ink-muted); }
.hero__services,
.hero__cta,
.hero__rating,
.hero__guarantee { order: 3; }

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

/* Trust row (insurance + fixed quote). The design showed it on desktop only; the owner wants it on
   phones too (issue 06). Since issue 22 it sits under the button at every width. */
.hero__rating {
  display: flex;
  align-items: center;
  gap: 12px;
}

.hero__rating svg {
  flex-shrink: 0;
}

/* What we clean (issue 22): a ticked list, not controls. Two columns on phones, three on desktop.
   Main services first. Six ticks since washmates-garden-clearance 10 fill both grids evenly, so
   issue 42's Gutters-starts-a-new-line rule (.hero__services-minor) is gone. */
.hero__services {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 16px;
}

.hero__services li {
  display: flex;
  align-items: center;
  gap: 8px;
}

.hero__services svg {
  flex-shrink: 0;
}

/* "Get my free quote" (to /quote, which opens at step 1). The phone number that sat under it moved
   to the contact strip (washmates-seo 13). */
.hero__cta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.hero__cta .wm-btn,
.quote-bar .wm-btn {
  text-decoration: none;
}

/* The roof photo takes the old form's column on desktop. It is lazy, so phones (where it is
   hidden) never fetch it; desktop pays a little on first paint for that. */
.hero__media {
  display: none;
}

.hero__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--radius-lg);
}


/* 360px phones: the 50px headline would clip "YOUR ROOF?", so step it down a touch. */
@media (max-width: 379px) {
  .hero__title {
    font-size: 44px;
  }
}

@media (min-width: 1024px) {
  .hero {
    padding: 64px var(--gutter) 80px;
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: 24px;
    align-items: start;
  }

  .hero__copy {
    --flow-gap: 28px;
    grid-column: span 5;
    display: flex;
    flex-direction: column;
    gap: var(--flow-gap);
    padding-top: 16px;
  }

  .hero__services {
    grid-template-columns: repeat(3, max-content);
    gap: 12px 24px;
  }

  .hero__cta {
    flex-direction: row;
    gap: 24px;
  }

  .hero__cta .wm-btn {
    width: auto;
  }

  /* Positioned: the offer ticket (washmates-mvp 66) pins to its top-left corner. */
  .hero__media {
    position: relative;
    display: block;
    grid-column: 7 / span 6;
    align-self: stretch;
    min-height: 520px;
  }
}

/* display-xl (72px) from 1280px, where the 5-column copy column is wide enough for it. */
@media (min-width: 1280px) {
  .hero__title {
    font: var(--text-display-xl);
    letter-spacing: -2px;
  }
}

/* ---------- Section headings ---------- */

.section-title {
  margin: 0;
}

@media (min-width: 1024px) {
  /* display-m on phones, display-l on desktop (same as the artboards). */
  .section-title--grow {
    font: var(--text-display-l);
    letter-spacing: -1px;
  }
}

/* ---------- Services ---------- */

.services {
  padding: 0 var(--gutter) 48px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  scroll-margin-top: 16px;
}

.services__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

/* An odd number of cards: the first (roof) card takes the full row on phones, so none is orphaned. */
.services__grid > .service-card:first-child:nth-last-child(odd) {
  grid-column: 1 / -1;
}

.service-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.service-card img {
  display: block;
  width: 100%;
  height: 132px;
  object-fit: cover;
}

.service-card__body {
  padding: 12px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex-grow: 1;
}

.service-card__body h3,
.service-card__body p {
  margin: 0;
}

/* washmates-seo 01: a card whose service has its own page links its title there. */
.service-card__link {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--aqua);
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

.service-card__link:hover {
  color: var(--aqua-ink);
}

.service-card__link:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 2px;
}

/* The whole card is clickable (owner, 2026-10-20): the card's main link stretches over the photo
   and the text. Hover and keyboard focus show on the whole card. Any other link in a card would
   sit above it and keep its own page (the "patios" link did, until washmates-mvp 60). */
.service-card__link--card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

.service-card__link:not(.service-card__link--card) {
  position: relative;
  z-index: 2;
}

.service-card:has(.service-card__link--card) {
  transition: border-color 0.15s ease;
}

.service-card:has(.service-card__link--card):hover {
  border-color: var(--aqua-ink);
}

.service-card:has(.service-card__link--card):hover .service-card__link--card {
  color: var(--aqua-ink);
}

.service-card:has(.service-card__link--card:focus-visible) {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.service-card__link--card:focus-visible {
  outline: none;
}

/* The one-line description only appears on the desktop artboard; phones show the name only. */
.service-card__desc {
  display: none;
  color: var(--ink-muted);
}

/* "From £[PRICE]": caption in ink-muted on phones, label in aqua-ink on desktop. */
.service-card__price {
  color: var(--ink-muted);
}

@media (min-width: 640px) {
  .service-card img {
    height: 180px;
  }

  .service-card__desc {
    display: block;
  }
}

@media (min-width: 1024px) {
  .services {
    padding: 72px var(--gutter);
    background: var(--surface-sunken);
    gap: 32px;
    margin-bottom: 0;
  }

  .services__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 16px;
  }

  .services__grid > .service-card:first-child:nth-last-child(odd) {
    grid-column: auto;
  }

  /*
   * Exactly five cards (driveways and patios split, washmates-mvp 60): three over two, the
   * second row's two cards wider so both rows fill the width. Phones show roof alone, then 2×2.
   */
  .services__grid:has(> .service-card:nth-child(5):last-child) {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }

  .services__grid:has(> .service-card:nth-child(5):last-child) > .service-card {
    grid-column: span 2;
  }

  .services__grid:has(> .service-card:nth-child(5):last-child) > .service-card:nth-child(n + 4) {
    grid-column: span 3;
  }

  .service-card img {
    height: 220px;
  }

  .service-card__body {
    padding: 20px 24px 24px;
    gap: 8px;
  }

  /* label on phones, title on desktop. */
  .service-card__body h3 {
    font: var(--text-title);
  }

  /* The description grows so every card's price lines up at the bottom. */
  .service-card__desc {
    flex-grow: 1;
  }

  .service-card__price {
    font: var(--text-label);
    color: var(--aqua-ink);
  }
}

/* ---------- Why choose Washmates (owner, 2026-11-09, prototype C2) ----------
 *
 * Between Services and Reviews on the home page. A teal panel with the headline, three main
 * promises with aqua icon discs, then a "Plus" list with aqua-ink line icons. Stacked on phones;
 * from 1024px the panel sits beside the main promises and the Plus list runs in three columns.
 */
.why {
  padding: 0 var(--gutter) 48px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  scroll-margin-top: 16px;
}

.why__layout {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.why__lead {
  background: var(--surface);
  color: var(--ink);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.why__lead p {
  margin: 0;
  color: var(--ink-muted);
}

.why__lead-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 28px;
  line-height: 1.05;
  letter-spacing: -0.5px;
}

.why__accent {
  color: var(--aqua-ink);
}

.why__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.why__item {
  display: flex;
  gap: 14px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
}

.why__item:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.why__item > svg {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
}

.why__item svg {
  fill: none;
  stroke: var(--aqua-ink);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.why__item--main {
  padding: 16px 0;
}

/* The main promises' icon sits in an aqua disc (on-aqua stroke). */
.why__disc {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  background: var(--aqua);
  display: flex;
  align-items: center;
  justify-content: center;
}

.why__disc svg {
  width: 24px;
  height: 24px;
  stroke: var(--on-aqua);
}

.why__item-title {
  margin: 0 0 4px;
  font: var(--text-label);
}

.why__item--main .why__item-title {
  font-size: 18px;
  line-height: 24px;
}

.why__item p {
  margin: 0;
  color: var(--ink-muted);
}

.why__plus {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: 8px;
}

.why__plus-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 24px;
  line-height: 1.05;
}

/* washmates-mvp 108: the "See our service standards →" text link under the three promises,
   44px tall to tap. */
.why__more {
  margin: 4px 0 0;
}

.why__more a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--aqua-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.why__more a:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 4px;
}

@media (min-width: 1024px) {
  .why {
    padding: 72px var(--gutter);
    gap: 32px;
  }

  .why__layout {
    display: grid;
    grid-template-columns: 5fr 7fr;
    gap: 48px;
    align-items: start;
  }

  .why__lead {
    padding: 40px;
    gap: 20px;
  }

  .why__lead-title {
    font-size: 44px;
    line-height: 1.02;
    letter-spacing: -1px;
  }

  .why__item--main {
    gap: 20px;
    padding: 24px 0;
  }

  .why__item--main:first-child {
    padding-top: 0;
  }

  .why__disc {
    width: 52px;
    height: 52px;
  }

  .why__disc svg {
    width: 28px;
    height: 28px;
  }

  .why__item--main .why__item-title {
    font: var(--text-title);
  }

  .why__plus {
    gap: 16px;
  }

  .why__plus-title {
    font-size: 28px;
  }

  .why__plus-list {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: 40px;
  }

  .why__plus-list .why__item,
  .why__plus-list .why__item:last-child {
    padding: 20px 0;
    border-bottom: 0;
    border-top: 1px solid var(--border);
  }

  .why__plus-list .why__item > svg {
    width: 26px;
    height: 26px;
  }

  .why__plus-list .why__item-title {
    font-size: 17px;
    line-height: 24px;
  }
}

/* ---------- Google reviews (washmates-mvp issue 52) ----------
 *
 * The stars, the "Rated 5-star on Google" line (ReviewsLine.astro; the home hero's is a link to
 * #reviews) and the Reviews section (GoogleReviews.astro), between Services and Questions on the
 * home page. One native scroll-snap row at every width (it swipes, trackpad-scrolls and keyboard-
 * scrolls with no script); app.js (initReviews) adds the controls and moves it with scrollTo.
 *  - below 1024px (owner pick "mobile A"): one card with the next peeking, the 16px (32px from
 *    640px) gutter on both sides, and prev/next round the dots under it;
 *  - from 1024px (owner pick "Desktop B", 2026-10-17, replacing the moving row): exactly three
 *    cards across the content width (sized in container units of the row), the row clipped with
 *    rounded corners, big aqua arrows over its edges, then the dots (the current one a wider sun
 *    pill) and the pause/play button for the 3-second auto-advance. Until app.js takes over
 *    (`.is-enhanced`), the row keeps its scrollbar so a mouse can still reach every card.
 */

.wm-stars {
  display: inline-flex;
  gap: 2px;
  flex-shrink: 0;
}

.wm-star {
  display: block;
  fill: var(--sun);
}

/* A thin dark edge so the sun fill reads on a light ground (the hero, the white cards). */
.wm-stars--outlined .wm-star {
  stroke: var(--on-sun);
  stroke-width: 1;
}

.wm-star--empty {
  fill: none;
  stroke: var(--sun);
  stroke-width: 1.5;
}

.reviews-line {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--ink);
}

/* The <p> form's UA margins, in a layer so a data-support line's margin (flow layer) still wins. */
@layer reset {
  .reviews-line {
    margin: 0;
  }
}

/* The link form (the hero's): a 44px tap target, sized to its content. */
.reviews-line--link {
  display: inline-flex;
  align-self: flex-start;
  min-height: 44px;
  font: var(--text-body);
  text-decoration: none;
}

.reviews-line--link .reviews-line__text {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.reviews-line--link:hover .reviews-line__text {
  color: var(--aqua-ink);
}

.reviews-line--link:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 4px;
}

/* On phones .hero__copy is display: contents, so the line takes its place with the button's order. */
.hero__reviews {
  order: 3;
}

/* The section: a full-width teal band (data-theme="teal" in the markup). */
.reviews {
  padding: 40px 0;
  /* Questions has no top padding on phones (it follows Services' bottom padding there). */
  margin-bottom: 48px;
  background: var(--surface);
  color: var(--ink);
  display: flex;
  flex-direction: column;
  gap: 20px;
  scroll-margin-top: 16px;
}

.reviews__head {
  padding: 0 var(--gutter);
  display: flex;
  flex-direction: column;
  gap: var(--gap-heading);
}

.reviews__rating {
  color: var(--ink);
}

/* The row and, on desktop, its arrows. */
.reviews__stage {
  position: relative;
}

/* The row. position: relative because app.js measures the cards' offsetLeft from here. */
.reviews__viewport {
  position: relative;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  scrollbar-width: none;
}

.reviews__viewport::-webkit-scrollbar {
  display: none;
}

.reviews__viewport:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: -3px;
}

/* max-content, so the end gutter is part of the scrollable width. */
.reviews__list {
  list-style: none;
  margin: 0;
  padding: 0 var(--gutter);
  width: max-content;
  display: flex;
  gap: 12px;
}

/* White cards (data-theme="light" on each: surface-raised is white, ink the dark teal). The width
   leaves 40px of the screen for the gap and the next card's peek. */
.review-card {
  flex: 0 0 auto;
  width: min(300px, calc(100vw - 2 * var(--gutter) - 40px));
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 20px;
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  color: var(--ink);
}

.review-card__quote {
  margin: 0;
  flex-grow: 1;
}

/* Long reviews are cut with an ellipsis on screen; the full text is on Google. */
.review-card__text {
  margin: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  line-clamp: 6;
  overflow: hidden;
}

.review-card__by {
  margin: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

.review-card__source {
  color: var(--ink-muted);
}

/* Phones: prev, the dots and next (app.js unhides them). Desktop: the dots alone. */
.reviews__controls {
  padding: 0 var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.reviews__controls[hidden],
.reviews__arrow[hidden],
.reviews__dot[hidden] {
  display: none;
}

.reviews__nav {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 2px solid var(--aqua);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.reviews__nav:hover {
  background: var(--surface-sunken);
}

.reviews__nav:focus-visible,
.reviews__arrow:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* The desktop arrows (hidden below 1024px whatever app.js does). */
.reviews__arrow {
  display: none;
}

.reviews__dots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
}

.reviews__dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--border);
  transition: background-color 0.2s ease;
}

.reviews__dot.is-current {
  background: var(--aqua);
}

/* "Read all on Google": full width (inside the gutter) on phones. */
.reviews__all {
  margin: 0 var(--gutter);
  text-decoration: none;
}

.reviews__all svg {
  flex-shrink: 0;
}

@media (min-width: 640px) {
  .review-card {
    width: min(340px, calc(100vw - 2 * var(--gutter) - 40px));
  }

  /* Sized to its text from 640px, like the "Rather talk?" band's button. */
  .reviews__all {
    align-self: flex-start;
  }
}

@media (min-width: 1024px) {
  /* Heading and line on the left, "Read all on Google" on the right; the row, then the dots. */
  .reviews {
    padding: 72px 0 80px;
    margin-bottom: 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 24px;
    row-gap: 32px;
    align-items: end;
  }

  .reviews__head {
    grid-column: 1;
    grid-row: 1;
    padding: 0 0 0 var(--gutter);
  }

  .reviews__all {
    grid-column: 2;
    grid-row: 1;
    margin: 0 var(--gutter) 0 0;
  }

  .reviews__stage {
    grid-column: 1 / -1;
    grid-row: 2;
    margin: 0 var(--gutter);
  }

  /* The row fills the content width and clips the cards with rounded corners. It is the size
     container the cards measure (cqi), so exactly three fit with two gaps between them. */
  .reviews__viewport {
    container-type: inline-size;
    border-radius: var(--radius-lg);
    scroll-padding-inline: 0;
    scrollbar-width: auto;
  }

  .reviews__viewport::-webkit-scrollbar {
    display: initial;
  }

  /* Once app.js drives the row (arrows, dots), the scrollbar goes; trackpads and keys still scroll. */
  .reviews.is-enhanced .reviews__viewport {
    scrollbar-width: none;
  }

  .reviews.is-enhanced .reviews__viewport::-webkit-scrollbar {
    display: none;
  }

  .reviews__list {
    padding: 0;
    gap: 24px;
  }

  .review-card {
    width: calc((100cqi - 2 * 24px) / 3);
    gap: 14px;
    padding: var(--space-6);
  }

  .review-card__text {
    -webkit-line-clamp: 5;
    line-clamp: 5;
  }

  /* Big round aqua arrows over the row's edges, vertically centred (Desktop B). Always shown: app.js
     wraps them round at the start / end (owner, 2026-10-18). */
  .reviews__arrow {
    position: absolute;
    top: 50%;
    z-index: 1;
    width: 56px;
    height: 56px;
    padding: 0;
    border: 3px solid var(--surface);
    border-radius: var(--radius-pill);
    background: var(--aqua);
    color: var(--on-aqua);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transform: translateY(-50%);
  }

  .reviews__arrow:hover {
    filter: brightness(1.06);
  }

  .reviews__arrow--prev {
    left: -28px;
  }

  .reviews__arrow--next {
    right: -28px;
  }

  /* Dots centred under the row; the phone prev/next are gone. */
  .reviews__controls {
    grid-column: 1 / -1;
    grid-row: 3;
    justify-content: center;
  }

  .reviews__nav {
    display: none;
  }

  .reviews__dots {
    gap: 10px;
  }

  .reviews__dot {
    width: 10px;
    height: 10px;
  }
}

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

/* ---------- FAQ ---------- */

.faq {
  padding: 0 var(--gutter) 48px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  scroll-margin-top: 16px;
}

.faq .section-title {
  margin-bottom: 12px;
}

.faq__item {
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
}

.faq__item summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  min-height: 32px;
}

.faq__item summary svg {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
}

/* One chevron: points down when closed, up when open (as drawn in the artboards). */
.faq__item[open] summary svg {
  transform: rotate(180deg);
}

/* The answer is `data-support` (FaqItem.astro): --gap-support under the question, no margin here. */
.faq__item p {
  color: var(--ink-muted);
}

/* Answers glide open and shut (0.3s) where the browser can animate <details>; elsewhere they open
   instantly, as before. Off for anyone who has asked for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .faq__item {
    interpolate-size: allow-keywords;
  }

  .faq__item::details-content {
    block-size: 0;
    overflow-y: clip;
    transition: block-size 0.3s ease, content-visibility 0.3s allow-discrete;
  }

  .faq__item[open]::details-content {
    block-size: auto;
  }

  .faq__item summary svg {
    transition: transform 0.3s ease;
  }
}

@media (min-width: 1024px) {
  .faq {
    padding: 72px var(--gutter);
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: 24px;
    align-content: start;
    gap: 0 24px;
  }

  .faq .section-title {
    grid-column: span 4;
    margin: 0;
  }

  .faq__list {
    grid-column: 6 / span 7;
  }

  .faq__item {
    padding: 20px 0;
  }

  .faq__item summary {
    font: var(--text-title);
    min-height: 0;
  }

  .faq__item summary svg {
    width: 24px;
    height: 24px;
  }
}

/* ---------- "Rather talk?" band (data-theme="teal") ---------- */

.contact-band {
  padding: 40px var(--gutter);
  background: var(--surface);
  color: var(--ink);
  display: flex;
  flex-direction: column;
  gap: 16px;
  scroll-margin-top: 16px;
}

.contact-band__copy {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.contact-band h2,
.contact-band p {
  margin: 0;
}

/* The band's one action (washmates-seo 13): the quote button, full width on phones. */
.contact-band__cta {
  text-decoration: none;
}

.contact-band p {
  color: var(--ink-muted);
}

@media (min-width: 640px) {
  .contact-band {
    margin: 0 var(--gutter) 48px;
    padding: 40px;
    border-radius: var(--radius-lg);
  }

  .contact-band__cta.wm-btn--block {
    display: inline-flex;
    width: auto;
    align-self: flex-start;
  }
}

@media (min-width: 1024px) {
  .contact-band {
    margin: 0 var(--gutter) 72px;
    padding: 56px;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 48px;
  }

  .contact-band__copy {
    gap: 12px;
  }

  /* body on phones, body-l on desktop. */
  .contact-band__copy p {
    font: var(--text-body-l);
  }

  .contact-band__cta.wm-btn--block {
    align-self: center;
    flex-shrink: 0;
  }
}

/* ---------- Footer ---------- */

.site-footer {
  padding: 40px var(--gutter) 32px;
  background: var(--surface-sunken);
  display: flex;
  flex-direction: column;
  gap: 28px;
}

.site-footer__top {
  display: flex;
  flex-direction: column;
  gap: 28px;
}

.site-footer__nav {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 24px;
}

.site-footer__nav a {
  color: var(--ink);
  text-decoration: none;
}

.site-footer__nav a:hover {
  color: var(--aqua-ink);
}

.site-footer__bottom {
  display: flex;
  flex-direction: column-reverse;
  gap: 28px;
}

.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.site-footer__legal a,
.site-footer__legal .link-button,
.site-footer__copy {
  color: var(--ink-muted);
}

.site-footer__brand {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.site-footer__tagline {
  margin: 0;
  max-width: 32ch;
  color: var(--ink-muted);
}

.site-footer__copy {
  margin: 0;
}

/* The Contact column (washmates-seo 13): phone, email, WhatsApp (the word, never the number) and
   the hours, which the contact strip hides on phones. */
.site-footer__contact {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* The target of every "Contact" link: a little air above it when jumped to. */
  scroll-margin-top: var(--space-6);
}

.site-footer__heading {
  margin: 0;
  color: var(--ink-muted);
}

.site-footer__contact-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.site-footer__contact-list a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.site-footer__contact-list a:hover {
  color: var(--aqua-ink);
}

.site-footer__contact-list svg {
  flex-shrink: 0;
}

.site-footer__contact-list svg:not(.wa-glyph) {
  color: var(--aqua-ink);
}

.site-footer__hours {
  margin: 0;
  color: var(--ink-muted);
}

@media (min-width: 1024px) {
  .site-footer {
    padding: 48px var(--gutter) 40px;
    gap: 32px;
  }

  /* Brand · service links · Contact. The links column starts level with the Contact column. */
  .site-footer__top {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
    gap: 32px;
  }

  .site-footer__nav {
    flex: 1 1 0;
    min-width: 0;
    padding-top: 24px;
  }

  .site-footer__contact {
    flex-shrink: 0;
  }

  .site-footer__contact-list a {
    min-height: 32px;
    font: var(--text-label);
  }

  .site-footer__nav {
    display: flex;
    flex-wrap: wrap;
    gap: 12px 32px;
  }

  /* body on phones, label on desktop. */
  .site-footer__nav a {
    font: var(--text-label);
  }

  .site-footer__bottom {
    flex-direction: row;
    justify-content: space-between;
    gap: 32px;
    padding-top: 24px;
    border-top: 1px solid var(--border);
  }

  .site-footer__legal {
    gap: 24px;
  }
}

/* ---------- Legal / 404 pages ---------- */

.page {
  padding: 40px var(--gutter) 64px;
}

.page__inner {
  max-width: 720px;
}

.page h1 {
  margin: 0 0 12px;
}

/* Plain prose defaults (privacy, about, the 404), in the `prose` layer (washmates-seo 16): any
   component rule beats them, and guides.css's `reset` layer clears them inside guides and service
   pages, so neither ever needs a specificity bump to win. */
@layer prose {
  .page h2 {
    margin: 40px 0 12px;
  }

  .page p,
  .page ul {
    margin: 0 0 var(--flow-prose);
  }
}

.page li + li {
  margin-top: 8px;
}

.page__meta {
  color: var(--ink-muted);
}

/* The referral terms (washmates-referrals 11): the lede under "Last updated", and the sunken box
   for "When we're running a roof offer". Plain .page prose otherwise, as the privacy notice. */
.page__lede {
  margin-top: 20px;
}

.page__box {
  margin: 0 0 var(--flow-prose);
  padding: 16px 18px;
  border-radius: var(--radius-lg);
  background: var(--surface-sunken);
}

.page__box p:last-child {
  margin-bottom: 0;
}

@media (min-width: 1024px) {
  .page {
    padding: 64px var(--gutter) 80px;
  }
}

/* ============================================================================================
   4. Quote form (QuoteForm.dc.html)
   ============================================================================================ */

/* A size container so steps 2-5 go two-up when the FORM (not the viewport) has room: on a
   1024px laptop the form column is narrow even though the viewport is "desktop". */
.hero__form {
  container-type: inline-size;
}

.quote-form {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding: 24px;
  background: var(--surface-raised);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  font-family: var(--font-sans);
}

@media (max-width: 379px) {
  .quote-form {
    padding: 20px 16px;
  }
}

.qf-step,
.qf-done {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.qf-step[hidden],
.qf-done[hidden],
.qf-progress[hidden],
.qf-question[hidden],
.qf-select-field[hidden] {
  display: none;
}

.qf-progress {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.qf-progress__text {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
}

.qf-progress__step {
  color: var(--aqua-ink);
}

.qf-progress__time {
  color: var(--ink-muted);
  flex-shrink: 0;
}

.qf-progress__bar {
  display: flex;
  gap: 6px;
}

.qf-progress__seg {
  flex-grow: 1;
  height: 6px;
  border-radius: 999px;
  background: var(--surface-sunken);
}

.qf-progress__seg.is-done {
  background: var(--aqua);
}

.qf-fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* legend is excluded from flex layout in some engines; margin keeps the designed spacing. */
.qf-fieldset > legend {
  padding: 0;
  margin-bottom: 16px;
}

.qf-question {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.qf-question > legend {
  padding: 0;
  margin-bottom: 4px;
}

.qf-caption {
  color: var(--ink-muted);
}

/*
 * The "Why do we ask?" toggles (issue 30, .qf-why) were removed in washmates-mvp issue 50: the
 * owner's prototype pick dropped them on every step. The one reason kept, why we ask for the
 * address, is a line under the Address step's heading (.qf-address-why).
 */

.qf-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/* Services step: main services in the first row, gutter and solar in their own row under a thin
   divider (washmates-mvp issue 42, prototype E). Each row wraps on its own. */
.qf-service-groups {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.qf-chips--minor {
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

/*
 * Floors (washmates-mvp issue 50, prototype .chips--big): three big single-choice buttons in one
 * row, a large number with a small word under it ("1" / "Bungalow"). Same aqua outline and fill as
 * every chip, squarer corners, and well over the 44px tap target.
 */
.qf-chips--big {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

button.qf-big-chip {
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-height: 76px;
  padding: 8px;
  border-radius: 14px;
  text-align: center;
}

.qf-big-chip__n {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 26px;
  line-height: 1;
}

.qf-big-chip__word {
  font-weight: 500;
  font-size: 13px;
  line-height: 16px;
}

/*
 * When (washmates-mvp 68): the same big single-choice buttons as Floors, but four of them with a
 * few words each, so two by two rather than three in a row. Each label wraps inside its button
 * on a narrow phone; the buttons in a row stay the same height (grid).
 */
.qf-chips--when {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.qf-chips--when > .qf-big-chip {
  line-height: 20px;
  overflow-wrap: break-word;
}

/*
 * Your roof tiles (washmates-mvp 07, owner's prototype round 4, layout C): four single-choice
 * answers stacked one above the other. The two picture cards (TileShapeCard.astro) have a square
 * mosaic of the group's shapes on the left and the words on the right; Something else and Not sure
 * are full-width buttons under them (.qf-tile--plain). Resting: a hairline card like the access
 * boxes; chosen (aria-pressed): the aqua-ink border with an aqua halo, the sunken fill and a tick.
 * Every width is min-width: 0 or fixed, so nothing pushes the page sideways at 320px.
 * washmates-mvp 08: the driveway/patio surface on About your roof uses the same answers, four cards
 * with one photo each, then Other and Not sure.
 */
.qf-tiles {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}

.qf-tile {
  position: relative;
  display: flex;
  align-items: stretch;
  width: 100%;
  min-width: 0;
  margin: 0;
  padding: 0;
  font: inherit;
  color: var(--ink);
  text-align: left;
  background: var(--surface-raised);
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  cursor: pointer;
}

.qf-tile:hover {
  border-color: var(--aqua);
}

.qf-tile[aria-pressed='true'] {
  border-color: var(--aqua-ink);
  background: var(--surface-sunken);
  box-shadow: 0 0 0 2px var(--aqua);
}

/* The halo sits inside the 2px focus offset, so the ring still shows clear of it. */
.qf-tile:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 4px;
}

/* The square picture block: 2 x 2 for four shapes; with three the first spans both rows. */
.qf-tile__mosaic {
  flex: none;
  width: 132px;
  aspect-ratio: 1;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 2px;
  background: var(--surface-raised);
}

.qf-tile__mosaic--3 > picture:first-child {
  grid-row: span 2;
}

/* One photo (the driveway/patio surface cards, washmates-mvp 08) fills the whole square. */
.qf-tile__mosaic > picture:only-child {
  grid-area: 1 / 1 / -1 / -1;
}

.qf-tile__mosaic picture {
  display: block;
  min-width: 0;
  min-height: 0;
}

.qf-tile__mosaic img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--surface-sunken);
}

.qf-tile__text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  padding: 10px 44px 12px 12px;
}

.qf-tile__name {
  font: var(--text-label);
  font-size: 16px;
}

.qf-tile__hint {
  color: var(--ink-muted);
}

/* Something else / Not sure: a plain full-width button, the same card at rest and when chosen. */
.qf-tile--plain {
  align-items: center;
  min-height: 52px;
  padding: 0 48px 0 16px;
  font: var(--text-label);
  font-size: 16px;
}

/* The tick: an aqua-ink disc with a white check, shown only on the chosen answer. */
.qf-tile__tick {
  position: absolute;
  right: 10px;
  bottom: 12px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--aqua-ink);
  box-shadow: 0 0 0 2px var(--surface-raised);
  display: none;
  place-items: center;
}

.qf-tile__tick::after {
  content: '';
  width: 11px;
  height: 6px;
  border: solid var(--surface-raised);
  border-width: 0 0 2.5px 2.5px;
  transform: translateY(-1px) rotate(-45deg);
}

.qf-tile[aria-pressed='true'] .qf-tile__tick {
  display: grid;
}

.qf-tile--plain .qf-tile__tick {
  bottom: auto;
  top: 50%;
  transform: translateY(-50%);
}

@media (min-width: 1024px) {
  .qf-tile__mosaic {
    width: 168px;
  }
}

/*
 * Your garden (washmates-garden-clearance 01, owner's layout pick 2026-10-28: prototype B's picture
 * tiles). "What needs doing?" is multi-choice: the same `.qf-tile` cards as the roof tiles step, two
 * to a row, each a small aqua-ink line icon over its label with the tick disc top right; Something
 * else (.qf-tile--wide) spans the row, with no icon and its tick centred. Every tile is well over the
 * 44px tap target (96px; 56px for the wide one), and the columns are minmax(0, 1fr), so a long label
 * wraps inside its tile rather than pushing the page sideways at 320px.
 */
.qf-tiles--grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.qf-tile--icon {
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  min-height: 96px;
  /*
   * No room kept on the right for the tick disc: it sits top right beside the icon and ends above
   * the label, so the label has the full width and stays on fewer lines (two rows of three-line
   * labels pushed step 7 under the Back/Next bar at 360 x 640 on Linux fonts).
   */
  padding: 14px;
  font: var(--text-label);
  font-size: 16px;
  overflow-wrap: break-word;
}

.qf-tile--icon svg {
  flex: none;
  color: var(--aqua-ink);
}

/* The tick sits top right on an icon tile (bottom right on the roof tiles' cards). */
.qf-tile--icon .qf-tile__tick {
  top: 10px;
  bottom: auto;
}

.qf-tile--wide {
  grid-column: 1 / -1;
  flex-direction: row;
  align-items: center;
  min-height: 56px;
  /* Here the tick sits level with the label, so keep room on the right for it. */
  padding-right: 44px;
}

.qf-tile--wide .qf-tile__tick {
  top: 50%;
  transform: translateY(-50%);
}

/* "We take all the green waste away." under the tiles, with a tick in the aqua ink (CRO pass). */
.qf-garden-waste {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  color: var(--ink);
  font-weight: 700;
}

.qf-garden-waste svg {
  flex: none;
  color: var(--aqua-ink);
}

/*
 * The "Tell us what needs doing" box under the tiles shows only while Something else is ticked
 * (washmates-garden-clearance 09); `.wm-field`'s display: flex would otherwise beat [hidden].
 */
.wm-field[data-garden-notes][hidden] {
  display: none;
}

/*
 * "Roughly how big is the garden?" (prototype A): the When step's 2 x 2 big buttons, the size word
 * bold with what it compares to under it. Taller than When's, for the two lines.
 */
.qf-chips--size > .qf-big-chip {
  gap: 4px;
  min-height: 84px;
}

.qf-big-chip__size {
  font-weight: 700;
  font-size: 16px;
  line-height: 20px;
}

.qf-big-chip__compare {
  font-weight: 500;
  font-size: 13px;
  line-height: 16px;
}

/*
 * Access (washmates-mvp issue 50): each answer is a full-width card with its checkbox, the ticked
 * ones outlined in aqua, then "Don't know" and "None of the above" (issue 75 removed the "or"
 * divider).
 */
.qf-access {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.qf-check--card {
  align-items: flex-start;
  min-height: 44px;
  padding: 12px 14px;
  border: 2px solid var(--border);
  border-radius: 14px;
  background: var(--surface-raised);
}

.qf-check--card input {
  margin-top: 1px;
}

.qf-check--card:has(input:checked) {
  border-color: var(--aqua);
}

/*
 * washmates-mvp 107 (prototype A): the hairline between the access boxes and the
 * neighbour-permission question. The CRO pass asked for tight spacing on the now-taller step, so
 * the step's gap (24px, 20px on phones) is pulled in by 8px on each side of the line.
 */
.qf-divider {
  border: 0;
  border-top: 1px solid var(--border);
  margin: -8px 0;
}

/*
 * The neighbour-permission question's cards use the access card look, but native radios (round
 * markers) so they read as "pick one", unlike the square tick boxes above. 10px between the cards
 * and lines, as the access boxes have.
 */
.qf-neighbour {
  gap: 10px;
}

.qf-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
}

.qf-grid--property {
  gap: 28px 32px;
}

/* The design's `wide` prop: two columns in steps 2-5 when the form is wide enough. */
@container (min-width: 520px) {
  .qf-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* A step-2 cell that takes the whole row in the two-up grid (postcode, address control). */
.qf-span {
  grid-column: 1 / -1;
}

/* What the referrer will see (washmates-referrals 11, prototype 11·A): a sunken note under the
   referral code on Details, with the Referral terms link. app.js shows it while the field has a code. */
.qf-referral-note {
  margin: 0;
  padding: 12px;
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

.qf-referral-note a {
  color: var(--aqua-ink);
  font-weight: 700;
}

/* ---------- Postcode -> address picker (issue 09, prototype B "tap a row") ---------- */

/* Postcode input with room on the right for the lookup spinner. */
.qf-pc {
  position: relative;
}

.qf-pc .wm-field-input {
  padding-right: 48px;
}

.qf-pc__spinner {
  position: absolute;
  right: 14px;
  top: 50%;
  width: 20px;
  height: 20px;
  margin-top: -10px;
  border: 3px solid var(--border);
  border-top-color: var(--aqua-ink);
  border-radius: 50%;
  animation: qf-spin 0.8s linear infinite;
}

.qf-pc__spinner[hidden] {
  display: none;
}

@keyframes qf-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Slowed rather than stopped: a frozen ring would read as "stuck". */
@media (prefers-reduced-motion: reduce) {
  .qf-pc__spinner {
    animation-duration: 2.4s;
    animation-iteration-count: infinite;
  }
}

/* Lookup status (polite live region). Kept rendered, even when quiet, so it always announces. */
.qf-pc__status {
  margin: 0;
  font: var(--text-caption);
  color: var(--ink-muted);
}

.qf-pc__status--warn {
  color: var(--danger);
}

/*
 * Quiet (washmates-mvp issue 36 dropped the idle sentence): the empty line is 0px tall but would
 * still take the field's 8px flex gap, a blank strip under the hint. Pull it back over that gap
 * instead of hiding it, so the live region stays rendered. No line is reserved: the first message
 * ("Finding addresses…") arrives as the lookup starts, with the address list right behind it.
 */
.qf-pc__status:empty {
  margin-top: calc(-1 * var(--space-2));
}

.qf-address[hidden],
.qf-rows-fieldset[hidden],
.qf-picked[hidden],
.qf-manual[hidden],
.qf-link[hidden] {
  display: none;
}

.qf-rows-fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

.qf-rows-fieldset > legend {
  padding: 0;
  margin-bottom: 8px;
  color: var(--ink);
}

/* The address rows: about 5 visible, the rest scroll inside the box. */
.qf-rows {
  max-height: 292px;
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 2px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
}

.qf-row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  padding: 8px 16px;
  cursor: pointer;
  font: var(--text-body);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.qf-row + .qf-row {
  border-top: 1px solid var(--border);
}

.qf-row:hover {
  background: var(--surface-sunken);
}

.qf-row input {
  width: 20px;
  height: 20px;
  margin: 0;
  flex: none;
  accent-color: var(--aqua-ink);
}

/* The confirmed address, shown once a row is picked. */
.qf-picked {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  border: 2px solid var(--aqua);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--ink);
}

.qf-picked__text {
  min-width: 0;
  overflow-wrap: anywhere;
}

.qf-picked__text strong {
  display: block;
  font: var(--text-label);
}

.qf-picked__text span {
  display: block;
  font: var(--text-caption);
  color: var(--ink-muted);
}

/*
 * Roof estimate on the confirmed address card (washmates-mvp issue 13, prototype A). The card
 * stacks: the address row on top, then the roof panel behind a hairline. app.js fills the panel
 * with a loading state, then the result; with no figure the panel stays hidden.
 */
.qf-picked--roof {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
}

.qf-picked__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.roof[hidden],
.roof [hidden] {
  display: none !important;
}

.roof {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
  color: var(--ink);
  font-family: var(--font-sans);
}

.roof--a {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border-strong);
}

/*
 * Loading (washmates-mvp issue 76, prototype B "checklist of stages"): "Measuring your roof" with a
 * large percentage on the same line, a 10px progress bar, then the four stages. app.js builds it
 * once per run and only changes the fill width, the %, aria-valuenow and the stage classes.
 */
.roof__loading {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.roof__loading p {
  margin: 0;
}

.roof__progress-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
}

/* The display face with tabular figures, so the number doesn't jitter as it counts. */
.roof__pct {
  font: 400 22px/1 var(--font-display);
  font-variant-numeric: tabular-nums;
  min-width: 4ch;
  text-align: right;
}

.roof__track {
  position: relative;
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  box-shadow: inset 0 0 0 1px var(--border);
  overflow: hidden;
}

.roof__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  border-radius: inherit;
  background: var(--aqua-ink);
  transition: width 0.12s linear;
}

.roof__stages {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/*
 * Pending: muted text (plain --ink-muted, no opacity, so it keeps AA contrast) and a lighter ring.
 * Current: ink, bold, a spinning ring. Done: muted text with a filled tick.
 */
.roof__stage {
  display: flex;
  align-items: center;
  gap: 10px;
  font: 500 14px/20px var(--font-sans);
  color: var(--ink-muted);
}

.roof__stage-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  box-sizing: border-box;
  border-radius: 50%;
  border: 2px solid var(--border);
}

.roof__stage.is-current {
  color: var(--ink);
  font-weight: 700;
}

.roof__stage.is-current .roof__stage-icon {
  border-color: var(--aqua);
  border-top-color: var(--aqua-ink);
  animation: roof-spin 0.8s linear infinite;
}

.roof__stage.is-done .roof__stage-icon {
  border-color: var(--aqua-ink);
  background: var(--aqua-ink);
}

.roof__stage.is-done .roof__stage-icon::after {
  content: "";
  width: 8px;
  height: 4px;
  border: solid var(--surface-raised);
  border-width: 0 0 2px 2px;
  transform: translateY(-1px) rotate(-45deg);
}

@keyframes roof-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Result: its arrival is the one moment of motion on the panel. */
.roof__result {
  display: flex;
  flex-direction: column;
  gap: 12px;
  animation: roof-in 0.35s ease-out;
}

@keyframes roof-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Reduced motion: no spinner and no glide (app.js steps the bar 25/50/75/95, then 100), and the
   result simply appears. */
@media (prefers-reduced-motion: reduce) {
  .roof__fill {
    transition: none;
  }

  .roof__stage.is-current .roof__stage-icon {
    animation: none;
  }

  .roof__result {
    animation: none;
  }
}

.roof__title {
  margin: 0;
  font: var(--text-title);
}

.roof__title b {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: -0.5px;
}

.roof__note {
  margin: 0;
  color: var(--ink-muted);
}

/*
 * washmates-mvp 77: the main-house-only line sits 4px under the aerial-estimate line (the result's
 * 12px gap less 8px), so the two quiet captions read as one block and the Address step grows as
 * little as it can on phones.
 */
.roof__note + .roof__note {
  margin-top: -8px;
}

/* The slope count in the headline (issue 16): the same weight as the title, not the display face. */
.roof__count {
  white-space: nowrap;
}

/*
 * "See each slope" (issue 16): a link-style disclosure button under the share bar. The chevron
 * points down while the table is closed and up while it is open.
 */
.roof__toggle {
  gap: 6px;
  /* A full 44px tap target; the negative margin keeps it visually close to the bar above. */
  margin: -8px 0;
}

.roof__chevron {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 0.15s ease-out;
}

.roof__toggle[aria-expanded='true'] .roof__chevron {
  transform: rotate(180deg);
}

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

/* The proportions bar: one block per slope, in table order, flat parts hatched. Decorative; the
   table below carries the numbers for everyone. */
.roof__bar {
  display: flex;
  gap: 3px;
  height: 14px;
}

.roof__bar span {
  min-width: 6px;
  border-radius: 3px;
  background: var(--aqua);
}

.roof__bar span:nth-child(even) {
  background: var(--aqua-ink);
}

.roof__bar .roof__bar-flat {
  background: repeating-linear-gradient(135deg, var(--border-strong) 0 2px, transparent 2px 6px) !important;
  box-shadow: inset 0 0 0 1px var(--border-strong);
}

.roof__table {
  width: 100%;
  border-collapse: collapse;
  font: var(--text-body);
  font-variant-numeric: tabular-nums;
}

.roof__table th,
.roof__table td {
  padding: 6px 0;
  text-align: left;
  vertical-align: middle;
}

.roof__table td {
  padding-left: 12px;
  text-align: right;
  white-space: nowrap;
}

.roof__table thead th {
  padding-top: 0;
  font: var(--text-caption);
  color: var(--ink-muted);
}

.roof__table thead th + th {
  padding-left: 12px;
  text-align: right;
}

.roof__table tbody tr + tr th,
.roof__table tbody tr + tr td {
  border-top: 1px solid var(--border);
}

.roof__table tbody th {
  font-weight: 700;
}

.roof__dir {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

.roof__arrow {
  flex: none;
  width: 24px;
  height: 24px;
}

.roof__arrow circle {
  fill: none;
  stroke: var(--border-strong);
  stroke-width: 1.5;
}

.roof__arrow path {
  fill: var(--aqua-ink);
}

.roof__table .roof__flat th,
.roof__table .roof__flat td {
  color: var(--ink-muted);
  border-top: 1px dashed var(--border-strong) !important;
}

.roof__table .roof__flat th {
  font-weight: 500;
}

.roof__table tfoot th,
.roof__table tfoot td {
  padding-top: 8px;
  border-top: 2px solid var(--ink);
  font-weight: 700;
}

/* The sum may wrap between its terms on a narrow phone rather than push the page sideways. */
.roof__sum {
  display: inline-block;
  margin-left: 6px;
  font: var(--text-caption);
  color: var(--ink-muted);
}

/* "Change" and "My address isn't listed": buttons that read as links, with a usable tap target. */
.qf-link {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: var(--text-label);
  color: var(--aqua-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  flex-shrink: 0;
}

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

/* Typed fallback: first line + town, two-up when the form has room. */
.qf-manual {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
}

@container (min-width: 520px) {
  .qf-manual {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* An address error reddens the list box and the first-line box, not the optional town. */
.qf-address.wm-field--error .qf-rows {
  border-color: var(--danger);
}

.qf-address.wm-field--error .wm-field-input:not([data-error-control]) {
  border-color: var(--border-strong);
}

/* Native select, styled as in QuoteForm.dc.html. */
.qf-select-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.qf-select {
  width: 100%;
  min-height: 48px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--ink);
  font: var(--text-body);
}

.wm-field--error .qf-select {
  border: 2px solid var(--danger);
}

/* Checkbox rows (the access boxes and the contact-method boxes). */
.qf-check {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 48px;
  cursor: pointer;
}

.qf-check input {
  width: 22px;
  height: 22px;
  margin: 0;
  flex-shrink: 0;
  accent-color: var(--aqua-ink);
}

/*
 * How we may contact them (washmates-mvp 79): the consent line as the legend, then the boxes,
 * which wrap onto as few rows as fit to keep the Details step short on phones. Since
 * washmates-mvp 80 there are two, Email and Text/WhatsApp, which share one row at 360px. Each row is a 44px tap target. With an error app.js sets
 * aria-invalid on every box and the boxes turn red as well as the message under them (as the single
 * consent box did after the PR #92 preview). An outline, not a border: native checkboxes ignore
 * border in some browsers.
 */
.qf-contact-ways {
  border: 0;
  /* 16px under the message box (the step's 24px gap less 8), the gap between the fields above, so
     the Details step still fits a 390x844 phone above the sticky buttons. */
  margin: -8px 0 0;
  padding: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.qf-contact-ways__legend {
  padding: 0;
  margin-bottom: 0;
}

.qf-contact-ways__boxes {
  display: flex;
  flex-wrap: wrap;
  gap: 0 24px;
}

.qf-contact-ways .qf-check {
  min-height: 44px;
}

.qf-contact-ways input[aria-invalid='true'] {
  outline: 2px solid var(--danger);
  outline-offset: 2px;
  border-radius: 2px;
}

/*
 * The mobile number when a ticked Text/WhatsApp box needs it (washmates-mvp 79, 80): the same red
 * border as a field error, while the message itself sits under the boxes.
 */
.wm-field--flagged .wm-field-input {
  border-color: var(--danger);
}

.qf-error {
  margin: 0;
  color: var(--danger);
}

.qf-error:empty {
  display: none;
}

.qf-actions {
  display: flex;
  gap: 12px;
}

.qf-actions__grow {
  flex-grow: 1;
  min-width: 0;
}

/*
 * Back (washmates-mvp 50, owner pick 2 from the PR #92 preview): a soft grey fill with an arrow and
 * no outline. The aqua outline it had made it read as one more answer card on steps full of them.
 */
.qf-back {
  background: var(--surface-sunken);
  color: var(--ink);
  padding: 0 var(--space-6) 0 18px;
}

.qf-back:hover {
  filter: brightness(0.96);
}

.qf-back svg {
  flex-shrink: 0;
}

@media (max-width: 379px) {
  .qf-actions .wm-btn--lg {
    padding: 0 20px;
  }
}

/* The optional message box on the last step (issue 17): the text field's look, several lines tall. */
.qf-textarea {
  display: block;
  min-height: 0;
  padding: 10px var(--space-4);
  resize: vertical;
}

/* Form-level alert (network, rate limit, server). */
.qf-alert {
  padding: 16px;
  border: 2px solid var(--danger);
  border-radius: var(--radius-sm);
  color: var(--ink);
  background: var(--surface-raised);
}

.qf-alert[hidden] {
  display: none;
}

.qf-done {
  gap: 16px;
  padding: 8px 0;
}

.qf-done h2,
.qf-done p {
  margin: 0;
}

.qf-done p {
  color: var(--ink-muted);
}

.qf-done__icon {
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--aqua);
  color: var(--on-aqua);
}

.qf-done .wm-btn--ghost {
  padding: 0;
}

/*
 * "Help us price it" (washmates-mvp issue 18): the optional photo slots under the thank-you. A
 * hairline sets it apart from the thank-you above; the slots are one column on phones and on the
 * desktop card (about 550px wide), and two only once the form is 640px or wider, so a slot's
 * buttons never wrap mid-label. Each
 * slot is a thumbnail beside its prompt, status line and buttons; the state lives in
 * data-photo-state (empty / reading / uploading / done / failed / undecodable / full).
 */
.qf-photos {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding-top: 24px;
  border-top: 1px solid var(--border);
}

.qf-photos[hidden],
.qf-photos__intro[hidden],
.qf-photos__closed[hidden],
.qf-photos__finish[hidden],
.qf-photos__thanks[hidden],
.qf-photos__skipline[hidden],
.qf-photos__track[hidden],
.qf-photo [hidden] {
  display: none;
}

.qf-photos__intro {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.qf-photos h3,
.qf-photos p {
  margin: 0;
}

.qf-photos__intro p,
.qf-photos__closed {
  color: var(--ink-muted);
}

/* "Send photos" under the slots: full width on phones, its own width from 520px of form. */
.qf-photos__finish {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.qf-photos__finishing:empty {
  display: none;
}

/*
 * "Send photos" and, with a portal link, the "No photos? Go to your portal" line
 * (washmates-homeowner-portal 08; washmates-mvp 81, option C): on phones Send is full width and
 * the line sits centred under it; from 520px of form the line sits beside Send.
 */
.qf-photos__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
}

.qf-photos__send,
.qf-photos__track {
  width: 100%;
  min-height: 44px;
}

/* The "No photos?" line: centred under Send on phones (washmates-mvp 81). */
.qf-photos__skipline {
  width: 100%;
  font: var(--text-caption);
  text-align: center;
  color: var(--ink-muted);
}

/* Skip is the quiet way on: a text-style button, still a 44px tap target. */
.qf-photos__skip {
  display: inline-flex;
  min-height: 44px;
  padding: 0 4px;
  vertical-align: middle;
  font-size: 15px;
}

/* Track your job stays full width at every size (washmates-mvp 90, owner pick C). */
@container (min-width: 520px) {
  .qf-photos__send {
    width: auto;
    align-self: flex-start;
  }

  /* Beside Send on a wider form, so it reads as one row (washmates-mvp 81). */
  .qf-photos__skipline {
    width: auto;
    text-align: left;
  }
}

/* The confirmation after "Send photos" (it takes focus, so no outline on a programmatic focus). */
.qf-photos__thanks {
  font-weight: 700;
}

.qf-photos__thanks:focus {
  outline: none;
}

.qf-photos__thanks:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.qf-photos__slots {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

@container (min-width: 640px) {
  .qf-photos__slots {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.qf-photo {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* A slot takes focus (tabindex -1) when the control just used hides, e.g. the pick once it's sending. */
.qf-photo:focus {
  outline: none;
}

.qf-photo:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.qf-photo[data-photo-state='done'] {
  border-color: var(--success);
}

.qf-photo[data-photo-state='failed'],
.qf-photo[data-photo-state='undecodable'] {
  border-color: var(--danger);
}

.qf-photo__thumb {
  flex: none;
  width: 72px;
  height: 72px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

.qf-photo__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.qf-photo[data-photo-state='reading'] .qf-photo__img,
.qf-photo[data-photo-state='uploading'] .qf-photo__img {
  opacity: 0.6;
}

.qf-photo__body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.qf-photo__hint {
  color: var(--ink-muted);
}

.qf-photo__status:empty {
  display: none;
}

.qf-photo[data-photo-state='done'] .qf-photo__status {
  color: var(--success);
  font-weight: 700;
}

.qf-photo[data-photo-state='failed'] .qf-photo__status,
.qf-photo[data-photo-state='undecodable'] .qf-photo__status,
.qf-photo[data-photo-state='full'] .qf-photo__status {
  color: var(--danger);
}

.qf-photo__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-top: 4px;
}

/* Compact buttons inside a slot (44px still clears the tap-target minimum). */
.qf-photo__actions .wm-btn {
  min-height: 44px;
  padding: 0 16px;
  font-size: 15px;
}

.qf-photo__actions .wm-btn--ghost {
  padding: 0 4px;
}

/* The file input is visually hidden inside its label-button, so the label shows its focus. */
.qf-photo__pick:focus-within {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Honeypot: off-screen for people and hidden from assistive tech; bots fill it and are dropped. */
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ============================================================================================
   5. Cookie banner and Cookie settings (cookie-consent 02)

   Layer 1 (.cookie): the owner's layout (CC-1, from the energysavingtrust.org.uk banner): a white
   panel with a heading, one short paragraph, "How we use cookies" on its own line, then two equal
   rectangular buttons, ALLOW ALL COOKIES (ink fill) and COOKIE SETTINGS (ink-muted fill). A bottom
   sheet on phones, a floating card in the bottom-right corner from 768px. app.js publishes its height as --cookie-sheet-h,
   so the sticky bars on /quote and the landing page (issues 15 and 22) sit on top of it.
   Below 360px the two buttons stack, still the same size.
   Below 640px the paragraph is hidden (owner, 2026-10-21): heading, "How we use cookies" and the buttons only.

   Layer 2 (.cookie-panel): a modal <dialog> (top layer, so no z-index): Necessary, Analytics and
   Advertising, then Reject all, Allow all and Save settings as three equal buttons.

   Both use the light theme's tokens (data-theme="light" in the markup): white = surface-raised,
   "black" = ink, dark grey = ink-muted. Vertical rhythm is .flow (gap set here) and
   [data-support]; only UA margins are zeroed, never a supporting line's.
   ============================================================================================ */

.cookie {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  /* The home-indicator inset keeps the buttons tappable. */
  padding: var(--space-4) var(--gutter) calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
  background: var(--surface-raised);
  color: var(--ink);
  border-top: 1px solid var(--border);
  box-shadow: var(--shadow-sheet);
}

.cookie[hidden] {
  display: none;
}

.cookie__inner {
  --flow-gap: var(--space-3);
  max-width: 1280px;
  margin: 0 auto;
}

.cookie__title,
.cookie-panel__title {
  margin: 0;
  font: var(--text-title);
  font-size: 18px;
  line-height: 24px;
  color: var(--ink);
}

.cookie__text,
.cookie-panel__text,
.cookie__more {
  margin: 0;
  font: var(--text-body);
  font-size: 15px;
  line-height: 22px;
  color: var(--ink);
}

/* Underlined, in ink like the reference; 3px top/bottom padding lifts the tap area to 24px+. */
.cookie__link {
  padding: 3px 0;
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

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

.cookie__link:focus-visible {
  border-radius: 2px;
}

/* Two equal columns (1fr each, stretched to the taller label), so the buttons are always the same
   width and height whatever their text. */
.cookie__actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
}

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

/* Phones (owner, 2026-10-21, washmates-mvp 56): the slim notice. Only the heading, "How we use cookies" and
   the two buttons show; the paragraph is gone at this width (not just visually hidden), so the
   sheet takes less of the first screen. Tablets and desktop keep it. The heading still labels the
   region (aria-labelledby). */
@media (max-width: 639px) {
  .cookie__text {
    display: none;
  }
}

/* Rectangular, uppercase and bold (the reference), with 44px+ tap targets. A long label wraps
   inside its button rather than widening it. */
.cookie__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: 0;
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 14px;
  line-height: 18px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-align: center;
  color: var(--surface-raised);
  cursor: pointer;
}

.cookie__btn--primary {
  background: var(--ink);
}

.cookie__btn--secondary {
  background: var(--ink-muted);
}

.cookie__btn:hover {
  filter: brightness(1.25);
}

.cookie__btn:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

@media (min-width: 768px) {
  /* The floating card, tucked into the bottom-right corner (owner, 2026-10-18: inset by the
     page gutter it covered page content), now tall enough for the paragraph. */
  .cookie {
    left: auto;
    right: 16px;
    bottom: 16px;
    max-width: 460px;
    padding: var(--space-6);
    border: 1px solid var(--border);
  }
}

/* ---------- Layer 2: Cookie settings ---------- */

.cookie-panel {
  width: min(560px, calc(100% - 2 * var(--space-4)));
  max-height: calc(100dvh - 2 * var(--space-4));
  padding: 0;
  border: 0;
  background: var(--surface-raised);
  color: var(--ink);
  box-shadow: var(--shadow-sheet);
  overflow: auto;
  overscroll-behavior: contain;
}

.cookie-panel::backdrop {
  background: rgba(15, 59, 58, 0.55);
}

.cookie-panel__inner {
  --flow-gap: var(--space-4);
  padding: var(--space-6) var(--space-4);
}

@media (min-width: 640px) {
  .cookie-panel__inner {
    padding: var(--space-6);
  }
}

.cookie-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.cookie-panel__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
}

.cookie-cats {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--border);
}

.cookie-cat {
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--border);
}

.cookie-cat__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 32px;
}

.cookie-cat__name {
  margin: 0;
  font: var(--text-label);
  color: var(--ink);
}

label.cookie-cat__name {
  cursor: pointer;
}

.cookie-cat__always {
  font: var(--text-caption);
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* The category's description is [data-support]: the flow layer owns its margin, so none here. */
.cookie-cat__text {
  font: var(--text-caption);
  font-size: 14px;
  line-height: 20px;
  color: var(--ink-muted);
}

/* A native checkbox (role="switch") drawn as a switch: the track is the box, the knob ::before. */
.cookie-switch {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex-shrink: 0;
  width: 48px;
  height: 28px;
  margin: 0;
  border: 2px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.cookie-switch::before {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--border-strong);
  transition: transform 0.15s ease, background-color 0.15s ease;
}

.cookie-switch:checked {
  border-color: var(--ink);
  background: var(--ink);
}

.cookie-switch:checked::before {
  transform: translateX(20px);
  background: var(--surface-raised);
}

@media (prefers-reduced-motion: reduce) {
  .cookie-switch,
  .cookie-switch::before {
    transition: none;
  }
}

/* Three equal buttons: stacked on phones, one row from 480px. Reject all is first and in the same
   ink fill as Allow all, so refusing is as prominent as allowing (CC-2). */
.cookie-panel__actions {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2);
}

@media (min-width: 480px) {
  .cookie-panel__actions {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ------------------------------------------------------------------------------------------
   Quote chips (washmates-mvp issue 38): the locked Gutter chip and its notes.
   ------------------------------------------------------------------------------------------ */
/*
 * The Gutter chip while Roof cleaning is ticked (washmates-mvp issue 38): shown ticked, but locked
 * (aria-disabled, still focusable), with "Included with roof cleaning" on a second line inside the
 * chip, so the pill becomes a rounded box. The hover and active looks stay the selected ones.
 */
.wm-chip--locked {
  flex-direction: column;
  align-items: flex-start;
  padding: 6px 18px;
  border-radius: var(--radius-md);
  text-align: left;
}

button.wm-chip--locked {
  cursor: default;
}

.chip-note {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 13px;
  line-height: 18px;
  font-weight: 500;
}

.chip-note[hidden] {
  display: none;
}

.chip-note svg {
  flex-shrink: 0;
}

/* "Included with your roof clean" under "Where are the panels?" (issue 38): empty means hidden. */
.qf-note {
  margin: 0;
}

.qf-note:empty {
  display: none;
}

/* ============================================================================================
   6. Quote page (/quote): the wizard on its own page, washmates-mvp issue 15, prototype B
   "Focused, checkout-style". Slim header, a named stepper, the form in a 600px column with a
   line of fine print under it. Phones (< 640px) drop the card and stick the step buttons to the
   bottom of the screen.
   ============================================================================================ */

/* Slim header: logo (home) on the left, the phone number on the right. */
.qp-slim {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px var(--gutter);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
}

.qp-slim__home {
  display: inline-flex;
  text-decoration: none;
}

.qp-slim__call {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  color: var(--aqua-ink);
  text-decoration: none;
  white-space: nowrap;
}

/*
 * Narrow phones (washmates-mvp 82, the same steps as the landing header in 67): the row doesn't
 * shrink, so at full size the call link ended at 368px and the page scrolled sideways on any
 * screen under ~384px. The number stays visible and the link keeps its 44px height at every width.
 *  - under 380px: the smaller logo and a 12px row gap (the landing header's own gap; at 16px the
 *    row left only ~6px spare at 360);
 *  - under 360px: a smaller logo again, a 14px number, a smaller icon and tighter gaps.
 * Measured with the web fonts loaded, both steps leave about 10px between logo and number at 360
 * and 320; from 380px the full-size row fits as before (~6px spare at 390).
 */
@media (max-width: 379px) {
  .qp-slim {
    gap: 12px;
  }

  .qp-slim .wm-logo--s24 .wm-word {
    font-size: 20px;
  }

  .qp-slim .wm-logo--s24 .wm-mark {
    width: 32px;
    height: 32px;
  }
}

@media (max-width: 359px) {
  .qp-slim {
    gap: 8px;
  }

  .qp-slim .wm-logo--s24 .wm-word {
    font-size: 17px;
  }

  .qp-slim .wm-logo--s24 .wm-mark {
    width: 28px;
    height: 28px;
  }

  .qp-slim__call {
    gap: 4px;
    font-size: 14px;
  }

  .qp-slim__call svg {
    width: 18px;
    height: 18px;
  }
}

/* Bottom padding clears the cookie banner while it is open, so nothing ends up under it. */
.qp {
  padding: 16px var(--gutter) calc(32px + var(--cookie-sheet-h, 0px));
  background: var(--surface);
}

.qp__col {
  max-width: 600px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* The stepper replaces the form's own "Step n of 6" line, which app.js still keeps up to date. */
.qp .qf-progress {
  display: none;
}

/*
 * Named stepper: a bar per step; done and current steps are aqua, the current one bold. Six steps
 * since issue 30, seven since washmates-mvp 68 (When), eight on a roof job since washmates-mvp 07
 * (Tiles); five or six on a driveway/patio-only or gutter-only job since washmates-mvp 97/99 (skipped
 * steps hide), one short word each: a column never shrinks
 * below its word (max-content) and the columns share what is left, and words never break, so
 * nothing reads "Service s" on a 320px phone.
 */
.qp-stepper {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /*
   * One column per SHOWN item (washmates-mvp 07: Tiles shows only on a roof job, so seven or
   * eight), laid out by auto-flow so a hidden item leaves no empty column.
   */
  grid-auto-flow: column;
  grid-auto-columns: minmax(max-content, 1fr);
  gap: 6px;
}

.qp-stepper li {
  min-width: 0;
  padding-top: 8px;
  border-top: 4px solid var(--surface-sunken);
  color: var(--ink-muted);
  font: var(--text-caption);
  overflow-wrap: normal;
}

.qp-stepper li.is-done,
.qp-stepper li.is-current {
  border-top-color: var(--aqua);
}

.qp-stepper li.is-current {
  color: var(--ink);
  font-weight: 700;
}

/*
 * Seven words don't fit a 320px phone at the caption size (washmates-mvp 68 added When): below
 * 380px the words are a size smaller and the gaps narrower, so the row still never scrolls sideways.
 * The step was under 360px at first, but at exactly 360px the row ran 1px wide with the fallback
 * fonts (CI); from washmates-mvp 82 it switches with the slim header's first step.
 */
@media (max-width: 379.98px) {
  .qp-stepper {
    gap: 3px;
  }

  .qp-stepper li {
    font-size: 11px;
  }
}

/*
 * Eight words (a roof job shows Tiles, washmates-mvp 07) need more room again. With the web fonts
 * the row ran 3px wide at 320 and 390px and 13px at 380px; the 11px step now starts under 430px,
 * and under 360px the words drop to 10.5px with 2px gaps (16px or more to spare at every width,
 * still a few px with the wider fallback fonts CI uses). Seven-step jobs keep the rules above.
 * washmates-garden-clearance 01: a garden job without Roof cleaning can show eight words too (Garden
 * instead of Tiles, e.g. gutter + solar + garden: it ran 31px wide at 380px), so Garden switches
 * these on as well.
 */
@media (max-width: 429.98px) {
  .qp-stepper:has(> [data-stepper-tiles]:not([hidden]), > [data-stepper-garden]:not([hidden])) {
    gap: 3px;
  }

  .qp-stepper:has(> [data-stepper-tiles]:not([hidden]), > [data-stepper-garden]:not([hidden])) li {
    font-size: 11px;
  }
}

@media (max-width: 359.98px) {
  .qp-stepper:has(> [data-stepper-tiles]:not([hidden]), > [data-stepper-garden]:not([hidden])) {
    gap: 2px;
  }

  .qp-stepper:has(> [data-stepper-tiles]:not([hidden]), > [data-stepper-garden]:not([hidden])) li {
    font-size: 10.5px;
  }
}

/*
 * Nine words: a roof + garden job shows Tiles and Garden (washmates-garden-clearance 01). Measured
 * with the bold current word in every position, web fonts and the fallback fonts CI uses: at the
 * eight-word sizes the row ran 15px wide at 430px, 5px at 360px and 23px at 320px. So 11px from
 * 480px down, 10.5px with 2px gaps under 380px and 9.5px with 1px gaps under 360px, which leaves
 * 13px or more to spare at every width from 320px. A garden job without Roof cleaning has eight
 * words at most and takes the eight-word rules above, plus 10px under 340px: "Garden" is wider
 * than "Tiles", and gutter + solar + garden had under 2px to spare at 320px with the web fonts.
 */
@media (max-width: 339.98px) {
  .qp-stepper:has(> [data-stepper-garden]:not([hidden])) li {
    font-size: 10px;
  }
}

@media (max-width: 479.98px) {
  .qp-stepper:has(> [data-stepper-tiles]:not([hidden])):has(> [data-stepper-garden]:not([hidden])) {
    gap: 3px;
  }

  .qp-stepper:has(> [data-stepper-tiles]:not([hidden])):has(> [data-stepper-garden]:not([hidden])) li {
    font-size: 11px;
  }
}

@media (max-width: 379.98px) {
  .qp-stepper:has(> [data-stepper-tiles]:not([hidden])):has(> [data-stepper-garden]:not([hidden])) {
    gap: 2px;
  }

  .qp-stepper:has(> [data-stepper-tiles]:not([hidden])):has(> [data-stepper-garden]:not([hidden])) li {
    font-size: 10.5px;
  }
}

@media (max-width: 359.98px) {
  .qp-stepper:has(> [data-stepper-tiles]:not([hidden])):has(> [data-stepper-garden]:not([hidden])) {
    gap: 1px;
  }

  .qp-stepper:has(> [data-stepper-tiles]:not([hidden])):has(> [data-stepper-garden]:not([hidden])) li {
    font-size: 9.5px;
  }
}

/*
 * One centred line under the form: Privacy and Cookies, which live here because there's no footer.
 * The padlock trust line that sat above them moved under the step buttons in issue 50 (.qf-trust).
 */
.qp-fine {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  color: var(--ink-muted);
  text-align: center;
}

.qp-fine p {
  margin: 0;
}

/*
 * Under the step buttons (washmates-mvp issue 50, prototype "rating under the button"): the padlock
 * line, then the rating line, both centred, 20px apart so they read as two separate lines (owner, PR
 * #92 preview: 12px looked lumped together). The form's own gap is pulled in so the lines read as
 * part of the buttons, not as a new block.
 */
.qf-trust {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: -12px;
  color: var(--ink-muted);
  text-align: center;
}

.qf-trust[hidden] {
  display: none;
}

.qf-trust p {
  margin: 0;
}

/* The padlock sits in the text run, so it leads the first line when the sentence wraps on phones. */
/* Scoped like `.qf-trust p` above, which otherwise wins and zeroed the old 12px gap (why the two
   lines looked lumped together on the PR #92 preview). */
.qf-trust .qf-trust__secure {
  margin-bottom: 20px;
}

.qf-trust__secure svg {
  display: inline-block;
  margin-right: 2px;
  vertical-align: -3px;
  color: var(--aqua-ink);
}

/*
 * Why we ask for the address (washmates-mvp issue 50; owner, PR #92 preview): under the Address
 * step's heading, above the postcode, left-aligned with the info icon. The fieldset's own 16px gap
 * spaces it from the postcode; the legend's margin spaces it from the heading.
 */
.qf-address-why {
  display: flex;
  gap: 6px;
  align-items: flex-start;
  margin: 0;
  color: var(--ink-muted);
}

/* Roof cleaning jobs only (washmates-mvp 98): app.js sets [hidden], which display: flex would beat. */
.qf-address-why[hidden] {
  display: none;
}

.qf-address-why svg {
  flex-shrink: 0;
  color: var(--aqua-ink);
}

.qf-trust .reviews-line {
  justify-content: center;
  gap: 8px;
  min-height: 24px;
  color: var(--ink);
  font-weight: 700;
}

/* Phones: no card, the form is the page, and the step buttons stick to the bottom of the screen. */
@media (max-width: 639px) {
  /* Tabbing to a field near the bottom scrolls it clear of the sticky bar (WCAG 2.4.11). */
  html:has(.qp) {
    scroll-padding-bottom: calc(112px + var(--cookie-sheet-h, 0px));
  }

  .qp {
    background: var(--surface-raised);
  }

  .qp .quote-form {
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
  }

  /*
   * One step, one screen (issue 16): tighter vertical rhythm on /quote phones so each step fits
   * between the stepper and the sticky bar. Tap targets stay at 44px and inputs at 16px; nothing is
   * hidden. Since issue 30 the Roof step with roof, driveway/patio AND solar questions is the
   * tallest, and still overflows (accepted in issue 30; a third step would be a later slice).
   */
  .qp-slim {
    padding-top: 8px;
    padding-bottom: 8px;
  }

  .qp {
    padding-top: 12px;
  }

  .qp__col {
    gap: 12px;
  }

  .qp .quote-form,
  .qp .qf-step {
    gap: 20px;
  }

  .qp .qf-grid--property {
    gap: 20px;
  }

  .qp .qf-question {
    gap: 8px;
  }

  .qp .qf-grid--property .qf-chips {
    gap: 8px;
  }

  /*
   * washmates-mvp 77: the main-house-only caption made the Address step run past the sticky bar at
   * 360x740, so the confirmed-address card with its roof result is tighter on phones: the hairline
   * sits 8px from the address and the result (14px each before), the card's bottom padding is
   * 10px (14px), and the result's gap is 8px (12px). Spacing only: no font size changes, nothing
   * hidden, the "See each slope" toggle keeps its 44px tap target. The two captions stay 4px apart.
   */
  .qp .qf-picked--roof {
    padding-bottom: 10px;
  }

  .qp .roof--a {
    margin-top: 8px;
    padding-top: 8px;
  }

  .qp .roof__result {
    gap: 8px;
  }

  .qp .roof__note + .roof__note {
    margin-top: -4px;
  }

  /*
   * While the cookie banner is open, the bar sits on top of it (app.js sets --cookie-sheet-h).
   * Issue 50 keeps the bar to the buttons only: the trust, address-reason and rating lines under
   * them (.qf-trust) follow in the page, so the bar doesn't grow to three or four lines tall.
   */
  .qp .qf-actions {
    position: sticky;
    bottom: var(--cookie-sheet-h, 0px);
    z-index: 5;
    margin: 0 calc(-1 * var(--gutter));
    padding: 12px var(--gutter) calc(12px + env(safe-area-inset-bottom, 0px));
    background: var(--surface-raised);
    box-shadow: var(--shadow-sheet);
  }
}

@media (min-width: 640px) {
  .qp {
    padding-top: 32px;
    padding-bottom: calc(56px + var(--cookie-sheet-h, 0px));
  }
}

/* ============================================================================================
   7. Quote bar: the landing page's phone sticky bar (washmates-mvp issue 22, prototype B + C's
   bar). app.js adds .is-shown once the hero button has scrolled out of view, and removes it when
   the button comes back. Below 1024px only; sits on top of the cookie sheet while that is open.
   ============================================================================================ */

.quote-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--cookie-sheet-h, 0px);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px var(--gutter) calc(12px + env(safe-area-inset-bottom, 0px));
  background: var(--surface-raised);
  box-shadow: var(--shadow-sheet);
  transform: translateY(calc(100% + var(--cookie-sheet-h, 0px)));
  visibility: hidden;
  transition: transform 0.2s ease, visibility 0s linear 0.2s;
}

.quote-bar.is-shown {
  transform: none;
  visibility: visible;
  transition: transform 0.2s ease, visibility 0s;
}

.quote-bar .wm-btn {
  flex: 1 1 auto;
}

/* Room at the end of the landing page, so the bar never covers the footer's last line (below
   1024px only; 80px is the bar's height without the safe area). A class, not :has(), so older
   Safari and Chrome get it too. */
@media (max-width: 1023px) {
  .site-footer--quote-bar {
    padding-bottom: calc(32px + 80px + env(safe-area-inset-bottom, 0px));
  }
}

@media (prefers-reduced-motion: reduce) {
  .quote-bar,
  .quote-bar.is-shown {
    transition: none;
  }
}

@media (min-width: 1024px) {
  .quote-bar {
    display: none;
  }
}

/* ============================================================================================
   8. No Moss Guarantee: the 3-Year No Moss Guarantee's pill, footer badge and inline link (owner,
      2026-10-20; GuaranteePill.astro, GuaranteeFooterBadge.astro, GuaranteeText.astro). Here
      because the roof-town, roof service and roof landing pages load different sheets, and the
      footer is on every page; all of them load this one. The guarantee page itself is
      guarantee.css.
   ============================================================================================ */

/* The pill: a sun-yellow tag on its own line under a roof page's rating line (prototype PILL.b).
   In a top CTA (a .flow stack) it is data-support, so the 12px above it is the flow rule's; it
   never stretches across the column. */
.guarantee-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  width: fit-content;
  max-width: 100%;
  align-self: flex-start;
  justify-self: start;
  padding: 7px 14px 7px 10px;
  border-radius: var(--radius-sm);
  background: var(--sun);
  color: var(--on-sun);
  text-decoration: none;
}

.guarantee-pill:hover {
  color: var(--on-sun);
  filter: brightness(0.97);
}

.guarantee-pill:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.guarantee-pill__shield {
  flex: 0 0 auto;
}

.guarantee-pill__body {
  fill: var(--on-sun);
}

.guarantee-pill__tick {
  fill: none;
  stroke: var(--sun);
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The footer badge: a pill-shaped lockup under the brand line (prototype footSeal), the mini seal
   spanning both text rows. Its own link; the footer has no other guarantee link. */
.guarantee-badge {
  display: inline-grid;
  grid-template-columns: 48px minmax(0, auto);
  column-gap: var(--space-3);
  align-items: center;
  width: fit-content;
  max-width: 100%;
  margin-top: 10px;
  padding: 8px 16px 8px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  color: var(--ink);
  text-decoration: none;
}

.guarantee-badge:hover {
  border-color: var(--aqua-ink);
  color: var(--ink);
}

.guarantee-badge:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.guarantee-badge__seal {
  grid-row: span 2;
  display: block;
}

.guarantee-badge__name {
  min-width: 0;
  font: var(--text-label);
  line-height: 18px;
}

.guarantee-badge__more {
  min-width: 0;
  font: var(--text-caption);
  color: var(--aqua-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The mini seal's colours (data-theme="teal" on the SVG: --surface is the brand teal, --ink white). */
.guarantee-badge__disc {
  fill: var(--surface);
}

.guarantee-badge__ring {
  fill: none;
  stroke: var(--aqua);
  stroke-width: 1.2;
  stroke-dasharray: 2.2 2.2;
}

.guarantee-badge__roof {
  fill: none;
  stroke: var(--sun);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.guarantee-badge__num {
  fill: var(--ink);
  font: 400 15px var(--font-display);
}

.guarantee-badge__yrs {
  fill: var(--aqua);
  font: 700 4.6px var(--font-sans);
  letter-spacing: 0.8px;
}

/* "No Moss Guarantee certificate" in the roof work pack (included item 8, step 5). */
.guarantee-link {
  color: var(--aqua-ink);
  font-weight: 700;
}

/* ============================================================================================
   9. Offer ticket (washmates-mvp 56, prototype C v2; OfferTicket.astro, src/data/offers.ts): a
      sun-yellow ticket with a notch on each side (a mask) and a dashed divider after the
      "20% / OFF" stub. Two forms since washmates-mvp 66 (owner's picks, prototypes A + P2):
       - the full ticket (`.offer-pin`), pinned on the home hero photo's top-left corner, tilted
         −7°, from 1024px only (the photo is hidden below that);
       - the slim one-line tag (`.offer-tag`), tilted −3°, with no days-left line: above the H1 on
         home phones and the roof landing page, under the title header on the roof service and
         roof town pages (none of which has a framed photo near the top).
      On the roof town pages (no FAQ) one plain line under the tag explains the offer
      (`.offer-note`, washmates-growth-audit 03). Only one form is displayed at a width, so a
      screen reader hears the offer once. The mask
      clips any shadow on the card itself, so the tilt and drop shadow sit on each form's wrapper.
      Here because those pages load different sheets and every page loads this one; the landing
      page's desktop grid row is in landing.css. app.js removes every [data-offer] once the offer
      ends and fills the days-left line, which ships empty and invisible with its height reserved.
   ============================================================================================ */
.offer-ticket {
  --notch: 10px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 6px 16px;
  /* As wide as its words: it hangs off the photo's corner, never stretched across it. */
  width: max-content;
  max-width: 460px;
  padding: 12px 16px 12px 0;
  border-radius: 14px;
  background: var(--sun);
  color: var(--on-sun);
  /* Two halves, each cut by a half-circle notch on its outer edge. */
  -webkit-mask:
    radial-gradient(circle var(--notch) at 0 50%, #0000 98%, #000) left / 51% 100% no-repeat,
    radial-gradient(circle var(--notch) at 100% 50%, #0000 98%, #000) right / 51% 100% no-repeat;
  mask:
    radial-gradient(circle var(--notch) at 0 50%, #0000 98%, #000) left / 51% 100% no-repeat,
    radial-gradient(circle var(--notch) at 100% 50%, #0000 98%, #000) right / 51% 100% no-repeat;
}

/* Unlayered, so it beats the prose layer's paragraph margins on .page service pages. */
.offer-ticket p,
.offer-tag p {
  margin: 0;
}

/* The dashed divider runs the height of the title and terms; the days-left line sits under them. */
.offer-ticket__stub {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 4px 16px 4px 22px;
  border-right: 2px dashed color-mix(in srgb, var(--on-sun) 45%, transparent);
  line-height: 1;
}

.offer-ticket__pct {
  font: 400 34px/1 var(--font-display);
  letter-spacing: -1px;
}

.offer-ticket__off {
  margin-top: 2px;
  font: 400 14px/1 var(--font-display);
  letter-spacing: 2px;
}

.offer-ticket__body {
  min-width: 0;
}

.offer-ticket__title {
  font: 700 18px/22px var(--font-sans);
}

.offer-ticket__terms {
  font: 500 14px/19px var(--font-sans);
}

.offer-ticket__left {
  grid-column: 2;
  font: 700 12px/16px var(--font-sans);
  letter-spacing: 0.6px;
  text-transform: uppercase;
  opacity: 0.8;
  /* One line reserved before app.js fills it, so the ticket never grows after load. */
  min-height: 16px;
}

/* Shipped empty and invisible; app.js fills the line and removes data-pending. */
.offer-ticket__left[data-pending] {
  visibility: hidden;
}

/* The full ticket's wrapper: the tilt and the shadow (prototype A). Hidden below 1024px with the
   photo it sits on; from there it hangs off the photo's top-left corner (`.hero__media` is its
   positioned box), about 32px down and 36px out over the gap beside the copy column. */
.offer-pin {
  display: none;
  filter: drop-shadow(0 10px 16px rgb(10 46 43 / 0.26)) drop-shadow(0 2px 3px rgb(10 46 43 / 0.18));
}

@media (min-width: 1024px) {
  .offer-pin {
    display: block;
    position: absolute;
    z-index: 2;
    top: 32px;
    left: -36px;
    transform: rotate(-7deg);
  }
}

/* The slim tag's wrapper (prototype P2): its own width, never stretched by a flex or grid parent,
   tilted −3° about its left end and with a lighter shadow than the full ticket. The small margins
   are the prototype's: they make room for the tilt without moving the headline. */
.offer-tag {
  justify-self: start;
  align-self: flex-start;
  width: max-content;
  max-width: 100%;
  margin: 2px 0 -4px 4px;
  transform: rotate(-3deg);
  transform-origin: 0 50%;
  filter: drop-shadow(0 6px 10px rgb(10 46 43 / 0.24));
}

/* The phone form of a page that pins the full ticket on its photo (home): gone where the photo shows. */
@media (min-width: 1024px) {
  .offer-tag--phones {
    display: none;
  }
}

/* Service and service-town pages give every article child a max-width in guides.css (720px) and
   areas.css (none, the town frame); at (0,2,1) the tag keeps its own width on both. */
article.guide > .offer-tag {
  max-width: 100%;
}

/* The card: one line, the same notch mask as the ticket at a smaller size. */
.offer-tag__card {
  --notch: 6px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px 6px 0;
  border-radius: 10px;
  background: var(--sun);
  color: var(--on-sun);
  -webkit-mask:
    radial-gradient(circle var(--notch) at 0 50%, #0000 98%, #000) left / 51% 100% no-repeat,
    radial-gradient(circle var(--notch) at 100% 50%, #0000 98%, #000) right / 51% 100% no-repeat;
  mask:
    radial-gradient(circle var(--notch) at 0 50%, #0000 98%, #000) left / 51% 100% no-repeat,
    radial-gradient(circle var(--notch) at 100% 50%, #0000 98%, #000) right / 51% 100% no-repeat;
}

.offer-tag__stub {
  display: flex;
  flex-shrink: 0;
  align-items: baseline;
  gap: 5px;
  padding: 2px 10px 2px 15px;
  border-right: 2px dashed color-mix(in srgb, var(--on-sun) 45%, transparent);
  line-height: 1;
}

.offer-tag__pct {
  font: 400 22px/1 var(--font-display);
  letter-spacing: -0.5px;
}

.offer-tag__off {
  font: 400 12px/1 var(--font-display);
  letter-spacing: 1.5px;
}

/* One line, always: the tag never wraps (it is short enough for a 320px phone). */
.offer-tag__body {
  font: 700 14px/18px var(--font-sans);
  white-space: nowrap;
}

.offer-tag__ends {
  font-weight: 500;
}

/* 320–379px phones: a touch smaller, so the line keeps clear of the gutter with fallback fonts. */
@media (max-width: 379px) {
  .offer-tag__card {
    gap: 8px;
    padding-right: 12px;
  }

  .offer-tag__stub {
    padding: 2px 8px 2px 13px;
  }

  .offer-tag__pct {
    font-size: 20px;
  }

  .offer-tag__body {
    font-size: 13px;
  }
}

/* The offer's explanation line (washmates-growth-audit 03, prototype option A; OfferNote.astro):
   the offer FAQ answer as one plain line straight under the slim tag, on pages with no Questions
   section (the roof town pages). Muted ink, a touch larger than a caption (the prototype's
   14px/20px DM Sans 500; no token sits between --text-caption and --text-body). It stops at the
   tag's 460px cap on wide screens and takes the column on phones.

   Spacing: it sits --gap-support below the tag's box. The article is a `.flow` (--flow-gap), and
   the tag's −4px bottom margin (its room for the tilt) is given back here, so the line clears the
   tag's lower-left corner by 12px. Not `data-support`: the tilted tag's bounding box makes the
   spacing standard's measure meaningless here. (0,2,1) like `article.guide > .offer-tag`, so the
   town frame's `.guide.town > * { max-width: none }` doesn't stretch it. */
article.guide > .offer-note {
  max-width: 460px;
  margin: 0;
  color: var(--ink-muted);
  font: 500 14px/20px var(--font-sans);
}

article.guide > .offer-tag + .offer-note {
  margin-top: calc(var(--gap-support) - var(--flow-gap, 0px) + 4px);
}

/* The quote form's "-20%" tag on the offer's service chip (aria-hidden; the chip's name carries
   the offer in words). */
.chip-tag {
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--sun);
  color: var(--on-sun);
  font-size: 12px;
  font-weight: 700;
}
