/* =========================================================================
 THE HARDWOOD GUYS — COMPONENT LIBRARY
 Depends on tokens.css (load it first). Every color/spacing/type value
 below is a var(--semantic-token) reference — no raw hex, no raw px
 color literal, ever. That is what makes a retone a one-file change.
 ========================================================================= */

/* ---------------------------------------------------------------------
 0. RESET / BASE
 --------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html {
 -webkit-text-size-adjust: 100%;
 scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
 html { scroll-behavior: auto; }
}

body {
 margin: 0;
 font-family: var(--font-body);
 font-size: var(--text-base);
 line-height: var(--leading-normal);
 color: var(--color-text-body);
 background: var(--color-bg-page);
 font-variant-numeric: normal;
}

img, svg { max-width: 100%; display: block; }
button, input, select, textarea { font: inherit; color: inherit; }
ul, ol { padding: 0; margin: 0; list-style: none; }
h1, h2, h3, h4, p, figure, blockquote { margin: 0; }
a { color: inherit; text-decoration: none; }

h1, h2, h3, h4 {
 font-family: var(--font-heading);
 color: var(--color-text-heading);
 line-height: var(--leading-tight);
 font-weight: var(--weight-semibold);
}

.tabular-nums { font-variant-numeric: tabular-nums; }

/* Universal focus-visible treatment — solid outline, never a glow-only
 box-shadow, so it survives on every background in the system. */
:focus-visible {
 outline: 3px solid var(--color-focus-ring);
 outline-offset: 2px;
 border-radius: var(--radius-sm);
}
.site-header :focus-visible,
.site-header__mobile-nav :focus-visible,
.mobile-cta-bar :focus-visible,
.site-footer :focus-visible,
.hero :focus-visible {
 outline-color: var(--color-focus-ring-inverse);
}

/* ---------------------------------------------------------------------
 1. LAYOUT UTILITIES
 --------------------------------------------------------------------- */
.container {
 max-width: var(--container-max);
 margin-inline: auto;
 padding-inline: var(--container-pad);
}

.section {
 padding-block: var(--space-16);
}
.section--alt { background: var(--color-bg-page-alt); }
.section--dark { background: var(--color-bg-header); color: var(--color-text-inverse); }

.section-head {
 max-width: 640px;
 margin-bottom: var(--space-8);
}
.section-head p { color: var(--color-text-muted); margin-top: var(--space-3); }

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

/* Skip link — visible on keyboard focus, WCAG 2.4.1 */
.skip-link {
 position: absolute;
 top: -60px;
 left: var(--space-4);
 z-index: var(--z-skip-link);
 background: var(--color-cta-bg);
 color: var(--color-cta-text);
 padding: var(--space-3) var(--space-5);
 border-radius: 0 0 var(--radius-base) var(--radius-base);
 font-weight: var(--weight-semibold);
 transition: top var(--motion-base) var(--motion-ease);
}
.skip-link:focus {
 top: 0;
}

/* ---------------------------------------------------------------------
 2. BUTTONS
 --------------------------------------------------------------------- */
.btn {
 display: inline-flex;
 align-items: center;
 justify-content: center;
 gap: var(--space-2);
 min-height: var(--touch-target-min);
 padding: var(--space-3) var(--space-6);
 border-radius: var(--radius-base);
 font-family: var(--font-body);
 font-weight: var(--weight-semibold);
 font-size: var(--text-base);
 border: 2px solid transparent;
 cursor: pointer;
 transition: background-color var(--motion-base) var(--motion-ease),
 color var(--motion-base) var(--motion-ease),
 box-shadow var(--motion-base) var(--motion-ease),
 border-color var(--motion-base) var(--motion-ease);
 text-align: center;
}
.btn--lg { padding: var(--space-4) var(--space-8); font-size: var(--text-md); }
.btn--sm { padding: var(--space-2) var(--space-4); font-size: var(--text-sm); min-height: 36px; }
.btn--block { width: 100%; }

.btn--primary {
 background: var(--color-cta-bg);
 color: var(--color-cta-text);
}
.btn--primary:hover { background: var(--color-cta-bg-hover); box-shadow: var(--shadow-md); }
.btn--primary:disabled {
 background: var(--color-cta-bg-disabled);
 color: var(--color-cta-text-disabled);
 cursor: not-allowed;
}

.btn--secondary {
 background: var(--color-btn-secondary-bg);
 color: var(--color-btn-secondary-text);
 border-color: var(--color-btn-secondary-border);
}
.btn--secondary:hover {
 background: var(--color-btn-secondary-hover-bg);
 color: var(--color-btn-secondary-hover-text);
}

/* Outline button — used on top of the hero photo scrim (dark). Hover
 fills solid rather than swapping to red, because red-on-dark is one
 of the documented contrast traps. */
.btn--outline {
 background: transparent;
 color: var(--color-btn-outline-text);
 border-color: var(--color-btn-outline-border);
}
.btn--outline:hover {
 background: var(--color-btn-outline-hover-bg);
 color: var(--color-btn-outline-hover-text);
}

.btn svg { width: 1.15em; height: 1.15em; flex: none; }

/* ---------------------------------------------------------------------
 3. HEADER
 --------------------------------------------------------------------- */
.site-header {
 position: sticky;
 top: 0;
 z-index: var(--z-header);
 background: var(--color-bg-header);
 color: var(--color-text-inverse);
 box-shadow: var(--shadow-header);
}

.site-header__bar {
 display: flex;
 align-items: center;
 gap: var(--space-6);
 min-height: 72px;
 padding-block: var(--space-2);
}

.site-header__logo {
 display: flex;
 align-items: center;
 gap: var(--space-2);
 flex: none;
 max-width: 260px;
 color: var(--color-text-inverse);
}
/* The mark is a near-square badge (1391x1175), not a wide wordmark — it needs real
   height to read. David asked for it prominently displayed. */
.site-header__logo-mark { height: 92px; width: auto; display: block; flex: none; }
@media (max-width: 900px) {
  .site-header__logo-mark { height: 76px; }
}
@media (max-width: 480px) {
  .site-header__logo-mark { height: 58px; }
}

/* The bar carries logo + 6 nav items + phone + hours + CTA. Below 1400px the
   decorative tagline is the first thing to go — the crest already says who we
   are, and the footer NAP block repeats the location. */
@media (max-width: 1400px) {
  .site-header__logo-tag { display: none; }
}
/* Keep a real gutter between the last nav item and the phone block. */
.site-header__bar { column-gap: var(--space-8); }

.site-header__logo-word {
 font-family: var(--font-heading);
 font-weight: var(--weight-bold);
 font-size: 1.35rem;
 line-height: 1.1;
 letter-spacing: var(--tracking-tight);
}
.site-header__logo-tag {
 display: block;
 white-space: nowrap;
 font-family: var(--font-body);
 font-size: var(--text-xs);
 font-weight: var(--weight-medium);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--color-text-inverse);
 opacity: 0.85;
}

.site-nav { flex: 1; display: none; min-width: 0; }
.site-nav ul { display: flex; gap: var(--space-5); flex-wrap: nowrap; white-space: nowrap; }
@media (max-width: 1100px) {
  .site-nav ul { gap: var(--space-4); }
}
.site-nav a {
 display: inline-block;
 padding: var(--space-2) 0;
 font-weight: var(--weight-medium);
 color: var(--color-nav-link);
 border-bottom: 2px solid transparent;
}
.site-nav a:hover,
.site-nav a:focus-visible {
 text-decoration: underline;
 text-underline-offset: 4px;
}

.site-header__meta {
 display: none;
 flex-direction: column;
 align-items: flex-end;
 line-height: 1.3;
 flex: none;
}
.site-header__phone {
 font-weight: var(--weight-bold);
 font-size: var(--text-md);
 color: var(--color-text-inverse);
}
.site-header__phone:hover,
.site-header__phone:focus-visible { text-decoration: underline; }
.site-header__hours {
 font-size: var(--text-xs);
 color: var(--color-text-inverse);
 opacity: 0.85;
}
.site-header__cta { display: none; flex: none; }

.hamburger {
 flex: none;
 margin-left: auto;
 display: inline-flex;
 align-items: center;
 justify-content: center;
 width: var(--touch-target-min);
 height: var(--touch-target-min);
 background: transparent;
 border: 2px solid transparent;
 border-radius: var(--radius-base);
 color: var(--color-text-inverse);
 cursor: pointer;
}
.hamburger svg { width: 26px; height: 26px; }
.hamburger .icon-close { display: none; }
.hamburger[aria-expanded="true"] .icon-menu { display: none; }
.hamburger[aria-expanded="true"] .icon-close { display: block; }

.site-header__mobile-nav {
 position: fixed;
 inset: 72px 0 0 0;
 background: var(--color-bg-header);
 z-index: var(--z-mobile-nav);
 overflow-y: auto;
 padding: var(--space-6) var(--container-pad) var(--space-24);
}
.site-header__mobile-nav[hidden] { display: none; }

.site-header__mobile-nav ul { display: flex; flex-direction: column; }
.site-header__mobile-nav a {
 display: block;
 padding: var(--space-4) 0;
 font-family: var(--font-heading);
 font-size: var(--text-lg);
 font-weight: var(--weight-semibold);
 color: var(--color-text-inverse);
 border-bottom: 1px solid rgba(255, 255, 255, 0.18);
}
.site-header__mobile-nav a:hover,
.site-header__mobile-nav a:focus-visible { text-decoration: underline; }

.site-header__mobile-meta {
 margin-top: var(--space-8);
 padding-top: var(--space-8);
 border-top: 1px solid rgba(255, 255, 255, 0.25);
}
.site-header__mobile-meta .site-header__phone { font-size: var(--text-xl); }
.site-header__mobile-meta .site-header__hours { display: block; margin-top: var(--space-2); }

@media (min-width: 1024px) {
 .site-nav { display: block; }
 .site-header__meta { display: flex; }
 .site-header__cta { display: inline-flex; }
 .hamburger { display: none; }
}

/* ---------------------------------------------------------------------
 4. MOBILE STICKY BOTTOM CTA BAR
 The single most important conversion element on the site: it is the
 only UI guaranteed to be on-screen no matter how far a phone user has
 scrolled. Two 44px+ targets, safe-area aware, always visible.
 --------------------------------------------------------------------- */
/* The bar has THREE children on every page -- call, text, estimate -- and
   the column count here must always track that. This mismatched to a
   2-column grid once (call + estimate only, text forgotten) and the bar
   silently wrapped to two rows on every page below 1024px, covering page
   content. Call and text are equal-weight "reach us" actions; estimate
   stays widest since it's the red primary conversion action. */
.mobile-cta-bar {
 position: fixed;
 left: 0; right: 0; bottom: 0;
 z-index: var(--z-bottom-bar);
 display: grid;
 grid-template-columns: 1fr 1fr 1.3fr;
 gap: 1px;
 background: var(--color-border-default);
 box-shadow: var(--shadow-bottom-bar);
 padding-bottom: env(safe-area-inset-bottom, 0);
}
.mobile-cta-bar a {
 display: flex;
 align-items: center;
 justify-content: center;
 gap: var(--space-2);
 min-height: var(--touch-target-min);
 padding-block: var(--space-4);
 font-weight: var(--weight-bold);
 font-size: var(--text-base);
}
.mobile-cta-bar__call {
 background: var(--color-bg-header);
 color: var(--color-text-inverse);
}
.mobile-cta-bar__call:hover,
.mobile-cta-bar__call:focus-visible { text-decoration: underline; }
.mobile-cta-bar__text {
 background: var(--color-bg-header);
 color: var(--color-text-inverse);
}
.mobile-cta-bar__text:hover,
.mobile-cta-bar__text:focus-visible { text-decoration: underline; }
.mobile-cta-bar__estimate {
 background: var(--color-cta-bg);
 color: var(--color-cta-text);
}
.mobile-cta-bar__estimate:hover,
.mobile-cta-bar__estimate:focus-visible { background: var(--color-cta-bg-hover); }
.mobile-cta-bar svg { width: 20px; height: 20px; }

/* Reserve space so the fixed bar never covers the last content/footer.
   This must track the bar's actual rendered height (~62px at 1 row, once
   the column count above matches the child count) -- it was left at 64px
   from a previous 2-row/122px bar and happened to still clear a 1-row bar,
   but if the bar ever grows again this reserve has to grow with it. */
.has-mobile-cta-bar { padding-bottom: 64px; }

@media (min-width: 1024px) {
 .mobile-cta-bar { display: none; }
 .has-mobile-cta-bar { padding-bottom: 0; }
}

/* ---------------------------------------------------------------------
 5. PLACEHOLDER PHOTO BLOCK
 Every "photo" in this system is a placeholder — never a hotlinked
 live-site image, never an external request. The pattern + label make
 it obvious in review that real photography drops in later.
 --------------------------------------------------------------------- */
.photo-placeholder {
 position: relative;
 display: flex;
 align-items: flex-end;
 overflow: hidden;
 background:
 repeating-linear-gradient(135deg,
 rgba(var(--navy-rgb), 0.10) 0px, rgba(var(--navy-rgb), 0.10) 2px,
 transparent 2px, transparent 14px),
 var(--color-bg-page-alt);
 border: 1px solid var(--color-border-default);
 color: var(--color-text-muted);
 aspect-ratio: 4 / 3;
}
.photo-placeholder--wide { aspect-ratio: 16 / 9; }
.photo-placeholder--tall { aspect-ratio: 3 / 4; }
.photo-placeholder--square { aspect-ratio: 1 / 1; }
.photo-placeholder__label {
 display: flex;
 align-items: center;
 gap: var(--space-2);
 width: 100%;
 padding: var(--space-3);
 background: linear-gradient(to top, rgba(var(--near-black-rgb), 0.55), transparent);
 color: var(--white);
 font-size: var(--text-xs);
 font-weight: var(--weight-medium);
}
.photo-placeholder__label svg { width: 18px; height: 18px; flex: none; }

/* ---------------------------------------------------------------------
 6. HERO
 --------------------------------------------------------------------- */
.hero {
 position: relative;
 color: var(--color-text-inverse);
 /* .hero__media runs a Ken Burns scale/translate (see header-final.css's
    thg-kenburns keyframes) that, by the end of the 20s animation, grows
    .hero__media's own box to roughly 108% of .hero's width and shifts it
    left of 0 -- e.g. a 768px-wide hero measures its media at left:-43px
    width:829px once fully scaled. overflow:hidden is what keeps that
    contained; overflow:clip is added as a stricter guarantee (some engines
    have handled transformed-descendant overflow differently under plain
    `hidden`) that a scaled/translated child can never register as real
    page-level horizontal scroll. Keep both declarations.
    Verified live: with the animation forced to its end state,
    document.documentElement.scrollWidth still equals clientWidth. */
 overflow: hidden;
 overflow: clip;
}
.hero__media {
 position: absolute;
 inset: 0;
 background:
 linear-gradient(rgba(var(--near-black-rgb), 0.62), rgba(var(--near-black-rgb), 0.5)),
 repeating-linear-gradient(120deg,
 rgba(255, 255, 255, 0.06) 0px, rgba(255, 255, 255, 0.06) 2px,
 transparent 2px, transparent 18px),
 var(--color-bg-header);
 z-index: 0;
}
.hero__media-tag {
 position: absolute;
 bottom: var(--space-4);
 right: var(--space-4);
 z-index: 1;
 font-size: var(--text-xs);
 color: var(--white);
 opacity: 0.7;
 background: rgba(var(--near-black-rgb), 0.4);
 padding: var(--space-1) var(--space-3);
 border-radius: var(--radius-full);
}
.hero__content {
 position: relative;
 z-index: 1;
 padding-block: var(--space-20) var(--space-16);
 max-width: 760px;
}
.hero__eyebrow {
 font-size: var(--text-sm);
 font-weight: var(--weight-semibold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--white);
 opacity: 0.9;
 margin-bottom: var(--space-4);
}
.hero__title {
 font-size: var(--text-4xl);
 color: var(--white);
 letter-spacing: var(--tracking-tight);
 margin-bottom: var(--space-5);
}
.hero__subtitle {
 font-size: var(--text-md);
 line-height: var(--leading-relaxed);
 color: var(--white);
 max-width: 56ch;
 margin-bottom: var(--space-8);
}

.proof-chips {
 display: flex;
 flex-wrap: wrap;
 gap: var(--space-3);
 margin-bottom: var(--space-8);
}
.proof-chip {
 display: inline-flex;
 align-items: center;
 gap: var(--space-2);
 padding: var(--space-2) var(--space-4);
 border-radius: var(--radius-full);
 background: rgba(255, 255, 255, 0.12);
 border: 1px solid rgba(255, 255, 255, 0.35);
 font-size: var(--text-sm);
 font-weight: var(--weight-medium);
 color: var(--white);
}
.proof-chip svg { width: 18px; height: 18px; flex: none; }
/* Gold, sparingly: only the review-star and TV icons, per brand
 direction. Icon-only so chip text stays white for readability against
 the photo scrim (gold text on white/red both fail contrast — see
 tokens.css known traps). */
.proof-chip__icon--gold { color: var(--color-rating-star); }

.hero__actions {
 display: flex;
 flex-wrap: wrap;
 gap: var(--space-4);
}

/* Mobile-only hero photo caption (site/index.html). Job name + stain/finish,
   captioned the same way as the stain & finish carousel (section 20 below:
   .sgal__cap/.sgal__what/.sgal__spec) so the two read as one system --
   same heading font for the "what", same small-caps gold treatment for the
   spec. Hidden here by default: on desktop the hero copy already sits over
   the photo (kept as-is, David's call), so this caption only ever needs to
   show up under the 767px breakpoint. That breakpoint switch, and the
   positioning against the mobile banner's height, live in site/index.html's
   own mobile hero rules, since --hero-banner-h is defined there. */
.hero__photo-cap {
 display: none;
 position: absolute;
 z-index: 1;             /* .hero__scrim is also absolute and comes LATER in the DOM; without this it paints over the caption */
 margin: 0;
 flex-wrap: wrap;
 align-items: baseline;
 gap: 0 var(--space-2);
 color: var(--color-text-inverse);
 /* A shadow, not a panel. David's standing rule is no dark box behind hero
    text -- brighten the photo instead. A shadow buys legibility where the
    caption crosses a sunlit patch of floor without laying a slab over the
    photograph. */
 text-shadow: 0 1px 3px rgba(var(--near-black-rgb), 0.75);
}
/* Small on purpose. This answers "what floor am I looking at?" for the one
   visitor in twenty who wonders; it is a photo credit, not a headline, and it
   must not eat the picture or the page. */
.hero__photo-cap-what { font-family: var(--font-heading); font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.hero__photo-cap-spec {
 font-size: var(--text-xs);
 font-weight: var(--weight-semibold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--gold);            /* gold on navy -- the verified pairing, see tokens.css section 1 */
}

/* ---------------------------------------------------------------------
 7. REVIEW BAND
 --------------------------------------------------------------------- */
.reviews__head {
 display: flex;
 flex-wrap: wrap;
 align-items: center;
 justify-content: space-between;
 gap: var(--space-4);
 margin-bottom: var(--space-8);
}
.review-badge {
 display: inline-flex;
 align-items: center;
 gap: var(--space-3);
 padding: var(--space-3) var(--space-5);
 background: var(--color-bg-card);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-base);
 box-shadow: var(--shadow-sm);
}
.review-badge__stars { display: flex; gap: 2px; color: var(--color-rating-star); }
.review-badge__stars svg { width: 18px; height: 18px; }
.review-badge__text { font-size: var(--text-sm); color: var(--color-text-body); font-weight: var(--weight-medium); }
.review-badge__text strong { display: block; font-size: var(--text-base); color: var(--color-text-heading); }

/* Live Google rating line, revealed by site/review-stars.js once /api/reviews
   answers with a real number -- hidden (via the `hidden` attribute in the
   markup) reserves no space, so a visitor who never gets a live reading sees
   nothing different from today's badge. Muted like the rest of the badge's
   supporting text, not the gold used for the stars, because this is a
   caption on the claim above it, not a second headline. */
.review-badge__live {
 display: block;
 margin-top: var(--space-1);
 font-size: var(--text-xs);
 color: var(--color-text-muted);
}
.review-badge__live:hover,
.review-badge__live:focus-visible {
 text-decoration: underline;
 text-underline-offset: 2px;
}

.reviews__list {
 display: grid;
 gap: var(--space-6);
 grid-template-columns: 1fr;
}
.review-card {
 background: var(--color-bg-card);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-lg);
 padding: var(--space-6);
 box-shadow: var(--shadow-sm);
 display: flex;
 flex-direction: column;
 gap: var(--space-4);
}
.review-card__stars { display: flex; gap: 2px; color: var(--color-rating-star); }
.review-card__stars svg { width: 16px; height: 16px; }
.review-card blockquote {
 font-size: var(--text-base);
 line-height: var(--leading-relaxed);
 color: var(--color-text-body);
}
.review-card__author {
 font-weight: var(--weight-semibold);
 color: var(--color-text-heading);
 font-size: var(--text-sm);
}
.review-card__location { color: var(--color-text-muted); font-weight: var(--weight-regular); }

/* Platform mark on the author line, so a reader can see at a glance whether a
 review came from Google or Nextdoor. Nominative use -- it identifies where
 the review actually lives, which is exactly what these marks are for.

 Sized by HEIGHT with width:auto: the Google G is square (48x48) but the
 Nextdoor house is 110x89, so a shared square box would have squashed the
 house. Both sit on the same 18px cap line instead. */
.review-card__src {
 display: inline-block;
 height: 18px;
 width: auto;
 vertical-align: -3px;
 margin-right: var(--space-2);
 object-fit: contain;
}

.reviews__stat {
 margin-top: var(--space-8);
 text-align: center;
 color: var(--color-text-muted);
 font-size: var(--text-sm);
}
.reviews__stat strong { color: var(--color-text-heading); font-family: var(--font-heading); font-size: var(--text-lg); }

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

/* ---------------------------------------------------------------------
 8. QUOTE FORM
 --------------------------------------------------------------------- */
.quote-form-wrap {
 background: var(--color-bg-card);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-lg);
 box-shadow: var(--shadow-md);
 padding: var(--space-8);
 max-width: 720px;
 margin-inline: auto;
}

.form-error-summary {
 display: none;
 background: var(--color-bg-error);
 border: 1px solid var(--color-border-input-error);
 border-left: 4px solid var(--color-text-error);
 border-radius: var(--radius-base);
 padding: var(--space-4) var(--space-5);
 margin-bottom: var(--space-6);
 color: var(--color-text-error);
 font-weight: var(--weight-semibold);
 gap: var(--space-2);
 align-items: flex-start;
}
.form-error-summary.is-visible { display: flex; }
.form-error-summary svg { width: 22px; height: 22px; flex: none; margin-top: 2px; }
.form-error-summary a { text-decoration: underline; color: var(--color-text-error); }

.form-grid {
 display: grid;
 gap: var(--space-5);
 grid-template-columns: 1fr;
}
.form-field { display: flex; flex-direction: column; gap: var(--space-2); }
.form-field--span2 { grid-column: 1 / -1; }

.form-field label {
 font-weight: var(--weight-semibold);
 font-size: var(--text-sm);
 color: var(--color-text-heading);
}
.form-field .optional-tag {
 font-weight: var(--weight-regular);
 color: var(--color-text-muted);
 text-transform: none;
}
.form-field input[type="text"],
.form-field input[type="tel"],
.form-field input[type="email"],
.form-field input[type="number"],
.form-field select,
.form-field textarea {
 min-height: var(--touch-target-min);
 padding: var(--space-3) var(--space-4);
 border: 2px solid var(--color-border-input);
 border-radius: var(--radius-base);
 background: var(--color-bg-input);
 font-size: var(--text-base);
 color: var(--color-text-body);
}
.form-field textarea { min-height: 120px; resize: vertical; }
.form-field select {
 appearance: none;
 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B6B6B' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
 background-repeat: no-repeat;
 background-position: right var(--space-4) center;
 background-size: 18px;
 padding-right: var(--space-10);
}
.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible {
 outline: 3px solid var(--color-focus-ring);
 outline-offset: 1px;
 border-color: var(--color-focus-ring);
}
.form-field input[aria-invalid="true"],
.form-field select[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
 border-color: var(--color-border-input-error);
}
.field-error {
 display: none;
 align-items: center;
 gap: var(--space-1);
 font-size: var(--text-sm);
 color: var(--color-text-error);
 font-weight: var(--weight-medium);
}
.field-error.is-visible { display: flex; }
.field-error svg { width: 16px; height: 16px; flex: none; }
.field-hint { font-size: var(--text-sm); color: var(--color-text-muted); }

.form-checkbox {
 display: flex;
 align-items: flex-start;
 gap: var(--space-3);
}
.form-checkbox input[type="checkbox"] {
 width: 24px; height: 24px;
 flex: none;
 margin-top: 2px;
 accent-color: var(--color-cta-bg);
}
.form-checkbox label { font-weight: var(--weight-regular); font-size: var(--text-sm); color: var(--color-text-body); }

.form-actions { margin-top: var(--space-2); }
.form-fine-print {
 font-size: var(--text-xs);
 color: var(--color-text-muted);
 margin-top: var(--space-3);
}

.form-success {
 display: none;
 text-align: center;
 padding: var(--space-10) var(--space-4);
}
.form-success.is-visible { display: block; }
.form-success__icon {
 width: 56px; height: 56px;
 margin-inline: auto var(--space-4);
 color: var(--color-text-success);
 background: var(--color-bg-success);
 border: 2px solid var(--color-text-heading);
 border-radius: var(--radius-full);
 display: flex; align-items: center; justify-content: center;
}
.form-success h3 { margin-bottom: var(--space-2); }
.form-success p { color: var(--color-text-muted); max-width: 46ch; margin-inline: auto; }
.quote-form.is-submitted .form-grid,
.quote-form.is-submitted .form-checkbox,
.quote-form.is-submitted .form-actions,
.quote-form.is-submitted .form-fine-print { display: none; }

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

/* ---------------------------------------------------------------------
 9. VIDEO STRIP (YouTube facade — real iframe only injected on click)
 --------------------------------------------------------------------- */
.video-grid {
 display: grid;
 gap: var(--space-6);
 grid-template-columns: 1fr;
}
.video-card { display: flex; flex-direction: column; gap: var(--space-3); }
.video-thumb {
 position: relative;
 width: 100%;
 aspect-ratio: 16 / 9;
 border-radius: var(--radius-base);
 overflow: hidden;
 border: 0;
 padding: 0;
 cursor: pointer;
 background:
 linear-gradient(rgba(var(--near-black-rgb), 0.35), rgba(var(--near-black-rgb), 0.55)),
 repeating-linear-gradient(135deg,
 rgba(255, 255, 255, 0.08) 0px, rgba(255, 255, 255, 0.08) 2px,
 transparent 2px, transparent 16px),
 var(--color-bg-header);
}
.video-thumb__play {
 position: absolute;
 inset: 0;
 display: flex;
 align-items: center;
 justify-content: center;
}
.video-thumb__play svg {
 width: 64px; height: 64px;
 color: var(--white);
 filter: drop-shadow(0 2px 6px rgba(var(--near-black-rgb), 0.4));
}
.video-thumb:hover .video-thumb__play svg,
.video-thumb:focus-visible .video-thumb__play svg { transform: scale(1.08); }
.video-thumb__play svg { transition: transform var(--motion-base) var(--motion-ease); }
.video-thumb__duration {
 position: absolute;
 bottom: var(--space-2);
 right: var(--space-2);
 background: rgba(var(--near-black-rgb), 0.75);
 color: var(--white);
 font-size: var(--text-xs);
 font-weight: var(--weight-semibold);
 padding: 2px var(--space-2);
 border-radius: var(--radius-sm);
}
.video-card__title {
 font-size: var(--text-sm);
 font-weight: var(--weight-medium);
 color: var(--color-text-body);
 line-height: var(--leading-snug);
}
.video-embed-frame {
 width: 100%;
 aspect-ratio: 16 / 9;
 border: 0;
 border-radius: var(--radius-base);
}

@media (min-width: 768px) {
 .video-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
 .video-grid { grid-template-columns: repeat(4, 1fr); }
}

/* ---------------------------------------------------------------------
 10. PROCESS STRIP
 --------------------------------------------------------------------- */
.process-grid {
 display: grid;
 gap: var(--space-8);
 grid-template-columns: 1fr;
 counter-reset: process;
}
.process-step {
 position: relative;
 padding-top: var(--space-4);
 /* wood accent, not red — red is reserved exclusively for clickable CTAs
 so it never loses signal strength through decorative reuse */
 border-top: 3px solid var(--color-accent-wood);
}
.process-step__icon {
 width: 48px; height: 48px;
 color: var(--color-text-heading);
 margin-bottom: var(--space-4);
}
.process-step__number {
 font-family: var(--font-heading);
 font-size: var(--text-sm);
 font-weight: var(--weight-bold);
 color: var(--color-text-muted);
 letter-spacing: var(--tracking-wide);
 margin-bottom: var(--space-2);
}
.process-step h3 { font-size: var(--text-lg); margin-bottom: var(--space-2); }
.process-step p { color: var(--color-text-muted); }

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

/* ---------------------------------------------------------------------
 11. FINANCING STRIP
 --------------------------------------------------------------------- */
.financing-strip {
 background: var(--color-strip-bg);
 color: var(--color-strip-text);
 border-block: 1px solid var(--color-border-default);
 padding-block: var(--space-6);
}
.financing-strip__row {
 display: flex;
 flex-wrap: wrap;
 align-items: center;
 justify-content: center;
 gap: var(--space-3);
 text-align: center;
 font-size: var(--text-base);
 font-weight: var(--weight-medium);
}
.financing-strip__row svg { width: 28px; height: 28px; color: var(--color-strip-accent); flex: none; }
.financing-strip strong { color: var(--color-text-heading); }
.financing-strip small { display: block; color: var(--color-text-muted); font-size: var(--text-xs); margin-top: var(--space-1); }

/* ---------------------------------------------------------------------
 12. SERVICE CARDS
 --------------------------------------------------------------------- */
.service-grid {
 display: grid;
 gap: var(--space-6);
 grid-template-columns: 1fr;
}
.service-card {
 display: flex;
 flex-direction: column;
 background: var(--color-bg-card);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-lg);
 overflow: hidden;
 box-shadow: var(--shadow-sm);
 transition: box-shadow var(--motion-base) var(--motion-ease), transform var(--motion-base) var(--motion-ease);
}
.service-card:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); }
.service-card__body { padding: var(--space-6); display: flex; flex-direction: column; gap: var(--space-3); flex: 1; }
.service-card h3 { font-size: var(--text-lg); }
.service-card p { color: var(--color-text-muted); flex: 1; }
.service-card__links {
 display: flex;
 flex-wrap: wrap;
 gap: var(--space-4);
 margin-top: var(--space-2);
}
.service-card__links a {
 font-weight: var(--weight-semibold);
 font-size: var(--text-sm);
 color: var(--color-text-link);
 display: inline-flex;
 align-items: center;
 gap: var(--space-1);
 min-height: var(--touch-target-min);
}
.service-card__links a:hover,
.service-card__links a:focus-visible { color: var(--color-text-link-hover); text-decoration: underline; }
.service-card__links a.is-primary { color: var(--color-cta-bg); }
.service-card__links a.is-primary:hover,
.service-card__links a.is-primary:focus-visible { color: var(--color-cta-bg-hover); }
.service-card__links svg { width: 16px; height: 16px; }
/* An inline SVG with no size rule expands to its intrinsic/replaced-element
   default (~300x300 for a square viewBox with no width/height attribute) --
   that is what shipped here: a 298x298px phone icon swallowing the card.
   Sized to match .service-card__links svg above so both icons in a card
   agree. */
.service-card__note svg { width: 16px; height: 16px; flex: none; }

@media (min-width: 640px) {
 .service-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
 .service-grid { grid-template-columns: repeat(4, 1fr); }
}

/* ---------------------------------------------------------------------
 13. BEFORE / AFTER SLIDER
 Vanilla JS drag + keyboard operable (Arrow Left/Right, Home, End).
 Respects prefers-reduced-motion (no transition on drag/keys, only an
 instant position update) — see components.html script.
 --------------------------------------------------------------------- */
.ba-slider {
 position: relative;
 width: 100%;
 aspect-ratio: 4 / 3;
 border-radius: var(--radius-lg);
 overflow: hidden;
 border: 1px solid var(--color-border-default);
 box-shadow: var(--shadow-sm);
 touch-action: pan-y;
 user-select: none;
}
.ba-slider__after,
.ba-slider__before {
 position: absolute;
 inset: 0;
 aspect-ratio: auto;
 border: 0;
 border-radius: 0;
}
.ba-slider__before {
 /* clipped by JS setting inline width % — starts at 50 */
 width: 50%;
 overflow: hidden;
}
.ba-slider__before .photo-placeholder,
.ba-slider__after .photo-placeholder {
 position: absolute;
 inset: 0;
 width: var(--ba-fixed-width, 100%);
 height: 100%;
 border-radius: 0;
 border: 0;
}
.ba-slider__before .photo-placeholder { width: 100vw; max-width: none; }
.ba-slider__handle {
 position: absolute;
 top: 0;
 bottom: 0;
 left: 50%;
 width: 4px;
 background: var(--white);
 transform: translateX(-50%);
 box-shadow: 0 0 0 1px rgba(var(--near-black-rgb), 0.25);
}
.ba-slider__handle-grip {
 position: absolute;
 top: 50%;
 left: 50%;
 transform: translate(-50%, -50%);
 width: var(--touch-target-min);
 height: var(--touch-target-min);
 border-radius: var(--radius-full);
 background: var(--white);
 color: var(--color-text-heading);
 display: flex;
 align-items: center;
 justify-content: center;
 box-shadow: var(--shadow-md);
 cursor: ew-resize;
 border: 3px solid var(--color-cta-bg);
}
.ba-slider__handle-grip:focus-visible {
 outline: 3px solid var(--color-focus-ring);
 outline-offset: 3px;
}
.ba-slider__handle-grip svg { width: 22px; height: 22px; }
.ba-slider__tag {
 position: absolute;
 top: var(--space-3);
 z-index: 1;
 padding: var(--space-1) var(--space-3);
 border-radius: var(--radius-full);
 font-size: var(--text-xs);
 font-weight: var(--weight-bold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 background: rgba(var(--near-black-rgb), 0.65);
 color: var(--white);
}
.ba-slider__tag--before { left: var(--space-3); }
.ba-slider__tag--after { right: var(--space-3); }
.ba-caption {
 margin-top: var(--space-3);
 font-size: var(--text-sm);
 color: var(--color-text-muted);
 text-align: center;
}

/* ---------------------------------------------------------------------
 14. FOOTER — the only place disclaimers live (Canonical Decision #4)
 --------------------------------------------------------------------- */
.site-footer {
 background: var(--color-bg-footer);
 color: var(--color-text-inverse);
 padding-block: var(--space-16) var(--space-8);
}
.footer-grid {
 display: grid;
 gap: var(--space-10);
 grid-template-columns: 1fr;
 padding-bottom: var(--space-10);
 border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
/* Intentionally a styled <p>, not a heading element: footer nav groups are
 already labeled via aria-label on each <nav>, so no heading tag is
 introduced here. Keeps the document's heading outline (H1 → H2 → H3)
 uninterrupted by the footer, which sits after the last real heading. */
.footer-col-title {
 color: var(--color-text-inverse);
 font-size: var(--text-sm);
 font-weight: var(--weight-bold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 margin-bottom: var(--space-4);
}
.footer-col ul { display: flex; flex-direction: column; gap: var(--space-3); }
.footer-col a { color: var(--color-nav-link); opacity: 0.9; }
.footer-col a:hover, .footer-col a:focus-visible { text-decoration: underline; opacity: 1; }
.footer-brand p { color: var(--color-text-inverse); opacity: 0.85; max-width: 40ch; margin-top: var(--space-3); }
.footer-brand .site-header__phone { font-size: var(--text-lg); display: inline-block; margin-top: var(--space-4); }
.footer-social { display: flex; gap: var(--space-3); margin-top: var(--space-5); }
.footer-social a {
 width: 40px; height: 40px;
 display: flex; align-items: center; justify-content: center;
 border: 1px solid rgba(255, 255, 255, 0.4);
 border-radius: var(--radius-full);
}
.footer-social svg { width: 18px; height: 18px; }

.footer-bottom {
 margin-top: var(--space-8);
 display: flex;
 flex-direction: column;
 gap: var(--space-4);
 font-size: var(--text-xs);
 color: var(--color-text-inverse);
 opacity: 0.75;
}
.footer-disclaimer { max-width: 80ch; line-height: var(--leading-relaxed); }
.footer-legal { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); }
.footer-legal a { text-decoration: underline; }

@media (min-width: 640px) {
 .footer-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
 .footer-grid { grid-template-columns: 1.4fr repeat(5, 1fr); }
 .footer-bottom { flex-direction: row; align-items: center; justify-content: space-between; }
}

/* ---------------------------------------------------------------------
 15. MISC
 --------------------------------------------------------------------- */
.badge-row {
 display: flex;
 flex-wrap: wrap;
 gap: var(--space-3);
 align-items: center;
}

.eyebrow {
 display: inline-block;
 font-size: var(--text-sm);
 font-weight: var(--weight-semibold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--color-cta-bg);
 margin-bottom: var(--space-3);
}

.showcase-block {
 border: 1px dashed var(--color-border-default);
 border-radius: var(--radius-lg);
 padding: var(--space-6);
 margin-bottom: var(--space-16);
}
.showcase-index {
 padding-block: var(--space-4);
 border-bottom: 1px solid var(--color-border-default);
 background: var(--color-bg-page-alt);
}
.showcase-index p {
 font-size: var(--text-xs);
 font-weight: var(--weight-bold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--color-text-muted);
 margin-bottom: var(--space-2);
}
.showcase-index ul { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-5); }
.showcase-index a {
 display: inline-flex;
 align-items: center;
 min-height: 32px;
 font-size: var(--text-sm);
 font-weight: var(--weight-semibold);
 color: var(--color-text-link);
}
.showcase-index a:hover, .showcase-index a:focus-visible { color: var(--color-text-link-hover); text-decoration: underline; }

.showcase-block__label {
 display: inline-block;
 font-family: var(--font-body);
 font-size: var(--text-xs);
 font-weight: var(--weight-bold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--color-text-muted);
 background: var(--color-bg-page-alt);
 padding: var(--space-1) var(--space-3);
 border-radius: var(--radius-sm);
 margin-bottom: var(--space-6);
}

/* Belt-and-suspenders global reduced-motion guard, in addition to the
 token-level override in tokens.css (covers any literal duration a
 future component author forgets to tokenize). */
@media (prefers-reduced-motion: reduce) {
 *, *::before, *::after {
 animation-duration: 0.01ms !important;
 animation-iteration-count: 1 !important;
 transition-duration: 0.01ms !important;
 }
}

/* ---------------------------------------------------------------------
 16. CREDO / "WHY HOMEOWNERS PICK US" SECTION (homepage)
 Mobile-first. Breakpoints match the finish-hero/vfeat sections already
 inline on site/index.html (760px / 1040px) rather than this file's
 768/1024 pair, so the homepage keeps one consistent responsive rhythm
 across the credo, video-feature and reviews blocks that sit next to it.
 --------------------------------------------------------------------- */
.credo .section-head { margin-bottom: var(--space-10); }

/* -- stat cards: 1 col mobile / 2 col tablet / 4 col desktop -- */
.credo-stats {
 display: grid;
 grid-template-columns: 1fr;
 gap: var(--space-5);
}
.credo-stat {
 display: flex;
 flex-direction: column;
 gap: var(--space-2);
 background: var(--color-bg-card);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-lg);
 padding: var(--space-6);
 box-shadow: var(--shadow-sm);
 text-align: center;
}
.credo-stat__figure {
 font-family: var(--font-heading);
 font-size: var(--text-2xl);
 font-weight: var(--weight-bold);
 color: var(--color-text-heading);
 line-height: var(--leading-tight);
}
.credo-stat__label {
 font-size: var(--text-sm);
 color: var(--color-text-muted);
 line-height: var(--leading-snug);
}

/* -- recurring h3 divider, used 3x through the section -- */
.credo-subhead {
 margin: var(--space-12) 0 var(--space-6);
 font-size: var(--text-xl);
}

/* -- recognition / awards cards: 1 / 2 / 4 col -- */
.credo-recognition {
 display: grid;
 grid-template-columns: 1fr;
 gap: var(--space-5);
}
.credo-rec {
 background: var(--color-bg-card);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-lg);
 padding: var(--space-6);
 box-shadow: var(--shadow-sm);
}
/* Gold fails contrast on a white card (1.77:1 — see the known contrast
 traps documented in tokens.css), so the icon uses navy here. Gold is
 reserved for the dark .credo-kit band below, where navy-on-gold is the
 one verified-safe (7.67:1 AAA) pairing. */
/* One shared icon slot for BOTH the line icons and the real award badge.
   They were 30px and 56px with different bottom margins, so the badge
   dwarfed its neighbours AND pushed its heading down out of line with the
   other three. Matching height and margin is what aligns the headings --
   there is no separate alignment rule, the icons themselves are the ruler.
   Sized by HEIGHT with width:auto because the artwork is not all square. */
.credo-rec__icon,
.credo-rec__badge {
 display: block;
 width: auto;
 height: 40px;
 margin-bottom: var(--space-4);
}
.credo-rec__icon {
 color: var(--color-text-heading);
}

/* Per-card colour. Navy line art next to the full-colour Nextdoor badge read
 as unfinished, so each icon now carries its own hue.

 CONTRAST: every stroke colour below clears 3:1 on white (WCAG 1.4.11 for
 meaningful graphics) -- red 5.01:1, blue 5.29:1, navy 13.60:1. Gold is the
 exception at 1.77:1 and is therefore used ONLY as a FILL behind a navy
 stroke, never as the stroke itself: the navy outline carries the contrast
 and the gold just makes the medal look like a medal. Same rule the hero
 star chips follow. */
.credo-rec__icon--award { color: var(--color-text-heading); }
.credo-rec__icon--award .medal  { fill: var(--gold); }
.credo-rec__icon--award .ribbon { stroke: var(--red); }

.credo-rec__icon--tv { color: var(--red); }
.credo-rec__icon--tv .screen { fill: rgba(var(--near-black-rgb), 0.06); }

.credo-rec__icon--ai { color: var(--blue); }
.credo-rec__icon--ai .spark { fill: var(--gold); stroke: var(--gold); }
/* Real award artwork, used in place of .credo-rec__icon on the cards where a
 genuine badge exists. Sized to the same optical weight as the line icons so
 a mixed row still reads as one row -- David asked for these "small". The
 supplied PNG is transparent, so no card, no plate, no border. */
/* Sizing now comes from the shared .credo-rec__icon/.credo-rec__badge rule
   above so the two can never drift apart again. */
.credo-rec h4 {
 font-size: var(--text-md);
 margin-bottom: var(--space-2);
}
.credo-rec p {
 color: var(--color-text-muted);
 font-size: var(--text-sm);
 line-height: var(--leading-normal);
}
/* Call to action on a recognition card. Styled as a link now so that swapping
 the <p> for an <a> when the Best of 2026 ballot URL lands is a one-element
 change with no CSS to chase. --pending is the pre-ballot state: same slot,
 same rhythm, but muted and not pretending to be clickable. */
.credo-rec__cta {
 display: inline-block;
 margin-top: var(--space-3);
 font-size: var(--text-sm);
 font-weight: var(--weight-semibold);
 color: var(--color-text-link);
 text-decoration: underline;
 text-underline-offset: 2px;
}
.credo-rec__cta:hover,
.credo-rec__cta:focus-visible { color: var(--color-text-link-hover); }
.credo-rec__cta--pending {
 color: var(--color-text-muted);
 font-weight: var(--weight-regular);
 text-decoration: none;
}

/* -- equipment "kit" band: dark navy, full width, the visual anchor -- */
.credo-kit {
 margin-top: var(--space-12);
 background: var(--color-bg-header);
 color: var(--color-text-inverse);
 border-radius: var(--radius-lg);
 padding: var(--space-8) var(--space-6);
}
.credo-kit h3,
.credo-kit h4 {
 color: var(--color-text-inverse);
}
.credo-kit__eyebrow {
 font-size: var(--text-sm);
 font-weight: var(--weight-semibold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--gold);
 margin-bottom: var(--space-2);
}
.credo-kit__lede {
 color: var(--color-text-inverse);
 font-size: var(--text-md);
 line-height: var(--leading-relaxed);
 max-width: 62ch;
 margin-top: var(--space-3);
}
.credo-kit__items {
 display: grid;
 grid-template-columns: 1fr;
 gap: var(--space-6);
 margin-top: var(--space-8);
}
/* Stacked until there is genuinely room for two columns INSIDE the item.
 Side-by-side was tried first and fails everywhere below ~1040px: at 360px
 the 110px machine left a ~190px text column, so "Hypergrinder I-4 Wood
 planetary sander" wrapped onto four lines and the body copy ran at about
 four words per line. Two columns of a two-column grid is not enough width
 for an image AND prose — the row only earns its keep on desktop. */
.credo-kit__item {
 display: flex;
 flex-direction: column;
 align-items: flex-start;
 gap: var(--space-4);
}
/* Transparent PNGs shot on a dark background — no card, no light
 background, no border, so the navy band shows straight through.

 Sized by HEIGHT, not width. The two machines have very different aspect
 ratios (the sander is 334x693, the vacuum 290x453), so a shared max-width
 rendered the sander 290px tall next to a 219px vacuum — they read as one
 big machine and one small one rather than two pieces of the same kit.
 Pinning the height and letting width follow puts them on the same scale. */
.credo-kit__item img {
 flex: none;
 width: auto;
 height: 220px;
 max-width: 100%;
 object-fit: contain;
 background: transparent;
 border: 0;
}
.credo-kit__item > div { min-width: 0; }
.credo-kit__item h4 {
 font-size: var(--text-md);
 margin-bottom: var(--space-2);
}
.credo-kit__item p {
 color: var(--color-text-inverse);
 font-size: var(--text-sm);
 line-height: var(--leading-normal);
 opacity: 0.9;
}
.credo-kit__note {
 color: var(--color-text-inverse);
 margin-top: var(--space-8);
 padding-top: var(--space-6);
 border-top: 1px solid rgba(255, 255, 255, 0.2);
 font-size: var(--text-sm);
 line-height: var(--leading-normal);
 opacity: 0.85;
}

/* -- numbered steps: 1 / 2 / 4 col -- */
.credo-steps {
 display: grid;
 grid-template-columns: 1fr;
 gap: var(--space-6);
}
.credo-step {
 display: flex;
 flex-direction: column;
 gap: var(--space-2);
}
.credo-step__num {
 display: inline-flex;
 align-items: center;
 justify-content: center;
 width: 40px;
 height: 40px;
 border-radius: var(--radius-full);
 background: var(--gold);
 color: var(--color-text-heading); /* navy-on-gold = 7.67:1 AAA, tokens.css */
 font-family: var(--font-heading);
 font-weight: var(--weight-bold);
 font-size: var(--text-lg);
 margin-bottom: var(--space-2);
}
.credo-step h4 { font-size: var(--text-lg); }
.credo-step p { color: var(--color-text-muted); }
.credo-step p a {
 color: var(--color-text-link);
 font-weight: var(--weight-semibold);
 text-decoration: underline;
 text-underline-offset: 2px;
}
.credo-step p a:hover,
.credo-step p a:focus-visible {
 color: var(--color-text-link-hover);
}

/* -- recent-floors photo gallery: 1 / 2 / 3 col --
 CURRENTLY UNUSED — no .credo-gallery markup ships today. The homepage
 gallery this was written for was cut on 2026-08-12: David wanted it to show
 dark / medium brown / light floors, and no genuinely light floor photo
 exists yet. Measured floor luminance across every candidate: darks 62-79,
 mediums 104-127, and the LIGHTEST available 100-102 — a grey-washed medium,
 not a natural blonde, so the "light" panel would not have read as lighter
 than the "medium" one. Kept rather than deleted because the markup comes
 straight back the day that photo arrives. See
 assets/incoming/gallery-candidates/README.txt. */
.credo-gallery {
 display: grid;
 grid-template-columns: 1fr;
 gap: var(--space-4);
}
.credo-gallery img {
 width: 100%;
 height: auto;
 display: block;
 /* --radius-md is not defined in tokens.css (only sm/base/lg/full exist);
 --radius-lg is the closest defined step and matches the review-card /
 service-card / ba-slider radius used elsewhere in this file. */
 border-radius: var(--radius-lg);
 aspect-ratio: 4 / 3;
 object-fit: cover;
 border: 1px solid var(--color-border-default);
 box-shadow: var(--shadow-sm);
}

/* -- closing CTA row -- */
.credo-cta {
 display: flex;
 flex-wrap: wrap;
 justify-content: center;
 align-items: center;
 gap: var(--space-4);
 margin-top: var(--space-12);
}
/* .btn--outline is authored for use on top of the dark hero photo scrim
 (white text/border on transparent — see section 2 above). The credo CTA
 sits on this section's light background, where that would render an
 invisible button, so it's scoped here to look/behave like .btn--secondary
 instead of touching the shared .btn--outline rule. */
.credo-cta .btn--outline {
 color: var(--color-btn-secondary-text);
 border-color: var(--color-btn-secondary-border);
}
.credo-cta .btn--outline:hover,
.credo-cta .btn--outline:focus-visible {
 background: var(--color-btn-secondary-hover-bg);
 color: var(--color-btn-secondary-hover-text);
}

@media (min-width: 760px) {
 .credo-stats { grid-template-columns: repeat(2, 1fr); }
 .credo-recognition { grid-template-columns: repeat(2, 1fr); }
 .credo-kit { padding: var(--space-10) var(--space-8); }
 .credo-kit__items { grid-template-columns: repeat(2, 1fr); }
 .credo-steps { grid-template-columns: repeat(2, 1fr); }
 .credo-gallery { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1040px) {
 .credo-stats { grid-template-columns: repeat(4, 1fr); }
 /* 5 cards (2 awards + Google/Yelp/Houzz) fit one row from here up; the
    2-per-row rule above still covers mobile/tablet, where 5-across would
    be too narrow to read. */
 .credo-recognition { grid-template-columns: repeat(5, 1fr); }
 .credo-kit { padding: var(--space-12) var(--space-10); }
 /* Only here is each item wide enough (~520px) to put the machine beside
    the copy and still leave a readable measure. See .credo-kit__item. */
 .credo-kit__item { flex-direction: row; }
 .credo-kit__item img { height: 290px; }
 .credo-steps { grid-template-columns: repeat(4, 1fr); }
 .credo-gallery { grid-template-columns: repeat(3, 1fr); }
}

/* =========================================================================
   17. THANK-YOU PAGE  (/thank-you)

   Reached only by a successful form post, and the only page that fires the
   Google Ads conversion. Deliberately plain: the visitor has already
   converted, so this page's job is to reassure and to offer the phone --
   not to sell again.
   ========================================================================= */
.thanks { max-width: 46rem; margin-inline: auto; text-align: center; }
/* The global reset sets `img, svg { display: block }`, and a block-level SVG
   ignores the parent's text-align -- so this sat hard against the left edge
   while every other element on the page was centred. margin-inline:auto is
   what actually centres it. */
.thanks__tick {
 width: 56px; height: 56px;
 color: var(--color-text-heading);
 margin: 0 auto var(--space-4);
}
.thanks__lede {
 font-size: var(--text-lg);
 line-height: var(--leading-relaxed);
 color: var(--color-text-body);
 max-width: 40ch; margin-inline: auto;
}
.thanks__now {
 margin-top: var(--space-8);
 padding: var(--space-6);
 background: var(--color-bg-page-alt);
 border-radius: var(--radius-lg);
}
.thanks__now-label {
 font-weight: var(--weight-semibold);
 color: var(--color-text-heading);
 margin-bottom: var(--space-3);
}
.thanks__hours { margin-top: var(--space-3); font-size: var(--text-sm); color: var(--color-text-muted); }
.thanks__next-head { margin-top: var(--space-12); }
.thanks__steps {
 list-style: none; padding: 0;
 margin-top: var(--space-6);
 display: grid; gap: var(--space-6);
 text-align: left;
}
.thanks__steps > li { display: flex; gap: var(--space-4); align-items: flex-start; }
.thanks__num {
 flex: none;
 display: inline-flex; align-items: center; justify-content: center;
 width: 36px; height: 36px;
 border-radius: var(--radius-full);
 background: var(--gold);
 color: var(--color-text-heading);   /* navy on gold = 7.67:1 AAA */
 font-family: var(--font-heading);
 font-weight: var(--weight-bold);
}
.thanks__steps h3 { font-size: var(--text-md); margin-bottom: var(--space-1); }
.thanks__steps p { color: var(--color-text-muted); font-size: var(--text-sm); line-height: var(--leading-normal); }
.thanks__meanwhile { margin-top: var(--space-10); color: var(--color-text-muted); font-size: var(--text-sm); }
.thanks__meanwhile a { color: var(--color-text-link); text-decoration: underline; text-underline-offset: 2px; }
@media (min-width: 760px) {
 .thanks__steps { grid-template-columns: repeat(3, 1fr); }
 .thanks__steps > li { flex-direction: column; }
}

/* =========================================================================
   18. FINISH PAGE IMAGERY  (/water-vs-oil-polyurethane and siblings)

   These comparison pages were pure text -- no images at all, placeholder
   hero, black type on white for the whole scroll. Colour here comes from
   real product photography rather than decorative tint.
   ========================================================================= */

/* Product cutout floated beside the prose. The PNG/WebP is transparent, so
   no card, no plate, no border -- it sits on the page background. */
.finish-figure--product {
 margin: var(--space-6) 0;
 text-align: center;
}
.finish-figure--product img {
 width: auto;
 max-width: 180px;
 height: auto;
 margin-inline: auto;
}
.finish-figure--product figcaption {
 margin-top: var(--space-3);
 font-size: var(--text-sm);
 color: var(--color-text-muted);
}
@media (min-width: 760px) {
 /* Float it out of the text flow so the comparison copy stays one column
    and the jug reads as an aside, not an interruption. */
 .finish-figure--product {
  float: right;
  width: 200px;
  margin: 0 0 var(--space-6) var(--space-8);
 }
}

/* The two THG-branded reference sheets. They are tall portraits (1195x1400)
   full of small type, so they get real width rather than thumbnail width --
   shrunk to a card they would be unreadable and pointless. */
.finish-sheets {
 display: grid;
 grid-template-columns: 1fr;
 gap: var(--space-8);
 margin-top: var(--space-6);
}
.finish-sheets img {
 width: 100%;
 height: auto;
 border-radius: var(--radius-lg);
 border: 1px solid var(--color-border-default);
 background: var(--white);
}
.finish-sheets figcaption {
 margin-top: var(--space-3);
 font-size: var(--text-sm);
 color: var(--color-text-muted);
}
@media (min-width: 760px) {
 .finish-sheets { grid-template-columns: repeat(2, 1fr); }
}

/* Revision stamp on the privacy policy. The policy text promises to keep a
   "Last Updated" date current, and for months the page showed no date at all
   -- a policy that says it is dated and is not undermines the one thing a
   privacy page has to be, which is trustworthy. */
.policy-updated {
 margin-top: var(--space-4);
 padding: var(--space-4);
 background: var(--color-bg-page-alt);
 border-radius: var(--radius-base);
 font-size: var(--text-sm);
 color: var(--color-text-body);
 line-height: var(--leading-normal);
}

/* =========================================================================
   19. TIMED LEAD POPUP  (site/lead-popup.js)

   Bottom sheet on mobile, centred dialog on desktop. The mobile treatment is
   not a style choice: Google demotes pages that show intrusive interstitials
   to mobile search traffic, and a third of this site's visits are people
   arriving on blog posts from search. A panel in the lower part of the
   screen with a visible close control is outside what that targets; a
   full-screen takeover would not be.
   ========================================================================= */
.thg-popup {
 position: fixed;
 inset: 0;
 z-index: 9998;
 display: flex;
 align-items: flex-end;              /* bottom sheet on mobile */
 justify-content: center;
 background: rgba(var(--near-black-rgb), 0.45);
}
.thg-popup__panel {
 position: relative;
 width: 100%;
 max-width: 26rem;
 max-height: 78vh;
 overflow-y: auto;
 background: var(--color-bg-card);
 border-radius: var(--radius-lg) var(--radius-lg) 0 0;
 padding: var(--space-5);
 box-shadow: 0 -8px 40px rgba(var(--near-black-rgb), 0.28);
 animation: thg-popup-in var(--motion-base) var(--motion-ease);
}
@keyframes thg-popup-in { from { transform: translateY(14px); opacity: 0; } to { transform: none; opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .thg-popup__panel { animation: none; } }

.thg-popup__close {
 position: absolute;
 top: var(--space-1);
 right: var(--space-1);
 width: 44px; height: 44px;         /* full touch target — it must be easy to dismiss */
 display: flex; align-items: center; justify-content: center;
 background: transparent; border: 0; cursor: pointer;
 font-size: 28px; line-height: 1;
 color: var(--color-text-muted);
 border-radius: var(--radius-full);
}
.thg-popup__close:hover { color: var(--color-text-heading); background: var(--color-bg-page-alt); }

.thg-popup__eyebrow {
 font-size: var(--text-xs);
 font-weight: var(--weight-semibold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--color-text-muted);
}
.thg-popup__title { font-size: var(--text-lg); margin: var(--space-1) 0 var(--space-2); }
.thg-popup__lede { color: var(--color-text-body); font-size: var(--text-sm); line-height: var(--leading-normal); }
.thg-popup__lede a { color: var(--color-text-link); text-decoration: underline; }

.thg-popup__form { margin-top: var(--space-4); display: grid; gap: var(--space-1); }
.thg-popup__form label { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--color-text-heading); }
.thg-popup__form input {
 width: 100%;
 /* Stays at the 44px touch minimum -- shrinking the tap target to save
    height would trade a real usability floor for a cosmetic one. */
 min-height: var(--touch-target-min);
 padding: var(--space-2) var(--space-3);
 border: 1px solid var(--color-border-input);
 border-radius: var(--radius-base);
 background: var(--color-bg-input);
 margin-bottom: var(--space-1);
}
.thg-popup__form input:focus-visible { border-color: var(--color-border-input-focus); }
.thg-popup__form button { margin-top: var(--space-3); }
.thg-popup__err:empty { display: none; }
.thg-popup__err {
 color: var(--color-text-error);
 font-size: var(--text-sm);
 font-weight: var(--weight-semibold);
}
.thg-popup__alt { margin-top: var(--space-3); text-align: center; font-size: var(--text-sm); color: var(--color-text-muted); }
.thg-popup__alt a { color: var(--color-text-link); font-weight: var(--weight-semibold); text-decoration: underline; }

@media (min-width: 640px) {
 .thg-popup { align-items: center; }
 .thg-popup__panel { border-radius: var(--radius-lg); }
}

/* =========================================================================
   20. STAIN & FINISH CAROUSEL  (site/gallery.js, /api/gallery)

   Ten of David's best floors, each captioned with its stain colour and
   finish. Content is edited at /admin/gallery, so nothing here may assume a
   fixed number of slides.
   ========================================================================= */
.sgal { --sgal-radius: var(--radius-lg); }
.sgal__frame {
 position: relative;
 overflow: hidden;
 border-radius: var(--sgal-radius);
 /* The space is reserved BEFORE the photos arrive. Every uploaded image is
    cropped to exactly 1600x900 by the admin page, so 16/9 is a promise the
    data actually keeps and the page never jumps as slides load. */
 aspect-ratio: 16 / 9;
 background: var(--color-bg-header);
 border: 1px solid var(--color-border-default);
 box-shadow: var(--shadow-md);
}
.sgal__track {
 display: flex;
 height: 100%;
 transition: transform var(--motion-slow, 420ms) var(--motion-ease);
}
@media (prefers-reduced-motion: reduce) { .sgal__track { transition: none; } }

.sgal__slide { position: relative; flex: 0 0 100%; margin: 0; height: 100%; }
.sgal__slide img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Caption sits on a gradient rather than a solid bar: the photo is the point,
   and a solid strip crops it. Dark enough at the base for white text. */
/* Bottom left, and deliberately restrained. The photo is the point of this
   carousel -- the caption only has to answer "which floor is this and what is
   on it". An earlier version ran 20px/15px type over a scrim that started
   two-thirds of the way up the frame, and on a phone the job name wrapped to
   two lines and buried the floor. Type is now the same 15px/13px as the hero
   photo credit, and the scrim is shorter and starts lower, so far more of the
   photograph survives. */
.sgal__cap {
 position: absolute;
 inset: auto 0 0 0;
 padding: var(--space-6) var(--space-5) var(--space-3);
 /* Shorter and tighter to the bottom edge than a full-height wash: opaque
    enough under the words to keep them legible over a sunlit floor, but
    clear of the picture well before the halfway mark. */
 background: linear-gradient(to top,
   rgba(var(--near-black-rgb), 0.78) 0%,
   rgba(var(--near-black-rgb), 0.55) 45%,
   transparent 100%);
 color: var(--white);
 display: flex;
 flex-wrap: wrap;
 align-items: baseline;
 gap: 0 var(--space-3);
 text-shadow: 0 1px 3px rgba(var(--near-black-rgb), 0.6);
}
.sgal__what { font-family: var(--font-heading); font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.sgal__spec {
 font-size: var(--text-xs);
 font-weight: var(--weight-semibold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--gold);            /* gold on a dark scrim -- the verified pairing */
}

.sgal__prev, .sgal__next {
 position: absolute; top: 50%; transform: translateY(-50%);
 width: 44px; height: 44px;     /* full touch target */
 display: flex; align-items: center; justify-content: center;
 border: 0; border-radius: var(--radius-full);
 background: rgba(255, 255, 255, 0.9);
 color: var(--color-text-heading);
 font-size: 20px; line-height: 1; cursor: pointer;
}
.sgal__prev { left: var(--space-3); } .sgal__next { right: var(--space-3); }
.sgal__prev:hover, .sgal__next:hover { background: var(--white); }

.sgal__dots { display: flex; justify-content: center; gap: 6px; margin-top: var(--space-4); position: relative; }
/* The visible dot stays 10x10; the button itself is the 24x24 tap target and
   the dot is painted by ::before, so an audit that measures the button box
   (rather than a pseudo-element) sees a full-size target. */
.sgal__dot {
 position: relative; width: 24px; height: 24px; padding: 0; margin: 5px 0;
 border: 0; background: transparent; cursor: pointer;
}
.sgal__dot::before {
 content: ""; position: absolute; top: 50%; left: 50%;
 width: 10px; height: 10px; transform: translate(-50%, -50%);
 border-radius: var(--radius-full); background: var(--border);
}
.sgal__dot.is-on::before { background: var(--color-text-heading); }

@media (max-width: 480px) {
 /* A phone frame is only ~190px tall, so every pixel the caption takes is a
    pixel of floor the visitor does not get to see.

    This block used to read `font-size: var(--text-base)`, stepping the title
    DOWN from a 20px base. Once the base became 15px that same rule silently
    scaled it back UP to 17px on exactly the screens that needed it smallest,
    and at 17px a name like "Harmony On The Lakes Home in Canton, Ga" wrapped
    to two lines and the caption swallowed 51% of the picture. Overrides that
    are written as an absolute value rather than a direction do this: they
    stop tracking the thing they were meant to follow.

    13px keeps the longer job names to a single line at 375px, and the tighter
    line-height and padding take the caption to roughly a third of the frame.
    The spec line shares the size -- uppercase, gold and letterspaced is
    already enough to separate the two. */
 .sgal__cap {
  padding: var(--space-4) var(--space-4) var(--space-3);
  line-height: 1.3;
 }
 .sgal__what { font-size: var(--text-xs); }
 .sgal__prev, .sgal__next { display: none; }   /* swipe instead */
}

/* Watermark on the carousel. Top-left, above the photo, clear of the caption
   and of the prev arrow (which is vertically centred). Semi-transparent with
   a soft shadow so it reads on a pale floor and a dark one alike -- the logo
   is red, white and gold, and some of these photos are pale oak in full sun.
   Small on purpose: it is a signature, not a billboard. */
.sgal__mark {
 position: absolute;
 top: var(--space-3);
 left: var(--space-3);
 z-index: 2;
 pointer-events: none;          /* never intercepts a swipe */
 display: block;
 width: clamp(64px, 8vw, 96px);
 opacity: 0.88;
 filter: drop-shadow(0 2px 6px rgba(var(--near-black-rgb), 0.55));
}
.sgal__mark img { width: 100%; height: auto; display: block; }

/* The carousel sat flush against the recognition cards above it, so the two
   read as one run of boxes. It is its own thing and needs room to be that. */
.credo .sgal { margin-top: var(--space-10); }

/* ---------------------------------------------------------------------
   21. CREW VAN BREAKER
   A single photograph dropped into the middle of a long text page. Several
   of these pages -- /costguide worst of all at ~11,000 characters with no
   image whatsoever -- were an unbroken wall of prose, which is where readers
   give up. This is not decoration: a marked van in a real driveway is the
   proof that an actual local crew turns up, placed at the point in the page
   where the reader is deciding whether to trust that.
   --------------------------------------------------------------------- */
.crew-van {
  margin: var(--space-12) auto;
  /* This figure sits as a direct child of <main>, OUTSIDE the .container that
     wraps every other block on these pages, so it inherits no gutter. Left as
     just `max-width`, it rendered edge to edge on a phone -- measured 0..390px
     on a 390px screen -- while the text around it stayed inset, which reads as
     a broken image rather than a deliberate full-bleed. Subtracting the
     container gutter from the width restores the alignment on narrow screens
     and still caps at 760px on wide ones. */
  width: calc(100% - (var(--container-pad) * 2));
  max-width: 760px;
}
.crew-van img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-base);
  border: 1px solid var(--color-border-default);
}
/* No figcaption rule here on purpose. This photo carries no caption: David no
   longer owns this van, so any line describing it as the crew's current
   vehicle would be a claim that is no longer true. The picture stays because
   it is a good picture; it must not be captioned back into a promise. */

/* ---------------------------------------------------------------------
   22. PRODUCT GROUP HEAD (stair-parts item-type sections)
   Measured on the live site: the old markup reused .section-head for these
   in-page group dividers ("Newels & Caps", "Balusters & Iron", etc.), which
   left the group <h3> unstyled by any font-size rule -- it fell through to
   the browser's UA default and rendered at 19.89px/600, while the
   .service-card h3 titles inside the group (var(--text-lg), 24px) rendered
   larger than the label meant to organize them. With only ~32px of margin
   above it, a group heading read as another card caption, not a section
   break. .pgroup-head gives these dividers their own class instead of
   overloading .section-head, sized clearly above the 24px card titles, with
   a border-top divider and a short gold accent rule so each group reads as
   unmistakably its own section.
   --------------------------------------------------------------------- */
.pgroup-head {
  margin: var(--space-16) 0 var(--space-6);
  padding-top: var(--space-8);
  border-top: 1px solid var(--color-border-default);
}
/* The first group on a page has nothing above it to divide from -- a
   :first-of-type selector doesn't reliably match here because each
   .pgroup-head is followed by a sibling .service-grid, which breaks the
   "of-type" run across the page, so the "no border on the first group" case
   is emitted explicitly by the generator as this modifier instead. */
.pgroup-head--first {
  margin-top: var(--space-8);
  border-top: none;
}
.pgroup-head__title {
  font-family: var(--font-heading);
  font-size: var(--text-2xl);   /* 36px -- clearly larger than the 24px
                                    (var(--text-lg)) .service-card h3 titles
                                    it organizes */
  font-weight: var(--weight-semibold);
  margin: 0;
}
.pgroup-head__title::after {
  content: "";
  display: block;
  width: 48px;
  height: 3px;
  background: var(--gold);
  margin-top: var(--space-2);
}

/* ---------------------------------------------------------------------
   Card alignment inside grouped stair grids -- second half of the
   /products/stair-parts jumbled-grid fix. The first half (giving
   .service-card__photo a uniform 4:3 aspect-ratio, in page-extra.css) is
   handled in build-products.py; it stops real photos rendering at their
   natural height (344/416/355px, measured live) beside 4:3 placeholders
   (266px). Even with every photo the same height, product names still
   wrap 1-3 lines ("Balusters" vs "5 Base Red Oak Box Newel Unfinished x
   5 Base"), so everything below the title -- the optional description
   paragraph, the "Call for availability" note, and the View details /
   Free estimate links -- started at a different y-offset card to card
   within the same row.

   Scoped with the .pgroup-head ~ .service-grid sibling pattern so this
   reaches ONLY grids that follow a group head -- currently just the
   stair-parts category page's grouped listing. The homepage's
   .service-grid has no preceding .pgroup-head, so it is untouched.
   --------------------------------------------------------------------- */
.pgroup-head ~ .service-grid .service-card h3 {
  /* Reserve two title lines so a one-line name doesn't leave its card
     shorter than a same-row neighbor with a two- or three-line name.
     .service-card h3 sets font-size only (no local line-height), so it
     inherits line-height: var(--leading-tight) -- 1.15, not the rounder
     1.3 an unverified guess would use -- from this file's sitewide
     `h1, h2, h3, h4` rule. Measured off that token, not assumed. */
  min-height: calc(2 * 1.15em);
}
.pgroup-head ~ .service-grid .service-card__body > :last-child {
  /* .service-card is display:flex;flex-direction:column, but ITS OWN
     last child is .service-card__body, which already carries flex:1 --
     .service-card__body fills 100% of the card's remaining height with
     no free space left over, so `.service-card > :last-child { margin-
     top: auto }` (the naive version of this rule) would be a no-op: auto
     margins can only claim free space that exists. The actual
     raggedness lives one level deeper: some cards in a row have an
     optional products.json `description` paragraph and some don't, and
     both that paragraph and .service-card__note match this file's
     sitewide `.service-card p { flex: 1; }` rule, so they fight over
     .service-card__body's free space differently depending on whether
     the description exists -- pushing the links row below them to a
     different height card to card. .service-card__body is ALSO
     display:flex;flex-direction:column (this file's own rule for it),
     and ITS last child is always .service-card__links, description or
     not -- so that is the element that actually needs anchoring here.
     margin-top:auto on it claims .service-card__body's free space ahead
     of the flex-grow paragraphs above, pinning the links row to the
     card's bottom edge and leaving the title/description/note at their
     natural heights. */
  margin-top: auto;
}

/* Cards WITH a .service-card__note (every stair-parts card has one --
   "Call for availability & installed pricing.") still had a second source
   of raggedness even with the rule above: .service-card__note matches
   this file's sitewide `.service-card p { flex: 1; }` rule, and with only
   .service-card__links pinned by margin-top:auto, the note's flex-grow
   was left to fight that auto margin for .service-card__body's leftover
   space -- measured up to 55px of ragged note-bottom offset across a row,
   depending on whether a sibling card also had a description paragraph
   competing for the same space.

   Fixed by moving the auto margin off the links and onto the note itself:
   the note (not the paragraph above it) claims 100% of the free space, so
   .service-card__links has nothing left to auto-margin against and gets a
   small fixed gap instead. Result: notes+links become a level bottom
   cluster, flush to the card's bottom edge, on every card that has a note.
   Same specificity as the :last-child rule above (four classes each), so
   source order decides -- these two rules, being later, win on cards WITH
   a note; the :last-child rule above still governs cards WITHOUT one,
   since .service-card__note doesn't exist there for it to lose to. */
.pgroup-head ~ .service-grid .service-card__note {
  margin-top: auto;
}
.pgroup-head ~ .service-grid .service-card__note ~ .service-card__links {
  margin-top: var(--space-2);
}

/* Owner's call: the stair-part photos were landscape 4:3 tiles cropped by
   page-extra.css's sitewide `.service-card__photo { aspect-ratio: 4/3; }`
   (section 10 of that file), which amputated the tops and bottoms of tall
   subjects -- newel posts, balusters -- leaving a cropped middle band. The
   assets themselves are now composited portrait (1050x1400, full subject
   top-to-bottom, see composite_portrait.py), so the tile needs to go
   portrait too or the browser just re-crops the new tall photo back down
   to the old 4:3 box. Scoped with the same .pgroup-head ~ .service-grid
   sibling pattern as the rest of this section so it reaches only the
   grouped stair-parts grids, not the homepage's .service-grid (no
   preceding .pgroup-head there). Three classes here (0,3,0) beats
   page-extra.css's single-class .service-card__photo (0,1,0) regardless
   of stylesheet order, so this wins without !important. object-fit:
   cover is a no-op crop-wise -- the assets are exactly 3:4, matching this
   aspect-ratio exactly -- kept only so a future off-ratio photo degrades
   by cropping instead of stretching. */
.pgroup-head ~ .service-grid .service-card__photo {
  aspect-ratio: 3 / 4;
  object-fit: cover;
}

/* ---------------------------------------------------------------------
   VIDEO TILES (vfeat) — shared click-to-play YouTube facades.
   Used on service hubs, cost guides and the water-damage page. The
   homepage keeps its own inline block (featured tile, 3-up grid), which
   loads after this file and wins there.
   --------------------------------------------------------------------- */
.vfeat__grid { display: grid; gap: var(--space-6); grid-template-columns: 1fr; margin-top: var(--space-6); }
@media (min-width: 760px) { .vfeat__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .vfeat__grid { grid-template-columns: repeat(3, 1fr); } .prose .vfeat__grid { grid-template-columns: repeat(2, 1fr); } }
/* Inside a text column (.prose, ~770px) three-up tiles shrink to ~214px — too small to read. Two-up max there. */
.vfeat__grid--one { grid-template-columns: 1fr !important; max-width: 560px; }
.vfeat__tile { display: block; color: inherit; text-decoration: none; }
.vfeat__media { position: relative; display: block; border-radius: var(--radius-lg); overflow: hidden; background: var(--navy); box-shadow: var(--shadow-sm); }
.vfeat__media img { width: 100%; height: auto; display: block; aspect-ratio: 16 / 9; object-fit: cover; transition: transform var(--motion-base) var(--motion-ease); }
.vfeat__tile:hover .vfeat__media img { transform: scale(1.04); }
.vfeat__play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--white); }
.vfeat__play svg { width: 64px; height: 64px; filter: drop-shadow(0 2px 8px rgba(0, 0, 0, .55)); }
button.vfeat__media { appearance: none; border: 0; padding: 0; width: 100%; background: var(--navy); cursor: pointer; font: inherit; color: inherit; }
button.vfeat__media:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.vfeat__title { display: block; margin-top: var(--space-3); font-weight: var(--weight-semibold); color: var(--color-heading); line-height: 1.4; font-size: var(--text-base); }
.vfeat__yt { display: inline-block; margin-top: var(--space-2); font-size: var(--text-sm); color: var(--color-text-link); text-decoration: none; }
.vfeat__yt:hover { text-decoration: underline; }
.vfeat__tile .video-embed-frame { width: 100%; aspect-ratio: 16 / 9; border: 0; display: block; border-radius: var(--radius-lg); background: var(--navy); }

/* Placeholder notes for David. Preview builds only — the production
   build strips every <!--PENDING--> block and refuses to ship if any remain. */
.pending { color: #9a3412; background: #fff7ed; border: 1px dashed #9a3412; border-radius: 6px; padding: .5rem .75rem; }
span.pending { padding: .1rem .35rem; }

/* ---------------------------------------------------------------------
   AREA / SERVICE CHIPS (.area-chip) — shared.
   These rules lived only in the homepage's inline <style>, so the chip
   grids on the 20 town hubs and the water-damage page rendered their
   SVG icons at full container width (1,120px) and pushed the estimate
   form ~13,000px down the page (found 2026-10-01). The homepage keeps
   its inline copy; this makes every other page match it.
   --------------------------------------------------------------------- */
.area-chip-grid { display: grid; gap: var(--space-4); grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 640px) { .area-chip-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.area-chip { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-4) var(--space-5); background: var(--color-bg-card); border: 1px solid var(--color-border-default); border-radius: var(--radius-base); font-weight: var(--weight-semibold); color: var(--color-text-heading); min-height: var(--touch-target-min); min-width: 0; text-decoration: none; }
.area-chip:hover, .area-chip:focus-visible { border-color: var(--color-text-link); color: var(--color-text-link-hover); }
.area-chip svg { width: 20px; height: 20px; color: var(--color-cta-bg); flex: none; }

/* ---------------------------------------------------------------------
   IN-TEXT LINKS. The reset above sets `a { color: inherit; text-decoration:
   none }`, so links inside body copy looked like plain text (2026-10-01).
   Style them as links in light content areas only — never buttons, chips,
   cards, the dark hero, CTA bands or the financing strip.
   --------------------------------------------------------------------- */
main .prose a:not([class]),
main .faq-item__a a:not([class]),
main .section-head p a:not([class]),
main .check-list a:not([class]),
main p > a:not([class]):not(.hero *):not(.cta-band *):not(.financing-strip *):not(.proof-strip *) {
  color: var(--color-text-link);
  text-decoration: underline;
  text-underline-offset: 2px;
}
main .prose a:not([class]):hover,
main .faq-item__a a:not([class]):hover,
main .section-head p a:not([class]):hover,
main .check-list a:not([class]):hover,
main p > a:not([class]):not(.hero *):not(.cta-band *):not(.financing-strip *):not(.proof-strip *):hover {
  color: var(--color-text-link-hover);
}

/* Town pages: the "Water damage repair in {Town}" note sits in a white card
   INSIDE the brown .cta-band, so its paragraph and link inherited the band's
   white text and rendered white-on-white -- the whole sentence (including the
   "we meet with you, and with your adjuster on site if needed" wording and the
   link to /water-damaged-hardwood-floor) was invisible on all 19 town pages
   from the Oct 1-2 rebuild until this fix (found 2026-10-05). The card has its
   own white background, so it sets its own ink colors. */
/* .cta-band p { color: white } is inline in each page and equally specific, so this selector is one class stronger. */
.cta-band .town-services__wd p { color: var(--ink); }
.cta-band .town-services__wd a { color: var(--color-text-link); text-decoration: underline; text-underline-offset: 2px; }
.cta-band .town-services__wd a:hover { color: var(--color-text-link-hover); }


/* =========================================================================
   AUDIT FIXES 2026-10-05
   Visual audit (desktop 1440 + mobile 320-390, ~35 pages). Appended rather
   than edited in place so each fix sits next to the reason for it; nothing
   above this line was rewritten. Later rules win at equal specificity, and
   where an inline <style> on a page defines the same selector (home, about,
   gallery) the selector here is made one step more specific so it still wins.
   ========================================================================= */

/* ---- 1. Focus ring inside the white hero form card -------------------------
   `.hero :focus-visible` flips the ring to --color-focus-ring-inverse (white)
   because the hero sits on a dark photo. The quote card inside the hero is a
   WHITE panel, so every field and button in it got a white ring on white --
   keyboard users saw no focus at all. Blue is 5.29:1 on white (needs 3:1). */
.hero .hero__form-card :focus-visible { outline-color: var(--color-focus-ring); }

/* ---- 2. Outline button for LIGHT backgrounds ------------------------------
   .btn--outline is the dark-background variant (white text and border on the
   hero scrim / navy CTA band), so on a light section it is white-on-white.
   Four one-off scoped copies of this fix already exist (.credo-cta, header,
   .ig-feed__placeholder, the 404). This is the reusable one: navy text and a
   2px navy border on transparent; hover/focus fills navy and flips the label
   white (13.60:1). Use it ONLY on light surfaces -- inside .hero or
   .cta-band it would be navy on dark. */
.btn--outline-light {
  background: transparent;
  color: var(--color-btn-secondary-text);
  border-color: var(--color-btn-secondary-border);
}
.btn--outline-light:hover,
.btn--outline-light:focus-visible {
  background: var(--color-btn-secondary-hover-bg);
  color: var(--color-btn-secondary-hover-text);
  border-color: var(--color-btn-secondary-border);
}

/* ---- 3. Type scale -------------------------------------------------------
   The base heading rule sets family/weight/leading but NO size, so every
   unclassed <h2> fell back to the browser's 1.5em (25.5px at a 17px body)
   while several component <h3>s were set to 30-36px -- the sub-heading was
   bigger than the heading above it ("Awarded, reviewed and recommended" 30px
   under a 25.5px H2, the Bona card title 36px, the stair-parts group heads
   36px). The scale in tokens.css already says H2 = --text-xl; apply it. The
   H1 rules (.hero__title, page-level h1) are untouched: nothing here targets h1
   except text-wrap. */
h2 { font-size: var(--text-xl); }                         /* 30px */
.section-head h2 { font-size: clamp(1.75rem, 2.5vw + 1rem, 2.25rem); }  /* 28px -> 36px */
/* Balanced line breaks stop one-word last lines ("...refinishing in / Georgia").
   overflow-wrap is the safety net for the larger H2: one imported blog title
   ("HardwoodFloorRefinishing BonaMegaOne...", no spaces) fit a 390px phone at
   25.5px and pushed the page 26px sideways at 28px. */
h1, h2, h3 { text-wrap: balance; overflow-wrap: break-word; }

/* Component h3s capped at --text-lg (1.5rem): never bigger than their H2. */
.credo-subhead { font-size: var(--text-lg); }
/* .finish-hero__title is set in the homepage's inline <style>, which loads after
   this file; the extra class makes this win without touching the page. */
.finish-hero__body .finish-hero__title { font-size: var(--text-lg); }
/* Stair-parts group heads were deliberately 36px "so they read above the 24px
   card titles". At 24px they match those titles, so the divider rule + gold
   accent above carry the grouping and the weight goes up one step to keep a
   hierarchy (700 vs the cards' 600). */
.pgroup-head__title { font-size: var(--text-lg); font-weight: var(--weight-bold); }

/* ---- 5. Tap targets (WCAG 2.5.8 min is 24px; the site's own floor is 44px) --
   Measured on the live site at 390px: header phone 22px tall, footer column
   links 20px, footer social 40x40, footer legal links 21px, breadcrumb links
   24-32px, carousel dots 24x24. */
/* Header phone: 22px tall at 390px. 44px where it sits in the single mobile
   row (<1024px), in the drawer and in the footer. NOT on the desktop bar: there
   the phone, the "Text Us a Photo" button and the hours are stacked in one
   column, and a 44px phone pushed that stack to 111px and the whole header from
   110px to 127px. At desktop the line box is already 26px (above the 24px
   WCAG 2.5.8 minimum). */
.site-header__phone { display: inline-flex; align-items: center; }
@media (max-width: 1023px) { .site-header__phone { min-height: var(--touch-target-min); } }
.footer-brand .site-header__phone,
.site-header__mobile-meta .site-header__phone { display: inline-flex; min-height: var(--touch-target-min); }

/* Footer: social icons are 44px everywhere (harmless at any width). The link
   columns and legal row get the 44px hit box on phones/tablets/touch only: at
   desktop a 44px pitch added 103px to the footer (1098 -> 1201px) for mouse
   users who do not need it. On touch the box comes from the links themselves, so
   the list gap goes to 0 (it was 12px between 20px-tall links; rows now sit on
   a 44px pitch instead of 44px + 12px). */
.footer-social { gap: var(--space-2); }
.footer-social a { width: var(--touch-target-min); height: var(--touch-target-min); }
.footer-social svg { width: 20px; height: 20px; }
@media (max-width: 1023px), (pointer: coarse) {
  .footer-col ul { gap: 0; }
  .footer-col ul a { display: inline-flex; align-items: center; min-height: var(--touch-target-min); min-width: var(--touch-target-min); }
  .footer-legal { gap: 0 var(--space-5); }
  .footer-legal a { display: inline-flex; align-items: center; min-height: var(--touch-target-min); }
  /* Stand-alone text links that were 18-24px tall on phones (they are not inside
     a sentence, so the inline-link exemption does not cover them): "Watch on
     YouTube", "See the reviews", the "Google reviews" line under the badge.
     The extra element selector beats the homepage's inline copies of these. */
  a.vfeat__yt,
  p.credo-rec__cta > a { display: inline-flex; align-items: center; min-height: var(--touch-target-min); }
  /* was display:block (its own line under the rating); stays block-level */
  a.review-badge__live { display: flex; align-items: center; width: fit-content; min-height: var(--touch-target-min); }
}

/* Breadcrumbs live in three places (service-page.css, page-extra.css and the
   town pages' own inline CSS, which uses .breadcrumb-list). `ol a` is one
   element more specific than any of them, so the 44px box wins everywhere. */
.breadcrumb-nav ol a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target-min);
  padding-inline: var(--space-1);
}

/* Lightbox / carousel arrows: never under 44px (the gallery lightbox dropped
   to 40px under 640px). */
.lightbox__prev, .lightbox__next, .lightbox__close,
.sgal__prev, .sgal__next, .wd-gallery__btn, .vcarousel__nav {
  min-width: var(--touch-target-min);
  min-height: var(--touch-target-min);
}

/* Stain & finish carousel dots. The dot stays a 10px circle (painted by
   ::before); on phones and tablets the BUTTON becomes 44px tall. Width is
   flexible rather than a fixed 44px because the carousel has 10 slides and
   10 x 44px = 440px, which would overflow a 390px screen: the buttons share the
   row (never narrower than 28px, never wider than 44px) and wrap to a second
   row only if an editor adds more slides than fit (>12 at 390px). */
@media (max-width: 1023px), (pointer: coarse) {
  .sgal__dots { gap: 0; flex-wrap: wrap; margin-top: var(--space-2); }
  .sgal__dot {
    flex: 1 1 28px;              /* basis 28 so wrap only kicks in past ~12 dots; grows to the 44px cap */
    width: auto;
    min-width: 28px;
    max-width: var(--touch-target-min);
    height: var(--touch-target-min);
    margin: 0;
  }
}

/* ---- 6. Phone numbers never break mid-number ("470-/826-3581") -------------
   Buttons are excluded from the nowrap: /book has a 33-character tel button
   ("Prefer to Talk? Call 470-826-3581") that is 418px wide on one line and
   would push a 390px page 14px sideways. Those wrap at the spaces instead;
   balance prefers the space break over the hyphen break. */
a[href^="tel:"]:not(.btn) { white-space: nowrap; }
.btn[href^="tel:"] { text-wrap: balance; }
/* A number that is plain text (not a link) can only be protected from the
   markup side: wrap it in <span class="nowrap">. e.g. the "...call 470-/826-3581"
   break in the stair-parts CTA band is plain text, so CSS alone cannot reach it. */
.nowrap { white-space: nowrap; }

/* ---- 14. Hover states -------------------------------------------------------
   The hero/fine-print phone links and the footer legal links had no visible
   hover (the legal links are underlined all the time, so hover changed
   nothing). Phone links underline on hover/focus; the legal links keep their
   quiet underline but go full-strength and thicker on hover. */
a[href^="tel:"]:not(.btn):hover,
a[href^="tel:"]:not(.btn):focus-visible { text-decoration: underline; text-underline-offset: 3px; }
.footer-legal a { text-underline-offset: 3px; text-decoration-color: rgba(255, 255, 255, 0.55); }
.footer-legal a:hover,
.footer-legal a:focus-visible { text-decoration-color: currentColor; text-decoration-thickness: 2px; }

/* ---- 7. /gallery "Watch the Work" video carousel (.vcarousel) ---------------
   The markup shipped on /gallery with NO CSS at all: the <ul> track rendered
   as a vertical stack of 630px-tall thumbnails and the arrow buttons as 6px
   browser-default buttons. This is the shared base: a native scroll-snap row
   (touch swipe and momentum for free), ~85% of the track per card up to 360px
   so the next card always peeks in as the cue to swipe.
   The homepage carries its own inline copy of these selectors (a flex track
   with percentage basis) which loads after this file and wins there, so the
   homepage is unchanged; the grid-only properties below are inert on a flex
   container. */
.vcarousel { position: relative; }
.vcarousel__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(85%, 360px);
  gap: var(--space-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;      /* a sideways swipe never turns into browser back/forward */
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--space-1);
  scrollbar-width: thin;
  list-style: none;
  margin: 0;
  padding: var(--space-2) var(--space-1) var(--space-4);
}
.vcarousel__item { min-width: 0; scroll-snap-align: start; }
.vcarousel__item .video-thumb { width: 100%; aspect-ratio: 16 / 9; }
.vcarousel__item .video-thumb__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Arrows: 44px round, white with a navy glyph and a shadow so they read over
   any thumbnail. Hidden on phones, where swiping is the interaction; the track
   is also keyboard-scrollable (it has tabindex=0). -12px keeps them inside the
   viewport at 640-767px, where the page gutter is only 16px. */
.vcarousel__nav {
  position: absolute;
  top: 109px;                          /* middle of a 360px-wide 16:9 thumbnail + track padding */
  transform: translateY(-50%);
  z-index: 2;
  display: none;
  align-items: center;
  justify-content: center;
  width: var(--touch-target-min);
  height: var(--touch-target-min);
  padding: 0;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-full);
  background: var(--color-bg-card);
  color: var(--color-text-heading);
  box-shadow: var(--shadow-md);
  cursor: pointer;
}
.vcarousel__nav svg { width: 22px; height: 22px; }
.vcarousel__nav:hover { background: var(--color-bg-page-alt); }
.vcarousel__nav[disabled] { opacity: 0.35; cursor: default; }
.vcarousel__nav--prev { left: -12px; }
.vcarousel__nav--next { right: -12px; }
@media (min-width: 640px) { .vcarousel__nav { display: inline-flex; } }

/* ---- 8. /about stat row overlap at 320-430px ---------------------------------
   Three 36px figures in three equal columns do not fit a phone: the numbers
   ran into each other. .stat-row is used only on /about (its CSS is inline in
   that page, hence `main` for the extra specificity). One column under 600px;
   fluid figure size above it; anywhere as a last resort so a long number can
   never run into its neighbour. */
@media (max-width: 600px) {
  main .stat-row { grid-template-columns: 1fr; }
}
main .stat-row strong { font-size: clamp(1.5rem, 6vw, 2.25rem); overflow-wrap: anywhere; }

/* ---- 9. Form polish ---------------------------------------------------------
   (a) `margin-inline: auto var(--space-4)` set inline-START to auto and
   inline-END to 16px, so the success tick sat 8px right of centre. It wants
   auto both sides and the gap underneath.
   (b) Busy state. The submit script sets button.disabled while it posts, which
   fell through to .btn--primary:disabled: grey #C8C8C8 with #6B6B6B text =
   3.2:1, and a not-allowed cursor on a button that is working. While busy the
   button stays brand red (a notch darker, so it reads as "pressed"), keeps its
   white label (--red-hover 6.08:1; 5.2:1 even at the .85 opacity here) and
   shows the progress cursor.
   (c) "Request My Free Estimate" is 239px wide at 20px/600 and wrapped to two
   lines (100px tall button) on every phone. Under 480px the submit drops to the
   17px base size with 12px side padding (203px of text in a 220px+ box at 360px),
   and the form wrap gives back 12px a side. */
.form-success__icon { margin: 0 auto var(--space-4); }
.quote-form button[disabled],
.hero-form button[disabled],
.thg-popup__form button[disabled] {
  background: var(--color-cta-bg-hover);
  color: var(--color-cta-text);
  opacity: 0.85;
  cursor: progress;
  box-shadow: none;
}
@media (max-width: 480px) {
  .quote-form-wrap { padding: var(--space-6) var(--space-5); }
  form button[type="submit"].btn--lg {
    font-size: var(--text-base);
    padding-inline: var(--space-3);
    letter-spacing: 0;
  }
}

/* ---- 10. Mobile sticky bar at 320px ------------------------------------------
   "Call Now" wrapped to "Call / Now" and the bar grew from 62px to 86px,
   covering more of the page. Labels never wrap; under 360px the type and the
   icon gap come down a step so all three still fit one row. */
.mobile-cta-bar a { white-space: nowrap; }
@media (max-width: 359px) {
  .mobile-cta-bar a { font-size: var(--text-sm); gap: var(--space-1); }
  .mobile-cta-bar svg { width: 18px; height: 18px; }
}

/* ---- 11. Contrast -------------------------------------------------------------
   (a) Hero proof chips measured 4.05-4.29:1 (white label over a *white* 12%
   tint on a bright part of the photo; the tint lightened exactly what it
   needed to darken). Now a small dark glass pill: a 22% near-black fill plus a
   light blur, still a chip, not a box behind the headline. Measured on the
   home and Kennesaw heroes at 1440px: label contrast on the worst chip goes
   from 3.1:1 (white tint) to 5.6:1; alpha .14 would already clear 4.8:1 on
   those two photos, .22 leaves headroom for a brighter hero photo.
   (b) Review stars: --color-rating-star (gold) is 1.77:1 on the white review
   cards; --color-rating-star-on-light (#AA6600) is 4.56:1. */
.proof-chip {
  background: rgba(var(--near-black-rgb), 0.22);
  border-color: rgba(255, 255, 255, 0.55);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.review-badge__stars,
.review-card__stars { color: var(--color-rating-star-on-light); }
/* (c) The footer's red "Free Estimate" button inherited `.footer-col a { opacity: .9 }`,
   which is meant for the quiet text links: white on red dimmed to 4.2:1 (flagged
   on every page). Buttons are not quiet links; full opacity = 5.01:1. */
.footer-col a.btn { opacity: 1; }

/* ---- 15. 404: primary "call" button is white-on-white ---------------------------
   /404 puts a .btn--outline inside .cta-band__actions, but that container sits
   in a plain light section (not inside .cta-band), so the dark-background
   outline style renders white on white. Targets "a .cta-band__actions that is
   NOT inside a .cta-band" so the real navy bands keep their white button.
   Superseded on a page the moment its markup moves to .btn--outline-light. */
.cta-band__actions:not(.cta-band *) .btn--outline {
  color: var(--color-btn-secondary-text);
  border-color: var(--color-btn-secondary-border);
}
.cta-band__actions:not(.cta-band *) .btn--outline:hover,
.cta-band__actions:not(.cta-band *) .btn--outline:focus-visible {
  background: var(--color-btn-secondary-hover-bg);
  color: var(--color-btn-secondary-hover-text);
}

/* The two 404 buttons use .cta-band__actions, but that class is only styled in
   service-page.css / page-extra.css and the 404 loads neither, so the buttons
   sat 4px apart as inline-blocks. Same rule those files carry. */
.cta-band__actions { display: flex; flex-wrap: wrap; gap: var(--space-4); }

/* /estimate?sent=0 -- a no-JS form post that failed lands here; the notice
   must read as an error, not fine print. */
#sent-fail { color: var(--color-error, #b42318); font-weight: 600; font-size: 1rem; }
