/* ==========================================================================
   AZ Home Remodeling — Self-Hosted Fonts
   Self-hosted instead of Google Fonts' own stylesheet so the site never
   depends on Google's response time for the correct brand font to show.
   font-display: swap shows the fallback immediately, then swaps in the
   real font the instant it loads (typically well under 100ms, same-origin,
   no extra DNS/connection) -- this replaces an earlier attempt that used
   Google's `display=optional` flag, which (worse) can permanently stick
   with the wrong fallback font for the whole page view if the font
   doesn't arrive within Chrome's very short optional-mode block window.
   Outfit ships as a single variable-font file covering weights 400-700;
   each @font-face below pins one weight to that same file, which modern
   browsers correctly render as a distinct static instance per weight.
   ========================================================================== */
@font-face {
  font-family: 'Bebas Neue';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/bebas-neue-v16-latin-regular.woff2') format('woff2');
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/outfit-v15-latin-variable.woff2') format('woff2');
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/outfit-v15-latin-variable.woff2') format('woff2');
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/outfit-v15-latin-variable.woff2') format('woff2');
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/outfit-v15-latin-variable.woff2') format('woff2');
}

/* ==========================================================================
   AZ Home Remodeling — Design Tokens (design-system.md)
   ========================================================================== */

:root {
  /* Light tokens — default, most of the site */
  --bg: #F5F3EC;
  --surface: #FFFFFF;
  --surface-2: #E8E3D6;
  --primary: #EFCE11;
  --accent: #EFCE11;
  --text: #131313;
  --muted: #656363;
  --border: rgba(19, 19, 19, 0.08);
  --radius: 12px;
  --shadow: 0 24px 60px rgba(0, 0, 0, 0.15);
  --ease: cubic-bezier(0.4, 0, 0.2, 1);

  --font-heading: 'Bebas Neue', sans-serif;
  --font-body: 'Outfit', sans-serif;

  --section-padding-mobile: 56px;
  --section-padding-desktop: 112px;
  --container-max: 1360px;
}

/* Dark section override — hero, key CTA bands, footer only */
.section-dark {
  --bg: #131313;
  --bg-deep: #0A0A0A;
  --surface: #1C1C1C;
  --surface-2: #262626;
  --text: #FFFFFF;
  --muted: #656363;
  --border: rgba(255, 255, 255, 0.08);
  --shadow: 0 24px 60px rgba(0, 0, 0, 0.4);
  /* Redeclaring --text alone doesn't repaint inherited `color` on
     descendants (color is fixed at body's computed value once
     inherited) — this re-applies it so anything relying on
     inheritance inside a dark section actually turns white. */
  color: var(--text);
}

/* ==========================================================================
   Reset / Base
   ========================================================================== */

* { margin: 0; padding: 0; box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img, video { max-width: 100%; display: block; }

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

.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 24px;
}

/* text-wrap:balance is the default line-break rule for every heading,
   site-wide (client request 2026-08-19) — lets the browser distribute a
   multi-line heading's words evenly at whatever width it's actually
   rendered at, instead of the default wrap (fills each line greedily,
   which routinely strands one short word alone on the last line — an
   "orphan" — especially on narrow mobile widths). Graceful no-op in any
   browser that doesn't support it (falls back to normal wrap, never
   breaks anything). This replaces guessing a single hardcoded <br> that
   only looks balanced at one specific viewport width. Only override with
   a manual <br> when a specific two-line split is a deliberate content
   choice (e.g. isolating an accent-colored phrase, like a location name,
   onto its own line) rather than a pure line-balancing decision — see
   `~/.claude/reference/design-standards.md` Heading Line-Break Balance. */
h1, h2, h3, h4 {
  font-family: var(--font-heading);
  font-weight: 400;
  letter-spacing: -0.03em;
  line-height: 1;
  text-wrap: balance;
}

.eyebrow {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--primary);
}

/* Angular eyebrow tag — chamfered parallelogram badge, echoes the
   diagonal slant of the AZ logo mark instead of a rounded pill. Mobile
   size dropped a full step below desktop (was 11px, one px off desktop's
   12px — close enough to read as "not actually responsive," client
   flagged 2026-08-19) so the badge visibly scales down on a phone
   instead of just carrying the desktop size. */
.eyebrow-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  width: fit-content;
  background: #131313;
  padding: 5px 12px 5px 9px;
  font-size: 10px;
  clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
}

.eyebrow-tag::before {
  content: '';
  flex-shrink: 0;
  width: 5px;
  height: 5px;
  background: var(--primary);
  clip-path: polygon(35% 0, 100% 0, 65% 100%, 0 100%);
}

@media (min-width: 768px) {
  .eyebrow-tag {
    gap: 8px;
    padding: 8px 18px 8px 14px;
    font-size: 12px;
    clip-path: polygon(12px 0, 100% 0, calc(100% - 12px) 100%, 0 100%);
  }

  .eyebrow-tag::before {
    width: 7px;
    height: 7px;
  }
}

.section {
  padding: var(--section-padding-mobile) 0;
}

@media (min-width: 768px) {
  .section { padding: var(--section-padding-desktop) 0; }
}

/* ==========================================================================
   Buttons
   ========================================================================== */

/* Mobile-first sizing (client request 2026-08-19) — buttons were rendering
   at the exact same padding/font-size on mobile as desktop, so every CTA
   read oversized on a phone screen. min-height:48px is a hard floor at
   every breakpoint (mobile tap-target rule) and never shrinks; padding
   and font-size are what scale down. Desktop restores the fuller size
   below. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 12px 22px;
  min-height: 48px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 15px;
  border-radius: var(--radius);
  cursor: pointer;
  border: none;
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease), background-color 0.3s var(--ease);
  white-space: nowrap;
}
@media (min-width: 768px) {
  .btn { padding: 14px 32px; font-size: 16px; }
}

.btn-primary {
  background: var(--primary);
  color: #131313;
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(239, 206, 17, 0.3); }
.btn-primary:active { transform: scale(0.97); }
.btn-primary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Solid onyx by default, gold on hover — client direction 2026-09-02:
   every "outline, dark text" button site-wide reads as solid instead
   (same treatment as .btn-dark-solid). The one deliberate exception is
   the homepage hero's "See Our Work", which stays a white outline via
   the .section-dark override directly below — hero is the one place
   we don't want to call extra attention to the secondary action. */
.btn-secondary {
  background: #131313;
  color: #fff;
  border: none;
}
.btn-secondary:hover { background: var(--primary); color: #131313; transform: translateY(-2px); box-shadow: 0 8px 25px rgba(239, 206, 17, 0.3); }
.btn-secondary:active { transform: scale(0.97); }
.btn-secondary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Homepage hero exception — kept as a quiet white outline on purpose */
.hero .btn-secondary { background: transparent; color: #fff; border: 2px solid #fff; }
.hero .btn-secondary:hover { background: #fff; color: #131313; box-shadow: none; transform: none; }

/* Accent shape — reserved for the site's persistent primary CTA (nav + hero)
   per the trust-first doctrine (~/.claude/reference/design-doctrine-trust-first.md).
   Everything else stays the soft .btn radius. Reuses the exact clip-path shear
   already established on .eyebrow-tag so the accent reads as one repeated
   fragment of the logo, not a second angle. box-shadow is clipped away by
   clip-path in all major browsers, so the hover glow moves to filter/drop-shadow. */
.btn-accent-shape {
  border-radius: 0;
  clip-path: polygon(10px 0, 100% 0, calc(100% - 10px) 100%, 0 100%);
}
.btn-accent-shape.btn-primary:hover {
  box-shadow: none;
  filter: drop-shadow(0 8px 25px rgba(239, 206, 17, 0.3));
}
.btn-accent-shape.btn-dark-solid:hover {
  box-shadow: none;
  filter: drop-shadow(0 8px 25px rgba(19, 19, 19, 0.3));
}
@media (min-width: 768px) {
  .btn-accent-shape {
    clip-path: polygon(12px 0, 100% 0, calc(100% - 12px) 100%, 0 100%);
  }
}

/* ==========================================================================
   Navigation
   ========================================================================== */

.nav {
  position: sticky;
  top: 0;
  z-index: 1100000;
  /* Solid dark at rest — nav sits in normal flow above the hero, not
     overlapping it, so there is nothing dark behind it to show through
     a transparent background. Never rely on transparent-at-rest unless
     nav is actually layered on top of a dark hero image/video. */
  background: var(--bg);
  transition: background-color 0.4s var(--ease), backdrop-filter 0.4s var(--ease), box-shadow 0.4s var(--ease);
}

.nav.is-scrolled {
  background: rgba(19, 19, 19, 0.85);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08);
  transform: translateZ(0);
}

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 92px;
}

.nav-logo img { height: 80px; width: auto; }
.nav-logo .logo-text-fallback {
  font-family: var(--font-heading);
  font-size: 24px;
  color: #fff;
  letter-spacing: -0.02em;
}

.nav-links {
  display: none;
  align-items: center;
  gap: 40px;
}

.nav-links a {
  color: #fff;
  font-size: 15px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  position: relative;
  padding: 4px 0;
}

.nav-links a::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 2px;
  background: var(--primary);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.3s var(--ease);
}

.nav-links a:hover::after { transform: scaleX(1); }

.nav-cta { display: none; }

.nav-hamburger {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 48px;
  height: 48px;
  background: none;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  z-index: 1100001;
}

/* Was missing hover/focus feedback entirely -- caught in the 2026-09-08
   pre-launch sweep's Thunk Test pass. */
.nav-hamburger:hover span { background: var(--primary); }
.nav-hamburger:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.nav-hamburger span {
  display: block;
  width: 24px;
  height: 2px;
  background: #fff;
  margin: 0 auto;
  transition: transform 0.3s var(--ease), opacity 0.3s var(--ease), background 0.3s var(--ease);
}

.nav-hamburger.is-open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-hamburger.is-open span:nth-child(2) { opacity: 0; }
.nav-hamburger.is-open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

@media (min-width: 1024px) {
  .nav-links { display: flex; }
  .nav-cta { display: inline-flex; }
  .nav-hamburger { display: none; }
}

/* Mobile nav drawer. justify-content: flex-start (not center) pins
   content to the top always, scrolling via overflow-y when content
   is taller than the viewport (accordion opened on a short phone) —
   replaces an earlier auto-margin centering technique that could
   clip content under real mobile overflow, see known-bugs/
   flex-auto-margin-centering-clips-content-under-overflow.md.
   padding-top clears the fixed header (92px) so "Home" never sits
   underneath the hamburger/close button even in the scrolling case.
   Relies on main.js's lockBodyScroll()/unlockBodyScroll() (not just
   `overflow: hidden` on body) to actually prevent background scroll
   on iOS while this panel is open — see known-bugs/
   ios-safari-overflow-hidden-body-scroll-lock-freeze.md.
   Markup lives as a sibling right after </header> in every page, NOT
   nested inside it — .nav.is-scrolled applies backdrop-filter and
   transform once the page scrolls ~40px, and both properties create a
   new CSS containing block for any position:fixed descendant. Nested
   inside <header>, that trapped this drawer's fixed positioning inside
   the header's own small box instead of the viewport, so it visually
   opened only ~92px tall and cut off right at "Home" — see
   known-bugs/nav-drawer-trapped-by-scrolled-header-containing-block.md.
   z-index must stay BELOW .nav's 1100000, not above it: the header
   (with the hamburger/close button) is meant to stay pinned on top of
   the drawer at all times — that's what the drawer's own padding-top
   above reserves space for. Briefly set above the header instead of
   below it, which buried the whole header (hamburger included) under
   the drawer, making the close (X) button invisible and unclickable
   once the menu was open — the only way out was tapping a nav link. */
.nav-drawer {
  position: fixed;
  inset: 0;
  z-index: 1000000;
  background: rgba(19, 19, 19, 0.97);
  backdrop-filter: blur(16px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 20px;
  padding: 104px 24px 32px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-16px);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease), visibility 0.35s;
}

.nav-drawer.is-open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.nav-drawer a {
  color: #fff;
  font-family: var(--font-heading);
  font-size: 32px;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

/* CTA keeps the sitewide primary-button treatment (onyx text on the
   yellow fill) instead of inheriting the white drawer-link color above
   — needs the extra class in the selector to outrank `.nav-drawer a`. */
.nav-drawer a.btn-primary {
  color: #131313;
}

/* ==========================================================================
   Nav — Services Dropdown (standing template: every page's nav gets this
   exact markup; add a new <a> to both the desktop panel and the mobile
   group whenever a new service page ships — see build-lessons note)
   ========================================================================== */

.nav-item-dropdown { position: relative; }

/* Invisible bridge over the visual gap between the trigger and the panel
   (top: calc(100% + 20px) below) — without this, the 20px gap is empty
   space belonging to neither element, so :hover drops the instant the
   cursor crosses it and the panel starts fading before the pointer ever
   reaches it. A pseudo-element counts as a descendant for :hover
   purposes, so this keeps .nav-item-dropdown:hover continuously true
   across the whole gap. */
.nav-item-dropdown::before {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  width: 230px;
  height: 20px;
}

.nav-dropdown-trigger { display: inline-flex; align-items: center; gap: 2px; }
.nav-dropdown-caret { font-size: 18px !important; transition: transform 0.3s var(--ease); }
.nav-item-dropdown:hover .nav-dropdown-caret,
.nav-item-dropdown:focus-within .nav-dropdown-caret { transform: rotate(180deg); }

.nav-dropdown-panel {
  position: absolute;
  top: calc(100% + 20px);
  left: 50%;
  min-width: 230px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 8px;
  display: flex;
  flex-direction: column;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-50%) translateY(-8px);
  transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0.25s;
  z-index: 10;
}

.nav-item-dropdown:hover .nav-dropdown-panel,
.nav-item-dropdown:focus-within .nav-dropdown-panel {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}

.nav-dropdown-panel a {
  color: #fff;
  font-size: 14px;
  font-weight: 500;
  text-transform: none;
  letter-spacing: normal;
  padding: 10px 12px;
  border-radius: calc(var(--radius) - 4px);
  white-space: nowrap;
}
.nav-dropdown-panel a::after { display: none; }
.nav-dropdown-panel a:hover { background: rgba(255, 255, 255, 0.06); color: var(--primary); }

/* Active nav link — set by initActiveNavLink() in main.js, which compares
   window.location.pathname against every nav link's href. This runs on
   every page load, so a shared nav block copied verbatim to a new page
   highlights correctly with zero per-page markup changes. */
.nav-links a.is-active,
.nav-drawer a.is-active {
  color: var(--primary);
}
.nav-links a.is-active::after { transform: scaleX(1); }
.nav-dropdown-panel a.is-active {
  color: var(--primary);
  background: rgba(239, 206, 17, 0.1);
}

/* Mobile drawer — collapsible sub-link group (Services today, Service
   Areas once its location pages exist — reuse this exact markup/class
   set, initNavDrawerAccordion() in main.js picks up any
   .nav-drawer-group with a .nav-drawer-sublinks child automatically).
   Defaults CLOSED on load/every drawer open (client confirmed 2026-08-20
   — do not default this open) — the toggle button only collapses/
   re-expands, it never navigates, the "Services" text stays the actual
   link. The leading .nav-drawer-group-spacer mirrors the trailing
   toggle button's width so "Services" itself lands dead-center over
   the row, matching every other centered drawer link, instead of
   drifting left of center to make room for a trailing-only icon. */
.nav-drawer-group { display: flex; flex-direction: column; align-items: center; gap: 6px; }

.nav-drawer-group-head { display: flex; align-items: center; gap: 6px; }
.nav-drawer-group-head a { padding: 4px 0; }

.nav-drawer-group-spacer { width: 36px; flex-shrink: 0; }

.nav-drawer-group-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: #fff;
}
.nav-drawer-group-caret {
  font-size: 26px !important;
  line-height: 1;
  transition: transform 0.35s var(--ease);
}
.nav-drawer-group.is-open .nav-drawer-group-caret { transform: rotate(180deg); }

.nav-drawer-sublinks {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-height 0.4s var(--ease), opacity 0.3s var(--ease);
}
.nav-drawer-group.is-open .nav-drawer-sublinks {
  opacity: 1;
  margin-top: 6px;
}

.nav-drawer-sublink {
  font-family: var(--font-body) !important;
  font-size: 16px !important;
  font-weight: 500;
  text-transform: none !important;
  letter-spacing: normal !important;
  color: rgba(255, 255, 255, 0.65) !important;
}
.nav-drawer-sublink:hover,
.nav-drawer-sublink.is-active { color: var(--primary) !important; }

/* ==========================================================================
   Hero — framed/inset technique
   ========================================================================== */

.hero {
  position: relative;
  background: var(--bg);
}

/* Gold "picture mat" margin between the full-bleed Onyx section and the
   inset video frame — the hero's signature framing technique. Thickness
   halved 2026-08-18 (was 24px/32px), then halved again site-wide
   2026-08-20 (was 12px/16px) per client request, confirmed first on the
   homepage alone before the site-wide rollout — a thin accent line
   drawing the eye inside the frame, not a competing border. Applies to
   every .hero-mat instance: homepage, service-page photo heroes, and
   the no-media inner-page headers (About, Contact, Services Hub,
   Service Areas) alike. */
.hero-mat {
  padding: 6px;
  background: var(--primary);
}

@media (min-width: 768px) {
  .hero-mat { padding: 8px; }
}

/* Height reworked 2026-08-20 — MOBILE ONLY (client corrected 2026-08-20:
   an earlier pass applied this to the base rule, which silently changed
   the desktop hero too even though only mobile was ever asked for; the
   min-width:768px override below restores desktop's original forced
   viewport height). On mobile, the frame was forced to
   min-height: calc(100dvh - 88px - 48px) with content bottom-anchored via
   align-items:flex-end, which left a huge empty gap above the content on
   any viewport where the content itself was much shorter than ~100dvh —
   not a "padding" gap, an emergent one (frame height minus content
   height, all stacked above the bottom-pinned content). Replaced the
   forced near-full-viewport floor with an explicit 56px top padding on
   the frame itself instead — the frame now sizes to its content plus
   that fixed 56px, matching the ~56px bottom padding hero-content
   already carries, rather than reserving viewport-driven empty space
   above. align-items:flex-end is kept (harmless no-op once there's no
   leftover space to distribute) so nothing about the bottom-anchored
   overlay gradient's contrast tuning needs to change. */
.hero-frame {
  position: relative;
  border-radius: 20px;
  overflow: hidden;
  padding-top: 56px;
  display: flex;
  align-items: flex-end;
  background: var(--surface);
}

@media (min-width: 768px) {
  /* Desktop restored to the original cinematic near-full-viewport hero
     (design-standards.md quality gate) — the content-sized mobile fix
     above never belonged here. align-items:center (client request
     2026-08-20) vertically centers the whole content block — badge
     through CTAs — instead of bottom-anchoring it, so the empty space
     above and below reads as equal. Mobile keeps align-items:flex-end
     from the base rule, unaffected. */
  .hero-frame {
    padding-top: 0;
    min-height: calc(100dvh - 88px - 48px);
    align-items: center;
  }
}

.hero-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 1;
}

.hero-media-placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background:
    repeating-linear-gradient(135deg, rgba(255,255,255,0.03) 0 2px, transparent 2px 24px),
    linear-gradient(160deg, #262626 0%, #1a1a1a 60%, #131313 100%);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.hero-media-placeholder span {
  font-family: var(--font-body);
  font-size: 13px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px dashed rgba(255,255,255,0.2);
  padding: 10px 20px;
  border-radius: 100px;
}

.hero-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(to top, rgba(19,19,19,0.92) 0%, rgba(19,19,19,0.55) 45%, rgba(19,19,19,0.15) 75%);
}

/* The base gradient above is tuned for bottom-anchored hero content (the
   homepage) — it's nearly untinted in the top quarter, which is fine
   when nothing sits there. .hero--short content is vertically centered
   instead, so the headline/subhead land in that lighter band and lose
   contrast, especially the thinner-weight subhead text. Flatter, more
   uniformly dark overlay for the centered-content case. */
.hero--short .hero-overlay {
  background: linear-gradient(180deg, rgba(19,19,19,0.75) 0%, rgba(19,19,19,0.62) 50%, rgba(19,19,19,0.78) 100%);
}

/* Centered per client request 2026-08-20 (was left-aligned, the
   mobile-hero.md image-hero default) — same technique already proven
   on .hero--short: flex column + align-items:center centers each
   child's own box regardless of its individual max-width, rather than
   relying on text-align alone (text-align only centers wrapped lines
   within a box that's still pinned left). Only index.html uses the
   bare .hero (every other page is .hero--short), so this is a
   homepage-only change in practice despite living on the base rule. */
.hero-content {
  position: relative;
  z-index: 3;
  width: 100%;
  padding: 40px 24px 56px;
  color: #fff;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

@media (min-width: 768px) {
  .hero-content { padding: 64px 64px 80px; }
}

/* Mini rolling trust marquee — replaces static badge row. Two identical
   groups sit side by side in the track; JS (initHeroMarquee in main.js)
   measures the exact pixel width of the first group and drives the loop
   by that precise offset via --marquee-shift, rather than a -50% transform
   (which is never exactly seamless once a connecting gap is involved —
   it lands half a gap short/long and produces a visible snap each loop).
   JS also caps the container's width to align with the right edge of
   "Remodeling" in the headline. */
.hero-marquee {
  overflow: hidden;
  max-width: 100%;
  /* 14px even gap above (to the H1) and below (to the CTAs) per client
     direction 2026-09-02 */
  margin-bottom: 14px;
  background: rgba(239, 206, 17, 0.14);
  border-radius: var(--radius);
  padding: 9px 4px;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}

/* Smaller scale on mobile — the marquee was sized to sit under the
   desktop headline; against the much smaller mobile H1 (clamp floor
   48px) the same font/icon/gap size read oversized and out of
   proportion. Client direction 2026-09-02. */
@media (max-width: 767px) {
  .hero-marquee { padding: 6px 4px; }
  .hero-marquee-track,
  .hero-marquee-group { gap: 20px; }
  .hero-marquee-item { font-size: 10px; gap: 6px; }
  .hero-marquee-item svg { width: 11px; height: 11px; }
}

.hero-marquee-track {
  display: flex;
  align-items: center;
  gap: 32px;
  width: max-content;
  animation: hero-marquee-scroll 16s linear infinite;
  transform: translateX(0);
}

/* No horizontal padding here — the group's own 32px gap already spaces
   every item evenly, including at the seam between this group and its
   duplicate (the track's own 32px gap continues the same rhythm).
   Padding here used to stack with the track gap at that one seam only
   (12px + 32px + 12px = 56px vs. 32px everywhere else), reading as an
   uneven "lag" once per loop as the wider gap scrolled past. */
.hero-marquee-group {
  display: flex;
  align-items: center;
  gap: 32px;
  flex-shrink: 0;
}

.hero-marquee-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #fff;
}

.hero-marquee-item svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--primary);
}

.hero-marquee-item-sub {
  font-weight: 500;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.6);
}

@keyframes hero-marquee-scroll {
  to { transform: translateX(var(--marquee-shift, -50%)); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-marquee-track { animation: none; }
}

/* Watermark-style logo mark above the headline, client request
   2026-09-02. Same clamp/9vw scaling curve as .hero-headline so it
   grows and shrinks in proportion to the headline it sits above,
   floor set above the nav's fixed 80px so it reads deliberately
   larger there per client direction. Same opacity as .hero-headline
   below so both read as one softened, watermark-like layer instead
   of a hard logo pasted over soft text. */
.hero-logo {
  height: clamp(72px, 9vw, 116px);
  width: auto;
  opacity: 0.88;
  /* 14px even gap to the headline below, matching .hero-headline's and
     .hero-marquee's own margin-bottom so all three hero gaps match */
  margin-bottom: 14px;
}

.hero-headline {
  /* Floor lowered from 48px to 40px (client direction 2026-09-05) so
     the homepage headline wraps "The Kitchen & Bath / Remodeler South
     Jersey / Homeowners Trust" as 3 lines instead of 4 on real phone
     widths (~360-430px, where 9vw sits below this floor). Only affects
     viewports under ~444px (40/0.09) — past that the 9vw curve already
     exceeds 40px and takes over exactly as before, so desktop/tablet
     sizing is untouched. Mirrored on .hero--short below so every
     page's hero headline stays the same size site-wide, per the
     existing "one uniform size" rule there. */
  font-size: clamp(40px, 9vw, 108px);
  letter-spacing: -0.03em;
  line-height: 0.95;
  max-width: 900px;
  /* 14px even gap to the marquee below, matching .hero-marquee's own
     margin-bottom down to the CTAs */
  margin-bottom: 14px;
  /* Slight softening per client request 2026-09-02 — matches .hero-logo's
     opacity so the mark above it and the headline read as one treatment. */
  opacity: 0.88;
}

.hero-headline .gold { color: var(--primary); }

.hero-ctas {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px;
}

/* Placeholder content-authoring note (removed once real copy is written) */
.placeholder-note {
  display: inline-block;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #131313;
  background: var(--primary);
  padding: 3px 10px;
  border-radius: 4px;
  margin-bottom: 10px;
}

/* ==========================================================================
   Material Symbols base
   ========================================================================== */

.material-symbols-outlined {
  font-family: 'Material Symbols Outlined';
  font-weight: normal;
  font-style: normal;
  line-height: 1;
  display: inline-block;
  text-transform: none;
  letter-spacing: normal;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  -webkit-font-smoothing: antialiased;
}

/* ==========================================================================
   Shared: Section Header Stack (14px rule)
   ========================================================================== */

.section-head {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 48px;
  max-width: 680px;
}

.section-head.centered {
  align-items: center;
  text-align: center;
  margin-left: auto;
  margin-right: auto;
}

.section-head h2 {
  font-size: clamp(36px, 5vw, 56px);
}

.section-head p {
  font-family: var(--font-body);
  font-size: 18px;
  color: var(--muted);
  line-height: 1.7;
}

/* ==========================================================================
   Services Overview — two-column intro (heading left, body+CTA right),
   matching the client-referenced page's layout. Cycling word headline
   echoes that same reference's word-swap intro, adapted to AZ's own copy
   — Module 29, Typewriter Cycling Headline, from the animation catalog.
   ========================================================================== */

.services-intro {
  /* Column widths sized to match their real content (heading's widest line
     measured ~554px, paragraph capped at 420px) instead of an arbitrary
     1fr 1fr split — that's what was leaving ~230px of dead space to the
     right of the buttons while the heading sat flush left. Centering this
     right-sized block within .container (margin:auto) lands it centered
     relative to .services-grid automatically, since both share that same
     parent — no manual offset math needed. */
  display: grid;
  grid-template-columns: 600px 409px;
  gap: 28px;
  align-items: center;
  max-width: 1037px;
  margin: 0 auto 48px;
}

.services-intro-body p {
  color: var(--muted);
  font-size: 18px;
  line-height: 1.7;
  max-width: 420px;
}
.services-intro-body p a { text-decoration: underline; text-underline-offset: 3px; }
.services-intro-body p a.text-link-accent { color: var(--text); font-weight: 700; }

.services-intro-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 20px; }

@media (max-width: 767px) {
  .services-intro { grid-template-columns: 1fr; gap: 24px; }
}

.services-cycle-heading {
  /* Sized so the 3-line block's bottom edge ("One Room At A Time.") lands
     level with the bottom of the Request Quote/See Our Work button row in
     the right column — both columns share the same top edge already, so
     this is a height-match, not an arbitrary size bump. Tuned against the
     1440px measurement (52px -> 96px hit the target exactly); scales down
     on narrower viewports since the columns naturally get shorter there too. */
  /* Client asked for line-height halved (1 -> 0.5); at this font-size Bebas
     Neue's actual glyph ink is 75px tall (measured via canvas
     actualBoundingBoxAscent/Descent) against a 90px em, so anything below
     ~0.83 clips into the adjacent line regardless of font-size (the ratio
     scales with size, so shrinking the font doesn't fix it). 0.88 is the
     tightest value that stays clear of that floor with real margin. */
  font-size: clamp(32px, 8vw, 90px);
  line-height: 0.88;
}

/* Each piece of the heading — the cycling word, "Your Home,", and
   "One Room At A Time." — is its own line, all one uniform size (the
   cycling word previously ran larger as an accent piece; client asked to
   try uniform sizing instead). */
.cycle-word-wrap,
.services-cycle-line {
  display: block;
}

.cycle-word-wrap { overflow: hidden; }
.cycle-word {
  display: inline-block;
  color: var(--primary);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
}
.cycle-word.is-swapping { opacity: 0; transform: translateX(16px); }

/* Trust strip — simple badge row, distinct from the homepage's kinetic
   marquee (that's a hero-adjacent moment; this is a quiet confirmation row) */
.trust-mini-strip {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px;
}
.trust-mini-item {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 22px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.trust-mini-item .material-symbols-outlined { color: var(--primary); font-size: 20px; }

/* Quick CTA band — gold, between the trust strip and the final embedded-form
   CTA. Mirrors the reference page's "contact us today" band structurally
   (heading + short line + two link-style actions), AZ voice and colors. */
.quick-cta-band {
  background: var(--primary);
  border-radius: var(--radius);
  padding: 48px 40px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.quick-cta-band h2 { color: #131313; font-size: clamp(36px, 5vw, 56px); }
.quick-cta-band p { color: #131313; opacity: 0.75; margin-top: 6px; max-width: 480px; }
.quick-cta-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.btn-dark-solid {
  background: #131313;
  color: #fff;
}
/* Solid dark, gold on hover — same precedent as .svc-cta-btn elsewhere on
   the site, applied to the general-purpose solid button class */
.btn-dark-solid:hover { background: var(--primary); color: #131313; transform: translateY(-2px); box-shadow: 0 8px 25px rgba(239, 206, 17, 0.3); }
.btn-dark-solid:active { transform: scale(0.97); }
.btn-dark-solid:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

@media (max-width: 767px) {
  .quick-cta-band { padding: 32px 24px; flex-direction: column; align-items: flex-start; }
  .quick-cta-actions { width: 100%; }
  .quick-cta-actions .btn { flex: 1; justify-content: center; }
}

/* ==========================================================================
   Shared: Split-Media (image/copy pairing — mobile order rule)
   design-standards.md — Mobile Order: Image/Media + Copy Sections
   ========================================================================== */

.split-media {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "eyebrow" "heading" "media" "body" "cta";
  row-gap: 20px;
  align-items: start;
}
.split-media-eyebrow { grid-area: eyebrow; }
.split-media-heading { grid-area: heading; font-size: clamp(36px, 5vw, 56px); }
.split-media-media { grid-area: media; }
.split-media-body { grid-area: body; color: var(--muted); font-size: 18px; }
.split-media-cta { grid-area: cta; }
.split-media-copy { display: contents; }

@media (min-width: 768px) {
  .split-media { grid-template-columns: 5fr 7fr; column-gap: 3rem; align-items: center; }
  .split-media--image-left { grid-template-areas: "media copy"; }
  .split-media--image-right { grid-template-areas: "copy media"; }
  .split-media-copy { display: flex; flex-direction: column; grid-area: copy; row-gap: 20px; }
}

/* Modifier: adds a 6th "accordion" row after cta, mobile only (grid
   areas only matter while .split-media-copy is display:contents).
   Scoped to this modifier so sections that don't use it — Why Choose
   Us, About — get no extra empty row/gap. Mobile-only media query so
   it doesn't fight the desktop 2-column "media copy" area map below. */
@media (max-width: 767px) {
  .split-media--with-accordion { grid-template-areas: "eyebrow" "heading" "media" "body" "cta" "accordion"; }
}
.split-media-accordion { grid-area: accordion; margin-top: 8px; }

/* Flips the column TRACK sizes (not just which area sits where) so
   this --image-left instance's media column matches --image-right's
   effective size (both 7fr) instead of the base rule's 5fr — see the
   HTML comment above the Our Work section for the full reasoning.
   Scoped to this modifier + --image-left combo only; other
   --image-left sections (services teaser, etc.) keep the base 5fr/7fr.
   align-items:stretch (overriding the base rule's center) makes the
   media column's own box match the row's full height — top edge level
   with the eyebrow, bottom edge level with the accordion's bottom —
   instead of a fixed-aspect box centered within a taller row. */
/* Flips the column TRACK sizes so the media column dominates (7fr)
   over copy (5fr) instead of the base 5fr/7fr — used where the photo
   should read as the section's focal point rather than the copy
   block. Scoped to this modifier so other --image-left/--image-right
   sections keep the base 5fr/7fr split. */
@media (min-width: 768px) {
  .split-media--media-dominant { grid-template-columns: 7fr 5fr; }
}

/* Photo-Grid Intro — for a combined service page (e.g. Accent & Media
   Walls) where the intro's job is showing two distinct real-photo
   examples side by side rather than pitching one photo + one long
   copy block like the standard split-media intro. Two photo/caption
   pairs stacked on mobile, side by side from tablet up. */
.photo-grid-intro {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  margin-top: 40px;
}
@media (min-width: 768px) {
  .photo-grid-intro { grid-template-columns: 1fr 1fr; gap: 40px; }
}
.photo-grid-item img {
  width: 100%;
  aspect-ratio: 5/4;
  object-fit: cover;
  border-radius: var(--radius);
  display: block;
}
.photo-grid-item h3 {
  font-size: 24px;
  margin: 20px 0 8px;
}
.photo-grid-item p {
  color: var(--muted);
  font-size: 16px;
  line-height: 1.6;
}

/* This section's video is object-fit:cover into its own real aspect ratio
   (3/4 = 1248x1664, the kitchen transformation video's actual dimensions),
   not the shared placeholder's 4:3. Previously forced into a client-tuned
   ~1066:940 near-square crop left over from the wet bar video this section
   used before the 2026-09-07 swap to the kitchen video — client feedback
   2026-09-07: that near-square box was visibly cropping the video, and
   asked to see the real un-cropped ratio instead (the same idea already
   used for this video's portrait framing on the portfolio page's vertical
   case study). Scoped to this one section (.split-media--with-accordion
   appears nowhere else) so no other .story-media-placeholder instance
   (About page owner photo, homepage Why Choose Us timeline) is affected.
   Applies at every width, not just desktop, so mobile isn't cropped
   differently from desktop either. */
.split-media--with-accordion .story-media-placeholder { aspect-ratio: 3 / 4; }

@media (min-width: 768px) {
  /* align-items:start (not stretch) — client direction 2026-09-07,
     round 3: video must match BOTH edges of the copy column (top level
     with the eyebrow, bottom level with the accordion), not just sit
     centered or bottom-anchored inside a taller box. A pure-CSS
     stretch + aspect-ratio attempt (round 2) couldn't do this reliably:
     with align-items:stretch, the grid still sizes its row track from
     each item's own natural/intrinsic height BEFORE stretch resolves,
     and a percentage height on a flex child (height:100%) against its
     not-yet-sized ancestor resolves as auto during that pass — so the
     video's aspect-ratio box fell back to filling the column's full
     737px WIDTH first and computed an oversized height from that
     instead, reinflating the row (measured: back to the original
     ~983px, the exact same number as the very first "too big" bug).
     Fixed instead by measuring in JS: align-items:start here means the
     grid still computes the row's track height from both items'
     natural sizes (so pre-JS, before the video shrinks, the row is
     briefly as tall as the video's own default full-width size), but
     — critically — it stops FORCING copy to stretch up into that
     height, so copy's rendered height is always its own true,
     unstretched content height. initOurWorkVideoHeight() (main.js)
     reads that real height and sets it directly as this placeholder's
     inline height; once applied, the video's own natural height
     shrinks to match, the row re-measures down to that same shared
     value, and both items end up exactly the same height, top-aligned
     by align-items:start — which, since neither has any dimension
     left over, makes their bottoms land in the same place too. */
  .split-media--with-accordion.split-media--image-left { grid-template-columns: 7fr 5fr; align-items: start; }
  .split-media--with-accordion .story-media-placeholder { width: auto; margin: 0 auto; }
}

/* "Opens upward": a normal top-anchored block can only ever push
   subsequent content down when it grows (its own top edge is fixed by
   whatever precedes it — true regardless of flex-direction tricks).
   Genuine upward growth needs the block bottom-anchored instead, so
   reserve a fixed zone tall enough for 3 collapsed rows + one open
   answer, and pin the real list to the BOTTOM of that zone. Collapsed,
   this leaves empty space above the list — the "gap before the
   accordion" — which is exactly the room a row expands into.
   Desktop/tablet-only (≥768px, see the mobile override below): this
   exists so the section's TOTAL height stays constant regardless of
   accordion state, because the media column sits side-by-side with
   the copy column there and needs to stay height-matched with it —
   letting the accordion push the row taller on expand would visually
   mismatch the two columns. Breakpoint values measured directly (not
   guessed) after the media column was widened to 7fr/5fr: the copy
   column is narrowest right at 768px (~307px wide, needs ~293px
   reserved) — narrower than mobile itself (375px needs only ~273px)
   — because the 2-column split kicks in before the row has regained
   enough total width for its 5fr share to grow back. It isn't safe
   past ~1024px (~386px+ wide, ~210-233px needed) until the column is
   wide enough that the answers stop wrapping as much. Too little
   height here isn't just wasted space — it makes the expanded item
   overflow upward into the CTA button above it. */
.split-media-accordion { position: relative; min-height: 310px; }
@media (min-width: 1024px) {
  .split-media-accordion { min-height: 250px; }
}
.split-media-accordion .faq-list { position: absolute; left: 0; right: 0; bottom: 0; }

/* Mobile: media stacks ABOVE copy instead of beside it, so there's no
   column to height-match — the whole reserved-zone/opens-upward trick
   above is solving a desktop-only problem. On mobile it was only
   producing dead space between the CTA and the visibly collapsed
   accordion (up to ~190px measured, reading as two disconnected
   sections — client-flagged 2026-09-05). Reverting to plain static
   flow here removes that dead space entirely: the list sits right
   after the CTA with just the grid's normal row-gap, and an opened
   answer simply pushes the section below it down like any ordinary
   accordion — perfectly fine on mobile since nothing needs to stay
   height-matched to it. */
@media (max-width: 767px) {
  .split-media-accordion { position: static; min-height: 0; }
  .split-media-accordion .faq-list { position: static; }
}

/* ==========================================================================
   Case Study — video-hero + thumbnail-strip. Sits inside .split-media-body
   (no dedicated grid area — nests under the existing "body" area so it
   doesn't need a --with-accordion-style modifier of its own) and
   .split-media-media (frame + thumbs + caption stacked as one block).
   ========================================================================== */

.case-study-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 20px 28px;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}
.case-study-fact { display: flex; flex-direction: column; gap: 4px; }
.case-study-fact-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.case-study-fact-value { font-size: 15px; font-weight: 600; color: var(--text); }

.case-study-media { display: flex; flex-direction: column; gap: 14px; }

.case-study-media-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface-2);
}
.case-study-media-frame video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.case-study-thumbs {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
}
/* 3-up modifier — for a case study with only 3 real photos instead of
   the base 4-up strip (client direction 2026-09-07: the wet bar case
   study's 4th thumbnail was a lower-quality duplicate of its 3rd,
   removed rather than kept as a placeholder). Same tile size/shape as
   the 4-up strip (aspect-ratio and gap untouched) — only the column
   count changes, so 3 real photos fill the row evenly instead of
   leaving a phantom 4th slot. Scoped to this modifier so the base
   `.case-study-thumbs` 4-column rule keeps governing every other/future
   case study that has a genuine 4-photo set. */
.case-study-thumbs.case-study-thumbs--3up { grid-template-columns: repeat(3, 1fr); }
/* Smaller, denser variant of .portfolio-card for the 4-up strip under
   the main video — same click-to-lightbox behavior (data-images +
   initPortfolioLightbox()), a smaller label so it doesn't overwhelm a
   quarter-width tile, and a landscape 4:3 shape matching the frame's
   own wide orientation above it. Chained with .portfolio-card (0,2,0)
   instead of a bare single-class rule (0,1,0), so this deliberately
   wins over .portfolio-card's own aspect-ratio (also 4/3) regardless
   of source order. Real bug caught 2026-09-03: the old bare-class
   version of this rule was silently losing that same specificity tie
   to .portfolio-card, which happens to also be 4/3 — so the shape
   looked right by coincidence, but wasn't actually governed by this
   rule, and the vertical variant's different 3:4 tiles below need a
   genuine working override here, not a lucky value match. */
.portfolio-card.case-study-thumb { aspect-ratio: 4 / 3; border-radius: calc(var(--radius) - 4px); }
.case-study-thumb .portfolio-card-overlay { padding: 10px; }
.case-study-thumb .portfolio-card-label { font-size: 11px; gap: 4px; }
.case-study-thumb .portfolio-card-label .material-symbols-outlined { font-size: 14px; }
@media (max-width: 480px) {
  .case-study-thumbs { gap: 8px; }
}

.case-study-media-caption { font-size: 14px; color: var(--muted); }

/* Vertical variant — main frame runs portrait instead of landscape, and
   the thumbnail strip sits beside it (a narrow stacked column) instead
   of underneath, for a project whose best photo/video is naturally tall
   rather than wide. Client direction 2026-09-07: this side-by-side
   arrangement now runs at every width, mobile included — the earlier
   mobile fallback (frame on top at full width, thumbs as a row of 4
   below) put a full-bleed 9:16 frame at ~2x the horizontal section's
   16:9 frame height on a phone screen, badly out of proportion next to
   it. Putting the frame beside a narrow thumbnail column instead keeps
   both sections feeling similarly scaled at every breakpoint. Touch
   targets checked down to a 320px container width before making this
   change — thumbnails stay well clear of the 48px floor at every real
   phone size (64x72px at 320px, up to 84x96px at 414px). */
/* Same 14px gap as .case-study-media (the horizontal variant's own
   frame-to-thumbs gap) — client direction 2026-09-03: the spacing
   between the big rectangle and the small ones must match exactly
   between the two layouts, not just look similar. */
.case-study-media-row { display: flex; flex-direction: column; gap: 14px; }

/* 9:16 — the true 90° rotation of the horizontal frame's 16:9, kept as
   a fallback shape only. Chained with .case-study-media-frame for
   specificity over .story-media-placeholder's own aspect-ratio. As of
   2026-09-07 the height:100cqw rule further down (unconditional, every
   width) always supplies an explicit height, so this aspect-ratio has
   no effect in practice — it only matters if that rule is ever removed.
   Source video/photos for this frame are still cut to 9:16 so the crop
   itself has headroom regardless of which shape rule is live. */
.case-study-media-frame.case-study-media-frame--vertical { aspect-ratio: 9 / 16; }

/* Container query units (cqw) below solve the thumbnail column's width
   and the frame's height so the 4 tiles + gaps land precisely flush
   against the frame, at every width. */
.case-study-media--vertical { container-type: inline-size; }

.case-study-media--vertical .case-study-media-row { flex-direction: row; align-items: flex-start; }
/* flex:1/min-width:0 is what lets the frame share the row with the
   thumbnail column at any width. The height:100cqw rule further down
   sets the actual height at every width — see that rule. */
.case-study-media--vertical .case-study-media-frame { flex: 1; min-width: 0; }
.case-study-media--vertical .case-study-thumbs {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-self: stretch;
  /* Client direction 2026-09-03 (desktop-tuned, now applied at every
     width): shrink the frame a bit / grow the thumbnails a bit relative
     to a naive equal split — flex:1 on the frame just absorbs whatever
     width this column doesn't use, so this value alone controls the
     big-rectangle-vs-small-ones balance. */
  flex: 0 0 calc(22cqw - 6.6px);
}
/* 3-class selector (0,3,0) — deliberately more specific than
   .case-study-thumbs .case-study-thumb-placeholder's own 2-class base
   rule (0,2,0) below, so this wins at every width, not just desktop.
   Two 2-class rules tied on specificity is exactly the bug this whole
   file already has one write-up for
   (modifier-class-loses-to-shared-component-same-specificity.md). Not a
   fixed 3:4 — the thumbnail column's width is tuned independently of
   the true-rotation value, so a fixed aspect-ratio would drift out of
   sync with the frame's actual height; height is pinned directly
   instead, derived from the frame's own rendered height (100cqw, set
   below) divided across 4 tiles + 3 gaps. */
.case-study-media--vertical .case-study-thumbs .case-study-thumb,
.case-study-media--vertical .case-study-thumbs .case-study-thumb-placeholder {
  aspect-ratio: auto;
  width: 100%;
  flex: 0 0 auto;
  /* Exact desktop-derived formula, applied at every width, not just
     768px+ — client direction 2026-09-07: mobile must be a true scaled
     copy of the desktop shape, not an independently-tuned 9:16 portrait.
     Both dimensions are cqw-relative, so the ratio is scale-invariant by
     construction: (frame height [100cqw] - 30px [3 gaps]) / 4 tiles. */
  height: calc(25cqw - 7.5px);
}

/* Height pinned to 100cqw (the container's full width) at every width,
   not just desktop — client direction 2026-09-07: this is what makes
   the frame land at the exact same shape relative to the section,
   scaled uniformly from desktop down to phone, deliberately shorter/
   boxier than a true 9:16 rotation, tuned over 3 rounds on desktop and
   now applied everywhere instead of mobile falling back to true 9:16.
   Width and height become independent, definite values (flex:1 /
   100cqw) so the aspect-ratio declaration above has no effect at any
   width. */
.case-study-media--vertical .case-study-media-frame { height: 100cqw; }

/* Coming-soon state for a case-study slot with no real project content
   yet (client direction 2026-09-03: build the section now, fill it in
   once real photos/video for the next project exist). Reuses
   .story-media-placeholder's exact dashed-hatch visual language rather
   than a blank/broken-looking empty box, so it reads as deliberate.
   Generic fallback shape (4:3, matching .case-study-thumb's own
   real-photo shape) for any future case-study placeholder that isn't
   the vertical variant below — the vertical "coming soon" section's own
   placeholders are always overridden by its more specific stacked-column
   rule (now unconditional, not just at 768px+, per the 2026-09-07
   mobile-layout change above), so this rule no longer actually governs
   them at any width. Bug caught 2026-09-03, before that change: this
   rule was left at 1/1 when the real .case-study-thumb shape was
   corrected to 4/3, so the vertical section's mobile placeholders
   (which at the time still fell back to this rule below 768px) stayed
   square while desktop's override already read 3/4 — a mismatch easy to
   miss without checking mobile specifically. */
.case-study-thumbs .case-study-thumb-placeholder {
  aspect-ratio: 4 / 3;
  border-radius: calc(var(--radius) - 4px);
}
/* Scoped as .story-media-placeholder's own descendant (0,2,0) rather
   than a bare .case-study-pending-badge (0,1,0), so it beats that
   shared placeholder's generic ".story-media-placeholder span" rule
   (0,1,1) instead of losing to it on padding/text-align/color. */
.story-media-placeholder .case-study-pending-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 20px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: left;
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: 100px;
  background: var(--surface);
}
/* Higher specificity than .story-media-placeholder span's own
   text-transform/padding/letter-spacing (0,1,1) so the nested icon
   glyph isn't corrupted by it — text-transform:uppercase in particular
   would rewrite the ligature text Material Symbols matches against,
   silently breaking the icon into literal visible text. */
.case-study-pending-badge .material-symbols-outlined {
  font-size: 16px;
  color: var(--primary);
  padding: 0;
  text-transform: none;
  letter-spacing: normal;
}

/* ==========================================================================
   About Snapshot
   ========================================================================== */

.about-photo {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  overflow: hidden;
  background: linear-gradient(160deg, var(--surface-2) 0%, var(--border) 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed rgba(19, 19, 19, 0.15);
}

.about-photo img,
.about-photo video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Video controls bar — seek/scrub range + mute toggle, for an
   autoplaying video with real audio (the Victor welcome clip). Sits
   as one row pinned to the video's bottom edge so visitors can drag
   to any point (restart from the top included) on desktop or mobile,
   not just mute/unmute. Client direction 2026-09-02.
   Hidden by default and revealed on hover (desktop, real pointers only
   — see the hover:hover media query below, so a touchscreen's "sticky
   hover" after a tap can't leave it stuck open) or via the .controls-
   visible class (JS-driven tap-to-reveal + timed auto-hide on touch,
   initVideoControlsReveal() in main.js). :has(:focus-visible) keeps
   it shown while a control has genuine keyboard focus — deliberately
   NOT plain :focus-within, which would also match the lingering focus
   a browser leaves on the range input after an ordinary mouse/touch
   drag, permanently blocking the auto-hide timer from ever visibly
   taking effect. Client direction 2026-09-05. */
.video-controls {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 16px;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 12px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.about-photo--filled.controls-visible .video-controls,
.about-photo--filled:has(:focus-visible) .video-controls {
  opacity: 1;
  pointer-events: auto;
}
@media (hover: hover) and (pointer: fine) {
  .about-photo--filled:hover .video-controls {
    opacity: 1;
    pointer-events: auto;
  }
}
/* Soft scrim behind the bar only (not the whole photo) so the gold
   track/thumb stay legible over a bright frame of video */
.video-controls::before {
  content: '';
  position: absolute;
  inset: -40px -16px -16px -16px;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(to top, rgba(19,19,19,0.7) 0%, rgba(19,19,19,0.35) 55%, rgba(19,19,19,0) 100%);
}
.video-seek {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 20px;
  background: transparent;
  cursor: pointer;
  --seek-pct: 0%;
}
/* Track: gold-fill-to-here, dim rest, via a background gradient driven
   by --seek-pct (set from JS on timeupdate/input) rather than the
   ::-webkit-slider-fill pseudo (no cross-browser equivalent) */
.video-seek::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: 3px;
  background: linear-gradient(to right, var(--primary) 0%, var(--primary) var(--seek-pct), rgba(255,255,255,0.35) var(--seek-pct), rgba(255,255,255,0.35) 100%);
}
.video-seek::-moz-range-track {
  height: 5px;
  border-radius: 3px;
  background: rgba(255,255,255,0.35);
}
.video-seek::-moz-range-progress {
  height: 5px;
  border-radius: 3px;
  background: var(--primary);
}
.video-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  margin-top: -5.5px;
  border-radius: 50%;
  background: var(--primary);
  border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(0,0,0,0.4);
  transition: transform 0.2s var(--ease);
}
.video-seek::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--primary);
  border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(0,0,0,0.4);
  transition: transform 0.2s var(--ease);
}
.video-seek:hover::-webkit-slider-thumb,
.video-seek:active::-webkit-slider-thumb { transform: scale(1.15); }
.video-seek:hover::-moz-range-thumb,
.video-seek:active::-moz-range-thumb { transform: scale(1.15); }
.video-seek:focus-visible { outline: none; }
.video-seek:focus-visible::-webkit-slider-thumb { outline: 2px solid #fff; outline-offset: 2px; }
.video-seek:focus-visible::-moz-range-thumb { outline: 2px solid #fff; outline-offset: 2px; }

.video-mute-toggle {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(19, 19, 19, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.35);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: background 0.3s var(--ease), transform 0.2s var(--ease);
}
.video-mute-toggle:hover { background: var(--primary); color: #131313; border-color: var(--primary); }
.video-mute-toggle:active { transform: scale(0.93); }
.video-mute-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.video-mute-toggle .material-symbols-outlined { font-size: 20px; }

/* Real photo in place — drop the dashed placeholder border/gradient */
.about-photo--filled {
  border: none;
  background: none;
}

.about-photo span {
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: center;
  padding: 0 24px;
}

.about-owner-line {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 8px;
}

.about-owner-line .material-symbols-outlined {
  color: var(--primary);
  font-size: 22px;
}

.about-owner-line strong { font-size: 15px; display: block; }
.about-owner-line span.role { font-size: 13px; color: var(--muted); }

/* ==========================================================================
   Services Grid — Photo Tiles (image / heading / body / link)
   Reference: shadcnblocks "Feature 287" — outer card + inset image frame,
   heading, body, link, in that vertical order. 6 regular tiles in a 3x2
   grid + 1 wide tile spanning full width for the last service, matching
   design-standards.md's Mobile Order rule (CSS Grid + named areas) for
   the wide tile's image/copy pairing. Card radius is var(--radius) — the
   house base-shape default (design-doctrine-trust-first.md: cards are
   base shape, never a bespoke radius).
   ========================================================================== */

.services-tile-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}

/* 4-column variant — Service Areas hub's Featured Areas tiles (4 towns,
   client wants all 4 on one row instead of wrapping 3+1). Placed before
   the responsive overrides below so tablet/mobile still win at their
   breakpoints (equal specificity, later source order wins the tie). */
.services-tile-grid--four-col { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 1023px) {
  .services-tile-grid { grid-template-columns: repeat(2, 1fr); }
  .service-tile--wide { grid-column: 1 / -1; }
}

@media (max-width: 767px) {
  .services-tile-grid { grid-template-columns: 1fr; gap: 20px; }
}

/* A real <a> now (client-reported bug 2026-09-02: the hover lift/shadow
   made the whole card read as clickable, but only the small nested
   "Learn More" text actually navigated) — reset link defaults so it
   still reads as a plain card, not a wall-of-text link. */
.service-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
.service-tile:hover { transform: translateY(-4px); box-shadow: 0 16px 32px rgba(19, 19, 19, 0.08); }
.service-tile:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

.service-tile-media {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: calc(var(--radius) - 4px);
  overflow: hidden;
  margin-bottom: 20px;
  background: var(--surface-2);
}
.service-tile-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.5s var(--ease);
}
.service-tile:hover .service-tile-media img { transform: scale(1.05); }

/* Placeholder variant — reuses .story-media-placeholder's exact visual
   language (same hatch gradient, dashed border) for a .service-tile-media
   slot that has no real photo yet (e.g. a town's welcome-sign photo,
   per location-page-blueprint.md's Hero Image Rule, not yet sourced). */
.service-tile-media--placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background:
    repeating-linear-gradient(135deg, rgba(19,19,19,0.04) 0 2px, transparent 2px 24px),
    linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 60%, var(--border) 100%);
  border: 1px dashed var(--border);
}
.service-tile-media--placeholder .material-symbols-outlined { font-size: 32px; color: var(--muted); }
.service-tile-media--placeholder span:last-child {
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: center;
  padding: 0 16px;
}

.service-tile h3 { font-size: 22px; margin-bottom: 8px; }
.service-tile p { color: var(--muted); font-size: 16px; line-height: 1.6; margin-bottom: 20px; flex-grow: 1; }

.service-learn-more {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.service-learn-more .arrow { transition: transform 0.3s var(--ease); }
.service-tile:hover .service-learn-more .arrow { transform: translateX(4px); }

/* Wide tile (last service, e.g. Flooring) — text left, taller portrait
   image right. Portrait ratio matches the site's own .about-photo
   convention (4/5) rather than inventing a new one. Mobile order forced
   via named grid areas (design-standards.md Mobile Order rule), same
   mechanism as the site's .split-media component: heading, then media,
   then body, then the link — media visually interrupts the copy instead
   of sitting before or after all of it. */
.service-tile--wide {
  grid-column: 1 / -1;
  padding: 20px;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "heading" "media" "body" "cta";
  row-gap: 16px;
}
.service-tile--wide .service-tile-media {
  grid-area: media;
  aspect-ratio: 16 / 9;
  margin-bottom: 0;
}
.service-tile--wide .service-tile-copy { display: contents; }
.service-tile--wide h3 { grid-area: heading; margin-bottom: 0; }
.service-tile--wide .service-tile-body { grid-area: body; }
.service-tile--wide .service-tile-body p { margin-bottom: 0; }
.service-tile--wide .service-tile-cta { grid-area: cta; }

/* Text column pinned to the same width as a regular tile's copy column
   (~360px) so it reads consistently with the other six tiles instead of
   stretching full-width. Gap tightened to the site's standard 24px grid
   gap (was 48px) so the photo's left edge lines up with the column
   boundary from the row above instead of leaving a visible dead zone.
   Image height is fixed rather than aspect-ratio-driven so it matches
   the copy block's own height instead of hanging far past it. */
@media (min-width: 768px) {
  .service-tile--wide {
    grid-template-columns: 360px 1fr;
    grid-template-areas: "copy media";
    column-gap: 24px;
    row-gap: 0;
    align-items: start;
  }
  .service-tile--wide .service-tile-copy {
    display: flex;
    flex-direction: column;
    grid-area: copy;
    row-gap: 14px;
    align-items: flex-start;
  }
  .service-tile--wide .service-tile-media {
    aspect-ratio: auto;
    height: 320px;
  }
}

/* ==========================================================================
   Differentiators
   ========================================================================== */

.diff-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
}

/* 2-card variant (cost/permit-style callouts) — used instead of an
   inline style so the shared mobile stack-to-1-column rule below
   actually applies. An inline grid-template-columns on the element
   itself would outrank the @media rule at any width, which is what
   squished this into two narrow, word-wrapped columns on mobile
   across every service-area hub + combo page copying this pattern. */
.diff-grid-2col {
  grid-template-columns: repeat(2, 1fr);
  max-width: 900px;
  margin: 0 auto;
}

@media (max-width: 767px) {
  .diff-grid,
  .diff-grid-2col { grid-template-columns: 1fr; gap: 20px; max-width: none; margin: 0; }
}

.diff-card {
  padding: 36px 28px;
  background: var(--surface);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
.diff-card:hover { transform: translateY(-4px); box-shadow: var(--shadow); }

.diff-icon {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--primary);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 20px;
}
.diff-icon .material-symbols-outlined { font-size: 26px; color: #131313; }

.diff-card h3 { font-size: 22px; margin-bottom: 10px; }
.diff-card p { color: var(--muted); font-size: 16px; }

/* ==========================================================================
   Stats Bar — Odometer Counters (Module 20)
   ========================================================================== */

.stats-section { background: var(--bg); }

.stats-gauge-value {
  font-family: var(--font-heading);
  font-size: clamp(24px, 3.4vw, 30px);
  color: var(--primary);
  letter-spacing: -0.02em;
  line-height: 1;
  display: flex;
  align-items: baseline;
  flex-shrink: 0;
}
.stats-gauge-value .odo-digit { display: inline-block; overflow: hidden; height: 1em; position: relative; }
.stats-gauge-value .odo-strip { display: flex; flex-direction: column; transition: transform 1.5s cubic-bezier(0.16, 1, 0.3, 1); }
.stats-gauge-value .odo-strip span { display: block; height: 1em; line-height: 1; }
.stats-gauge-value .odo-suffix { margin-left: 2px; }

/* ==========================================================================
   Testimonials / Live Reviews
   ========================================================================== */

.reviews-rating-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-bottom: 40px;
}
.reviews-rating-row .stars { color: var(--primary); font-size: 22px; letter-spacing: 3px; }
.reviews-rating-row .rating-text { font-weight: 600; font-size: 16px; }
.reviews-rating-row a { color: var(--muted); font-size: 14px; text-decoration: underline; text-underline-offset: 3px; }
.reviews-rating-row a:hover { color: var(--text); }

.reviews-shell {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
@media (max-width: 900px) {
  .reviews-shell { grid-template-columns: 1fr; }
}

.review-skel-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 28px;
}
.review-skel-stars { color: var(--primary); font-size: 16px; letter-spacing: 2px; margin-bottom: 14px; }

.skel-line {
  height: 12px;
  border-radius: 6px;
  background: linear-gradient(90deg, var(--surface-2) 0%, var(--border) 50%, var(--surface-2) 100%);
  background-size: 200% 100%;
  animation: skelShimmer 1.8s infinite;
  margin-bottom: 10px;
}
.skel-line.short { width: 55%; }
@keyframes skelShimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

.reviews-cta { text-align: center; margin-top: 32px; }
.reviews-embed-note {
  text-align: center;
  font-size: 12px;
  color: var(--muted);
  margin-top: 16px;
  letter-spacing: 0.02em;
}

/* ==========================================================================
   Our Process page — /our-process/, built 2026-09-02.
   (.process-steps/.process-num previously existed here as an unused,
   never-referenced-in-HTML leftover from an earlier iteration —
   replaced outright rather than left as dead code alongside these.)
   ========================================================================== */

/* Intro — eyebrow/heading/intro-paragraph stack with the real AZ logo
   watermarked behind the heading (client reference:
   nextlevelremodeling.com/process/ uses a small house-line icon above
   its heading; client asked for AZ's own logo layered BEHIND the text
   instead, at low opacity — client's literal words: "have the drop
   opacity on the logo and layer How It Works in front of it").
   AZ-logo-dark.svg (transparent background, dark strokes for a light
   section) — not .reveal-up, since that utility's GSAP-driven inline
   opacity would fight this element's own low-opacity CSS. */
.process-intro { position: relative; text-align: center; }
.process-intro-watermark {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 520px;
  max-width: 90vw;
  height: auto;
  opacity: 0.06;
  pointer-events: none;
  z-index: 0;
}
.process-intro .section-head { position: relative; z-index: 1; }
.process-intro-body { max-width: 760px; margin: 0 auto; position: relative; z-index: 1; }
.process-intro-body p { color: var(--muted); font-size: 16px; line-height: 1.7; }

/* Steps — each step is its own full-width section (alternating
   background, matching how this site alternates backgrounds between
   content blocks elsewhere) rather than a card grid, since these carry
   full paragraphs, not one-line teasers like the homepage's
   .phase-timeline. */
.process-step-section { padding: 40px 0; }
.process-step {
  display: grid;
  grid-template-columns: 200px 1fr;
  column-gap: 40px;
  align-items: start;
}
.process-step-num-wrap { display: flex; flex-direction: column; }
.process-num { font-family: var(--font-heading); font-size: 64px; color: var(--primary); line-height: 1; opacity: 0.4; }
.process-step h3 { font-size: 22px; margin-top: 6px; }
.process-step-accent { width: 40px; height: 3px; background: var(--primary); margin-top: 10px; margin-bottom: 4px; }
.process-step p { color: var(--muted); font-size: 16px; line-height: 1.7; }
@media (max-width: 767px) {
  .process-step { grid-template-columns: 1fr; row-gap: 4px; }
  .process-step-section { padding: 32px 0; }
}

/* Callout — small highlighted aside inside a step's paragraph column
   (used once, inside Step 3, for the on-site live-design detail) */
.process-callout {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  margin-top: 16px;
  padding: 16px 18px;
  border-radius: var(--radius);
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.process-callout .material-symbols-outlined { color: var(--primary); font-size: 22px; flex-shrink: 0; margin-top: 1px; }
.process-callout p { margin: 0; font-size: 16px; color: var(--text); }
.process-callout strong { color: var(--text); }

/* Real-projects photo strip, between the steps and the final CTA */
.process-gallery-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}
.process-gallery-photo { aspect-ratio: 4 / 3; border-radius: var(--radius); overflow: hidden; }
.process-gallery-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (max-width: 900px) {
  .process-gallery-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ==========================================================================
   Service Area
   ========================================================================== */

.town-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 4px 0 8px;
}

.town-pill {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  font-size: 14px;
  font-weight: 600;
  padding: 9px 18px;
  border-radius: 100px;
  border: 1px solid var(--border);
  background: var(--surface);
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease), color 0.3s var(--ease), transform 0.2s var(--ease);
}
.town-pill:hover { border-color: var(--primary); background: var(--primary); color: #131313; transform: translateY(-2px); }
.town-pill:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Towns named as served but without a dedicated page yet (Phase 2) — same
   pill shape for visual consistency, but not styled as a clickable link
   since there's nowhere real for it to go yet. Swap back to a real <a>
   the same session that town's page ships. */
.town-pill--soon { cursor: default; opacity: 0.75; }
.town-pill--soon:hover { border-color: var(--border); background: var(--surface); color: inherit; transform: none; }

.map-embed {
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
  aspect-ratio: 4 / 3;
}
.map-embed iframe { width: 100%; height: 100%; border: 0; }

/* Service area map — replaces the old Google Maps iframe (pinned to the
   owner's home address, no public office). A real Mapbox Static Images API
   base map (license-clean, same product as the /service-areas/ town tiles —
   see .claude/skills/service-area-map-tiles/SKILL.md) desaturated onyx-dark,
   with the real Pennsauken Township boundary (OpenStreetMap/Nominatim
   admin polygon, simplified) drawn as an animated gold dotted outline —
   never a pin on the exact house. Client direction 2026-09-02. */
.service-area-map {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
}
.service-area-map img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(80%) brightness(0.6) contrast(1.15);
}
.service-area-map-tint {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 50%, rgba(19,19,19,0.15) 0%, rgba(19,19,19,0.55) 100%);
}
.service-area-map-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* Boundary stays still (client direction 2026-09-02 — the "marching
   ants" dashoffset animation read as too busy) — thinner stroke, longer
   dash per segment, same gap as before. */
.service-area-map-boundary {
  fill: none;
  stroke: var(--primary);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 3 11;
}
.service-area-map-dot {
  fill: #fff6d5;
  filter: drop-shadow(0 0 4px var(--primary)) drop-shadow(0 0 9px var(--primary));
}
/* Comet-style trailing tail on the moving dot: four ghost circles run the
   same animateMotion path/duration as the main dot but with a small
   positive `begin` delay each, so at any instant each one is rendered at
   the position the dot itself passed through slightly earlier — a lag,
   not a duplicate. Decreasing radius + opacity (no glow filter, to avoid
   stacking bloom) tapers it into a soft fading tail instead of a hard
   trailing dot chain. */
.service-area-map-dot--trail { filter: none; }
.service-area-map-dot--trail-1 { opacity: 0.5; }
.service-area-map-dot--trail-2 { opacity: 0.3; }
.service-area-map-dot--trail-3 { opacity: 0.16; }
.service-area-map-dot--trail-4 { opacity: 0.07; }
.service-area-map-label {
  position: absolute;
  left: 16px;
  bottom: 16px;
  z-index: 2;
  padding: 6px 12px;
  border-radius: var(--radius);
  background: rgba(19, 19, 19, 0.65);
  backdrop-filter: blur(4px);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* ==========================================================================
   FAQ Accordion
   ========================================================================== */

.faq-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.faq-item { background: var(--surface); }

.faq-question {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 24px 28px;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 17px;
  color: var(--text);
  transition: background 0.3s var(--ease);
}
/* Was missing a hover state entirely -- cursor:pointer alone gave no visual
   acknowledgment before clicking, caught in the 2026-09-08 pre-launch
   sweep's Thunk Test pass. Same elevation-shift language as other cards. */
.faq-question:hover { background: var(--surface-2); }
.faq-question:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

.faq-icon { flex-shrink: 0; width: 24px; height: 24px; position: relative; }
.faq-icon::before, .faq-icon::after {
  content: '';
  position: absolute;
  background: var(--primary);
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}
.faq-icon::before { width: 14px; height: 2px; }
.faq-icon::after { width: 2px; height: 14px; transition: transform 0.3s var(--ease); }
.faq-item.is-open .faq-icon::after { transform: translate(-50%, -50%) rotate(90deg) scaleX(0); }

.faq-answer { max-height: 0; overflow: hidden; transition: max-height 0.4s var(--ease); }
.faq-answer-inner { padding: 0 28px 24px; color: var(--muted); font-size: 15px; line-height: 1.7; }

/* Modifier: thinner collapsed rows for a narrower column (Our Work
   accordion, sitting in a squeezed copy column next to a widened
   media panel) — frees up vertical room without touching the FAQ
   section's own (unmodified) row thickness. */
.faq-list--compact .faq-question { padding: 14px 20px; font-size: 16px; }
.faq-list--compact .faq-answer-inner { padding: 0 20px 16px; font-size: 14px; }
.faq-list--compact .faq-icon { width: 20px; height: 20px; }
.faq-list--compact .faq-icon::before { width: 12px; }
.faq-list--compact .faq-icon::after { height: 12px; }

/* ==========================================================================
   Final CTA Banner
   ========================================================================== */

/* Deliberately var(--bg-deep), not var(--bg) — the form card floating on
   top is Surface (#1C1C1C, lighter than Background), and this section
   sits directly above the footer (also plain Background, #131313).
   Without a visibly distinct tone here, the CTA band and the footer
   below it were two back-to-back sections at the identical color with
   no seam between them. */
.final-cta-section { background: var(--bg-deep); }

/* Contact page only: this CTA band sits directly under the no-media
   hero header, not directly above the footer (a light town-pills
   section comes between it and the footer here), so --bg-deep's
   separation-from-footer purpose doesn't apply and instead reads as a
   mismatch against the hero's onyx tone right above it. Client
   direction 2026-08-20: match the hero, not the footer, on this page. */
.final-cta-section--match-hero { background: var(--bg); }

.final-cta-grid {
  display: grid;
  grid-template-columns: 0.75fr 1.25fr;
  gap: 56px;
  align-items: stretch;
}
/* Grid items default to min-width:auto, so a child with a wide unbreakable
   row (e.g. the email address in .contact-info-row) refuses to shrink below
   its own content width even inside a 1fr track that's already been sized
   down for a narrow viewport -- the whole grid "blows out" past the
   container instead of collapsing. min-width:0 lets tracks actually honor
   the 1fr collapse below. */
.final-cta-grid > * { min-width: 0; }
@media (max-width: 900px) {
  .final-cta-grid { grid-template-columns: 1fr; gap: 40px; }
}

/* Flips which side the form sits on via `order`, not DOM order, so the
   HTML stays in natural reading order (copy first, form second) and
   mobile still shows heading -> info -> form. Same technique as
   .trust-panel-grid--image-left. Contact page only. */
@media (min-width: 901px) {
  .final-cta-grid--form-left { grid-template-columns: 1.3fr 0.7fr; }
  .final-cta-grid--form-left > :first-child { order: 2; }
  .final-cta-grid--form-left > :last-child { order: 1; }
}

/* Compact "Information" card — phone/email rows + social icons, next
   to nextlevelremodeling.com/contact's gray info-card reference
   (client direction 2026-09-08). Replaces the old oversized clickable
   phone number + separate hours/social blocks. */
.contact-info-card {
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: 40px;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Sub-label inside the card ("Information", "Follow Us") — same job as
   .footer-col h4, not a section heading. Was rendering at the full
   clamp(36px,5vw,56px) H2 scale (up to 56px) before this fix, which put
   a card sub-label at the same visual weight as the actual H2 above it
   in the same card ("Tell Us About Your Project") -- caught in the
   2026-09-08 pre-launch sweep's heading-role-consistency audit. */
.contact-info-heading {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 16px;
}
.contact-info-row {
  display: flex;
  align-items: center;
  gap: 14px;
  color: var(--text);
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 14px;
}
.contact-info-row:last-of-type { margin-bottom: 0; }
.contact-info-icon {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  background: var(--primary);
  color: #131313;
  display: flex;
  align-items: center;
  justify-content: center;
}
.contact-info-icon .material-symbols-outlined { font-size: 20px; }
.contact-info-divider { height: 1px; background: var(--border); margin: 22px 0; }
.contact-info-social a {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--text);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.3s var(--ease), color 0.3s var(--ease);
}
.contact-info-social a:hover { background: var(--primary); color: #131313; }
.contact-info-social .footer-social-icon { width: 16px; height: 16px; }

.cta-form {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 32px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  border: 1px solid var(--border);
}
.cta-form label {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 6px;
  display: block;
}
.cta-form input, .cta-form select, .cta-form textarea {
  width: 100%;
  padding: 14px 16px;
  min-height: 48px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  transition: border-color 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
.cta-form textarea { min-height: 96px; resize: vertical; }
.cta-form input:focus, .cta-form select:focus, .cta-form textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(239, 206, 17, 0.2);
}
.cta-form .btn { width: 100%; justify-content: center; margin-top: 4px; }

/* Wraps the GoHighLevel form iframe embed in the same card treatment
   as .cta-form, so the third-party widget still reads as part of the
   page instead of a bare floating iframe. */
.cta-form-embed {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 16px;
  border: 1px solid var(--border);
}
.cta-form-embed iframe { display: block; width: 100%; border: none; }

/* Centered single-column layout, homepage final-cta-section (client
   direction 2026-09-07, round 2) — replaces the old two-column
   .final-cta-grid (phone number + note on the left, a card of buttons
   on the right) once the phone link and the redundant "or request a
   quote" note were both dropped as duplicating the Call Now button and
   the paragraph above it. Not yet the pattern for the other 36 pages
   still using .final-cta-grid — this is homepage-only pending approval,
   see agency-learnings.md. */
.final-cta-centered { text-align: center; max-width: 640px; margin: 0 auto; }

/* Button pair — plain centered row (same pattern as .hero-ctas), not
   a card, since it now sits directly under centered copy in one column
   rather than occupying its own grid column next to a phone number. */
.final-cta-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px;
  margin-top: 28px;
}
/* Solid white, not .btn-secondary's solid onyx — this section is
   already dark (.section-dark), so an onyx button on an onyx
   background would be nearly invisible, the same pitfall documented
   for .consult-gallery-content .btn-secondary elsewhere in this file. */
.btn-cta-call { background: #fff; color: #131313; }
.btn-cta-call:hover { background: var(--primary); color: #131313; transform: translateY(-2px); box-shadow: 0 8px 25px rgba(239, 206, 17, 0.3); }
.btn-cta-call:active { transform: scale(0.97); }
.btn-cta-call:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ==========================================================================
   Footer
   ========================================================================== */

.footer { background: var(--bg); padding: 80px 0 32px; }

/* Columns size to their own content (auto), then space-between spreads the
   leftover row width evenly across the gaps — this is what makes the visual
   gap consistent column-to-column regardless of how much whitespace a given
   column's text leaves inside a fixed fr-track. gap is a floor, not the only
   source of spacing; space-between adds the rest on top. */
.footer-grid {
  display: grid;
  grid-template-columns: auto auto auto auto;
  justify-content: space-between;
  gap: 32px;
  margin-bottom: 56px;
}
@media (max-width: 900px) { .footer-grid { grid-template-columns: 1fr 1fr; justify-content: initial; gap: 36px; } }
@media (max-width: 540px) { .footer-grid { grid-template-columns: 1fr; } }

.footer-logo img { height: 112px; margin-bottom: 24px; }
.footer-desc { color: rgba(255, 255, 255, 0.6); font-size: 14px; max-width: 280px; margin-bottom: 20px; }

.footer-social { display: flex; gap: 12px; }
/* .footer-grid prefix bumps specificity above .footer-col a — the social
   icons now live inside a .footer-col (moved under Licensing), and without
   this, .footer-col a's `display:block` wins on source order and collapses
   the icon back to inline, breaking the centering. */
.footer-grid .footer-social a {
  width: 40px; height: 40px;
  margin-bottom: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.15);
  display: flex; align-items: center; justify-content: center;
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
}
.footer-grid .footer-social a:hover { background: var(--primary); border-color: var(--primary); }
.footer-grid .footer-social a:hover .footer-social-icon { color: #131313; }
.footer-grid .footer-social a .footer-social-icon { color: #fff; width: 16px; height: 16px; }

.footer-col h4 {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
  margin-bottom: 18px;
}
.footer-social-heading { margin-top: 20px; }
.footer-col a, .footer-col p {
  display: block;
  color: rgba(255, 255, 255, 0.75);
  font-size: 14px;
  margin-bottom: 12px;
  transition: color 0.3s var(--ease);
}
.footer-col a:hover { color: var(--primary); }

/* License number link also copies the HIC number to the clipboard on
   click (initLicenseCopy() in main.js) before opening NJ's verification
   search in a new tab, so a visitor can paste it straight into the
   License Number field once they land there. No extra button, the
   existing link just does double duty; this small toast is the only
   visual sign the copy happened. */
.license-verify-link { position: relative; }
.license-copied-toast {
  position: absolute;
  left: 0;
  bottom: 100%;
  margin-bottom: 6px;
  padding: 4px 10px;
  border-radius: var(--radius);
  background: var(--primary);
  color: #131313;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
  transition: opacity 0.2s var(--ease), transform 0.2s var(--ease);
}
.license-copied-toast.is-visible { opacity: 1; transform: translateY(0); }

/* Information column — icon + text rows, instead of plain stacked links.
   max-width caps the longest lines (email, address) so this column wraps
   to two lines instead of stretching the whole row wider than intended. */
.footer-col--info a, .footer-col--info p {
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 230px;
}
.footer-icon { font-size: 18px; color: var(--primary); flex-shrink: 0; }

.footer-bottom {
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  padding-top: 28px;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 16px;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.5);
}
.footer-credit {
  color: #fff;
  font-family: var(--font-heading);
  font-size: 17px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  transition: opacity 0.3s var(--ease);
}
.footer-credit:hover { opacity: 0.75; }
.footer-credit-accent { color: var(--primary); }
.footer-legal-links { display: flex; gap: 20px; }
.footer-legal-links a { color: rgba(255, 255, 255, 0.5); text-decoration: none; transition: color 0.3s var(--ease); }
.footer-legal-links a:hover { color: var(--primary); }

/* ==========================================================================
   Mobile Sticky CTA Bar — Call / Request Quote / Text Us
   web-build.md: build day one, always visible on mobile
   ========================================================================== */

.mobile-cta-bar {
  display: flex;
  position: fixed;
  bottom: 0; left: 0; right: 0;
  z-index: 999999;
  background: #131313;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.25);
}
.mobile-cta-bar a {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 9px 4px 12px;
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  min-height: 56px;
  transition: background 0.2s var(--ease);
}
.mobile-cta-bar a .material-symbols-outlined { font-size: 21px; color: var(--primary); }
.mobile-cta-bar a.is-primary { background: var(--primary); color: #131313; }
.mobile-cta-bar a.is-primary .material-symbols-outlined { color: #131313; }
.mobile-cta-bar a:active { background: rgba(255, 255, 255, 0.08); }
.mobile-cta-bar a.is-primary:active { background: #d9b70f; }

@media (min-width: 1024px) {
  .mobile-cta-bar { display: none; }
}
@media (max-width: 1023px) {
  body { padding-bottom: 64px; }
}

/* ==========================================================================
   Legal Content — Privacy Policy / Terms & Conditions pages. Plain
   document-style typography, not marketing section headings -- these H2s
   are a genuinely different job (document hierarchy inside a wall of
   text) than the site's section-intro H2 scale, per design-standards.md's
   "component-specific size is only legitimate when the job is genuinely
   different" exception.
   ========================================================================== */
.legal-content { padding: 24px 0 40px; }
.legal-content > p:first-child { color: var(--muted); font-size: 15px; margin-bottom: 32px; }
.legal-content h2 { font-size: 28px; margin-top: 40px; margin-bottom: 16px; letter-spacing: -0.02em; }
.legal-content h2:first-of-type { margin-top: 0; }
.legal-content p { margin-bottom: 16px; }
.legal-content ul { margin: 0 0 16px 24px; }
.legal-content li { margin-bottom: 8px; }
.legal-content a { color: var(--primary); text-decoration: underline; }

/* ==========================================================================
   Cookie Consent Banner — Day-One shared component, every page. Sits above
   the mobile CTA bar (bottom:64px) on mobile so the two never overlap;
   bottom:0 on desktop since the CTA bar is hidden there. Dismissal is
   remembered via localStorage so it never reappears once accepted.
   ========================================================================== */
.cookie-banner {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 80px;
  z-index: 900000;
  background: #131313;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg, 0 24px 60px rgba(0, 0, 0, 0.4));
  padding: 18px 20px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 20px;
  max-width: 640px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(12px);
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease), visibility 0.4s;
}
.cookie-banner.is-visible { opacity: 1; visibility: visible; transform: translateY(0); }
.cookie-banner p { margin: 0; color: rgba(255, 255, 255, 0.75); font-size: 16px; line-height: 1.6; flex: 1 1 260px; }
.cookie-banner a { color: var(--primary); text-decoration: underline; }
.cookie-banner-actions { display: flex; gap: 10px; flex-shrink: 0; }
.cookie-banner-actions .btn { padding: 10px 20px; font-size: 14px; }

@media (min-width: 1024px) {
  .cookie-banner { bottom: 24px; left: 24px; right: auto; }
}

/* ==========================================================================
   Scroll reveal (generic, applied across sections 4-15)
   ========================================================================== */

.reveal-up { opacity: 0; }
.no-js .reveal-up { opacity: 1; }

/* ==========================================================================
   Stats Gauge Card — 3 stacked label/value/fill-bar rows in one
   elevated card. Layout pattern: shadcnblocks.com/block/stats7.
   ========================================================================== */

.stats-gauge-card {
  max-width: 760px;
  margin: 0 auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 32px 24px;
  box-shadow: var(--shadow);
}
@media (min-width: 768px) {
  .stats-gauge-card { padding: 48px 56px; }
}

.stats-gauge-rows {
  display: flex;
  flex-direction: column;
  gap: 28px;
}
@media (min-width: 768px) {
  .stats-gauge-rows { gap: 34px; }
}

.stats-gauge-row-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 12px;
}

.stats-gauge-label {
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 500;
  color: var(--muted);
}
@media (min-width: 768px) {
  .stats-gauge-label { font-size: 16px; }
}

/* Bar thickness: deliberately chunkier than a typical thin progress
   bar (client reference called for thicker), radius pinned to
   var(--radius) so it matches the button system, not a full pill. */
.stats-gauge-track {
  position: relative;
  width: 100%;
  height: 20px;
  border-radius: var(--radius);
  background: var(--border);
  overflow: hidden;
}
@media (min-width: 768px) {
  .stats-gauge-track { height: 24px; }
}

.stats-gauge-fill {
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
  background: linear-gradient(90deg, rgba(239, 206, 17, 0.55), var(--primary));
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 1.4s cubic-bezier(0.16, 1, 0.3, 1);
}

/* ==========================================================================
   Our Story — narrative rewrite, gold-frame accent, pull-quote
   ========================================================================== */

.about-photo-frame { position: relative; }
.about-photo-frame::before {
  content: '';
  position: absolute;
  inset: 16px -16px -16px 16px;
  background: var(--primary);
  border-radius: var(--radius);
  z-index: -1;
}
@media (max-width: 767px) {
  .about-photo-frame::before { inset: 12px -12px -12px 12px; }
}

.about-quote {
  font-family: var(--font-body);
  font-style: italic;
  font-size: clamp(18px, 2.4vw, 21px);
  line-height: 1.6;
  color: var(--text);
  border-left: 3px solid var(--primary);
  padding-left: 20px;
  margin: 4px 0;
}

/* ==========================================================================
   Services Explorer — "What We Do". A synced list/tile + carousel
   pattern (adapted from a shadcnblocks.com "Feature 57" reference,
   rebuilt in vanilla CSS/JS to match this site's own components).
   Desktop/tablet: narrow clickable list beside a wide image carousel.
   Mobile: a 2x2 tile grid above a full-width swipeable carousel,
   so nothing requires scrolling to become interactive — the mobile
   layout is a deliberate departure from the reference (which just
   stacks every row below the carousel) per client direction.
   Capped at AZ's 4 priority services (Kitchen/Bath/Basement/Additions)
   on every breakpoint so the list, tiles, and carousel slides always
   stay in sync 1:1 — full service depth still lives in the footer
   and /services/ hub, this is a curated homepage moment, not a directory.
   ========================================================================== */

.svc-explorer {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "tiles" "carousel" "dots" "more";
  row-gap: 20px;
}

/* Mobile: 2x2 tile grid (shown), desktop list (hidden) */
.svc-tile-grid {
  grid-area: tiles;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}
/* Exactly 3 tiles (Kitchen/Bathroom/Basement) as of 2026-09-02 — 2 even
   tiles in row 1, 3rd tile spans full width in row 2, instead of an
   uneven 2-then-1 row or cramming 3 cramped columns into one row. */
.svc-tile-grid .svc-tile:last-child { grid-column: span 2; }

.svc-tile {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 64px;
  padding: 12px 14px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--border);
  text-align: left;
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease), color 0.3s var(--ease);
}
.svc-tile .material-symbols-outlined { font-size: 22px; flex-shrink: 0; color: var(--primary); transition: color 0.3s var(--ease); }
/* Full service names ("Kitchen Remodeling") don't fit narrow mobile
   tiles on one line at a readable size — sized to comfortably wrap
   to 2 lines instead of shrinking past a legible caption size */
.svc-tile span.svc-tile-label { font-size: 12.5px; font-weight: 600; line-height: 1.25; }
.svc-tile.is-active { background: var(--primary); border-color: var(--primary); color: #131313; }
.svc-tile.is-active .material-symbols-outlined { color: #131313; }
.svc-tile:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.svc-list { grid-area: list; display: none; }

.svc-list-row {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 16px;
  border-radius: var(--radius);
  border: 1px solid transparent;
  background: transparent;
  text-align: left;
  cursor: pointer;
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
}
.svc-list-row:hover:not(.is-active) { background: var(--surface); }
.svc-list-row.is-active { background: var(--surface); border-color: var(--border); box-shadow: var(--shadow); }
.svc-list-row .svc-list-icon {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  background: var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
}
.svc-list-row.is-active .svc-list-icon { background: var(--primary); }
.svc-list-row .svc-list-icon .material-symbols-outlined { font-size: 22px; color: var(--primary); }
.svc-list-row.is-active .svc-list-icon .material-symbols-outlined { color: #131313; }
.svc-list-row h3 { font-family: var(--font-body); font-weight: 600; font-size: 17px; margin-bottom: 4px; }
.svc-list-row p { color: var(--muted); font-size: 16px; line-height: 1.5; }
.svc-list-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* Nested real <a> inside each row — separate click target from the
   row's own "preview this slide" behavior, so tapping the row selects
   it, tapping this specific link navigates to that service's page.
   Lives inline inside the description <p> (not a block below it) so
   it naturally trails the sentence — same line if it fits, wraps to
   its own line if it doesn't, rather than always forcing a 3rd line. */
.svc-list-link {
  /* flex (block-level) instead of inline-flex — always starts its own
     line below the description, on every row, instead of only
     wrapping down when a given row's sentence happened to run out
     of horizontal room first */
  display: flex;
  width: fit-content;
  align-items: center;
  gap: 4px;
  margin-top: 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.svc-list-link:hover { color: var(--primary); }
/* Underline only the text, not the icon glyph — underlining the whole
   link made the icon look like a separate, disconnected fragment */
.svc-list-link-text { text-decoration: underline; text-underline-offset: 3px; }
.svc-list-link .material-symbols-outlined { font-size: 15px; transition: transform 0.3s var(--ease); }
.svc-list-link:hover .material-symbols-outlined { transform: translateX(3px); }
.svc-list-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 2px; }

.svc-more { grid-area: more; }

/* Shared CTA style for "View All Services" / "See More Services" —
   dark by default, gold on hover, so it reads as a bold call to
   action rather than a quiet outlined button once real photos
   replace the dark placeholder panels around it. Spans the full
   width of whichever column it sits in (list column on desktop,
   carousel width on mobile) instead of shrinking to its own text. */
.svc-cta-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 48px;
  padding: 14px 20px;
  margin-top: 4px;
  border-radius: var(--radius);
  background: #131313;
  color: #fff;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 15px;
  border: none;
  cursor: pointer;
  transition: background 0.3s var(--ease), color 0.3s var(--ease), transform 0.2s var(--ease), box-shadow 0.3s var(--ease);
}
.svc-cta-btn:hover { background: var(--primary); color: #131313; transform: translateY(-2px); box-shadow: 0 8px 25px rgba(239, 206, 17, 0.3); }
.svc-cta-btn:active { transform: scale(0.97); }
.svc-cta-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.svc-cta-btn .material-symbols-outlined { font-size: 18px; transition: transform 0.3s var(--ease); }
.svc-cta-btn:hover .material-symbols-outlined { transform: translateX(3px); }

/* Auto-advancing progress dots — sits directly under the carousel
   on every breakpoint. Active dot expands into a pill and its inner
   bar fills over the auto-advance duration; resets to a plain dot
   when another slide becomes active. Also acts as a 3rd clickable
   "remote control" alongside the tiles/list. */
.svc-progress-dots {
  grid-area: dots;
  display: flex;
  justify-content: center;
  gap: 8px;
}

.svc-progress-dot {
  position: relative;
  width: 10px;
  height: 10px;
  padding: 0;
  border: none;
  border-radius: 100px;
  background: var(--border);
  overflow: hidden;
  cursor: pointer;
  transition: width 0.3s var(--ease);
}
.svc-progress-dot.is-active { width: 36px; }
.svc-progress-dot:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.svc-progress-fill {
  position: absolute;
  inset: 0;
  width: 0%;
  background: var(--primary);
  border-radius: 100px;
}

/* Carousel — shared across breakpoints, native scroll-snap so a
   real finger swipe on mobile feels native (not JS drag-tracked) */
.svc-carousel {
  grid-area: carousel;
  border-radius: 20px;
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}

.svc-carousel-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.svc-carousel-track::-webkit-scrollbar { display: none; }

.svc-slide {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: center;
  aspect-ratio: 4 / 3;
}
@media (min-width: 768px) {
  /* 3/2 (not 4/3) so the carousel column's height lands flush with the
     list column's bottom edge (4 rows + the "View All Services" row) at
     the 1360px container width — 4/3 overshot list height by ~66px once
     the container widened from 1200px, since a fixed aspect-ratio scales
     with the column's own width while the list's height is text-driven
     and stays roughly constant. Re-check this pairing if the container
     max-width or the list copy length changes again. */
  .svc-slide { aspect-ratio: 3 / 2; }
}

.svc-slide-photo {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(135deg, rgba(255,255,255,0.03) 0 2px, transparent 2px 24px),
    linear-gradient(160deg, #2a2a2a 0%, #1a1a1a 60%, #131313 100%);
  display: flex;
  align-items: center;
  justify-content: center;
}
.svc-slide-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.svc-slide-photo span {
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.4);
  border: 1px dashed rgba(255,255,255,0.2);
  padding: 10px 20px;
  border-radius: 100px;
  text-align: center;
}

.svc-slide-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: flex-end;
  gap: 14px;
  padding: 24px;
  /* Tightened to a bottom-only fade (client direction 2026-09-02) — the
     old gradient stayed at 50%+ opacity halfway up the photo and muted
     the real project photography. This keeps full readable contrast
     right behind the copy but clears to the untouched photo by ~55%
     up, so the kitchen/bath work itself reads at full color. */
  background: linear-gradient(to top, rgba(19,19,19,0.92) 0%, rgba(19,19,19,0.6) 22%, rgba(19,19,19,0.15) 45%, rgba(19,19,19,0) 60%);
}
.svc-slide-icon {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  background: var(--primary);
  display: flex;
  align-items: center;
  justify-content: center;
}
.svc-slide-icon .material-symbols-outlined { font-size: 22px; color: #131313; }
.svc-slide-copy { flex: 1; min-width: 0; }
.svc-slide-copy strong { display: block; color: #fff; font-size: 17px; margin-bottom: 4px; }
.svc-slide-copy p { color: rgba(255,255,255,0.75); font-size: 16px; line-height: 1.5; }
.svc-slide-link {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(255,255,255,0.12);
  border: 1px solid rgba(255,255,255,0.25);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.3s var(--ease), transform 0.2s var(--ease);
}
.svc-slide-link:hover { background: var(--primary); transform: translateX(2px); }
.svc-slide-link:hover .material-symbols-outlined { color: #131313; }
.svc-slide-link .material-symbols-outlined { font-size: 18px; color: #fff; }
.svc-slide-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

@media (min-width: 768px) {
  .svc-explorer {
    grid-template-columns: 5fr 7fr;
    /* "." = empty cell — the desktop CTA now lives inside .svc-list
       itself (its last row) instead of a separate grid area */
    grid-template-areas: "list carousel" ".    dots";
    column-gap: 3rem;
    row-gap: 16px;
    align-items: start;
  }
  .svc-tile-grid { display: none; }
  .svc-list { display: flex; flex-direction: column; gap: 6px; }
  .svc-more.svc-more-mobile { display: none; }
  /* Only 3 rows now (down from 4, see .svc-explorer comment in
     index.html) — taller vertical padding per row makes the list +
     "View All Services" button fill the same height as the carousel
     image beside it, so the top and bottom stay flush like they did
     with 4 rows. Re-measure via getBoundingClientRect if the row
     count or copy length changes again. */
  .svc-list-row { padding-top: 33px; padding-bottom: 33px; }
}

/* ==========================================================================
   Shared media-placeholder box shape (4:3, 20px radius, dashed border
   until filled with a real photo/video). Originally built for the "Why
   Choose Us" story timeline (removed 2026-08-30); still used by the
   homepage "See The Transformation" video section and reused wherever
   a real photo/video is pending.
   ========================================================================== */

.story-media-placeholder {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: 20px;
  overflow: hidden;
  background:
    repeating-linear-gradient(135deg, rgba(19,19,19,0.04) 0 2px, transparent 2px 24px),
    linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 60%, var(--border) 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--border);
}
.story-media-placeholder span {
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: center;
  padding: 0 32px;
}

/* Real video in place — drop the dashed placeholder border/gradient,
   same pattern as .about-photo--filled */
.story-media-placeholder--filled {
  border: none;
  background: none;
}
.story-media-placeholder--filled video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Circular prev/next control, shared by carousel-style nav (currently
   the portfolio lightbox). Rebuilt 2026-08-20: this rule went missing
   when the reviews section's older single-slide carousel (its original
   owner, per task-log.md) was replaced by the uniform-tile marquee —
   there was never a breakpoint-specific cause. */
.carousel-arrow {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 20px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.3s var(--ease), color 0.3s var(--ease), border-color 0.3s var(--ease), transform 0.2s var(--ease);
}
.carousel-arrow:hover {
  background: var(--primary);
  border-color: var(--primary);
  color: #131313;
  transform: translateY(-2px);
}
.carousel-arrow:active { transform: scale(0.94); }
.carousel-arrow:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ==========================================================================
   Reviews — uniform-tile infinite marquee
   Same measured-offset loop technique as .hero-marquee (initReviewsMarquee
   in main.js): two identical groups sit side by side, JS measures the exact
   pixel width of one group (tile width is fixed, so this is exact, not
   estimated) and drives the loop by that offset via --marquee-shift, so the
   loop is always seamless regardless of how many reviews exist. Second group
   is aria-hidden — real content lives once in the DOM.
   Cataloged as a repeatable section: _agency-ops/section-library/testimonials/
   ========================================================================== */

/* Fixed-px fade (not a %), so the fade distance stays short and consistent
   at any container width instead of growing on wide desktop screens.
   Desktop only — mobile's narrower viewport removes the mask entirely
   (below) and reinstates a fixed 24px inset instead, so tiles cut off
   cleanly at that inset edge rather than fading, which reads better in
   a smaller window. */
.reviews-marquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent);
}
@media (max-width: 640px) {
  .reviews-marquee {
    -webkit-mask-image: none;
    mask-image: none;
    /* Client direction 2026-09-07: full-bleed (edge-to-edge, no fade) was
       only ever meant for desktop — mobile should keep the same 24px
       inset it always had from living inside .container before the
       full-bleed change moved this markup outside of it. Restored here
       directly since the marquee is no longer a padded container's
       child at any width. */
    padding: 0 24px;
  }
}

.reviews-marquee-track {
  display: flex;
  width: max-content;
  animation: reviews-marquee-scroll 85s linear infinite;
  transform: translateX(0);
}
/* Hover-pause is driven by JS (mouseenter/mouseleave per tile in
   initReviewsMarquee), not a CSS :hover rule here — see the comment in
   main.js for why: CSS :hover state can go stale when the hovered element
   shrinks out from under a stationary cursor (exactly what happens when
   "View less" collapses a tile), and only a real pointer-move event
   reliably clears it. focus-within is still fine here since keyboard
   focus doesn't have that staleness risk. */
.reviews-marquee:focus-within .reviews-marquee-track,
.reviews-marquee-track.is-paused {
  animation-play-state: paused;
}

.reviews-marquee-group {
  display: flex;
  gap: 20px;
  flex-shrink: 0;
  padding-right: 20px;
}

@keyframes reviews-marquee-scroll {
  to { transform: translateX(var(--marquee-shift, -50%)); }
}
@media (prefers-reduced-motion: reduce) {
  .reviews-marquee-track { animation: none; }
}

/* Resting height is a max-height (not a fixed height) so "View more" can
   expand the tile by transitioning to a taller max-height instead of
   snapping — the surrounding flex row grows/shrinks smoothly with it
   (hovering/expanding already pauses the whole marquee, so nothing is
   sliding underneath while this happens). See initReviewsMarquee's
   review-tile-expand logic in main.js.
   Expanded/resting max-height values are NOT set here — they're driven
   by inline style from JS (exact scrollHeight on expand, the real
   breakpoint value on collapse), not a generic placeholder like 700px.
   Transitioning between two values far from the real content height
   makes the box sit capped at content height (invisible, no motion) for
   most of the transition and only actually move in whatever's left —
   confirmed by frame-sampling the rendered height. See main.js. */
.review-tile {
  width: 300px;
  max-height: 260px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 22px;
  overflow: hidden;
  position: relative;
  z-index: 1;
  transition: max-height 0.4s var(--ease), border-color 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
.review-tile.is-expanded {
  border-color: var(--primary);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
  z-index: 10;
}
.review-tile-stars { color: var(--primary); font-size: 14px; letter-spacing: 2px; margin-bottom: 10px; flex-shrink: 0; }
/* opacity transition: text must stay unclamped (full length) for the
   duration of a collapse so the tile's own max-height shrink has real
   content to clip against (see max-height-transition-generic-placeholder
   known-bug) — but that means what's on screen mid-shrink is a full
   paragraph getting cut off mid-sentence. Fading it out in sync with the
   shrink (see collapse() in main.js) masks that cutoff as a crossfade
   instead of a visible hard clip. */
.review-tile-text {
  font-size: 14px;
  line-height: 1.6;
  color: var(--text);
  margin: 0 0 8px;
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
  opacity: 1;
  transition: opacity 0.32s var(--ease);
}
.review-tile.is-expanded .review-tile-text {
  display: block;
  overflow: visible;
}
/* Hidden by default; JS shows it only for tiles whose text is actually
   truncated by the line-clamp above (never show a "View more" that reveals
   nothing new). */
.review-tile-toggle {
  display: none;
  align-self: flex-start;
  background: none;
  border: none;
  padding: 0;
  margin: 0 0 12px;
  font-size: 12px;
  font-weight: 600;
  color: var(--primary);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
  flex-shrink: 0;
}
.review-tile-toggle:hover { opacity: 0.75; }
.review-tile-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.review-tile-meta { display: flex; align-items: center; gap: 10px; margin-top: auto; padding-top: 12px; border-top: 1px solid var(--border); flex-shrink: 0; }
.review-tile-avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: var(--surface-2); }
.review-tile-avatar-fallback {
  display: none;
  width: 32px; height: 32px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--primary);
  color: #131313;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 12px;
}
.review-tile-meta-text { display: flex; flex-direction: column; min-width: 0; }
.review-tile-name { font-weight: 600; font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.review-tile-time { font-size: 11px; color: var(--muted); }

@media (max-width: 640px) {
  .review-tile { width: 260px; max-height: 244px; padding: 18px; }
}

/* ==========================================================================
   Process — asymmetric emphasis grid. Steps 1-3 plain/equal weight,
   step 4 gets a bold full-width gold-filled card instead of a
   connecting line — visual weight follows which step actually sells
   the feeling (service-copy.md "step 4" rule), not just position.
   Number badge overlaps the card's top-left corner (borrowed detail
   from a real reference: bisque-condor-171999.framer.app/how_we-_work,
   a construction-trade site using the same "only one step gets color
   fill" mechanic, adapted here to the step that's actually the payoff).
   ========================================================================== */

/* Horizontal phase timeline — pattern: shadcnblocks.com/block/timeline11.
   Mobile (default): stacked column, badge+connector on the left, content
   indented to the right — every phase (including the last) carries a
   connector that continues past its own icon into the trailing gap, so
   the spine runs unbroken start to finish. Desktop/tablet (768px+):
   4-column grid; each phase gets its own left-justified line sitting
   above its own icon/content, sized to that column only (not bridging
   into the next), so there are 4 distinct lines, not 3 connectors
   spanning gaps. See initPhaseTimeline() in main.js for the base-line-
   to-solid-line reveal, which now plays as one sequential chronological
   pass (Phase 1 finishes, then Phase 2, then 3, then 4) rather than all
   4 fading in together. */
.phase-timeline { display: flex; flex-direction: column; }

/* padding-bottom is the mobile gap between one phase's text and the
   next phase's icon — 24px = 2/3 of the original 36px per client
   feedback (remove a third). Desktop overrides this to 0 below. */
.phase-item { position: relative; display: flex; gap: 16px; padding-bottom: 24px; }

.phase-badge {
  position: relative;
  z-index: 2;
  flex-shrink: 0;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--bg);
  padding: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.phase-badge-mid {
  width: 100%;
  height: 100%;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.phase-badge-inner {
  width: 100%;
  height: 100%;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
}
.phase-badge-inner .material-symbols-outlined { font-size: 20px; color: var(--text); }

/* Vertical connector (mobile) — sits behind the badge, centered on it,
   spanning down into the gap toward the next item. Fixed height (not
   top+bottom stretched to the item's own dynamic bottom edge) so every
   phase gets the exact same line length regardless of how much its own
   text wraps — Phase 4's longer paragraph was previously stretching its
   connector taller than Phases 1-3. 86px = the shared length Phases 1-3
   already rendered at (114px), shortened 25% per client feedback.
   base = permanent faded line, fill = solid overlay animated via GSAP.
   58px = a further 1/3 cut from the previous 86px, matching the same
   1/3 reduction applied to the gap above, per client feedback that both
   the line and the gap between phases were still too long/large. */
.phase-connector {
  position: absolute;
  z-index: 1;
  top: 56px;
  height: 58px;
  left: 27px;
  width: 2px;
}
.phase-connector-base { position: absolute; inset: 0; background: var(--border); }
.phase-connector-fill {
  position: absolute;
  inset: 0;
  background: var(--text);
  width: 100%;
  height: 0%;
}

.phase-content { padding-top: 4px; }
.phase-eyebrow {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--primary);
  margin-bottom: 4px;
}
.phase-content h3 { font-size: 22px; margin-bottom: 6px; }
.phase-content p { color: var(--muted); font-size: 16px; line-height: 1.6; }

@media (min-width: 768px) {
  .phase-timeline { display: grid; grid-template-columns: repeat(4, 1fr); column-gap: 32px; }

  .phase-item { flex-direction: column; align-items: flex-start; padding-bottom: 0; padding-top: 88px; gap: 0; }

  .phase-badge { position: absolute; top: 0; left: 0; width: 64px; height: 64px; }

  /* Self-contained to this item's own column — left-justified with the
     badge/icon and text below it, not reaching into the next column. */
  .phase-connector {
    top: 32px;
    bottom: auto;
    left: 0;
    width: 100%;
    height: 2px;
  }
  .phase-connector-fill { width: 0%; height: 100%; }

  .phase-content { padding-top: 0; }
}

/* ==========================================================================
   Short Hero — inner/service pages (blueprint calls for ~50vh, not full
   viewport). Reuses every homepage hero class (.hero-mat gold frame,
   .hero-frame, .hero-media, .hero-content, .hero-marquee) unchanged —
   only the frame height is overridden, so initHeroParallax/initHeroMarquee
   in main.js work identically with no JS changes.
   ========================================================================== */
/* Frame height and content padding brought down to match the /services/
   .page-header's compact scale (client request 2026-08-19) — was 62vh/
   58vh, which read as a second full hero rather than an inner-page
   banner. min-height is a floor, not a cap, so the frame still grows if
   subhead/CTA content needs more room; it no longer reserves empty vh. */
.hero--short .hero-frame { align-items: center; padding-top: 0; min-height: 180px; }
@media (min-width: 768px) {
  .hero--short .hero-frame { min-height: 30vh; }
}

/* Centered headline/subhead/CTAs, per client request — flex column +
   align-items:center centers each child's own box regardless of its
   individual max-width, rather than relying on text-align alone (which
   only centers wrapped text lines within a box that's still pinned
   left). Scoped to .hero--short so the homepage's full hero (left-
   aligned, per mobile-hero.md's image-hero default) is unaffected. */
.hero--short .hero-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 28px 24px;
}
@media (min-width: 768px) {
  .hero--short .hero-content { padding: 40px 64px 48px; }
}
/* Headline size reverted 2026-08-20 to match the homepage hero exactly
   — was clamp(40px,6vw,64px), a 2026-08-19 decision to match the old
   /page-header/ h1 scale that no longer applies now that every
   inner-page hero (service pages, About, Contact, Services Hub,
   Service Areas) dropped its subhead/CTA row and reads as
   headline-only. Client direction: every hero's text should be one
   uniform size site-wide. Floor lowered again 2026-09-05 alongside the
   base .hero-headline (see its own comment) — mirrored here so this
   stays in sync with that value rather than hardcoding it twice. */
.hero--short .hero-headline {
  /* Floor matches the base .hero-headline's 40px (see its comment) so
     every page's hero text stays the same size, per the rule above. */
  font-size: clamp(40px, 9vw, 108px);
  letter-spacing: -0.03em;
  margin-bottom: 0;
}
@media (min-width: 768px) {
  /* Mobile zeroes this — every .hero--short page is headline-only (no
     subhead/CTA below it), so a trailing margin-bottom just biases the
     vertical centering upward with nothing below to balance it against.
     Kept on desktop unchanged, where the frame's own larger min-height
     makes this a non-issue either way. */
  .hero--short .hero-headline { margin-bottom: 12px; }
}

/* No-media variant — same gold-mat frame and compact hero--short scale,
   but a solid onyx panel instead of a photo. Unified inner-page header
   for hub/one-off pages (About, Contact, Services Hub, Service Areas)
   per client direction 2026-08-20: every page header should read as
   either "photo hero" (service pages, kept as-is) or "solid onyx + gold
   outline" (everything else) — never the un-bordered flat panel this
   replaces. Title only, no subhead/CTAs, to match the simple existing
   look on Services Hub/Service Areas that prompted this unification. */
.hero--no-media .hero-frame {
  background: var(--bg);
}

/* ==========================================================================
   Trust Strip — service-page section (service-page-blueprint.md #2).
   New shared component, first used here; copy verbatim to every future
   service page. Slim badge row: stars/review count, licensed/insured,
   years in business, family-owned — deliberately not a second copy of
   the homepage hero's scrolling marquee (that's a homepage-hero-specific
   device), just a plain static row so it reads as its own trust beat.
   ========================================================================== */
.trust-strip-section {
  padding: 28px 0;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.trust-strip-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 16px 40px;
}
.trust-strip-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  white-space: nowrap;
}
.trust-strip-item .material-symbols-outlined,
.trust-strip-item svg {
  color: var(--primary);
  font-size: 20px;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}
.trust-strip-stars { color: var(--primary); letter-spacing: 1px; font-size: 15px; }

@media (max-width: 767px) {
  .trust-strip-row { gap: 12px 24px; }
  .trust-strip-item { font-size: 13px; }
}

/* ==========================================================================
   Service Gallery Grid — service-page-blueprint.md #9. Reusable across
   every service page, not just Kitchen. Real photos only, same house
   radius as every other image frame on the site.
   ========================================================================== */
.service-gallery-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}
.service-gallery-item {
  border-radius: var(--radius);
  overflow: hidden;
  aspect-ratio: 4 / 3;
  position: relative;
}
.service-gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.5s var(--ease);
}
.service-gallery-item:hover img { transform: scale(1.05); }

@media (max-width: 767px) {
  .service-gallery-grid { grid-template-columns: 1fr; gap: 16px; }
}

/* ==========================================================================
   Trust Panel — new shared component (service-page-blueprint.md). Merges
   Why Choose Us + Service Area into one section: a dark card (heading,
   "two components" checklist, trust paragraph, CTA) beside two stacked
   real photos. Reference: nextlevelremodeling.com/kitchen-remodeling —
   layout borrowed, copy and photos entirely original.
   ========================================================================== */
.trust-panel-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
}
@media (min-width: 768px) {
  /* Text panel is the main focus, per the reference (a wider text card,
     smaller stacked photos) — not an even split. */
  .trust-panel-grid { grid-template-columns: 3fr 2fr; gap: 32px; align-items: stretch; }
}

.trust-panel-card {
  background: #131313;
  color: #fff;
  padding: 40px 32px;
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Matches .split-media-heading exactly, so this heading reads at the
   same size as the Service Intro heading right above it. */
.trust-panel-card h2 { color: #fff; margin-bottom: 0; font-size: clamp(36px, 5vw, 56px); }
.trust-panel-eyebrow { margin-bottom: 14px; }
.trust-panel-subhead {
  color: var(--primary);
  font-weight: 600;
  font-size: 15px;
  margin: 18px 0 12px;
}
.trust-panel-checklist {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 22px;
}
.trust-panel-checklist li {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-weight: 600;
  font-size: 15px;
}
.trust-panel-checklist .material-symbols-outlined {
  color: var(--primary);
  font-size: 20px;
  flex-shrink: 0;
  margin-top: 1px;
}
.trust-panel-body {
  color: rgba(255, 255, 255, 0.75);
  line-height: 1.7;
  font-size: 16px;
  margin-bottom: 26px;
}
.trust-panel-card .btn { align-self: flex-start; }

/* Flips which side the photos/card sit on via `order`, not DOM order,
   so the HTML stays in natural reading order (card markup, then
   photos markup) regardless of which modifier is applied. Scoped to
   this modifier so every other trust-panel-grid instance keeps the
   default text-left/photos-right arrangement. */
@media (min-width: 768px) {
  .trust-panel-grid--image-left { grid-template-columns: 2fr 3fr; }
  .trust-panel-grid--image-left .trust-panel-card,
  .trust-panel-grid--image-left .trust-panel-copy { order: 2; }
  .trust-panel-grid--image-left .trust-panel-photos { order: 1; }
}

/* Widens the photo column slightly against the base 3fr/2fr split —
   used where the text column carries less content (e.g. no subhead/
   checklist) and needs the photos sized up to keep the section
   balanced instead of leaving the text column looking sparse. */
@media (min-width: 768px) {
  .trust-panel-grid--photos-lg { grid-template-columns: 5fr 4fr; }
}

/* No-photos variant: card only, no stacked photo column. Card fills
   the full container width (matches the sections above/below) with
   all content centered instead of the default left-aligned layout. */
.trust-panel-grid--no-photos {
  grid-template-columns: 1fr;
}
.trust-panel-grid--no-photos .trust-panel-card {
  align-items: center;
  text-align: center;
}
.trust-panel-grid--no-photos .trust-panel-checklist,
.trust-panel-grid--no-photos .trust-panel-checklist li {
  align-items: center;
  justify-content: center;
}
.trust-panel-grid--no-photos .trust-panel-card .btn {
  align-self: center;
}
@media (min-width: 768px) {
  .trust-panel-grid--no-photos { grid-template-columns: 1fr; }
}

/* Light variant of the trust-panel text column — same grid, same
   eyebrow/heading/subhead/checklist/body pieces, no dark card
   background. Used where the trust-panel LAYOUT (text column beside
   two stacked real photos) is the right fit but a dark card would be
   too heavy for the section's job (e.g. a photo-led service intro
   instead of a reinforcing trust block further down the page). */
.trust-panel-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.trust-panel-copy h2 {
  margin-bottom: 0;
  font-size: clamp(36px, 5vw, 56px);
}
.trust-panel-copy p {
  color: var(--muted);
  font-size: 16px;
  line-height: 1.7;
  margin-bottom: 26px;
}

.trust-panel-photos {
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.trust-panel-photo {
  border-radius: var(--radius);
  overflow: hidden;
  flex: 1;
}
.trust-panel-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
@media (min-width: 768px) {
  .trust-panel-photo { aspect-ratio: 3 / 2; }
}
@media (max-width: 767px) {
  .trust-panel-photo { aspect-ratio: 4 / 3; }
}

/* ==========================================================================
   Consult + Gallery Teaser — new shared component. Closing section before
   the real lead-capture form: a real-photo panel (recent work + inline
   links to service-area pages, preserving the internal linking the
   removed Service Area section used to carry) beside a dark consultation
   CTA panel. Both panels share one rounded/clipped outer shape.
   Reference: nextlevelremodeling.com/kitchen-remodeling — layout
   borrowed, copy and photos entirely original.
   ========================================================================== */
.consult-gallery-grid {
  display: grid;
  grid-template-columns: 1fr;
  border-radius: var(--radius);
  overflow: hidden;
}
@media (min-width: 768px) {
  .consult-gallery-grid { grid-template-columns: 1fr 1fr; }
}

.consult-gallery-photo-panel {
  position: relative;
  min-height: 320px;
  display: flex;
  align-items: flex-end;
}
.consult-gallery-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.consult-gallery-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(19, 19, 19, 0.92) 0%, rgba(19, 19, 19, 0.6) 55%, rgba(19, 19, 19, 0.2) 100%);
}
.consult-gallery-content {
  position: relative;
  z-index: 2;
  padding: 40px 32px;
  color: #fff;
}
.consult-gallery-content p { margin-bottom: 20px; line-height: 1.7; color: rgba(255, 255, 255, 0.85); }
.consult-gallery-content p a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
/* Sits on a dark photo overlay, so the site-wide solid-onyx
   .btn-secondary would be nearly invisible here (onyx on near-onyx) —
   solid white instead, same "solid, not outline" treatment, gold on
   hover to match every other button site-wide. */
.consult-gallery-content .btn-secondary { background: #fff; color: #131313; }
.consult-gallery-content .btn-secondary:hover { background: var(--primary); color: #131313; }

.consult-panel {
  background: #131313;
  color: #fff;
  padding: 48px 32px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.consult-panel h2 { color: #fff; font-size: clamp(36px, 5vw, 56px); margin-bottom: 14px; }
.consult-panel p { color: rgba(255, 255, 255, 0.75); margin-bottom: 24px; line-height: 1.7; }
.consult-panel .btn { align-self: flex-start; }

/* ==========================================================================
   Portfolio Hub — category grids + lightbox
   Hub-only MVP (portfolio-gallery-blueprint.md, adapted per client direction
   2026-08-21 after studying nextlevelremodeling.com/portfolio): each card is
   one real completed project, click opens a lightbox with that project's
   full photo set. No category archive pages or individual project URLs yet
   — those are Phase 2. The lightbox reuses .carousel-arrow (main.css nav
   section) for prev/next so no new visual language is introduced.
   ========================================================================== */

.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}
@media (max-width: 900px) {
  .portfolio-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .portfolio-grid { grid-template-columns: 1fr; }
}

.portfolio-card {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  overflow: hidden;
  border: none;
  padding: 0;
  cursor: pointer;
  background: var(--surface-2);
}
.portfolio-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.4s var(--ease);
}
.portfolio-card:hover img { transform: scale(1.05); }

.portfolio-card-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  padding: 20px;
  background: linear-gradient(to top, rgba(19,19,19,0.75) 0%, rgba(19,19,19,0) 55%);
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}
.portfolio-card:hover .portfolio-card-overlay,
.portfolio-card:focus-visible .portfolio-card-overlay { opacity: 1; }

.portfolio-card-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: #fff;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 14px;
  letter-spacing: 0.02em;
}
.portfolio-card-label .material-symbols-outlined { font-size: 18px; color: var(--primary); }

.portfolio-card:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Bathroom grid — 3:4 portrait override (client direction 2026-09-07),
   uniform height across all 8 real photos instead of the site-wide 4:3
   default. All 8 source photos are already portrait and close to 3:4
   natively (0.75-0.81), except two outliers cropped further by
   object-fit:cover: bath-powder-room-brass-bidet (0.558, ~26% height
   trim) gets an inline object-position bias toward the bottom (see its
   <img> tag) so the crop favors the vanity/mirror/toilet and cuts the
   vaulted ceiling above the light fixture instead of the subject;
   bath-subway-tub-pebble-niche (1.0 square, ~25% width trim) needs no
   bias since the excess width is the green door-frame casing on both
   sides — a plain center crop already removes it evenly. Every other
   photo's own crop is negligible (<7%) at default center. */
#bathroom .portfolio-card { aspect-ratio: 3 / 4; }

.portfolio-category-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 28px; flex-wrap: wrap; }
.portfolio-category-more { color: var(--muted); font-size: 14px; font-weight: 500; }

/* Lightbox */
.portfolio-lightbox {
  position: fixed;
  inset: 0;
  z-index: 1200000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 10, 10, 0.92);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s var(--ease), visibility 0.3s;
}
.portfolio-lightbox.is-open { opacity: 1; visibility: visible; }

.portfolio-lightbox-frame {
  position: relative;
  width: min(90vw, 1000px);
  max-height: 86vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
.portfolio-lightbox-image-wrap {
  width: 100%;
  max-height: 74vh;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius);
  overflow: hidden;
  background: #131313;
}
.portfolio-lightbox-image-wrap img {
  width: 100%;
  height: 100%;
  max-height: 74vh;
  object-fit: contain;
  display: block;
}

.portfolio-lightbox-close {
  position: absolute;
  top: -48px;
  right: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(255,255,255,0.1);
  border: 1px solid rgba(255,255,255,0.25);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.3s var(--ease);
}
.portfolio-lightbox-close:hover { background: var(--primary); color: #131313; }
.portfolio-lightbox-close:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.portfolio-lightbox-nav {
  display: flex;
  align-items: center;
  gap: 24px;
}
.portfolio-lightbox-nav .carousel-arrow { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.25); color: #fff; }
.portfolio-lightbox-nav .carousel-arrow:hover { background: var(--primary); border-color: var(--primary); color: #131313; }
.portfolio-lightbox-counter { color: rgba(255,255,255,0.75); font-size: 14px; font-weight: 500; min-width: 44px; text-align: center; }

@media (max-width: 640px) {
  .portfolio-lightbox-close { top: -44px; width: 36px; height: 36px; }
}
