/* =========================================================================
 THE HARDWOOD GUYS — CT & NY — COMPONENT LIBRARY
 Ported from the Georgia design system (/Users/davidbrannan/THG Website/
 design/components.css), adapted red-forward for this site. Depends on
 tokens.css (load it first). Every color/spacing/type value below is a
 var(--semantic-token) reference — no raw hex, no raw color literal, ever.
 That is what makes a retone a one-file change.

 Class names are IDENTICAL to Georgia's — the CT build script emits
 Georgia-shaped markup, so nothing here may be renamed.

 Deltas from the Georgia file (see also header.css for header-specific
 deltas):
   - Georgia's wood accent (--color-accent-wood) does not exist in this
     token set. CT has no third accent color — navy is the sole supporting
     accent — so the one Georgia rule that used wood (.process-step) now
     uses --color-accent-navy.
   - Several Georgia rules used raw rgba(255, 255, 255, N) literals for
     translucent-white overlays on dark surfaces. Replaced with
     color-mix(in srgb, var(--white) N%, transparent) so nothing in this
     file is a bare color literal — everything traces back to a token.
   - Added .btn--inverse (not in Georgia): originally a white-filled /
     deep-red-text button for the header and footer bands, where CT's
     bright red CTA fill was invisible on the old --red-deep surface
     (2.20:1). As of the 2026-08-04 gray-glass restyle those bands are
     --gray-ink instead, red passes there (3.32:1, clears the 3:1 non-text
     minimum — see tokens.css "KNOWN CONTRAST TRAPS"), so
     --color-cta-inverse-bg / --color-cta-inverse-text are now simply
     aliases of the primary CTA tokens and .btn--inverse renders identically
     to .btn--primary. Kept as its own class (rather than removed) so the
     header/footer/footer-cta-btn markup didn't need editing.
 ========================================================================= */

/* ---------------------------------------------------------------------
 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-solid); 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); }
/* Centered variant (homepage v2 bottom estimate section, home_estimate_
   section() in build-site.py) — kept a modifier class rather than an
   inline style so every color/spacing value in this file stays a
   var(--token) reference reachable from one place. */
.section-head--center { text-align: center; margin-inline: auto; }

.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. DARK CONTEXTS ONLY — see header.css
 for why the Georgia light-context overrides were not ported. */
.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);
}

/* Inverse button — CT-only, not in Georgia. Originally needed for the dark
 red header/footer bands, where a bright-red CTA fill was invisible (red on
 red-deep = 2.20:1, a documented trap in tokens.css). Since the 2026-08-04
 gray-glass restyle those bands are --gray-ink and red passes there as a
 button fill (3.32:1), so --color-cta-inverse-bg/-text are now aliases of
 the primary CTA tokens (see tokens.css) and this hover state matches
 .btn--primary's darken-and-shadow treatment instead of its old
 translucent-fill-plus-underline compromise. */
.btn--inverse {
 background: var(--color-cta-inverse-bg);
 color: var(--color-cta-inverse-text);
}
.btn--inverse:hover,
.btn--inverse:focus-visible {
 background: var(--color-cta-bg-hover);
 color: var(--color-cta-inverse-text);
 box-shadow: var(--shadow-md);
}

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

/* ---------------------------------------------------------------------
 3. HEADER
 --------------------------------------------------------------------- */
/* Base layer: sticky + solid. header.css loads LAST and overrides
   position/background to fixed + translucent glass (see its "GLASS HEADER"
   section) — this solid value is what @supports fallback and any
   header.css load failure would leave visible, so it must not be a stale
   token. */
.site-header {
 position: sticky;
 top: 0;
 z-index: var(--z-header);
 background: var(--color-bg-header-solid);
 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;
 /* Top offset matches the fixed header's real height (--header-h, set in
    header.css), not a hardcoded guess. Solid, not glass: a translucent
    blurred panel full of stacked nav text would be harder to read than
    the header bar itself, where it sits over a photo/short label only. */
 inset: var(--header-h) 0 0 0;
 /* TRAP: .site-header's backdrop-filter makes the header the containing
    block for this fixed panel, so `bottom: 0` resolves against the 56px
    header box and the panel collapses to its own padding (~120px) — the
    owner hit exactly this (menu showed one clipped row, 2026-08-06).
    The header is always pinned at the viewport top, so top coordinates
    coincide; only the stretch breaks. Viewport units don't care about
    containing blocks, so pin the height explicitly. dvh (with vh
    fallback) tracks mobile browser chrome collapse. */
 height: calc(100vh - var(--header-h));
 height: calc(100dvh - var(--header-h));
 background: var(--color-bg-header-solid);
 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 color-mix(in srgb, var(--white) 18%, transparent);
}
.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 color-mix(in srgb, var(--white) 25%, transparent);
}
.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.
 --------------------------------------------------------------------- */
.mobile-cta-bar {
 position: fixed;
 left: 0; right: 0; bottom: 0;
 z-index: var(--z-bottom-bar);
 display: grid;
 grid-template-columns: 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-solid);
 color: var(--color-text-inverse);
}
.mobile-cta-bar__call:hover,
.mobile-cta-bar__call: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 */
.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);
 overflow: hidden; /* required so the Ken Burns zoom below never shows a scaled edge */
}
.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,
 color-mix(in srgb, var(--white) 6%, transparent) 0px, color-mix(in srgb, var(--white) 6%, transparent) 2px,
 transparent 2px, transparent 18px),
 var(--color-bg-header-solid);
 z-index: 0;
}

/* ---- Ken Burns: slow drift on the homepage hero photo -----------------
   The real homepage hero photo is <img class="hero__media"> (see
   header.css "HERO PHOTO AS <img>" — no page in site/ uses the
   ".hero-media img" wrapper shape the original brief named; the actual
   built markup is a single <img class="hero__media">). Targeting
   .hero__media directly is the equivalent selector for this codebase.
   header.css's older thg-kenburns keyframe no longer touches this
   selector (see the note in header.css) so this is the only animation
   driving it. */
@keyframes hwg-kenburns {
  from { transform: scale(1.02); }
  to   { transform: scale(1.14) translate(-1.5%, -1%); }
}
.hero__media {
  animation: hwg-kenburns 24s ease-out forwards;
  transform-origin: 60% 40%;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .hero__media {
    animation: none;
  }
}

/* ---- Mobile-only Ken Burns re-tune (owner screenshot, 2026-08-06) ------
   Not a fight with object-position -- proven empirically that object-position
   has ZERO visible effect on this element: the homepage hero photo
   (1800x1200, landscape) sits inside an inherently portrait mobile box, and
   for object-fit:cover, whenever box-aspect < image-aspect (always true for
   a portrait box against a landscape source) the "cover" scale is bound by
   the HEIGHT axis, so the image's full height renders with zero vertical
   crop no matter what the box's actual height is -- there is no vertical
   slack for object-position's Y component to shift. Verified live in the
   browser (375x812 and 390x844) by swapping object-position through
   50%/90% and beyond: pixel-identical. So the fix instead re-tunes the ONE
   thing that already moves pixels here -- the Ken Burns transform -- rather
   than adding a no-op object-position rule. See .hero .hero__media below
   (HOMEPAGE MOBILE HERO FOLD FIX block) for where this keyframe is wired
   up, gated on prefers-reduced-motion the same way hwg-kenburns is above. */
@keyframes hwg-kenburns-mobile {
  from { transform: scale(1.5); }
  to   { transform: scale(1.68); }
}

.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;
 /* The header is now fixed + translucent (header.css), so it no longer
    pushes .hero down in normal flow — the photo intentionally runs full
    bleed under it, but the readable copy must clear it. Top padding gains
    --header-h; the original --space-20 is trimmed down so the total visual
    gap stays in the same ballpark rather than doubling. */
 padding-block: calc(var(--header-h) + var(--space-10)) 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: color-mix(in srgb, var(--white) 12%, transparent);
 border: 1px solid color-mix(in srgb, var(--white) 35%, transparent);
 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); }

/* TV badge (owner-confirmed 2026-08-05) is the one linked chip — wraps its
 own <a> rather than putting the class on the <li> so the whole pill is one
 click target, with the same white/gold treatment every other chip has
 (color: inherit means it stays white, not the default link-blue). */
.proof-chip__link {
 display: inline-flex;
 align-items: center;
 gap: var(--space-2);
 color: inherit;
 text-decoration: none;
}
.proof-chip__link:hover,
.proof-chip__link:focus-visible { text-decoration: underline; }
.proof-chip__sub { opacity: 0.85; font-weight: var(--weight-regular); }

/* SMS affordance (owner-confirmed 2026-08-05, "(203) 290-1510 RECEIVES
 TEXTS") — .btn--secondary (white/navy) rather than .btn--outline: it isn't
 confined to the hero's dark scrim (one instance also sits in the light
 bottom estimate section), and verify-site.py's outline-button check only
 permits .btn--outline inside the hero anyway. */
.hero-sms-link { margin-top: var(--space-4); }
.estimate-sms { margin-top: var(--space-6); text-align: center; }

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

/* ---------------------------------------------------------------------
 6b. HERO SPLIT + LEAD FORM CARD
 Homepage v2 (owner rebuild, 2026-08-05, "model it on Georgia with two
 forms"): a second lead form embedded in the hero, Georgia's pattern —
 copy left, form card right on desktop, card collapses below the copy on
 mobile. Scoped to .hero__content--split (already emitted by build-site.py's
 home_page() markup, previously unstyled — only .hero__proof used the
 split flag) so the other pages sharing plain .hero__content are untouched.
 Mirrors design/../../THG Website (Georgia) index.html's inline hero-form
 styles structurally, but every token below is this project's own CT set —
 nothing here references a Georgia-only variable name.
 --------------------------------------------------------------------- */
.hero__content--split { max-width: var(--container-max); }

/* Mobile order: headline/CTAs first (so the visitor knows what page this
   is), THEN the form card, THEN the proof chips — matches the owner's "one
   above [the fold]" hero-form placement without pushing the H1 down. */
@media (max-width: 1039px) {
 .hero__content--split { display: flex; flex-direction: column; }
 .hero__copy { order: 1; }
 .hero__form-card { order: 2; margin-top: var(--space-6); }
 .hero__proof { order: 3; margin-top: var(--space-6); }
}

@media (min-width: 1040px) {
 .hero__content--split {
 display: grid;
 grid-template-columns: minmax(0, 1fr) 380px;
 grid-template-areas: "copy form" "proof form";
 column-gap: var(--space-10);
 align-items: start;
 }
 .hero__copy { grid-area: copy; max-width: none; }
 .hero__proof { grid-area: proof; max-width: none; margin-top: var(--space-6); }
 .hero__form-card { grid-area: form; align-self: center; }
}

.hero__form-card {
 background: var(--white);
 color: var(--color-text-body);
 border-radius: var(--radius-lg);
 padding: var(--space-6);
 box-shadow: var(--shadow-lg);
}
/* Light-surface card sitting inside .hero, which sets color: inverse
   (white) for everything under it — h1-h4 already resolve to
   --color-text-heading globally (base rule, section 0) regardless of
   .hero's inherited color, and .form-field/.form-hint already carry their
   own explicit colors, so only this card's own lede paragraph needs an
   explicit override here. */
.hero-form__title {
 font-size: var(--text-xl);
 margin: 0 0 var(--space-2);
}
.hero-form__lede {
 font-size: var(--text-sm);
 color: var(--color-text-muted);
 margin: 0 0 var(--space-5);
}
/* Narrow (380px) card — the shared .form-grid's >=640px 2-column rule
   would squeeze every field to ~170px here. Selector specificity (0,2,0)
   beats the plain .form-grid media-query rule (0,1,0) regardless of media
   query, so this applies at every viewport width. */
.hero__form-card .form-grid { grid-template-columns: 1fr; gap: var(--space-4); }

/* ---------------------------------------------------------------------
   HOMEPAGE v2 MOBILE HEIGHT BUDGET (owner rebuild 2026-08-05; revised
   2026-08-05 JOB 3 after owner re-review: "still a jumbled mess in
   Mobile view. Need to not bundle text and photos so much.")
   Two lead forms plus a reviews strip added real, non-removable content
   to "/" — the owner/audit spec's "no removal of facts" rule means the
   375px docHeight budget (target <=8,500px) has to come from tightening
   and RESTRUCTURING presentation, not content. Every selector below is
   scoped to a homepage-only id/class (#why-choose-us, #reviews,
   #estimate, .hero__form-card, #home-services, #home-areas) so no other
   page's spacing changes. JOB 3 also asks for slightly MORE whitespace
   BETWEEN sections even as each section's own interior tightens — the
   jumble was partly adjacent dense blocks touching with no breathing
   room, not only interior density — hence padding-block going UP one
   step (space-10 -> space-12) from the original v2 pass while
   why-choose-us/reviews/home-areas's interiors get restructured (see
   .wc-group, .reviews__list, #home-areas .card-grid below) rather than
   just shrunk. */
@media (max-width: 640px) {
  .hero__form-card { padding: var(--space-5); }
  .hero__form-card .form-grid { gap: var(--space-3); }

  #home-services,
  #reviews,
  #why-choose-us,
  #finishes-we-trust,
  #recent-work,
  #home-areas,
  #estimate {
    padding-block: var(--space-12);
  }

  #reviews .reviews__head { margin-bottom: var(--space-5); }

  #estimate .quote-form-wrap { padding: var(--space-5); }
}

/* -------------------------------------------------------------------
   HOMEPAGE MOBILE HERO FOLD FIX (owner screenshot, 2026-08-06)
   "I want the Get a Free Estimate form to be a little bit lower and not
   the first thing you see... make the initial screen more appealing than
   a cut-off form." At 375x812 the form card started ~520px down, so a
   ~290px sliver of white card was the first thing visible on load.

   Fix has two parts, both scoped to .hero__content--split (homepage only
   — see section 6b above; no other page emits this class) so nothing
   else on the site changes:

   1. Reorder .hero__proof (order 2) ahead of .hero__form-card (order 3),
      so the trust chips render BEFORE the form card in the mobile flex
      column. Desktop's CSS Grid (min-width:1040px) is untouched — this
      only overrides the <=1039px flex .order rule from section 6b.

   2. Size the copy+chips group to the actually-visible band (viewport
      minus the fixed 56px header, --header-h, and the ~64px sticky
      bottom Call/Estimate bar, .mobile-cta-bar, section 4 above) and
      center it there, so the form card's margin-top lands its top edge
      at 100dvh — dvh, not vh, so this tracks mobile browser-chrome
      collapse the same way .site-header__mobile-nav does (section 3).

   Both calc()s share one hand-measured constant, HERO_FOLD_GROUP (460px
   below): the .hero__copy + .hero__proof combined height once tightened
   by the rules just below, verified with getBoundingClientRect at both
   375x812 and 390x844 — not guessed. If this copy is ever edited,
   re-measure and update the constant; max() floors both values so the
   worst case of a stale constant is a smaller push/less-perfect
   centering, never an overlap with the form card. */
@media (max-width: 640px) {
  /* Owner follow-up (2026-08-06): hiding 2 of 4 proof chips (below) frees
     ~110px that would otherwise pull .hero__form-card up with it (see the
     HERO_FOLD_GROUP note ahead of these rules — its calc()s are a fixed
     constant, not a live measurement, so shrinking real content height
     without adding it back moves the form). These margins were tightened
     down from their base values by the original fold-fix pass to buy room
     for the form card; now that the chip declutter frees room back up,
     they're loosened again — a little more generous than the original
     fold-fix numbers, not just restored — so the H1/subcopy/CTA group reads
     as centered and generous rather than cramped. The rest of the freed
     space is added back on .hero__proof below so the total .hero__copy +
     .hero__proof height (and therefore the form card's position) doesn't
     regress. */
  .hero__content--split .hero__eyebrow  { margin-bottom: var(--space-4); }
  .hero__content--split .hero__title    { margin-bottom: var(--space-5); }
  .hero__content--split .hero__subtitle { margin-bottom: var(--space-7); }
  .hero__content--split .hero-ctas      { margin-top: var(--space-5); }
  .hero__content--split .hero-sms-link  { margin-top: var(--space-3); }

  .hero__content--split .hero__proof {
    order: 2;
    margin-top: var(--space-8);
    /* Compensation for the 2 hidden chips — see HERO_FOLD_GROUP comment
       below. Measured/tuned live, not hand-calculated (flex items don't
       margin-collapse in this column, so this adds cleanly). */
    margin-bottom: var(--space-10);
  }
  .hero__content--split .hero__form-card {
    order: 3;
  }

  /* HERO_FOLD_GROUP = 650px (measured via getBoundingClientRect at
     375x812: .hero__proof bottom minus .hero__copy top, after the
     tightening rules above). .hero__content.hero__content--split (both
     classes, specificity 0,2,0) is deliberate here — a single-class
     `.hero__content--split` selector ties in specificity with the plain
     `.hero__content` rule in the MOBILE CLEANUP block below (section 6
     tail), which comes later in the file and would otherwise win the
     cascade on source order alone and silently discard this padding. */
  .hero__content.hero__content--split {
    padding-block: calc(var(--header-h) + ((100vh  - var(--header-h) - 64px - 650px) / 2)) var(--space-10);
    padding-block: calc(var(--header-h) + ((100dvh - var(--header-h) - 64px - 650px) / 2)) var(--space-10);
  }
  /* The +32px nudges the form's top edge a little past the raw 100dvh
     line (rather than landing exactly on it) — headroom against
     measurement rounding, and the "small scroll affordance" the spec
     welcomes (hero photo continuing past the fold). */
  .hero__content--split .hero__form-card {
    margin-top: max(var(--space-6), calc((100vh  - var(--header-h) + 64px - 650px) / 2 + 32px));
    margin-top: max(var(--space-6), calc((100dvh - var(--header-h) + 64px - 650px) / 2 + 32px));
  }

  /* Owner screenshot feedback (2026-08-06, dark-photo era): the hero photo
     "is currently quite dark" on a phone, so this rule originally LIGHTENED
     header.css's flat 0.55/0.45 near-black .hero__scrim down to 0.48/0.36
     for mobile. header.css's .hero__scrim (single class, specificity 0,1,0,
     loads after this file — see this file's own section-3 comment "header.css
     loads LAST and overrides") isn't in this job's edit scope, so the
     override lives here instead, with the extra `.hero` ancestor class
     making it 0,2,0 — high enough specificity to win regardless of cascade
     order.

     Mobile hero SOURCE PHOTO CHANGE (2026-08-06, this job): mobile now loads
     assets/photos/hero-mobile.jpg via <picture><source media="(max-width:
     640px)"> in build-site.py's home_page() — a bright, sunlit pale-oak
     floor crop, replacing the dark fireplace-room photo this rule was
     tuned for. Desktop is untouched (still the dark photo, still 0.55/0.45
     via header.css) so only this mobile block changes. Over the sunlit
     patches of the new photo the old 0.48/0.36 washes the white H1 out —
     verified live at 375x812/390x844. Darkened well past even header.css's
     base 0.55/0.45 to hold up against the brightest part of the image.
     design/contrast-check.py only proves claims about flat token-to-token
     pairs (e.g. white-on-gray-ink); it cannot score white text over a
     photograph, so there is no automated gate for this value — checked by
     eye against the rendered screenshot instead. */
  .hero .hero__scrim {
    /* Graduated, not flat: heaviest behind the H1/subcopy at the top where
       white text sits over the photo's brightest sunlit patches, easing off
       toward the bottom so the floor and the light coming across it stay
       visible. Flat 0.66/0.60 held contrast but muted the whole photo — the
       owner's standing note is to brighten the image rather than sit text on
       a dark slab ([[thg-design-preferences]]). */
    background: linear-gradient(rgba(var(--near-black-rgb), 0.66) 0%,
                                rgba(var(--near-black-rgb), 0.62) 45%,
                                rgba(var(--near-black-rgb), 0.4) 100%);
  }

  /* Trust-chip declutter (owner screenshot, 2026-08-06 follow-up): 4
     full-width stacked pills — "10,000+ projects completed", the HGTV
     badge, "Free estimates", "Serving CT & NY" — read as a list, not a
     trust signal, and ate ~300px. "Free estimates" and "Serving CT & NY"
     are both redundant on this screen: the red CTA already says "Get a
     Free Estimate" and .hero__eyebrow already says "SERVING CONNECTICUT &
     NEW YORK". CSS-only per spec — build-site.py still emits all 4 <li>,
     desktop (min-width:1040px grid) is untouched since this whole rule is
     inside the 640px query. nth-child rather than a new class so no markup
     edit is needed. */
  .hero__proof .proof-chip:nth-child(n + 3) {
    display: none;
  }

  /* HGTV badge line-wrap (same screenshot): "As Seen on HGTV's Property
     Brothers" and "— 3 homes featured on national TV" are two separate
     flex items (icon, title text, .proof-chip__sub) inside
     .proof-chip__link's nowrap flex row — on a narrow chip they were each
     shrinking and wrapping independently, reading as two ragged columns
     instead of one badge. Allowing the row to wrap and forcing the sub
     line to its own row (flex-basis:100%) fixes that; the left padding
     lines its text up under the title instead of under the icon. Wording
     is untouched — only how it flows. */
  .hero__proof .proof-chip__link {
    flex-wrap: wrap;
    align-items: flex-start;
  }
  .hero__proof .proof-chip__sub {
    flex-basis: 100%;
    margin-top: 2px;
    padding-left: calc(18px + var(--space-2));
  }

  /* Hero photo framing (same screenshot): "the photo shows the wrong part
     of the room" — dark ceiling beams and a stone fireplace dominate the
     first screen instead of floor, on a flooring company's homepage.
     object-position cannot fix this: verified live (375x812 and 390x844,
     swapping object-position through several values including the
     originally-suggested 50%/78-88%) that it has ZERO visible effect here.
     Why: the real photo is 1800x1200 (landscape, 1.5 aspect); object-fit:
     cover inside a portrait mobile box is always height-bound (box-aspect
     < image-aspect whenever the box is taller than wide), so the full
     image height renders with zero vertical crop no matter the box's
     height — there's no vertical slack for object-position's Y component
     to shift. See the hwg-kenburns-mobile keyframe (section 6 top) for the
     full derivation. The fix instead re-tunes the one thing that DOES move
     pixels here, the existing Ken Burns transform: a bigger mobile-only
     scale (1.5 → 1.68 vs desktop's 1.02 → 1.14) around a transform-origin
     pushed below the element's own bottom edge (50% 132%) pulls the
     floor into the visible band and pushes the ceiling out of it. The
     static transform below is the resting value (unconditional — applies
     even under prefers-reduced-motion, so those users still get correct
     framing, just without the drift); the animation is layered on top of
     it, gated separately on prefers-reduced-motion so it never fights the
     .hero__media reduced-motion guard above (section 6 top). object-fit:
     cover and the Ken Burns animation itself are both left exactly as they
     were — only transform-origin/transform/animation-name change here. */
  /* REVERTED 2026-08-06 — the scale/origin approach above BROKE the hero:
     .hero__media and its <img> BOTH carry the transform, so scale 1.68
     compounds to ~2.8x, and origin 50% 132% then threw the photo ~1,415px
     above the hero box. Measured live: img rect top -1415, height 2649 in
     a 1577px hero — i.e. a black hero with no photo at all. The framing
     idea was sound but cannot be done with transforms on this nested pair.
     The real fix is a mobile-specific, floor-dominant SOURCE image via
     <picture>/srcset (this photo is a tall room whose middle band is
     ceiling); logged in OWNER-ANSWERS.md. Desktop framing is unaffected
     either way — this block is mobile-only. */
}

/* Homepage hero photo caption -- ported from The Hardwood Guys Georgia
   (design/components.css section 6, 2026-08-14) as part of the gallery
   admin port. Base rule only: hidden by default, no breakpoint override
   to show it. Georgia's show/position rules for this class live in a
   page-local <style> block in ITS site/index.html (a hand-authored static
   file, not a build-site.py template) keyed to a --hero-banner-h variable
   that only exists on that page's own narrow mobile "banner" hero layout.
   This site's hero is architecturally different -- hero__content--split
   runs the same two-column layout at every width, with its own
   extensively owner-tuned Ken Burns mobile framing above -- so that
   positioning was NOT ported; wiring it in would mean designing new
   placement rules against a hero this file's own history shows is easy to
   break, which is out of scope here. The class stays default-hidden
   (inert) until a developer adds matching markup (build-site.py) and
   show/position CSS for this site's actual hero shape. See
   functions/hero-photo.js and site/gallery.js for the working half of this
   feature (the hero photo itself already swaps via the /hero-photo
   redirect with no JS or caption markup required). */
.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. Standing rule (design preferences memory): 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);
}
.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 a dark surface -- 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); }

.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);
}

/* Reviews carousel (JOB 3, owner mobile feedback 2026-08-05): below 768px
   (the breakpoint the 3-up grid rule already turns on at) the 3 stacked
   review-cards ran ~1,132px of vertical scroll for one testimonial at a
   time. Horizontal scroll-snap instead — one card ~85vw (a visible sliver
   of the next card is the swipe affordance) — cuts that to roughly the
   height of a single card. Bleeds to the viewport edge past .container's
   own padding via the negative margin / matching padding-inline pair, so
   the peeking next card isn't clipped by the container. */
@media (max-width: 767px) {
 .reviews__list {
 display: flex;
 grid-template-columns: none;
 overflow-x: auto;
 scroll-snap-type: x mandatory;
 gap: var(--space-4);
 margin-inline: calc(-1 * var(--container-pad));
 padding-inline: var(--container-pad);
 padding-bottom: var(--space-2);
 }
 .review-card {
 flex: 0 0 85%;
 scroll-snap-align: start;
 }
}
.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); }

.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; }
/* build-site.py's lead_form_html() (the notes textarea + submit row)
   emits this exact class — was missing here, which silently left both
   fields squeezed into the left grid column at >=640px on every page that
   uses the form (quote/contact/homepage). Same rule as --span2. */
.form-field--full { 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,
 color-mix(in srgb, var(--white) 8%, transparent) 0px, color-mix(in srgb, var(--white) 8%, transparent) 2px,
 transparent 2px, transparent 16px),
 var(--color-bg-header-solid);
}
.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);
 /* Georgia used --color-accent-wood here (a third, non-CTA accent color).
    CT has no wood accent — navy is the sole supporting color on this
    red-forward site — so this uses --color-accent-navy instead. Still
    never red: red stays reserved exclusively for clickable CTAs so it
    never loses signal strength through decorative reuse. */
 border-top: 3px solid var(--color-accent-navy);
}
.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; }

@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);
 /* hairline divider: the footer sits directly under the same-family gray
    CTA band on every page; without a visible seam the two surfaces read as
    one oversized void (owner screenshot 2026-08-05) */
 border-top: 1px solid rgba(255, 255, 255, 0.12);
 padding-block: var(--space-12) var(--space-8);
}
.footer-grid {
 display: grid;
 gap: var(--space-10);
 grid-template-columns: 1fr;
 padding-bottom: var(--space-10);
 border-bottom: 1px solid color-mix(in srgb, var(--white) 20%, transparent);
}
/* 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); }
/* Owner-confirmed 2026-08-05 hours (OWNER-ANSWERS.md "Stamford-audit
   round"), sitewide next to the phone number. Smaller + tighter margin
   than .footer-brand p's default so it reads as a label under the phone
   link, not another paragraph of body copy. Two classes so specificity
   (0,2,0) beats .footer-brand p's (0,1,1) without !important. */
.footer-brand .footer-hours { font-size: var(--text-sm); margin-top: var(--space-1); }
.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 color-mix(in srgb, var(--white) 40%, transparent);
 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 1fr 1fr 1fr; }
 .footer-bottom { flex-direction: row; align-items: center; justify-content: space-between; }
}

/* Footer tightening (JOB 3, owner mobile feedback 2026-08-05): below the
   existing 640px breakpoint the 4 footer columns (brand, Services, Areas
   We Serve, Company) stacked single-column, ~1,746px on its own — nearly
   a sixth of the whole page. 2-col grid brought forward from 640px to
   375px (brand+Services / Areas+Company, same 4 blocks, nothing removed)
   plus smaller type/gaps gets this under the ~1,200px target. */
@media (max-width: 639px) {
 .footer-grid {
 grid-template-columns: repeat(2, 1fr);
 gap: var(--space-5) var(--space-4);
 padding-bottom: var(--space-6);
 }
 .site-footer { padding-block: var(--space-8) var(--space-6); }
 .footer-col-title { font-size: var(--text-xs); margin-bottom: var(--space-2); }
 .footer-col ul { gap: var(--space-2); }
 .footer-col a { font-size: var(--text-sm); }
 .footer-brand p { font-size: var(--text-sm); margin-top: var(--space-2); }
 .footer-brand .site-header__phone { font-size: var(--text-base); margin-top: var(--space-3); }
 .footer-bottom { margin-top: var(--space-5); gap: var(--space-2); }
}

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

/* =========================================================================
 16. PAGE-LEVEL COMPONENTS
 Added after the initial Georgia port: these classes style the CT build
 script's page-level markup (page headers, card grids, prose/article
 bodies, galleries, the navy CTA band, blog list/tag/pager furniture, the
 404 template, and a text-only header logo fallback). Georgia kept the
 equivalent rules in per-page inline <style> blocks, so nothing to port
 existed in components.css or header-final.css; these are modeled on
 /Users/davidbrannan/Flooring Guys Website/design/components.css (Florida),
 with every token translated from Florida's naming (--sp-N / --fs-* /
 --lh-* / --radius / --shadow-card / --color-bg-alt / --color-bg-band /
 --color-text-accent / --color-error) to this system's Georgia-style
 semantic names (--space-N / --text-* / --leading-* / --radius-base /
 --shadow-sm+--shadow-md / --color-bg-page-alt / --color-bg-accent-navy /
 --color-text-heading+--color-text-muted / --color-text-error).

 Two classes were intentionally NOT re-added here because they already
 exist, unchanged, earlier in this file and already match the request
 exactly:
   - .site-header__logo-tag (section 3, HEADER) is already --text-xs,
     opacity .85, color: var(--color-text-inverse).
   - .cta-band's mobile width-fix (.cta-band__inner, .cta-band
     .hero__actions) already lives in header.css. The band styled below
     reuses the existing .hero__actions button-row class rather than
     inventing a second button-row class, so there is exactly one
     definition of "the CTA band's button row" across both files.
 ========================================================================= */

/* ---- page header band ---------------------------------------------- */
/* Dark --gray-ink surface, its own dedicated token (--color-bg-page-hero)
   rather than reusing --color-bg-header — the header is now translucent
   glass and this band is not, so they need to be independently swappable.
   --color-text-inverse on --color-bg-page-hero is 16.64:1 AAA (see
   tokens.css). This band is also the first element under the now-fixed
   header on 39 of the site's 40 pages, so its top padding gains
   --header-h — see the .hero__content comment above for why. */
.page-hero {
 background: var(--color-bg-page-hero);
 color: var(--color-text-inverse);
 padding-block: calc(var(--header-h) + var(--space-8)) var(--space-8);
}
/* h1's global rule (section 0) sets color: var(--color-text-heading),
   which is --navy — near-invisible on this band's own --gray-ink
   background, so it must be overridden here or the title would be
   unreadable. */
.page-hero h1 {
 color: var(--color-text-inverse);
 margin-bottom: var(--space-2);
}

.breadcrumbs {
 margin: 0;
 font-size: var(--text-sm);
 color: var(--color-text-inverse);
 opacity: 0.85;
}
.breadcrumbs a {
 color: var(--color-text-inverse);
 text-decoration: underline;
 opacity: 0.85;
}
.breadcrumbs a:hover,
.breadcrumbs a:focus-visible { opacity: 1; }

/* Town landing pages (JOB 2, TOWN-PAGES-SPEC.md 2026-08-05): a licensed
   stock location photo sits directly under the page-hero band, full-bleed.
   These are Wikimedia Commons CC BY / CC BY-SA photos
   (assets/incoming-stock/LICENSES.md) — the license requires attribution,
   so the figcaption carries a visible, on-page credit line rather than
   relying on alt text or a buried credits page. ink-muted on white is
   5.33:1 (AA, see design/contrast-check.py). */
.town-hero-photo {
 margin: 0;
}
.town-hero-photo img {
 width: 100%;
 max-height: 420px;
 object-fit: cover;
 display: block;
}
.town-hero-photo figcaption {
 font-size: var(--text-xs);
 color: var(--color-text-muted);
 text-align: right;
 padding: var(--space-2) var(--container-pad);
}
.town-hero-photo figcaption a {
 color: inherit;
}
@media (max-width: 640px) {
 .town-hero-photo img { max-height: 220px; }
}

/* "Nearby towns" cross-link list at the bottom of each town page's prose,
   above cta_band() — a plain link row, not another gallery-grid or
   town-list (those imply "we list every town we serve here", which this
   isn't; it's just 2-3 sibling links). */
.nearby-towns {
 display: flex;
 flex-wrap: wrap;
 gap: var(--space-2) var(--space-4);
 padding: 0;
 margin: var(--space-2) 0 0;
 list-style: none;
}
.nearby-towns a {
 font-weight: var(--weight-semibold);
}

/* Small uppercase eyebrow for LIGHT sections (unlike .hero__eyebrow, which
   is white-on-dark-scrim). Deliberately not colored --color-cta-bg like
   the existing .eyebrow utility (section 15): the brief for this class
   named only --color-text-muted or --color-text-heading as options, to
   keep bright red reserved for clickable CTAs. --color-text-heading
   (--navy as of the 2026-08-04 gray-glass restyle — 13.60:1 AAA on white,
   12.58:1 AAA on off-white) is used here, without spending the brand's one
   "this is clickable" color on a label. */
.section-kicker {
 display: block;
 font-size: var(--text-sm);
 font-weight: var(--weight-bold);
 letter-spacing: var(--tracking-wide);
 text-transform: uppercase;
 color: var(--color-text-heading);
 margin-bottom: var(--space-3);
}

/* ---- card grid -------------------------------------------------------- */
.card-grid {
 display: grid;
 gap: var(--space-6);
 grid-template-columns: 1fr;
}
@media (min-width: 768px) {
 .card-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
 .card-grid { grid-template-columns: repeat(3, 1fr); }
}

/* Homepage-only service-card grid ("Hardwood Floors, Start to Finish"
   section, home_page() in build-site.py): 2-up on mobile instead of the
   generic .card-grid's single column, so 6 service cards don't stack into
   thousands of px of dead scroll on a 375px phone. Scoped to
   .home-services-grid (an extra class on the same element, alongside
   .card-grid) rather than changing .card-grid's mobile behavior globally —
   .card-grid is also reused by .post-list for blog cards, which are
   text-heavy excerpts that need the full-width single column below 768px. */
@media (max-width: 640px) {
 .home-services-grid {
 grid-template-columns: repeat(2, 1fr);
 gap: var(--space-3);
 }
 .home-services-grid .card img {
 aspect-ratio: 4 / 3;
 }
 .home-services-grid .card-body {
 padding: var(--space-3);
 gap: var(--space-1);
 }
 .home-services-grid .card-body h3 {
 font-size: var(--text-sm);
 font-weight: var(--weight-semibold);
 }
 /* min-height (not padding, which would visibly enlarge the row) keeps
    the tap target at the WCAG 44px floor without changing the compact
    look — .card-link is already display:inline-flex/align-items:center,
    so the extra box height centers the text rather than pushing it. */
 .home-services-grid .card-link {
 font-size: var(--text-xs);
 min-height: 44px;
 }
}

/* Areas We Serve chip grid (JOB 3, owner mobile feedback 2026-08-05): 7
   full photo-less cards (h3 + generic one-line sentence + "Floors in X"
   link) stacked single-column ran ~1,381px for what is really just 7
   links to county pages. 2-up compact chips instead — the boilerplate
   sentence (identical copy per card except the county name — not a
   unique fact) is hidden at this width rather than deleted from the
   markup, so it's still there for a larger viewport/reader; the heading
   and link alone are enough to identify and tap a chip. Card's own
   `overflow:hidden` (section 12 above) still clips the corners, so no
   radius override is needed here. */
@media (max-width: 640px) {
 /* Chips need less top/bottom air than a text section — own padding
    scaled back a step from the sitewide space-12 mobile default. Same
    (1,0,0) specificity as that shared rule, so source order (this block
    comes later in the file) is what wins here — no !important needed. */
 #home-areas { padding-block: var(--space-8); }
 #home-areas .card-grid {
 grid-template-columns: repeat(2, 1fr);
 gap: var(--space-2);
 }
 #home-areas .card-body {
 padding: var(--space-3);
 gap: var(--space-1);
 }
 #home-areas .card-body h3 {
 font-size: var(--text-sm);
 }
 #home-areas .card-body p {
 display: none;
 }
 #home-areas .card-link {
 font-size: var(--text-xs);
 min-height: 44px;
 }
}

.card {
 display: flex;
 flex-direction: column;
 background: var(--color-bg-card);
 border-radius: var(--radius-base);
 box-shadow: var(--shadow-sm);
 overflow: hidden;
 transition: box-shadow var(--motion-base) var(--motion-ease);
}
.card:hover { box-shadow: var(--shadow-md); }
.card img {
 width: 100%;
 aspect-ratio: 3 / 2;
 object-fit: cover;
 display: block;
}
.card-body {
 display: flex;
 flex-direction: column;
 gap: var(--space-2);
 flex: 1;
 padding: var(--space-5);
}
.card-body h3 { font-size: var(--text-lg); }
.card-body p { color: var(--color-text-muted); flex: 1; }
.card-link {
 display: inline-flex;
 align-items: center;
 gap: var(--space-1);
 font-weight: var(--weight-semibold);
 font-size: var(--text-sm);
 color: var(--color-text-link);
}
.card-link:hover,
.card-link:focus-visible { color: var(--color-text-link-hover); text-decoration: underline; }
/* Arrow glyph is decorative-only text, not an icon font or image, so no
   token is needed for it. */
.card-link::after { content: "\2192"; }

/* ---- prose (generated article / service-page bodies) ------------------ */
.prose {
 max-width: 70ch;
 line-height: var(--leading-normal);
}
.prose h2,
.prose h3 {
 color: var(--color-text-heading);
 margin-top: var(--space-8);
 margin-bottom: var(--space-4);
}
.prose h2:first-child,
.prose h3:first-child { margin-top: 0; }
.prose p { margin: 0 0 var(--space-4); }
.prose ul,
.prose ol {
 margin: 0 0 var(--space-4);
 padding-left: var(--space-6);
}
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li { margin-bottom: var(--space-2); }
.prose a {
 color: var(--color-text-link);
 text-decoration: underline;
}
.prose a:hover,
.prose a:focus-visible { color: var(--color-text-link-hover); }

/* "Why Choose Us" highlight chips (home_content_section() in
   build-site.py) — real archived label text ("Quick Installs", etc.) in a
   <ul class="highlight-chips"> that, unstyled, fell back to .prose's plain
   disc-bulleted vertical list. Pills instead, matching the hero's
   .proof-chip visual language but light-surface (this section is
   .section--alt/off-white, not the dark hero) — .color-bg-card (white)
   for contrast against that off-white section, same navy-on-white pairing
   already verified elsewhere (13.60:1 AAA, tokens.css). Two selector
   classes so specificity beats the plain .prose ul/.prose li rules above.*/
.prose .highlight-chips {
 display: flex;
 flex-wrap: wrap;
 gap: var(--space-2);
 list-style: none;
 padding-left: 0;
 margin-bottom: var(--space-5);
}
.prose .highlight-chips li {
 margin-bottom: 0;
 padding: var(--space-1) var(--space-4);
 border-radius: var(--radius-full);
 background: var(--color-bg-card);
 border: 1px solid var(--color-border-default);
 font-size: var(--text-sm);
 font-weight: var(--weight-medium);
 color: var(--color-text-heading);
}
.prose img {
 max-width: 100%;
 border-radius: var(--radius-base);
 margin: var(--space-5) 0;
}

/* "Why Choose Us" accordion (JOB 3, owner mobile feedback 2026-08-05).
   home_content_section() (build-site.py) wraps each former <h3> group —
   the Property Brothers blurb, the 3 finish options together, each of the
   3 project lists, and the FAQ — in a native <details class="wc-group">,
   summary text taken verbatim from that group's old heading. Mobile
   default: native <details> behavior, closed. Desktop (>=641px): forced
   open, CSS-only — details:not([open]) > :not(summary) { display:none }
   is a normal UA-stylesheet rule an author !important overrides like any
   other, so the body is unconditionally shown and the summary's own
   click affordance is disabled (pointer-events:none) rather than actually
   toggled, per JOB 3's spec: "prefer CSS-only". No JS needed or used. */
.wc-groups {
 display: flex;
 flex-direction: column;
 gap: var(--space-2);
 margin-bottom: var(--space-4);
}
.wc-group {
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-base);
 background: var(--color-bg-card);
}
.wc-group summary {
 display: flex;
 align-items: center;
 justify-content: space-between;
 gap: var(--space-3);
 padding: var(--space-3) var(--space-4);
 font-family: var(--font-heading);
 font-weight: var(--weight-semibold);
 color: var(--color-text-heading);
 cursor: pointer;
 list-style: none; /* hide default marker triangle — Firefox */
}
.wc-group summary::-webkit-details-marker { display: none; } /* Chrome/Safari */
.wc-group__chevron {
 width: 20px;
 height: 20px;
 flex: none;
 color: var(--color-text-muted);
 transition: transform var(--motion-base) var(--motion-ease);
}
.wc-group[open] > summary .wc-group__chevron { transform: rotate(180deg); }
.wc-group__body {
 padding: 0 var(--space-4) var(--space-4);
}
/* h3 since 2026-08-12 (heading-hierarchy fix — these were h4 and skipped a
   level under the section's h2). h4 kept in the selector so the rule still
   matches if any older markup lingers; the visual size is unchanged. */
.wc-group__body h3,
.wc-group__body h4 { margin-top: var(--space-3); font-size: 1.05rem; }
.wc-group__body h3:first-child,
.wc-group__body h4:first-child { margin-top: 0; }
.wc-group__body p:last-child { margin-bottom: 0; }

@media (min-width: 641px) {
 .wc-group summary {
 cursor: default;
 pointer-events: none;
 }
 .wc-group__chevron { display: none; }
 .wc-group > .wc-group__body {
 display: block !important;
 }
}

/* Two-column body + photo rail, ≥1024px; stacks to a single column below
   that (the rail simply falls after the prose column in source order). */
.prose-layout {
 display: grid;
 gap: var(--space-8);
 grid-template-columns: 1fr;
}
@media (min-width: 1024px) {
 .prose-layout { grid-template-columns: 1fr 320px; align-items: start; }
}
.photo-rail {
 display: grid;
 gap: var(--space-4);
}
.photo-rail img {
 width: 100%;
 display: block;
 border-radius: var(--radius-base);
 box-shadow: var(--shadow-sm);
}
/* The rail's real <h2> label (build-site.py's page_images_html()) is only
   ever shown alongside the mobile grid treatment below — at every wider
   width the rail is still the narrow, unlabeled sidebar list it always
   was ("desktop rail unchanged", mobile audit 2026-08-05). */
.photo-rail__title {
 display: none;
}
/* Mobile audit (2026-08-05, systemic finding): below 640px .prose-layout
   already collapses to one column (see above), which turned the photo
   rail into an uncaptioned wall of full-width photos stacked after the
   article text — the exact "photo wall" complaint pattern found on
   /services-1, /bleached-oak-hardwood-1 and others. Fixed the same way the
   homepage/gallery photo walls were fixed: a labeled 2-column grid of
   square tiles instead of a single full-width column. Scoped to
   .photo-rail--gallery (the multi-photo rail built by page_images_html())
   rather than bare .photo-rail, which product_page() also uses for a
   single swatch image that must stay full-width, not squeezed into a
   half-width grid cell. */
@media (max-width: 640px) {
 .photo-rail--gallery {
 grid-template-columns: repeat(2, 1fr);
 gap: var(--space-2);
 }
 .photo-rail--gallery img {
 aspect-ratio: 1 / 1;
 object-fit: cover;
 }
 .photo-rail--gallery .photo-rail__title {
 display: block;
 grid-column: 1 / -1;
 font-size: var(--text-lg);
 font-weight: var(--weight-semibold);
 color: var(--color-text-heading);
 margin: 0 0 var(--space-2);
 }
}

/* Inline comparison/detail photos placed directly under their own heading
   in .prose (mobile audit 2026-08-05, /hardwoodinstallations-1 — see
   INLINE_IMAGES in build-site.py). Full-width in the article column at
   every size; .prose img already handles max-width/radius, this only
   controls the figure's own spacing so it matches the rest of the prose
   rhythm instead of the browser's default figure margin. */
.prose-figure {
 margin: var(--space-5) 0;
}
.prose-figure img {
 margin: 0;
}

/* ---- gallery ------------------------------------------------------- */
.gallery-grid {
 display: grid;
 gap: var(--space-4);
 grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.gallery-grid img {
 width: 100%;
 display: block;
 aspect-ratio: 4 / 3;
 object-fit: cover;
 border-radius: var(--radius-sm);
}

/* Below 1024px the auto-fill/minmax(260px) layout above only ever fits one
   column at phone/small-tablet widths, which is what turned the homepage
   into a 13,600px scroll of full-width stacked photos. Fixed 2/3-column
   grids of square tiles fix that site-wide — this is intentionally global
   (not scoped to one page): /gallery's 146 photos and every area page's
   "Recent Work" gallery all get the same fix, and there's no reason a
   phone visitor to any of those pages should see the same one-column
   stack. Desktop (≥1024px) keeps the existing auto-fill/4:3 behavior. */
@media (max-width: 640px) {
 .gallery-grid {
 grid-template-columns: repeat(2, 1fr);
 gap: var(--space-2);
 }
 .gallery-grid img {
 aspect-ratio: 1 / 1;
 }
}
@media (min-width: 641px) and (max-width: 1023px) {
 .gallery-grid {
 grid-template-columns: repeat(3, 1fr);
 gap: var(--space-2);
 }
 .gallery-grid img {
 aspect-ratio: 1 / 1;
 }
}

/* Homepage-only "Recent Work" strip (home_gallery_section() in
   build-site.py, scoped via the extra .home-gallery class it adds
   alongside .gallery-grid): cap visible tiles at 6 on mobile so the strip
   previews rather than dumping the full 12-photo set above the "See the
   Full Gallery" button. /gallery and area-page galleries do NOT get this
   class, so their nth-child siblings are unaffected — all photos there
   stay visible on every screen size. */
@media (max-width: 640px) {
 .home-gallery img:nth-child(n+7) {
 display: none;
 }
}

/* ---- homepage "Latest From Instagram" carousel (home_instagram_section()
   in build-site.py, "/" only, owner connected Behold 2026-08-06) — took
   the #recent-work slot the static .home-gallery grid above used to hold.
   ig-feed.js renders the cards client-side (mediaUrls are expiring
   Instagram CDN links, never baked into this markup), so everything here
   has to work for an empty .ig-carousel container at first paint too.

   Horizontal scroll-snap row, same mechanism as .reviews__list's mobile
   carousel above, but ALWAYS on (owner asked for a carousel on both mobile
   and desktop, not just a mobile fallback) — so this isn't wrapped in a
   max-width media query the way .reviews__list is. */
.ig-carousel {
 display: flex;
 gap: var(--space-4);
 overflow-x: auto;
 scroll-snap-type: x mandatory;
 -webkit-overflow-scrolling: touch;
 scrollbar-width: none;                 /* Firefox */
 -ms-overflow-style: none;               /* legacy Edge */
 margin-top: var(--space-6);
 margin-inline: calc(-1 * var(--container-pad));
 padding-inline: var(--container-pad);
 padding-bottom: var(--space-2);
}
.ig-carousel::-webkit-scrollbar { display: none; }  /* Chrome/Safari */

.ig-card {
 flex: 0 0 280px;
 scroll-snap-align: start;
 display: block;
 background: var(--color-bg-card);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-sm);       /* matches .gallery-grid img */
 box-shadow: var(--shadow-sm);
 overflow: hidden;                      /* clips img + caption to the radius */
 text-decoration: none;
}
.ig-card__img {
 display: block;
 width: 100%;
 aspect-ratio: 1 / 1;
 object-fit: cover;
}
.ig-card__caption {
 display: flex;
 align-items: center;
 gap: var(--space-2);
 padding: var(--space-2) var(--space-3);
 color: var(--color-text-muted);        /* 5.33:1 AA on --color-bg-card */
 font-size: var(--text-xs);
}
.ig-card__glyph {
 width: 16px;
 height: 16px;
 flex: none;
}
.ig-card__date {
 font-weight: var(--weight-medium);
}

/* Honest-failure fallback (ig-feed.js: fetch fails, times out at 6s, or
   the feed returns 0 posts) — a single centered card, not an empty strip. */
.ig-card--fallback {
 flex: 0 0 100%;
 scroll-snap-align: start;
 max-width: 480px;
 margin-inline: auto;
 padding: var(--space-8) var(--space-6);
 text-align: center;
 font-weight: var(--weight-semibold);
 color: var(--color-text-link);         /* 5.29:1 AA on white */
}
.ig-card--fallback:hover,
.ig-card--fallback:focus-visible {
 color: var(--color-text-link-hover);
 text-decoration: underline;
}

.ig-noscript {
 margin-top: var(--space-6);
 text-align: center;
}
.ig-noscript a {
 color: var(--color-text-link);
 font-weight: var(--weight-semibold);
}

@media (max-width: 640px) {
 .ig-card { flex-basis: 72vw; }
 .ig-card--fallback { flex-basis: 100%; }
}

/* ---- homepage "rating band" + "Finishes We Trust" tiles (owner-directed
   2026-08-05: the stretch between "Why Choose Us" and "Recent Work" read
   too text-heavy — these two strips break it up visually). Both sit
   between home_content_section() and home_gallery_section() in
   build-site.py (home_rating_band() / home_finishes_section()). --------- */

/* Full-bleed dark band so the gold stars have a legal home: gold on
   --gray-ink is 9.39:1 AAA (see tokens.css "GRAY-GLASS SURFACE" note).
   Gold on white/--color-bg-page-alt is the documented 1.77:1 trap, so this
   band deliberately does NOT use --color-bg-page-alt even though that was
   one of the two options offered — gray-ink is the only one of the two
   that keeps the stars legible without switching them to an icon fill. */
.rating-band {
 background: var(--color-bg-header-solid);
}
.rating-band__inner {
 display: flex;
 flex-wrap: wrap;
 align-items: center;
 justify-content: center;
 gap: var(--space-2) var(--space-4);
 padding-block: var(--space-5);
 text-align: center;
}
.rating-band__stars {
 color: var(--color-rating-star);
 font-size: 1.25rem;
 letter-spacing: 0.12em;
 line-height: 1;
}
.rating-band__text {
 margin: 0;
 color: var(--color-text-inverse);
 font-weight: var(--weight-semibold);
 font-size: var(--text-base);
}
/* White + underline, not blue — blue text on --gray-ink is 3.15:1 and
   fails, same reason --color-nav-link never swaps color on hover here. */
.rating-band__link {
 margin-left: var(--space-3);
 color: var(--color-text-inverse);
 font-weight: var(--weight-medium);
 font-size: var(--text-sm);
 text-decoration: underline;
}
.rating-band__link:hover,
.rating-band__link:focus-visible {
 text-decoration-thickness: 2px;
}
@media (max-width: 480px) {
 .rating-band__inner {
 flex-direction: column;
 gap: var(--space-2);
 padding-block: var(--space-4);
 }
 .rating-band__link {
 margin-left: 0;
 }
}

/* Product tiles, photo + name + one-line caption. Desktop: N-up grid, image
   boxed on --color-bg-page-alt so mismatched product-shot aspect ratios (a
   wide Bona Traffic HD jobsite crop, a portrait PoloPlaz pail) sit on one
   even backdrop instead of jumping sizes. Mobile: collapses to stacked
   horizontal rows (image left, text right) rather than narrow columns —
   a 33vw-wide column left almost no room for long finish names to wrap
   without ballooning the section's height, and this page has a hard
   mobile docHeight budget. */
.finish-tiles {
 display: grid;
 grid-template-columns: repeat(3, 1fr);
 gap: var(--space-6);
 margin-top: var(--space-6);
}
/* 2026-08-05: FINISHES dropped to exactly 2 tiles (PolyWhey removed per
   owner — "No need for polywhey. Stick with Bona"). Left on the plain
   3-column grid, 2 tiles sit flush left with a bare empty 3rd column,
   which reads broken/unfinished. This modifier (applied by build-site.py's
   home_finishes_section() whenever len(FINISHES) == 2) switches to a
   2-column grid sized to the tile content and centers it in the section,
   so the pair reads as an intentional layout rather than a gap where a
   3rd tile should be. */
.finish-tiles--pair {
 grid-template-columns: repeat(2, minmax(0, 280px));
 justify-content: center;
 max-width: 640px;
 margin-inline: auto;
}
.finish-tile {
 text-align: center;
}
.finish-tile__img-wrap {
 aspect-ratio: 1 / 1;
 display: flex;
 align-items: center;
 justify-content: center;
 background: var(--color-bg-page-alt);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-base);
 padding: var(--space-3);
}
.finish-tile img {
 max-width: 100%;
 max-height: 100%;
 object-fit: contain;
}
.finish-tile h3 {
 margin-top: var(--space-3);
 font-size: var(--text-base);
 color: var(--color-text-heading);
}
.finish-tile p {
 margin-top: var(--space-1);
 color: var(--color-text-muted);
 font-size: var(--text-sm);
}
@media (max-width: 640px) {
 .finish-tiles {
 grid-template-columns: 1fr;
 gap: var(--space-4);
 margin-top: var(--space-4);
 }
 .finish-tile {
 display: flex;
 align-items: center;
 gap: var(--space-4);
 text-align: left;
 }
 .finish-tile__img-wrap {
 width: 72px;
 height: 72px;
 flex: none;
 aspect-ratio: auto;
 padding: var(--space-2);
 }
 .finish-tile h3 {
 margin-top: 0;
 font-size: var(--text-sm);
 }
 .finish-tile p {
 font-size: var(--text-xs);
 }
}

/* ---- area/county pages (fairfieldcounty, hartford-county, litchfield,
   tolland, danbury, manhattan, westchester, cities-we-serve) -----------
   Owner audit (2026-08-05): "jumbled mess... fonts too big, too bold...
   organize the photos." area_page() in build-site.py collapses each raw
   Wix block stream (dozens of H2/H4/H6 headings) into a handful of real
   section headings plus one collapsed <ul class="town-list"> for runs of
   consecutive town-name headings, and a bottom "Recent Work" gallery in
   place of the old side rail. Scoped to the .area-page wrapper the
   renderer adds around <main>'s content, so no other page's prose is
   touched — .prose h2/h3 (color, margin) still apply underneath these;
   only font-size/weight are added here. */
.area-page h2 {
 font-size: var(--text-lg);
}
.area-page h3 {
 font-size: var(--text-md);
 font-weight: var(--weight-semibold);
}
.area-page p {
 font-size: var(--text-base);
}

/* Higher specificity than .prose ul (list-style: disc) so the collapsed
   town list actually renders as columns, not a bulleted paragraph list. */
.area-page .town-list {
 list-style: none;
 columns: 1;
 column-gap: var(--space-8);
 font-size: var(--text-sm);
 font-weight: var(--weight-regular);
 margin: 0 0 var(--space-4);
 padding: 0;
}
@media (min-width: 768px) {
 .area-page .town-list { columns: 2; }
}
@media (min-width: 1024px) {
 .area-page .town-list { columns: 3; }
}
.area-page .town-list li {
 break-inside: avoid;
 margin: 0;
 padding: var(--space-1) 0;
 color: var(--color-text-body);
}
.area-page .town-list li::before {
 content: "\2713";
 display: inline-block;
 width: 1.25em;
 color: var(--color-strip-accent);
}

/* ---- CTA band -------------------------------------------------------- */
/* 2026-08-04 gray-glass restyle: moved off --color-bg-accent-navy onto the
   same --color-bg-header-solid (--gray-ink) dark surface as the header,
   footer and page-hero bands, so there is one consistent "dark band" color
   across the site instead of two (navy kept its own role elsewhere —
   .process-step border, badge text — just not as a full-bleed CTA band
   fill any more). --color-text-inverse on --gray-ink is 16.64:1 AAA.
   Reuses .hero__actions (section 6) for the button row — see the
   file-header note above on why a second button-row class was not
   created. */
.cta-band {
 background: var(--color-bg-header-solid);
 color: var(--color-text-inverse);
 text-align: center;
 padding-block: var(--space-16);
}
@media (max-width: 640px) {
 /* mobile: the band + footer stack is mostly dark surface on a small
    screen — tighten so the last content and the footer don't drift apart */
 .cta-band { padding-block: var(--space-12); }
}
.cta-band__inner {
 max-width: 640px;
 margin-inline: auto;
}
.cta-band h2 {
 color: var(--color-text-inverse);
 margin-bottom: var(--space-6);
}
.cta-band p {
 color: var(--color-text-inverse);
 opacity: 0.9;
 margin-bottom: var(--space-6);
}
.cta-band .hero__actions { justify-content: center; }
/* .btn--primary and .btn--outline are unmodified here on purpose: the
   button color is only ever the FILL behind the button, never a
   text/background pair being measured against the band itself.
   .btn--primary keeps its documented white-on-red (5.01:1 AA) pairing —
   and red as a button fill is legal directly on --gray-ink too (3.32:1,
   clears the 3:1 non-text minimum; see tokens.css). .btn--outline keeps
   its dark-context white border/text, which reads as a white-outline
   button on any dark fill, gray-ink included. */

/* ---- blog: post list, pager, tags -------------------------------------- */
.post-list {
 display: grid;
 gap: var(--space-6);
 grid-template-columns: 1fr;
}
@media (min-width: 768px) {
 .post-list { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
 .post-list { grid-template-columns: repeat(3, 1fr); }
}
/* .post-list reuses .card / .card-grid's card component; the only
   post-list-specific override is a smaller heading size than the
   default .card-body h3. */
.post-list .card h3 { font-size: var(--text-md); }

.blog-pager {
 display: flex;
 flex-wrap: wrap;
 align-items: center;
 justify-content: center;
 gap: var(--space-3);
 margin-top: var(--space-6);
}

.tag-cloud {
 display: flex;
 flex-wrap: wrap;
 gap: var(--space-2);
}
.tag-cloud li { display: inline-flex; }
.tag-cloud a {
 display: inline-flex;
 align-items: center;
 padding: var(--space-1) var(--space-3);
 border-radius: var(--radius-full);
 background: var(--color-badge-bg);
 color: var(--color-badge-text);
 border: 1px solid var(--color-badge-border);
 font-size: var(--text-xs);
 font-weight: var(--weight-medium);
}
/* Hover fills navy (13.60:1 AAA), not red: red stays reserved for
   clickable CTA buttons only (see the .process-step and .eyebrow
   comments elsewhere in this file) — a tag chip is a filter/label
   control, not a primary call to action, so it does not spend the
   brand's one "this is the action" color. */
.tag-cloud a:hover,
.tag-cloud a:focus-visible {
 background: var(--color-bg-accent-navy);
 color: var(--color-text-inverse);
 border-color: var(--color-bg-accent-navy);
}

/* ---- 404 -------------------------------------------------------------- */
.error-page {
 text-align: center;
 padding-block: var(--space-16);
 /* 404 is the first element under the now-fixed header — margin, not
    padding: this band is on the page (light) surface, not a dark band that
    needs the header height baked into its own visible padding. */
 margin-top: var(--header-h);
}
.error-page h1 { margin-bottom: var(--space-4); }
.error-page p {
 color: var(--color-text-muted);
 margin-bottom: var(--space-6);
}

/* ---- hero button row (Florida-shaped alternative to .hero__actions) --- */
/* Distinct from .hero__actions (section 6): the CT build's hero__copy
   markup emits this class name for its button row. Both classes are
   kept — neither replaces the other — since Georgia-shaped markup uses
   .hero__actions and Florida-shaped markup uses .hero-ctas. */
.hero-ctas {
 display: flex;
 flex-wrap: wrap;
 gap: var(--space-3);
 margin-top: var(--space-6);
}

/* ---- forms: Florida-named helper text ---------------------------------- */
/* Equivalent to .field-hint (section 8) under the Florida-shaped class
   name the CT build script emits for the same role. */
.form-hint {
 font-size: var(--text-sm);
 color: var(--color-text-muted);
 margin-top: var(--space-1);
}

/* ---- header: text-only logo fallback ----------------------------------- */
/* No CT logo file has been supplied yet (see tokens.css "OPEN" note). This
   is the visible text lockup used until real logo art lands — distinct
   from .site-header__logo-word (header.css), which is a visually-hidden
   duplicate wordmark kept only for the accessibility tree once a crest
   image exists. .site-header__logo-tag already existed in section 3 with
   exactly the requested styling (--text-xs, opacity .85,
   --color-text-inverse) and is not redefined here. */
.site-header__logo-text {
 font-family: var(--font-heading);
 font-size: var(--text-lg);
 font-weight: var(--weight-bold);
 color: var(--color-text-inverse);
 line-height: 1.1;
}

/* =========================================================================
   MOBILE CLEANUP — 2026-08-04 gray-glass restyle, narrow phones (≤640px)

   Complements header.css's own ≤560px block (proof-chip/hero__actions
   stacking, Ken Burns) with the remaining owner-requested narrow-screen
   fixes: a headline that actually fits, one fewer competing hero button,
   tighter chips, and less dead vertical space above the fold.
   ========================================================================= */
@media (max-width: 640px) {
  /* --text-4xl's floor (2.25rem) still doesn't shrink enough for a 320-375px
     phone with this serif display face. Lower floor only here — the token
     itself is left alone since it's shared with any future desktop use. */
  .hero__title {
    font-size: clamp(1.9rem, 5vw + 0.9rem, 2.25rem);
  }

  /* The sticky bottom bar already has a Call button on every screen size;
     the hero's second CTA is redundant weight competing with "Get a Free
     Estimate" on a screen where every pixel of vertical space matters. */
  .hero-ctas .btn--outline {
    display: none;
  }
  /* Owner mobile-hero fix (2026-08-06): re-shown. Previously hidden here
     (2026-08-05 reasoning above, now superseded) to save vertical space,
     but the owner's fold rebuild explicitly asks for this link as one of
     the required above-the-fold elements — see the hero-fold rules below,
     which reclaim the space this needed by tightening everything else. */

  .proof-chip {
    font-size: var(--text-xs);
    padding: var(--space-1) var(--space-3);
  }
  .proof-chips {
    gap: var(--space-2);
  }

  /* Tighter than the base (header-aware) padding — the goal on a phone is
     hero + eyebrow/title/subtitle + chips + one CTA fitting close to one
     viewport, not full desktop breathing room stacked on top of the fixed
     header's own offset. */
  .hero__content {
    padding-block: calc(var(--header-h) + var(--space-6)) var(--space-10);
  }
  .page-hero {
    padding-block: calc(var(--header-h) + var(--space-5)) 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;
 }
}

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

   Ported verbatim from The Hardwood Guys Georgia (design/components.css
   section 20, 2026-08-14) as part of the gallery admin port. Every custom
   property this section references was checked against this file's own
   tokens.css by name before porting (trap: undefined custom properties
   silently drop the whole declaration, no error) -- all 35 resolved
   unchanged; the only "new" property here, --sgal-radius, is a local alias
   defined in the very next rule, not a tokens.css lookup.

   Ten of the owner'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. */
.sgal__cap {
 position: absolute;
 inset: auto 0 0 0;
 padding: var(--space-6) var(--space-5) var(--space-3);
 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: var(--space-2); margin-top: var(--space-4); }
.sgal__dot {
 width: 10px; height: 10px; padding: 0;
 border: 0; border-radius: var(--radius-full);
 background: var(--border); cursor: pointer;
}
.sgal__dot.is-on { background: var(--color-text-heading); }
/* The visible dot is small, but the thing you tap is not. */
.sgal__dot::after { content: ""; position: absolute; }
.sgal__dots { position: relative; }
.sgal__dot { position: relative; margin: 12px 6px; }
.sgal__dot::before { content: ""; position: absolute; inset: -12px -6px; }

@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. Written as an ABSOLUTE
    size intentionally matching Georgia's post-mortem: an earlier version of
    this exact rule on Georgia scaled the title back UP when the base token
    changed size, because it was written relative to --text-base instead of
    a fixed value (see Georgia's design/components.css section 20 for the
    full incident writeup). Keep this as a literal size, not a step off
    --text-base, or the same bug reproduces here. */
 .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. 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; }

/* =========================================================================
 17. SHARED COMPONENTS (handoff spec WI-4 / WI-5 / WI-11, 2026-10-02)
 proof_strip(), town_grid(), the "Water damage repair in {Town}" block and
 the water-damage hub's hero furniture. ALL component CSS lives HERE, in the
 shared stylesheet, never in one page's inline <style>: the Georgia site
 shipped its by-town grid's CSS only inside the homepage's inline style, so
 the chips' SVG house icons rendered 1,120px wide on 21 pages. Every inline
 SVG these components emit ALSO carries explicit width="20" height="20"
 attributes, so the icons stay icon-sized even if this file fails to load.
 Only SEMANTIC-LAYER tokens (tokens.css section 2) are referenced below —
 an undefined custom property silently kills the whole declaration.
 ========================================================================= */

/* The one icon size every component SVG uses. Mirrors the width/height
   attributes on the markup; CSS wins when loaded, the attributes win when
   it is not. Capped so no future icon here can balloon. */
.ui-icon {
 width: 20px;
 height: 20px;
 max-width: 24px;
 max-height: 24px;
 flex: none;
 display: inline-block;
 vertical-align: middle;
}

/* ---- proof strip ------------------------------------------------------ */
/* Light band directly under a hero. --color-strip-bg is --off-white; the
   text is --color-strip-text (--ink, 12.04:1 on off-white) and the icons use
   --color-strip-accent (--blue, 4.90:1 on off-white) — icon-only accent,
   red stays reserved for clickable CTAs. Items are plain statements, never
   star ratings or review counts. */
.proof-strip {
 background: var(--color-strip-bg);
 color: var(--color-strip-text);
 border-bottom: 1px solid var(--color-border-default);
 padding-block: var(--space-4);
}
.proof-strip__list {
 display: flex;
 flex-wrap: wrap;
 justify-content: center;
 gap: var(--space-3) var(--space-6);
 list-style: none;
 margin: 0;
 padding: 0;
}
.proof-strip__item {
 display: flex;
 align-items: center;
 gap: var(--space-2);
 font-size: var(--text-sm);
 font-weight: var(--weight-semibold);
 line-height: var(--leading-snug);
 min-width: 0;
}
.proof-strip__item .ui-icon { color: var(--color-strip-accent); }
.proof-strip__item a {
 color: inherit;
 text-decoration: underline;
}
.proof-strip__item a:hover,
.proof-strip__item a:focus-visible { color: var(--color-text-link-hover); }
@media (max-width: 640px) {
 /* two columns on a phone so five short statements do not stack into a
    tall column; the longest (licences, financing) wrap inside their cell */
 .proof-strip__list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3) var(--space-4);
  justify-content: stretch;
 }
 .proof-strip__item { font-size: var(--text-xs); align-items: flex-start; }
 .proof-strip__item .ui-icon { margin-top: 1px; }
}

/* ---- by-town grid ------------------------------------------------------ */
.town-grid {
 display: grid;
 grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
 gap: var(--space-3);
 list-style: none;
 margin: var(--space-6) 0 0;
 padding: 0;
}
.town-grid li { margin: 0; min-width: 0; }
.town-chip {
 display: flex;
 align-items: center;
 gap: var(--space-2);
 min-height: var(--touch-target-min);
 padding: var(--space-2) var(--space-4);
 background: var(--color-bg-card);
 color: var(--color-text-heading);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-base);
 font-weight: var(--weight-semibold);
 font-size: var(--text-sm);
 text-decoration: none;
 transition: box-shadow var(--motion-base) var(--motion-ease),
  border-color var(--motion-base) var(--motion-ease);
}
.town-chip .ui-icon { color: var(--color-strip-accent); }
.town-chip:hover,
.town-chip:focus-visible {
 border-color: var(--color-text-heading);
 box-shadow: var(--shadow-sm);
 text-decoration: underline;
}
@media (max-width: 480px) {
 .town-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
 .town-chip { padding-inline: var(--space-3); }
}

/* ---- "Water damage repair in {Town}" block (WI-5) ---------------------- */
.prose .water-block {
 background: var(--color-bg-page-alt);
 border: 1px solid var(--color-border-default);
 border-radius: var(--radius-base);
 padding: var(--space-6);
 margin: var(--space-8) 0 var(--space-4);
}
.prose .water-block h2 { margin-top: 0; }
.prose .water-block p:last-child { margin-bottom: 0; }

/* In-town service links added to each town page's services list (WI-5):
   the anchors are lowercase by spec ("hardwood floor refinishing in
   {Town}"), so the list capitalises the first letter visually. */
.prose .town-services li::first-letter { text-transform: uppercase; }

/* ---- service-hub hero furniture (WI-4) ---------------------------------- */
/* The lede sits on the --gray-ink page-hero band: white on it is 16.64:1
   (AAA). No overlay box, no photo — plain type on the band. */
.page-hero__lede {
 color: var(--color-text-inverse);
 font-size: var(--text-md);
 line-height: var(--leading-snug);
 max-width: 62ch;
 margin: var(--space-3) 0 var(--space-6);
}
.page-hero .hero__actions { margin-top: var(--space-2); }
@media (max-width: 640px) {
 .page-hero__lede { font-size: var(--text-base); }
 .page-hero .hero__actions { flex-direction: column; align-items: stretch; gap: var(--space-3); }
 .page-hero .hero__actions .btn { padding: var(--space-3) var(--space-4); font-size: var(--text-base); }
}
