@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/cormorant-garamond-600.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/cormorant-garamond-700.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/inter-var.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light;

  --brand-primary: #2B3537;
  --brand-accent: #976A43;
  --brand-neutral: #DDD8CB;
  --brand-muted: #C2C8C6;
  --brand-bg: #F7F8F6;
  --brand-gray: #505352;
  --brand-on-primary: #F7F8F6;

  /* 6.23:1 on --brand-bg, 4.67:1 on --brand-neutral (was #7C5737 at 4.506:1, too close) */
  --color-accent-text: #795535;
  /* ON-DARK ACCENT. Was #BE926C, which measured 4.5093:1 on --brand-primary -
     nine thousandths over the 4.5 bar. That is not headroom, it is a rounding
     accident, and it broke the moment anything touched the ground underneath:
     .site-header paints rgba(43,53,55,0.97), and letting 3% of the off-white
     page through lifts the bar to #313B3D, where the header quote button
     measured 4.1224:1. A real failure, on every page, since the build.
     #C59D7B clears every surface the token lands on and regresses none:
       #2B3537 section/hero/footer   4.509 -> 5.088
       #252E30 vignette darkest      4.971 -> 5.610
       #313B3D header over off-white 4.112 -> 4.652   (was failing)
       #303A3B header over tan       4.190 -> 4.728   (was failing)
       #2C3638 hero under mobile     4.443 -> 5.014   (was failing)
       as a FILL under #2B3537 text  4.509 -> 5.088
     Lightening is the safe direction here because on this site the token is
     only ever text-on-dark or a fill under dark text - it never becomes a
     surface that light text has to sit on. Guardrails unchanged: raw #976A43
     stays fills-and-strokes only, and bronze still never goes on sage. */
  --color-accent-on-dark: #C59D7B;

  --color-bg: var(--brand-bg);
  --color-surface: #FFFFFF;
  --color-text: var(--brand-primary);
  --color-text-soft: var(--brand-gray);
  --color-border: var(--brand-muted);

  --focus-ring: var(--brand-accent);
  --focus-ring-width: 3px;
  --focus-ring-offset: 3px;

  --font-display: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  --font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  --step-0: 1rem;
  --step-1: 1.125rem;
  --step-2: clamp(1.25rem, 0.5vw + 1.15rem, 1.5rem);
  --step-3: clamp(1.75rem, 1.4vw + 1.4rem, 2.25rem);
  --step-4: clamp(2rem, 2.6vw + 1.4rem, 3rem);
  --step-5: clamp(2.75rem, 6vw + 1rem, 5rem);

  --space-xs: 0.5rem;
  /* HEADING RHYTHM - the single gap between any heading and the block that
     follows it. Applied per mechanism below (flow margin, flex gap or grid
     row-gap) because a blanket margin would ADD to a flex gap rather than
     replace it. Change it here and every heading on the site moves together. */
  --heading-gap: 1rem;
  --space-sm: 1rem;
  --space-md: 2rem;
  --space-lg: 4rem;
  --space-xl: 6rem;

  --radius: 4px;
  --hairline: 1px;
  --photo-rule-offset: 12px;
  --container: 1200px;
  --measure: 68ch;

  --dur-base: 250ms;
  --dur-slow: 700ms;
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* scroll-padding keeps an anchored heading clear of the sticky header.
   Smooth scrolling is deliberately NOT on plain html: it would animate the
   load-time fragment jump, so someone arriving from another page at
   /lighting#holiday-lighting would watch the page crawl 5000px instead of
   landing there. app.js adds .is-loaded once that jump is done, so only
   same-page clicks animate. base.css forces auto under reduced-motion. */
html { scroll-padding-top: 6rem; }
html.is-loaded { scroll-behavior: smooth; }

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-4); }
h3 { font-size: var(--step-3); }
h4 { font-size: var(--step-2); }

p, li { text-wrap: pretty; }
a { color: var(--color-accent-text); text-underline-offset: 0.2em; }
a:hover { color: var(--brand-primary); }

.skip-link { background: var(--brand-primary); color: var(--brand-bg); border-radius: var(--radius); }

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 4vw, 2.5rem);
}

.section { padding-block: clamp(3.5rem, 8vw, var(--space-xl)); }
.section--tight { padding-block: clamp(2.5rem, 5vw, 4rem); }

/* Every surface that paints itself dark must re-point the tokens, or anything
   inside it resolves the light-ground values against a dark background. The
   .callout was missing from this list and its hover label came out at 1.96:1. */
.section--dark,
.hero,
.cta-band,
.trust-strip,
.site-footer,
.consent-banner,
.plan--featured,
.callout {
  --color-text: var(--brand-bg);
  --color-text-soft: var(--brand-muted);
  --color-accent-text: var(--color-accent-on-dark);
  --color-border: rgba(247, 248, 246, 0.22);
  --color-surface: rgba(247, 248, 246, 0.06);
  --focus-ring: var(--color-accent-on-dark);
}
.section--dark { background: var(--brand-primary); color: var(--brand-bg); }
.section--neutral { background: var(--brand-neutral); }
.section--surface { background: var(--color-surface); }

.section--dark h1, .section--dark h2, .section--dark h3, .section--dark h4 { color: var(--brand-bg); }
.section--dark a { color: var(--color-accent-on-dark); }
/* Section intros on a dark ground read at full strength rather than muted -
   .fit-note declares its own bronze later in the sheet and is unaffected. */
.section--dark .lead { color: var(--color-text); }
.section--dark a:hover { color: var(--brand-bg); }

.eyebrow {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: var(--space-sm);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-text);
}
.icon {
  display: inline-block;
  flex: none;
  width: 1.25em;
  height: 1.25em;
  vertical-align: -0.25em;
}

.eyebrow svg { flex: none; width: 1.125rem; height: 1.125rem; }
.eyebrow::after {
  content: "";
  flex: 1;
  max-width: 4rem;
  height: var(--hairline);
  background: currentColor;
  transform-origin: left;
}

/* The measure belongs on the heading, NOT on this wrapper. 34ch here resolves
   against the wrapper's inherited 16px body size (~343px), which then clamps a
   48px h2 to about 7 words and made every section heading stack 2-5 lines. The
   h1/h2 rule below is the one that should govern, so the wrapper stays open. */
.section-title { margin-bottom: var(--space-sm); }
.section-title h1, .section-title h2 { max-width: 20ch; }
@media (min-width: 861px) { .section-title h1, .section-title h2 { max-width: none; } }
.lead { font-size: var(--step-1); color: var(--color-text-soft); max-width: var(--measure); }
.prose { max-width: var(--measure); }
/* FIT NOTE - the closing "a good fit if..." line in each service section, plus
   the holiday booking note, which is the one of the eight that is NOT a good-fit
   line and is why these boxes carry no fixed label.

   Was an italic bronze sentence hanging off the bottom of the checklist. Chance
   asked for it in its own notice box on 2026-08-25 and picked the filled-disc
   treatment. There is no outline: the bronze disc does the signalling. The
   italic went with it, because the box now does the separating the italic was
   doing, and italic body text in a box is harder to read for no gain.

   The tint is 9% rather than 10% for one measured reason. At 10% the disc came
   to 2.99:1 against its own panel on the tan ground, a hair under the 3:1
   non-text bar. 9% puts it at 3.02:1 and is visually identical.

   Measured on all three grounds these appear on:
     off-white   panel #F0EEE9   text 10.86:1   disc 4.06:1
     tan         panel #D7CEBF   text  8.14:1   disc 3.02:1
     dark slate  panel #3F4340   text  9.44:1   disc 4.06:1

   stroke-width is forced to 2 because icon() hardcodes 1.5, which disappears at
   the 17px the glyph renders at inside the disc. */
.fit-note {
  --note-tint:  rgba(151, 106, 67, 0.09);
  --note-disc:  var(--brand-accent);
  --note-glyph: var(--brand-bg);
  display: flex;
  align-items: center;
  gap: 0.875rem;
  margin-top: var(--space-md);
  padding: 0.95rem 1.15rem;
  background: var(--note-tint);
  border-radius: var(--radius);
}
.section--dark .fit-note {
  --note-tint:  rgba(197, 157, 123, 0.13);
  --note-disc:  var(--color-accent-on-dark);
  --note-glyph: var(--brand-primary);
}
.fit-note__mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: var(--note-disc);
  color: var(--note-glyph);
}
.fit-note__mark svg { width: 1.05rem; height: 1.05rem; stroke-width: 2; }
.fit-note p {
  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--color-text);
}
@media (max-width: 480px) {
  .fit-note { align-items: flex-start; gap: 0.75rem; padding: 0.85rem 1rem; }
  .fit-note__mark { width: 1.75rem; height: 1.75rem; }
  .fit-note__mark svg { width: 0.95rem; height: 0.95rem; }
}

/* MISSION LINE - the founding-mission sentence in the About story. It moved out
   of the .lead slot beside the photo and into the prose, so the accent brown is
   what keeps it reading as a statement of intent rather than another body
   paragraph. Same ground-aware token as .fit-note: 6.02:1 on off white. */
.mission-line { color: var(--color-accent-text); }
/* Placement lives with the .split rules further down - it has to be declared
   after .split's `gap` shorthand or the row-gap override loses the cascade. */

.prose > * + * { margin-top: var(--space-sm); }

/* A heading followed by a .prose WRAPPER rather than a bare <p>: the wrapper
   carries no margin and `.prose > * + *` never fires on its first child, so the
   paragraph sat directly against the heading at 0px. This was the tense spacing
   on every service section. Sibling margins collapse, so where the next element
   already supplies the same value nothing doubles. */
main :is(h1, h2, h3, h4, h5, h6) + :is(.prose, .checklist, .faq, ul, ol, dl) {
  margin-top: var(--heading-gap);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 48px;
  padding: 0.75rem 1.75rem;
  border: var(--hairline) solid transparent;
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-base) var(--ease), color var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.btn-primary { background: var(--brand-accent); color: #FFFFFF; }

/* On a dark ground raw bronze is only 2.67:1 against the background, so the
   button SHAPE fails WCAG 1.4.11 even though its white label passes. The
   lightened bronze clears both: 4.51:1 as a surface, 4.51:1 under slate text. */
.section--dark .btn-primary,
.hero .btn-primary,
.cta-band .btn-primary,
.callout .btn-primary,
.consent-banner .btn-primary,
.plan--featured .btn-primary,
.site-header .btn-primary {
  background: var(--color-accent-on-dark);
  color: var(--brand-primary);
}

/* PRIMARY HOVER - the fill drops away and the button becomes a bronze outline
   with bronze text. One rule covers both grounds because --color-accent-text is
   already re-pointed to the lightened bronze inside every dark context, so the
   label and border land on the accessible tone either way: 6.02:1 on off white,
   4.51:1 on tan, 4.51:1 on slate. Raw bronze is never used here - it is 4.42:1
   on the site's off-white ground and would fail as a label.
   The base .btn already carries a 1px transparent border, so nothing shifts. */
.btn-primary:hover,
.section--dark .btn-primary:hover,
.hero .btn-primary:hover,
.cta-band .btn-primary:hover,
.callout .btn-primary:hover,
.consent-banner .btn-primary:hover,
.plan--featured .btn-primary:hover,
.site-header .btn-primary:hover {
  background: transparent;
  color: var(--color-accent-text);
  border-color: var(--color-accent-text);
  transform: translateY(-2px);
}

.btn-ghost { background: transparent; color: var(--color-text); border-color: var(--color-border); }
.section--dark .btn-ghost,
.hero .btn-ghost,
.cta-band .btn-ghost { color: var(--brand-bg); border-color: rgba(247, 248, 246, 0.4); }

/* GHOST HOVER - inverts against whatever it is sitting on. On the dark grounds
   where these mostly live (hero, CTA bands) that means an off-white fill with
   slate text, which is what was asked for. The single ghost on a light ground
   inverts the other way for the same reason - hovering to white on an off-white
   page would be no visible change at all. */
.btn-ghost:hover {
  background: var(--brand-primary);
  color: var(--brand-bg);
  border-color: var(--brand-primary);
  transform: translateY(-2px);
}
.section--dark .btn-ghost:hover,
.hero .btn-ghost:hover,
.cta-band .btn-ghost:hover {
  background: var(--brand-bg);
  color: var(--brand-primary);
  border-color: var(--brand-bg);
}
.btn-lg { min-height: 56px; padding: 1rem 2.25rem; font-size: 1rem; }

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  /* Opaque, deliberately. At 0.97 the page ground bled through and moved the
     header's effective colour with whatever section happened to be under it -
     #313B3D over the off-white ground, #303A3B over a tan band - which is what
     put the quote button under the contrast bar. An opaque bar is one known
     colour everywhere. backdrop-filter went with it: it does nothing behind an
     opaque fill and was costing a compositing layer on every scroll. */
  background: var(--brand-primary);
  border-bottom: var(--hairline) solid rgba(247, 248, 246, 0.14);
}
/* Wider than --container on purpose. With the logo held at its real 163px, the
   bar needs 1230px for logo + six nav labels + labelled phone + quote button, and
   --container only leaves 1120px inside the padding. 85rem leaves ~50px spare. */
.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  max-width: 85rem;
  margin-inline: auto;
  padding: 0.875rem clamp(1.25rem, 4vw, 2.5rem);
}
/* flex-shrink: 0 is load-bearing. Without it the brand link was the only flex
   item allowed to give up width when the nav grew (Care Plans, 2026-09-09), and
   the img's max-width: 100% followed it down while its height stayed 42px: the
   wordmark rendered 53px wide at desktop and 0px at 1024. */
.brand {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: 0.625rem;
  margin-right: auto;
  color: #F7F8F6;
  text-decoration: none;
}
/* The client's supplied wordmark, off-white artwork, so it is only ever placed
   on the slate header and footer. Height is driven here and width left auto;
   the intrinsic width/height attributes on the <img> hold the aspect ratio so
   the bar does not reflow while the file loads. The second line of the lockup
   ("Exterior & Events") is only a quarter of the artwork's height, which sets
   the floor on how small this can go before it stops reading. */
.brand__logo { display: block; height: 42px; width: auto; }
/* Header only. The narrow step exists because the logo shares the bar with the
   menu button; the footer lockup has a full column to itself and does not. */
@media (max-width: 560px) { .site-header .brand__logo { height: 36px; } }

/* Direct child only. As `.nav-desktop ul` this also matched .dropdown at a
   higher specificity (0,1,1) than .dropdown itself (0,1,0), so the dropdown
   was laid out as a flex ROW and no rule on .dropdown could override it. */
/* NAV - letterspaced capitals in the HEADING face at bold. Cormorant reads
   optically smaller and lighter than Inter at the same size, so the caps are set
   at 15px/700 rather than the 12px/600 the sans version used; at 12px this face
   goes spindly and the tracking pulls it apart. Tracking is eased to 0.12em for
   the same reason - serif capitals are already wider than sans ones. */
.nav-desktop > ul { display: flex; align-items: center; gap: 0; list-style: none; padding: 0; margin: 0; }
.nav-desktop .dropdown { list-style: none; margin: 0; }
.nav-desktop a {
  display: block;
  padding: 0.55rem 0;
  margin-inline: 0.875rem;
  border-bottom: 1px solid transparent;
  color: #F7F8F6;
  font-family: var(--font-display);
  font-size: 0.9375rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease),
              background var(--dur-base) var(--ease);
}
/* Hover and current page are deliberately different: both carry the rule, but
   only hover recolours the label, so the two states never look identical. */
.nav-desktop a:hover { color: var(--color-accent-on-dark); border-bottom-color: var(--color-accent-on-dark); }
.nav-desktop a[aria-current="page"] { color: #F7F8F6; border-bottom-color: var(--color-accent-on-dark); }

/* Submenu labels stay sentence case at a readable size - tracking capitals is a
   navigation-bar device, and it stops being legible on four-word service names. */
.nav-desktop .dropdown a {
  margin-inline: 0;
  padding: 0.5rem 0.875rem;
  border-bottom: 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: none;
  white-space: nowrap;
}
.nav-desktop .dropdown a:hover,
.nav-desktop .dropdown a[aria-current="page"] { color: var(--color-accent-on-dark); }

/* Outline, not a slab. Inverts to the fill on hover, which is the same
   direction of travel every other bronze button on the site takes. */
.nav-desktop a.cta {
  margin-left: 1.75rem;
  margin-right: 0;
  padding: 0.7rem 1.35rem;
  border: 1px solid var(--color-accent-on-dark);
  border-radius: 2px;
  background: transparent;
  color: var(--color-accent-on-dark);
  /* 700 is a real loaded weight in both faces, so this is not a synthesised
     faux-bold in either the bar or the drawer. */
  font-weight: 700;
  letter-spacing: 0.12em;
}
.nav-desktop a.cta:hover {
  background: var(--color-accent-on-dark);
  border-color: var(--color-accent-on-dark);
  color: #2B3537;
  box-shadow: none;
}

.has-dropdown { position: relative; }
.dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  /* .nav-desktop ul also matches this element, so it was inheriting
     display:flex and laying the items out in a ROW - the old min-width:15rem
     hid that by forcing each label to wrap. Stack them explicitly. */
  display: grid;
  /* max-content so the widest label sets the width and nothing wraps */
  min-width: max-content;
  padding: 0.5rem;
  background: #222B2C;
  border: var(--hairline) solid rgba(190, 146, 108, 0.28);
  border-radius: 2px;
  list-style: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease), visibility var(--dur-base);
}
.has-dropdown:hover .dropdown,
.has-dropdown:focus-within .dropdown { opacity: 1; visibility: visible; transform: translateY(0); }
.dropdown a { font-size: 1rem; white-space: nowrap; }

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: var(--hairline) solid rgba(247, 248, 246, 0.3);
  border-radius: var(--radius);
  color: #F7F8F6;
  cursor: pointer;
}
.nav-toggle svg { width: 24px; height: 24px; }

.nav-mobile {
  padding: var(--space-md) var(--space-sm);
  background: #2B3537;
  color: #F7F8F6;
  overflow-y: auto;
}
.nav-mobile ul { list-style: none; padding: 0; margin: 0; }
.nav-mobile > ul { margin-top: var(--space-md); }
/* The drawer carries the same letterspaced capitals as the desktop bar, a step
   larger because it is a touch target rather than a pointer one. */
.nav-mobile a, .nav-mobile summary {
  display: block;
  padding: 0.875rem 0.5rem;
  border-bottom: var(--hairline) solid rgba(247, 248, 246, 0.14);
  color: #F7F8F6;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
}
.nav-mobile summary { font-weight: 600; list-style: none; }
.nav-mobile summary::-webkit-details-marker { display: none; }
.nav-mobile summary::after { content: " +"; color: var(--color-accent-on-dark); }
.nav-mobile details[open] summary::after { content: " -"; }
.nav-mobile details ul { padding-left: var(--space-sm); }
/* Submenu labels stay sentence case, same reasoning as the desktop dropdown. */
.nav-mobile details a {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: none;
  color: #C2C8C6;
}
.nav-mobile a.cta {
  margin-top: var(--space-sm);
  padding: 0.9375rem 1rem;
  border: 1px solid var(--color-accent-on-dark);
  border-radius: 2px;
  background: transparent;
  color: var(--color-accent-on-dark);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-align: center;
}
.nav-mobile a.cta:hover {
  background: var(--color-accent-on-dark);
  border-color: var(--color-accent-on-dark);
  color: #2B3537;
  box-shadow: none;
}
.nav-mobile__close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: transparent;
  border: var(--hairline) solid rgba(247, 248, 246, 0.3);
  border-radius: var(--radius);
  color: #F7F8F6;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}
/* Collapse point. Logo + six labels + phone icon + quote button measure ~1119px,
   so below ~1200px viewport the bar cannot hold them; 1240px keeps ~40px spare.
   If a nav item is added, re-measure this rather than letting it squeeze. */
@media (max-width: 1240px) {
  .nav-desktop { display: none; }
  .nav-toggle { display: inline-flex; }
}

.hero {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: clamp(30rem, 82vh, 46rem);
  padding-block: var(--space-xl) var(--space-lg);
  background: var(--brand-primary);
  color: var(--brand-bg);
  overflow: hidden;
  isolation: isolate;
}
.hero__media { position: absolute; inset: 0; z-index: -2; }
.hero__media img { width: 100%; height: 100%; object-fit: cover; }

.hero__carousel .hero__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1.4s var(--ease);
}
.hero__carousel .hero__slide.is-active { opacity: 1; }

.hero__controls {
  position: absolute;
  right: clamp(1.25rem, 4vw, 2.5rem);
  bottom: clamp(1.25rem, 3vw, 2rem);
  display: flex;
  align-items: center;
  gap: 0.875rem;
}
.hero__controls[hidden] { display: none; }
.hero__dots { display: flex; gap: 0.5rem; list-style: none; padding: 0; margin: 0; }
.hero__dot {
  display: block;
  width: 30px;
  height: 30px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.hero__dot::before {
  content: "";
  display: block;
  width: 9px;
  height: 9px;
  margin: auto;
  border: 1px solid var(--color-accent-on-dark);
  border-radius: 50%;
  transition: background var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.hero__dot:hover::before { background: rgba(190, 146, 108, 0.55); }
.hero__dot.is-active::before { background: var(--color-accent-on-dark); transform: scale(1.25); }

.hero__pause {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: rgba(43, 53, 55, 0.55);
  border: 1px solid var(--color-accent-on-dark);
  border-radius: 50%;
  cursor: pointer;
}
.hero__pause:hover { background: var(--color-accent-on-dark); }
.hero__pause-icon {
  display: block;
  width: 10px;
  height: 11px;
  border-left: 3px solid var(--color-accent-on-dark);
  border-right: 3px solid var(--color-accent-on-dark);
}
.hero__pause:hover .hero__pause-icon { border-color: var(--brand-primary); }
.hero__pause[aria-pressed="true"] .hero__pause-icon {
  width: 0;
  height: 0;
  border-left: 11px solid var(--color-accent-on-dark);
  border-right: 0;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
}
.hero__pause[aria-pressed="true"]:hover .hero__pause-icon { border-left-color: var(--brand-primary); }
/* HERO SCRIM - design-direction.md specifies "#2B3537 at 55% over it so text
   clears comfortably". The build had drifted to 0.94 at the text edge and never
   dropped below 0.55, so the two scrims (this one plus .hero__dim) stacked to
   roughly 0.97 under the headline and 0.80 at the far edge - which is why the
   hero photograph did not read at all. Nothing on the right half needs
   protecting: .hero__card is capped at 46rem inside a 1200px container, so the
   image side carries no text.

   FIRST CUT WAS WRONG and the correction is the useful part. It ramped down
   from 34%, but .hero__card runs to 61% of the viewport at 1536 and the
   highlight span - the tail of the h1, so the rightmost text on the page - sits
   at 48.8%. It was landing on 0.68 of scrim instead of 0.80, and measured
   1.95:1 against a 3.0 bar. The stops now hold 0.86 through 62%, past the end
   of the card, and only open up over the bare image beyond it.

   Measure the CARD's right edge as a percentage of the VIEWPORT before touching
   these stops, not the container - the container is centred, so the card ends
   further left than it looks.

   Verified against the brightest pixel in the hero plate: off-white body copy
   8.15:1, the highlight span 3.21:1, the eyebrow's bronze rule 3.20:1. The
   heroes are still placeholder art, and those plates carry bright label text
   exactly where the copy sits, so this is the pessimistic case - but re-measure
   against the real dusk photography when it lands. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(100deg, rgba(43, 53, 55, 0.90) 0%, rgba(43, 53, 55, 0.86) 62%, rgba(43, 53, 55, 0.40) 84%, rgba(43, 53, 55, 0.16) 100%),
    linear-gradient(0deg, rgba(43, 53, 55, 0.45) 0%, rgba(43, 53, 55, 0) 42%);
}

/* Below the two-column breakpoint the card spans the full width, so a
   left-to-right ramp would strand the end of every line over an unprotected
   image. Go vertical instead and keep the whole text block covered. */
@media (max-width: 860px) {
  .hero::after {
    background: linear-gradient(180deg, rgba(43, 53, 55, 0.82) 0%, rgba(43, 53, 55, 0.88) 45%, rgba(43, 53, 55, 0.92) 100%);
  }
}
/* Left-anchored at every width (Chance, 2026-09-23): the text block no longer
   centres inside --container, so on wide screens it stays on the left over the
   darkest part of the scrim instead of drifting toward the middle. */
.hero__inner { width: 100%; padding-inline: clamp(1.25rem, 5vw, 6rem); }
.hero__card { max-width: 46rem; }
/* HERO EYEBROW - the one place on the site where bronze is not available.
   #BE926C tops out at 4.51:1 on flat slate, so over a PHOTOGRAPH it has
   nothing left: solved for the minimum scrim alpha that would carry it at 13px
   and the answer came back "impossible at any alpha, including 1.0". Lifting
   the token to #C59D7B does not rescue it either - 3.61:1 against the
   brightest pixel in the plate. This is structural, not a tuning problem, and
   it holds whatever photography ships.
   So the LABEL takes the warm neutral, which reads 6.27:1 in the same spot,
   and the rule under it stays bronze - that is non-text and owes 3:1, which it
   clears at 3.61. The bronze cue survives; the unreadable part does not. */
.hero .eyebrow { color: var(--brand-neutral); }
.hero .eyebrow::after { background: var(--color-accent-on-dark); }
.hero h1 { color: var(--brand-bg); margin-bottom: var(--heading-gap); }
.hero__highlight { color: var(--color-accent-on-dark); }
.hero__sub { font-size: var(--step-1); color: var(--brand-bg); max-width: 44ch; margin-bottom: var(--space-md); }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.875rem; }
.hero--compact { min-height: clamp(24rem, 56vh, 32rem); align-items: center; padding-block: clamp(3.5rem, 8vw, 5.5rem); }

.trust-strip { background: var(--brand-primary); border-top: var(--hairline) solid rgba(247, 248, 246, 0.16); color: var(--brand-bg); }
.trust-strip ul {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: var(--space-sm);
  list-style: none;
  padding: var(--space-md) 0;
  margin: 0;
}
.trust-strip li {
  display: flex;
  align-items: center;
  /* The grid already gives every item an identical cell. Left-aligned content
     inside those cells is what made the spacing read as uneven: the labels run
     from 13 to 28 characters, so the ink ended in a different place in each
     one. Centring puts each item in the middle of its own equal slot. */
  justify-content: center;
  gap: 0.625rem;
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--brand-neutral);
}
.trust-strip svg { flex: none; width: 22px; height: 22px; color: var(--color-accent-on-dark); }

/* BLOCK SPACING - .gallery, .timeline, .pair, .faq and .service-index all
   already default to margin-top: var(--space-md). These three were left at
   margin: 0, so every use site was nudging them inline instead, at 1.5rem,
   2rem, 2.5rem and 4rem in twelve different places for what is the same job.
   Now they take the same step off the scale and the inline styles are gone.

   .audience is deliberately NOT in this list: it always follows a heading
   directly, so the heading-adjacency rule above already gives it
   --heading-gap, which is the correct, tighter step. */
.cards { display: grid; gap: var(--space-sm); list-style: none; padding: 0; margin: var(--space-md) 0 0; }
/* Explicit column counts, not auto-fit. The item counts are known (4 service
   cards, 6 service cards), and auto-fit was packing them 3+1 and 4+2 - an
   orphan row that reads as a mistake. */
.cards--3 { grid-template-columns: 1fr; }
@media (min-width: 640px) {
  .cards--3 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1000px) {
  .cards--3 { grid-template-columns: repeat(3, 1fr); }
}

.card {
  display: flex;
  flex-direction: column;
  gap: var(--heading-gap);
  padding: var(--space-md) var(--space-sm);
  background: var(--color-surface);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  transition: transform var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
}
.card:hover { transform: translateY(-3px); border-color: var(--brand-accent); }
.card__icon { color: var(--color-accent-text); }
.card__icon svg { width: 34px; height: 34px; }
.card h3 { font-size: var(--step-2); }
.card p { font-size: 0.9375rem; color: var(--color-text-soft); }
.card a { font-weight: 600; font-size: 0.9375rem; margin-top: auto; }
.section--dark .card__icon { color: var(--color-accent-on-dark); }

/* Sub-headings inside a service section are labels, not section titles - at the
   global h3 size they compete with the h2 above them. Scoped to direct children
   of the split's text column so .card/.timeline/.promise/.callout h3s, which
   set their own size, are untouched. */
.split > div > h3,
.service-grid > div > h3 {
  font-size: var(--step-2);
  margin-top: var(--space-md);
  margin-bottom: var(--heading-gap);
}
.split > div > h3 + .checklist,
.service-grid > div > h3 + .checklist { margin-top: 0; }

/* PHOTO RULE - a bronze hairline sits behind each photo, offset down and right.
   Depth without a drop shadow, which the build's design direction rules out.

   The pseudo-element takes its height from the same 4/3 aspect the photos use,
   so it tracks the image box without needing a wrapper element around every
   <img>. Left/right insets are mirrored, so its width equals the photo's width
   and the derived height matches too.

   Excluded deliberately: hero backgrounds (full-bleed, nothing to outline) and
   the brand mark SVGs. */
.split__media,
.gallery figure,
.pair figure { position: relative; }

.split__media > img,
.gallery figure > img,
.pair figure > img { position: relative; z-index: 1; }

.split__media::after,
.gallery figure::after,
.pair figure::after {
  content: "";
  position: absolute;
  z-index: 0;
  top: var(--photo-rule-offset);
  left: var(--photo-rule-offset);
  right: calc(var(--photo-rule-offset) * -1);
  aspect-ratio: 4 / 3;
  border: var(--hairline) solid var(--brand-accent);
  border-radius: var(--radius);
  pointer-events: none;
}

/* Raw bronze is 2.67:1 on the dark ground - below even the 3:1 non-text bar -
   so the rule switches to the lightened tone there. */
.section--dark .split__media::after,
.section--dark .gallery figure::after,
.section--dark .pair figure::after { border-color: var(--color-accent-on-dark); }

/* The rule overhangs its container by the offset. Container padding is 20px at
   the narrowest breakpoint, so shrink the offset there to keep the overhang
   inside the gutter and off the page's horizontal scroll. */
@media (max-width: 640px) { :root { --photo-rule-offset: 8px; } }

/* SERVICE GRID - a two-by-two variant of .split for a service section that has
   more than one supporting block. Row 1 pairs the photo with the intro; row 2
   puts the two supporting blocks side by side beneath them. align-items:start
   so each cell tops out on its own row line rather than centering. */
.service-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 4vw, 3.5rem) clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
.service-grid > div > h3:first-child { margin-top: 0; }
@media (max-width: 860px) { .service-grid { grid-template-columns: 1fr; } }

/* MEDIA STACK - two photos on the same side of a split, one above the other.
   Each keeps its own .split__media wrapper so each gets its own offset rule.
   The gap clears the 12px the rule overhangs below each image. */
.media-stack { display: grid; gap: calc(var(--photo-rule-offset) + 1.5rem); align-content: start; }

.split { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; }
/* .media-stack is a media wrapper just like .split__media, so it has to honour
   the reverse order too. Without it a reversed split that stacks two photos
   silently flips its image back to the left and breaks the page's alternating
   left/right rhythm - which is exactly what happened when #window-gutter on
   exterior-care.php gained a second photo. */
.split--reverse .split__media,
.split--reverse .media-stack { order: 2; }
.split--top { align-items: start; }
.split__media img { width: 100%; border-radius: var(--radius); aspect-ratio: 4 / 3; object-fit: cover; }

/* MEDIA NOTE - a line that belongs to the photo rather than to the section, so
   it sits directly under the image at exactly the image's width instead of
   running the full container.

   Kept a DIRECT CHILD of .split rather than nested inside .split__media:
   nested, it came before the story text once the grid collapsed to one column,
   which is also the order a screen reader reads. As a third grid item it falls
   last in source order and the reading order survives at every width.

   The text column is explicitly spanned across BOTH rows. Without that, row 1
   is the taller of image/text - and the text column overtakes the image as soon
   as the columns narrow, which stranded the note 126px below the photo at a
   900px viewport even though it looked right at 1280px. Spanning pins row 1 to
   the image height, so the note hugs the photo at every two-column width.

   row-gap is zeroed because the note carries its own clearance for the offset
   bronze rule; the column gap applies to rows too and was adding 64px on top.
   Placement is scoped to the two-column breakpoint - applying grid-column: 2 in
   the single-column layout would invent a second column.

   Known and accepted: the clearance is exactly 36px from 1000px up, and grows
   to at most 75px between 861px and 999px, where the story column runs taller
   than the photo and the spanning item still feeds some height back into row 1.
   Tried min-content rows and align-self to stop it; neither does, because a
   spanning item contributes to every intrinsically-sized row it crosses. The
   only layouts that pin it exactly - nesting the note in .split__media, or
   wrapping both in one grid item - put the note BEFORE the story on mobile and
   in the accessibility tree, which is the worse trade. A little extra air on
   small laptops beats reversing the reading order on every phone. */
@media (min-width: 861px) {
  .split--media-note { row-gap: 0; }
  .split--media-note > .split__media { grid-column: 1; grid-row: 1; }
  .split--media-note > .prose        { grid-column: 2; grid-row: 1 / span 2; }
  .split--media-note > .mission-line {
    grid-column: 1;
    grid-row: 2;
    margin-top: calc(var(--photo-rule-offset) + 1.5rem);
  }
}

@media (max-width: 860px) {
  .split { grid-template-columns: 1fr; gap: var(--space-md); }
  .split--reverse .split__media,
  .split--reverse .media-stack { order: 0; }
}

/* SERVICE INDEX - the four-service jump list under the intro on Lighting and
   Exterior Care.

   Replaced the white-chip row on 2026-08-25. Those chips were --color-surface
   on the tan --section--neutral ground with a --brand-muted border, and that
   border measures 1.19:1. It was effectively invisible, so the row had no
   structure and the whole band read as flat. There are no boxes here at all,
   so there is no border left to fail: structure comes from a hairline rule at
   1.70:1 and a real jump in type scale instead. The icons also move from a
   text-relative 18px, too small to identify anything, to a fixed 2.1rem.

   Deliberately NOT numbered 01-04. The four services are parallel choices, not
   a sequence, and .timeline further down the same page IS a sequence. Two
   numbered lists where only one carries meaning is worse than none.

   The rule colour is written as a token rather than --color-border because the
   whole point is that --color-border fails on this ground. */
.service-index {
  --index-rule: rgba(43, 53, 55, 0.30);
  --index-wash: rgba(43, 53, 55, 0.045);
  list-style: none;
  padding: 0;
  /* Capped like .faq is. Left to run the full 1200px container the chevron
     ends up a long way from the label it belongs to. */
  max-width: 54rem;
  margin: var(--space-md) 0 0;
}
.section--dark .service-index {
  --index-rule: rgba(247, 248, 246, 0.26);
  --index-wash: rgba(247, 248, 246, 0.05);
}
.service-index li { border-top: var(--hairline) solid var(--index-rule); }
.service-index li:last-child { border-bottom: var(--hairline) solid var(--index-rule); }
.service-index a {
  display: grid;
  grid-template-columns: 2.75rem 1fr auto;
  align-items: center;
  gap: 1.5rem;
  padding: 1.35rem 0.5rem;
  text-decoration: none;
  color: var(--color-text);
  transition: background-color var(--dur-base) var(--ease);
}
.service-index a:hover { background: var(--index-wash); }
.service-index svg { width: 2.1rem; height: 2.1rem; color: var(--brand-accent); }
.section--dark .service-index svg { color: var(--color-accent-on-dark); }
.service-index__name {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--step-2);
  line-height: 1.15;
  letter-spacing: -0.01em;
  transition: color var(--dur-base) var(--ease);
}
.service-index a:hover .service-index__name { color: var(--color-accent-text); }
.service-index__desc {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.9375rem;
  color: var(--color-text-soft);
}
@media (min-width: 700px) { .service-index__name { font-size: var(--step-3); } }
@media (max-width: 480px) {
  .service-index a { grid-template-columns: 2rem 1fr auto; gap: 1rem; padding: 1.1rem 0.25rem; }
  .service-index svg { width: 1.6rem; height: 1.6rem; }
}

/* CENTRED BAND - the service index sits in a 54rem column inside a 1200px
   container, so left-aligned it left a dead half. Centring the block closes
   that without stretching the rows back out.

   The rows themselves stay left-aligned: the chevron lives on the right edge
   and centring the row contents would strand it. The eyebrow gets a mirrored
   ::before so its signature hairline is symmetrical rather than hanging off
   one side. */
.band-centered { text-align: center; }
.band-centered .eyebrow { justify-content: center; }
.band-centered .eyebrow::before {
  content: "";
  flex: 1;
  max-width: 4rem;
  height: var(--hairline);
  background: currentColor;
}
.band-centered .section-title h2,
.band-centered .lead { margin-inline: auto; }
.band-centered .service-index { margin-inline: auto; text-align: left; }
/* FAQ bands (home, contact, care plans) centre the accordion as a block but
   keep questions left-aligned so they scan (Chance, 2026-09-21). */
.band-centered .faq { margin-inline: auto; text-align: left; }
/* Care plans: header centred, plan cards keep their left-aligned lists (Chance, 2026-09-24). */
.band-centered .plans { text-align: left; }

.gallery { display: grid; gap: var(--space-sm); list-style: none; padding: 0; margin: var(--space-md) 0 0; }
.gallery--3 { grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr)); }
.gallery--4 { grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); }
.gallery figure { margin: 0; }
.gallery img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius);
}
.gallery figcaption {
  /* clears the offset photo rule, which overhangs the image by 12px */
  margin-top: 1.5rem;
  text-align: center;
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  color: var(--color-text-soft);
}

.pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm); margin-top: var(--space-md); }
@media (max-width: 640px) { .pair { grid-template-columns: 1fr; } }
.pair figure { position: relative; margin: 0; }
.pair img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--radius); }
.pair figcaption {
  position: absolute;
  z-index: 2; /* the img above is z-index 1; without this the label paints under the photo */
  top: 0.75rem;
  left: 0.75rem;
  padding: 0.3125rem 0.75rem;
  background: var(--brand-primary);
  color: var(--brand-bg);
  border-radius: var(--radius);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.checklist { list-style: none; padding: 0; margin-top: var(--space-sm); display: grid; gap: 0.625rem; }
.checklist li { display: flex; gap: 0.75rem; align-items: flex-start;
  /* 1rem, not 0.9375 - at 15px the lists read as a different face from the
     16px body copy beside them. */
  font-size: 1rem; }
.checklist svg { flex: none; width: 20px; height: 20px; margin-top: 0.2rem; color: var(--color-accent-text); }
.section--dark .checklist svg { color: var(--color-accent-on-dark); }

.promise { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr)); gap: var(--space-md); list-style: none; padding: 0; margin: var(--space-md) 0 0; }
.promise li { display: grid; grid-template-columns: auto 1fr; gap: var(--space-sm); align-items: start; }
/* Ground-aware: these tokens re-point inside .section--dark, so the component
   is safe on a light section too. Hardcoding the dark-ground values here put
   the About page's copy at 1.59:1. */
.promise__num {
  font-family: var(--font-display);
  font-size: 3rem;
  line-height: 0.85;
  color: var(--color-accent-text);
}
.promise h3 { font-size: var(--step-2); margin-bottom: var(--heading-gap); }
.promise p { font-size: 0.9375rem; color: var(--color-text-soft); }

/* BOXED VARIANT - a fixed 2x2 of bordered cards. Kept as a modifier because the
   plain .promise is also used on About with three items, where forcing two
   columns would leave an orphan on the second row.
   Two columns rather than four gives each card a landscape footprint, which is
   what lets the copy sit on fewer, longer lines and carry a larger type size. */
.promise--boxed { grid-template-columns: 1fr; gap: var(--space-sm); }
@media (min-width: 760px) { .promise--boxed { grid-template-columns: repeat(2, 1fr); gap: var(--space-md); } }

.promise--boxed li {
  grid-template-columns: auto 1fr;
  /* Cards in a row stretch to match the tallest. The numeral stays pinned to the
     top of the card; only the copy beside it centres on the vertical axis, which
     is why this is align-items:start with align-self on the text cell alone. */
  align-items: start;
  gap: 1.25rem;
  padding: clamp(1.5rem, 3vw, 2.25rem);
  border-radius: var(--radius);
  transition: border-color var(--dur-base) var(--ease);

  /* Back to the section's own surface tint - the card no longer paints itself
     light, so it inherits the dark-ground tokens again and needs no overrides. */
  background: var(--color-surface);
  border: var(--hairline) solid var(--color-border);
}

.promise--boxed li > div { align-self: center; }
.promise--boxed li:hover { border-color: var(--color-accent-text); }
.promise--boxed .promise__num { font-size: 3.5rem; }
/* 1.875rem, not --step-3 (36px): at 36px "We show up when we say we will."
   measures 460px against 405px of column, so cards 1 and 3 wrapped. 30px brings
   them to 383px and 363px, comfortably on one line, and still well above the
   24px these were before card 2 is longer and wraps by design. */
.promise--boxed h3 { font-size: 1.875rem; margin-bottom: var(--heading-gap); }
/* Body copy takes the heading's colour, not the muted one, so the two read as
   one block rather than two tones inside a single card. */
.promise--boxed p { font-size: var(--step-0); color: var(--color-text); }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr)); gap: var(--space-md); list-style: none; padding: 0; margin: 0; text-align: center; }
.stats__num { display: block; font-family: var(--font-display); font-size: clamp(2.5rem, 5vw, 3.75rem); line-height: 1; color: var(--color-accent-text); }
.stats__label { display: block; margin-top: 0.375rem; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-text-soft); }
.section--dark .stats__num { color: var(--color-accent-on-dark); }
.section--dark .stats__label { color: var(--brand-muted); }

.timeline { list-style: none; padding: 0; margin: var(--space-md) 0 0; display: grid; gap: var(--space-md); }
.timeline li { position: relative; padding-left: 3.25rem; }
.timeline li::before {
  content: counter(list-item);
  position: absolute;
  left: 0;
  top: 0;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  /* Solid bronze bubble with a white numeral. White on #976A43 is 4.71:1; the
     bubble against the tan section ground is 3.31:1, clearing the non-text bar. */
  background: var(--brand-accent);
  border: var(--hairline) solid var(--brand-accent);
  border-radius: 50%;
  font-family: var(--font-display);
  font-size: 1.125rem;
  color: #FFFFFF;
}
.section--dark .timeline li::before {
  background: var(--color-accent-on-dark);
  border-color: var(--color-accent-on-dark);
  /* Slate numeral on the lightened bronze, 4.60:1. A second copy of this
     selector used to follow and re-set the colour to --color-accent-on-dark,
     i.e. the bubble's own background, so every numeral on a dark section was
     painted invisible - the contact page's "What Happens Next" showed three
     empty circles. */
  color: var(--brand-primary);
}
.timeline h3 { font-size: var(--step-2); margin-bottom: var(--heading-gap); }
.timeline p { font-size: 0.9375rem; color: var(--color-text-soft); }

/* HORIZONTAL TIMELINE - .timeline--row runs the steps left to right, bubble on
   top, with a hairline joining each bubble to the next. Opt-in: the lighting
   (4 steps) and Elevate (5 steps) process sections use it (Chance,
   2026-09-21). Auto columns, so the row fits any step count. Two columns on
   tablets, the plain vertical list on phones. */
@media (min-width: 640px) {
  .timeline--row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-md); }
  .timeline--row li { padding-left: 0; padding-top: 3.25rem; }
}
@media (min-width: 1040px) {
  .timeline--row { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
  .timeline--row li:not(:last-child)::after {
    content: "";
    position: absolute;
    top: 1.125rem;
    left: 3rem;
    right: calc(0.75rem - var(--space-md));
    height: var(--hairline);
    background: var(--brand-accent);
  }
}

.callout {
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-md);
  margin-top: var(--space-md);
  background: var(--brand-primary);
  color: var(--brand-bg);
  border-left: 3px solid var(--color-accent-on-dark);
  border-radius: var(--radius);
}
.callout h3 { color: var(--brand-bg); font-size: var(--step-2); }
.callout p { color: var(--brand-neutral); font-size: 0.9375rem; }
.callout .btn { justify-self: start; }

.audience { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: 0.5rem var(--space-md); list-style: none; padding: 0; margin: 0; }
.audience li { display: flex; gap: 0.625rem; align-items: center; padding-block: 0.5rem; border-bottom: var(--hairline) solid var(--color-border); font-size: 0.9375rem; }
.audience li::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--brand-accent); }
.section--dark .audience li::before { background: var(--color-accent-on-dark); }

.faq { max-width: 52rem; margin-top: var(--space-md); }
.faq details { border-bottom: var(--hairline) solid var(--color-border); }
.faq summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-sm);
  padding: 1.25rem 0;
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+";
  flex: none;
  font-family: var(--font-display);
  font-size: 1.75rem;
  line-height: 1;
  color: var(--color-accent-text);
  transition: transform var(--dur-base) var(--ease);
}
.faq details[open] summary::after { transform: rotate(45deg); }
.faq details p { padding-bottom: 1.25rem; color: var(--color-text-soft); max-width: var(--measure); }

.cta-band { background: var(--brand-primary); color: var(--brand-bg); text-align: center; }
.cta-band h2 { color: var(--brand-bg); margin-inline: auto; }
.cta-band p { color: var(--brand-bg); max-width: 52ch; margin: var(--space-sm) auto var(--space-md); }
.cta-band__actions { display: flex; flex-wrap: wrap; gap: 0.875rem; justify-content: center; }

.quote-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem 1.5rem;
  padding: 0.875rem clamp(1.25rem, 4vw, 2.5rem);
  background: var(--brand-accent);
  color: #FFFFFF;
  font-size: 0.9375rem;
  font-weight: 500;
  text-align: center;
}
.quote-bar a { color: #FFFFFF; font-weight: 700; text-underline-offset: 0.25em; }
.quote-bar a:hover { color: #2B3537; }

.form { display: grid; gap: var(--space-sm); }
.form-row { display: grid; gap: 0.375rem; }
.form-row label { font-size: 0.875rem; font-weight: 600; }
.form-row input, .form-row select, .form-row textarea {
  width: 100%;
  padding: 0.8125rem 0.9375rem;
  background: var(--color-surface);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  color: var(--color-text);
  font-size: 1rem;
  transition: border-color var(--dur-base) var(--ease);
}
.form-row input:hover, .form-row select:hover, .form-row textarea:hover { border-color: var(--brand-gray); }
.form-row textarea { min-height: 9rem; resize: vertical; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm); }
@media (max-width: 640px) { .form-grid { grid-template-columns: 1fr; } }
.form-help { font-size: 0.8125rem; color: var(--color-text-soft); }
/* The privacy reassurance under the submit button. Italic so it reads as an
   aside rather than as another instruction; scoped with a modifier so the
   reCAPTCHA legal line beside it stays upright. */
.form-help--privacy { font-style: italic; }
.form-status { padding: 0.875rem 1rem; border-radius: var(--radius); font-size: 0.9375rem; }
.form-status:not(:empty) { border: var(--hairline) solid var(--color-border); background: var(--color-surface); }
.field-error { color: #A32017; font-weight: 500; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Nothing styled fieldset/legend, so this one control was wearing the browser
   default border and legend notch - the only place that appears on the site, and
   it boxed the beige panel inside a second box. Stripped back so the legend
   behaves like every other field label, sitting above its control. */
.form fieldset { margin: 0; padding: 0; border: 0; }
.form legend { padding: 0; margin-bottom: 0.375rem; font-size: 0.875rem; font-weight: 600; }

/* The service picker sits on the beige panel tone so it reads as one field
   rather than six loose checkboxes. Body text on --brand-neutral is 12.1:1. */
.checkgroup {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: 0.5rem;
  padding: var(--space-sm);
  background: var(--brand-neutral);
  border-radius: var(--radius);
}
.checkgroup label { display: flex; gap: 0.5rem; align-items: center; font-size: 0.9375rem; font-weight: 400; }
.checkgroup input { width: auto; min-width: 20px; min-height: 20px; accent-color: var(--brand-accent); }

.contact-layout { display: grid; grid-template-columns: 1.35fr 1fr; gap: clamp(2rem, 5vw, 4rem); align-items: start; margin-top: var(--space-md); }
/* Clear the offset bronze rule before the details panel starts. The rule
   overhangs the photo bottom by --photo-rule-offset, so the old 1rem left only
   4px of daylight between the two and they read as one stuck-together block. */
.contact-layout .split__media { margin-bottom: calc(var(--photo-rule-offset) + 1.5rem); }
@media (max-width: 900px) { .contact-layout { grid-template-columns: 1fr; } }
.contact-detail { display: grid; gap: var(--space-sm); padding: var(--space-md); background: var(--brand-neutral); border-radius: var(--radius); }
.contact-detail dt { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--brand-gray); }
.contact-detail dd { margin: 0.25rem 0 0; font-size: 1.0625rem; }
.contact-detail a { font-weight: 600; }

.breadcrumbs { padding-block: var(--space-sm); font-size: 0.8125rem; color: var(--color-text-soft); }
.breadcrumbs ol { display: flex; flex-wrap: wrap; gap: 0.5rem; list-style: none; padding: 0; margin: 0; }
.breadcrumbs li:not(:last-child)::after { content: "/"; margin-left: 0.5rem; color: var(--brand-muted); }

.site-footer { background: var(--brand-primary); color: var(--brand-bg); padding-top: var(--space-lg); font-size: 0.9375rem; }
.site-footer__grid {
  display: grid;
  grid-template-columns: 1.6fr repeat(3, 1fr);
  gap: var(--space-md);
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: 0 clamp(1.25rem, 4vw, 2.5rem) var(--space-lg);
}
@media (max-width: 900px) { .site-footer__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .site-footer__grid { grid-template-columns: 1fr; } }
.site-footer .brand { margin-right: 0; margin-bottom: var(--space-sm); }
.site-footer__tagline { max-width: 34ch; margin-bottom: var(--space-sm); color: var(--brand-bg); font-size: 0.875rem; }
.site-footer__address { font-style: normal; line-height: 1.9; }
.site-footer__address a { color: var(--brand-bg); font-weight: 600; }
.site-footer h2 { font-family: var(--font-body); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-accent-on-dark); margin-bottom: 0.875rem; }
.site-footer ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.5rem; }
.site-footer a { color: var(--brand-bg); text-decoration: none; }
.site-footer a:hover { color: var(--brand-bg); text-decoration: underline; }
.site-footer__social { display: flex; gap: 0.75rem; margin-top: var(--space-sm); }
.site-footer__social a { display: grid; place-items: center; width: 40px; height: 40px; border: var(--hairline) solid rgba(247, 248, 246, 0.24); border-radius: var(--radius); }
.site-footer__social svg { width: 18px; height: 18px; }
.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem var(--space-sm);
  justify-content: space-between;
  align-items: center;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-sm) clamp(1.25rem, 4vw, 2.5rem);
  border-top: var(--hairline) solid rgba(247, 248, 246, 0.16);
  font-size: 0.8125rem;
  color: var(--brand-bg);
}
.site-footer__credit a { color: var(--brand-bg); text-decoration: underline; }
.spark-mark { display: inline-block; width: 1.6em; height: 1.6em; margin-left: 0.15em; vertical-align: -0.5em; }

.consent-banner[hidden] { display: none; }
.consent-banner {
  position: fixed;
  left: 50%;
  bottom: 1rem;
  z-index: 200;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  width: min(60rem, calc(100vw - 2rem));
  padding: 1rem 1.25rem;
  background: var(--brand-primary);
  color: var(--brand-neutral);
  border: var(--hairline) solid rgba(247, 248, 246, 0.24);
  border-radius: var(--radius);
  font-size: 0.875rem;
  opacity: 0;
  transform: translate(-50%, calc(100% + 1.5rem));
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease-out);
}
.consent-banner.is-visible { opacity: 1; transform: translate(-50%, 0); }
.consent-banner__text { flex: 1 1 22rem; margin: 0; }
.consent-banner a { color: var(--color-accent-on-dark); }
.consent-banner__actions { flex: none; }
.consent-banner .btn { min-height: 42px; padding: 0.5rem 1.5rem; }

.js .reveal { opacity: 0; transform: translateY(24px); }
.js .reveal.is-in { opacity: 1; transform: none; transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out); }
.reveal-delay-1.is-in { transition-delay: 90ms; }
.reveal-delay-2.is-in { transition-delay: 180ms; }
.reveal-delay-3.is-in { transition-delay: 270ms; }

.hero__media img { transform: scale(1.04); transition: opacity var(--dur-slow) var(--ease-out), transform 1.6s var(--ease-out); }
.hero__dim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: #10181A;
  opacity: 0.55;
  transition: opacity 1.2s var(--ease-out);
}
.hero.is-lit .hero__media img { transform: scale(1); }
.hero.is-lit .hero__dim { opacity: 0; }
.hero .eyebrow::after { transform: scaleX(0); transition: transform var(--dur-slow) var(--ease-out) 300ms; }
.hero.is-lit .eyebrow::after { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  html, html.is-loaded { scroll-behavior: auto; }
  .js .reveal { opacity: 1; transform: none; }
  .hero__dim { opacity: 0; }
  .hero__media img { transform: scale(1); }
  .hero .eyebrow::after { transform: scaleX(1); }
}

@media print {
  .site-header, .nav-mobile, .consent-banner, .quote-bar, .cta-band { display: none; }
  body { background: #FFFFFF; color: #000000; }
}

/* ============================================================================
   DESIGN PASS - 2026-08-20
   Each block answers a measured defect rather than a preference. The numbers
   quoted are from the review; re-measure before changing any of them.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   1. SPLIT FILL - the service rows were stranding 225-283px of empty column
   above AND below every photo. Measured on all seven service blocks across
   Lighting and Exterior Care: a fixed 393px image (4/3 at half a 1200px
   container) centred against a text column running 843-958px.

   .split--top was already available and would only have pushed all the
   emptiness to the bottom instead of splitting it in two. This stretches the
   photo to the height of the text beside it, which is the layout the
   alternating image-side rhythm was drawn for in the first place.

   The bronze photo rule derives its height from `aspect-ratio: 4/3`, so it has
   to be re-anchored to `bottom` here or it keeps tracking a 4/3 box the image
   no longer is. min-height stops a short text column from crushing the photo.
   --------------------------------------------------------------------------- */
@media (min-width: 861px) {
  .split--fill { align-items: stretch; }

  .split--fill > .split__media { display: flex; }
  .split--fill > .split__media > img {
    height: 100%;
    min-height: 22rem;
    aspect-ratio: auto;
    object-fit: cover;
  }
  .split--fill > .split__media::after {
    aspect-ratio: auto;
    bottom: calc(var(--photo-rule-offset) * -1);
  }

  /* Two-photo variant. The stack becomes two equal rows so the pair fills the
     same column height; align-content:start on .media-stack would otherwise
     hold both photos at their natural size at the top of the column. */
  .split--fill > .media-stack {
    grid-template-rows: 1fr 1fr;
    align-content: stretch;
  }
  .split--fill > .media-stack > .split__media { display: flex; }
  .split--fill > .media-stack > .split__media > img {
    height: 100%;
    min-height: 0;
    aspect-ratio: auto;
    object-fit: cover;
  }
  .split--fill > .media-stack > .split__media::after {
    aspect-ratio: auto;
    bottom: calc(var(--photo-rule-offset) * -1);
  }
}

/* ---------------------------------------------------------------------------
   2. DARK-GROUND DEPTH - this build's own direction is "dark grounds make lit
   photography read as light" and "bronze earns its place because it reads as
   warm light". The dark sections were shipping as flat #2B3537 with no light in
   them at all, which is most of why the page reads flat across 8000px.

   The obvious move - a warm bronze glow added over the slate - is not available
   on this palette, and the reason is worth writing down. Bronze-on-dark
   #BE926C sits at exactly 4.51:1 on #2B3537: it clears the 4.5 bar for text
   with no headroom whatsoever. ANY lightening of that ground pushes every
   bronze eyebrow, fit-note and dark-section link under it. Measured: a 0.09
   warm glow took it to 3.95:1, a real WCAG failure.

   So the light is subtractive instead. The section keeps the stock ground at
   the top right - conceptually where the light is - and a neutral shade deepens
   it toward the far corners. Every ratio on the section now IMPROVES rather
   than degrades: at the darkest point bronze goes 4.51 -> 4.97, off-white
   11.82 -> 13.04, tan 8.85 -> 9.76. The brightest point is the unchanged base,
   so nothing can regress. Depth for free, which is the correct trade.

   Sage was the obvious candidate for a fourth ground and does not work: it is
   1.19:1 against tan and 1.59:1 against the off-white page ground, so a sage
   band beside either reads as a rendering fault rather than a change of
   surface. Depth had to come from inside the dark family instead.
   --------------------------------------------------------------------------- */
.section--dark,
.cta-band {
  background-color: var(--brand-primary);
  background-image: radial-gradient(120% 90% at 78% 6%, rgba(20, 26, 27, 0) 0%, rgba(20, 26, 27, 0.26) 72%);
  background-repeat: no-repeat;
}

/* ---------------------------------------------------------------------------
   3. THE CTA BAND AND THE FOOTER WERE ONE BLOCK - both painted --brand-primary
   with only a hairline between them, so every page ended in an unbroken run of
   slate. Same on the homepage, where the Elevate intro and How It Works stacked
   to 1394px of continuous dark.

   The CTA band drops a step to #232B2D. That is 1.15:1 against the section
   above it, deliberately below the threshold where it reads as a different
   colour - it reads as the band receding, which is the point. Off-white text on
   it is 13.55:1 and the lightened bronze 5.17:1, so both clear comfortably.
   --------------------------------------------------------------------------- */
:root { --brand-primary-deep: #232B2D; }
.cta-band { background-color: var(--brand-primary-deep); }

/* Two dark sections in a row: the second takes the deep step, so the seam
   between them is a change of surface rather than a hairline on flat colour. */
.section--dark + .section--dark { background-color: var(--brand-primary-deep); }

/* ---------------------------------------------------------------------------
   4. THE GABLE MOTIF - design-direction.md names "the gable angle from the
   Secondary Logo as a recurring motif" as this build's shape language, and it
   was never actually built: there was no clip-path, polygon or gable anywhere
   in the stylesheet. It is the one accent device the brief had already asked
   for and nobody had spent.

   Used once per page, on the closing CTA band, as an oversized outline mark
   bleeding off the right edge. Decorative only - it carries nothing a screen
   reader needs, and as a background image there is nothing in the tree.
   --------------------------------------------------------------------------- */
.cta-band {
  position: relative;
  overflow: hidden;
}
.cta-band::before {
  content: "";
  position: absolute;
  top: 50%;
  right: 2.5rem;
  width: 17rem;
  height: 17rem;
  transform: translateY(-50%);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 210' fill='none' stroke='%23BE926C' stroke-opacity='0.16' stroke-width='2'%3E%3Cpath d='M10 92 100 12l90 80'/%3E%3Cpath d='M10 92v106M190 92v106'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: contain;
  background-position: center;
  pointer-events: none;
}
/* First placement bled it off the right edge at 26rem. Only about 60% of the
   mark stayed on screen and the visible part read as a stray diagonal rather
   than as the gable, so it is now sized to sit whole inside the band. It would
   still collide with the centred copy once the band narrows, so it leaves. */
@media (max-width: 1100px) { .cta-band::before { display: none; } }

/* ---------------------------------------------------------------------------
   5. PROCESS-STEP MEASURE - the step descriptions in .timeline carried no
   measure and ran the full 1215px container, about 150 characters a line.
   Everything else on the site is capped at --measure; these were missed.
   --------------------------------------------------------------------------- */
.timeline li > p { max-width: var(--measure); }

/* ---------------------------------------------------------------------------
   6. AFFORDANCES - the service-index rows are real anchors to sections thousands of
   pixels down the page, and the two nav parents really do open submenus, but
   neither said so. A chevron on each.
   --------------------------------------------------------------------------- */
.service-index a::after {
  content: "";
  width: 0.55rem;
  height: 0.55rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  color: var(--color-accent-text);
  transform: rotate(-45deg);
  transition: transform var(--dur-base) var(--ease);
}
.service-index a:hover::after { transform: rotate(-45deg) translate(3px, -3px); }

.nav-desktop .has-dropdown > a::after {
  content: "";
  display: inline-block;
  width: 0.36rem;
  height: 0.36rem;
  margin-left: 0.45rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-0.12em);
  opacity: 0.75;
}

/* ---------------------------------------------------------------------------
   7. HEADER PHONE - the most valuable click on a local service site was not in
   the header on either breakpoint. It sits left of the quote button on desktop
   and becomes a tap target beside the menu button on mobile.
   --------------------------------------------------------------------------- */
.site-header__tel {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin-left: 0.5rem;
  padding: 0.5rem 0.25rem;
  color: #F7F8F6;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-base) var(--ease);
}
.site-header__tel svg { flex: none; width: 17px; height: 17px; color: var(--color-accent-on-dark); }
.site-header__tel:hover { color: var(--color-accent-on-dark); }

/* The compact version rides in the bar beside the hamburger, so it is an icon
   in a matching box - there is no room for the number at that width. */
.site-header__tel--compact {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  min-height: 44px;
  margin: 0;
  padding: 0;
  border: var(--hairline) solid rgba(247, 248, 246, 0.3);
  border-radius: var(--radius);
}
.site-header__tel--compact svg { width: 20px; height: 20px; }
/* The labelled number needs about 150px of bar. Between the nav collapse point
   (1240px) and 1340px there is not enough room for it alongside the full-width
   logo, six nav labels and the quote button. Hand over to the icon there. */
/* The labelled link is emitted as an <li><a> INSIDE .nav-desktop, so
   `.nav-desktop a { display: block }` (0,1,1) outranks `.site-header__tel`
   (0,1,0) - and a media query adds no specificity, so the hide below never
   fired. Both phone links rendered together from 1001px to 1240px, and the
   `display: block` also threw away the inline-flex gap, butting the glyph
   against the "(". Match the nav's specificity in both directions. */
.nav-desktop a.site-header__tel { display: inline-flex; }

@media (max-width: 1340px) {
  .nav-desktop a.site-header__tel { display: none; }
  .site-header__tel--compact { display: inline-flex; }
}
/* Below the collapse point the icon lives beside the hamburger instead. */
@media (max-width: 1240px) {
  .site-header__tel--compact { display: inline-flex; }
}

/* Nothing in the bar may wrap. The caret pseudo-element on the two dropdown
   parents is an inline-block, so without this it was treated as a wrap
   opportunity and dropped under its own label. */
.nav-desktop a,
.nav-desktop a.cta { white-space: nowrap; }
.nav-desktop > ul > li > a { margin-inline: 0.7rem; }
.nav-desktop a.cta { margin-left: 1.1rem; padding-inline: 1.15rem; }

/* The drawer gets the number spelled out, above the quote CTA. */
.nav-mobile__tel {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: var(--space-sm);
  padding: 0.9375rem 1rem;
  border: 1px solid rgba(247, 248, 246, 0.3);
  border-radius: 2px;
  color: #F7F8F6;
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 600;
  text-decoration: none;
}
.nav-mobile__tel svg { flex: none; width: 18px; height: 18px; color: var(--color-accent-on-dark); }

/* ---------------------------------------------------------------------------
   8. WHOLE-CARD CLICK TARGET - only the "Learn more" line was clickable, a 90px
   target inside a 300px card. The stretched pseudo-element keeps exactly one
   link in the accessibility tree; the card itself stays a plain container.
   --------------------------------------------------------------------------- */
.card { position: relative; }
.card a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
}
.card:hover a { color: var(--color-text); }
.section--dark .card:hover a { color: var(--brand-bg); }

/* Not every .card is a link. The credentials trio on About and the six feature
   cards on Elevate carry no anchor at all, so they get no overlay - but they
   were still inheriting the lift-and-glow hover, which now promises a click
   that only the linked cards deliver. Stand them down. Browsers without :has()
   keep the old behaviour, which is the pre-existing state, not a break. */
.card:not(:has(a)):hover {
  transform: none;
  border-color: var(--color-border);
}

/* ---------------------------------------------------------------------------
   9. CONTACT TITLE BAND - Contact was the only page with no dark opening, so
   arriving from any other page it read as a different site. A full hero would
   fight design-direction.md, which specifies Contact as "form-first ... form is
   above the fold", so this is a slim band that keeps the form high.
   --------------------------------------------------------------------------- */
.page-band {
  background-color: var(--brand-primary);
  background-image: radial-gradient(120% 120% at 78% 8%, rgba(20, 26, 27, 0) 0%, rgba(20, 26, 27, 0.26) 72%);
  background-repeat: no-repeat;
  color: var(--brand-bg);
  --color-text: var(--brand-bg);
  --color-text-soft: var(--brand-muted);
  --color-accent-text: var(--color-accent-on-dark);
  --color-border: rgba(247, 248, 246, 0.22);
  padding-block: clamp(2.25rem, 5vw, 3.25rem);
}
.page-band h1 { color: var(--brand-bg); font-size: var(--step-4); margin-bottom: 0; }
.page-band .eyebrow { color: var(--color-accent-on-dark); }

/* ---------------------------------------------------------------------------
   10. STACKED BUTTON PAIRS - once the hero and CTA action rows wrap to one
   button per line, each button keeps its own label width, so "Get a Free Quote"
   and "Explore Our Lighting" sat at 210px and 240px directly above one another.
   Two near-but-not-quite-equal blocks read as a mistake rather than a pair.
   Stretch them once they are stacked.
   --------------------------------------------------------------------------- */
@media (max-width: 520px) {
  .hero__actions,
  .cta-band__actions {
    flex-direction: column;
    align-items: stretch;
  }
  .hero__actions .btn,
  .cta-band__actions .btn { width: 100%; }
}

/* ---------------------------------------------------------------------------
   11. VIDEO EMBED - a click-to-play facade for the Elevate walkthrough. It is
   deliberately NOT .split__media / .gallery figure / .pair figure: all three of
   those hard-lock their <img> AND their bronze ::after rule to aspect-ratio
   4/3, so a 16:9 frame inside one gets an outline that no longer traces it.
   This follows the .split--fill precedent instead - own aspect-ratio, and the
   rule re-anchored to bottom so it derives its height from the box rather than
   from a ratio of its own.

   Nothing loads from YouTube until the button is pressed. The poster is served
   from our own /images/Photos/, so img-src needs no third-party origin and the
   player's ~1MB of script never touches first paint.
   --------------------------------------------------------------------------- */
.video-embed {
  position: relative;
  max-width: 60rem;
  margin: var(--space-md) auto 0;
}

.video-embed__frame {
  position: relative;
  z-index: 1;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--brand-primary);
}

.video-embed__frame > img,
.video-embed__frame > iframe {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
  display: block;
}

/* Same offset bronze rule the photos get, but its height comes from the box. */
.video-embed::after {
  content: "";
  position: absolute;
  z-index: 0;
  top: var(--photo-rule-offset);
  left: var(--photo-rule-offset);
  right: calc(var(--photo-rule-offset) * -1);
  bottom: calc(var(--photo-rule-offset) * -1);
  border: var(--hairline) solid var(--brand-accent);
  border-radius: var(--radius);
  pointer-events: none;
}
.section--dark .video-embed::after { border-color: var(--color-accent-on-dark); }

/* The whole poster is the control, so the hit area is the frame itself. With
   JS off this is an <a> to YouTube; with JS on it is swapped for a <button>
   that replaces itself with the iframe. Both keep the same look. */
.video-embed__play {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  padding: 0;
  cursor: pointer;
  background: linear-gradient(180deg, rgba(30, 33, 34, 0.10), rgba(30, 33, 34, 0.55));
  border-radius: var(--radius);
  color: var(--brand-bg);
  text-decoration: none;
}

.video-embed__play::before {
  content: "";
  width: 4.5rem;
  height: 4.5rem;
  border-radius: 50%;
  background: var(--brand-accent);
  box-shadow: 0 0.5rem 1.5rem rgba(30, 33, 34, 0.45);
  transition: transform 180ms ease, background-color 180ms ease;
}

.video-embed__glyph {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-42%, -50%);
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 0.85rem 0 0.85rem 1.4rem;
  border-color: transparent transparent transparent var(--brand-bg);
  pointer-events: none;
}

.video-embed__play:hover::before,
.video-embed__play:focus-visible::before { transform: scale(1.06); }

@media (prefers-reduced-motion: reduce) {
  .video-embed__play::before,
  .video-embed__play:hover::before { transition: none; transform: none; }
}

/* The label sits under the button for sighted users and names the link for
   screen readers, so the control is never just an unlabelled circle. */
.video-embed__label {
  position: absolute;
  left: 50%;
  bottom: 1.25rem;
  transform: translateX(-50%);
  margin: 0;
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius);
  background: rgba(30, 33, 34, 0.72);
  color: var(--brand-bg);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

@media (max-width: 480px) {
  .video-embed__play::before { width: 3.5rem; height: 3.5rem; }
  .video-embed__glyph { border-width: 0.65rem 0 0.65rem 1.1rem; }
  .video-embed__label { font-size: 0.6875rem; bottom: 0.85rem; }
}

/* ---------------------------------------------------------------------------
   12. SPACING UTILITIES - the only two steps any page needed once the twelve
   ad-hoc inline margins were removed. Both come straight off the --space-*
   scale, so nothing on the site sets a spacing value that is not on it.

   .stack-md  a button or a closing line under the block above it
   .stack-lg  a second full block stacked inside one section, where the two are
              peers rather than intro-then-content, so the join needs to read
              as a break

   .section--joined is structural rather than spacing: it drops the top padding
   on the second of two consecutive dark sections so the pair reads as one
   band. Was an inline padding-top: 0 on the homepage.
   --------------------------------------------------------------------------- */
.stack-md { margin-top: var(--space-md); }
.stack-lg { margin-top: var(--space-lg); }
.section--joined { padding-top: 0; }


/* ---------------------------------------------------------------------------
   PROPERTY CARE PLANS  (/property-care-plans)

   Three recurring-service tiers. Deliberately NOT built on .card: that
   component makes the whole card one click target via `.card a::after
   { inset: 0 }`, which cannot work here because a plan carries a CTA button
   AND the tier is not itself a link. The visual language (surface, hairline,
   radius, 3px hover lift) is borrowed so the two read as siblings.

   .plan--featured paints itself --brand-primary, so it is registered in the
   dark-ground token block near the top of this file. Without that entry its
   price and check icons resolve --color-accent-text to the light-ground bronze
   (#795535), which measures 2.16:1 on the dark card. With it they resolve to
   --color-accent-on-dark (#C59D7B) at 5.09:1.

   Card tops line up because the name, price and price note are one line each
   by construction. Keep them that way - a two-line tier name in one column
   pushes that card's checklist out of step with the other two.
   --------------------------------------------------------------------------- */
.plans {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-sm);
  list-style: none;
  padding: 0;
  margin: var(--space-md) 0 0;
  align-items: stretch;
}
@media (min-width: 720px)  { .plans { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .plans { grid-template-columns: repeat(3, 1fr); } }

.plan {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--space-md) var(--space-sm);
  background: var(--color-surface);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  transition: transform var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
}
.plan:hover { transform: translateY(-3px); border-color: var(--brand-accent); }

.plan--featured {
  background: var(--brand-primary);
  border-color: var(--brand-accent);
  /* The explicit color is NOT redundant with the dark-ground token block. That
     block re-points --color-text, but h3 and .checklist li take their colour by
     INHERITANCE from body, and an already-inherited computed colour does not
     change when you re-point the variable behind it. Without this line the tier
     name and every checklist row render slate-on-slate and vanish. .section--dark
     sets both for the same reason. */
  color: var(--brand-bg);
}
/* On screens wide enough to show all three abreast, lift the featured tier so
   the row reads as a recommendation rather than three equal options. Below
   that the cards stack and the offset would just look like a layout bug. */
@media (min-width: 1040px) {
  .plan--featured { transform: translateY(-0.75rem); }
  .plan--featured:hover { transform: translateY(-1.05rem); }
}

.plan__badge {
  position: absolute;
  top: 0;
  left: var(--space-sm);
  transform: translateY(-50%);
  /* NOT raw --brand-accent. Off-white on #976A43 measures 4.42:1, which fails
     the 4.5 bar for text this size, and raw bronze is fills-and-strokes only on
     this site anyway. The lightened bronze with slate text gives 5.09:1 for the
     label and 5.09:1 for the badge shape against the card it sits on, matching
     how .plan--featured .btn-primary is treated. */
  background: var(--color-accent-on-dark);
  color: var(--brand-primary);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius);
}

.plan h3 { font-size: var(--step-2); margin-bottom: 0.25rem; }
.plan__tagline { font-size: 0.9375rem; color: var(--color-text-soft); margin-bottom: var(--space-sm); }
/* Reserve two lines for the tagline once the three sit abreast. Signature's is
   one line and the other two wrap, which pushed its price 25px above theirs and
   made the row look broken. Only applied at the 3-across breakpoint: below it
   the cards stack and reserved empty space would just be a gap.
   2 lines x 1.65 body line-height x 0.9375rem. */
@media (min-width: 1040px) {
  .plan__tagline { min-height: calc(2 * 1.65 * 0.9375rem); }
}

.plan__price {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(2.25rem, 4vw, 3rem);
  line-height: 1;
  color: var(--color-accent-text);
}
.plan__price-from {
  display: block;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-soft);
  margin-bottom: 0.35rem;
}
.plan__price .plan__per {
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 500;
  bottom: 0;
  color: var(--color-text-soft);
}
.plan__price-note {
  font-size: 0.8125rem;
  color: var(--color-text-soft);
  margin-top: 0.5rem;
  padding-bottom: var(--space-sm);
  border-bottom: var(--hairline) solid var(--color-border);
}

.plan__what { font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--color-text-soft); margin-top: var(--space-sm); }
.plan .checklist { margin-top: 0.625rem; }
.plan .checklist li { font-size: 0.9375rem; }

/* The holiday credit is the one thing a pressure-washing competitor cannot
   match, so it gets its own panel rather than a fourth checklist row. */
.plan__perk {
  /* auto, not a fixed gap: this is what collects the leftover height in a short
     tier, so the perk and the CTA stay together at the foot of every card
     instead of leaving a dead band under Essential. The rule below guarantees a
     real gap in the TALLEST card, where auto resolves to zero. */
  margin-top: auto;
  padding: 0.75rem 0.875rem;
  background: var(--color-surface);
  border-left: 3px solid var(--brand-accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: 0.875rem;
}
.plan:not(.plan--featured) .plan__perk { background: var(--brand-neutral); }
/* Darkened, NOT lightened. An 8% white lift here raised the ground to #3B4546
   and dropped the bronze <strong> to 3.99:1, the same translucent-surface trap
   the accent token is warned about elsewhere in this file. Going the other way
   deepens the well to #232C2D, which lifts bronze to 5.6:1 and the off-white
   body text with it, and it reads as an inset panel rather than a smudge. */
.plan--featured .plan__perk { background: rgba(20, 26, 27, 0.35); }
.plan__perk strong { color: var(--color-accent-text); }

/* margin-top:auto on the WRAPPER pins every CTA to the bottom edge no matter
   how many services the tier lists, so the three buttons sit on one line.
   Putting it on .btn instead does nothing: the wrapper is the flex item. */
/* The last checklist in a card needs its own bottom gap: in the tallest tier
   there is no free space left for .plan__perk margin-top:auto to resolve into,
   so without this the perk panel butts straight against the final list item. */
.plan .checklist:last-of-type { margin-bottom: var(--space-sm); }
.plan__cta-wrap { padding-top: var(--space-sm); }
.plan__cta-wrap .btn { width: 100%; justify-content: center; text-align: center; }

/* ---------------------------------------------------------------------------
   PLAN COMPARISON TABLE

   The three cards sell; this answers "what actually differs". It scrolls
   inside its own wrapper rather than shrinking the type, because the row
   labels stop being readable below about 34rem.
   --------------------------------------------------------------------------- */
.plan-table-wrap { margin-top: var(--space-md); overflow-x: auto; -webkit-overflow-scrolling: touch; }
.plan-table {
  width: 100%;
  min-width: 34rem;
  border-collapse: collapse;
  font-size: 0.9375rem;
}
.plan-table caption { text-align: left; color: var(--color-text-soft); font-size: 0.875rem; padding-bottom: 0.75rem; }
.plan-table th, .plan-table td { padding: 0.75rem 0.875rem; text-align: left; border-bottom: var(--hairline) solid var(--color-border); }
.plan-table thead th {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-soft);
  border-bottom-width: 2px;
}
.plan-table tbody th { font-weight: 500; }
.plan-table td { color: var(--color-text-soft); }
.plan-table .is-featured { background: var(--brand-neutral); color: var(--color-text); }
.plan-table thead .is-featured { color: var(--color-text); }
.plan-table .plan-table__none { color: var(--brand-gray); }

/* The Premier roof line. This exists because the tier includes an INSPECTION,
   not a cleaning, and that distinction has real money behind it. Do not let a
   redesign quietly drop it. */
.plan__fineprint {
  margin-top: 0.625rem;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--color-text-soft);
}


/* ---------------------------------------------------------------------------
   TEAM  (/about)

   The only portrait image component on the site. Every other media container -
   .split__media, .gallery figure, .pair figure - hard-locks aspect-ratio 4/3,
   and .card carries no image styling at all, so a headshot dropped into any of
   them gets cropped to landscape and loses the top of the head. This owns its
   own 4/5 ratio, which is the same move .video-embed makes for 16/9 further
   down this file, including re-anchoring the bronze rule to suit the shape.

   The rule is anchored to the image box rather than the whole card, or it would
   trace the name and role text underneath as well.
   --------------------------------------------------------------------------- */
.team {
  display: grid;
  grid-template-columns: 1fr;
  gap: calc(var(--photo-rule-offset) + 1.5rem) var(--space-sm);
  list-style: none;
  padding: 0;
  margin: var(--space-md) 0 0;
}
@media (min-width: 640px)  { .team { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .team { grid-template-columns: repeat(3, 1fr); } }

.team__card { display: flex; flex-direction: column; }

.team__media { position: relative; margin: 0 0 calc(var(--photo-rule-offset) + 1rem); }
.team__media > img {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--radius);
}
.team__media::after {
  content: "";
  position: absolute;
  z-index: 0;
  top: var(--photo-rule-offset);
  left: var(--photo-rule-offset);
  right: calc(var(--photo-rule-offset) * -1);
  aspect-ratio: 4 / 5;
  border: var(--hairline) solid var(--brand-accent);
  border-radius: var(--radius);
  pointer-events: none;
}
.section--dark .team__media::after { border-color: var(--color-accent-on-dark); }

.team__card h3 { font-size: var(--step-2); margin-bottom: 0.15rem; }
.team__role {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent-text);
  margin-bottom: var(--space-xs);
}
.team__card p:last-child { font-size: 0.9375rem; color: var(--color-text-soft); margin-bottom: 0; }

.leadli-form iframe { display: block; min-height: 1412px; }

.member-logos { list-style: none; padding: 0; margin: var(--space-md) 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm) var(--space-md); }
.member-logos a { display: block; }
.member-logos img { display: block; height: 96px; width: auto; }
.site-footer__members-label { margin-top: var(--space-md); font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-accent-on-dark); }
.site-footer .member-logos--footer { display: flex; margin-top: var(--space-xs); gap: 0.75rem; }
.site-footer .member-logos--footer a { display: block; background: #FFFFFF; border-radius: var(--radius); padding: 0.5rem 0.75rem; }
.member-logos--footer img { height: 56px; }
.reviews { list-style: none; padding: 0; margin: var(--space-md) 0 0; display: grid; gap: var(--space-sm); grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
.review-card { display: flex; flex-direction: column; background: var(--color-surface); border: var(--hairline) solid var(--color-border); border-radius: var(--radius); padding: var(--space-md) var(--space-sm); }
.review-card__top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); margin-bottom: var(--space-sm); }
.review-card__stars { display: inline-flex; gap: 2px; color: var(--brand-accent); }
.review-card__stars svg { width: 18px; height: 18px; }
.review-card__src { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-soft); }
.review-card blockquote { margin: 0 0 var(--space-sm); flex: 1; }
.review-card blockquote p { font-size: 1rem; line-height: 1.6; color: var(--color-text); }
.review-card__name { margin-top: auto; font-weight: 600; font-size: 0.9375rem; color: var(--color-text); }
.reviews__more { margin-top: var(--space-md); text-align: center; }
.reviews__more a { font-weight: 600; }
