/* =================================================================
   Bloom Auto Lab — shared stylesheet
   One file powers every page. Sections are ordered: tokens, reset,
   layout utilities, shell (header/footer/sticky bar/CTA band),
   shared components, then page-specific sections, then responsive.
   ================================================================= */

/* Self-hosted fonts — see docs/DESIGN-SYSTEM.md. Weights match actual
   usage only (400/500/600/700 body, 500/700/800 display). */
@font-face {
  font-family: "General Sans";
  src: url("../fonts/general-sans-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "General Sans";
  src: url("../fonts/general-sans-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "General Sans";
  src: url("../fonts/general-sans-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "General Sans";
  src: url("../fonts/general-sans-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
/* Cabinet Grotesk 500 removed 2026-07-14 — the same finding AUDIT.md made for
   cabinet-grotesk-800, in the file next to it. Cabinet Grotesk is the display
   face and every element that asks for it is a heading at 700. The only
   `font-weight: 500` rule in the stylesheet is `.primary-nav`, which is body
   text (General Sans). Verified against the network, not just grep: the browser
   fetches general-sans 400/500/600/700 and cabinet-grotesk-700 — five files —
   and has never requested this one.
   Repo weight only (-19.8 KB); browsers never downloaded it, so it is NOT a
   load-time win and is not claimed as one. */
@font-face {
  font-family: "Cabinet Grotesk";
  src: url("../fonts/cabinet-grotesk-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Type scale */
  --text-xs: clamp(0.75rem, 0.72rem + 0.15vw, 0.85rem);
  --text-sm: clamp(0.9rem, 0.86rem + 0.2vw, 1rem);
  --text-base: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
  --text-lg: clamp(1.15rem, 1.05rem + 0.6vw, 1.5rem);
  --text-xl: clamp(1.5rem, 1.25rem + 1.2vw, 2.15rem);
  --text-2xl: clamp(2.3rem, 1.4rem + 3vw, 4.4rem);
  --text-3xl: clamp(1.9rem, 1.3rem + 2.2vw, 3rem);

  /* Spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-20: 5rem;
  --space-24: 6rem;

  /* The seam between any two stacked blocks. EVERY block-level band — .section,
     .hero, .cta-band — pads top and bottom with exactly this, so every seam on
     every page is 2× it and they cannot disagree.

     They used to disagree, four ways, because each band invented its own
     formula: .section clamp(2rem,5vw,4.5rem), .cta-band clamp(2.5rem,6vw,4.5rem),
     .hero <top> 2.5rem, .hero-compact 4rem 3rem. A seam is the SUM of two of
     those, so one page served 104px / 128px / 136px gaps and the city pages a
     fifth. It also hid a discontinuity: a max-width:759px override set .section
     padding to 56px while the base rule gave 38px at 760px, so the spacing
     jumped *wider* as the screen got smaller.

     Tuned down from the old 128px desktop seam, which read as spacey: this
     lands ~96px at 1280 and ~64px on a phone. */
  --section-gap: clamp(2rem, 1rem + 2.5vw, 3.25rem);
  /* Distinct from the seam: this is clearance under the sticky header, not a
     gap between blocks. */
  --hero-top: clamp(1.75rem, 5vw, 3.5rem);

  /* Neutral foundation — crisp, near-white */
  --color-bg: #fbfaff;
  --color-surface: #ffffff;
  --color-surface-2: #f4f0fc;
  --color-surface-3: #ece2fb;
  --color-border: rgba(50, 32, 95, 0.15);
  --color-border-strong: rgba(50, 32, 95, 0.26);
  --color-divider: rgba(27, 21, 35, 0.09);

  /* Ink */
  --color-text: #1b1523;
  --color-text-muted: #454056;
  --color-text-faint: #635c78;
  --color-on-accent: #ffffff;

  /* Brand ramp — "Ink Violet". Full scale kept for tints/shadows;
     the semantic --color-accent-* aliases below are what components
     actually use, so the ramp can shift in one place. See
     docs/DESIGN-SYSTEM.md for rationale + contrast results. */
  --brand-50: #f3f0fe;
  --brand-100: #e7e0fd;
  --brand-200: #cdbefa;
  --brand-300: #ac93f5;
  --brand-400: #8c68ee;
  --brand-500: #7248e4;
  --brand-600: #5b35d6;
  --brand-700: #4926b0;
  --brand-800: #391d87;
  --brand-900: #281460;
  --brand-600-rgb: 91, 53, 214;
  /* Sampled straight out of the original sakura PNG — the mark's own colour,
     hue 254.0°. brand-600 sits at 254.2°, which is why the ramp needed no
     repaint. Decorative only: 1.40:1 on white, so never put text on it. */
  --brand-heritage: #ded4ff;

  /* Accent — semantic aliases onto the ramp above */
  --color-accent: var(--brand-600);
  --color-accent-strong: var(--brand-700);
  --color-accent-light: var(--brand-400);
  --color-accent-soft: rgba(var(--brand-600-rgb), 0.1);
  --color-accent-soft-strong: rgba(var(--brand-600-rgb), 0.18);

  /* Per-page glow tint (overridden by body[data-page]) — temperature
     shifts within the one accent family, not separate hues. */
  --glow-a: 140, 110, 240;
  --glow-b: 108, 70, 218;

  /* Shadows & radius */
  --shadow-sm: 0 12px 28px rgba(40, 20, 96, 0.1);
  --shadow-md: 0 24px 60px rgba(40, 20, 96, 0.15);
  --shadow-lg: 0 32px 76px rgba(40, 20, 96, 0.2);
  --radius-sm: 0.5rem;
  --radius-md: 0.85rem;
  --radius-lg: 1.25rem;
  --radius-xl: 1.75rem;
  --radius-full: 9999px;

  /* Layout & fonts */
  --content: 1200px;
  --font-body: "General Sans", "Segoe UI", sans-serif;
  --font-display: "Cabinet Grotesk", "General Sans", sans-serif;

  /* Motion — one easing curve site-wide. UI feedback stays under
     300ms and invisible; signature moments are slow and cinematic.
     Nothing lives in between. See docs/DESIGN-SYSTEM.md. */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --duration-ui: 180ms;
  --duration-signature: 1100ms;
  --transition: var(--duration-ui) var(--ease);
  --transition-signature: var(--duration-signature) var(--ease);

  --sticky-bar-height: 4.25rem;
  --header-height: 4.75rem;
}

:root {
  color-scheme: light;
}

/* Per-page accent temperature — same one hue, subtly warmer/cooler
   per service so the site reads as one system, not five clones.
   Kept inside the violet family deliberately (see restraint budget
   in docs/DESIGN-SYSTEM.md — one accent, no second hue). */
body[data-page="detailing"] {
  --glow-a: 120, 128, 244;
  --glow-b: 100, 76, 220;
}

body[data-page="paint-correction"] {
  --glow-a: 168, 96, 238;
  --glow-b: 122, 62, 210;
}

body[data-page="ceramic-coating"] {
  --glow-a: 104, 132, 240;
  --glow-b: 92, 96, 224;
}

/* ============ Reset & base ============ */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: none;
  text-size-adjust: none;
  scroll-padding-top: calc(var(--header-height) + 2rem);
}

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

body {
  min-height: 100dvh;
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--color-text);
  background: radial-gradient(
      circle at top left,
      rgba(var(--glow-a), 0.08),
      transparent 26%
    ),
    radial-gradient(circle at 90% 8%, rgba(var(--glow-b), 0.05), transparent 22%),
    linear-gradient(180deg, #ffffff 0%, #fbfaff 40%, var(--color-bg) 100%);
  overflow-x: clip;
}

body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(27, 21, 35, 0.03),
    transparent 20%,
    transparent 80%,
    rgba(27, 21, 35, 0.02)
  );
  z-index: -2;
}

body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(rgba(40, 20, 96, 0.07) 0.6px, transparent 0.6px);
  background-size: 22px 22px;
  opacity: 0.28;
  z-index: -1;
  mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0));
}

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

/* The [hidden] attribute must win over any component's own `display`
   rule (e.g. .btn, .price-card) — author styles otherwise silently
   defeat the browser's built-in [hidden]{display:none} UA rule. */
[hidden] {
  display: none !important;
}

a {
  color: inherit;
  text-decoration: none;
}

button,
input,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: none;
  cursor: pointer;
}

h1,
h2,
h3,
h4 {
  line-height: 1.08;
  text-wrap: balance;
  letter-spacing: -0.03em;
  font-weight: 600;
}

p {
  text-wrap: pretty;
}

ul,
ol {
  list-style: none;
}

:focus-visible {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute;
  left: 1rem;
  top: -4rem;
  z-index: 500;
  padding: var(--space-3) 1.1rem;
  background: #ffffff;
  color: var(--color-text);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
  font-weight: 600;
  transition: top var(--transition);
}

.skip-link:focus {
  top: 1rem;
}

/* ============ Layout helpers ============ */
.container {
  width: min(calc(100% - 2rem), var(--content));
  margin-inline: auto;
}

.section {
  padding-block: var(--section-gap);
  position: relative;
}

.section-tint {
  background: linear-gradient(180deg, transparent, var(--color-surface-2) 12%, var(--color-surface-2) 88%, transparent);
}

.section-head {
  display: grid;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
  max-width: 46rem;
}

.section-head.center {
  margin-inline: auto;
  text-align: center;
  justify-items: center;
}

.eyebrow {
  display: inline-flex;
  width: fit-content;
  align-items: center;
  gap: 0.55rem;
  padding: 0.45rem 0.85rem;
  border-radius: var(--radius-full);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.eyebrow::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  background: var(--color-accent-strong);
  box-shadow: 0 0 14px rgba(var(--brand-600-rgb), 0.55);
  flex: 0 0 auto;
}

/* One heading voice for every section-level h2. The last three live outside
   .section-head (band/panel layouts), and with no default h2 size in this file
   they silently fell back to the browser's 1.5em in the BODY font — so the
   ceramic band (the homepage's highest-margin section), the interior offer and
   both explainer panels ran smaller than every sibling heading and in the wrong
   typeface. Shared selectors, not copied declarations, so they cannot drift. */
.section-head h2,
.ceramic-band-copy h2,
.interior-offer-copy h2,
.explainer-panel h2 {
  font-family: var(--font-display);
  font-size: var(--text-xl);
}

.section-head p {
  color: var(--color-text-muted);
  max-width: 60ch;
}

/* ============ Glass surfaces ============ */
.glass {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.86), var(--color-surface));
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-md);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

.glass-soft {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.72), var(--color-surface-2));
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

/* ============ Buttons ============ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 2.9rem;
  padding: 0.8rem 1.3rem;
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 600;
  border: 1px solid transparent;
  transition: transform var(--transition), background var(--transition),
    border-color var(--transition), color var(--transition), box-shadow var(--transition);
  white-space: nowrap;
}

.btn:hover {
  transform: translateY(-1px);
}

.btn-primary {
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-strong) 100%);
  color: var(--color-on-accent);
  box-shadow: 0 14px 34px rgba(var(--brand-600-rgb), 0.32);
  border-color: rgba(255, 255, 255, 0.35);
}

.btn-primary:hover {
  background: linear-gradient(135deg, var(--brand-700) 0%, var(--brand-800) 100%);
}

.btn-secondary {
  background: rgba(27, 21, 35, 0.045);
  color: var(--color-text);
  border-color: var(--color-border);
}

.btn-secondary:hover {
  background: rgba(27, 21, 35, 0.08);
  border-color: var(--color-border-strong);
}

.btn-lg {
  min-height: 3.35rem;
  padding: 0.95rem 1.75rem;
  font-size: var(--text-base);
}

.btn-sm {
  min-height: 44px;
  padding: var(--space-2) var(--space-5);
  font-size: var(--text-sm);
}

.btn-full {
  width: 100%;
}

/* ---- Pressed state (2026-07-19 interaction-states pass) ----
   The third of the three feedback states. Hover and focus existed everywhere;
   :active existed NOWHERE (0 rules in 468, measured live), so a click gave no
   confirmation between "cursor over" and "action done". One quiet treatment:
   raised elements settle back from their hover lift, flat pills compress a
   hair. Inside no-preference because it is pure motion — hover colour shifts
   already carry the state for reduced-motion visitors. Stays on --transition
   (180ms): UI feedback, invisible, per DESIGN-SYSTEM.md's motion tiers. */
@media (prefers-reduced-motion: no-preference) {
  .btn:active,
  .header-pill:active {
    transform: translateY(0) scale(0.99);
  }

  .sticky-bar-btn:active,
  .nav-toggle:active,
  .lightbox-btn:active,
  .footer-social .social-link:active,
  .size-option:active,
  .rec-option:active {
    transform: scale(0.97);
  }
}

/* ---- Disabled state (same pass) ----
   No rule styled a disabled control, and main.js now disables the submit
   button during an async send — without this it would look enabled while
   refusing clicks. After the :hover/:active rules on purpose: equal
   specificity, so source order is what lets disabled win the tie and cancel
   the lift/press. The frozen .btn-primary background stops the hover gradient
   swap, which still matches on a disabled element. */
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.btn-primary:disabled,
.btn-primary[aria-disabled="true"] {
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-strong) 100%);
}

/* The same guard .footer-social .social-link already carries for its hover
   lift — these two lifts predate it and were never brought under it. */
@media (prefers-reduced-motion: reduce) {
  .btn:hover,
  .header-pill:hover {
    transform: none;
  }
}

/* ============ Header & navigation ============ */
.site-header {
  position: sticky;
  top: 0;
  z-index: 300;
  background: rgba(255, 255, 255, 0.75);
  border-bottom: 1px solid transparent;
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
}

.site-header.scrolled {
  background: rgba(255, 255, 255, 0.94);
  border-color: var(--color-border);
  box-shadow: var(--shadow-sm);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--header-height);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  flex: 0 1 auto;
  min-height: 44px;
}

.brand-mark {
  width: 2.35rem;
  height: 2.35rem;
  flex: 0 0 auto;
}

.brand-copy {
  display: grid;
  gap: 0.05rem;
  min-width: 0;
}

.brand-copy strong {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  white-space: nowrap;
}

.brand-copy span {
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.primary-nav {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  font-weight: 500;
}

.primary-nav a {
  position: relative;
  padding-block: 0.35rem;
  white-space: nowrap;
}

.primary-nav a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  border-radius: 999px;
  background: var(--color-accent-strong);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--transition);
}

.primary-nav a:hover,
.primary-nav a:focus-visible {
  color: var(--color-text);
}

.primary-nav a:hover::after,
.primary-nav a:focus-visible::after {
  transform: scaleX(1);
}

.primary-nav a[aria-current="page"] {
  color: var(--color-text);
  font-weight: 700;
}

.primary-nav a[aria-current="page"]::after {
  width: 100%;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
}

.header-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 2.9rem;
  padding: 0.7rem 1.1rem;
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
  border: 1px solid var(--color-border);
  transition: transform var(--transition), background var(--transition), border-color var(--transition),
    box-shadow var(--transition), color var(--transition);
}

.header-pill:hover {
  transform: translateY(-1px);
}

.header-pill-call {
  color: var(--color-text);
  background: rgba(27, 21, 35, 0.04);
}

.header-pill-call:hover {
  background: rgba(27, 21, 35, 0.08);
  border-color: var(--color-border-strong);
}

.header-pill-quote {
  color: var(--color-on-accent);
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-strong) 100%);
  border-color: rgba(255, 255, 255, 0.3);
  box-shadow: 0 10px 24px rgba(var(--brand-600-rgb), 0.32);
}

.header-pill-quote:hover {
  background: linear-gradient(135deg, var(--brand-700) 0%, var(--brand-800) 100%);
}

.pill-icon {
  width: 1.05rem;
  height: 1.05rem;
  flex: 0 0 auto;
}

/* Same phone glyph as the header pill, sized for a .btn rather than a pill. */
.btn-icon {
  width: 1.1rem;
  height: 1.1rem;
  flex: 0 0 auto;
}

.nav-toggle {
  display: none;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: rgba(27, 21, 35, 0.04);
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
}

/* Same hover treatment as .header-pill-call — the two sit side by side in the
   compact header and read as one control family, so they shift together. */
.nav-toggle:hover,
.nav-toggle:focus-visible {
  background: rgba(27, 21, 35, 0.08);
  border-color: var(--color-border-strong);
}

.nav-toggle svg {
  width: 1.2rem;
  height: 1.2rem;
}

.nav-toggle .icon-close {
  display: none;
}

.nav-toggle[aria-expanded="true"] .icon-open {
  display: none;
}

.nav-toggle[aria-expanded="true"] .icon-close {
  display: block;
}

/* ============ Sticky mobile bottom bar ============ */
.sticky-bar {
  display: none;
  position: fixed;
  left: 0.75rem;
  right: 0.75rem;
  /* The 0.75rem float plus the home-indicator inset. In a browser tab the
     inset is 0 — there is deliberately no viewport-fit=cover in the meta, so
     landscape notches can never overlap .container, which pads no left/right
     inset. It goes live in standalone mode (site.webmanifest is
     display: standalone), where the page IS edge-to-edge and body +
     .site-footer already pad with the same env() below. */
  bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
  z-index: 400;
  gap: 0.6rem;
  padding: 0.55rem;
  border-radius: var(--radius-xl);
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-lg);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  transition: transform var(--transition), opacity var(--transition);
}

.sticky-bar.is-hidden {
  transform: translateY(calc(100% + 1.5rem));
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .sticky-bar {
    transition: opacity var(--transition);
  }
  .sticky-bar.is-hidden {
    transform: none;
  }
}

.sticky-bar-btn {
  flex: 1;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 3.1rem;
  border-radius: var(--radius-lg);
  font-size: var(--text-sm);
  font-weight: 700;
  white-space: nowrap;
}

.sticky-bar-btn svg {
  width: 1.1rem;
  height: 1.1rem;
}

.sticky-bar-call {
  background: rgba(27, 21, 35, 0.05);
  color: var(--color-text);
}

.sticky-bar-quote {
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-strong) 100%);
  color: var(--color-on-accent);
}

/* ============ CTA band ============ */
.cta-band {
  position: relative;
  padding-block: var(--section-gap);
  overflow: hidden;
  isolation: isolate;
}

.cta-band::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-strong) 100%);
  z-index: -2;
}

.cta-band::after {
  content: "";
  position: absolute;
  inset: 0;
  /* The two glows ramp to rgba(...,0) rather than `transparent`, which would
     drag them through grey on the way out and dull the accent band. The 0.6px
     dot tile stays: at sub-pixel size on a saturated ground it reads as grain,
     not as the 1px/24px grid that made the placeholder art look dithered. */
  background: radial-gradient(circle at 15% 20%, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 40%),
    radial-gradient(circle at 85% 85%, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0) 45%),
    radial-gradient(rgba(255, 255, 255, 0.16) 0.6px, transparent 0.6px);
  background-size: auto, auto, 20px 20px;
  z-index: -1;
}

.cta-band-inner {
  display: grid;
  gap: var(--space-5);
  justify-items: center;
  text-align: center;
  color: #fff;
}

.cta-band h2 {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  max-width: 20ch;
}

.cta-band p {
  color: rgba(255, 255, 255, 0.86);
  max-width: 50ch;
  font-size: var(--text-base);
}

.cta-band .btn-primary {
  background: #fff;
  color: var(--color-accent-strong);
  box-shadow: 0 18px 40px rgba(40, 20, 96, 0.28);
  border-color: transparent;
}

.cta-band .btn-primary:hover {
  background: #f4f0ff;
}

.cta-band-note {
  color: rgba(255, 255, 255, 0.82);
  font-size: var(--text-sm);
}

/* ============ Page transitions ============ */
/* Cross-document view transitions: the browser cross-fades the outgoing page
   into the incoming one instead of showing the white navigation flash. This is
   the whole implementation — no JS, no router, no library. Browsers without
   support ignore it and navigate exactly as before.
   180ms on purpose: long enough to read as deliberate, short enough that it
   never sits between the visitor and the booking form. */
@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 180ms;
  animation-timing-function: var(--ease);
}

/* The header is on every page, so let it stay put and morph rather than
   cross-fade with the content underneath it. */
.site-header {
  view-transition-name: site-header;
}

@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

/* ============ Footer ============ */
.site-footer {
  padding-block: var(--space-10) var(--space-6);
  border-top: 1px solid var(--color-divider);
  color: var(--color-text-faint);
}

/* Brand | Services | Contact | Follow. The brand column stays widest because it
   carries a sentence; the other three are link lists and share the rest. */
.footer-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr 0.8fr;
  gap: var(--space-8);
  padding-bottom: var(--space-6);
}

.footer-brand {
  display: grid;
  gap: var(--space-3);
  max-width: 24rem;
}

.footer-brand .brand strong {
  color: var(--color-text);
}

.footer-brand p {
  font-size: var(--text-sm);
  line-height: 1.6;
}

.footer-col {
  display: grid;
  gap: var(--space-1);
  align-content: start;
  font-size: var(--text-sm);
}

.footer-col a {
  display: flex;
  align-items: center;
  min-height: 32px;
}

.footer-hours {
  margin-top: var(--space-2);
  line-height: 1.6;
}

.footer-col h3 {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text);
  font-weight: 700;
}

.footer-col a:hover,
.footer-col a:focus-visible {
  color: var(--color-accent-strong);
}

/* ---- Follow column ---- */
/* The h3 sits on the same baseline as Services/Contact; the icons form their own
   row underneath rather than a fourth stacked link list, because three glyphs in
   a column would read as a stray edge rather than a group. */
.footer-social .social-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* 44px square: the tap-target floor this codebase already settled on, and the
   reason these are not bare 16px glyphs. Overrides .footer-col a's flex/32px —
   same specificity, and this rule comes later. */
.footer-social .social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-height: 44px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-muted);
  transition: color var(--transition), border-color var(--transition),
    background var(--transition), transform var(--transition);
}

.footer-social .social-link svg {
  width: 1.15rem;
  height: 1.15rem;
  flex: 0 0 auto;
}

.footer-social .social-link:hover,
.footer-social .social-link:focus-visible {
  color: var(--color-accent-strong);
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
  .footer-social .social-link:hover,
  .footer-social .social-link:focus-visible {
    transform: none;
  }
}

.footer-areas {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-5);
  padding-bottom: var(--space-6);
}

.footer-areas h3 {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text);
  font-weight: 700;
  white-space: nowrap;
}

.footer-bottom {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding-top: var(--space-6);
  border-top: 1px solid var(--color-divider);
  font-size: var(--text-xs);
}

.footer-legal {
  display: flex;
  gap: var(--space-5);
}

.footer-legal a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
}

.footer-legal a:hover,
.footer-legal a:focus-visible {
  color: var(--color-accent-strong);
}

/* ============ FAQ (details/summary) ============ */
.faq-list {
  display: grid;
  gap: var(--space-3);
}

.faq-item {
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.faq-item details {
  padding: 0 var(--space-5);
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}

.faq-item summary {
  list-style: none;
  cursor: pointer;
  min-height: 4rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  font-weight: 600;
}

.faq-item summary::-webkit-details-marker {
  display: none;
}

.faq-item summary .faq-icon {
  flex: 0 0 auto;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 999px;
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  display: grid;
  place-items: center;
  font-size: 1.1rem;
  line-height: 1;
  /* The + → × turn used to snap while the select chevron — the same open/close
     gesture — animates. One gesture, one behaviour: 180ms, the UI tier. */
  transition: transform var(--transition), background var(--transition);
}

/* The row is clickable but nothing said so until the cursor became a pointer —
   a hover-only affordance the icon now carries too. Deliberately the icon and
   not the text: a colour shift on bold summary text reads as a link to
   somewhere, and this row goes nowhere. */
.faq-item summary:hover .faq-icon,
.faq-item summary:focus-visible .faq-icon {
  background: var(--color-accent-soft-strong);
}

.faq-item details[open] summary .faq-icon {
  transform: rotate(45deg);
}

.faq-answer {
  padding: 0 0 1.3rem;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  max-width: 65ch;
}

.faq-answer strong {
  color: var(--color-text);
}

/* ---- In-prose links (2026-07-19c audit, F3) ----
   The reset strips color and underline from every <a>, and nothing restored
   them for links inside running text — "Signature Detail" in the interior
   nudge rendered IDENTICAL to the sentence around it (measured: same color,
   same weight, no underline). A link nobody can see is a link nobody taps.
   Underline + accent, never color alone (WCAG 1.4.1); 600 matches
   .hint-action, the closest existing in-sentence link.

   The padding is the .crumbs/.hint-action tap trick: block padding on an
   inline box paints outward without moving the line, lifting the ~18px text
   target to ~40px for thumbs. The negative inline margin cancels the
   horizontal advance the inline padding would otherwise add mid-sentence.
   Keep this list to links that really sit in flowing prose — anything
   display:flex/inline-flex (footer columns, .explainer-list a) would grow
   for real if added here. */
.faq-answer a,
.section-foot a,
.interior-offer-nudge a,
.legal-body a {
  color: var(--color-accent-strong);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  padding-block: 0.7rem;
  padding-inline: 0.15rem;
  margin-inline: -0.15rem;
}

.faq-answer a:hover,
.faq-answer a:focus-visible,
.section-foot a:hover,
.section-foot a:focus-visible,
.interior-offer-nudge a:hover,
.interior-offer-nudge a:focus-visible,
.legal-body a:hover,
.legal-body a:focus-visible {
  color: var(--color-accent);
}

/* ============ Reveal & load animations ============ */
/* The hidden state only exists where there is JS to undo it.
   This was an unconditional `opacity: 0`, and `.visible` is added by main.js and
   nothing else — no <noscript> anywhere on the site. So with JS blocked (privacy
   extensions, locked-down corporate browsers) every .reveal rendered invisible:
   measured 28 of 28 on /detailing — the price ladder, the FAQ, the lot — on a
   site whose stated principle is that the HTML is correct and crawlable before
   JS runs. It also meant any early throw in main.js blanked the page.

   `scripting: enabled` fails safe in BOTH directions, which is why it beats a
   .js class here: no JS and the rule never applies, so content shows; a browser
   too old to know the query and the rule never applies, so content shows. The
   worst case anywhere is "content appears without animating". */
@media (scripting: enabled) {
  .reveal {
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 550ms var(--ease), transform 550ms var(--ease);
  }

  .reveal.visible {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Still wins: authored after the block above, same specificity. */
@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

@keyframes hero-in {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.hero-stagger > * {
  opacity: 0;
  animation: hero-in 700ms var(--ease) forwards;
}

.hero-stagger > *:nth-child(1) {
  animation-delay: 60ms;
}
.hero-stagger > *:nth-child(2) {
  animation-delay: 160ms;
}
.hero-stagger > *:nth-child(3) {
  animation-delay: 260ms;
}
.hero-stagger > *:nth-child(4) {
  animation-delay: 360ms;
}
.hero-stagger > *:nth-child(5) {
  animation-delay: 440ms;
}

@media (prefers-reduced-motion: reduce) {
  .hero-stagger > * {
    opacity: 1;
    animation: none;
  }
}

/* ============ Hero ============ */
.hero {
  padding-block: var(--hero-top) var(--section-gap);
  position: relative;
}

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(var(--space-8), 4vw, var(--space-12));
  align-items: center;
}

.hero-copy {
  display: grid;
  gap: var(--space-5);
  min-width: 0;
}

.hero h1 {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  max-width: 14ch;
}

.hero-lead {
  color: var(--color-text-muted);
  max-width: 56ch;
}

/* Brand payoff line ("Bloom" → "in full bloom"). Ported from the coming-soon
   portal's approved treatment: its own visual register, NOT a second H1 — the
   display face at a modest size, so it lands as a signature without competing
   with the headline above it. Colour is set in the .hero-video block below,
   where the type inverts over footage. */
.hero-slogan {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.2rem, 1.05rem + 0.7vw, 1.55rem);
  letter-spacing: -0.01em;
  line-height: 1.15;
  margin-top: calc(var(--space-5) * -0.35); /* pull toward the lead it closes */
}

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

.hero-points {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  max-width: 34rem;
  margin-top: var(--space-2);
}

.hero-point {
  min-width: 0;
  padding: 0.95rem 1.05rem;
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid var(--color-border);
}

.hero-point strong {
  display: block;
  font-size: var(--text-sm);
  margin-bottom: 0.15rem;
}

.hero-point span {
  color: var(--color-text-faint);
  font-size: var(--text-xs);
}

.hero-visual {
  position: relative;
  min-height: 100%;
  min-width: 0;
}

.hero-card {
  position: relative;
  min-height: 34rem;
  height: 100%;
  border-radius: 2rem;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  isolation: isolate;
}

.hero-card::before {
  content: "";
  position: absolute;
  inset: 0.9rem;
  border-radius: 1.4rem;
  border: 1px solid rgba(255, 255, 255, 0.5);
  pointer-events: none;
  z-index: 2;
}

.hero-panel {
  position: relative;
  z-index: 2;
  margin: 1.1rem;
  padding: var(--space-5);
  border-radius: 1.4rem;
  display: grid;
  gap: var(--space-4);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.97), rgba(255, 255, 255, 0.92));
}

.hero-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3);
}

.hero-stat {
  min-width: 0;
}

.hero-stat strong {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
}

.hero-stat span {
  color: var(--color-text-muted);
  font-size: var(--text-xs);
}

.hero-note {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-divider);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  flex-wrap: wrap;
}

.hero-note a {
  color: var(--color-accent-strong);
  font-weight: 700;
}

/* ============ Signature moment: ink bloom ============ */

@media (prefers-reduced-motion: no-preference) {
  }

.hero-copy,
.hero-visual {
  position: relative;
  z-index: 1;
}

/* ============ Hero background video (homepage only) ============ */
/* Everything here is scoped to .hero-video. The other 17 pages keep the light
   hero: only index.html carries the class, so nothing else shifts.

   The homepage hero inverts to light-on-dark when the video is present, because
   the default hero text is #1b1523 on #fbfaff — near-black on near-white. A dark
   scrim over footage is what makes text readable, and dark text on a dark scrim
   is not readable at any opacity, so the text has to flip with it. */

/* min-height + centring is what gives the copy room to breathe over footage.
   The block centres inside the padding box, and .hero's `padding-block:
   var(--hero-top) …` is the header clearance, so centring cannot ride up under
   the sticky header. */
.hero-video {
  min-height: clamp(32rem, 76vh, 46rem);
  display: flex;
  align-items: center;
  overflow: hidden;
  isolation: isolate;
}

/* NOTE: do not add `.hero-video > .container { width: 100% }` here.
   It looks harmless next to `display: flex` — "make the flex child fill" — and
   it is what put the h1 at x=0. `.container` is `width: min(calc(100% - 2rem),
   var(--content))`, so overriding `width` throws away BOTH the 1rem gutters and
   the 1200px cap, and then `margin-inline: auto` has no free space left to
   centre with. Measured at 1920: the hero container went full-bleed at 1905px
   while the header's stayed 1200px, so the headline sat 353px left of the
   navbar logo directly above it; at 375 the h1 ran 0 → 375, touching both
   edges. A flex item already honours its own `width` and auto margins — the
   container needs no help. Alignment with the header is the whole point: both
   use .container, so they track each other at every breakpoint by construction
   rather than by two hand-tuned numbers agreeing. */

/* Media layer. Paint order inside is source order: poster (this element's own
   background) -> video -> scrim. .hero-copy/.hero-visual are already z-index 1,
   so z-index 0 here puts the whole stack behind them. */
.hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background-color: #16101f;
  background-image: url("../video/hero-bg-poster.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.hero-media-video {
  display: block;
  width: 100%;
  height: 100%;
  /* cover, always: it is what guarantees the box is filled at every breakpoint,
     so there is never a letterbox bar to see. */
  object-fit: cover;
  /* `center` is measured, not lazy — leave it alone unless the footage changes.
     The obvious-looking tweak is to push the car off the copy with something
     like `object-position: 75% center`. It does nothing, for two reasons:

     1. On desktop the horizontal component has nothing to pan. The clip is
        1366x720 (1.897); the hero box is 2.105 at 1440 and 2.609 at 1920 — both
        WIDER than the clip — so `cover` scales to fill width and crops the
        HEIGHT. 100% of the frame's width is already on screen; there is no
        horizontal overflow for object-position to move. It only becomes live
        below ~1100px (92% of width visible at 1024, 55% at 768, 31% at 375).
     2. Where it IS live, centre is already right. The clip is a multi-shot
        edit and the subject moves between cuts — car centre-right in the wide
        shots, centre-LEFT in two others, one shot is the interior with no car
        at all. No single static value tracks it, and biasing right pushes the
        car out of frame on the cuts where it sits left.

     Keeping the car clear of the copy is the scrim's job, not this property's —
     that is what the 102deg ramp in .hero-scrim is for. */
  object-position: center;
  /* Never let the video take a pointer event meant for the CTAs above it. */
  pointer-events: none;
  will-change: transform;
  /* Fades in over the poster on canplaythrough (main.js). Stays 0 if autoplay is
     refused, which leaves the poster showing and nothing broken-looking. */
  opacity: 0;
  transition: opacity 600ms var(--ease);
}

.hero-media-video.is-playing {
  opacity: 1;
}

/* Above the video, below the text. Three layers, first listed = topmost.
   Sized against a measured worst case, not by eye: the brightest pixel of
   footage sitting behind the headline is luma 255 — the sky in the wide shots
   blows out to pure white — so the ramp has to hold white-on-white. It does;
   see tools/check-hero-contrast.py, which composites these exact stops and
   reports the floor across the text box.

   Why a ramp and not a flat wash: the copy sits left, the car sits right, and
   a uniform rgba(0,0,0,.4) would have to be dark enough for the text
   everywhere — which means flattening the footage it exists to show. */
.hero-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    /* 1 — Vignette. The reason the video reads as "slapped on" is that it has
       no edge: full-bleed footage butted against a white header is a flat
       rectangle with a hard seam. Falling off toward the corners lets it
       recede instead. Centred at 62% 42% rather than 50% 50% so the clear
       middle sits over the car's side of the frame, not the copy's. */
    radial-gradient(
      135% 118% at 62% 42%,
      transparent 40%,
      rgba(8, 5, 14, 0.26) 78%,
      rgba(8, 5, 14, 0.44) 100%
    ),
    /* 2 — Directional ramp: heavy under the copy, clearing toward the car.
       102deg rather than 90deg so it also carries a little weight into the
       bottom-left, under the CTAs, where the road is brightest.

       The stop at 54% is the hinge, and it is placed, not guessed: the copy
       column ends at x=50% (a 1200px container at 1440 => the 1.05fr column
       runs 120 -> 722). So everything up to ~54% is doing contrast work and is
       spoken for; everything past it is only veiling the car. Splitting them
       lets the right half open up to 0.16 — the footage actually reads there
       now — while the text side never moves. Check the floor after touching
       any of these: check-hero-contrast.py samples the copy box, so lightening
       a stop left of the hinge will show up there. */
    linear-gradient(
      102deg,
      rgba(15, 10, 22, 0.92) 0%,
      rgba(15, 10, 22, 0.8) 34%,
      rgba(15, 10, 22, 0.62) 54%,
      rgba(15, 10, 22, 0.3) 74%,
      rgba(15, 10, 22, 0.16) 100%
    ),
    /* 3 — Vertical pass: seats the section against the white header above and
       the next block below. */
    linear-gradient(
      180deg,
      rgba(15, 10, 22, 0.52) 0%,
      rgba(15, 10, 22, 0.1) 42%,
      rgba(15, 10, 22, 0.5) 100%
    );
}

/* --- Inverted hero type --- */
.hero-video .hero-copy h1,
.hero-video .hero-lead strong {
  color: #fff;
}

.hero-video .hero-lead {
  color: rgba(255, 255, 255, 0.88);
}

/* White, not the portal's heritage lavender. The portal earns a lavender slogan
   because its scrim is dark (0.80–0.91); THIS scrim is deliberately lighter so
   the car stays visible, and #ebe5ff measures only 4.02:1 over the full copy box
   on single-column (≤1080px) — it fails AA on every phone. Any lavender light
   enough to clear 4.5 here (~#f7f5ff) is indistinguishable from white anyway, so
   the tint buys risk and nothing else. The signature is carried by the display
   face + weight against the sans lead, not by colour. White also keeps this line
   under check-hero-contrast.py's existing guard (which tests #fff) rather than
   adding a bespoke colour that could silently rot. */
.hero-video .hero-slogan {
  color: #fff;
}

.hero-video .eyebrow {
  background: rgba(255, 255, 255, 0.14);
  border-color: rgba(255, 255, 255, 0.3);
  color: rgba(255, 255, 255, 0.94);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* btn-secondary is dark-on-faint-dark by default — invisible on the scrim. */
.hero-video .btn-secondary {
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  border-color: rgba(255, 255, 255, 0.38);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.hero-video .btn-secondary:hover {
  background: rgba(255, 255, 255, 0.24);
  border-color: rgba(255, 255, 255, 0.6);
}

/* The panel is ~opaque white already, so its dark text needs no change; it only
   needs a shadow to sit on footage instead of floating. */
.hero-video .hero-panel {
  margin: 0;
  box-shadow: 0 1.5rem 3rem rgba(10, 6, 16, 0.45);
}

.hero-video .hero-stats {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.hero-video .hero-visual {
  justify-self: end;
  width: min(100%, 26rem);
}

/* Reduced motion: do not autoplay moving footage at someone who asked for less
   of it. main.js skips play() entirely and the poster is what remains, so hide
   the video element outright rather than leave a paused first frame over it. */
@media (prefers-reduced-motion: reduce) {
  .hero-media-video {
    display: none;
  }
}

@media (max-width: 1080px) {
  /* Single column: the copy sits over the middle of the frame now, so a
     left-heavy ramp would darken the wrong side. The weight goes vertical
     instead — but it stays a ramp, and keeps the vignette, because an even
     wash at the opacity the text needs would flatten the whole clip. */
  .hero-scrim {
    background:
      radial-gradient(
        130% 90% at 50% 38%,
        transparent 34%,
        rgba(8, 5, 14, 0.26) 74%,
        rgba(8, 5, 14, 0.5) 100%
      ),
      linear-gradient(
        180deg,
        rgba(15, 10, 22, 0.74) 0%,
        rgba(15, 10, 22, 0.58) 46%,
        rgba(15, 10, 22, 0.8) 100%
      );
  }

  .hero-video .hero-visual {
    justify-self: start;
    width: min(100%, 26rem);
  }
}

@media (max-width: 640px) {
  .hero-video {
    min-height: clamp(30rem, 82vh, 40rem);
  }

  /* Match the portrait clip the <video> switches to at this same width — the
     poster is what shows during the fade and forever if autoplay is refused, so
     a landscape poster here would flash a different crop than the clip that
     replaces it. 8.8 KB instead of 70 KB, and it is the same centre slice. */
  .hero-media {
    background-image: url("../video/hero-bg-mobile-poster.jpg");
  }
}

/* ============ Signature moment: headline mask-reveal ============ */
/* Populated by main.js splitting the h1 into .word-reveal spans on
   load. Falls back to plain static text if JS doesn't run or motion
   is reduced — never a layout dependency, purely additive. */
.word-reveal {
  display: inline-block;
  opacity: 0;
  clip-path: inset(0 0 100% 0);
  animation: word-rise 700ms var(--ease) forwards;
  animation-delay: calc(var(--i, 0) * 40ms);
}

@keyframes word-rise {
  from {
    opacity: 0;
    clip-path: inset(0 0 100% 0);
  }
  to {
    opacity: 1;
    clip-path: inset(0 0 0% 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .word-reveal {
    opacity: 1;
    clip-path: none;
    animation: none;
  }
}

/* ============ Real media in hero cards ============ */
/* Service-page hero cards (.hero-card) hold a photo or a looping video that
   fills the card behind the near-opaque .hero-panel stats block. z-index 0
   keeps the media below both that panel and the ::before inset hairline
   (z-index 2), so nothing readable sits on bare footage. */
.hero-card-media {
  background-color: #16101f;
}

.hero-card-img,
.hero-card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

/* Ceramic hero: a contained, slow-looping gloss clip — deliberately a framed
   card, NOT the homepage's full-bleed background, so the two video heroes read
   as one system without cloning each other (distinct placement + pacing). The
   poster is a CSS background on the card so that if autoplay is refused or JS
   never runs (the video stays opacity:0), the still frame shows instead of the
   flat fallback colour — the same fail-safe the homepage .hero-media uses. */
.hero-card-video {
  opacity: 0;
  transition: opacity 600ms var(--ease);
}

.hero-card-video.is-playing {
  opacity: 1;
}

/* Detail hero framing (2026-07-21, owner). Source clip is 1200x1500 (4:5) and
   the card renders at ~1.0-1.08 aspect at every breakpoint, so `cover` fills the
   WIDTH and crops the HEIGHT — measured overflowX is 0 desktop AND mobile, so
   there is nothing to pan left/right (the base rule's default centre is correct
   horizontally). Vertical is the only live axis: pushing object-position past
   centre slides the frame down the source, revealing the full grille + both
   headlights (the car-forward read the owner asked for) and trimming ceiling.
   68% is a measured, moderate move — 78% starts clipping the detailer's head.
   Scoped to .detail-card so the ceramic card (same class) keeps its centre. */
.detail-card .hero-card-video {
  object-position: 50% 68%;
}

.hero-card-ceramic {
  background-image: url("../video/ceramic-hero-poster.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

@media (prefers-reduced-motion: reduce) {
  /* main.js strips the sources at someone who asked for less motion; the poster
     background is what remains, so hide the element rather than leave a paused
     first frame over it. */
  .hero-card-video {
    display: none;
  }
}

/* ============ Service hero architectures ============ */
/* The three service heroes were one skeleton with the copy swapped: the same
   .hero-grid ratio, the same 2x2 .hero-points chip grid, and the same 3-stat
   white panel floating on the media — on all three. Nothing in the composition
   said which service you were looking at.

   Each page now gets its own architecture and its own supporting-module
   archetype. The spine stays shared (.eyebrow, the display h1 + its word-reveal,
   .hero-lead, .hero-actions, the violet ramp, --ease), so they read as one
   family and differ in structure rather than in colour:

     ceramic     media-dominant  vertical durability ladder      (time)
     detailing   open + airy     horizontal breadth rail         (coverage)
     correction  split           before/after + precision gauges (measurement)

   All of it is scoped to the modifier class on the <section>, so the homepage
   (.hero-video) and the city pages (.hero-compact) — which share the base .hero*
   rules — are untouched. Card min-heights are scoped with the modifier too, and
   have to be: the responsive .hero-card rules further down carry equal
   specificity and land later in the file, so an unscoped .coat-card would lose
   to them below 1080px. */

/* Meters fill on load, not on an observer. A keyframe with `both` always settles
   on its end state, so JS-off and reduced-motion both land FULL rather than
   empty — the failure the .reveal block above documents. The hero is above the
   fold; there is nothing to wait for. */
@keyframes hero-meter-fill {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

.hero-meter {
  position: relative;
  height: 0.4rem;
  border-radius: 999px;
  background: rgba(var(--brand-600-rgb), 0.12);
  overflow: hidden;
}

.hero-meter > i {
  display: block;
  height: 100%;
  width: var(--fill, 50%);
  border-radius: inherit;
  background: linear-gradient(90deg, var(--color-accent-light), var(--color-accent-strong));
  transform-origin: left center;
}

@media (prefers-reduced-motion: no-preference) {
  .hero-meter > i {
    animation: hero-meter-fill 900ms var(--ease) both;
    animation-delay: calc(420ms + var(--n, 0) * 130ms);
  }
}

/* ---- Ceramic: depth & durability --------------------------------------- */
/* The gloss clip is the argument on this page, so the media column is the wider
   one and the card is the tallest of the three. The panel on it is a LADDER, not
   a stat row: three rungs climbing 2 -> 5 -> 7 years, each meter longer than the
   last (widths are proportional to the years, not decorative), so the only
   decision this page asks for — how many years do you want? — is legible as a
   shape before a single price is read. */
.hero-ceramic .hero-grid {
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
}

.hero-ceramic .hero-lead {
  max-width: 48ch;
}

.coat-spec {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.15rem;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-muted);
}

.coat-spec span {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  white-space: nowrap;
}

.coat-spec span::before {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 999px;
  background: var(--color-accent-strong);
  box-shadow: 0 0 10px rgba(var(--brand-600-rgb), 0.5);
  flex: 0 0 auto;
}

/* 40rem → 36rem, measured 2026-07-17: at 640px the card ran the hero 101px past
   a 720px fold and cut the ladder — the page's one decision mechanic — mid-rung.
   576px keeps this the tallest card of the three (detailing's is 528px), so the
   media-dominant hierarchy the architecture note promises survives, and the
   full ladder is readable without a scroll. */
.hero-ceramic .coat-card {
  min-height: 36rem;
}

.coat-ladder-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.coat-ladder-head strong {
  font-family: var(--font-display);
  font-size: var(--text-base);
}

.coat-ladder-head span {
  font-size: var(--text-xs);
  color: var(--color-text-faint);
}

.coat-rungs {
  display: grid;
  gap: var(--space-2);
}

/* The price column is a FIXED width, not `auto`, and that is the whole ladder
   working. Each rung is its own grid, so an `auto` price column is sized per-row
   — "From $1,300" is wider than "From $645", which stole 12px from the 7-Year's
   meter track (120/120/108 measured). The fills are proportional to the years
   (30/70/100%), so unequal tracks mean 7-Year's "100%" paints SHORTER than a
   full track and the shape misreports the thing it exists to report. A fixed
   column gives all three rows an identical track to measure against — and
   right-aligning lands the prices in a clean column as a bonus. 5.75rem clears
   the widest price the config can hold today with room to spare. */
.coat-rung {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr) 5.75rem;
  align-items: center;
  gap: var(--space-3);
  padding: 0.45rem 0.5rem;
  margin-inline: -0.5rem;
  border-radius: var(--radius-md);
}

/* Layout does the persuading, not colour: the picked rung gets a tint and a
   flag — no heavier border, no second hue. One flag, matching the one
   "Most Popular" badge rule the pricing ladder follows. */
.coat-rung.is-picked {
  background: rgba(var(--brand-600-rgb), 0.07);
}

.coat-rung-label {
  display: grid;
  gap: 0.1rem;
}

.coat-rung-name {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

/* Budget, measured: the label column is 4.5rem (72px) desktop / 4rem (64px)
   below 760px, and this renders at 9.92px/700/0.06em. "Popular" is 53.3px and
   clears both. "Most picked" was 77.3px — it wrapped to two lines at EVERY
   width, which silently pushed the picked rung to 71px against its neighbours'
   38px and read as a layout bug rather than emphasis. nowrap keeps a future
   edit from re-introducing that quietly: a longer string now visibly overruns
   the meter instead of resizing the row. */
.coat-flag {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent-strong);
  white-space: nowrap;
}

.coat-rung-price {
  font-size: var(--text-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-align: right;
}

.coat-ladder-note {
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-divider);
  font-size: var(--text-xs);
  color: var(--color-text-faint);
}

/* ---- Detailing: the open one ------------------------------------------- */
/* This page sells convenience and thoroughness, not a spec sheet — so it gets
   the most air of the three: an even split, a wider gap, a looser copy rhythm,
   and NO panel stacked on the photo. The media stays a photograph with one mark
   on it. The supporting module is a horizontal BREADTH rail (inside / outside /
   your driveway): coverage, which is this page's story, rather than the tier
   ladder the other two use. */
.hero-detailing .hero-grid {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(var(--space-8), 5vw, var(--space-16));
}

.hero-detailing .hero-copy {
  gap: var(--space-6);
}

.hero-detailing .hero-lead {
  max-width: 44ch;
}

.detail-foot {
  display: grid;
  gap: var(--space-4);
}

.detail-teaser {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
}

.detail-teaser-price {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
}

.detail-teaser-rest {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.detail-breadth {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  list-style: none;
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-divider);
}

.detail-breadth li {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-size: var(--text-sm);
  font-weight: 600;
}

.detail-breadth svg {
  width: 1.15rem;
  height: 1.15rem;
  flex: 0 0 auto;
  color: var(--color-accent-strong);
}

.hero-detailing .detail-card {
  min-height: 33rem;
  justify-content: flex-start;
  /* Poster as a card background — the fail-safe still frame behind the looping
     hero clip, same technique as .hero-card-ceramic. */
  background-image: url("../video/detail-hero-poster.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* The single mark on the photo. Borrows .ba-label's vocabulary — same ink, same
   pill, same blur — so the site keeps one idea of "a label sitting on an image".

   The alpha is the one thing it does NOT borrow, and that is measured, not
   taste: .ba-label's 0.55 rides over paint photos, but this ribbon sits on the
   top-left of the foam shot, and the brightest pixel behind it is literally
   255,255,255 — blown-out foam. White text on 0.55 over that composites to
   rgb(130,126,134) = 3.97:1, which FAILS AA for 13.6px bold (needs 4.5). AA
   needs alpha >= 0.589 and AAA >= 0.716; 0.78 lands 8.81:1 against that worst
   case, 13.4:1 over average foam, 18:1 over the dark parkade. Do not lighten it
   back toward .ba-label without re-measuring against the actual photo — and do
   not lean on backdrop-filter to rescue it, since it is not universally
   supported and the worst case here is a flat white field anyway. */
.detail-ribbon {
  position: relative;
  z-index: 2;
  align-self: start;
  margin: 1.35rem;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.9rem;
  border-radius: var(--radius-full);
  background: rgba(27, 21, 35, 0.78);
  color: #fff;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.detail-ribbon svg {
  width: 0.95rem;
  height: 0.95rem;
  flex: 0 0 auto;
}

/* ---- Paint correction: before -> after, measured ------------------------ */
/* Correction is the one service whose product is a visible change, so the media
   is not a photo OF the work — it IS the proof, and the visitor drags it. Reuses
   the shipped [data-ba-slider] component (and its JS + keyboard-accessible
   range) rather than inventing a hero-only variant.

   The supporting module is a pair of gauges, not a chip grid, because what this
   page sells is a measured, honest expectation: 70-80% vs 90-99% is the whole
   argument, so the number gets to be the design. */
/* The three heroes take the three positions on media-to-copy balance, on
   purpose: ceramic is media-led (0.92 : 1.08) because the gloss is the argument,
   detailing is exactly even (1 : 1) because it is the open one, and correction
   is copy-led (1 : 0.92) because here the argument is the pair of numbers, not
   the picture. This started at 1 : 1.02, which is a rounding error away from
   detailing's 1 : 1 — a difference that costs a line of CSS and buys nothing. */
/* Both tall heroes give back a little frame padding toward the fold budget —
   scoped to the modifier, so the homepage video hero and the city pages keep
   the shared --hero-top rhythm. */
.hero-ceramic,
.hero-correction {
  padding-block: 2.75rem 2.25rem;
}

.hero-correction .hero-grid {
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
}

.hero-correction .hero-lead {
  max-width: 46ch;
}

/* Measured 2026-07-17 at 1280x720: two stacked gauges made the panel 254px and
   pushed the hero 149px past the fold — the worst overflow of the four heroes,
   and the owner called it by eye before it was measured. The gauges now sit
   side by side from 760px up (the comparison even reads BETTER as a pair), the
   spec line spans both, and the hero ends ~1 viewport. Below 760px they stack
   again — 2-up at 375px wide would wrap "Two-Step Correction" against its own
   price. */
.corr-panel {
  display: grid;
  gap: var(--space-4);
  max-width: none;
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid var(--color-border);
}

@media (min-width: 760px) {
  .corr-panel {
    grid-template-columns: 1fr 1fr;
    column-gap: var(--space-6);
  }

  .corr-panel .corr-spec {
    grid-column: 1 / -1;
  }
}

.corr-gauge {
  display: grid;
  gap: 0.45rem;
}

.corr-gauge-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.corr-gauge-name {
  font-family: var(--font-display);
  font-size: var(--text-sm);
}

.corr-gauge-price {
  font-size: var(--text-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.corr-gauge-figure {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: var(--text-xs);
  color: var(--color-text-faint);
}

.corr-gauge-figure strong {
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}

.corr-spec {
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-divider);
  font-size: var(--text-xs);
  color: var(--color-text-faint);
}

/* The slider is a centred, capped, 16:10 section component everywhere else. In
   the hero it IS the column, so it fills the width and stands taller.

   4/3 is a floor, not a preference: the source pair is 16:10, and `cover` crops
   width to reach it (~17%). Going more portrait to chase the copy column's
   height would crop ~48% and leave the hood a narrow slice — which would cost
   the proof this page is built on. The media is allowed to be shorter than the
   copy; it just is not allowed to be top-aligned with the slack dumped under it,
   which is what .hero-visual's `min-height: 100%` does here. The other two heroes
   never hit this because .hero-card carries `height: 100%` and fills the column
   itself; the slider has no such rule, so this centres it. */
.hero-correction .hero-visual {
  display: grid;
  align-content: center;
}

.hero-correction .ba-slider {
  max-width: none;
  margin-inline: 0;
  aspect-ratio: 4 / 3;
}

/* ---- Service hero responsive ------------------------------------------- */
@media (max-width: 1080px) {
  /* The base rule collapses .hero-grid to one column at this width, but the
     per-page overrides above outrank it — so each has to stand down itself. */
  .hero-ceramic .hero-grid,
  .hero-detailing .hero-grid,
  .hero-correction .hero-grid {
    grid-template-columns: 1fr;
  }

  .hero-ceramic .coat-card {
    min-height: 32rem;
  }

  .hero-detailing .detail-card {
    min-height: 26rem;
  }

  .hero-correction .ba-slider {
    aspect-ratio: 16 / 10;
  }
}

@media (max-width: 759px) {
  /* The ladder is the floor here, not the video: three rungs need their room,
     and the card grows to whatever the panel asks for. */
  .hero-ceramic .coat-card {
    min-height: 30rem;
  }

  .coat-ladder-head {
    display: grid;
    gap: 0.1rem;
  }

  .coat-rung {
    grid-template-columns: 4rem minmax(0, 1fr) 5.5rem;
    gap: var(--space-2);
  }

  .hero-detailing .detail-card {
    min-height: 22rem;
  }

  .corr-panel {
    max-width: none;
  }
}

/* ============ Before / after slider ============ */
.ba-slider {
  position: relative;
  max-width: 46rem;
  margin-inline: auto;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-xl);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  border: 1px solid var(--color-border);
  touch-action: pan-y;
}

.ba-slider .ba-after,
.ba-slider .ba-before {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}

/* The two photos fill the frame identically (same 16:10 box, same cover rule),
   so the clip-path wipe reveals one over the other in perfect register. */
.ba-slider .ba-before img,
.ba-slider .ba-after img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ba-slider .ba-before {
  clip-path: inset(0 calc(100% - var(--ba-pos, 50%)) 0 0);
}

.ba-slider .ba-label {
  position: absolute;
  bottom: var(--space-3);
  padding: 0.4rem 0.85rem;
  border-radius: var(--radius-full);
  background: rgba(27, 21, 35, 0.55);
  color: #fff;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  backdrop-filter: blur(6px);
}

.ba-after .ba-label {
  right: var(--space-3);
}

.ba-before .ba-label {
  left: var(--space-3);
}

.ba-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--ba-pos, 50%);
  width: 2px;
  background: rgba(255, 255, 255, 0.85);
  transform: translateX(-50%);
  pointer-events: none;
  box-shadow: 0 0 0 1px rgba(27, 21, 35, 0.15);
}

.ba-handle::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 999px;
  transform: translate(-50%, -50%);
  background: #fff;
  box-shadow: var(--shadow-md);
  background-image: linear-gradient(90deg, transparent 46%, var(--color-border-strong) 46%, var(--color-border-strong) 54%, transparent 54%);
}

.ba-range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: ew-resize;
  appearance: none;
  -webkit-appearance: none;
}

.ba-range:focus-visible ~ .ba-handle {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.ba-range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 2.75rem;
  height: 100%;
  background: transparent;
}

.ba-range::-moz-range-thumb {
  width: 2.75rem;
  height: 100%;
  background: transparent;
  border: none;
}

/* ============ Value grid ============ */
.value-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-6) var(--space-5);
}

.value-item {
  display: flex;
  gap: var(--space-4);
  align-items: flex-start;
  min-width: 0;
}

.value-item > div {
  min-width: 0;
}

.value-icon {
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  display: grid;
  place-items: center;
}

.value-icon svg {
  width: 1.3rem;
  height: 1.3rem;
}

.value-item strong {
  display: block;
  font-size: var(--text-sm);
  margin-bottom: 0.2rem;
}

.value-item p {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* ============ Process steps ============ */
.step-num {
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 999px;
  display: grid;
  place-items: center;
  font-size: var(--text-xs);
  font-weight: 700;
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
}

/* ============ Vehicle size selector ============ */
/* Centred over the price cards. This was `inline-flex` + `margin-inline: auto`,
   which never centred anything: auto margins are inert on an inline-level box,
   so it computed to 0 and the bar sat flush left. Block-level + fit-content is
   what makes the auto margins bite; justify-content centres the second row once
   the pills wrap on narrow screens.

   The gallery filter shares this rule rather than cloning it. Both are a pill
   group of .size-option buttons, they must read as the same control, and a
   hand-copied second rule is precisely how the shell drifted for weeks. */
.size-select,
.gallery-filter {
  position: relative; /* containing block for .seg-thumb */
  display: flex;
  width: fit-content;
  max-width: 100%;
  flex-wrap: wrap;
  justify-content: center;
  margin-bottom: var(--space-10);
  gap: 0.35rem;
  padding: 0.35rem;
  border-radius: var(--radius-full);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  margin-inline: auto;
}

.size-option {
  position: relative; /* paint above .seg-thumb */
  z-index: 1;
  padding: var(--space-3) var(--space-4);
  min-height: 44px;
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  transition: background var(--transition), color var(--transition);
  white-space: nowrap;
}

/* ---- Sliding selection thumb ----
   The white pill marking the selection used to blink from one option to the
   next. It is now a single element that slides between them — the movement is
   what tells you the two options are one control and that your click landed.

   main.js builds it and adds .has-thumb. With no JS neither exists, and the
   per-button .is-active background below does exactly what it always did, so
   nothing here is load-bearing.

   Duration is --transition (180ms) on the one site-wide --ease, per
   DESIGN-SYSTEM.md: "UI feedback stays under 300ms and invisible ... Nothing
   lives in between." A slower, showier slide would be nice and would be wrong. */
.seg-thumb {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  border-radius: var(--radius-full);
  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
  pointer-events: none;
  opacity: 0;
  transition: transform var(--transition), width var(--transition),
    height var(--transition), opacity var(--transition);
}

/* Once the thumb exists it IS the selected background. The button must stop
   painting its own, or both are visible at once and you watch a white pill
   slide out from under a white pill. */
.has-thumb .size-option.is-active {
  background: transparent;
  box-shadow: none;
}

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

.size-option:hover,
.size-option:focus-visible {
  color: var(--color-text);
}

.size-option.is-active {
  background: var(--color-surface);
  color: var(--color-accent-strong);
  box-shadow: var(--shadow-sm);
}

/* ============ Package recommender ============ */
/* Still compact — it sits directly above the price cards, so if it out-grows a
   card it stops being a shortcut and becomes the thing in the way. But it was
   `--color-surface-2` on a `.section-tint`, whose background is the *same*
   `--color-surface-2`: an invisible card separated from its section by 1px of
   border. Hence a real surface + shadow. It reads as a lighter sibling of the
   price cards (`.glass`, shadow-md) — present, but clearly the helper and not
   the product. Options keep .size-option's pill shape and typography; they take
   the tint the card gave up so they still read as tappable. */
.rec {
  margin-block: 0 var(--space-8);
  padding: var(--space-5) var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
  max-width: 46rem;
  margin-inline: auto;
}

.rec-lede {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin: 0 0 var(--space-4);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--text-sm);
}

.rec-icon {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-full);
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
}

.rec-icon svg {
  width: 1.05rem;
  height: 1.05rem;
}

.rec-lede strong {
  color: var(--color-text);
}

.rec-lede-sub {
  color: var(--color-text-faint);
}

.rec-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.rec-question {
  font-size: var(--text-sm);
  font-weight: 600;
  margin: 0;
  flex: 1;
}

.rec-options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.rec-option {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  min-height: 44px;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}

.rec-option:hover,
.rec-option:focus-visible {
  color: var(--color-accent-strong);
  border-color: var(--color-accent);
}

/* The tick is the trail: come Back and you can see what you picked. That is
   why there is no separate row of answer chips repeating the same thing. */
.rec-option.is-selected {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
}

.rec-option.is-selected::before {
  content: "✓";
  margin-right: var(--space-2);
  font-weight: 700;
}

.rec-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-text-muted);
  min-height: 44px;
  white-space: nowrap;
}

.rec-back:hover,
.rec-back:focus-visible {
  color: var(--color-accent-strong);
}

.rec-result-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0 0 var(--space-2);
}

.rec-result-kicker {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.rec-result-name {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: 1.2;
  color: var(--color-accent-strong);
}

.rec-result-price {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text);
}

.rec-result-why {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-3);
  max-width: 62ch;
}

.rec-result-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
}

.rec-restart {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  text-decoration: underline;
  min-height: 44px;
}

.rec-restart:hover,
.rec-restart:focus-visible {
  color: var(--color-text);
}

/* ============ Gallery ============ */
/* auto-FILL, not auto-fit: with one or two photos auto-fit would stretch them
   across the full row into letterboxed slabs. auto-fill keeps the tile the size
   the tile should be and leaves the empty tracks empty — which is what a
   gallery holding its first three photos needs to look like. */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 1fr));
  gap: var(--space-5);
}

.gallery-item {
  margin: 0;
  display: grid;
  gap: var(--space-3);
  min-width: 0;
}

/* [hidden] is display:none in the UA sheet, but `display: grid` above is an
   author-origin rule and beats it outright regardless of specificity —
   without this the filter would "hide" nothing. */
.gallery-item[hidden] {
  display: none;
}

/* The box is reserved before the file arrives: aspect-ratio here plus width and
   height on every <img>. A page that is nothing but images is the easiest place
   to wreck CLS, and this is the whole defence. */
.gallery-media {
  display: block; /* it is an <a> now — a real link to the full-size image */
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  cursor: zoom-in;
}

/* Empty state — shown until the first real photo lands (main.js hides it the
   moment a <figure> exists). One calm, brand-tinted card with a photo glyph and a
   message: reads as "coming", never "broken". Sits in place of the grid, not a
   tile, so an empty gallery never looks like a wall of missing images. */
.gallery-empty {
  display: grid;
  justify-items: center;
  gap: var(--space-4);
  max-width: 34rem;
  margin: var(--space-6) auto 0;
  padding: var(--space-8) var(--space-6);
  text-align: center;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: linear-gradient(150deg, rgba(var(--brand-600-rgb), 0.1), rgba(var(--brand-600-rgb), 0.02));
}
.gallery-empty[hidden] {
  display: none;
}
.gallery-empty-glyph {
  width: 3rem;
  height: 3rem;
  opacity: 0.55;
  background: center / contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235b35d6' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='14' rx='2'/%3E%3Ccircle cx='12' cy='12' r='3.2'/%3E%3Cpath d='M8 5l1.2-2h5.6L16 5'/%3E%3C/svg%3E");
}
.gallery-empty h2 {
  font-size: var(--text-xl);
}
.gallery-empty p {
  color: var(--color-text-muted);
  margin: 0;
}

.gallery-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--transition);
}

/* Quiet "opens larger" cue: a gentle push-in on the photo and a maximise glyph
   fading up in the corner. Both fire on hover AND keyboard focus, so the
   affordance is never mouse-only. The glyph is an inline data-URI (no request),
   the same technique the custom <select> chevron uses. */
.gallery-media:hover img,
.gallery-media:focus-visible img {
  transform: scale(1.05);
}

.gallery-media::after {
  content: "";
  position: absolute;
  right: var(--space-3);
  bottom: var(--space-3);
  width: 2.2rem;
  height: 2.2rem;
  border-radius: var(--radius-full);
  background: rgba(15, 10, 22, 0.55) center / 1.05rem no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3'/%3E%3C/svg%3E");
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--transition), transform var(--transition);
  pointer-events: none;
}

.gallery-media:hover::after,
.gallery-media:focus-visible::after {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .gallery-media img,
  .gallery-media:hover img,
  .gallery-media:focus-visible img {
    transition: none;
    transform: none;
  }
  .gallery-media::after {
    transition: opacity var(--transition);
    transform: none;
  }
}

.gallery-item figcaption {
  display: grid;
  gap: 0.15rem;
}

.gallery-item figcaption strong {
  font-size: var(--text-sm);
  color: var(--color-text);
}

.gallery-item figcaption span {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

.gallery-count {
  margin: 0 0 var(--space-5);
  text-align: center;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* ============ Gallery lightbox ============ */
/* Built by main.js on first open and reused; the markup is a plain grid of
   <a href="…-full.jpg"> links, so with JS off a click just opens the image.
   Sits above the header (300) and sticky bar (400). Dialog semantics, focus
   trap, Esc/arrows/swipe all live in main.js. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 600;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: clamp(1rem, 3vw, 2.5rem);
  background: rgba(12, 8, 18, 0.86);
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  opacity: 0;
  transition: opacity var(--transition);
}

.lightbox.is-open {
  opacity: 1;
}

body.lightbox-open {
  overflow: hidden;
}

.lightbox-stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0; /* let the image shrink inside the flex column */
}

.lightbox-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  max-width: 100%;
  max-height: 100%;
}

.lightbox-img {
  display: block;
  max-width: min(94vw, 1400px);
  max-height: clamp(18rem, 74vh, 60rem);
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  background: #16101f;
  transition: opacity var(--transition);
}

/* Briefly cleared while the next full-size file decodes, so navigation reads as
   a crossfade rather than a flash of the previous shot. */
.lightbox-img.is-loading {
  opacity: 0;
}

.lightbox-caption {
  display: grid;
  gap: 0.2rem;
  text-align: center;
  max-width: 46ch;
}

.lightbox-caption strong {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: #fff;
}

.lightbox-caption span {
  font-size: var(--text-sm);
  color: rgba(255, 255, 255, 0.68);
}

.lightbox-counter {
  flex: 0 0 auto;
  color: rgba(255, 255, 255, 0.7);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

.lightbox-btn {
  position: absolute;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: background var(--transition), border-color var(--transition), transform var(--transition);
}

.lightbox-btn:hover {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.5);
}

.lightbox-btn svg {
  width: 1.4rem;
  height: 1.4rem;
  pointer-events: none;
}

.lightbox-close {
  top: clamp(1rem, 3vw, 2rem);
  right: clamp(1rem, 3vw, 2rem);
}

.lightbox-prev,
.lightbox-next {
  top: 50%;
  transform: translateY(-50%);
}

.lightbox-prev {
  left: clamp(0.5rem, 2vw, 1.5rem);
}

.lightbox-next {
  right: clamp(0.5rem, 2vw, 1.5rem);
}

.lightbox-prev:hover,
.lightbox-next:hover {
  transform: translateY(-50%) scale(1.06);
}

@media (max-width: 640px) {
  .lightbox-btn {
    width: 2.6rem;
    height: 2.6rem;
  }
  .lightbox-prev {
    left: 0.4rem;
  }
  .lightbox-next {
    right: 0.4rem;
  }
  .lightbox-img {
    max-height: clamp(16rem, 64vh, 40rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .lightbox,
  .lightbox-img {
    transition: none;
  }
  .lightbox-prev:hover,
  .lightbox-next:hover {
    transform: translateY(-50%);
  }
}

/* ============ Pricing cards ============ */
.price-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-5);
  align-items: stretch;
}

.price-grid.price-grid-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  max-width: 46rem;
  margin-inline: auto;
}

/* Interior Detail — the standalone interior-only tier (detailing.html). A copy
   column that explains who it is for and nudges to Signature, beside a single
   price card. Not a .price-grid (which forces 3 equal columns); the card sits in
   its own fixed-ish column so it reads as one intentional option, not an orphaned
   rung. Collapses to a stack on narrow screens. */
.interior-offer {
  display: grid;
  grid-template-columns: 1fr minmax(300px, 360px);
  gap: var(--space-8);
  align-items: center;
  max-width: 60rem;
  margin-inline: auto;
}

.interior-offer-copy h2 {
  margin: var(--space-2) 0 var(--space-4);
}

.interior-offer-nudge {
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* The lone card must not stretch to the copy column's full height like a
   grid-stretched price-card would; align it to the top of its own column. */
.interior-offer-card {
  align-self: stretch;
  display: grid;
}

@media (max-width: 760px) {
  .interior-offer {
    grid-template-columns: 1fr;
    gap: var(--space-6);
  }
}

.price-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: 1.75rem;
  border-radius: 1.75rem;
  min-width: 0;
  height: 100%;
}

.price-card.featured {
  background: linear-gradient(180deg, var(--color-accent-soft) 0%, rgba(255, 255, 255, 0.94) 60%);
  border-color: rgba(var(--brand-600-rgb), 0.35);
  box-shadow: var(--shadow-lg);
}

/* Top-RIGHT, not top-left. It never literally overlapped the tier name, but it
   sat 9px above it on the exact same left margin (both at 29px), so the badge
   and the h3 stacked in one column and the eye hit "MOST POPULAR" before it
   could read "Signature Detail". Moving it opposite the heading leaves the
   card's reading start clear and still overhangs the top edge. */
.price-badge {
  position: absolute;
  top: -0.9rem;
  right: 1.75rem;
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius-full);
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-strong) 100%);
  color: #fff;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  box-shadow: 0 10px 24px rgba(var(--brand-600-rgb), 0.35);
}

.price-head {
  display: grid;
  gap: 0.4rem;
}

.price-head h3 {
  font-family: var(--font-display);
  font-size: var(--text-lg);
}

.price-summary {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  min-height: 3.2em; /* 2 lines at 1.6 line-height — keeps price boxes aligned */
}

.price-sizes {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background: rgba(27, 21, 35, 0.03);
}

.price-kicker {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--color-text-faint);
  font-weight: 700;
}

.price-figure {
  display: block;
  line-height: 1.1;
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.5rem + 1.4vw, 2.4rem);
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

.price-list {
  display: grid;
  gap: 0.65rem;
}

.price-list li {
  display: flex;
  gap: 0.65rem;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.price-list li::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  margin-top: var(--space-2);
  border-radius: 999px;
  background: linear-gradient(180deg, var(--color-accent), var(--color-accent-strong));
  flex: 0 0 auto;
}

.price-foot {
  margin-top: auto;
  display: grid;
  gap: var(--space-2);
}

.price-foot .muted-note {
  color: var(--color-text-faint);
  font-size: var(--text-xs);
  text-align: center;
  min-height: 1.6em; /* holds the row even when a tier has no value line */
}

/* ============ Bundle cards ============ */
/* Was a hard 3-track. Pet & Odour was withdrawn with the Pet Hair add-on it was
   half made of, which would have stranded the survivor at a third width with
   two empty columns beside it. auto-fit stays correct at 1, 2 or 3 without a
   second rule to keep in sync. */
.bundle-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.bundle-card {
  padding: 1.35rem var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  display: grid;
  gap: var(--space-2);
  min-width: 0;
}

.bundle-card h3 {
  font-family: var(--font-display);
  font-size: var(--text-base);
}

.bundle-items {
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  line-height: 1.5;
}

.bundle-price-row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  margin-top: 0.2rem;
  flex-wrap: wrap;
}

.bundle-price-row strong {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--color-accent-strong);
  font-variant-numeric: tabular-nums;
}

.bundle-price-row span {
  color: var(--color-text-faint);
  font-size: var(--text-xs);
}

.price-disclosure {
  margin-top: var(--space-5);
  text-align: center;
  color: var(--color-text-faint);
  font-size: var(--text-xs);
  max-width: 60ch;
  margin-inline: auto;
}

/* ============ Add-ons strip ============ */
.addons-strip {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.85rem;
}

.addon-chip {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) 1.15rem;
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  min-width: 0;
}

.addon-chip span:first-child {
  font-size: var(--text-sm);
  font-weight: 600;
  min-width: 0;
}

.addon-chip strong {
  color: var(--color-accent-strong);
  font-size: var(--text-sm);
  white-space: nowrap;
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}

/* ============ Benefit / icon list ============ */
.benefit-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-5);
}

.benefit-card {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-6);
  border-radius: var(--radius-xl);
  min-width: 0;
}

.benefit-icon {
  flex: 0 0 auto;
  width: 2.85rem;
  height: 2.85rem;
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  display: grid;
  place-items: center;
}

.benefit-icon svg {
  width: 1.4rem;
  height: 1.4rem;
}

.benefit-card h3 {
  font-size: var(--text-base);
  margin-bottom: 0.3rem;
}

.benefit-card p {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* ============ Cross-sell banner ============ */
.cross-sell {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  padding: clamp(1.5rem, 4vw, 2.25rem);
  border-radius: var(--radius-xl);
  background: linear-gradient(120deg, var(--color-surface-3), var(--color-surface-2));
  border: 1px solid var(--color-border);
  flex-wrap: wrap;
}

.cross-sell-copy {
  min-width: 0;
  max-width: 42rem;
  display: grid;
  gap: 0.4rem;
}

.cross-sell-copy h3 {
  font-family: var(--font-display);
  font-size: var(--text-lg);
}

.cross-sell-copy p {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* ============ In-call / explainer panel ============ */
.explainer-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-8);
  align-items: center;
  padding: clamp(1.75rem, 4vw, 2.75rem);
  border-radius: var(--radius-xl);
}

.explainer-panel h2 {
  margin-top: var(--space-2);
}

.explainer-lead {
  margin-top: var(--space-3);
  color: var(--color-text-muted);
  max-width: 56ch;
}

.explainer-list {
  display: grid;
  gap: 0.9rem;
}

.explainer-list a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* NOT flex. A flex li makes the <strong> label and the text node after it
   separate flex items, so any item too long for one line crushed the label to
   min-content — "A certified installer" rendered as a three-line word stack
   beside its own description (and "One car at a time" as two). The dot is
   absolutely positioned instead, so every item wraps like a normal sentence. */
.explainer-list li {
  position: relative;
  padding-left: 1.25rem; /* 0.55rem dot + the old 0.7rem gap */
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.explainer-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: var(--space-2);
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 999px;
  background: var(--color-accent);
}

.explainer-list strong {
  color: var(--color-text);
}

/* ============ Quote page ============ */
.quote-intro-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: var(--space-8);
  align-items: start;
}

.quote-intro-grid h1 {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  max-width: 16ch;
}

.quote-contact-card {
  padding: var(--space-6);
  border-radius: var(--radius-xl);
  display: grid;
  gap: var(--space-4);
  align-content: start;
}

.quote-contact-card h2 {
  font-size: var(--text-base);
}

.quote-contact-list {
  display: grid;
  gap: var(--space-3);
}

.quote-contact-list a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.quote-contact-list li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
  font-size: var(--text-sm);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-divider);
}

.quote-contact-list li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.quote-contact-list li > span:first-child {
  color: var(--color-text-faint);
  flex: 0 0 auto;
}

.quote-contact-list a {
  font-weight: 600;
  text-align: right;
}

.quote-contact-list a:hover,
.quote-contact-list a:focus-visible {
  color: var(--color-accent-strong);
}

.quote-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: var(--space-8);
  align-items: start;
}

.quote-form {
  padding: clamp(1.5rem, 4vw, 2.25rem);
  border-radius: var(--radius-xl);
  display: grid;
  gap: var(--space-6);
  min-width: 0;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4) var(--space-5);
}

.form-field {
  display: grid;
  gap: 0.4rem;
  min-width: 0;
  align-content: start;
}

.form-field-wide {
  grid-column: 1 / -1;
}

.form-field label {
  font-size: var(--text-sm);
  font-weight: 600;
}

.form-field input,
.form-field select,
.form-field textarea {
  width: 100%;
  min-width: 0;
  padding: 0.8rem var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  /* The 1rem floor is load-bearing: iOS zooms the whole page in when a focused
     control is under 16px, and often stays zoomed after blur. --text-sm already
     resolves to 1rem on desktop, so this only lifts phones (14.5px → 16px). */
  font-size: max(1rem, var(--text-sm));
  line-height: 1.6;
  transition: border-color var(--transition), box-shadow var(--transition);
}

.form-field select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--space-10);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b6478' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
  background-size: 1.05rem;
  cursor: pointer;
}

/* ---- Custom select ----
   A native select's popup list is OS-drawn and unstylable, so once JS is up we
   hide the real control (it still holds the value and still submits) and draw
   the list ourselves. Everything below only ever renders when .select-shell
   exists, which only main.js creates — no JS, no change. */
.select-shell {
  position: relative;
  /* A grid item's automatic minimum is its min-content width, and the value
     span below is nowrap — so the longest option ("Somewhere else — we'll
     confirm we cover you") sized the whole control and pushed it 86px out of
     its column, under the sticky steps card. .form-field already carries this
     guard for the same reason; the shell was added later and missed it. */
  min-width: 0;
}

.select-native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.select-button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  min-height: 52px;
  padding: 0.8rem var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  /* Same 1rem floor as the native controls it replaces — keeps the JS and
     no-JS states of the select the same size. */
  font-size: max(1rem, var(--text-sm));
  line-height: 1.6;
  text-align: left;
  color: var(--color-text);
  transition: border-color var(--transition), box-shadow var(--transition);
}

.select-button::after {
  content: "";
  flex: none;
  width: 1.05rem;
  height: 1.05rem;
  background: currentColor;
  opacity: 0.55;
  transition: transform var(--transition);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
}

.select-button[aria-expanded="true"]::after {
  transform: rotate(180deg);
}

.select-button.is-placeholder .select-value {
  color: var(--color-text-faint);
}

.select-value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.select-button:hover {
  border-color: var(--color-accent);
}

.select-button[aria-expanded="true"] {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* mirror the native invalid state onto the visible control */
.form-field:has(select[aria-invalid="true"]) .select-button {
  border-color: #d92d20;
}

.select-list {
  position: absolute;
  z-index: 30;
  top: calc(100% + var(--space-2));
  left: 0;
  right: 0;
  max-height: 16rem;
  overflow-y: auto;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.select-option {
  padding: var(--space-3) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: max(1rem, var(--text-sm));
  color: var(--color-text-muted);
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
}

.select-option.is-placeholder {
  color: var(--color-text-faint);
}

.select-option.is-active {
  background: var(--color-surface-2);
  color: var(--color-text);
}

.select-option[aria-selected="true"] {
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  font-weight: 600;
}

.form-field textarea {
  resize: vertical;
  min-height: 6rem;
}

.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

.form-field input[aria-invalid="true"],
.form-field select[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
  border-color: #d92d20;
}

.field-error {
  display: block;
  font-size: var(--text-xs);
  color: #b3261e;
  min-height: 1.1em;
}

.field-hint {
  font-size: var(--text-xs);
  color: var(--color-text-faint);
  margin: 0;
}

/* Inline accept for the package suggestion. padding-block rather than
   min-height: 44px — this sits mid-sentence, so a block height would push the
   hint's own line apart. Same trick the breadcrumb links use to reach a real
   tap target without disturbing the text they live in. */
.hint-action {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-accent-strong);
  text-decoration: underline;
  padding-block: 0.8rem;
  padding-inline: 0.15rem;
  margin-left: 0.25rem;
}

.hint-action:hover,
.hint-action:focus-visible {
  color: var(--color-accent);
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

.form-note {
  font-size: var(--text-xs);
  color: var(--color-text-faint);
}

.form-status {
  padding: 0.9rem 1.1rem;
  border-radius: var(--radius-lg);
  font-size: var(--text-sm);
  font-weight: 600;
}

.form-status-success {
  background: rgba(16, 163, 74, 0.1);
  color: #0d7a3b;
  border: 1px solid rgba(16, 163, 74, 0.3);
}

.form-status-error {
  background: rgba(217, 45, 32, 0.08);
  color: #b3261e;
  border: 1px solid rgba(217, 45, 32, 0.28);
}

.form-status-pending {
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  border: 1px solid rgba(var(--brand-600-rgb), 0.25);
}

.quote-steps {
  padding: var(--space-6);
  border-radius: var(--radius-xl);
  position: sticky;
  top: calc(var(--header-height) + 1.5rem);
}

.quote-steps h2 {
  font-size: var(--text-base);
  margin-bottom: var(--space-4);
}

.steps-list {
  display: grid;
  gap: 1.1rem;
}

.steps-list li {
  display: flex;
  gap: 0.85rem;
}

.steps-list li > div {
  min-width: 0;
}

.steps-list strong {
  display: block;
  font-size: var(--text-sm);
  margin-bottom: 0.15rem;
}

.steps-list p {
  color: var(--color-text-muted);
  font-size: var(--text-xs);
}

/* ============ 404 page ============ */
.error-page {
  min-height: 60vh;
  display: grid;
  place-items: center;
  text-align: center;
  padding-block: clamp(var(--space-12), 8vw, var(--space-20));
}

.error-page-inner {
  display: grid;
  gap: var(--space-5);
  justify-items: center;
  max-width: 34rem;
}

.error-code {
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 12vw, 6rem);
  color: var(--color-accent-strong);
  line-height: 1;
}

.error-page h1 {
  font-family: var(--font-display);
  font-size: var(--text-xl);
}

.error-page p {
  color: var(--color-text-muted);
}

.error-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
}

.error-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  justify-content: center;
  font-size: var(--text-sm);
}

.error-links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.error-links a:hover,
.error-links a:focus-visible {
  color: var(--color-accent-strong);
}

/* ============ Component responsive ============ */
@media (max-width: 1080px) {
  .hero-grid {
    grid-template-columns: 1fr;
  }

  .hero-card {
    min-height: 26rem;
  }

  .value-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

    .price-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .price-grid .price-card:first-child:nth-last-child(3) {
    grid-column: 1 / -1;
    max-width: 26rem;
    margin-inline: auto;
  }

  .benefit-grid {
    grid-template-columns: 1fr;
  }

  .explainer-panel {
    grid-template-columns: 1fr;
  }

  .quote-intro-grid,
  .quote-layout {
    grid-template-columns: 1fr;
  }

  .quote-steps {
    position: static;
  }
}

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

  .form-field-wide {
    grid-column: auto;
  }

  .form-actions {
    flex-direction: column;
    align-items: stretch;
  }
}

@media (max-width: 900px) {

  .addons-strip,
  .bundle-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 759px) {
  .hero h1 {
    max-width: none;
    font-size: clamp(2.1rem, 9vw, 2.75rem);
  }

  .hero-points {
    grid-template-columns: 1fr;
  }

  .hero-actions .btn {
    width: 100%;
  }

  
  .value-grid,
  
  .price-grid,
  .price-grid.price-grid-2,
  .addons-strip,
  .bundle-grid {
    grid-template-columns: 1fr;
  }

  .price-grid .price-card:first-child:nth-last-child(3) {
    max-width: none;
  }

  .cross-sell {
    flex-direction: column;
    align-items: stretch;
    text-align: center;
  }

  .cross-sell-copy {
    max-width: none;
  }

  .hero-card {
    min-height: 20rem;
  }
}

@media (max-width: 380px) {
  .sticky-bar-btn {
    font-size: 0.85rem;
    gap: 0.35rem;
  }

  .sticky-bar-btn svg {
    width: 1rem;
    height: 1rem;
  }
}

/* =================================================================
   Responsive — header breakpoints first (fixes 761–1080px overflow
   by moving the compact/hamburger state up to 1099px), then the
   1080px content-grid break, then the sub-760px mobile treatment.
   ================================================================= */

/* Compact header: 760–1099px — hamburger nav, call+quote pills stay */
@media (max-width: 1099px) {
  .primary-nav {
    position: absolute;
    top: calc(100% + 0.5rem);
    left: 0.75rem;
    right: 0.75rem;
    flex-direction: column;
    align-items: stretch;
    gap: 0.15rem;
    padding: var(--space-3);
    border-radius: 1.1rem;
    background: rgba(255, 255, 255, 0.97);
    border: 1px solid var(--color-border);
    box-shadow: var(--shadow-md);
    opacity: 0;
    pointer-events: none;
    transform: translateY(-8px);
    transition: opacity var(--transition), transform var(--transition);
  }

  .primary-nav.open {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
  }

  .primary-nav a {
    padding: 0.9rem 0.9rem;
    border-radius: 0.8rem;
  }

  .primary-nav a::after {
    display: none;
  }

  .primary-nav a[aria-current="page"] {
    background: var(--color-accent-soft);
  }

  .primary-nav a:hover,
  .primary-nav a:focus-visible {
    background: rgba(27, 21, 35, 0.05);
  }

  .nav-toggle {
    display: inline-flex;
  }

  .brand-copy span {
    max-width: 11rem;
  }
}

/* Mobile: under 760px — pills hidden, sticky bar takes over */
@media (max-width: 759px) {
  /* 1.25rem (a 10px gutter) was tighter than the 1rem the same container gets
     on desktop — the gutter got *narrower* as the screen got smaller, which is
     backwards, and it read as cramped under the homepage's hero headline.
     2.5rem = 20px each side.

     Deliberately changed here and not in the hero, even though the hero is what
     exposed it: the header, the footer and every .section share this container,
     and the point of sharing it is that they cannot drift apart. Padding the
     hero alone would have bought 20px of breathing room at the cost of standing
     the h1 10px inboard of the navbar logo directly above it. Everything moves
     together or nothing does. */
  .container {
    width: min(calc(100% - 2.5rem), var(--content));
  }

  .header-pill-call,
  .header-pill-quote {
    display: none;
  }

  .brand-copy span {
    display: none;
  }

  .sticky-bar {
    display: flex;
  }

  body {
    padding-bottom: calc(var(--sticky-bar-height) + env(safe-area-inset-bottom, 0px));
  }

  .site-footer {
    padding-bottom: calc(var(--space-8) + var(--sticky-bar-height));
  }

  /* .section padding-block override removed — --section-gap's clamp already
     scales it, and this rule actively fought it: at 759px it computed 56px
     while the base rule gave 38px at 760px, so the gap got *wider* as the
     screen got narrower. */

  .section-head {
    margin-bottom: 1.75rem;
  }

  /* Same selector group as the base rule above — the band/panel h2s must track
     .section-head h2 at every breakpoint, or the drift this fixed comes back
     mobile-only. */
  .section-head h2,
  .ceramic-band-copy h2,
  .interior-offer-copy h2,
  .explainer-panel h2 {
    font-size: clamp(1.55rem, 6vw, 2.1rem);
  }

  .section-head p {
    font-size: 0.98rem;
  }

  .cta-band h2 {
    font-size: clamp(1.7rem, 8vw, 2.3rem);
  }
}

@media (max-width: 480px) {
  .header-inner {
    min-height: 4.25rem;
  }

  .brand-mark {
    width: 2.05rem;
    height: 2.05rem;
  }
}

/* Footer collapses under 900px */
@media (max-width: 900px) {
  /* touch widths — restore the 44px floor.
     .footer-areas .footer-cities a, NOT .footer-cities a: the city links' base
     rule (min-height: 32px) lives in the v2 footer section BELOW this block,
     so a bare .footer-cities a here ties on specificity and loses on source
     order — which is exactly what shipped for a while: nine 32px links wrapped
     into rows touching edge-to-edge on every page, while this rule sat dead
     (found by measurement 2026-07-19c, computed min-height 32px at 375px).
     The extra ancestor wins the tie regardless of order. .footer-col a and
     .footer-legal a are fine bare: their base rules sit above this block. */
  .footer-col a,
  .footer-areas .footer-cities a,
  .footer-legal a {
    min-height: 44px;
  }

  /* Brand spans the top, then Services | Contact | Follow sit in three equal
     columns beneath it. With the old `1fr 1fr` the new fourth column would have
     wrapped onto a row of its own and left a hole beside it. */
  .footer-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-6);
  }

  .footer-areas {
    gap: var(--space-1) var(--space-5);
  }

  .footer-brand {
    grid-column: 1 / -1;
    max-width: none;
  }
}

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

  .footer-bottom {
    flex-direction: column;
  }
}

/* =================================================================
   Homepage ceramic band + footer service area (v2)
   Ceramic is the tier we most want to sell, so it earns one calm
   band of its own between the ladder and the proof.
   ================================================================= */
.ceramic-band {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: var(--space-12);
  align-items: center;
}

.ceramic-band-copy h2 {
  margin: var(--space-2) 0 var(--space-4);
}

.ceramic-band-copy p {
  color: var(--color-text-muted);
  max-width: 46ch;
  margin-bottom: var(--space-6);
}

.ceramic-band-facts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

.ceramic-band-facts li {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-4) var(--space-5);
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(var(--brand-600-rgb), 0.14);
  border-radius: var(--radius-md);
}

.ceramic-band-facts strong {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--color-accent-strong);
  font-variant-numeric: tabular-nums;
}

.ceramic-band-facts span {
  font-size: var(--text-sm);
  color: var(--color-text-faint);
}

/* Quiet closing note under the ladder — not a section, just a line. */
.section-foot {
  margin-top: var(--space-8);
  text-align: center;
  font-size: var(--text-sm);
  color: var(--color-text-faint);
}

.footer-cities {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-4);
  font-size: var(--text-sm);
  flex: 1 1 24rem;
}

.footer-cities a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
}

.footer-cities a:hover,
.footer-cities a:focus-visible {
  color: var(--color-accent-strong);
}

.footer-note {
  flex: 1 1 100%;
  margin: 0;
  font-size: var(--text-xs);
  opacity: 0.75;
}

@media (max-width: 860px) {
  .ceramic-band {
    grid-template-columns: 1fr;
    gap: var(--space-8);
  }
}

/* =================================================================
   Service-area city pages (v2)
   ================================================================= */
/* .hero-compact no longer sets padding-block — it inherits .hero's, so a city
   page's hero seam matches every other page's. It used to say 4rem/3rem, which
   was not even compact: it was *larger* on top than the .hero it claims to
   shrink. Narrowing the copy is the part that was real, and all that is left. */
.hero-compact .hero-copy {
  max-width: 46rem;
}

.crumbs {
  font-size: var(--text-sm);
  color: var(--color-text-faint);
  margin-bottom: var(--space-6);
}

.crumbs a {
  color: var(--color-accent-strong);
  padding-block: var(--space-3);
}

.crumbs span[aria-hidden] {
  margin: 0 var(--space-2);
  opacity: 0.5;
}

.local-notes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-5);
}

.local-note {
  padding: var(--space-6);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}

/* A hairline of the brand at the top of each note — the one decorative
   flourish on these pages, and it doubles as a reading anchor. */
.local-note {
  border-top: 2px solid rgba(var(--brand-600-rgb), 0.4);
}

.local-note p {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  margin: 0;
}

@media (max-width: 960px) {
  .local-notes {
    grid-template-columns: 1fr;
  }
}

/* Legal pages — long-form reading, so give it a measure and rhythm. */
.legal-body {
  max-width: 46rem;
}

.legal-body h2 {
  font-size: var(--text-lg);
  margin: var(--space-10) 0 var(--space-3);
}

.legal-body h2:first-child {
  margin-top: 0;
}

.legal-body p {
  color: var(--color-text-muted);
  margin-bottom: var(--space-4);
}

.legal-updated {
  margin-top: var(--space-12);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
  font-size: var(--text-sm);
  color: var(--color-text-faint);
}
