/* Power Line Electrical Contractors — site stylesheet.
   Design language carried over from the approved homepage preview.
   Palette is the real logo's: #ce070c → #750104 gradient on #393939 charcoal.
   The old bright red that was never a PLEC brand colour is banned sitewide and
   the build fails if it appears in this file.
   Fonts are the system stack — no webfont requests, no external assets.

   Structure: design tokens → base → components (each with its own media
   queries, mobile-first) → JS-enhancement hooks → touch targets → motion/print.
   Breakpoint scale (custom properties cannot be used in @media conditions,
   so queries use these literals):
     640px  sm — landscape/large phones
     768px  md — tablets portrait
     1024px lg — laptops (grid columns stop changing here; the 1180px wrap
                 cap makes 1280px equivalent)
     941px  — mobile-nav threshold only: the desktop nav + header CTA need
              ~900px, so 768px would wrap the header into two cramped rows. */

:root {
  --red: #ce070c;
  --red-dark: #750104;
  --charcoal: #393939;
  --ink: #1b1b1d;
  --muted: #5d5d64;
  --line: #e6e6ea;
  --wash: #f5f3f0;
  --maxw: 1180px;

  /* brand red as an RGB triple for rgba() derivations */
  --red-rgb: 206, 7, 12;

  /* fluid type scale (rem-based; 1rem = 16px at default UA setting) */
  --text-xs: clamp(0.75rem, 1.5vw, 0.875rem);   /* 12 -> 14px */
  --text-sm: clamp(0.875rem, 1.75vw, 1rem);     /* 14 -> 16px */
  --text-base: clamp(1rem, 2vw, 1.125rem);      /* 16 -> 18px */
  --text-lg: clamp(1.125rem, 2.25vw, 1.25rem);  /* 18 -> 20px */
  --text-xl: clamp(1.25rem, 2.5vw, 1.5rem);     /* 20 -> 24px */
  --text-2xl: clamp(1.5rem, 3vw, 2rem);         /* 24 -> 32px */
  --text-3xl: clamp(2rem, 4vw, 2.75rem);        /* 32 -> 44px */
  --text-4xl: clamp(2.5rem, 5vw, 3.5rem);       /* 40 -> 56px */

  /* spacing scale — 0.25rem increments */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-7: 1.75rem;
  --space-8: 2rem;
  --space-9: 2.25rem;
  --space-10: 2.5rem;
  --space-11: 2.75rem;
  --space-12: 3rem;
  --space-13: 3.25rem;
  --space-14: 3.5rem;
  --space-15: 3.75rem;
  --space-16: 4rem;

  /* semantic spacing */
  --space-section: clamp(4rem, 8vw, 6rem);      /* 64 -> 96px */
  --space-component: clamp(2rem, 4vw, 3rem);    /* 32 -> 48px */
  --space-element: clamp(1rem, 2vw, 1.5rem);    /* 16 -> 24px */

  /* radii */
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 8px;
  --radius-pill: 999px;

  /* v3 — brand + motion tokens. Every v3 track references these; defined
     exactly once here. The existing blanket prefers-reduced-motion
     kill-switch still neutralises everything. */
  --color-brand-red: var(--red);            /* #ce070c */
  --color-brand-red-dark: var(--red-dark);  /* #750104 */
  --color-charcoal: var(--charcoal);        /* #393939 */
  --color-near-black: #1a1a1a;
  --color-footer-black: #111111;
  --color-warm-white: var(--wash);          /* #f5f3f0 */
  --color-muted-light: rgba(255, 255, 255, 0.72);
  --color-muted-dark: rgba(26, 26, 26, 0.70);

  --motion-fast: 160ms;
  --motion-standard: 320ms;
  --motion-reveal: 520ms;
  --motion-slow: 800ms;

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-emphasis: cubic-bezier(0.16, 1, 0.3, 1);
}

*, *::before, *::after { box-sizing: border-box; }

html { font-size: 100%; -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: #fff;
  color: var(--ink);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif;
  font-size: var(--text-base);
  font-weight: 400;
  line-height: 1.65;
  /* Full-width .reveal--left/--right sections translateX off-screen before
     they settle; without this a narrow viewport can scroll horizontally
     during the pre-visible state. */
  overflow-x: hidden;
}

img, svg { max-width: 100%; height: auto; }
h1, h2, h3, h4 { line-height: 1.15; letter-spacing: -0.02em; margin: 0; }
h1 { font-weight: 800; }
h2 { font-weight: 700; }
h3, h4 { font-weight: 600; }
strong, b { font-weight: 600; }
p { margin: 0; }
a { color: inherit; }

.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 20px; }

/* Visible keyboard focus everywhere — never removed. */
a:focus-visible, button:focus-visible, summary:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 3px solid var(--red);
  outline-offset: 2px;
}

/* On dark or brand-red surfaces the red outline is invisible exactly where
   the CTAs live — recoloured, never suppressed. */
.hero :focus-visible,
.band :focus-visible,
.ft :focus-visible,
.util :focus-visible,
.why :focus-visible {
  outline-color: #fff;
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: #fff; color: var(--red); padding: var(--space-3) var(--space-4);
  font-weight: 600;
}
.skip:focus { left: 8px; top: 8px; }

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

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-2);
  font-weight: 700; letter-spacing: .02em; text-decoration: none;
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm); line-height: 1.3;
  border: 2px solid transparent; cursor: pointer; text-align: center;
  transition: transform .15s ease, filter .15s ease, background-color .15s ease,
              color .15s ease, border-color .15s ease, box-shadow .15s ease;
}
/* The translateY lift lives in the hover-capability guard at the bottom of
   this file so touch devices never get a stuck-lifted button. */
.btn:hover { filter: brightness(1.08); }
.btn--red {
  background: linear-gradient(180deg, var(--red), var(--red-dark));
  color: #fff; box-shadow: 0 3px 10px rgba(var(--red-rgb), .28);
}
.btn--red:hover { box-shadow: 0 6px 18px rgba(var(--red-rgb), .38); }
.btn--ghost { border-color: rgba(255, 255, 255, .55); color: #fff; }
.btn--ghost:hover { background: rgba(255, 255, 255, .1); }
.btn--onred { background: #fff; color: var(--red-dark); }
.btn--onred:hover { background: transparent; border-color: #fff; color: #fff; }
.btn--outline { border-color: var(--red); color: var(--red-dark); background: #fff; }
.btn--outline:hover { background: rgba(var(--red-rgb), .06); border-color: var(--red-dark); }
.btn[aria-disabled="true"] {
  opacity: .55; cursor: not-allowed; transform: none; filter: none;
}

/* ---------- utility bar ---------- */
.util { background: var(--charcoal); color: #e8e8ea; font-size: var(--text-sm); line-height: 1.3; }
.util .wrap {
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5);
  align-items: center; justify-content: space-between;
  padding-top: var(--space-2); padding-bottom: var(--space-2);
}
.util b { color: #fff; }
.util a { color: #fff; text-decoration: none; font-weight: 700; }
.util .dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--red); margin-right: 8px;
  box-shadow: 0 0 0 0 rgba(var(--red-rgb), .7); animation: pulse 2.4s infinite;
}
@keyframes pulse {
  70% { box-shadow: 0 0 0 9px rgba(var(--red-rgb), 0); }
  100% { box-shadow: 0 0 0 0 rgba(var(--red-rgb), 0); }
}

/* ---------- header ---------- */
.hdr {
  position: sticky; top: 0; z-index: 50; background: #fff;
  border-bottom: 1px solid #e9e9ec;
  box-shadow: none; /* transition lives in the v3 Track C block below — it
     must cover background, border and shadow together or the surface flip
     looks torn. */
}
/* enhancements.js adds this once the page scrolls past the top. */
.hdr--scrolled { box-shadow: 0 2px 14px rgba(0, 0, 0, .09); }
.hdr .wrap {
  position: relative; display: flex; align-items: center;
  gap: var(--space-4); padding: var(--space-3) 20px;
}
.brand {
  flex: 0 0 auto; display: flex; flex-direction: column; align-items: center;
  gap: 0.2em; line-height: 0; text-decoration: none;
}
.brand svg { width: 212px; max-width: 46vw; }
/* Desktop-only bump (~20%): mobile keeps the 212px/46vw sizing above —
   the header is already tight below the 941px nav breakpoint. */
@media (min-width: 941px) {
  .brand svg { width: 255px; }
}
.hdr__tagline {
  display: block; margin: 0; line-height: 1.2; max-width: 255px; width: 100%;
  font-size: calc(var(--text-xs) * 0.85); font-weight: 500; letter-spacing: 0.06em;
  text-align: center; white-space: normal;
  /* Solid/interior header: charcoal, not white — white-on-white would be a
     1:1 contrast ratio here. See the overlay override below for the dark
     surface. */
  color: var(--color-charcoal); opacity: 0.7;
}
.nav { margin-left: auto; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.nav a {
  text-decoration: none; font-size: var(--text-sm); font-weight: 500;
  line-height: 1.3; color: #4a4a4f;
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
  transition: color .15s ease, background-color .15s ease, box-shadow .15s ease;
}
.nav a:hover { color: var(--red); background: #faf1f1; }
.nav a[aria-current="page"] {
  color: var(--red); background: #faf1f1;
  box-shadow: inset 0 -2px 0 var(--red);
}
.hdr .btn { flex-shrink: 0; padding: var(--space-3) var(--space-5); font-size: var(--text-sm); }

/* Mobile nav is a <details> disclosure: it works with JavaScript disabled.
   enhancements.js adds outside-click close, Escape, focus trap, aria-expanded.
   Mobile-first: the toggle is the default state; ≥941px shows the full nav
   (below that the nav + header CTA would wrap into a cramped two-row header). */
.hdr > .wrap > .nav,
.hdr > .wrap > .btn { display: none; }
.navtoggle { display: block; margin-left: auto; }
.navtoggle > summary {
  list-style: none; cursor: pointer; border: 1px solid #ddd;
  border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; min-height: 44px; min-width: 44px;
  padding: var(--space-3);
  font-weight: 600; font-size: var(--text-sm); line-height: 1.3;
  color: var(--charcoal);
}
.navtoggle > summary::-webkit-details-marker { display: none; }
/* Three-bar menu icon drawn with box-shadow — no asset, sits beside the
   visible "Menu" label (icon + text beats icon-only for recognition). */
.navtoggle > summary::before {
  content: ""; display: inline-block; flex: 0 0 auto;
  width: 16px; height: 2px; margin-right: 8px;
  transform: translateY(-4px);
  background: currentColor;
  box-shadow: 0 4px 0 currentColor, 0 8px 0 currentColor;
}
.navtoggle[open] > .nav {
  display: flex; position: absolute; top: 100%; left: 0; right: 0;
  margin-left: 0; flex-direction: column; align-items: stretch; gap: 2px;
  background: #fff;
  padding: var(--space-2) var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
  box-shadow: 0 8px 18px rgba(0, 0, 0, .08);
  animation: navdrop .18s ease-out;
}
@keyframes navdrop {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
.navtoggle[open] > .nav a {
  display: flex; align-items: center; min-height: 44px;
  padding: 10px var(--space-3); font-size: var(--text-base);
}
@media (min-width: 941px) {
  .hdr > .wrap > .nav { display: flex; }
  .hdr > .wrap > .btn { display: inline-flex; }
  .navtoggle { display: none; }
}

/* ==========================================================================
   v3 Track C.1 — header: dark surface over the home hero → solid white after
   scroll, with a slight condense (merged Phase 2).
   `.hdr--overlay` is emitted by partials/header.html on pages with a dark
   hero directly under the header — currently the HOMEPAGE ONLY. State flip
   is driven by the EXISTING .hdr--scrolled sentinel IntersectionObserver in
   enhancements.js — no new JS for C.1. The global reduced-motion
   `transition: none !important` guard makes both the surface flip and the
   height condense instant. */

/* --- shared transition + scrolled condense (all pages) ------------------- */
/* Replaces the old shadow-only `transition: box-shadow .2s ease` removed
   from .hdr above. */
.hdr {
  transition:
    background-color var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
    border-color var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
    box-shadow var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}

/* Slight condense after scroll: padding only (12px -> 8px per side, an 8px
   total height change transitioned over --motion-fast). Transform/opacity
   cannot shrink a sticky header's flow box; this is the sanctioned padding
   mechanism — brief, stable, and its one-time CLS contribution is ~8px /
   viewport height ≈ 0.009, far under the 0.1 budget. Logo width stays
   constant so nothing reflows horizontally. */
.hdr .wrap {
  transition: padding var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
.hdr--scrolled .wrap {
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
}

/* --- overlay surface: dark over the home hero, until first scroll -------- */
/* Near-black matches the hero's rgba(26,26,26,.72) photo overlay, so the
   charcoal utility bar, header and hero read as one cinematic dark stack.
   All rules are scoped :not(.hdr--scrolled), so the moment the sentinel
   leaves the viewport the header falls back to the existing solid-white
   rules and the whole surface (bg, links, logo, button, focus) flips
   together in one --motion-fast transition. */

.hdr--overlay:not(.hdr--scrolled) {
  background: #1a1a1a;
  border-bottom-color: rgba(255, 255, 255, 0.08);
}

/* Nav links: light-on-dark. 85% white on #1a1a1a ≈ 12.4:1; hover/current
   white ≈ 17.4:1. Existing `.nav a` transition already covers color and
   background-color. */
.hdr--overlay:not(.hdr--scrolled) .nav a {
  color: rgba(255, 255, 255, 0.85);
}
.hdr--overlay:not(.hdr--scrolled) .nav a:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.08);
}
/* Current page: #ff8a8a is the established v2 dark-context tint (5.09:1 on
   charcoal, ≈8.9:1 on #1a1a1a). */
.hdr--overlay:not(.hdr--scrolled) .nav a[aria-current="page"] {
  color: #ff8a8a;
  background: rgba(255, 255, 255, 0.06);
  box-shadow: inset 0 -2px 0 #ff8a8a;
}

/* Logo legibility on the dark surface — NO template change and NO second
   SVG: the color logo's charcoal letterforms all carry class .plc-c1
   (partials/logo-color.svg.html), so the wordmark flips to white via fill,
   while the red bolt mark and its gradients stay brand red exactly as they
   do in the footer's dark context. Crossfades with the surface. */
.brand .plc-c1 {
  transition: fill var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
.hdr--overlay:not(.hdr--scrolled) .brand .plc-c1 {
  fill: #fff;
}

/* Tagline on the dark overlay surface: light-on-dark, same treatment as the
   nav links above. */
.hdr--overlay:not(.hdr--scrolled) .hdr__tagline {
  color: #ffffff;
  opacity: 0.82;
}

/* Request Service stays prominent on the dark surface: the red gradient
   already carries it; a deeper drop shadow + hairline ring lift it off the
   near-black instead of letting it sink in. */
.hdr--overlay:not(.hdr--scrolled) .btn--red {
  box-shadow:
    0 3px 12px rgba(0, 0, 0, 0.5),
    0 0 0 1px rgba(255, 255, 255, 0.08);
}

/* Mobile menu button on the dark surface (≤940px). */
.hdr--overlay:not(.hdr--scrolled) .navtoggle > summary {
  color: #fff;
  border-color: rgba(255, 255, 255, 0.4);
}

/* Keyboard focus must stay visible on the dark surface: the global red
   outline (~3:1 on near-black) is recoloured white, mirroring the existing
   .hero/.band/.ft/.util pattern above. */
.hdr--overlay:not(.hdr--scrolled) :focus-visible {
  outline-color: #fff;
}

/* ==========================================================================
   v3 Track C.2 — mobile menu: refined panel on the EXISTING <details
   class="navtoggle"> structure + restrained one-time link stagger + body
   scroll-lock helper (merged Phase 2).
   All rules override the existing .navtoggle rules by source order at equal
   or higher specificity. The existing `navdrop` panel animation is kept —
   it is the panel's entrance; the stagger below layers the rows into it.
   JS counterpart: the nav module in enhancements.js toggles html.nav-open. */

/* --- panel ---------------------------------------------------------------- */

.navtoggle[open] > .nav {
  gap: 0;
  padding: var(--space-2) var(--space-4) var(--space-4);
  box-shadow: 0 18px 36px rgba(0, 0, 0, 0.16);
  /* Never taller than the viewport under the header; scrolls in place while
     the page behind is locked (html.nav-open). */
  max-height: calc(100vh - 140px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Rows: hairline separators instead of floating gaps; 44px minimum height is
   already guaranteed by the existing rule above. */
.navtoggle[open] > .nav a {
  border-bottom: 1px solid var(--line);
}
.navtoggle[open] > .nav a:hover {
  background: #faf1f1;
}
.navtoggle[open] > .nav a[aria-current="page"] {
  color: var(--red);
  background: none;
  box-shadow: inset 3px 0 0 var(--red);
  padding-left: calc(var(--space-3) + 6px);
}

/* Phone action: the drawer's last row becomes a full-width red call button —
   immediately accessible, visually terminal, 48px tall. Attribute selector
   keeps the template untouched. */
.navtoggle[open] > .nav a[href^="tel:"] {
  margin-top: var(--space-3);
  justify-content: center;
  min-height: 48px;
  border-bottom: 0;
  border-radius: var(--radius-sm);
  background: linear-gradient(180deg, var(--red), var(--red-dark));
  color: #fff;
  font-weight: 700;
  box-shadow: 0 3px 10px rgba(var(--red-rgb), 0.28);
}

/* --- link stagger: one-time on open, 30ms/row (≤60ms budget) -------------- */
/* 10 rows -> last row starts at 270ms, settles ~430ms. `both` fill keeps
   rows hidden only until their own delay elapses. Re-runs on each open
   (attribute toggle restarts the animation) — never loops. The stagger sets
   initial opacity ONLY inside @keyframes with fill-mode both, never as a
   static style — so with the global `animation: none !important` guard,
   with JS off, or in any non-animating browser, every row is simply visible
   immediately. The @media wrapper is belt-and-braces on top of that guard. */

@media (prefers-reduced-motion: no-preference) {
  .navtoggle[open] > .nav a {
    animation: navitem var(--motion-fast, 160ms) var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1)) both;
  }
  .navtoggle[open] > .nav a:nth-child(2)  { animation-delay: 30ms; }
  .navtoggle[open] > .nav a:nth-child(3)  { animation-delay: 60ms; }
  .navtoggle[open] > .nav a:nth-child(4)  { animation-delay: 90ms; }
  .navtoggle[open] > .nav a:nth-child(5)  { animation-delay: 120ms; }
  .navtoggle[open] > .nav a:nth-child(6)  { animation-delay: 150ms; }
  .navtoggle[open] > .nav a:nth-child(7)  { animation-delay: 180ms; }
  .navtoggle[open] > .nav a:nth-child(8)  { animation-delay: 210ms; }
  .navtoggle[open] > .nav a:nth-child(9)  { animation-delay: 240ms; }
  .navtoggle[open] > .nav a:nth-child(n+10) { animation-delay: 270ms; }
}

@keyframes navitem {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

/* --- dark-header variant (home, before first scroll) ---------------------- */
/* When the menu opens over the C.1 overlay header, the panel keeps the
   cinematic dark surface instead of dropping a white sheet on the hero. */

.hdr--overlay:not(.hdr--scrolled) .navtoggle[open] > .nav {
  background: #1a1a1a;
  border-bottom-color: rgba(255, 255, 255, 0.12);
  box-shadow: 0 18px 36px rgba(0, 0, 0, 0.45);
}
.hdr--overlay:not(.hdr--scrolled) .navtoggle[open] > .nav a {
  color: rgba(255, 255, 255, 0.92);
  border-bottom-color: rgba(255, 255, 255, 0.08);
}
.hdr--overlay:not(.hdr--scrolled) .navtoggle[open] > .nav a:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.08);
}
.hdr--overlay:not(.hdr--scrolled) .navtoggle[open] > .nav a[aria-current="page"] {
  color: #ff8a8a;
  box-shadow: inset 3px 0 0 #ff8a8a;
}
/* tel: row already carries its own red surface + white text — no override. */

/* --- body scroll-lock helper (toggled by the nav module) ------------------ */
/* Root-level overflow lock; the panel itself stays scrollable via its own
   overflow-y + overscroll-behavior above. */

html.nav-open,
html.nav-open body {
  overflow: hidden;
}

/* ---------- hero ---------- */
.hero { background: var(--charcoal); color: #fff; position: relative; overflow: hidden; }
/* Hero card overlap: gives the services section below extra room to lift
   into via transform (see .section--services-first). */
.hero { padding-bottom: 48px; }
@media (max-width: 640px) {
  .hero { padding-bottom: 24px; }
}
.hero::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(1100px 460px at 78% -8%, rgba(var(--red-rgb), .30), transparent 62%);
}
.hero::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 5px;
  background: linear-gradient(90deg, var(--red), var(--red-dark));
}
.hero .wrap {
  position: relative; max-width: 900px; margin: 0 auto;
  padding: var(--space-12) 20px var(--space-13);
}
.eyebrow {
  display: inline-block; font-size: var(--text-xs); font-weight: 500;
  letter-spacing: 0.08em; line-height: 1.3;
  text-transform: uppercase; color: #ffb3b3; border: 1px solid rgba(255, 255, 255, .25);
  border-radius: var(--radius-pill); padding: var(--space-2) var(--space-4);
  margin-bottom: var(--space-5);
}
.hero h1 {
  font-size: var(--text-4xl); font-weight: 800;
  letter-spacing: -0.02em; line-height: 1.1; margin-bottom: var(--space-5);
}
/* #ff8a8a: 5.09:1 on the charcoal hero (the previous #ff6b6b was 4.16:1). */
.hero h1 .hl { color: #ff8a8a; }
.hero__sub {
  font-size: var(--text-base); color: #d6d6da;
  max-width: 660px; margin-bottom: var(--space-4);
}
.promise {
  font-size: var(--text-lg); font-weight: 600; color: #fff;
  border-left: 4px solid var(--red); padding-left: var(--space-4);
  margin-bottom: var(--space-8); max-width: 640px;
}
.hero__cta { display: flex; flex-wrap: wrap; gap: var(--space-3); }
@media (min-width: 768px) {
  .hero .wrap { padding: 64px 20px 72px; }
}
@media (min-width: 1024px) {
  .hero .wrap { padding: 76px 20px 84px; }
}

/* Compact hero for interior pages. */
.hero--sub .wrap {
  max-width: var(--maxw);
  padding: var(--space-10) 20px var(--space-11);
}
.hero--sub h1 { font-size: var(--text-3xl); margin-bottom: var(--space-4); }
@media (min-width: 768px) {
  .hero--sub .wrap { padding: 52px 20px 56px; }
}

/* CTA buttons: stacked full-width on phones, natural width from sm up. */
.hero__cta .btn,
.band .row .btn { width: 100%; }
@media (min-width: 640px) {
  .hero__cta .btn,
  .band .row .btn { width: auto; }
}

/* ---------- breadcrumbs ---------- */
.crumbs { background: var(--wash); border-bottom: 1px solid var(--line); font-size: var(--text-sm); }
.crumbs .wrap { padding: var(--space-3) 20px; }
.crumbs ol { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.crumbs li::after { content: "›"; margin-left: 6px; color: #9a9aa2; }
.crumbs li:last-child::after { content: ""; }
.crumbs a { color: var(--red-dark); text-decoration: none; font-weight: 600; }
.crumbs a:hover { text-decoration: underline; }
.crumbs [aria-current="page"] { color: var(--muted); }

/* ---------- sections ---------- */
section { padding: var(--space-14) 0; }
@media (min-width: 768px) {
  section { padding: var(--space-16) 0; }
}
@media (min-width: 1024px) {
  section { padding: var(--space-section) 0; }
}
.shead { text-align: center; max-width: 680px; margin: 0 auto var(--space-component); }
.kicker {
  display: block; font-size: var(--text-xs); font-weight: 500;
  letter-spacing: 0.08em; line-height: 1.3;
  text-transform: uppercase; color: var(--red); margin-bottom: var(--space-3);
}
.shead h2 {
  font-size: var(--text-2xl); font-weight: 700;
  letter-spacing: -0.02em; margin-bottom: var(--space-4);
}
.shead p { color: var(--muted); }
.wash { background: var(--wash); }

/* Card grids: explicit columns, mobile-first. Equal-height cards by default. */
.grid { display: grid; gap: var(--space-5); align-items: stretch; }
.g4 { grid-template-columns: repeat(2, 1fr); }
.g3 { grid-template-columns: 1fr; }
.g2 { grid-template-columns: 1fr; }
@media (min-width: 640px) {
  .g3 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 768px) {
  .g2 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .g4 { grid-template-columns: repeat(4, 1fr); }
  .g3 { grid-template-columns: repeat(3, 1fr); }
}

/* Trust strip CSS removed in v3 Track D: the .trust pill strip merged into
   the consolidated proof band (.proof-bar--merged) and no template renders
   .trust any more. */

/* ---------- service cards ---------- */
.svc {
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: var(--space-6) var(--space-5); position: relative;
  display: flex; flex-direction: column;
  transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease;
}
.svc:hover {
  border-color: var(--red);
  box-shadow: 0 12px 28px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .05);
}
/* At the 2-col-on-small-phones tier cards are ~165px wide; trim the padding
   so the text column holds the longest service names without overflow. */
@media (max-width: 639px) {
  .svc { padding: var(--space-5) var(--space-4); }
  .svc h3 { overflow-wrap: break-word; }
}
.svc__i { width: 46px; height: 46px; color: var(--red); margin-bottom: var(--space-4); }
.svc__i svg { width: 100%; height: 100%; }
.svc h3 { font-size: var(--text-lg); font-weight: 600; margin-bottom: var(--space-2); }
.svc h3 a { color: inherit; text-decoration: none; }
/* The whole card is the click target, but the heading link is the real anchor,
   so keyboard focus and screen readers still get one named destination. */
.svc h3 a::after { content: ""; position: absolute; inset: 0; }
.svc h3 a:hover, .svc h3 a:focus-visible { color: var(--red); }
.svc p { font-size: var(--text-sm); color: var(--muted); }
.svc__more {
  display: inline-block; margin-top: auto; padding-top: var(--space-4);
  font-size: var(--text-sm); font-weight: 600;
  color: var(--red); text-decoration: none;
}
.svc__more::after { content: " →"; }
.svc:hover .svc__more { text-decoration: underline; }

/* ---------- why choose ---------- */
.why {
  background: var(--charcoal); color: #fff; border-radius: var(--radius-lg);
  padding: var(--space-8) var(--space-6); position: relative; overflow: hidden;
}
.why::after {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: linear-gradient(180deg, var(--red), var(--red-dark));
}
/* #ffb3b3: 6.79:1 on charcoal (brand red itself is 2.01:1 there). */
.why__n {
  font-size: var(--text-xs); font-weight: 500; letter-spacing: 0.08em;
  color: #ffb3b3; display: block; margin-bottom: var(--space-3);
}
.why h3 { font-size: var(--text-lg); font-weight: 600; margin-bottom: var(--space-3); }
.why p { font-size: var(--text-sm); color: #c9c9d0; }

/* ---------- res/com split ---------- */
.split {
  background: #fff; border: 1px solid var(--line); border-top: 4px solid var(--red);
  border-radius: var(--radius-lg); padding: var(--space-8) var(--space-6);
}
.split h3 { font-size: var(--text-xl); font-weight: 600; margin-bottom: var(--space-2); }
.split > p { color: var(--muted); font-size: var(--text-sm); margin-bottom: var(--space-4); }
.split ul { margin: 0; padding-left: var(--space-5); color: #3f3f45; font-size: var(--text-sm); }
.split li { margin-bottom: var(--space-2); }

/* ---------- areas ---------- */
/* Flex-wrap pill flow: content-width tiles that wrap cleanly at any viewport
   (the old minmax(168px) grid sat exactly on the 390px borderline). */
.areas {
  list-style: none; margin: 0 auto var(--space-5); padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  justify-content: center; max-width: 940px;
}
.areas li {
  background: #fff; border: 1px solid var(--line); border-left: 3px solid var(--red);
  border-radius: 5px; padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm); font-weight: 600;
}
/* Whole tile is the tap target (~50px tall); also styles area.html's nearby
   list, which lacks the --linked modifier. */
.areas a {
  display: block; margin: calc(-1 * var(--space-3)) calc(-1 * var(--space-4));
  padding: var(--space-3) var(--space-4);
  color: inherit; font-weight: 600; text-decoration: none;
}
.areas a:hover,
.areas a:focus-visible { text-decoration: underline; }
.areas-note { text-align: center; color: var(--muted); font-size: var(--text-sm); max-width: 780px; margin: 0 auto; }
.area-county { color: rgba(255, 255, 255, .82); font-size: var(--text-sm); margin-top: var(--space-3); }

/* ---------- area-page hero photo ----------
   Unrelated to the service-page rebuild below: this is the plain framed
   image used on the Nashville area page hero (area.html) and the areas hub
   header visual (service-areas.html), from an earlier, separate patch. This
   mission is scoped to service pages, so it's kept as-is rather than folded
   into .svc-photo -- deleting it (as briefly happened while clearing the
   three service-page attempts below) left those two area images completely
   unstyled, which was a regression, not an intentional removal. */
.svc-hero-img {
  width: 100%;
  height: 420px;
  object-fit: cover;
  object-position: center;
  display: block;
  border-radius: 8px;
  margin-bottom: var(--space-8);
}
@media (max-width: 640px) {
  .svc-hero-img { height: 240px; }
}

/* ---------- service-page photo band (approved 65/35 staggered mockup) ----------
   Replaces every prior service-page photo attempt (.svc-gallery grid, its
   background retrofit, and the .svc-photo full-bleed bands -- history in
   PHOTO_BAND_AUDIT.md). One continuous near-black band per page holds ALL
   of that page's photos as staggered 65/35 photo/text rows.

   The <section> is a bare sibling of the page's other sections (outside
   .wrap), so the #1a1a1a band is natively full width -- same mechanism as
   .hero/.band. __inner reapplies the 1180px content width so the photos sit
   INSET with rounded corners: framed, not full-bleed. The band sits directly
   under the charcoal .hero--sub; the hero's existing 5px red-gradient seam
   already separates those two dark surfaces, so no extra seam is needed.

   Reveals: each row's photo carries data-reveal-dir for ITS side and the
   text the opposite side; enhancements.js adds .reveal--left/--right at
   runtime (templates never carry reveal classes -- no-JS-safe contract,
   REDESIGN_AUDIT.md §7). Text trails its photo by 120ms. */
.svc-band {
  background: var(--surface-dark);
  padding: var(--space-section) 0;
}
.svc-band__inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-10);
}
.svc-band__row {
  display: grid;
  grid-template-columns: 65% 1fr;
  gap: var(--space-8);
  align-items: center;
}
/* Flip rows: photo moves to the wide SECOND column, text mirrors right. */
.svc-band__row--flip {
  grid-template-columns: 1fr 65%;
}
.svc-band__row--flip .svc-band__photo { order: 2; }
.svc-band__row--flip .svc-band__text {
  order: 1;
  text-align: right;
}
.svc-band__photo img {
  width: 100%;
  height: clamp(280px, 32vw, 400px);
  object-fit: cover;
  object-position: center;
  border-radius: 8px;
  display: block;
}
/* 48px current-line accent above each heading -- .cline vocabulary reused
   (gradient/height come from the base class), only width + flow margins are
   set here. Mirrors to the right edge on flip rows via auto margin. */
.svc-band__text .cline {
  width: 48px;
  margin: 0 0 var(--space-4);
}
.svc-band__row--flip .svc-band__text .cline { margin-left: auto; }
.svc-band__text h3 {
  color: #fff;
  font-size: var(--text-xl);
  font-weight: 700;
  margin: 0 0 var(--space-3);
}
.svc-band__text p {
  color: rgba(255, 255, 255, 0.82);
  font-size: var(--text-base);
  line-height: 1.7;
  margin: 0;
}
/* Text trails its photo into view. Scoped to the band so the shared
   .reveal--left/--right timing elsewhere is untouched. */
.svc-band__text.reveal--left,
.svc-band__text.reveal--right {
  transition-delay: 120ms, 120ms;
}
@media (max-width: 768px) {
  .svc-band__row,
  .svc-band__row--flip {
    grid-template-columns: 1fr;
  }
  /* Stack: photo on top, text below, mirroring off -- left-aligned on BOTH
     variants. */
  .svc-band__row--flip .svc-band__photo { order: 1; }
  .svc-band__row--flip .svc-band__text {
    order: 2;
    text-align: left;
  }
  .svc-band__row--flip .svc-band__text .cline { margin-left: 0; }
  .svc-band__photo img { height: clamp(200px, 45vw, 280px); }
  /* Side slides become the site's standard mobile fade-up. */
  .svc-band__photo.reveal--left,
  .svc-band__photo.reveal--right,
  .svc-band__text.reveal--left,
  .svc-band__text.reveal--right {
    transform: translateY(24px);
  }
  .svc-band__photo.reveal--left.is-visible,
  .svc-band__photo.reveal--right.is-visible,
  .svc-band__text.reveal--left.is-visible,
  .svc-band__text.reveal--right.is-visible {
    transform: none;
  }
}

/* ---------- photo placeholder ----------
   Branded holding pattern. images.make_image_placeholder emits this when the
   photograph does not exist yet; checks.py still fails a --production build
   that contains one, so this styling is staging-only presentation — it should
   look intentional while it waits, not broken. */
.photo-ph {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-2); text-align: center;
  min-height: 220px; padding: var(--space-8) var(--space-6);
  position: relative; overflow: hidden;
  background: linear-gradient(135deg, var(--red), var(--charcoal));
  border: 1px dashed rgba(255, 255, 255, .4);
  border-radius: var(--radius-md); color: #fff;
}
/* CSS-drawn lightning-bolt mark — white at 20%, no external asset. */
.photo-ph::before {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 64px; height: 96px; background: #fff; opacity: .2;
  clip-path: polygon(70% 0, 20% 55%, 48% 55%, 30% 100%, 80% 45%, 52% 45%);
  pointer-events: none;
}
.photo-ph__label {
  position: relative; color: #fff; font-weight: 500;
  font-size: var(--text-xs); text-transform: uppercase;
  letter-spacing: 0.08em; line-height: 1.3;
}
.photo-ph__alt {
  position: relative; color: #fff; font-style: italic;
  font-size: var(--text-sm); max-width: 60ch;
}

/* ---------- customer reviews ----------
   One card shape for every review, on every page. A five-star Google review
   and a one-star Google review are styled identically on purpose: the card
   must not editorialise, and nothing here dims, shrinks or de-emphasises a
   review by its rating. Markup comes from builder/plecssg/reviews.py. */
.rev-summary { margin-top: var(--space-4); font-size: var(--text-sm); }
.rev-summary__dot { margin: 0 8px; opacity: .6; }

/* One rhythm everywhere: 1-col on phones, 2-col from md up.
   .rev-list--featured (homepage) and --service inherit it. */
.rev-list { display: grid; gap: var(--space-5); grid-template-columns: 1fr; }
@media (min-width: 768px) {
  .rev-list { grid-template-columns: repeat(2, 1fr); }
}
/* A section with exactly one review should not show a half-width card
   beside an empty cell. */
.rev-list > .rev:only-child {
  grid-column: 1 / -1; width: 100%; max-width: 720px; justify-self: center;
}

.rev {
  margin: 0; background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-6) var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-3);
  animation: rev-in .25s ease both;
}
.rev[hidden] { display: none; }
/* reviews.js toggles [hidden]; display:none → flex restarts the animation,
   so filter-revealed cards fade in with zero extra JS. */
@keyframes rev-in {
  from { opacity: 0; transform: translateY(5px); }
  to   { opacity: 1; transform: none; }
}
/* Star-only reviews: a compact separator row instead of a near-empty card. */
.rev--compact {
  flex-direction: row; flex-wrap: wrap; align-items: center; align-self: start;
  gap: var(--space-2) var(--space-4);
  background: transparent; border: 0; border-bottom: 1px solid var(--line);
  border-radius: 0; padding: var(--space-3) var(--space-2);
}
.rev--compact .rev__head { flex: 1 1 auto; }

.rev__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.rev__who { font-weight: 600; font-size: var(--text-sm); color: var(--charcoal); }
.rev__date { font-size: var(--text-sm); color: var(--muted); }
.rev__badge {
  font-size: var(--text-xs); font-weight: 500; letter-spacing: 0.08em;
  line-height: 1.3; text-transform: uppercase;
  padding: var(--space-1) var(--space-2); border-radius: var(--radius-pill);
  border: 1px solid var(--line); color: var(--muted);
}
/* Filled platform pills. Deeper shades of each platform's own colour so the
   white label passes 4.5:1 (Google's #4285f4 is 3.56:1, Thumbtack's #009fd9
   is 3.02:1). Identical on every card regardless of rating. */
.rev__badge--google { background: #1a73e8; border-color: #1a73e8; color: #fff; }
.rev__badge--thumbtack { background: #0073a8; border-color: #0073a8; color: #fff; }
.rev__svc { flex-basis: 100%; font-size: var(--text-xs); color: var(--muted); }
.rev__stars { color: var(--red); letter-spacing: 2px; font-size: var(--text-sm); line-height: 1; }
.rev__verified { font-size: var(--text-xs); font-weight: 600; color: #3d5c4c; }
.rev__q blockquote { margin: 0; font-size: var(--text-base); color: #3f3f45; font-style: italic; }

/* Filter bar. Ships hidden and is revealed by /js/reviews.js — without the
   script every card stays visible, so the filter can never hide content. */
.rev-filter { display: flex; flex-wrap: wrap; gap: var(--space-3); justify-content: center; margin-bottom: var(--space-7); }
.rev-filter[hidden] { display: none; }
.rev-filter button {
  font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: 1.3;
  cursor: pointer; background: #fff; color: var(--charcoal);
  border: 1px solid var(--line); border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4); min-height: 44px;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.rev-filter button:hover {
  border-color: var(--red); color: var(--red);
  background: rgba(var(--red-rgb), .05);
}
.rev-filter button[aria-pressed="true"] { background: var(--red); border-color: var(--red); color: #fff; }

/* Featured reviews (homepage): quiet accent + oversized quote watermark. */
.rev-list--featured .rev {
  position: relative; overflow: hidden;
  border-left: 3px solid var(--red);
}
.rev-list--featured .rev::before {
  content: "\201C";
  position: absolute; top: var(--space-1); right: var(--space-4);
  font-size: 84px; line-height: 1; font-weight: 800;
  color: rgba(var(--red-rgb), .15);
  pointer-events: none;
}

/* ---------- FAQ accordion ----------
   Built on <details>, so every answer is readable with JavaScript disabled.
   The script only adds one-open-at-a-time behaviour and a soft open. */
.accs { max-width: 820px; margin: 0 auto; }
.acc {
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius-md);
  margin-bottom: var(--space-3); overflow: hidden;
}
.acc > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center;
  justify-content: space-between; gap: var(--space-4); padding: var(--space-5);
  font-weight: 600; font-size: var(--text-base); line-height: 1.3;
  color: var(--charcoal);
}
.acc > summary::-webkit-details-marker { display: none; }
.acc > summary:hover { color: var(--red); }
.acc__c { width: 19px; height: 19px; flex: 0 0 auto; transition: transform .2s; color: var(--red); }
.acc[open] .acc__c { transform: rotate(180deg); }
.acc__a { padding: 0 var(--space-5) var(--space-5); font-size: var(--text-base); color: var(--muted); border-top: 1px solid #f0f0f3; }
.acc__a p { padding-top: var(--space-4); }
.acc__a p + p { padding-top: var(--space-3); }
.acc__a a { color: var(--red-dark); font-weight: 600; }

/* Copy quoted word-for-word from the recovered previous site, marked so the
   reader can tell PLEC's current words from its former ones. */
.prose [data-verbatim] {
  margin: var(--space-5) 0; padding: var(--space-4) var(--space-5);
  background: var(--wash); border-left: 3px solid var(--red);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.prose [data-verbatim] blockquote { margin: 0; padding: 0; border: 0; font-style: normal; }
.prose [data-verbatim] p { margin: 0 0 var(--space-2); }
.prose [data-verbatim] p:last-child { margin-bottom: 0; }
.prose [data-verbatim] cite {
  display: block; margin-top: var(--space-3); font-size: var(--text-xs);
  font-style: normal; color: var(--muted); letter-spacing: .01em;
}
.faq-cat { max-width: 820px; margin: 0 auto var(--space-4); }
.faq-cat h2 {
  font-size: var(--text-sm); font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--red);
  margin: var(--space-10) 0 var(--space-4); padding-bottom: var(--space-2);
  border-bottom: 2px solid var(--line);
}
.faq-more { text-align: center; margin-top: var(--space-6); }
.faq-more a { color: var(--red); font-weight: 600; text-decoration: none; border-bottom: 2px solid rgba(var(--red-rgb), .3); }

/* ---------- prose (about, legal, hubs) ---------- */
.prose { max-width: 780px; margin: 0 auto; }
.prose h2 { font-size: var(--text-xl); font-weight: 700; margin: var(--space-8) 0 var(--space-3); }
.prose h3 { font-size: var(--text-lg); font-weight: 600; margin: var(--space-6) 0 var(--space-2); }
.prose p, .prose li { color: #3f3f45; }
.prose p { margin-bottom: var(--space-4); }
.prose ul, .prose ol { margin: 0 0 var(--space-4); padding-left: var(--space-5); }
.prose li { margin-bottom: var(--space-2); }
.prose a { color: var(--red-dark); font-weight: 600; }
.prose strong { color: var(--ink); }
.prose table { width: 100%; border-collapse: collapse; margin-bottom: var(--space-5); font-size: var(--text-sm); }
.prose th, .prose td { border: 1px solid var(--line); padding: var(--space-2) var(--space-3); text-align: left; }
.prose th { background: var(--wash); }

/* ---------- AEO quick-answer block (article pages) ----------
   A direct, self-contained answer to the article's core question, placed
   before the intro paragraph so an answer engine (or a skimming reader) gets
   the point without reading the rest of the piece. */
.quick-answer {
  background: var(--wash);
  border-left: 4px solid var(--red);
  padding: var(--space-4) var(--space-5);
  margin: 0 0 var(--space-6);
  border-radius: 0 4px 4px 0;
  font-size: var(--text-base);
  line-height: 1.65;
}
.quick-answer strong {
  color: var(--red);
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ---------- blog hub + article byline ---------- */
.article-list {
  display: grid; gap: var(--space-5); grid-template-columns: 1fr;
  align-items: stretch;
}
@media (min-width: 640px) {
  .article-list { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .article-list { grid-template-columns: repeat(3, 1fr); }
}
.article-card {
  position: relative; display: flex; flex-direction: column;
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: var(--space-6) var(--space-5);
  transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease;
}
.article-card:hover {
  border-color: var(--red);
  box-shadow: 0 12px 28px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .05);
}
.article-card h3 {
  font-size: var(--text-xl); font-weight: 600; line-height: 1.2;
  letter-spacing: -0.02em; margin-bottom: var(--space-2);
}
/* Stretched-link pattern, same as .svc. */
.article-card h3 a { color: inherit; text-decoration: none; }
.article-card h3 a::after { content: ""; position: absolute; inset: 0; }
.article-card h3 a:hover,
.article-card h3 a:focus-visible { color: var(--red); }
.article-card__meta {
  font-size: var(--text-sm); color: var(--muted); line-height: 1.3;
  margin-bottom: var(--space-2);
}
/* The template text already carries the arrow; position:relative keeps the
   link clickable above the stretched link. */
.article-card__cta {
  position: relative; display: inline-block;
  margin-top: auto; padding-top: var(--space-4);
  font-size: var(--text-sm); font-weight: 600;
  color: var(--red); text-decoration: none;
}
.article-card:hover .article-card__cta { text-decoration: underline; }
/* Byline inside the charcoal article hero — #d6d6da is the hero's existing
   muted-on-dark (var(--muted) would be ~2.2:1 there). */
.article-meta { font-size: var(--text-sm); color: #d6d6da; margin-top: var(--space-3); }
.article-meta__sep { opacity: 0.6; }

/* ---------- notices ---------- */
.note {
  border: 1px solid #ffd88a; background: #fff8e8; color: #6b4a00;
  border-radius: var(--radius-md); padding: var(--space-4);
  font-size: var(--text-sm); margin-bottom: var(--space-5);
}
.note strong { color: #5a3d00; }
.empty {
  text-align: center; max-width: 620px; margin: 0 auto;
  border: 2px dashed var(--line); border-radius: 10px;
  padding: var(--space-8) var(--space-6); background: #fff;
}
.empty h2 { font-size: var(--text-xl); font-weight: 700; margin-bottom: var(--space-3); }
.empty p { color: var(--muted); margin-bottom: var(--space-4); }

/* ---------- contact / form ---------- */
.contact-grid { display: grid; gap: var(--space-8); grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); align-items: start; }
.card { background: #fff; border: 1px solid var(--line); border-radius: var(--radius-lg); padding: var(--space-8) var(--space-6); }
.card h2 { font-size: var(--text-xl); font-weight: 700; margin-bottom: var(--space-4); }
.card h3 {
  font-size: var(--text-sm); font-weight: 500; letter-spacing: 0.08em;
  line-height: 1.3; text-transform: uppercase; color: var(--muted);
  margin: var(--space-5) 0 var(--space-2);
}
.contact-list { list-style: none; margin: 0; padding: 0; font-size: var(--text-base); }
.contact-list li { margin-bottom: var(--space-3); display: flex; gap: var(--space-3); align-items: flex-start; }
.contact-list a { color: var(--red-dark); font-weight: 600; text-decoration: none; }
.contact-list a:hover { text-decoration: underline; }

.field { margin-bottom: var(--space-5); }
.field label { display: block; font-weight: 600; font-size: var(--text-sm); margin-bottom: var(--space-2); color: #33333a; }
.field .req { color: var(--red); }
/* --text-base floors at 1rem/16px, so iOS never zooms the focused field. */
.field input, .field select, .field textarea {
  width: 100%; font: inherit; font-size: var(--text-base); color: var(--ink); background: #fff;
  border: 1px solid #c9c9d2; border-radius: 5px; padding: var(--space-3) var(--space-4);
}
.field textarea { min-height: 130px; resize: vertical; }
.field .hint { display: block; font-size: var(--text-xs); color: var(--muted); margin-top: var(--space-1); }
.fieldset { border: 1px solid var(--line); border-radius: var(--radius-md); padding: var(--space-4); margin-bottom: var(--space-5); }
.fieldset legend { font-weight: 600; font-size: var(--text-sm); padding: 0 var(--space-2); }
.radio-row { display: flex; flex-wrap: wrap; gap: var(--space-4); }
.radio-row label { display: flex; align-items: center; gap: var(--space-2); font-weight: 500; font-size: var(--text-base); min-height: 44px; }
.radio-row input { width: auto; }
.consent { display: flex; gap: var(--space-3); align-items: flex-start; font-size: var(--text-sm); color: #3f3f45; }
.consent input { width: auto; margin-top: 4px; }

/* ---------- CTA band ---------- */
.band {
  background: linear-gradient(135deg, var(--red), var(--red-dark));
  color: #fff; text-align: center;
}
.band .wrap { padding: var(--space-section) 20px; max-width: 790px; }
.band h2 { font-size: var(--text-2xl); font-weight: 700; margin-bottom: var(--space-4); }
.band p { color: rgba(255, 255, 255, .93); margin-bottom: var(--space-element); font-size: var(--text-base); }
.band .row { display: flex; flex-wrap: wrap; gap: var(--space-3); justify-content: center; }
.band .fine { font-size: var(--text-sm); color: rgba(255, 255, 255, .85); margin-top: var(--space-5); margin-bottom: 0; }

/* ---------- footer ---------- */
.ft { background: var(--charcoal); color: #b9b9c1; font-size: var(--text-sm); }
.ft .wrap { padding: var(--space-section) 20px var(--space-6); }
/* Five blocks: logo/blurb (2fr) + four link columns. 2×2 on tablets with the
   logo block full-width; single column on phones. */
.ft__top { display: grid; grid-template-columns: 2fr repeat(4, 1fr); gap: var(--space-8) var(--space-6); }
@media (max-width: 1023px) {
  .ft__top { grid-template-columns: 1fr; gap: var(--space-8); }
}
@media (min-width: 640px) and (max-width: 1023px) {
  .ft__top { grid-template-columns: repeat(2, 1fr); }
  .ft__top > :first-child { grid-column: 1 / -1; }
}
.ft__logo {
  display: flex; flex-direction: column; align-items: center;
}
/* 240px preserves the footer logo's existing ~0.94 ratio to the header
   logo (200/212 pre-patch) against the header's new 255px desktop width. */
.ft__logo svg { width: 240px; margin-bottom: var(--space-4); }
.ft h2 {
  color: #fff; font-size: var(--text-xs); font-weight: 500;
  letter-spacing: 0.08em; line-height: 1.3;
  text-transform: uppercase; margin: 0 0 var(--space-4);
}
.ft ul { list-style: none; margin: 0; padding: 0; }
.ft li { margin-bottom: var(--space-2); }
.ft a { color: #b9b9c1; text-decoration: none; overflow-wrap: anywhere; transition: color .15s ease; }
.ft a:hover { color: #fff; text-decoration: underline; }
.ft__lic {
  display: inline-block; margin-top: var(--space-3); border: 1px solid rgba(255, 255, 255, .2);
  border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs); line-height: 1.3; color: #e0e0e6;
}
.ft__tagline {
  margin: 0 0 var(--space-3); width: 100%; text-align: center;
  font-size: calc(var(--text-xs) * 0.85); font-weight: 500; letter-spacing: 0.06em;
  line-height: 1.2; white-space: normal;
  color: #ffffff; opacity: 0.75;
}
/* #a3a3ac: 4.62:1 on charcoal (the previous #8f8f98 was 3.60:1). */
.ft__bot {
  border-top: 1px solid rgba(255, 255, 255, .13); margin-top: var(--space-10);
  padding-top: var(--space-5);
  display: flex; flex-wrap: wrap; gap: var(--space-3); justify-content: space-between;
  font-size: var(--text-xs); color: #a3a3ac;
}

/* ---------- staging banner ---------- */
.staging {
  background: #fff6e5; border-bottom: 1px solid #f0d9a8; color: #7a4b00;
  font-size: var(--text-sm); text-align: center; padding: var(--space-2) var(--space-5);
}

/* ---------- JS enhancement hooks (enhancements.js) ----------
   Both elements are created by the script; without JavaScript they simply
   never exist. */
.to-top {
  position: fixed; right: var(--space-5); bottom: var(--space-5); z-index: 80;
  width: 48px; height: 48px; min-width: 44px; min-height: 44px;
  border: 0; border-radius: 50%; cursor: pointer;
  background: var(--red); color: #fff;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .25);
  opacity: 0; pointer-events: none; transform: translateY(8px);
  transition: opacity .2s ease, transform .2s ease, background-color .15s ease;
}
.to-top--show { opacity: 1; pointer-events: auto; transform: none; }
.to-top:hover { background: var(--red-dark); }

/* Reading-progress bar (article pages only); the script sets scaleX(0–1). */
.progress {
  position: fixed; top: 0; left: 0; z-index: 90;
  width: 100%; height: 3px;
  background: linear-gradient(90deg, var(--red), var(--red-dark));
  transform: scaleX(0); transform-origin: 0 50%;
  pointer-events: none;
}

/* ---------- v3 Track C.3 — sticky mobile conversion bar (merged Phase 2) --
   The element is created by enhancements.js exactly like .to-top: no-JS
   pages never get it, so nothing here needs a no-JS fallback.
   CLS: position:fixed + transform/opacity/visibility only — the bar never
   participates in layout, so its appearance shifts nothing (ZERO CLS).
   FOCUS/A11Y: `visibility: hidden` while off-screen removes the two links
   from the tab order and the accessibility tree, so keyboard users never
   focus an invisible control. The visibility transition is delayed until
   the slide-out finishes; on show it is instant.
   REDUCED MOTION: the global `transition: none !important` guard makes
   show/hide instant — exactly the required behavior. No pulse, ever. */

.cta-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 70; /* above header (50), below .to-top (80) and .progress (90) */
  display: none; /* mobile-only: flex ≤767px below */
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4)
           calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  background: rgba(17, 17, 17, 0.96);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.28);
  transform: translateY(100%);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    transform var(--motion-standard, 320ms) var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1)),
    opacity var(--motion-standard, 320ms) var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1)),
    visibility 0s linear var(--motion-standard, 320ms);
}

@media (max-width: 767px) {
  .cta-bar { display: flex; }
}

.cta-bar--show {
  transform: none;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition-delay: 0s;
}

/* Both actions reuse the existing button system (.btn .btn--red for Call,
   .btn .btn--ghost for Request Estimate — ghost is already designed for
   dark surfaces: rgba(255,255,255,.55) border, white text). Equal width,
   48px minimum target height (>44px requirement). */
.cta-bar .btn {
  flex: 1 1 0;
  min-height: 48px;
  padding: var(--space-3) var(--space-3);
  font-size: var(--text-sm);
}

/* Focus outlines on the near-black bar: red is invisible here — recolour
   white, matching the established dark-surface pattern. */
.cta-bar :focus-visible {
  outline-color: #fff;
}

/* Keep the back-to-top button clear of the bar while it is shown
   (enhancements.js mirrors visibility onto body.has-cta-bar). */
@media (max-width: 767px) {
  body.has-cta-bar .to-top {
    bottom: calc(84px + env(safe-area-inset-bottom, 0px));
  }
}

/* ---------- touch targets ≤940px ----------
   44px minimum via inline padding + equal negative margin, so nothing shifts.
   Already ≥44px with no rule needed: .btn, form fields, .acc summary,
   drawer rows, .navtoggle summary, .areas a, .rev-filter button, .to-top. */
@media (max-width: 940px) {
  .hdr__tagline { display: none; }
  .svc__more { padding: 12px 16px 12px 0; margin: 2px 0 -12px; margin-top: auto; }
  .crumbs a { display: inline-block; padding: 11px 6px; margin: -11px -6px; }
  .util a { display: inline-block; padding: 11px 6px; margin: -11px -6px; }
  .faq-more a { display: inline-block; padding: 12px 6px; margin: -12px -6px; }
  .ft li { margin-bottom: 2px; }
  .ft a { display: inline-block; padding: 11px 0; }
}

/* ---------- overflow safety ---------- */
/* Tables in prose are the one component that can force page-level horizontal
   scroll at 390px; scroll them in place instead. */
.prose table { display: block; overflow-x: auto; }
.prose pre { overflow-x: auto; }
.prose a, .contact-list a { overflow-wrap: anywhere; }

/* ---------- hover-capability guard ----------
   Lift effects only for real pointers; touch devices keep colour/shadow cues
   without a stuck transform. */
@media (hover: hover) {
  .btn:hover { transform: translateY(-2px); }
  .btn[aria-disabled="true"]:hover { transform: none; }
  .svc:hover { transform: translateY(-3px); }
  .article-card:hover { transform: translateY(-3px); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}

@media print {
  .util, .hdr, .band, .staging, .crumbs, .to-top, .progress, .cta-bar { display: none; }
  body { color: #000; }
}

/* ==========================================================================
   v2 redesign — Agent A: section alternation (must follow all base rules)
   ========================================================================== */
/* v2 patch — Agent A: section alternation + visual depth.
   Must load AFTER main.css: several overrides here tie on specificity with
   main.css rules (.section--white vs a leftover .wash, dark-context recolours
   vs the light-surface defaults) and win on source order only.
   Scope: surface + colour only. Section padding stays in main.css so the
   responsive ramp to --space-section is untouched. Headings carry no colour
   of their own in main.css, so they inherit each section's colour; the rules
   below recolour only descendants that hard-code a light-surface colour,
   scoped under the dark section classes. */

:root {
  --surface-dark: #1a1a1a;          /* near-black authority surface */
  --surface-mid: var(--charcoal);   /* #393939 */
  --surface-light: var(--wash);     /* #f5f3f0 */

  /* Dark-context tints reuse main.css precedents: #ff8a8a (.hero .hl,
     5.09:1 on charcoal) and #ffb3b3 (.why__n, 6.79:1 on charcoal).
     Brand red itself fails as text on both dark surfaces (2.01:1 on #393939,
     3.03:1 on #1a1a1a) so on dark it stays decorative, never copy. */
  --on-dark-body: rgba(255, 255, 255, .75); /* 10.2:1 on #1a1a1a, 7.3:1 on #393939 */
  --on-dark-red: #ff8a8a;                   /* 7.7:1 on #1a1a1a, 5.1:1 on #393939 */
  --on-dark-kicker: #ffb3b3;                /* 10.2:1 on #1a1a1a, 6.8:1 on #393939 */
  --on-dark-card: rgba(255, 255, 255, .06);
  --on-dark-line: rgba(255, 255, 255, .12);
}

/* ---------- section surfaces ---------- */
.section--dark  { background: var(--surface-dark);  color: #fff; }
.section--mid   { background: var(--surface-mid);   color: #fff; }
.section--light { background: var(--surface-light); color: #1a1a1a; }
.section--white { background: #fff;                 color: #1a1a1a; }

/* Two dark surfaces meeting would read as one slab; a 1px translucent brand
   seam keeps the boundary without shouting. Covers every --dark/--mid
   adjacency in either order. (The hero needs no entry here: its existing 5px
   red gradient rule already separates it from a dark section below.) */
.section--dark + .section--dark,
.section--dark + .section--mid,
.section--mid + .section--dark,
.section--mid + .section--mid {
  border-top: 1px solid rgba(var(--red-rgb), .35);
}

/* ---------- hero card overlap ----------
   translateY lifts the ENTIRE services section (heading + cards together)
   as one rigid unit, so nothing needs to know the heading's rendered
   height — unlike the negative-margin approaches tried previously (both
   pulling the section's own margin/padding and pulling .grid.g4 alone),
   which either got absorbed by the heading before reaching the cards or
   required a fragile guess at the heading's height. transform doesn't
   affect document flow, so margin-bottom compensates by the same amount
   to stop the next section leaving a gap where the lifted section used
   to sit. */
.section--services-first {
  position: relative;
  z-index: 2;
  transform: translateY(-48px);
  margin-bottom: -48px;
}
/* Cards that overlap the hero get a stronger shadow to read as "lifted". */
.section--services-first .svc {
  box-shadow:
    0 -4px 12px rgba(0, 0, 0, 0.15),
    0 8px 32px rgba(0, 0, 0, 0.12);
}
@media (max-width: 640px) {
  .section--services-first {
    transform: translateY(-24px);
    margin-bottom: -24px;
  }
}

/* ---------- dark-context text ----------
   These descendants hard-code light-surface colours in main.css
   (--muted #5d5d64, brand red, red-dark) and go illegible on dark. */
.section--dark .shead p,
.section--mid .shead p,
.section--dark .areas-note,
.section--mid .areas-note {
  color: var(--on-dark-body);
}
.section--dark .kicker,
.section--mid .kicker {
  color: var(--on-dark-kicker);
}
.section--dark .faq-more a,
.section--mid .faq-more a {
  color: var(--on-dark-red);
  border-bottom-color: rgba(255, 138, 138, .35);
}

/* ---------- why cards on a dark section ----------
   Charcoal-on-white cards vanish on --mid; translucent face + hairline keeps
   the card shape. The red gradient edge (::after) is positioned inside the
   padding box, so the new border does not displace it. Card body text
   #c9c9d0 measures 5.8:1 on the composited face — no recolour needed. */
.section--mid .why,
.section--dark .why {
  background: var(--on-dark-card);
  border: 1px solid var(--on-dark-line);
}

/* ---------- FAQ accordion on a dark section ----------
   White cards glare on #1a1a1a; translucent face, white summary, softened
   answer text. The chevron keeps brand red per the v2 spec: it is a
   redundant cue (summary text + native <details> disclosure carry state), so
   its 2.6:1 on the dark card face is accepted and recorded, not hidden. */
.section--dark .acc,
.section--mid .acc {
  background: var(--on-dark-card);
  border-color: var(--on-dark-line);
}
.section--dark .acc > summary,
.section--mid .acc > summary {
  color: #fff;
}
.section--dark .acc > summary:hover,
.section--mid .acc > summary:hover {
  color: var(--on-dark-red);
}
.section--dark .acc__a,
.section--mid .acc__a {
  color: var(--on-dark-body);
  border-top-color: var(--on-dark-line);
}
.section--dark .acc__a a,
.section--mid .acc__a a {
  color: var(--on-dark-red);
}

/* ---------- area pills on a dark section ----------
   White tiles would either glare or (worse) inherit white text. Translucent
   tile, hairline border, red edge kept as the brand accent; text inherits
   the section's white (9.6:1 on the composited face over #393939). */
.section--dark .areas li,
.section--mid .areas li {
  background: var(--on-dark-card);
  border-color: var(--on-dark-line);
  border-left-color: var(--red);
}

/* ---------- legibility guard ----------
   Components that keep a white card face must not inherit the section's
   white text if a later composition drops them on a dark surface. Agent D
   owns the real dark-card variants for .svc/.rev; this only restores
   legibility in the meantime and costs nothing once those land. */
.section--dark .svc,
.section--mid .svc,
.section--dark .split,
.section--mid .split,
.section--dark .rev,
.section--mid .rev,
.section--dark .card,
.section--mid .card {
  color: var(--ink);
}

/* ---------- focus visibility ----------
   Same constraint as main.css line ~107: the global 3px red outline is
   invisible on dark surfaces exactly where interactive elements live —
   recoloured, never suppressed. */
.section--dark :focus-visible,
.section--mid :focus-visible {
  outline-color: #fff;
}

/* ---------- footer deepening ----------
   v2 map ends the page on #111 (audit §2). Modifier for the main session to
   add beside .ft; existing .ft text colours all clear AA on #111
   (#b9b9c1 → 9.7:1, #a3a3ac → 7.5:1, #e0e0e6 → 12.7:1). */
.ft--deep {
  background: #111;
}

/* ==========================================================================
   v2 redesign — Agent B: scroll reveal + interactions
   ========================================================================== */
/* PLEC redesign v2 — Agent B patch: scroll animations + interactions.
   Loaded AFTER main.css; purely additive.

   The reveal system is fully JS-driven (REDESIGN_AUDIT.md §7, binding):
   templates never carry .reveal — enhancements.js adds the class at runtime
   and then observes, so with JavaScript disabled nothing on the page ever
   renders at opacity 0. Every rule below is therefore inert until JS opts
   an element in. */

/* ---------- scroll reveal (classes added by JS only) ---------- */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .5s ease, transform .5s ease;
}
.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* Stagger for card grids — JS cycles these across siblings that share a
   container (delay-1 → delay-4, then wraps). */
.reveal--delay-1 { transition-delay: .1s; }
.reveal--delay-2 { transition-delay: .2s; }
.reveal--delay-3 { transition-delay: .3s; }
.reveal--delay-4 { transition-delay: .4s; }

/* ---------- alternating side reveal (quick patch) ----------
   Same JS-driven contract as .reveal above: templates carry a data-reveal-dir
   attribute only (never the class itself), and enhancements.js adds
   .reveal--left/.reveal--right at runtime before observing — see the
   REDESIGN_AUDIT.md §7 rule cited above, which this reuses rather than
   overrides. */
.reveal--left {
  opacity: 0;
  transform: translateX(-40px);
  transition:
    opacity var(--motion-reveal, 520ms) var(--ease-out),
    transform var(--motion-reveal, 520ms) var(--ease-out);
}
.reveal--right {
  opacity: 0;
  transform: translateX(40px);
  transition:
    opacity var(--motion-reveal, 520ms) var(--ease-out),
    transform var(--motion-reveal, 520ms) var(--ease-out);
}
.reveal--left.is-visible,
.reveal--right.is-visible {
  opacity: 1;
  transform: none;
}

/* Explicit reduced-motion coverage. The blanket kill-switch at the bottom of
   main.css already neutralises these transitions, and the JS never adds
   .reveal under reduced motion in the first place — this block is the
   third, explicit layer Phase 3 greps for. */
@media (prefers-reduced-motion: reduce) {
  .reveal,
  .reveal.is-visible {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .reveal--left,
  .reveal--right {
    transform: none;
    transition: opacity 0.01ms;
  }
  .photo-ph,
  img.area-photo { transition: none; }
}

/* ---------- photo placeholder hover ----------
   Guarded by hover:hover — touch devices never get a stuck hover state.
   Dashed border resolves to solid + a slight brightness lift. */
@media (hover: hover) {
  /* (.photo-ph hover removed at merge — Agent D ships the identical hover
     plus caption legibility; one copy only.) */

  /* Real photos in cards: grayscale at rest, colour on hover, .4s ease.
     No <img> card photos exist in the built site yet (all photography is
     .photo-ph placeholders); img.area-photo is the agreed class for when
     real imagery lands. Entirely inside hover:hover, so touch devices are
     never served grayscale — they always see full colour. */
  img.area-photo {
    filter: grayscale(1);
    transition: filter .4s ease;
  }
  .svc:hover img.area-photo,
  .rev:hover img.area-photo,
  .article-card:hover img.area-photo,
  .split:hover img.area-photo,
  a:hover img.area-photo,
  img.area-photo:hover { filter: grayscale(0); }
}

/* BOUNDARY NOTE: .svc hover styling (lift, red border, deepened shadows) is
   owned by Agent D and is deliberately untouched by this patch. */

/* ==========================================================================
   v2 redesign — Agent C: typography scale-up, proof bar, buttons
   ========================================================================== */
/* ===========================================================================
   v2 patch — Agent C: typography scale-up · social proof bar · button consistency
   Merge AFTER the existing :root tokens in main.css (later declarations win).
   Brand: #ce070c → #750104 gradient · #393939 · #1a1a1a · #f5f3f0.
   No external requests, no new fonts, no animations added here (count-up is
   Agent B's; tabular-nums below keeps his digits from jittering).
   =========================================================================== */

/* ---------- 1. Type-scale token overrides ---------- */
:root {
  --text-4xl: clamp(3rem, 6vw, 5rem);       /* 48 -> 80px (was 40 -> 56px) */
  --text-3xl: clamp(2.2rem, 4.5vw, 3.2rem); /* 35.2 -> 51.2px (was 32 -> 44px) */
  --text-2xl: clamp(1.6rem, 3.2vw, 2.2rem); /* 25.6 -> 35.2px (was 24 -> 32px) */
}

/* ---------- 2. Heading + body rhythm ---------- */
body { line-height: 1.75; } /* was 1.65; headings keep their own 1.1/1.15 */

h1 { font-weight: 900; } /* was 800, everywhere */

/* v2's blanket heading tabular-nums was deleted in v3 Track G.1c (the
   mission forbids tabular numerals on all headings); digits-as-data
   surfaces get it via the apply-list in the Track G block below. Count-up
   stability is unaffected — its targets are the .proof-num /
   .rev-hero__num spans, which keep tabular-nums. */

.shead h2 {
  font-weight: 800;       /* was 700 */
  text-wrap: balance;     /* progressive enhancement; ignored where unsupported */
}

/* Interior-page guard: the new --text-3xl anchors (35.2px floor) push the
   longest service title ("Trenching, Boring & Underground Conduit", 39 chars)
   from 2 lines to 3 at 390px. Re-pin the compact hero to today's proven 32px
   floor with a modest 48px desktop cap so the drama stays on the homepage and
   section heads while all 13 interior titles keep their as-built wrap.
   Math in REDESIGN_REPORT_C.md §2. */
.hero--sub h1 { font-size: clamp(2rem, 4vw, 3rem); } /* 32 -> 48px */

/* ---------- 3. Social proof bar (markup: js/staging/proof-bar.html) ---------- */
.proof-bar {
  background: #1a1a1a;
  border-bottom: 1px solid rgba(var(--red-rgb), .3);
  padding: var(--space-3) 0;
}
.proof-bar__inner {
  max-width: var(--maxw); margin: 0 auto; padding: 0 20px;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: var(--space-2) var(--space-4);
  color: rgba(255, 255, 255, .8); /* 11.9:1 on #1a1a1a */
  font-size: var(--text-sm); line-height: 1.4; text-align: center;
}
/* Each proof item wraps as one unit, so 390px lays out 1–2 items per row
   instead of breaking mid-phrase. */
.proof-item { white-space: nowrap; }
/* Brand red #ce070c is 3.03:1 on #1a1a1a — fails 4.5:1 for small bold text.
   #ff8a8a is the site's established on-dark red (.hl on the hero, 5.09:1 on
   charcoal); on #1a1a1a it is 7.67:1 — AA and AAA. Decision documented in
   REDESIGN_REPORT_C.md §3. */
.proof-num {
  font-weight: 700; color: #ff8a8a;
  font-variant-numeric: tabular-nums; /* steady width under Agent B's count-up */
}
.proof-divider { opacity: .3; }

/* ---------- 4. Buttons ---------- */
.btn--lg {
  padding: 1rem 2.5rem;
  font-size: var(--text-lg);
  font-weight: 700;
  min-height: 56px;
}

/* Red CTA glow on dark surfaces only (.section--dark/--mid are Agent A's).
   Rest + hover pairs: this patch loads after main.css, so an unscoped
   .btn--red:hover (equal specificity, earlier source) would lose to the
   rest-state rule — the hover deepening must be restated in scope. */
.hero .btn--red,
.section--dark .btn--red,
.section--mid .btn--red {
  box-shadow: 0 4px 24px rgba(var(--red-rgb), .35);
}
.hero .btn--red:hover,
.section--dark .btn--red:hover,
.section--mid .btn--red:hover {
  box-shadow: 0 6px 28px rgba(var(--red-rgb), .45);
}

/* Ghost hover: align to solid white fill (was translucent rgba wash).
   translateY lift stays in main.css's hover-capability guard — not duplicated. */
.btn--ghost:hover { background: #fff; border-color: #fff; color: #1a1a1a; }

/* .btn--outline (light surfaces) intentionally unchanged. */

/* ==========================================================================
   v2 redesign — Agent D: reviews page, card polish, placeholders
   ========================================================================== */
/* ============================================================================
   v2 patch — Agent D: reviews page overhaul + service-card polish + photo
   placeholder deltas.

   Loads AFTER main.css; overrides win by cascade order at equal specificity —
   no !important anywhere. Brand palette only (#ce070c → #750104, #393939,
   #1a1a1a, #f5f3f0). Accessible red-on-dark tint is #ff8a8a (5.09:1 on
   charcoal, per main.css). No external requests, no new assets.

   Neutrality contract: nothing in this file selects on rating, reviewer or
   platform in a way that dims, reorders or flags a card. The one platform-
   specific rule (.rev__badge--*) already exists in main.css and is identical
   on every card; .rev__verified below applies to every star-only Thumbtack
   record the builder emits, positive or negative alike.
   ========================================================================= */

/* ---------- 1. Reviews hero (cinematic dark) ----------
   Scoped to the .hero--reviews modifier added in reviews_v2.html. Inherits
   .hero's white text, focus-outline recolour and 5px red bottom rule. */
.hero--reviews {
  background: linear-gradient(180deg, #1a1a1a 0%, #2b2b2b 55%, var(--charcoal) 100%);
}
/* Deeper, two-source red wash; same specificity as main.css .hero::before,
   later in cascade so it wins. */
.hero--reviews::before {
  background:
    radial-gradient(900px 440px at 18% -12%, rgba(var(--red-rgb), .34), transparent 60%),
    radial-gradient(720px 400px at 96% 112%, rgba(var(--red-rgb), .16), transparent 65%);
}
.hero--reviews .wrap { padding-bottom: var(--space-13); }

.rev-hero {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-7) var(--space-10);
}
.rev-hero__lead { flex: 1 1 420px; min-width: 0; }
.rev-hero__figure {
  flex: 0 0 auto; margin: 0;
  display: flex; flex-direction: column; align-items: center; text-align: center;
}
@media (min-width: 768px) {
  .rev-hero__figure {
    border-left: 1px solid rgba(255, 255, 255, .18);
    padding-left: var(--space-10);
  }
}
/* Big count in the accessible brand tint — a COUNT, never a rating. */
.rev-hero__num {
  font-size: clamp(4rem, 11vw, 6.75rem); font-weight: 800; line-height: 1;
  letter-spacing: -0.03em; color: #ff8a8a; font-variant-numeric: tabular-nums;
}
.rev-hero__unit {
  margin-top: var(--space-2);
  font-size: var(--text-xs); font-weight: 500; letter-spacing: 0.08em;
  text-transform: uppercase; color: #d6d6da;
}

/* Platform summary line, restyled for the dark hero. */
.hero--reviews .rev-summary {
  margin-top: var(--space-5);
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-3);
  color: #d6d6da;
}
.hero--reviews .rev-summary a {
  color: #fff; font-weight: 600;
  text-decoration-color: rgba(255, 138, 138, .55);
  text-decoration-thickness: 2px; text-underline-offset: 3px;
}
.hero--reviews .rev-summary a:hover { color: #ff8a8a; }

/* ---------- 2. Filter pill polish ----------
   Semantics (aria-pressed, hidden-until-JS, ≥44px) and the rev-in fade are
   already correct in main.css; this only deepens the active state. The rev-in
   animation is driven by [hidden] display toggling, untouched here. */
.rev-filter button {
  transition: background-color .15s ease, border-color .15s ease,
              color .15s ease, box-shadow .15s ease;
}
.rev-filter button[aria-pressed="true"] {
  background: linear-gradient(180deg, var(--red), var(--red-dark));
  border-color: var(--red-dark);
  box-shadow: 0 3px 10px rgba(var(--red-rgb), .28);
}

/* ---------- 3. Thumbtack "Verified" → teal pill ----------
   Markup comes from the frozen builder (reviews.py emits .rev__verified), so
   this is CSS-only. #0073a8 matches the existing Thumbtack platform pill,
   whose white label already passes 4.5:1 (documented in main.css). Applies
   identically to every star-only record regardless of sentiment. */
.rev__verified {
  align-self: flex-start;
  background: #0073a8; color: #fff;
  font-size: var(--text-xs); font-weight: 500; letter-spacing: 0.08em;
  line-height: 1.3; text-transform: uppercase;
  padding: var(--space-1) var(--space-2); border-radius: var(--radius-pill);
}
/* In the compact star-only row the pill sits inline with the flex row. */
.rev--compact .rev__verified { align-self: center; }

/* ---------- 4. "Leave us a review" CTA panel ---------- */
.rev-cta {
  max-width: 780px; margin: 0 auto var(--space-10);
  background: var(--wash); border: 1px solid var(--line);
  border-top: 4px solid var(--red);
  border-radius: var(--radius-lg);
  padding: var(--space-8) var(--space-6);
  text-align: center;
}
.rev-cta h2 {
  font-size: var(--text-xl); font-weight: 700;
  margin-bottom: var(--space-3);
}
.rev-cta > p { color: #3f3f45; max-width: 60ch; margin: 0 auto var(--space-4); }
.rev-cta__actions {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: var(--space-3); margin: var(--space-5) 0;
}
.rev-cta__actions .btn { width: 100%; }
@media (min-width: 640px) {
  .rev-cta__actions .btn { width: auto; }
}
.rev-cta__alt { font-size: var(--text-sm); color: var(--muted); margin-bottom: 0; }
.rev-cta a:not(.btn) { color: var(--red-dark); font-weight: 600; }

/* ---------- 5. Service card polish (.svc + its mirror .article-card) ----------
   Borderless shadow-resting cards. The 1px border is kept but made
   transparent (border-width unchanged → zero layout shift). */
.svc,
.article-card {
  border-color: transparent;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .08);
  transition: border-color .16s ease, box-shadow .2s ease,
              transform .2s ease, background-color .16s ease;
}
.svc:hover,
.article-card:hover {
  border-color: transparent;
  box-shadow: 0 8px 32px rgba(0, 0, 0, .16);
}
/* Red top accent on hover/keyboard focus. Technique: absolutely-positioned
   ::before overlay (3px gradient bar) faded in with opacity — occupies no
   flow space and changes no border width, so there is zero layout shift.
   ::before is free on both cards (their stretched link uses h3 a::after). */
.svc::before,
.article-card::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: linear-gradient(90deg, var(--red), var(--red-dark));
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  opacity: 0; transition: opacity .16s ease;
  pointer-events: none;
}
.svc:hover::before,
.svc:focus-within::before,
.article-card:hover::before,
.article-card:focus-within::before { opacity: 1; }

/* .svc__i stays 46px red (main.css) — within the 40–46px brief, no override.
   .svc__more is already red / weight 600 / bottom-pinned via the flex column's
   margin-top:auto (main.css) — verified, no override. */

/* Deeper lift, hover-capable pointers only. Same guard pattern as main.css;
   cascade order overrides its -3px rules. */
@media (hover: hover) {
  .svc:hover,
  .article-card:hover { transform: translateY(-6px); }
}

/* Dark-section variants (Agent A defines .section--dark / .section--mid;
   these rules are inert until those classes exist). */
.section--dark .svc,
.section--mid .svc,
.section--dark .article-card,
.section--mid .article-card {
  background: rgba(255, 255, 255, .06);
  border-color: rgba(255, 255, 255, .1);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .28);
  color: #fff;
}
.section--dark .svc:hover,
.section--mid .svc:hover,
.section--dark .article-card:hover,
.section--mid .article-card:hover {
  background: rgba(255, 255, 255, .09);
  border-color: rgba(255, 255, 255, .1);
  box-shadow: 0 8px 32px rgba(0, 0, 0, .45);
}
/* Icon + link in the accessible brand tint (#ff8a8a, 5.09:1 on charcoal);
   body copy matches .why's muted-on-dark (#c9c9d0). */
.section--dark .svc__i,
.section--mid .svc__i,
.section--dark .svc__more,
.section--mid .svc__more,
.section--dark .article-card__cta,
.section--mid .article-card__cta { color: #ff8a8a; }
.section--dark .svc p,
.section--mid .svc p,
.section--dark .article-card__meta,
.section--mid .article-card__meta { color: #c9c9d0; }
.section--dark .svc h3 a:hover, .section--dark .svc h3 a:focus-visible,
.section--mid .svc h3 a:hover, .section--mid .svc h3 a:focus-visible,
.section--dark .article-card h3 a:hover, .section--dark .article-card h3 a:focus-visible,
.section--mid .article-card h3 a:hover, .section--mid .article-card h3 a:focus-visible {
  color: #ff8a8a;
}

/* ---------- 6. Photo placeholder deltas ----------
   Base treatment (gradient, bolt, dashed border) stays as-built in main.css.
   Caption legibility: soft shadow lifts the italic alt text off the brighter
   red corner of the gradient. data-photo-placeholder untouched (checks.py). */
.photo-ph { transition: filter .2s ease, border-style .2s ease; }
.photo-ph__label,
.photo-ph__alt { text-shadow: 0 1px 3px rgba(0, 0, 0, .35); }
.photo-ph__alt { line-height: 1.5; }
/* Hover: dashed → solid + slight brightness. NOTE for merge: the brief also
   lists this hover under Agent B — if enhancements/B's patch ships the same
   rule, keep exactly one copy (this one is self-contained and trivial). */
@media (hover: hover) {
  .photo-ph:hover { border-style: solid; filter: brightness(1.06); }
}

/* ---------- reduced motion ----------
   main.css already has the blanket kill-switch; explicit coverage per the
   audit's Phase 3 grep. */
@media (prefers-reduced-motion: reduce) {
  .svc, .article-card, .photo-ph, .rev-filter button { transition: none; }
  .svc:hover, .article-card:hover { transform: none; }
  .svc::before, .article-card::before { transition: none; }
}

/* ==========================================================================
   v2 redesign — main session: homepage photo hero
   ========================================================================== */
/* ---------- v2 homepage hero (main session) ----------
   Full-bleed truck photo behind a charcoal overlay, scoped to .hero--home so
   interior .hero--sub heroes keep the flat charcoal panel. The photo comes
   from the image pipeline (image_tag emits WebP + srcset + real dimensions);
   #1a1a1a behind it stands in while the image loads, so text stays legible. */
.hero--home {
  display: flex; flex-direction: column;
  justify-content: center; align-items: center; text-align: center;
  min-height: 90vh;
  background: #1a1a1a;
}
/* The base .hero radial red wash would paint behind the photo — pointless
   work; the animated wash below replaces it. The 5px gradient rule (::after)
   stays: it paints above the photo and anchors the brand at the fold. */
.hero--home::before { display: none; }

.hero__bg { position: absolute; inset: 0; z-index: 0; }
.hero__bg img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: center 30%;
}
.hero__overlay { position: absolute; inset: 0; background: rgba(26, 26, 26, .72); }
/* .hero__wash moved to the v3 Track A layer below: the infinite wash-shift
   pulse was replaced by a one-time settle-to-static (A.5). */

/* Everything readable sits in one z-lifted column; the base .hero .wrap
   padding rules still apply on top of this. */
.hero--home .wrap { position: relative; z-index: 1; }

.hero__proof {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: var(--space-2) var(--space-3);
  color: rgba(255, 255, 255, .8);
  font-size: var(--text-sm); letter-spacing: .05em; line-height: 1.4;
  margin-bottom: var(--space-5);
}
.hero__proof-divider { opacity: .4; }

/* 86-character H1 (content-locked): 22ch measure wraps it to ~4 balanced
   lines at desktop, and the sub-640px clamp keeps it under 6 lines on
   phones. Weight/tracking per the v2 type direction. */
.hero--home h1 {
  font-size: clamp(2.4rem, 5.5vw, 4.25rem);
  font-weight: 900; line-height: 1.05; letter-spacing: -0.03em;
  color: #fff; max-width: 22ch; margin: 0 auto var(--space-5);
  text-wrap: balance;
}
/* #ff8a8a is the site's established accessible red tint on dark (5.09:1);
   brand #ce070c is 3.0:1 over the overlay — borderline even for large text. */
.hero--home h1 .hero__hl { color: #ff8a8a; display: block; }

/* The one-line promise becomes the sub-headline; the longer service
   sentence drops to supporting-meta weight below it. Same copy, new order. */
.hero--home .hero__sub {
  font-size: var(--text-xl); font-weight: 600; color: #fff;
  max-width: 40ch; margin: 0 auto var(--space-4);
}
.hero__meta {
  font-size: var(--text-base); color: rgba(255, 255, 255, .72);
  line-height: 1.6; max-width: 60ch; margin: 0 auto var(--space-8);
}
.hero--home .hero__cta { justify-content: center; }
.hero__emerg {
  margin-top: var(--space-6);
  color: rgba(255, 255, 255, .65);
  font-size: var(--text-sm); letter-spacing: .04em;
}

@media (max-width: 639px) {
  /* svh, not vh: the URL-bar collapse must not cut the CTAs off-screen.
     (min-height + margins superseded below in the v3 Track A layer, which
     loads later at equal specificity.) */
  .hero--home { min-height: 100svh; }
  .hero--home h1 { font-size: clamp(2rem, 8vw, 3rem); }
}

/* ==========================================================================
   v3 Track A — hero + above-the-fold override layer (merged Phase 2)
   ==========================================================================
   Loads AFTER the v2 homepage-hero block above. Every rule either REPLACES
   a named v2 rule at equal-or-higher specificity or ADDS a new one.

   Deliberately ABSENT (binding, per VISUAL_AUDIT_V3.md):
   - .hero__overlay — verified correct by pixel sampling. Untouched.
   - .hero--home / .hero--home::before / .hero__bg / .hero__proof(base) /
     .hero__sub(base) / .hero__cta / .hero__emerg base rules — unchanged. */

/* ---------- A.5 hero wash: one-time settle, then static ----------
   REPLACES: the v2 .hero__wash animation (wash-shift 8s infinite alternate)
   and its explicit reduced-motion rule — both deleted above.
   The infinite opacity pulse (.6↔1 every 8s) was a background animation
   competing with the headline. Final opacity .8 matches the midpoint the old
   loop oscillated around (and the old reduced-motion value), so the settled
   frame is exactly the look the audit screenshots verified. `backwards` fill
   shows the from-state during any start delay; runs once on load, no JS. */
.hero__wash {
  position: absolute; right: 0; top: 0; width: 60%; height: 100%;
  background: linear-gradient(to left, rgba(var(--red-rgb), .18), transparent);
  opacity: .8;
  animation: hero-wash-settle var(--motion-slow, 800ms)
             var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1)) 1 backwards;
}
@keyframes hero-wash-settle {
  from { opacity: .45; }
  to   { opacity: .8; }
}
/* The global reduced-motion kill-switch already stops this; kept explicit so
   the intent survives if that blanket rule ever narrows. Static opacity .8
   is the rule's own base value — no separate override needed. */
@media (prefers-reduced-motion: reduce) {
  .hero__wash { animation: none; }
}

/* ---------- A.2 H1 rebalance: white opener leads, tint line supports ------
   REPLACES: .hero--home h1 .hero__hl above.
   Audit: at 1440/1920 the two red-tint lines were optically heavier than the
   white opener. The locale line drops to a subordinate size (em-based, so it
   tracks every breakpoint) — same text, same single H1, no hidden words.
   Result (verified in track_a/shots/): desktop 2 white + 1 tint = 3 lines,
   mobile 3 white + 2 tint = 5 lines, no one-word orphans.
   #ff8a8a stays: the established accessible on-dark red (5.09:1). */
.hero--home h1 .hero__hl {
  display: block;
  color: #ff8a8a;
  font-size: .58em;
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.01em;
  margin-top: .5em;
  text-wrap: balance;
}
/* ADDS: slightly wider measure so the subordinate tint line can hold one
   line at desktop; the white opener still balance-wraps to two lines.
   (REPLACES only the max-width member of .hero--home h1 above.) */
.hero--home h1 { max-width: 24ch; }

/* ---------- A.3 hero photograph: breakpoint focal points ----------
   ADDS: media overrides for .hero__bg img (base rule keeps
   `object-position: center 30%` for 640–1599px, where the audit found the
   crop already correct — 768 keeps its exact current framing).
   Source photo (plec-truck.jpg 1536×1024): door branding is centered near
   x≈60%; front bumper reaches the left edge; truck body sits in the lower
   ~55–85% band.
   - ≤639px: the hero is height-limited (cover crops horizontally), and at
     x=center the visible window is ≈34–66% of the image — the door branding
     (≈52–70%) loses its tail. x=58% shifts the window to ≈42–74%: branding
     fully readable behind/beside the copy (verified in shots/).
   - ≥1600px: cover is width-limited (no horizontal crop — the front cannot
     be un-clipped by x; it extends to the image edge). y=38% keeps more of
     the bumper/wheel base in frame instead of tree canopy. */
@media (max-width: 639px) {
  .hero__bg img { object-position: 58% 50%; }
}
@media (min-width: 1600px) {
  .hero__bg img { object-position: 50% 38%; }
}

/* ---------- A.1 mobile fold: compressed stack, intro after the CTAs ------
   REPLACES: the sub-640 block above and, below 640px only, the margins of
   .hero__proof/.hero__meta/.hero__sub.
   Strategy (audit: CTA below the 390×844 fold):
   1. min-height: the old 100svh guaranteed the hero's bottom sat below the
      fold because ~155px of chrome (utility bar + header) is above it.
      calc(100svh - 156px) sizes the hero to the space that actually remains;
      the 30rem floor protects short landscape phones. (vh line = fallback
      for engines without svh.)
   2. The service sentence (meta.intro) moves BELOW the CTA group — visual
      order only, via flex `order`; DOM/reading/SEO order is unchanged and
      the only focusables (the two CTAs) keep a matching tab order. It is
      also de-emphasised (text-sm, .66 white) but stays fully rendered —
      no clamp, no hiding.
   3. Tighter type/margins above the CTAs: proof→text-xs, promise lh 1.4.
   Fold math at 390×844 (production chrome ≈155px, budget 689px):
   pad-top 32 + proof (2×17≈34 +12) + H1 (5×32.8≈164 +16) + promise
   (2×28≈56 +20) = 334 → CTA group (2×56 +12 gap = 124) bottom ≈ 458px,
   emergency line ≈ +16+22 → 496px. Everything clears the fold with ~190px
   of slack even before the intro (≈+20+4×22) at ≈608px. Verified in shots/.
   Specificity: every selector here ties or beats its main.css counterpart
   and loads later, so the override wins without !important. */
@media (max-width: 639px) {
  .hero--home {
    min-height: max(30rem, calc(100vh - 156px));
    min-height: max(30rem, calc(100svh - 156px));
  }
  .hero--home .wrap {
    display: flex; flex-direction: column;
    padding: var(--space-8) 20px var(--space-8);
  }
  .hero--home .hero__proof {
    order: 1;
    font-size: var(--text-xs);
    margin-bottom: var(--space-3);
  }
  .hero--home .wrap h1 {
    order: 2;
    font-size: clamp(2rem, 8vw, 3rem);
    margin-bottom: var(--space-4);
  }
  .hero--home .hero__sub {
    order: 3;
    line-height: 1.4;
    margin-bottom: var(--space-5);
  }
  .hero--home .hero__cta { order: 4; }
  .hero--home .hero__emerg {
    order: 5;
    margin-top: var(--space-4);
  }
  .hero--home .hero__meta {
    order: 6;
    margin: var(--space-5) auto 0;
    max-width: 46ch;
    font-size: var(--text-sm);
    line-height: 1.55;
    color: rgba(255, 255, 255, .66);
  }
}

/* ---------- A.4 entrance sequence ----------
   ADDS. Same contract as the v2 scroll reveals: JS opts in, CSS never hides
   anything on its own. hero_entrance (enhancements.js) adds .hero-entrance
   (pre-animation state) then, on the next paint, .hero-entrance--run
   (transition to final). No JS / reduced motion → neither class is ever
   added → the hero renders fully visible, exactly as today.
   Order + delays (reading order): proof 0ms → H1 90ms → promise 200ms →
   intro 260ms → CTAs 340ms → availability 460ms; each step is one
   --motion-reveal (520ms) → the sequence completes at 980ms (< 1.2s).
   The photo, overlay and wash are NOT in these selectors — they are already
   visible and never animate (the wash settle above is independent). */
.hero--home.hero-entrance .hero__proof,
.hero--home.hero-entrance h1,
.hero--home.hero-entrance .hero__sub,
.hero--home.hero-entrance .hero__meta,
.hero--home.hero-entrance .hero__cta,
.hero--home.hero-entrance .hero__emerg {
  opacity: 0;
  transform: translateY(16px);
}
.hero--home.hero-entrance--run .hero__proof,
.hero--home.hero-entrance--run h1,
.hero--home.hero-entrance--run .hero__sub,
.hero--home.hero-entrance--run .hero__meta,
.hero--home.hero-entrance--run .hero__cta,
.hero--home.hero-entrance--run .hero__emerg {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--motion-reveal, 520ms) var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1)),
    transform var(--motion-reveal, 520ms) var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1));
}
.hero--home.hero-entrance--run .hero__proof { transition-delay: 0ms; }
.hero--home.hero-entrance--run h1           { transition-delay: 90ms; }
.hero--home.hero-entrance--run .hero__sub   { transition-delay: 200ms; }
.hero--home.hero-entrance--run .hero__meta  { transition-delay: 260ms; }
.hero--home.hero-entrance--run .hero__cta   { transition-delay: 340ms; }
.hero--home.hero-entrance--run .hero__emerg { transition-delay: 460ms; }
/* Belt-and-braces: the JS already refuses to add the classes under reduced
   motion; this guarantees final state even if the classes ever appear. */
@media (prefers-reduced-motion: reduce) {
  .hero--home.hero-entrance .hero__proof,
  .hero--home.hero-entrance h1,
  .hero--home.hero-entrance .hero__sub,
  .hero--home.hero-entrance .hero__meta,
  .hero--home.hero-entrance .hero__cta,
  .hero--home.hero-entrance .hero__emerg {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ============================================================================
   v2 merge resolutions (main session, Phase 2)
   ========================================================================= */
/* FAQ chevron on dark cards: brand red measures 2.6:1 there — below the 3:1
   graphics target. Re-tinted to the established on-dark red; state remains
   redundantly conveyed by the native <details> + summary text. Resolves
   REDESIGN_REPORT_A.md open question 1. */
.section--dark .acc__c,
.section--mid .acc__c { color: var(--on-dark-red); }

/* ==========================================================================
   v3 Track B — signature "current line" motif (merged Phase 2)
   A thin red line that draws once, left to right, when its host section
   reveals — electrical current, routed with precision.

   Contract with the existing reveal system (enhancements.js):
   - Templates never carry .reveal. JS adds it at runtime, flips .is-visible
     on first viewport entry, then unobserves — so the line draws exactly
     once and can never loop.
   - No JS / no IntersectionObserver / prefers-reduced-motion: .reveal is
     never added, so every .cline renders static at full length. Nothing is
     ever hidden and nothing moves.

   Homepage budget: 4 instances of a 5 max (see track_b/placements.md); the
   spare slot was reserved for Track D's featured-project fact connector.
   ========================================================================== */

/* ---------- base ---------- */
/* The line itself. Decorative only — markup must carry aria-hidden="true".
   currentColor drives the gradient start so surface variants recolour with
   a single `color` override. 2px tall (brief allows 1–3px). */
.cline {
  display: block;
  height: 2px;
  border-radius: 1px;
  color: var(--red);                    /* gradient start (#ce070c) */
  --cline-end: var(--red-dark);         /* gradient end   (#750104) */
  background: linear-gradient(90deg, currentColor, var(--cline-end));
  transform: scaleX(1);                 /* static full length by default */
  transform-origin: 0 50%;              /* current flows left -> right */
}

/* ---------- draw-on-entry, keyed to the existing reveal state ---------- */
/* Only when JS has opted the host in (.reveal) is the line collapsed; it
   draws when the observer flips .is-visible. The 200ms delay lets the
   host's own fade/rise start first (sheads carry reveal--delay-1 = 100ms),
   so the draw reads as a follow-through, not a second competing animation:
   host fade 100–600ms, line draw 200–720ms. */
.reveal .cline {
  transform: scaleX(0);
  transition: transform
    var(--motion-reveal, 520ms)
    var(--ease-emphasis, cubic-bezier(0.16, 1, 0.3, 1))
    200ms;
}
.reveal.is-visible .cline { transform: scaleX(1); }

/* ---------- variants ---------- */
/* Section-head underline: short centred rule tucked under .shead h2.
   Negative top margin pulls it inside the h2's own space-4 bottom margin
   (net 8px h2->line, 16px line->intro). */
.cline--u {
  width: 56px;
  margin: calc(-1 * var(--space-2)) auto var(--space-4);
}
/* Sheads with no trailing intro <p> (e.g. FAQ): don't pad the section gap. */
.cline--u:last-child { margin-bottom: 0; }

/* CTA-band accent: sits above the band headline. */
.cline--band {
  width: 44px;
  margin: 0 auto var(--space-5);
}

/* Vertical variant — the featured-project rail (Track D hand-off, homepage
   instance 5 of the 5 max). Geometry (absolute inset) comes from the host
   class .fproj__line; this only turns the gradient vertical and lets the
   inset stretch it. Static by design: the rail is not inside a reveal
   target, so it never collapses and never draws. */
.cline--v {
  width: 2px;
  height: auto;
  background: linear-gradient(180deg, currentColor, var(--cline-end));
}

/* ---------- surface recolours ---------- */
/* Dark sections: brand red is ~2.0:1 on charcoal/near-black. Use the
   graphic tint the codebase already uses for icons/links on dark
   (#ff8a8a — 5.09:1 on #393939, higher on #1a1a1a) fading into full brand
   red. Decorative (aria-hidden), so this is a visibility choice, not a
   WCAG requirement; ratio recorded per the mission's dark-red rule. */
.section--dark .cline,
.section--mid .cline {
  color: #ff8a8a;
  --cline-end: var(--red);
}

/* On the red CTA band a red line would vanish. The motif's form — thin
   line, single draw — carries the signature; colour inverts to white
   current fading out, like the band's own light-on-red text. */
.band .cline {
  color: #fff;
  --cline-end: rgba(255, 255, 255, .45);
}

/* ---------- reduced motion (explicit third layer, house pattern) ---------- */
/* enhancements.js never adds .reveal under reduced motion and main.css has
   the blanket kill-switch; this is the explicit per-feature coverage the
   Phase 3 audit greps for. Line appears static at full length. */
@media (prefers-reduced-motion: reduce) {
  .cline,
  .reveal .cline,
  .reveal.is-visible .cline {
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   v3 Track D — section flow, proof consolidation, featured project,
   transition vocabulary (merged Phase 2).

   TRANSITION VOCABULARY — exactly 3 types on the homepage (D.2):
     1. KEEP  — 1px red seam between adjacent dark sections
                (.section--dark/--mid adjacency rule already above).
     2. NEW   — background continuation: Why PLEC (#393939) flows into the
                featured-project section via a charcoal→near-black gradient;
                the seam is suppressed at that one boundary because the
                continuation replaces it (one treatment per boundary).
     3. NEW   — large low-opacity section-number figures (.snum) on
                alternating content sections. Fully static: no scroll
                linkage, no parallax, nothing moves on any viewport.
   No other transition device is introduced by this patch.
   ========================================================================== */

/* ---------- 1. Consolidated proof band (replaces proof bar + trust strip)
   One near-black proof moment under the hero. Outer class stays .proof-bar
   so enhancements.js keeps its reveal hook and [data-count] count-up with
   zero JS changes; --merged restyles the inside. Claims carried here, each
   exactly once: 29 Google / 41 Thumbtack / 25+ years (stats), license,
   Licensed-Bonded-Insured, Residential & Commercial, Locally Owned
   (credentials). Emergency wording deliberately NOT here — the utility bar
   and the hero already render it (audit: 4×→2×). */
.proof-bar--merged {
  border-bottom: 1px solid rgba(var(--red-rgb), .3);
  padding: var(--space-6) 0;
}
/* Neutralise the old single-row inner flex; the merged band manages its own
   layout. (Old .proof-bar__inner rules stay for any other page using them.) */
.proof-bar--merged .proof-merged {
  max-width: var(--maxw); margin: 0 auto; padding: 0 20px;
  display: grid; gap: var(--space-5);
  justify-items: center;
}
@media (min-width: 900px) {
  .proof-bar--merged .proof-merged {
    grid-template-columns: auto 1px auto;
    align-items: center; justify-content: center;
    column-gap: var(--space-8);
  }
  /* Vertical hairline between stats and credentials on wide screens. */
  .proof-merged::before {
    content: ""; grid-column: 2; align-self: stretch;
    background: var(--on-dark-line);
  }
  .proof-stats { grid-column: 1; grid-row: 1; }
  .proof-creds { grid-column: 3; grid-row: 1; }
}

/* Stat cells: big tabular numerals, small uppercase labels. */
.proof-stats {
  list-style: none; margin: 0; padding: 0;
  display: flex; gap: var(--space-6) var(--space-7);
  justify-content: center; text-align: center;
}
.proof-stat { display: flex; flex-direction: column; gap: 2px; }
.proof-stat__n {
  font-size: var(--text-2xl); font-weight: 800; line-height: 1.1;
  color: var(--on-dark-red);            /* #ff8a8a — 7.67:1 on #1a1a1a */
  font-variant-numeric: tabular-nums;   /* steady width under count-up */
}
.proof-stat__l {
  font-size: var(--text-xs); font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase; line-height: 1.3;
  color: rgba(255, 255, 255, .8);       /* 11.9:1 on #1a1a1a */
}

/* Credential rows: check icon + short claim, 2-col grid. */
.proof-creds {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, auto); gap: 10px var(--space-6);
  font-size: var(--text-sm); font-weight: 600;
  color: var(--on-dark-body);           /* 10.2:1 on #1a1a1a */
}
.proof-cred { display: flex; align-items: center; gap: var(--space-2); }
.proof-cred svg {
  width: 16px; height: 16px; flex: 0 0 auto;
  color: var(--on-dark-red);            /* brand red is 3.03:1 here; tint instead */
}
@media (max-width: 479px) {
  /* Keep the band short on small phones (audit: 390px is copy-heavy). */
  .proof-stat__n { font-size: var(--text-xl); }
  .proof-stats { gap: var(--space-4) var(--space-5); }
  .proof-creds { gap: 8px var(--space-4); }
}

/* ---------- 2. Section-number figures (transition type 3) ----------
   Static aria-hidden ordinals top-right of alternating content sections:
   services 01 · res/com split 02 · featured project 03 · reviews 04.
   Why PLEC is skipped on purpose (its cards already carry 01–03 in .why__n)
   and areas/FAQ are skipped to keep the device scarce. currentColor makes
   one rule serve light and dark surfaces. Desktop/tablet only: at phone
   widths the figure would crowd the centred section headers. */
.has-snum > .wrap { position: relative; }
.snum {
  display: none;
  position: absolute; top: -0.32em; right: 0; z-index: 0;
  font-size: clamp(4.5rem, 9vw, 7.5rem); font-weight: 900; line-height: 1;
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
  color: currentColor; opacity: .055;
  pointer-events: none; user-select: none;
}
@media (min-width: 768px) {
  .snum { display: block; }
}
/* Slightly stronger on dark surfaces so it survives the darker ground. */
.section--dark .snum,
.section--mid .snum { opacity: .07; }

/* ---------- 3. Featured project — cinematic dark section (D.3) ----------
   Replaces the thin "projects teaser" strip. No verified project photos or
   facts exist (config: "No genuine project photography in archive"), so this
   is the honest empty state designed as a feature: branded .photo-ph visual,
   the existing coming-soon copy, and the write-up format (problem → solution
   → result) taken verbatim in substance from content/projects.md. No
   invented jobs, cities, dollar figures or outcomes. */

/* Transition type 2 — background continuation from Why PLEC (#393939): the
   section opens on the same charcoal and settles to near-black, so the two
   dark sections read as one deepening block. */
.fproj {
  background: linear-gradient(180deg, var(--charcoal) 0, var(--surface-dark) 340px);
}
/* The continuation replaces the red seam at this one boundary (rule of one
   treatment per boundary). Same 0,2,0 specificity as the adjacency rule
   above; this block loads later, so source order decides. */
.section--mid + .fproj { border-top: 0; }

.fproj__grid { display: grid; gap: var(--space-7); align-items: center; }
@media (min-width: 900px) {
  .fproj__grid {
    grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
    gap: var(--space-9, 4rem);
  }
}

/* Left-align the section header inside this split layout only. */
.fproj .shead {
  text-align: left; max-width: none; margin: 0 0 var(--space-5);
}
.fproj .shead p { color: var(--on-dark-body); }

/* Large branded visual: the existing .photo-ph placeholder promoted to a
   cinematic panel. Solid hairline instead of the dashed "missing" border —
   deliberate holding art, not a broken image. */
.fproj__visual { position: relative; }
.photo-ph--cine {
  min-height: clamp(300px, 38vw, 460px);
  border: 1px solid rgba(255, 255, 255, .18);
  border-radius: var(--radius-lg);
  background: linear-gradient(150deg, var(--red) 0%, var(--red-dark) 42%, var(--charcoal) 100%);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .45);
}
.photo-ph--cine::before { width: 96px; height: 144px; opacity: .16; }

/* Area/service label chip pinned to the visual (honest: service area, not an
   invented project location). */
.fproj__tag {
  position: absolute; top: var(--space-4); left: var(--space-4);
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: .35em .9em; border-radius: var(--radius-pill);
  background: rgba(26, 26, 26, .78);
  border: 1px solid rgba(255, 255, 255, .22);
  color: #fff; font-size: var(--text-xs); font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; line-height: 1.3;
}

/* Problem → solution → result: the shape every case study will publish.
   The rail on the left is Track B's .cline--v — geometry only here. */
.fproj__lede { color: var(--on-dark-body); font-size: var(--text-sm); margin-bottom: var(--space-4); }
.fproj__steps-wrap { position: relative; margin-bottom: var(--space-6); }
.fproj__steps {
  list-style: none; counter-reset: fstep;
  margin: 0; padding: 0 0 0 var(--space-5);
  display: grid; gap: var(--space-4);
}
.fproj__line {
  /* Geometry only — visual identity belongs to Track B's .cline--v. */
  position: absolute; left: 0; top: 6px; bottom: 6px; width: 2px;
}
.fproj__step { position: relative; }
.fproj__step-k {
  display: block; margin-bottom: 2px;
  font-size: var(--text-xs); font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase; line-height: 1.3;
  color: var(--on-dark-kicker);         /* #ffb3b3 — 10.2:1 on #1a1a1a */
}
.fproj__step-k::before {
  counter-increment: fstep;
  content: counter(fstep, decimal-leading-zero) " · ";
  color: var(--on-dark-red);
  font-variant-numeric: tabular-nums;
}
.fproj__step p { margin: 0; font-size: var(--text-sm); color: var(--on-dark-body); }

/* Crawlable link to /projects/ restyled as the section's action. */
.fproj__more a {
  color: var(--on-dark-red);
  border-bottom: 2px solid rgba(255, 138, 138, .35);
  font-weight: 600; text-decoration: none;
}
.fproj__more a:hover,
.fproj__more a:focus-visible { border-bottom-color: var(--on-dark-red); }

/* ---------- 4. Featured-project motion ----------
   Image-mask reveal, once, via the existing JS-only reveal system:
   enhancements.js adds .reveal at runtime and .is-visible on first
   intersection (then unobserves — one-time by construction). Without JS or
   under reduced motion .reveal is never added, so the section renders in its
   final state — nothing below hides content. The visual overrides the stock
   translateY reveal with a clip-path wipe + ≤2% settle-down scale; the three
   facts keep the stock reveal and inherit reading-order stagger from the
   sibling counter (.fproj__visual and .fproj__step are in the reveal target
   list in enhancements.js). */
.fproj__visual.reveal {
  opacity: 1;                            /* mask does the work; never dim it */
  transform: scale(1.02);                /* ≤2% per D.3 */
  clip-path: inset(0 14% 0 0 round var(--radius-lg));
  transition:
    clip-path var(--motion-slow, 800ms) var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1)),
    transform var(--motion-slow, 800ms) var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1));
}
.fproj__visual.reveal.is-visible {
  clip-path: inset(0 0 0 0 round var(--radius-lg));
  transform: none;
}

/* Explicit reduced-motion cover for every rule this patch animates (the JS
   already never opts elements in under reduced motion; this is the CSS
   belt-and-braces layer Phase 3 greps for). .snum and the proof band are
   static by design and need no entry here. */
@media (prefers-reduced-motion: reduce) {
  .fproj__visual.reveal,
  .fproj__visual.reveal.is-visible {
    clip-path: none; transform: none; opacity: 1; transition: none;
  }
}

/* ==========================================================================
   v3 Track E — cards, buttons and microinteractions (merged Phase 2)
   Every override below ties on specificity with an earlier rule and wins on
   source order only — no !important.
   Conventions carried over from the rest of this file:
   - transforms triggered by HOVER live inside the @media (hover: hover)
     guard near the bottom of this block (no stuck lift on touch);
   - :focus-within / :focus-visible parity rules are unguarded — keyboard
     focus is deliberate, there is no stuck-state risk;
   - explicit prefers-reduced-motion block at the end (in addition to the
     blanket kill-switch).
   ========================================================================== */

/* --------------------------------------------------------------------------
   E.1a — service-card index mark (pure CSS counters — NO template edit)
   Any .grid containing .svc cards numbers them 01…n in source order
   (home/area: 01–08, services hub: 01–13). Rendered in the card's own
   ::after — free on .svc: the v2 top accent is ::before and the stretched
   link uses `h3 a::after`. Sits top-right, opposite the 46px icon, so it
   never fights the icon (E.1 rule).
   The mark is decorative. The second `content` declaration uses the
   alt-text syntax (`/ ""`) so supporting browsers hide the numeral from
   assistive technology; older browsers ignore that declaration and fall
   back to the plain one (numeral announced — accepted, it is two digits).
   ------------------------------------------------------------------------ */
.grid { counter-reset: svc-index; }
.svc { counter-increment: svc-index; }
.svc::after {
  content: counter(svc-index, decimal-leading-zero);
  content: counter(svc-index, decimal-leading-zero) / "";
  position: absolute;
  top: var(--space-5);
  right: var(--space-5);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
  color: rgba(27, 27, 29, 0.3);
  pointer-events: none;
  transition: color var(--motion-fast, 160ms) var(--ease-standard, ease);
}
.svc:hover::after,
.svc:focus-within::after { color: rgba(var(--red-rgb), 0.55); }
/* Dark-section card variants (v2): quiet at rest, on-dark tint when active. */
.section--dark .svc::after,
.section--mid .svc::after { color: rgba(255, 255, 255, 0.28); }
.section--dark .svc:hover::after,
.section--mid .svc:hover::after,
.section--dark .svc:focus-within::after,
.section--mid .svc:focus-within::after { color: rgba(255, 138, 138, 0.6); }

/* --------------------------------------------------------------------------
   E.1b — icon nudge (≤2px). Trigger for hover sits in the hover guard;
   focus parity is here, unguarded.
   ------------------------------------------------------------------------ */
.svc__i { transition: transform var(--motion-fast, 160ms) var(--ease-out, ease-out); }
.svc:focus-within .svc__i { transform: translateY(-2px); }

/* --------------------------------------------------------------------------
   E.1c / E.3 — link-arrow advance (~4px, zero layout shift)
   The v2 .svc__more arrow was `content: " →"` text. Re-declared as an
   inline-block glyph with an em margin standing in for the literal space,
   so the glyph can move on its own transform without reflow.
   .article-card__cta carries its arrow inside the template's text node
   ("Read the guide →"), so the glyph itself cannot be targeted without a
   template edit; the whole link advances instead — transform only, no
   layout shift.
   ------------------------------------------------------------------------ */
.svc__more::after {
  content: "→";
  display: inline-block;
  margin-left: 0.4em;
  transition: transform var(--motion-fast, 160ms) var(--ease-out, ease-out);
}
.svc:focus-within .svc__more::after { transform: translateX(4px); }

.article-card__cta { transition: transform var(--motion-fast, 160ms) var(--ease-out, ease-out); }
.article-card:focus-within .article-card__cta { transform: translateX(4px); }

/* --------------------------------------------------------------------------
   E.1d — :focus-within parity for every card hover effect
   v2 already gives the red top accent :focus-within; these complete the
   set: shadow deepening, lift, link underline, and the dark-section face
   change. Lift transforms for keyboard focus are intentionally unguarded
   (see conventions above).
   E.1e: no rule here changes card height on hover/focus — transforms only,
   border-width constant (1px, transparent at rest), and the accent is an
   absolutely-positioned overlay. Verified.
   ------------------------------------------------------------------------ */
.svc:focus-within,
.article-card:focus-within {
  border-color: transparent;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16);
  transform: translateY(-6px);
}
.svc:focus-within .svc__more { text-decoration: underline; }
.article-card:focus-within .article-card__cta { text-decoration: underline; }

.section--dark .svc:focus-within,
.section--mid .svc:focus-within,
.section--dark .article-card:focus-within,
.section--mid .article-card:focus-within {
  background: rgba(255, 255, 255, 0.09);
  border-color: rgba(255, 255, 255, 0.1);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
}

/* --------------------------------------------------------------------------
   E.2 — buttons: active compression + focus-ring context audit
   :active is unguarded on purpose — press feedback is the primary touch
   confirmation. It beats the guard's hover lift (equal specificity, later
   source) while the pointer is down. transform-only: width, padding and
   border never change between rest/hover/active/disabled states, so button
   width is stable (loading states included).
   Contrast notes (TRACK_E_REPORT.md):
   - .btn--ghost hover fill: #1a1a1a text on #fff = 17.4:1 (AA/AAA) — pass.
   - .btn--outline IS the on-light treatment (#750104 text on #fff = 10.9:1,
     brand-red border 4.0:1 on white) — no separate .on-light class needed.
   ------------------------------------------------------------------------ */
.btn:active { transform: translateY(1px) scale(0.98); }
.btn[aria-disabled="true"]:active { transform: none; }

/* Focus-ring audit: the white-outline-on-dark recolour covers .hero, .band,
   .ft, .util, .why, .section--dark, .section--mid. The v2 proof bar
   (#1a1a1a) was the one dark surface missing it. (Track C owns the sticky
   mobile bar's ring.) */
.proof-bar :focus-visible { outline-color: #fff; }

/* --------------------------------------------------------------------------
   E.3 — underline draw for content links
   Scope: .prose body links only (plus the .faq-more treatment below).
   Nav links belong to Track C; applying the draw to every link is noise.
   Technique: two stacked background layers — a persistent low-tint
   underline keeps the non-colour link cue at rest (a11y: links must not be
   colour-only), and a full-strength currentColor layer draws left→right on
   hover/focus in --motion-fast. background-size only: zero layout shift.
   box-decoration-break: clone keeps the line on each fragment of a wrapped
   link.
   ------------------------------------------------------------------------ */
.prose a:not(.btn) {
  text-decoration: none;
  background-image:
    linear-gradient(currentColor, currentColor),
    linear-gradient(rgba(var(--red-rgb), 0.35), rgba(var(--red-rgb), 0.35));
  background-repeat: no-repeat;
  background-position: 0 100%, 0 100%;
  background-size: 0 2px, 100% 1px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-size var(--motion-fast, 160ms) var(--ease-out, ease-out);
}
.prose a:not(.btn):hover,
.prose a:not(.btn):focus-visible {
  background-size: 100% 2px, 100% 1px;
}

/* .faq-more a keeps its existing border-bottom underline: its ≤940px touch
   padding (12px) would push a background-drawn line 12px below the text, so
   the draw technique is wrong there. Instead the resting 30%-tint rule
   resolves to solid on hover/focus — same "underline completes" idea,
   correct geometry at every viewport. */
.faq-more a {
  transition: color var(--motion-fast, 160ms) var(--ease-standard, ease),
              border-bottom-color var(--motion-fast, 160ms) var(--ease-standard, ease);
}
.faq-more a:hover,
.faq-more a:focus-visible { border-bottom-color: var(--red); }
.section--dark .faq-more a:hover,
.section--mid .faq-more a:hover,
.section--dark .faq-more a:focus-visible,
.section--mid .faq-more a:focus-visible { border-bottom-color: #ff8a8a; }

/* --------------------------------------------------------------------------
   E.4 — photo grayscale becomes explicit OPT-IN
   v2 shipped `img.area-photo` grayscale-at-rest for ANY future real photo
   dropped into a card. v3 policy (mission E.4): grayscale only on
   explicitly-marked SECONDARY thumbnails — never by default, never on the
   hero truck, primary featured project, mobile-critical imagery, platform
   marks or proof images.
   The v2 rule is neutralised here (unguarded reset, later source wins at
   equal 0,1,1 specificity), then re-established as opt-in via `.img-dim` or
   `[data-photo-secondary]` inside the hover guard below — touch devices
   always see full colour.
   ------------------------------------------------------------------------ */
img.area-photo { filter: none; }

/* --------------------------------------------------------------------------
   Hover-capability guard — all hover-triggered transforms + opt-in
   grayscale. Same pattern as the earlier guard; loads after it, so these
   win where they overlap.
   ------------------------------------------------------------------------ */
@media (hover: hover) {
  /* E.1b icon nudge (2px, within the ≤2px budget) */
  .svc:hover .svc__i { transform: translateY(-2px); }

  /* E.1c arrow advance (~4px) */
  .svc:hover .svc__more::after { transform: translateX(4px); }
  .article-card:hover .article-card__cta { transform: translateX(4px); }

  /* E.4 opt-in grayscale for secondary thumbnails only.
     Restored to colour by the owning card/link on hover or keyboard focus,
     or by hovering the image itself. */
  img.img-dim,
  img[data-photo-secondary] {
    filter: grayscale(1);
    transition: filter var(--motion-standard, 320ms) var(--ease-standard, ease);
  }
  .svc:hover img.img-dim,
  .rev:hover img.img-dim,
  .article-card:hover img.img-dim,
  .split:hover img.img-dim,
  a:hover img.img-dim,
  img.img-dim:hover,
  .svc:focus-within img.img-dim,
  .rev:focus-within img.img-dim,
  .article-card:focus-within img.img-dim,
  .split:focus-within img.img-dim,
  a:focus-visible img.img-dim,
  .svc:hover img[data-photo-secondary],
  .rev:hover img[data-photo-secondary],
  .article-card:hover img[data-photo-secondary],
  .split:hover img[data-photo-secondary],
  a:hover img[data-photo-secondary],
  img[data-photo-secondary]:hover,
  .svc:focus-within img[data-photo-secondary],
  .rev:focus-within img[data-photo-secondary],
  .article-card:focus-within img[data-photo-secondary],
  .split:focus-within img[data-photo-secondary],
  a:focus-visible img[data-photo-secondary] {
    filter: grayscale(0);
  }
}

/* --------------------------------------------------------------------------
   Reduced motion — explicit coverage for every rule this block adds.
   Follows the v2 precedent: transitions removed AND positional transforms
   removed, so nothing jumps; state is still conveyed by colour, shadow and
   underline. The blanket kill-switch remains the first line of defence.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .svc::after,
  .svc__i,
  .svc__more::after,
  .article-card__cta,
  .faq-more a,
  .prose a:not(.btn),
  img.img-dim,
  img[data-photo-secondary] { transition: none; }

  .svc:focus-within,
  .article-card:focus-within,
  .btn:active { transform: none; }

  .svc:hover .svc__i,
  .svc:focus-within .svc__i,
  .svc:hover .svc__more::after,
  .svc:focus-within .svc__more::after,
  .article-card:hover .article-card__cta,
  .article-card:focus-within .article-card__cta { transform: none; }
}

/* ==========================================================================
   v3 Track F.1 — /reviews/ page (merged Phase 2)
   Contracts kept: reviews.js data-review-filter / data-filter /
   data-review-list / aria-pressed / [hidden] — no selector below touches
   those semantics. review_block() markup (frozen builder) is untouched;
   everything here is CSS-only. Negative reviews receive identical styling:
   the featured slot is purely positional (first in source order = newest
   review, whatever its sentiment), never a hand-picked "best" card.
   ========================================================================== */

/* ---------- F.1d hero dead space at 1440 ----------
   Root cause (VISUAL_AUDIT_V3 §0.5): the hero is a <section>, so generic
   `section { padding: var(--space-section) 0 }` (≈96px) stacks on top of
   `.hero--sub .wrap` padding (52/56px) → ~150px of empty dark space above
   and below two lines of content. Zero the section padding and let the wrap
   carry a single balanced value. Track G owns the same fix for the generic
   .hero--sub; this rule is scoped to .hero--reviews so the two merges cannot
   fight (identical technique, disjoint selectors). */
section.hero--reviews { padding: 0; }
/* MERGE CONFLICT RESOLVED: the reviews hero also matches Track G.2's later
   `.hero--sub .wrap` compact scale at equal (0,2,0) specificity, which
   would override this scoped rule by source order. The double class lifts
   this to (0,3,0) so the reviews page keeps its slightly roomier padding
   (it hosts the big 70 figure); both rules remove the same stacked section
   padding. */
.hero--sub.hero--reviews .wrap {
  padding: clamp(3rem, 6vw, 4.5rem) 20px clamp(3.25rem, 6.5vw, 5rem);
}

/* ---------- F.1b featured-first layout ----------
   The first card in the list (newest review — data order is NOT changed)
   spans the full grid width and reads larger. Below 768px the grid is a
   single column, so the rule is inert there and no mobile override is
   needed. When the Google filter hides a Thumbtack first card, the featured
   slot simply collapses ([hidden] { display:none } removes it from the
   grid) and the remaining cards flow normally — no JS involvement. */
@media (min-width: 768px) {
  .rev-list > .rev:first-child:not([hidden]) {
    grid-column: 1 / -1;
    padding: var(--space-8) var(--space-8) var(--space-7);
    border-top: 4px solid var(--red);
  }
  .rev-list > .rev:first-child:not([hidden]) .rev__q blockquote {
    font-size: var(--text-lg);
    max-width: 62ch;
  }
  /* A star-only newest record has no quote; keep the enlarged slot from
     looking empty by letting the compact row breathe instead. */
  .rev-list > .rev--compact:first-child:not([hidden]) {
    padding: var(--space-7) var(--space-8);
  }
}

/* ---------- F.1a summary line (matches the reviews.html hero edit) ----------
   .rev-summary already exists (base + dark-hero recolour). Only the new
   verification-link span needs a rule: same underline treatment as the old
   counted link, but the text no longer carries a count. */
.hero--reviews .rev-summary__verify { flex-basis: 100%; }
@media (min-width: 640px) {
  .hero--reviews .rev-summary__verify { flex-basis: auto; }
}

/* ---------- F.1 micro: card hover parity (respects reduced motion) ---------- */
@media (prefers-reduced-motion: no-preference) {
  .rev {
    transition: box-shadow var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
                transform var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
  }
}
.rev:hover,
.rev:focus-within {
  box-shadow: 0 6px 18px rgba(0, 0, 0, .10);
}

/* ==========================================================================
   v3 Track F.2 — FAQ accordion motion (merged Phase 2)
   Strategy — layered, no JS required for the motion:
   1. Modern engines (interpolate-size): CSS-only smooth height expansion
      AND collapse via ::details-content. The [open] attribute still flips
      instantly (native semantics; the ARIA sync in enhancements.js fires on
      the same toggle event), and content-visibility's allow-discrete
      transition keeps the closing answer visible only for the brief
      collapse — screen-reader state is never out of step with the
      attribute. This retires the old "closing must stay instant"
      compromise ACCESSIBLY in these engines.
   2. Engines without interpolate-size: open stays native-instant; the
      soft-open in enhancements.js keeps softening it (gated so the two
      never stack).
   3. No JS at all: native <details>, every answer readable. Unchanged.
   ========================================================================== */

/* Height animates to/from auto only inside the accordion — interpolate-size
   is scoped to .acc rather than :root so no other height:auto transition on
   the page changes behaviour. */
@supports (interpolate-size: allow-keywords) {
  .acc { interpolate-size: allow-keywords; }

  .acc::details-content {
    height: 0;
    overflow: clip;
    content-visibility: hidden;
  }
  .acc[open]::details-content {
    height: auto;
    content-visibility: visible;
  }

  @media (prefers-reduced-motion: no-preference) {
    .acc::details-content {
      transition:
        height var(--motion-standard, 320ms) var(--ease-out, cubic-bezier(0.22, 1, 0.36, 1)),
        content-visibility var(--motion-standard, 320ms) allow-discrete;
    }
  }
  /* Reduced motion: the transition block above simply never applies —
     open/close are instant, exactly the pre-v3 behaviour. */
}

/* Chevron: token-based timing (replaces the hard-coded .2s). A rotating
   chevron is motion — frozen under reduced motion. */
.acc__c {
  transition: transform var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
@media (prefers-reduced-motion: reduce) {
  .acc__c { transition: none; }
}

/* ---------- no viewport jump ----------
   scroll-margin keeps a summary clear of the sticky header both for
   #fragment links to details ids (faq.html sets ids) and for the
   enhancements.js guard that nudges an off-screen trigger back into view
   after one-open-at-a-time closes a taller item above it. 88px ≈ header
   (72px) + breathing room. */
.acc { scroll-margin-top: 88px; }

/* ==========================================================================
   v3 Track F.3 — form state system (merged Phase 2)
   Scope: the /contact-us/ estimate form (.field inputs, .fieldset radios,
   .consent). The form is backendless (action placeholder
   #FORM-ENDPOINT-PENDING, submit disabled, DEV NOTE visible) — so the
   resting/hover/focus/valid/invalid states are live CSS behaviour, while
   the submitting/success/server-error rules are DORMANT: classes nothing
   currently sets, staged so the day a real endpoint lands they already
   exist (js/form_states.js wires them).
   Principles: visible labels stay (no floating labels), :user-valid /
   :user-invalid keep everything CSS-only until first interaction (neither
   matches an untouched field, so the empty form doesn't open covered in
   red), 44px targets, and error-message space is RESERVED so nothing jumps.
   Color is never the only signal: invalid pairs border with a visible
   message; valid pairs border with an inline check glyph (local data URI —
   zero external requests).
   ========================================================================== */

/* ---------- resting / hover / focus ---------- */
.field input,
.field select,
.field textarea {
  min-height: 44px;                                   /* explicit tap target */
  transition: border-color var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
              box-shadow var(--motion-fast, 160ms) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
@media (prefers-reduced-motion: reduce) {
  .field input,
  .field select,
  .field textarea { transition: none; }
}

.field input:hover,
.field select:hover,
.field textarea:hover { border-color: #9a9aa2; }

/* Ring + border on focus. The global 3px red outline is :focus-visible;
   this adds a soft ring on plain :focus so pointer users get confirmation
   too, without doubling up when the outline is showing. */
.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--charcoal);
  box-shadow: 0 0 0 3px rgba(57, 57, 57, .18);
  outline-offset: 2px;
}

/* ---------- valid ----------
   :user-valid only matches after the user has interacted with the field, so
   optional untouched fields never show a false "all good". Check glyph is a
   local data URI (currentColor can't reach background-image; #3d7a5c is the
   same green family as .rev__verified's, 4.6:1 on white). */
.field input:user-valid,
.field select:user-valid,
.field textarea:user-valid {
  border-color: #3d7a5c;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 6L9 17l-5-5' fill='none' stroke='%233d7a5c' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 18px 18px;
  padding-right: 40px;                 /* keep text clear of the glyph */
}
/* Radios/checkbox/file keep their native indicators — no glyph overlay. */
.field input[type="file"]:user-valid,
.consent input:user-valid { background-image: none; padding-right: 0; }

/* ---------- invalid ----------
   :user-invalid (CSS-only) and [aria-invalid="true"] (set by the dormant
   form_states.js helper once a backend exists) share one appearance. */
.field input:user-invalid,
.field select:user-invalid,
.field textarea:user-invalid,
.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
  border-color: var(--red-dark);
  box-shadow: 0 0 0 3px rgba(var(--red-rgb), .12);
}

/* ---------- specific error messages, reserved space ----------
   Markup in contact.html: a .field__error span AFTER the input, before any
   .hint, on required fields only. Space is reserved (visibility, not
   display) so a message appearing/disappearing never moves the layout —
   the stated no-layout-jump requirement, bought with one quiet 1.4em line
   under the four required fields. --red-dark on white: 10.9:1. */
.field .field__error {
  display: block;
  min-height: 1.4em;
  visibility: hidden;
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--red-dark);
}
.field input:user-invalid ~ .field__error,
.field select:user-invalid ~ .field__error,
.field textarea:user-invalid ~ .field__error,
.field input[aria-invalid="true"] ~ .field__error,
.field select[aria-invalid="true"] ~ .field__error,
.field textarea[aria-invalid="true"] ~ .field__error {
  visibility: visible;
}
/* Consent checkbox: the input is nested inside the label, so the sibling
   combinator can't reach a .field__error outside it — :has() covers it
   (baseline since 2023; without :has() the native bubble still fires). */
.field:has(.consent input:user-invalid) .field__error {
  visibility: visible;
}

/* =========================================================================
   DORMANT STATES — nothing currently sets these classes. They activate the
   day the form gets a real endpoint (form_states.js wires them; the DEV
   NOTE and disabled submit come off at the same time). Documented so a
   future merge doesn't mistake them for dead code.
   ========================================================================= */

/* ---------- submitting: .form--submitting on the <form> ----------
   Button keeps its width (no text swap — opacity drop + spinner overlay),
   controls lock visually while the request is in flight. */
.form--submitting .btn[type="submit"] {
  position: relative;
  pointer-events: none;
  color: transparent;                 /* text hidden, width unchanged */
}
.form--submitting .btn[type="submit"]::after {
  content: "";
  position: absolute; inset: 0; margin: auto;
  width: 20px; height: 20px;
  border: 3px solid rgba(255, 255, 255, .35);
  border-top-color: #fff;
  border-radius: 50%;
}
@media (prefers-reduced-motion: no-preference) {
  .form--submitting .btn[type="submit"]::after {
    animation: form-spin var(--motion-slow, 800ms) linear infinite;
  }
}
/* Reduced motion: static three-quarter ring reads as "busy" without spin. */
@keyframes form-spin { to { transform: rotate(360deg); } }

/* ---------- success / server error: .form-status block ----------
   One region (added alongside the endpoint), role="status", tabindex="-1"
   so form_states.js can move focus to it — "focus moves to success or
   error summary" per MISSION F.3. Hidden until a modifier lands. */
.form-status { display: none; }
.form-status--success,
.form-status--error {
  display: block;
  margin-bottom: var(--space-5);
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}
.form-status--success {
  background: #eef5f0;
  border: 1px solid #3d7a5c;
  color: #2c523f;                     /* 7.4:1 on #eef5f0 */
}
.form-status--error {
  background: #fbeeee;
  border: 1px solid var(--red-dark);
  color: var(--red-dark);             /* 9.9:1 on #fbeeee */
}
.form-status:focus { outline: none; } /* focus target, not a control; the
                                         visible ring stays on real controls */

/* ==========================================================================
   v3 Track G.1 — typography deltas + tabular-nums apply-list + .num utility
   (merged Phase 2). No new fonts, no motion, no external requests. Text
   content untouched — CSS only.

   Verified-in-range, deliberately NOT changed:
   - .hero--home h1 line-height 1.05          (target 1.0–1.08 ✓)
   - h2 line-height 1.15 via base h1–h4 rule  (target 1.1–1.2 ✓)
   - body line-height 1.75 (v2)               (target 1.65–1.75 ✓)
   - .hero__meta max-width 60ch               (target 55–70ch ✓)
   - .hero__sub (base hero) max-width 660px ≈ 67ch @18px (in range ✓)
   - Eyebrow/uppercase audit: every uppercase element already uses
     letter-spacing 0.08em / weight 500–600 / xs–sm sizes. Consistent; no
     uppercase body paragraphs exist. No changes needed.
   ========================================================================== */

/* G.1(c): the v2 blanket heading tabular-nums rule was DELETED at merge.
   Tabular numerals ONLY where digits are data: review counts, experience
   counts, license numbers, phone numbers, statistics. Every target below is
   CSS-selector-reachable today — zero template churn, zero span-wrapping.
   Applying the property to a mixed text block is safe: it only affects the
   digit glyphs. Count-up targets (.proof-num, .rev-hero__num) keep their
   existing declarations; restated here as the canonical apply-list so the
   intent survives future refactors. */
.num,                 /* generic opt-in utility for any future numeric span */
.proof-num,           /* proof-bar counts (count-up target) */
.rev-hero__num,       /* reviews-hero big count (count-up target) */
.hero__proof,         /* homepage hero proof line (review counts) */
.rev-summary,         /* platform review-count summary lines */
.util,                /* utility bar: license number + phone */
.eyebrow,             /* interior-hero pill: license number */
.ft__lic,             /* footer license pill */
.ft__bot,             /* footer legal line (license number) */
.contact-list,        /* contact card: phone, hours */
.article-card__meta,  /* blog card date + "N min read" */
.article-meta,        /* article byline date + read time */
.why__n {             /* "01"-style why-card indices */
  font-variant-numeric: tabular-nums;
}

/* ---------- G.1(a) line-heights ----------
   Interior hero H1 sat on the base heading 1.15; tighten toward the hero
   range. (Homepage .hero--home h1 already 1.05 — untouched.) */
.hero--sub h1 {
  line-height: 1.1;
  text-wrap: balance; /* progressive; matches .shead h2 / .hero--home h1 */
}

/* ---------- G.1(b) paragraph measure 55–70ch ----------
   .prose was 780px ≈ 79ch at the 18px body size — over the 70ch ceiling.
   65ch ≈ 645px at 18px. ch-based so the measure tracks the fluid type. */
.prose { max-width: 65ch; }

/* .shead is a 680px box; its <p> could run ≈ 69ch edge-to-edge. Cap the
   paragraph itself at a comfortable centered measure. */
.shead p { max-width: 60ch; margin-left: auto; margin-right: auto; }

/* ---------- G.1(d) text-wrap: pretty on intros/leads ----------
   Progressive enhancement — ignored where unsupported; no layout risk.
   Scoped to lead/intro copy per the mission (not blanket body text). */
.hero__sub,
.hero__meta,
.shead p,
.prose > p:first-of-type,
.areas-note {
  text-wrap: pretty;
}

/* ==========================================================================
   v3 Track G.2 — interior hero (.hero--sub) compaction + balance
   (merged Phase 2).

   THE BUG (VISUAL_AUDIT_V3 §0.5, SYSTEMIC): .hero--sub is a <section>, so
   the base `section { padding }` ramp (56 → 64 → 96px each side) STACKS on
   the `.hero--sub .wrap` padding (40/44 → 52/56px). At 1440 that is ~148px
   of dead dark space above the eyebrow and ~152px below the content —
   ~580px-tall heroes for two lines of copy, on every interior page.

   THE FIX: zero the section padding on the hero itself so the .wrap padding
   is the only vertical rhythm, then tune one compact scale. `.hero--sub`
   (0,1,0) beats `section` (0,0,1) at every breakpoint; the `.hero--sub
   .wrap` rules below beat the earlier identical-specificity rules on
   source order. The 5px red bottom rule (.hero::after) and the radial red
   wash (.hero::before) are untouched and keep anchoring the fold.
   ========================================================================== */

.hero--sub { padding: 0; }

/* Compact padding scale (replaces BOTH stacked layers).
   390:   44 top / 48 bottom   (was 96 / 100 combined)
   768+:  48 top / 56 bottom   (was 116 / 120)
   1024+: 52 top / 60 bottom   (was 148 / 152) */
.hero--sub .wrap { padding: 44px 20px 48px; }
@media (min-width: 768px) {
  .hero--sub .wrap { padding: 48px 20px 56px; }
}
@media (min-width: 1024px) {
  .hero--sub .wrap { padding: 52px 20px 60px; }
}
/* Note: the reviews hero keeps its own clamp()-based padding via Track
   F.1d's `.hero--sub.hero--reviews .wrap` (0,3,0 — deliberately above this
   block's 0,2,0 so source order cannot flip the winner). Both remove the
   same stacked section padding. */

/* ---------- balance: the empty right half ----------
   Decision per page type (from the before/ screenshots):

   1. BASE-TEMPLATE HEROES — blog hub, FAQ hub, contact, services hub,
      service-areas hub, projects, about/legal (h1 + optional intro only;
      no eyebrow, no CTA row): left-aligned content in a 1180px wrap left
      ~55% of the panel empty. These get a CENTERED, NARROWER MEASURE via
      the hero--center class on base.html; every hub inherits it.

   2. SERVICE / AREA / ARTICLE HEROES (eyebrow pill + H1 + sub + CTA row /
      byline): stay LEFT-ALIGNED — they are content-rich, well-formed per
      the audit, and after compaction the existing radial red wash occupies
      the reduced right half. No page-context element is added: the only
      candidate content (license number) is already the eyebrow pill, and
      duplicating it right would re-create the repeated-claims problem the
      audit flagged.

   3. REVIEWS HERO: already balanced (lead + big-number figure fills the
      right half) — inherits compaction only. */

.hero--sub.hero--center .wrap { text-align: center; }
.hero--sub.hero--center h1 { margin-left: auto; margin-right: auto; }
.hero--sub.hero--center .hero__sub {
  margin-left: auto; margin-right: auto;
  max-width: 54ch; /* centred measure reads best a notch under the 660px cap */
}

/* ==========================================================================
   v3 Track G.3 — hub-page rhythm additions (merged Phase 2). One rhythm
   element per hub — pages stay distinct, sharing only the v2 vocabulary
   (section surfaces, dark-context tints, card treatment). No motion added.

   Rhythm plan (template anchors applied at merge):
   - Contact:        dark contact-info card (card--dark) on the white form
                     section — one dark moment without touching the form.
   - FAQ hub:        section-number figures on category headers (CSS
                     counters — zero template churn).
   - Blog hub:       already has its light band (article-card grid on .wash)
                     — kept as-is on purpose; restraint is the decision.
   - Services hub:   closing "Where we work / Not sure" prose section
                     becomes a charcoal band (section--mid) before the red
                     CTA band — mirrors the homepage dark→red cadence.
   - Service-areas:  "Also Nearby" (tier-3) section becomes section--dark —
                     breaks the four-light-section run mid-page; v2 already
                     ships dark-section styling for .areas pills, .kicker,
                     .shead p and .areas-note, so no additional CSS.
   - Projects:       empty-state section becomes section--mid with a dark
                     .empty variant — the page's one dark moment.
   ========================================================================== */

/* ---------- contact: dark info card ----------
   Same surface + tint system as v2 dark sections; the red gradient edge
   reuses the .why::after pattern. White-recoloured focus outline follows
   the established dark-surface rule. */
.card--dark {
  position: relative; overflow: hidden;
  background: var(--surface-dark);
  border-color: var(--surface-dark);
  color: #fff;
}
.card--dark::after {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: linear-gradient(180deg, var(--red), var(--red-dark));
}
.card--dark h2 { color: #fff; }
.card--dark h3 { color: var(--color-muted-light); }
.card--dark p { color: var(--on-dark-body); }
.card--dark .contact-list { color: rgba(255, 255, 255, .85); }
.card--dark .contact-list a { color: var(--on-dark-red); } /* 7.7:1 on #1a1a1a */
.card--dark :focus-visible { outline-color: #fff; }

/* ---------- FAQ hub: section-number figures on category headers ----------
   Large low-opacity index numeral right-aligned on each category rule —
   the "large low-opacity section number" transition type from the D.2
   vocabulary, applied at hub scale. CSS counters: zero template churn.
   The `/ ""` alt-text hides the decorative numeral from assistive tech
   where supported; where unsupported a read-out of "01" is harmless. */
main { counter-reset: faq-cat; }
.faq-cat h2 {
  counter-increment: faq-cat;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--space-4);
}
.faq-cat h2::after {
  content: counter(faq-cat, decimal-leading-zero) / "";
  font-size: var(--text-2xl); font-weight: 800; line-height: .8;
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
  color: rgba(var(--red-rgb), .22); /* decorative figure, not text */
}

/* ---------- services hub: dark prose band (section--mid) ----------
   .prose hard-codes light-surface colours; dark-context recolours follow
   the exact v2 tint table. Written for both dark surfaces so the same
   rules serve any future dark prose band. */
.section--dark .prose p,
.section--mid .prose p,
.section--dark .prose li,
.section--mid .prose li {
  color: var(--on-dark-body);
}
.section--dark .prose a,
.section--mid .prose a {
  color: var(--on-dark-red);
}
.section--dark .prose strong,
.section--mid .prose strong {
  color: #fff;
}
/* .prose h2/h3 carry no colour of their own — they inherit the section's
   white. Nothing to recolour. */

/* ---------- projects: dark empty-state ----------
   Translucent card face + hairline, same treatment as v2's dark .svc/.acc.
   .empty h2 inherits the section white; the .btn--red inside already has
   its dark-surface glow. */
.section--dark .empty,
.section--mid .empty {
  background: var(--on-dark-card);
  border-color: var(--on-dark-line);
}
.section--dark .empty p,
.section--mid .empty p {
  color: var(--on-dark-body);
}
.section--dark .empty a:not(.btn),
.section--mid .empty a:not(.btn) {
  color: var(--on-dark-red);
}

/* ==========================================================================
   PROJECT REQUEST BUILDER — page-scoped styles (/project-request/ only).
   Merged from PROJECT_REQUEST_BUILDER/staging/track_c/prb.css (Phase 2).
   Uses only the v3 tokens defined above; selectors are all .prb-* scoped,
   so nothing here can restyle existing pages.
   ========================================================================== */
/* ==========================================================================
   prb.css — PLEC Project Request Builder (STAGING — Track C, not live)
   Page-scoped stylesheet for /project-request/, loaded after main.css and
   only by the builder template. Every colour is a main.css custom property
   or an rgba() derivation of one — no new hex brand values.
   Spec section 20 states covered: add / added / hover / focus / disabled /
   quantity / empty panel / filled panel / recommendation / safety warning /
   incomplete / complete / connector enabled / connector disabled /
   saved locally / export success / export error.
   ========================================================================== */

/* ---------- layout: step bar top, catalog left, panel right ---------- */

#prb-app { padding-bottom: var(--space-10); }

.prb-layout { display: block; }

@media (min-width: 1024px) {
  .prb-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    gap: var(--space-8);
    align-items: start;
  }
  #prb-panel {
    position: sticky;
    top: var(--space-6);
    /* 200%-zoom safe: cap, then scroll inside — never clip text width */
    max-height: calc(100vh - var(--space-12));
    overflow-y: auto;
  }
}

/* ---------- step bar ---------- */

#prb-steps { margin: var(--space-6) 0 var(--space-8); }

.prb-steps__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--space-2);
}
.prb-steps__item { flex: 1 1 auto; min-width: 0; }
.prb-steps__btn {
  display: flex; align-items: center; gap: var(--space-2);
  width: 100%; min-height: 44px;
  padding: var(--space-2) var(--space-3);
  background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius-md);
  font: inherit; font-size: var(--text-sm); font-weight: 600;
  color: var(--muted); cursor: pointer; text-align: left;
}
.prb-steps__btn:disabled { opacity: .55; cursor: not-allowed; }
.prb-steps__n {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 50%;
  border: 2px solid currentColor; font-size: var(--text-xs);
}
.prb-steps__item.is-current .prb-steps__btn {
  color: #fff; background: var(--charcoal); border-color: var(--charcoal);
}
.prb-steps__item.is-current .prb-steps__n {
  background: linear-gradient(180deg, var(--red), var(--red-dark));
  border-color: transparent; color: #fff;
}
.prb-steps__item.is-done .prb-steps__btn { color: var(--ink); }
.prb-steps__item.is-done .prb-steps__n { color: var(--red-dark); }
@media (max-width: 640px) {
  /* 320px-safe: numbers only, labels visually hidden but still read out */
  .prb-steps__label {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .prb-steps__btn { justify-content: center; }
}

/* ---------- catalog cards + add-to-project control (section 7) ---------- */

.prb-catalog {
  display: grid; gap: var(--space-5);
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
}
.prb-catalog__group { grid-column: 1 / -1; margin-top: var(--space-6); }
.prb-catalog__group .cline { width: 48px; margin: 0 0 var(--space-3); }

.prb-card {
  background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-6) var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-3);
  transition: border-color .15s ease, box-shadow .15s ease;
}
.prb-card:hover { box-shadow: 0 6px 18px rgba(0, 0, 0, .08); }
.prb-card.is-added {
  border-color: var(--red-dark);
  box-shadow: 0 0 0 1px var(--red-dark);
}
.prb-card h3 { font-size: var(--text-lg); font-weight: 600; }
.prb-card p { font-size: var(--text-sm); color: var(--muted); flex: 1 1 auto; }
.prb-card__link { font-size: var(--text-xs); }

/* The toggle. Both labels occupy the same reserved box (grid stack), so the
   swap causes zero width/height shift (section 7 "no card-height shift").
   JS keeps an accessible-name span (.prb-add__name) in sync; the two visual
   layers are aria-hidden in the template. */
.btn.prb-add {
  width: 100%; min-height: 44px;
  border: 2px solid var(--red); color: var(--red-dark); background: #fff;
}
.btn.prb-add:hover { background: rgba(var(--red-rgb), .06); border-color: var(--red-dark); filter: none; }
.btn.prb-add .prb-add__labels { display: inline-grid; }
.btn.prb-add .prb-add__labels > span { grid-area: 1 / 1; white-space: nowrap; }
.btn.prb-add .prb-add__label--added { visibility: hidden; }
.btn.prb-add.is-added .prb-add__label--add { visibility: hidden; }
.btn.prb-add.is-added .prb-add__label--added { visibility: visible; }
/* added state */
.btn.prb-add.is-added,
.btn.prb-add[aria-pressed="true"] {
  background: linear-gradient(180deg, var(--red), var(--red-dark));
  border-color: transparent; color: #fff;
  box-shadow: 0 3px 10px rgba(var(--red-rgb), .28);
}
.btn.prb-add:disabled,
.btn.prb-add[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }
/* focus: global 3px red outline from main.css applies; recolour on the
   filled (red) state so the ring stays visible */
.btn.prb-add.is-added:focus-visible { outline-color: var(--charcoal); }

/* ---------- "Your Project" panel ---------- */

#prb-panel {
  background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-6) var(--space-5);
}
#prb-panel.is-empty { border-style: dashed; }
.prb-panel__cline { width: 48px; margin: 0 0 var(--space-3); }
.prb-panel__title { font-size: var(--text-xl); font-weight: 700; margin: 0 0 var(--space-4); }
.prb-panel__sub {
  font-size: var(--text-sm); font-weight: 500; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
  margin: var(--space-5) 0 var(--space-2);
}
.prb-panel__empty { font-size: var(--text-sm); color: var(--muted); }

/* selection rows */
.prb-sel { list-style: none; margin: 0 0 var(--space-4); padding: 0; }
.prb-sel__row {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}
.prb-sel__top { display: flex; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.prb-sel__name { font-weight: 600; font-size: var(--text-sm); }
.prb-sel__actions { display: inline-flex; gap: var(--space-2); }

/* link-style buttons (Edit / Remove / Add / Dismiss) — still 44px targets */
.prb-linkbtn {
  background: none; border: 0; padding: var(--space-2) var(--space-2);
  min-height: 44px; min-width: 44px;
  font: inherit; font-size: var(--text-sm); font-weight: 600;
  color: var(--red-dark); text-decoration: underline; cursor: pointer;
}
.prb-linkbtn:hover { color: var(--red); }
.prb-linkbtn--rm { color: var(--muted); }
.prb-linkbtn--rm:hover { color: var(--ink); }

/* quantity stepper (section 7 quantity state) */
.prb-qty { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); flex-wrap: wrap; }
.prb-qty__btn {
  width: 44px; height: 44px; flex: none;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--wash); font: inherit; font-size: var(--text-lg);
  font-weight: 700; color: var(--ink); cursor: pointer;
}
.prb-qty__btn:hover { border-color: var(--charcoal); }
.prb-qty__input {
  width: 64px; min-height: 44px; text-align: center;
  font: inherit; font-size: var(--text-base);
  border: 1px solid #c9c9d2; border-radius: var(--radius-sm);
}
.prb-qty__unit { font-size: var(--text-xs); color: var(--muted); }

/* recommendation cards (section 10) */
.prb-recs { list-style: none; margin: 0 0 var(--space-4); padding: 0; }
.prb-rec {
  background: var(--wash); border: 1px solid var(--line);
  border-left: 4px solid var(--red);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-3);
}
.prb-rec__name { font-weight: 600; font-size: var(--text-sm); display: block; }
.prb-rec__reason { font-size: var(--text-xs); color: var(--muted); margin: var(--space-1) 0 0; }
.prb-rec__actions { display: flex; gap: var(--space-2); }

/* completeness (section 13): incomplete vs complete */
.prb-comp { margin: var(--space-4) 0; }
.prb-comp__label { font-size: var(--text-sm); font-weight: 600; margin-bottom: var(--space-2); }
.prb-comp__bar {
  height: 8px; border-radius: var(--radius-pill);
  background: var(--line); overflow: hidden;
}
.prb-comp__fill {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--charcoal), var(--ink));
  border-radius: var(--radius-pill);
  transition: width .3s ease;
}
.prb-comp.is-complete .prb-comp__fill {
  background: linear-gradient(90deg, var(--red), var(--red-dark));
}
.prb-comp.is-complete .prb-comp__label::after { content: " ✓"; color: var(--red-dark); }
.prb-comp__list { list-style: none; margin: var(--space-3) 0 0; padding: 0; font-size: var(--text-xs); }
.prb-comp__list li { padding: 2px 0; color: var(--muted); }
.prb-comp__list li.is-done { color: var(--ink); }
.prb-comp__mark { display: inline-block; width: 1.2em; font-weight: 700; }
.prb-comp__list li.is-done .prb-comp__mark { color: var(--red-dark); }

.prb-panel__continue { width: 100%; margin-top: var(--space-3); }

/* saved-locally line (section 20 "saved locally") */
.prb-saved {
  margin-top: var(--space-3);
  font-size: var(--text-xs); color: var(--muted); text-align: center;
}

/* ---------- guided form (section 9) ---------- */

#prb-form { max-width: 720px; }
.prb-step__cline { width: 48px; margin: var(--space-6) 0 var(--space-3); }
.prb-step__title { font-size: var(--text-2xl); font-weight: 700; margin-bottom: var(--space-5); }
.prb-step__title:focus { outline: none; } /* programmatic focus target only;
  keyboard focus never lands here via Tab */
.prb-fields__group {
  font-size: var(--text-sm); font-weight: 500; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
  margin: var(--space-6) 0 var(--space-3);
  padding-top: var(--space-4); border-top: 1px solid var(--line);
}
.prb-field__error, .field__error { color: var(--red-dark); font-size: var(--text-sm); font-weight: 600; margin-top: var(--space-1); }
.prb-field input[aria-invalid="true"],
.prb-field select[aria-invalid="true"],
.prb-field textarea[aria-invalid="true"] { border-color: var(--red-dark); }

/* ---------- returning-customer code (coupon patch) ----------
   Selectors match buildCodeDisclosure(): <details.prb-code> containing
   <summary.prb-code__toggle> and a div.field.prb-field.prb-code__field with
   label + input + p.prb-code__status[aria-live] ("is-known" when matched).
   No-JS degradation: native <details> opens/closes with no script; where
   <details> is unsupported the content renders expanded — a plain visible
   field. Nothing here hides the field behind a JS-only class. */

.prb-code { margin: var(--space-5) 0 var(--space-6); }

/* Quiet link-like affordance in the same voice as .prb-linkbtn. The native
   disclosure marker stays (summary keeps its default list-item display),
   recoloured to match the text. */
.prb-code__toggle {
  display: list-item;                /* explicit: preserves the native marker */
  min-height: 44px;                  /* tap target, as elsewhere in the PRB */
  padding: var(--space-2) var(--space-1);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--red-dark);
  text-decoration: underline;
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease-standard);
}
.prb-code__toggle:hover { color: var(--red); }
.prb-code__toggle::marker { color: var(--red-dark); }
.prb-code__toggle::-webkit-details-marker { color: var(--red-dark); } /* legacy WebKit */
/* Focus ring: the global summary:focus-visible rule already covers the
   toggle — not duplicated here. The input keeps the global .field ring. */

.prb-code__field {
  margin: var(--space-3) 0 0;
  padding: var(--space-4) var(--space-4) var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: #fff;
}

/* Codes cap at 24 characters (PRB.codes.sanitize), so the control doesn't
   need the full form column. A ch-based width overrides the global .field
   input sizing — deliberately no percentage sizing anywhere in this block. */
.prb-code__field input {
  width: 26ch;
  max-inline-size: stretch;          /* never wider than the field card */
  letter-spacing: .04em;             /* codes read as codes */
  text-transform: uppercase;         /* visual mirror of PRB.codes.sanitize;
                                        JS still canonicalises the value */
}

.prb-code__status {
  min-height: 1.4em;                 /* space reserved so the aria-live note
                                        never shifts the layout when it lands */
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--muted);               /* neutral "we'll verify" note */
}
/* Recognised code: confirming note with a check, same ::before glyph pattern
   as .prb-export__status.is-ok. Text green is .rev__verified's; glyph green
   is the form-validation check's — colour is never the only signal (the
   wording changes too, via aria-live). */
.prb-code__status.is-known { color: #3d5c4c; font-weight: 600; }
.prb-code__status.is-known::before { content: "✓ "; color: #3d7a5c; font-weight: 700; }

/* Reduced motion: main.css's blanket kill-switch already neutralises every
   transition; scoped again here so the block stays self-contained. */
@media (prefers-reduced-motion: reduce) {
  .prb-code__toggle { transition: none; }
}

.prb-stepnav { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-6); }

/* error summary (section 21) */
.prb-errsum {
  border: 2px solid var(--red-dark); border-radius: var(--radius-md);
  background: rgba(var(--red-rgb), .05);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-5);
}
.prb-errsum__title { font-size: var(--text-base); font-weight: 700; margin-bottom: var(--space-2); }
.prb-errsum__list { margin: 0; padding-left: var(--space-5); font-size: var(--text-sm); }
.prb-errsum__list a { color: var(--red-dark); font-weight: 600; }

/* ---------- safety warning (section 11) ---------- */

.prb-safety {
  border-left: 4px solid var(--red);
  background: var(--surface-dark);
  color: #fff;
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-5);
  margin: var(--space-5) 0;
}
.prb-safety__text { font-size: var(--text-base); font-weight: 600; line-height: 1.5; margin: 0; }
.prb-safety :focus-visible { outline-color: #fff; }
.prb-choice--safety { border-color: rgba(var(--red-rgb), .45); }

/* ---------- attachments (section 12) ---------- */

.prb-attach__tip { margin-bottom: var(--space-2); }
.prb-attach input[type="file"] { border: 0; padding: var(--space-2) 0; min-height: 44px; }
.prb-attach__status { font-size: var(--text-sm); color: var(--muted); min-height: 1.4em; }
.prb-attach__list { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.prb-attach__item {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2);
}
.prb-attach__thumb {
  width: 48px; height: 48px; object-fit: cover;
  border-radius: var(--radius-sm); flex: none; max-width: 100%;
}
.prb-attach__info { display: flex; flex-direction: column; min-width: 0; flex: 1 1 120px; }
.prb-attach__name { font-size: var(--text-sm); font-weight: 600; overflow-wrap: anywhere; }
.prb-attach__meta { font-size: var(--text-xs); color: var(--muted); }

.prb-badge {
  display: inline-block; padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 600; letter-spacing: .04em;
}
.prb-badge--local { background: var(--wash); color: var(--charcoal); border: 1px solid var(--line); }

/* ---------- review (section 14) + status/export (sections 15, 20) ---------- */

.prb-review__sec {
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--space-5); margin-bottom: var(--space-5);
  background: #fff;
}
.prb-review__head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
.prb-review__title { font-size: var(--text-lg); font-weight: 700; }
.prb-review__dl { margin: var(--space-3) 0 0; }
.prb-review__dl dt { font-size: var(--text-xs); font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); margin-top: var(--space-3); }
.prb-review__dl dd { margin: 0; font-size: var(--text-base); overflow-wrap: anywhere; }
.prb-review__svc { margin-top: var(--space-4); }
.prb-review__svcname { font-size: var(--text-base); font-weight: 600; }
.prb-review__empty, .prb-review__missing { font-size: var(--text-sm); color: var(--muted); margin-top: var(--space-2); }
.prb-review__files { margin: var(--space-3) 0 0; padding-left: var(--space-5); font-size: var(--text-sm); }

/* connector status: disconnected (honest) vs enabled */
.prb-status {
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-5);
  font-size: var(--text-sm);
}
.prb-status--disconnected {
  background: var(--wash);
  border: 1px dashed var(--charcoal);
  color: var(--ink);
}
.prb-status--connected {   /* dormant until a real connector is enabled */
  background: #fff;
  border: 1px solid var(--line);
  border-left: 4px solid var(--red);
}
.prb-status__line { font-weight: 600; margin: 0 0 var(--space-1); }
.prb-status__delivery { margin: 0; color: var(--muted); }

/* export actions + success/error status lines */
.prb-export { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; margin-bottom: var(--space-5); }
.prb-export__btn { flex: 1 1 200px; max-width: 100%; }
.prb-export__status { flex-basis: 100%; font-size: var(--text-sm); font-weight: 600; min-height: 1.4em; margin: 0; }
.prb-export__status.is-ok { color: var(--charcoal); }
.prb-export__status.is-ok::before { content: "✓ "; color: var(--red-dark); }
.prb-export__status.is-err { color: var(--red-dark); }
.prb-export__status.is-err::before { content: "! "; }
.prb-export__clear { flex-basis: 100%; text-align: left; }
.prb-export__fallback { flex-basis: 100%; }
.prb-export__ta {
  width: 100%; min-height: 160px; font: inherit; font-size: var(--text-sm);
  border: 1px solid #c9c9d2; border-radius: var(--radius-sm); padding: var(--space-3);
}

.prb-disclaimer { font-size: var(--text-xs); color: var(--muted); margin-bottom: var(--space-5); }

/* ---------- mobile dock + drawer (section 8, Amendment 3) ---------- */

#prb-dock {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3);
  background: var(--surface-dark); color: #fff;
  padding: var(--space-3) var(--space-4);
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  border-top: 3px solid var(--red);
}
#prb-dock.is-empty { display: none; }
.prb-dock__count { font-size: var(--text-sm); }
.prb-dock__count strong { font-size: var(--text-lg); margin-right: 4px; }
.prb-dock__open { min-height: 44px; }
#prb-dock :focus-visible { outline-color: #fff; }
/* keep page content clear of the dock */
@media (max-width: 1023px) {
  #prb-app { padding-bottom: calc(var(--space-16) + env(safe-area-inset-bottom, 0px) + 44px); }
}
@media (min-width: 1024px) {
  #prb-dock { display: none; }
}

/* drawer mode: the panel becomes a bottom-sheet dialog */
.prb-backdrop {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0, 0, 0, .5);
}
#prb-panel.prb-panel--drawer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 95;
  max-height: 85vh; overflow-y: auto;
  overscroll-behavior: contain;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  border: 0; border-top: 3px solid var(--red);
  padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
  box-shadow: 0 -8px 30px rgba(0, 0, 0, .25);
}
/* slide-up uses transform/opacity only; instant under reduced motion */
#prb-panel.prb-panel--animate {
  animation: prb-slide-up .28s var(--ease-out, ease-out);
}
@keyframes prb-slide-up {
  from { transform: translateY(24px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  #prb-panel.prb-panel--animate { animation: none; }
  .prb-comp__fill { transition: none; }
  .prb-card, .btn.prb-add { transition: none; }
}
.prb-panel__close {
  position: sticky; top: 0; float: right;
  width: 44px; height: 44px;
  border: 1px solid var(--line); border-radius: 50%;
  background: #fff; color: var(--ink);
  font-size: var(--text-xl); line-height: 1; cursor: pointer;
}
.prb-panel__close:hover { border-color: var(--charcoal); }
/* background scroll lock while the drawer is open */
html.prb-drawer-open, html.prb-drawer-open body { overflow: hidden; }

/* ---------- small screens / zoom safety ---------- */

@media (max-width: 480px) {
  .prb-catalog { grid-template-columns: 1fr; }
  .prb-stepnav .btn { width: 100%; }
  .prb-export__btn { flex-basis: 100%; }
}
/* no fixed widths above 280px anywhere; text containers never overflow:hidden */

/* ---------- print (section 15) ---------- */

#prb-print { display: none; }

@media print {
  /* hide the chrome and the interactive app */
  .hdr, .util, .ft, .band, .cta-bar, .to-top, .skip, .staging-banner,
  #prb-dock, #prb-panel, #prb-steps, #prb-form, .prb-catalog,
  .prb-backdrop, .prb-export, .prb-stepnav, button, nav, .breadcrumbs {
    display: none !important;
  }
  #prb-print {
    display: block !important;
    color: #000; background: #fff;
    font-size: 12pt; line-height: 1.5;
  }
  #prb-print h1 { font-size: 18pt; margin-bottom: 6pt; }
  #prb-print h2 { font-size: 14pt; margin: 10pt 0 4pt; }
  #prb-print ul { margin: 4pt 0 4pt 14pt; }
  #prb-print .prb-print__ref { font-weight: 700; }
  #prb-print .prb-print__disclaimer { margin-top: 10pt; font-size: 10pt; }
  #prb-print .prb-print__stamp { font-size: 10pt; color: #000; }
  /* empty fields are never rendered into #prb-print (JS skips them) */
}

/* PRB Phase 3 integration fix: the three JS-filled hosts ship server-empty
   (no hidden attr — prb-ui.js doesn't manage host hidden state). :empty
   keeps them invisible for no-JS visitors and until the UI renders. */
#prb-steps:empty,
#prb-panel:empty,
#prb-dock:empty { display: none; }

/* PRB Phase 3 fix 2: explicit display values (.prb-catalog { display:grid },
   etc.) beat the UA's [hidden] rule, so JS-managed hidden attrs inside the
   builder never actually hid anything. Scoped hard guard. */
#prb-app [hidden],
#prb-app[hidden],
#prb-steps[hidden],
#prb-steps [hidden],
#prb-dock[hidden],
#prb-dock [hidden] { display: none !important; }
