/* ==========================================================================
   Fortified Asset Solutions — main.css  (V3)
   Evolves 02_Assets/site_v1_baseline/assets/css/site.css — same token system,
   same palette (sand paper / rust / navy), same type stack. Nothing replaced.
   Added in V3: "work-order ticket" component layer — mono section indices,
   hairline rules, dotted-leader spec rows, numbered process ladder, placeholder
   chips, marked review slot, breadcrumbs, package + service index components.
   Vanilla CSS. No framework, no build step. Painting-order rules honored.
   ========================================================================== */

/* FONTS ARE LINKED FROM <head>, NOT @import-ED HERE.  (V3.1e — QA P-12)
   The @import that used to sit on this line chained the font request behind
   main.css's own download+parse, so the preconnect in <head> bought nothing and
   every first paint cost an extra round trip on a slow connection. common.head()
   now emits the <link rel="stylesheet"> for the three families immediately
   BEFORE this file, which is what the preconnect was always for. Do not put an
   @import back — if the families change, change them in common.head(). */

/* ---------- Design tokens (carried forward from V1, extended) ---------- */
:root {
  /* Palette — OKLCH per brand-pack.md */
  --color-bg-primary:     oklch(97.5% 0.012 85);
  --color-bg-secondary:   oklch(93% 0.02 75);
  --color-text-primary:   oklch(24% 0.03 250);
  --color-text-secondary: oklch(42% 0.025 250);
  --color-accent:         oklch(48% 0.13 45);
  --color-accent-soft:    oklch(88% 0.05 50);
  --color-ink:            oklch(20% 0.035 250);

  /* Fallback sRGB (older browser safety net) */
  --paper:     #F7F2E9;
  --paper-2:   #EDE2CE;
  --paper-3:   #FBF8F1;
  --ink:       #23272F;
  --ink-soft:  #4B5563;
  --rust:      #B5501E;
  --rust-deep: #8E3D14;
  --rust-soft: #F0D2B8;
  /* --rust-text: rust for TEXT ON SAND only. --rust (#B5501E) is the ACCENT —
     buttons, rules, dots, borders, icon fills — and it does NOT change: it is
     a white-on-rust pairing there (4.6:1) and non-text everywhere else.
     But as small text on the sand family it fails WCAG AA:
        #B5501E on --rust-soft #F0D2B8 = 3.54:1  (.eyebrow pill)
        #B5501E on --paper-2  #EDE2CE = 3.96:1  (.rule-head .idx)
     #8E3D14 takes the same two pairs to 5.17:1 and 5.79:1. Same hue family,
     same mark; only the text token darkened. Use --rust for accent, this for
     rust-colored words on paper. */
  --rust-text: #8E3D14;
  --navy:      #1B2333;
  --navy-2:    #2A3346;

  /* V3 additions */
  --hair:      rgba(27, 35, 51, 0.14);
  --hair-2:    rgba(27, 35, 51, 0.08);
  --hair-band: rgba(255, 255, 255, 0.16);
  --band-text: #F1EEE6;
  --band-soft: #C7CBD6;

  --font-display: "Manrope", system-ui, sans-serif;
  --font-body: "Public Sans", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;

  --radius: 6px;
  --radius-lg: 14px;
  --shadow-card: 0 4px 14px rgba(27, 35, 51, 0.10), 0 1px 3px rgba(27, 35, 51, 0.08);
  --shadow-lift: 0 14px 34px rgba(27, 35, 51, 0.14);

  --ease: cubic-bezier(.2,.8,.2,1);
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }
img, picture, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }
ul, ol { list-style: none; }

/* ---------- Painting order: html carries bg + decoration, body transparent ---------- */
html {
  background-color: var(--paper);
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

html::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    /* P-11: opacity comes down with the wider mask below — the grid now covers
       roughly twice the area, so the same 0.30 read as a much louder motif. */
    linear-gradient(90deg, rgba(181, 80, 30, 0.21) 1px, transparent 1px),
    linear-gradient(0deg, rgba(181, 80, 30, 0.21) 1px, transparent 1px);
  background-size: 56px 56px;
  /* P-11: the old ellipse (70% 55%, falloff stopping at 78%) cut the 56px grid
     at a visible hard boundary ~1180px in and clipped the top row against the
     breadcrumb bar. Wider ellipse + a falloff that runs to 96% means the motif
     dissolves rather than ending. */
  -webkit-mask-image: radial-gradient(ellipse 100% 78% at 50% 0%, black 26%, transparent 96%);
  mask-image: radial-gradient(ellipse 100% 78% at 50% 0%, black 26%, transparent 96%);
}

body {
  background: transparent;
  font-family: var(--font-body);
  color: var(--ink);
  line-height: 1.6;
  font-size: 17px;
  -webkit-font-smoothing: antialiased;
}

main { position: relative; z-index: 1; }
header.site-header, footer.site-footer { position: relative; z-index: 1; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 800;
  line-height: 1.1;
  color: var(--ink);
  letter-spacing: -0.01em;
  text-wrap: balance;
}
/* ---- TYPE SCALE — V3.1e SECTION-BREAK PASS (owner's direct note) ----------
   The owner read the numbered section headers as too small to mark a break:
   "01 You're Hiring", "02 What I Do" were 12px mono against 36px H2s, and the
   homepage H1 was 40px, so H1 and H2 sat 4px apart and nothing announced a new
   section. Solved as a SYSTEM, three moves that keep the ranking intact:

     1. the numeral gets its own element and its own size (.idx-n, 24px rust mono)
        instead of being a 12px string prefix — see .rule-head below;
     2. H2 up to 41.6px @1280 (was 36) so a section heading reads as a heading;
     3. both H1s move up ahead of it — home 40 -> 48px, interior 54.4px unchanged
        at the top end but floored higher on phones (2.1rem -> 2.3rem) where the
        old min put a 32.4px H1 against a 29.6px H2.

   Measured @1280:  interior H1 54.4  >  home H1 48.0  >  H2 41.6  >  idx-n 24.
   Measured  @390:  interior H1 36.8  >  home H1 31.0  >  H2 29.6  >  idx-n 24.
   The homepage lockup is untouched: --em is 133px @1280, FORTIFIED renders at
   ~103px cap height, so the emblem still leads the H1 by 2.15x. */
h1 { font-size: clamp(2.3rem, 4.2vw + 1rem, 3.4rem); }
h2 { font-size: clamp(1.85rem, 2.3vw + 1.15rem, 2.6rem); }
h3 { font-size: 1.2rem; font-weight: 700; }
h4 { font-size: 1rem; font-weight: 700; }

p { color: var(--ink-soft); max-width: 64ch; }
p + p { margin-top: 14px; }
/* H2s grew ~15% in V3.1e; a paragraph butted straight against one now reads
   cramped. Collapses with .section-head h2's own 16px, so nothing doubles. */
h2 + p { margin-top: 14px; }
.mono { font-family: var(--font-mono); font-weight: 500; letter-spacing: 0.01em; }
.lede { font-size: 1.13rem; color: var(--ink-soft); }
/* Same rust-on-sand text pair as .eyebrow/.idx — an inline link is body-size
   text on paper, so it takes --rust-text too (axe flagged it on /services/). */
a.inline-link { color: var(--rust-text); text-decoration: underline; text-underline-offset: 3px; font-weight: 600; }
a.inline-link:hover { color: #6F2C0B; }
/* An inline link inside a navy band kept --rust-text (#8E3D14 on #1B2333 = 1.9:1).
   V3.1e put one there for the first time (the T-5 reviews line in the homepage
   owner band), which axe caught. Same treatment as .section--band .ph. */
.section--band a.inline-link { color: #F6C9A9; }
.section--band a.inline-link:hover { color: #FFF; }

/* ---------- Layout helpers ---------- */
.wrap { max-width: 1140px; margin: 0 auto; padding: 0 24px; }
.wrap--narrow { max-width: 780px; }
.wrap--mid { max-width: 900px; }
/* Section rhythm — V3.1e. Padding up ~14% now that the homepage lost two whole
   sections (the duplicate pricing pitch and the duplicated 12-row list), so the
   page is shorter overall AND breathes more between breaks. */
.section { padding: 96px 0; background: transparent; }
.section--tight { padding: 60px 0; }
.section--band { background: var(--navy); color: var(--band-text); }
.section--band h1, .section--band h2, .section--band h3, .section--band h4 { color: var(--band-text); }
.section--band p { color: var(--band-soft); }
.section--band .eyebrow { background: rgba(255,255,255,0.10); color: #F4C7A6; }
.section--soft { background: var(--paper-2); }
/* .section--edge used to draw a hairline on the section box itself. Every
   .rule-head now carries its own top hairline (see below), so the two stacked up
   ~96px apart. The class is kept (it is referenced in several templates) but is
   now a no-op divider — the rule-head IS the divider. */
.section--edge { border-top: 0; }

@media (max-width: 768px) {
  .section { padding: 64px 0; }
  .section--tight { padding: 46px 0; }
}

/* ---------- Skip link + focus ---------- */
.skip-link { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.skip-link:focus {
  position: fixed; top: 12px; left: 12px; width: auto; height: auto; padding: 10px 16px;
  background: var(--rust); color: #fff; border-radius: 999px; z-index: 1000; font-weight: 700;
}
:focus { outline: none; }
:focus-visible { outline: 2px solid var(--rust); outline-offset: 2px; border-radius: 3px; }

/* ---------- Header / nav ---------- */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(247, 242, 233, 0.93);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hair);
}
.nav-row {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 24px; max-width: 1140px; margin: 0 auto;
}
.brand-wordmark {
  font-family: var(--font-display); font-weight: 800; font-size: 1.12rem;
  color: var(--ink); display: flex; flex-direction: column; line-height: 1;
}
.brand-wordmark .b1 { letter-spacing: -0.015em; }
.brand-wordmark .b2 {
  font-size: 0.6rem; font-weight: 600; letter-spacing: 0.16em; color: var(--rust);
  text-transform: uppercase; margin-top: 4px;
}
/* 7 nav items as of V3.1e (Selling Your Home added). The gap comes down from
   24px and the phone divider's padding from 24px to 16px so the row still fits
   inside the 1092px content column at the 981px breakpoint where the desktop nav
   is still shown. Measured row width at 981px: 921px. */
.nav-links { display: flex; gap: 13px; align-items: center; }
.nav-links a { font-weight: 600; font-size: 0.86rem; color: var(--ink-soft); white-space: nowrap; transition: color 180ms var(--ease); }
.nav-links a:hover, .nav-links a:focus-visible { color: var(--rust); }
.nav-phone {
  font-family: var(--font-mono); font-weight: 600; font-size: 0.85rem; color: var(--ink) !important;
  border-left: 1px solid var(--hair); padding-left: 13px;
}
.nav-cta {
  background: var(--rust); color: #fff !important; padding: 10px 16px; border-radius: 999px;
  font-weight: 700; font-size: 0.86rem;
  transition: transform 160ms var(--ease), box-shadow 160ms var(--ease);
}
.nav-cta:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(181,80,30,0.35); }

/* BREAKPOINT — 980px -> 1080px in V3.1e. The desktop nav is 7 items + the phone
   + the CTA button as of ruling A6, and at 981px that row plus the brand lockup
   overflowed the 1092px content column and produced horizontal scroll. The row
   was tightened first (gap 24->13, link 0.92->0.86rem, phone 0.9->0.85rem, CTA
   padding 18->16px); the breakpoint then moved to where the tightened row
   genuinely fits, measured rather than guessed. Below it the hamburger carries
   the identical seven items. */
.nav-mobile { display: none; }
@media (max-width: 1139px) {
  .nav-links.desktop-only { display: none; }
  .nav-mobile { display: block; }
  .nav-mobile summary {
    list-style: none; cursor: pointer; display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; border-radius: var(--radius);
  }
  .nav-mobile summary::-webkit-details-marker { display: none; }
  .nav-mobile summary::marker { content: ''; }
  .nav-mobile .bars { display: flex; flex-direction: column; gap: 4px; }
  .nav-mobile .bars span { width: 22px; height: 2px; background: var(--ink); display: block; }
  /* ==== THE OPEN MENU HAS TO ESCAPE THE HEADER — two traps, both fixed here.
     (2026-08-24, after "the pages menu does nothing when I tap it" on a real
     phone. The <details>/<summary> was never broken: it opened every time. What
     opened was invisible and untappable, which on a phone is the same bug.)

     TRAP 1 — SIZE. `.site-header` carries `backdrop-filter: blur(10px)`, and a
     filter or backdrop-filter on an ancestor makes that ancestor the containing
     block for its position:fixed descendants (CSS Filter Effects §5). This panel
     lives inside the header, so `inset: 0` resolved against the 73px HEADER box
     instead of the viewport: measured 390x48 at 390x844, i.e. 48px of padding
     and nothing else, with all nine links scrolled out of a box too short to
     show one of them. Height is therefore stated explicitly rather than implied
     by `bottom: 0`, which is correct whichever element wins the containing
     block — the header is in normal flow at the very top of <body>, so its
     padding box starts at viewport y=0 either way, and engines that do not
     apply the blur (Safari before the unprefixed property landed) get the same
     geometry from the viewport.

     TRAP 2 — PAINT ORDER. `header.site-header` and `main` are BOTH
     `position: relative; z-index: 1` (see the two lines under `body` at the top
     of this file) and <main> comes later in the DOM, so main wins the tie and
     paints over the header — taking this panel's `z-index: 60` down with it,
     because that 60 only ever competed inside the header's own stacking
     context. Even at full height the menu rendered UNDER the hero: an
     elementFromPoint at the centre of the FAQ link returned the hero's
     <label>. The header is lifted over main and over the z-index:80 sticky CTA
     bar, but ONLY while the menu is open — the closed state on all 26 pages
     keeps z-index 1, so the hero's `margin-top: -1px` hairline trick and every
     other stacking relationship on the site are untouched.

     Do not "simplify" either of these back to `inset: 0`. ==== */
  .site-header:has(.nav-mobile[open]) { z-index: 90; }
  .nav-mobile[open] .mobile-panel {
    position: fixed; left: 0; right: 0; top: 62px; bottom: auto;
    height: calc(100vh - 62px);
    height: calc(100dvh - 62px);
    background: var(--paper); padding: 24px;
    padding-bottom: calc(24px + env(safe-area-inset-bottom));
    display: flex; flex-direction: column; gap: 18px; z-index: 60; overflow-y: auto;
  }
  .nav-mobile .mobile-panel a { font-size: 1.1rem; font-weight: 700; }
  .nav-mobile .mobile-panel .nav-phone { border-left: none; padding-left: 0; }
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 14px 26px; border-radius: 999px; font-weight: 700; font-size: 1rem; min-height: 44px;
  transition: transform 160ms var(--ease), box-shadow 160ms var(--ease), background 160ms var(--ease), color 160ms var(--ease);
  border: 2px solid transparent; text-align: center;
}
.btn-primary { background: var(--rust); color: #fff; }
.btn-primary:hover, .btn-primary:focus-visible { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(181,80,30,0.32); }
.btn-outline { background: transparent; color: var(--ink); border-color: var(--ink); }
.btn-outline:hover, .btn-outline:focus-visible { background: var(--ink); color: #fff; }
.btn-band-outline { background: transparent; color: var(--band-text); border-color: var(--band-text); }
.btn-band-outline:hover, .btn-band-outline:focus-visible { background: var(--band-text); color: var(--navy); }
.btn-sm { padding: 10px 18px; font-size: 0.9rem; min-height: 40px; }

/* ---------- Eyebrow + section index rules (work-order motif) ---------- */
.eyebrow {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--rust-text); background: var(--rust-soft); padding: 6px 12px; border-radius: 999px;
  margin-bottom: 18px; font-weight: 600;
}
/* ---- THE SECTION HEADER — V3.1e ------------------------------------------
   Was: one 12px mono string ("02 / What I do") next to a hairline. The numeral,
   the separator and the label all rendered at the same tiny size, so a section
   break read as a caption.

   Now: the head is a DIVIDER (full-width hairline across the top of the section,
   with real air under it), and the eyebrow is two ranked parts —

       [ 02 ] | WHAT I DO  ------------------------------------------------
         ^ 24px rust mono   ^ 14.4px uppercase mono, tracked

   Templates that have no numeral (".idx" with plain text, e.g. "The list")
   inherit the label styling from .idx itself and are unaffected. */
.rule-head {
  display: flex; align-items: center; gap: 16px;
  border-top: 1px solid var(--hair);
  padding-top: 20px;
  margin-bottom: 30px;
}
.rule-head .idx {
  display: inline-flex; align-items: center; gap: 12px;
  font-family: var(--font-mono); font-size: 0.9rem; font-weight: 700; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--rust-text); white-space: nowrap;
}
.rule-head .idx-n {
  font-size: 1.5rem; font-weight: 700; letter-spacing: 0; line-height: 1;
  color: var(--rust); padding-right: 12px; border-right: 1px solid var(--hair);
}
.rule-head .line { flex: 1; height: 1px; background: var(--hair); }
.section--band .rule-head { border-top-color: var(--hair-band); }
.section--band .rule-head .line { background: var(--hair-band); }
.section--band .rule-head .idx { color: #F4C7A6; }
.section--band .rule-head .idx-n { color: #F4C7A6; border-right-color: var(--hair-band); }
/* .related already draws its own top rule + padding; don't double it. */
.related .rule-head { border-top: 0; padding-top: 0; }
@media (max-width: 640px) {
  .rule-head { gap: 12px; padding-top: 16px; margin-bottom: 24px; }
  .rule-head .idx { gap: 10px; font-size: 0.82rem; letter-spacing: 0.11em; }
  .rule-head .idx-n { font-size: 1.3rem; padding-right: 10px; }
}
.section-head { max-width: 680px; margin-bottom: 44px; }
.section-head.center { margin-left: auto; margin-right: auto; text-align: center; }
.section-head h2 { margin-bottom: 16px; }

/* ---------- Breadcrumb ---------- */
.crumbs {
  font-family: var(--font-mono); font-size: 0.76rem; letter-spacing: 0.04em;
  color: var(--ink-soft); padding: 18px 0 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
}
.crumbs a { color: var(--ink-soft); }
.crumbs a:hover { color: var(--rust); }
.crumbs .sep { color: var(--rust); }
.crumbs .here { color: var(--ink); font-weight: 600; }

/* ---------- Placeholder chips (client-input markers, intentionally visible) ---------- */
.ph {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: 0.74rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--rust-deep);
  background: repeating-linear-gradient(45deg, rgba(181,80,30,0.07) 0 8px, transparent 8px 16px);
  border: 1px dashed var(--rust); border-radius: 4px; padding: 5px 10px; white-space: nowrap;
}
.ph--price { font-size: 0.9rem; padding: 8px 14px; }
/* .ph is nowrap by default because most chips are two words. The V3.1b chips
   ([WARRANTY — Connor decision pending], [PAYMENT METHODS — Connor]) are long
   enough to force a horizontal scroll at 320px if they can't break, so they opt
   out. Verified in the 320px sweep. */
.ph--long { white-space: normal; text-align: left; }
.section--band .ph { color: #F6C9A9; border-color: #C97A48; }
/* SF-1: the footer is navy but is not .section--band, so the 27 footer chips kept
   --rust-deep on navy = 2.12:1 — the most-repeated placeholder on the site and the
   least legible. Same navy treatment as the band chips: 6.9:1. */
.site-footer .ph { color: #F6C9A9; border-color: #C97A48; background: none; }
.slot {
  border: 1px dashed var(--rust); border-radius: var(--radius-lg); padding: 26px;
  background: repeating-linear-gradient(45deg, rgba(181,80,30,0.05) 0 10px, transparent 10px 20px);
}
.slot h3 { margin-bottom: 8px; }
.slot p { font-size: 0.95rem; }

/* ---------- Hero ---------- */
.hero { background: transparent; padding: 64px 0 56px; position: relative; }
.hero--home { padding: 72px 0 64px; }
.hero-grid { display: grid; grid-template-columns: 1.12fr 0.88fr; gap: 52px; align-items: center; }
@media (max-width: 940px) { .hero-grid { grid-template-columns: 1fr; gap: 36px; } }
.hero h1 { margin-bottom: 18px; }
.hero .lede { margin-bottom: 28px; max-width: 56ch; }
.hero-ctas { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 24px; }
.hero-trust-line {
  font-size: 0.88rem; color: var(--ink-soft); display: flex; align-items: center; gap: 14px;
  flex-wrap: wrap; font-family: var(--font-mono); font-weight: 500;
}
.hero-trust-line span.item { display: inline-flex; align-items: center; gap: 7px; }
.hero-trust-line .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--rust); display: inline-block; }

/* Hero visual panel — CSS-only (no client photos yet) */
.hero-visual {
  background: var(--navy); border-radius: var(--radius-lg); aspect-ratio: 4/5;
  position: relative; overflow: hidden; box-shadow: var(--shadow-card);
}
.hero-visual--square { aspect-ratio: 1/1; }
.hero-visual::before {
  content: ''; position: absolute; inset: 0;
  background-image:
    linear-gradient(90deg, rgba(255,255,255,0.075) 1px, transparent 1px),
    linear-gradient(0deg, rgba(255,255,255,0.075) 1px, transparent 1px);
  background-size: 34px 34px;
}
.hero-visual::after {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 70% 55% at 25% 15%, rgba(181,80,30,0.55), transparent 60%),
    radial-gradient(ellipse 60% 50% at 80% 85%, rgba(181,80,30,0.26), transparent 60%);
}
/* PHOTO SLOT — fail-open, sitewide (V3.1b). Every .hero-visual panel is emitted
   by common.photo_panel() and accepts an <img class="hero-visual-img"
   data-photo-slot> as its FIRST child. The img sits above the grid/wash
   decoration and below the content, so the trade tag and the name stay readable.
   initPhotoSlots() in main.js removes the img if it fails to load and the panel
   falls back to this treatment — no broken-image glyph anywhere on the site. */
.hero-visual-img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1;
}
/* Scrim only exists while a real photo does. If initPhotoSlots() removes a
   failed image, :has() re-evaluates and the scrim disappears with it — the
   fail-open path needs no second code path. */
.hero-visual:has(.hero-visual-img) .hero-visual-content {
  background: linear-gradient(180deg, rgba(18,23,36,0.62) 0%, rgba(18,23,36,0.18) 42%, rgba(18,23,36,0.80) 100%);
}
.hero-visual-content {
  position: relative; z-index: 2; height: 100%;
  display: flex; flex-direction: column; justify-content: space-between; padding: 26px; color: var(--band-text);
}
.hero-visual-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.hero-visual-tag {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: #F4C7A6; border: 1px solid rgba(244,199,166,0.4); padding: 5px 9px; border-radius: 4px;
}
.hero-visual-content .initials {
  width: 62px; height: 62px; border-radius: 50%; background: var(--rust); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-weight: 800; font-size: 1.3rem; margin-bottom: 14px;
}
.hero-visual-content .name { font-family: var(--font-display); font-weight: 800; font-size: 1.1rem; }
.hero-visual-content .role { font-size: 0.84rem; color: var(--band-soft); margin-top: 2px; }

/* STOCK PHOTO DISCLOSURE (V3.1c, 2026-08-02). Renders in place of the
   initials/name/role identity block whenever photo_panel() is passed
   stock=True — see common.py. Sits inside the same scrim as the identity
   block it replaces, so contrast against any photo underneath is guaranteed
   the same way. Kept short and constrained so it never overruns the panel
   at any width the hero-visual grid stacks to (320px included).
   Fail-open: both the chip and the caption only make sense once a photo is
   actually showing, so they're gated by the same :has(.hero-visual-img) check
   as the scrim above. If initPhotoSlots() removes a failed stock image, the
   "Stock photo" flag and the disclosure sentence disappear with it — the
   panel reverts to the bare trade-tag placeholder instead of describing a
   photo that isn't there. */
.ph--stock { color: #F6C9A9; border-color: #C97A48; background: none; display: none; }
.hero-visual:has(.hero-visual-img) .ph--stock { display: inline-block; }
.stock-disclosure {
  font-size: 0.74rem; line-height: 1.4; max-width: 30ch; color: var(--band-soft); display: none;
}
.hero-visual:has(.hero-visual-img) .stock-disclosure { display: block; }

/* ---------- Cards ---------- */
.grid { display: grid; gap: 22px; }
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 960px) { .grid--4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 860px) { .grid--3, .grid--2 { grid-template-columns: 1fr; } }
@media (max-width: 560px) { .grid--4 { grid-template-columns: 1fr; } }

.card {
  background: var(--paper-3); border: 1px solid var(--hair-2); border-radius: var(--radius-lg);
  padding: 26px; box-shadow: var(--shadow-card); position: relative;
}
/* A card heading is a card heading whatever its LEVEL. Several cards moved from
   <h3> to <h2> in V3.1e to fix heading-order (a card was the first heading after
   the page <h1> on /contact/, /contact/thank-you/ and /estimate/); the level is a
   document-outline fact and must not become a 41.6px type decision. */
.card h2, .card h3 { font-size: 1.2rem; font-weight: 700; margin-bottom: 10px; }
.card h2 { letter-spacing: -0.01em; }
.card p { font-size: 0.97rem; }
.card .icon-chip {
  width: 42px; height: 42px; border-radius: 10px; background: var(--rust-soft); color: var(--rust-text);
  display: flex; align-items: center; justify-content: center; margin-bottom: 14px;
  font-family: var(--font-mono); font-weight: 600; font-size: 0.9rem;
}
.card--ticket { padding-top: 30px; }
.card--ticket::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: var(--rust); border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.card--ticket .tick-idx {
  position: absolute; top: 10px; right: 16px;
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.1em; color: var(--rust);
}

/* ---------- Service index (hub): full-width numbered rows ---------- */
/* P-1: the rows ran the full 1092px column with the description capped at 60ch,
   leaving a ~400px void before the mono "View ->" pinned to the right edge. The
   index is capped at 920px so the affordance sits close to the text it belongs to.
   (The whole row is a link; the arrow is a hint, not the target.) */
.svc-index { border-top: 1px solid var(--hair); margin-top: 8px; max-width: 920px; }
.svc-row {
  display: grid; grid-template-columns: 68px 1fr auto; gap: 20px; align-items: center;
  padding: 24px 8px; border-bottom: 1px solid var(--hair);
  /* P-7: the hover used to animate padding-left, which is a LAYOUT property —
     12 rows re-laying out on every mouse move. Same 16px slide, composited. */
  transition: background 180ms var(--ease), transform 180ms var(--ease);
}
.svc-row:hover, .svc-row:focus-visible { background: rgba(181,80,30,0.055); transform: translateX(16px); }
.svc-row .num {
  font-family: var(--font-mono); font-weight: 600; font-size: 1.5rem; color: var(--rust); line-height: 1;
}
.svc-row h3 { margin-bottom: 5px; }
.svc-row p { font-size: 0.95rem; margin: 0; max-width: 60ch; }
.svc-row .go {
  font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--rust); white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;
}
.svc-row:hover .go { color: var(--rust-deep); }
@media (max-width: 700px) {
  .svc-row { grid-template-columns: 44px 1fr; gap: 14px; padding: 20px 4px; }
  .svc-row .num { font-size: 1.15rem; }
  .svc-row .go { grid-column: 2; }
}

/* ---------- Process ladder (Ace "Our Process" block, our voice) ---------- */
.ladder { margin-top: 8px; border-left: 2px solid var(--rust-soft); padding-left: 0; }
.ladder-step { position: relative; padding: 0 0 30px 34px; }
.ladder-step:last-child { padding-bottom: 0; }
.ladder-step::before {
  content: attr(data-step); position: absolute; left: -13px; top: 0;
  width: 24px; height: 24px; border-radius: 50%; background: var(--rust); color: #fff;
  font-family: var(--font-mono); font-size: 0.72rem; font-weight: 600;
  display: flex; align-items: center; justify-content: center;
}
.ladder-step h3 { margin-bottom: 6px; }
.ladder-step p { font-size: 0.97rem; }

/* ---------- Spec rows (dotted-leader "what you get" lists) ---------- */
.specs { border-top: 1px solid var(--hair); }
.spec {
  display: flex; align-items: baseline; gap: 10px; padding: 13px 0; border-bottom: 1px solid var(--hair);
}
.spec .k { font-weight: 600; color: var(--ink); font-size: 0.97rem; }
.spec .lead { flex: 1; border-bottom: 1px dotted rgba(27,35,51,0.28); transform: translateY(-4px); min-width: 18px; }
.spec .v { font-family: var(--font-mono); font-size: 0.88rem; color: var(--ink-soft); text-align: right; }
.section--band .spec { border-color: var(--hair-band); }
.section--band .spec .k { color: var(--band-text); }
.section--band .spec .v { color: var(--band-soft); }
.section--band .spec .lead { border-color: rgba(255,255,255,0.28); }

.checklist { display: grid; gap: 11px; margin-top: 6px; }
.checklist li {
  position: relative; padding-left: 26px; color: var(--ink-soft); font-size: 0.97rem;
}
.checklist li::before {
  content: ''; position: absolute; left: 0; top: 8px; width: 12px; height: 7px;
  border-left: 2px solid var(--rust); border-bottom: 2px solid var(--rust); transform: rotate(-45deg);
}
.section--band .checklist li { color: var(--band-soft); }

/* ---------- Package cards ---------- */
.pkg {
  background: var(--paper-3); border: 1px solid var(--hair); border-radius: var(--radius-lg);
  padding: 0; overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-card);
}
.pkg-head { padding: 24px 26px 18px; border-bottom: 1px solid var(--hair); background: rgba(181,80,30,0.045); }
.pkg-head .kicker {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--rust); display: block; margin-bottom: 8px;
}
.pkg-head h3 { font-size: 1.35rem; margin-bottom: 6px; }
.pkg-time { font-family: var(--font-mono); font-size: 0.85rem; color: var(--ink-soft); }
.pkg-body { padding: 22px 26px; flex: 1; }
.pkg-foot { padding: 18px 26px 24px; border-top: 1px solid var(--hair); display: flex; flex-direction: column; gap: 12px; }
.pkg-foot .price-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.pkg-foot .price-label {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-soft);
}

/* ---------- Related content module ---------- */
.related { border-top: 1px solid var(--hair); padding-top: 34px; }
.related-links { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 6px; }
.related-links a {
  display: inline-flex; align-items: center; gap: 7px; padding: 9px 15px;
  border: 1px solid var(--hair); border-radius: 999px; background: var(--paper-3);
  font-size: 0.9rem; font-weight: 600; color: var(--ink);
  transition: border-color 160ms var(--ease), color 160ms var(--ease), transform 160ms var(--ease);
}
.related-links a:hover { border-color: var(--rust); color: var(--rust); transform: translateY(-1px); }
.related-links a .arw { font-family: var(--font-mono); color: var(--rust); font-size: 0.8rem; }

/* ---------- Area list ---------- */
.area-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.area-list li {
  font-family: var(--font-mono); font-size: 0.82rem; color: var(--ink-soft);
  border: 1px solid var(--hair); border-radius: 4px; padding: 6px 11px; background: var(--paper-3);
}
.section--band .area-list li { color: var(--band-soft); border-color: var(--hair-band); background: rgba(255,255,255,0.04); }

/* ---------- FAQ accordion ---------- */
.faq-item { border-bottom: 1px solid var(--hair); }
.faq-item:first-child { border-top: 1px solid var(--hair); }
.faq-q {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 16px;
  background: none; border: none; text-align: left; padding: 21px 4px;
  font-family: var(--font-display); font-weight: 700; font-size: 1.03rem; color: var(--ink);
}
.faq-q:hover { color: var(--rust); }
.faq-chevron { transition: transform 220ms var(--ease); flex-shrink: 0; }
.faq-q[aria-expanded="true"] .faq-chevron { transform: rotate(180deg); }
.faq-a { padding: 0 4px 21px; }
.faq-a[hidden] { display: none; }

/* ---------- Forms ---------- */
.form-card {
  background: var(--paper-3); border: 1px solid var(--hair); border-radius: var(--radius-lg);
  padding: 30px; box-shadow: var(--shadow-card);
}
@media (max-width: 560px) { .form-card { padding: 22px 18px; } }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
@media (max-width: 640px) { .form-row { grid-template-columns: 1fr; gap: 0; } }
.form-field { margin-bottom: 18px; }
.form-field label { display: block; font-weight: 700; font-size: 0.88rem; margin-bottom: 7px; color: var(--ink); }
.form-field label .req { color: var(--rust); font-family: var(--font-mono); }
.form-field .hint { font-size: 0.81rem; color: var(--ink-soft); margin-top: 6px; }
.form-field input, .form-field textarea, .form-field select {
  width: 100%; padding: 13px 14px; border: 1.5px solid rgba(27,35,51,0.18); border-radius: var(--radius);
  background: #fff; font: inherit; color: var(--ink);
  transition: border-color 150ms var(--ease), box-shadow 150ms var(--ease);
}
.form-field input:focus-visible, .form-field textarea:focus-visible, .form-field select:focus-visible {
  border-color: var(--rust); box-shadow: 0 0 0 3px rgba(181,80,30,0.14); outline: none;
}
.form-field.has-error input, .form-field.has-error textarea, .form-field.has-error select { border-color: #B3261E; }
.field-error { display: none; color: #B3261E; font-size: 0.82rem; font-weight: 600; margin-top: 6px; }
.form-field.has-error .field-error { display: block; }
.form-check { display: flex; align-items: flex-start; gap: 11px; margin-bottom: 16px; }
.form-check input[type="checkbox"] { width: 20px; height: 20px; margin-top: 2px; flex-shrink: 0; accent-color: var(--rust); }
.form-check label { font-weight: 400; font-size: 0.86rem; color: var(--ink-soft); line-height: 1.55; }
.consent-block { border-top: 1px solid var(--hair); margin-top: 24px; padding-top: 22px; }
.consent-block .consent-title {
  font-family: var(--font-mono); font-size: 0.74rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--rust); margin-bottom: 14px; display: block; font-weight: 600;
}
/* Server-decided states. .form-alert is the banner /api/lead bounces a rejected
   submission back into (?error=missing / ?error=send); .heldup-note is the
   honest line on the thank-you page when the CRM write failed (?heldup=1).
   Both ship hidden and are unhidden by initServerFormState() in main.js. */
.form-alert {
  margin: 0 0 22px; padding: 13px 15px; border-radius: var(--radius);
  border: 1.5px solid #B3261E; background: rgba(179,38,30,0.06);   /* P-6: no left stripe */
  color: #8E1D17; font-weight: 600; font-size: 0.9rem; line-height: 1.5;
}
.form-alert[hidden] { display: none; }
.heldup-note {
  margin-top: 18px; padding: 13px 15px; border-radius: var(--radius);
  border: 1.5px solid var(--rust); background: rgba(181,80,30,0.07);  /* P-6: no left stripe */
  color: var(--rust-deep); font-weight: 600; font-size: 0.92rem; line-height: 1.55;
}
.heldup-note[hidden] { display: none; }
.form-status { margin-top: 14px; font-weight: 600; font-size: 0.94rem; min-height: 1.2em; }
.form-status.is-error { color: #B3261E; }
.form-status.is-ok { color: var(--rust-deep); }
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---------- Sticky CTA bar ---------- */
/* A6 (2026-08-24) — BREAKPOINT 760px -> 1139px. There was a dead band between
   761px and 1139px with NO visible CTA anywhere on the page: .nav-links is
   display:none below 1140px (it takes the phone link AND the rust estimate pill
   into the hamburger with it) and this bar did not start until 760px. On a
   tablet or a small laptop the only chrome was a hamburger. The two numbers now
   meet: the bar covers everything the desktop nav does not.
   Chosen over adding a compact CTA pair to the header on all 23 pages because
   it is one line of CSS against 23 markup edits, and it reuses a component the
   site already ships rather than introducing a new one. */
.sticky-cta { display: none; }
@media (max-width: 1139px) {
  .sticky-cta {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
    background: var(--navy); padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); gap: 10px;
  }
  .sticky-cta a { flex: 1; text-align: center; }
  body { padding-bottom: 74px; }

  /* A1 — the homepage bar now prints the digits, and "Call or text
     (850) 266-7764" does not fit beside a second button on a 390px phone.
     .btn is inline-flex with no wrap, so the label and the <span class="mono">
     were being held apart as two flex columns. Let them wrap into two centred
     lines instead, and give the phone the larger share of the bar — it is the
     only zero-navigation conversion path on the site, so it wins the ties. */
  .sticky-cta a[href^="tel:"] { flex: 1.7; flex-wrap: wrap; gap: 0 7px; line-height: 1.22; }
  .sticky-cta a[href^="tel:"] .mono { font-size: 0.92em; }
  .sticky-cta--single a[href^="tel:"] { flex: 1; }
}
@media (max-width: 400px) {
  .sticky-cta { padding-left: 10px; padding-right: 10px; gap: 8px; }
  .sticky-cta a { padding-left: 10px; padding-right: 10px; font-size: 0.86rem; }
}

/* ---------- Footer ---------- */
.site-footer { background: var(--navy); color: var(--band-soft); padding: 54px 0 26px; }
.footer-grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1.2fr; gap: 30px; margin-bottom: 34px; }
@media (max-width: 900px) { .footer-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer-grid { grid-template-columns: 1fr; } }
/* The four footer column headings are <h2> as of V3.1e — they were <h4> after an
   <h3> (or after the <h1> on /faq/), which is the heading-order violation axe
   reported on every page. Visual weight is unchanged. */
.site-footer h2 { font-family: var(--font-display); font-weight: 800; color: var(--band-text); font-size: 0.92rem; line-height: 1.1; letter-spacing: -0.01em; margin-bottom: 13px; }
.site-footer a { color: var(--band-soft); font-size: 0.9rem; }
.site-footer a:hover, .site-footer a:focus-visible { color: #fff; }
.site-footer p { color: var(--band-soft); font-size: 0.9rem; }
.footer-links { display: flex; flex-direction: column; gap: 9px; }
.footer-addr { font-style: normal; font-size: 0.9rem; line-height: 1.7; color: var(--band-soft); }
.footer-bottom {
  border-top: 1px solid var(--hair-band); padding-top: 20px;
  display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px;
  font-size: 0.8rem; color: #8E96A8;
}
.footer-bottom a { color: #8E96A8; font-size: 0.8rem; }

/* ---------- Legal pages ---------- */
.legal-content h2 { margin-top: 38px; margin-bottom: 13px; font-size: 1.28rem; }
.legal-content h2:first-child { margin-top: 0; }
.legal-content p, .legal-content li { margin-bottom: 13px; color: var(--ink-soft); }
.legal-content ul, .legal-content ol { padding-left: 22px; margin-bottom: 13px; }
.legal-content ul li, .legal-content ol li { list-style: revert; }
.legal-content strong { color: var(--ink); }
.legal-content a { color: var(--rust); text-decoration: underline; text-underline-offset: 3px; }
.legal-updated {
  font-family: var(--font-mono); font-size: 0.84rem; color: var(--rust);
  margin-bottom: 30px; display: inline-block;
}

/* ---------- Reveal-on-scroll (quiet baseline) ---------- */
.reveal { opacity: 0; transform: translateY(12px); transition: opacity 350ms var(--ease), transform 350ms var(--ease); }
.reveal.is-visible { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; transition: none; } }

/* ---------- Utility ---------- */
.text-center { text-align: center; }
.mt-0 { margin-top: 0; }
.mt-sm { margin-top: 14px; }
.mt-md { margin-top: 26px; }
.mt-lg { margin-top: 40px; }
.mb-sm { margin-bottom: 14px; }
.mb-md { margin-bottom: 26px; }
.mb-lg { margin-bottom: 40px; }
.note {
  /* P-6: the colored left stripe is the most recognizable generated-UI tell
     there is, and this build avoids the rest of that family. The note now reads
     as an indented work-order remark with a mono dagger, which matches the
     ticket conceit the rest of the page is built on. */
  position: relative; font-size: 0.9rem; color: var(--ink-soft);
  padding-left: 26px; max-width: 66ch;
}
.note::before {
  content: '\2020'; position: absolute; left: 0; top: 0;
  font-family: var(--font-mono); font-weight: 600; color: var(--rust);
}
.section--band .note::before { color: #F4C7A6; }
.stack-sm > * + * { margin-top: 10px; }
.flex-ctas { display: flex; gap: 14px; flex-wrap: wrap; }


/* ==========================================================================
   V3.1 LAYER — brand emblem, Ace-mirror hero, service picker, opening intro
   Added 2026-08-02. Everything below is additive: no V3 rule above this line
   was edited, so reverting V3.1 is a matter of deleting from here down plus
   the four initXxx() calls in main.js.
   ========================================================================== */

/* ---------- EMBLEM + LOCKUP -----------------------------------------------
   ONE custom property (--em, the emblem width) drives the entire lockup; every
   other dimension is a fixed ratio of it. That is what makes it a true logo —
   it scales as one rigid unit, and it is why the opening animation can land its
   big centered lockup on the hero lockup with a MEASURED scale factor rather
   than an eyeballed guess. Do not hard-code px inside this block.
   The shield plate is inline SVG with no glyphs; "FAS" is real HTML text layered
   over it, so the monogram renders with the same font everywhere.
   -------------------------------------------------------------------------- */
.emblem { position: relative; width: var(--em, 180px); aspect-ratio: 120 / 140; flex: 0 0 auto; }
.emblem svg { width: 100%; height: 100%; }
.emblem-fas {
  position: absolute; left: 0; right: 0; top: 61%;
  font-family: var(--font-display); font-weight: 800; letter-spacing: 0.01em;
  text-align: center; color: var(--navy); line-height: 1;
  font-size: calc(var(--em, 180px) * 0.285);
  transform: translateY(-50%); pointer-events: none;
}
.emblem-fas i { font-style: normal; display: inline-block; }

/* HORIZONTAL BUILD — owner-revised 2026-08-02 (V3.1a), refined V3.1c.
   Shield left; one left-aligned stack to its right: FORTIFIED, the rust chalk
   line directly beneath it, HANDYMAN SERVICES beneath the line. One tier, one
   tight unit. This is the header lockup's arrangement at hero scale.

   ---- V3.1c VERTICAL ALIGNMENT (--cap-fix) --------------------------------
   The cap-height top of the "F" in FORTIFIED is aligned with the TOP OF THE
   LIGHT HORIZONTAL STRIPE inside the shield — the sand band between the rust
   gable and the rust hairline. In the variant-B plate that stripe's top edge is
   the roof triangle's base, at viewBox y=36 of a 0 0 120 140 box. The SVG fills
   the emblem box exactly (aspect-ratio 120/140, no letterboxing), so:

       stripe top, from the emblem box top =  36/120 * --em  =  0.30 * --em

   Inside the word's own box the cap top sits below the box top by
   half-leading + fontAscent - capHeight. That distance is MEASURED OFF PIXELS,
   not taken from canvas metrics: Chrome's measureText('F').actualBoundingBoxAscent
   reports 0.735 of the font-size for Manrope 800, but the raster's cap height is
   0.71995 — trusting the canvas number puts the word ~0.9px low at 1280. So the
   figure below comes from an 8-sample sweep of the real element blown up to
   --em 500-920px, reading the F's flat top arm at the 50%-coverage row:

       cap top below the word's box top = 0.0914 of the font-size
       the word is 0.52 * --em          = 0.0914 * 0.52 = 0.047528 * --em

       --cap-fix = 0.30 - 0.047528 = 0.252472   (rounded to 0.2525)

   Cross-check, analytic: line-height 0.86 with Manrope 800's own ascent 1.066
   and descent 0.300 puts the baseline 0.813 below the box top; minus the 0.71995
   raster cap height gives 0.09305 — 0.0016 off the measured mean, i.e. inside
   the baseline's device-pixel snapping. Both routes agree; the pixel sweep wins
   because it is what the visitor actually sees.

   V3.1a aligned to the shield's OUTER TOP EDGE and therefore pulled the stack
   UP (margin-top was negative). V3.1c aligns to the stripe and pushes it DOWN,
   so the sign flipped. FORTIFIED's SIZE is unchanged at 0.52 * --em.
   The lockup's total height is still set by the emblem (1.1667 * --em), not by
   the stack (which now bottoms out at ~1.04 * --em), so nothing below the
   lockup moved: the hero's vertical rhythm is untouched.
   -------------------------------------------------------------------------- */
.lockup {
  --cap-fix: 0.2525;
  display: flex; align-items: flex-start;
  gap: calc(var(--em, 180px) * 0.125);
}
.lockup-stack {
  display: flex; flex-direction: column; align-items: flex-start;
  margin-top: calc(var(--em, 180px) * var(--cap-fix));
}
.lockup-word {
  font-family: var(--font-display); font-weight: 800; letter-spacing: -0.025em;
  line-height: 0.86; color: var(--paper); font-size: calc(var(--em, 180px) * 0.52);
  white-space: nowrap;
}
/* the chalk line spans the stack, whose width is set by FORTIFIED (the widest
   child), so the rule ends exactly where the word ends */
.lockup-rule {
  position: relative; align-self: stretch;
  height: max(2px, calc(var(--em, 180px) * 0.017)); background: var(--rust);
  margin-top: calc(var(--em, 180px) * 0.062);
}
/* ---- V3.1c SUB-LINE: width-matched to FORTIFIED, tucked up under the rule ----
   The sub-line is a flex row with justify-content:space-between and
   align-self:stretch, so it fills the stack — and the stack's width is set by
   FORTIFIED. "Sub-line width == FORTIFIED width" is therefore a PROPERTY OF THE
   LAYOUT, exact at every --em, not a number to re-tune when the font or the
   string changes. There is no letter-spacing on this element any more: the
   distributed gaps ARE the tracking.
   Font-size is chosen so those gaps read as deliberate tracking rather than as
   a scattered line. IBM Plex Mono advances 0.6em/char; 17 chars gives a natural
   width of 10.2 * font-size, and FORTIFIED measures 2.4781 * --em. At
   0.175 * --em the natural width is 1.785 * --em, leaving 0.693 * --em spread
   over 16 gaps = 0.0433 * --em each = 0.247 of the sub's own font-size — i.e.
   an effective ~0.25em tracking.
   HARD CONSTRAINT: keep font-size below 0.2429 * --em. Above that the sub's
   max-content exceeds FORTIFIED's width, the sub starts defining the stack
   width instead, and the chalk line stops matching the word. */
.lockup-sub {
  display: flex; align-self: stretch; justify-content: space-between;
  font-family: var(--font-mono); font-weight: 500; text-transform: uppercase;
  letter-spacing: 0; text-indent: 0;
  color: var(--rust-soft); font-size: calc(var(--em, 180px) * 0.175);
  line-height: 1.1;
  margin-top: calc(var(--em, 180px) * 0.026);
  white-space: nowrap;
}
.lockup-sub i { font-style: normal; display: inline-block; }

/* ---------- Compact lockup: header + footer ---------- */
.brand-lockup { display: flex; align-items: center; gap: 11px; color: var(--ink); }
.brand-lockup .emblem--nav { --em: 36px; }
.brand-lockup .bl-text { display: flex; flex-direction: column; line-height: 1; }
.brand-lockup .bl1 {
  font-family: var(--font-display); font-weight: 800; font-size: 1.06rem; letter-spacing: -0.015em;
}
.brand-lockup .bl2 {
  font-family: var(--font-mono); font-size: 0.58rem; font-weight: 600; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--rust); margin-top: 4px;
}
.brand-lockup--dark { color: var(--band-text); }
.brand-lockup--dark .bl2 { color: var(--rust-soft); }
@media (max-width: 420px) {
  .brand-lockup .emblem--nav { --em: 30px; }
  .brand-lockup .bl1 { font-size: 0.95rem; }
  .brand-lockup .bl2 { font-size: 0.52rem; letter-spacing: 0.14em; }
}

/* ==========================================================================
   ACE-MIRROR HERO  (homepage only)
   Full-bleed dark field, photo panel left-weighted, emblem + one headline +
   one action center-right. The H1 is deliberately set at body-copy scale — the
   emblem is the hero. It is still a real <h1> with the full headline.
   ========================================================================== */
.hero-ace {
  position: relative; display: flex;
  /* The mockup's hero was full-viewport with a transparent overlay header. The
     production site keeps its sticky sand header on all 23 pages for consistency,
     so the hero subtracts the header band — otherwise the trust strip sits a full
     header-height below the fold at every viewport. --hdr is the measured height
     of .nav-row (14px + 44px + 14px) plus its 1px border. */
  --hdr: 73px;
  /* --peek: how much of the trust strip is pulled ABOVE the fold.
     V3.1c sized the hero to consume the viewport exactly (min-height =
     100svh - header), which put the trust strip top at 899px on a 1280x900
     screen — one pixel below the cut, so licensed & insured / owner-operated /
     Pensacola local / the phone number all sat below the first screen (QA
     design SB-3). Subtracting --peek lifts the whole strip into the first
     screen AND leaves a cut edge that signals scrollability. The emblem is not
     touched: hero CONTENT measures ~560px at 1280, well inside the reduced
     box, so this trims dead space, not the mark. */
  --peek: 88px;
  min-height: min(820px, calc(100svh - var(--hdr) - var(--peek)));
  background: var(--navy); overflow: hidden; isolation: isolate;
  margin-top: -1px;
}

/* PHOTO PANEL — graceful degradation (locked decision #4).
   No <img> ships, so this IS the panel: textured navy, work-hatch, crop marks.
   An <img data-photo-slot> dropped in later paints over it; if that image fails
   to load main.js removes it and the visitor lands back on this treatment
   rather than a broken-image glyph. */
.hero-photo {
  position: absolute; inset: 0 auto 0 0; width: clamp(300px, 46vw, 760px); z-index: 0;
  background: linear-gradient(160deg, #26314A 0%, #1C2436 55%, #151B29 100%);
  overflow: hidden;
}
.hero-photo::before {
  content: ''; position: absolute; inset: 0;
  background-image: repeating-linear-gradient(-38deg, rgba(247,242,233,0.085) 0 2px, transparent 2px 13px);
}
.hero-photo::after {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 62% 48% at 34% 26%, rgba(181,80,30,0.42), transparent 66%),
    radial-gradient(ellipse 70% 60% at 74% 92%, rgba(181,80,30,0.16), transparent 64%),
    linear-gradient(90deg, transparent 52%, var(--navy) 99%);
}
.hero-photo-img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1;
}
.photo-marks { position: absolute; inset: 22px; z-index: 2; pointer-events: none; }
.photo-marks i { position: absolute; width: 26px; height: 26px; border: 2px solid rgba(247,242,233,0.30); }
.photo-marks i:nth-child(1) { top: 0; left: 0; border-right: 0; border-bottom: 0; }
.photo-marks i:nth-child(2) { top: 0; right: 0; border-left: 0; border-bottom: 0; }
.photo-marks i:nth-child(3) { bottom: 0; left: 0; border-right: 0; border-top: 0; }
.photo-marks i:nth-child(4) { bottom: 0; right: 0; border-left: 0; border-top: 0; }
.photo-label {
  position: absolute; z-index: 3;
  left: clamp(20px, 3.4vw, 46px); bottom: clamp(20px, 3.4vw, 46px);
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.10em; color: var(--paper);
  background: rgba(18,23,36,0.72); border: 1px dashed rgba(247,242,233,0.42);
  padding: 9px 14px; border-radius: var(--radius);
  max-width: min(84%, 340px); line-height: 1.4;
}
.photo-label .ph { color: #F6C9A9; border-color: #C97A48; background: none; }
.photo-label b {
  display: block; color: var(--rust-soft); font-weight: 500; font-size: 0.64rem;
  letter-spacing: 0.16em; margin-top: 5px;
}

.hero-ace-inner {
  position: relative; z-index: 2; margin-left: auto;
  width: min(100%, 72ch); flex: 0 1 auto;
  /* vertical padding tightened with the --peek change above (was 8vh/7vh with
     110/80 caps) so the reduced hero box does not have to fight its own gutters
     on short viewports; horizontal padding unchanged. */
  padding: clamp(44px, 6vh, 84px) clamp(20px, 5vw, 64px) clamp(38px, 5vh, 62px);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; color: var(--paper);
}
/* The center-right column is 58% of the hero — but NOT below 640px of it. At
   exactly 1000px the old flat `width:58%` was NARROWER (580px) than the 72ch the
   rule replaced (~640px), so the column pinched inward at the breakpoint; with
   the V3.1c H1 two steps larger that pinch wrapped "Handyman" onto a second line
   between 1000 and 1070px. max() removes the discontinuity: 640px wins until
   1104px, 58% wins above it, and the two meet exactly. */
@media (min-width: 1000px) {
  .hero-ace-inner { width: max(58%, 640px); margin-right: clamp(0px, 3vw, 60px); }
}

/* THE EMBLEM IS THE HERO — one variable sizes the whole lockup.
   Measured, not estimated: the horizontal build is 3.6028 x --em wide
   (1 emblem + 0.125 gap + FORTIFIED at 2.4778 x --em) and 1.1667 x --em tall
   (the emblem sets the height; even after the V3.1c cap-fix push the stack
   bottoms out at 0.997 x --em, 1.023 on the mobile ratio, so it never reaches
   the emblem and the lockup's box did NOT grow). Against ~3.48x for
   the old stacked build, so the clamp came down to keep the unit inside the hero
   content box at every viewport from 320px to 2560px. */
.hero-lockup { --em: clamp(88px, 10.4vw, 168px); }

/* The H1 still sits BELOW the emblem in the visual hierarchy — the emblem is the
   hero — but V3.1c takes it off body-copy scale, where the owner read it as
   undersized against the enlarged lockup. It is a real <h1> carrying the full
   headline, so this is a visual-weight decision, not a markup one.

   ---- V3.1c TYPE BUMP -----------------------------------------------------
   H1 up TWO steps on a 1.125 modular scale (x1.2656):
       min  1.08rem -> 1.37rem   (x1.2685)
       max  1.74rem -> 2.20rem   (x1.2644)
       mid  1.28vw + 0.6rem -> 1.62vw + 0.76rem  (both terms x1.266)
   The sub-line goes up ONE step (x1.125) — half the H1's move — so the gap
   between them WIDENS and the sub stays clearly subordinate.
   max-width 34ch -> 42ch because at the new size 34ch clipped the line short of
   its natural width and wrapped "Handyman" onto its own line.
   Swept in 20px steps from 320px to 2560px: ONE LINE from 440px up, everywhere.
   At 420px and below text-wrap:balance splits it evenly onto two, on purpose.

   ---- V3.1d HIERARCHY FIX (QA design SB-2) --------------------------------
   The V3.1c size still measured 32.9px @1280 against 36px H2s on the same page,
   so the homepage ranked its own thesis below its section headings. Retargeted
   to land ON the 36-42px window the QA asked for, by two fixed anchor points:
       @390px  -> 28.0px   (was 21.9)
       @1280px -> 40.0px   (was 32.9, H2 is 36)
   which is exactly clamp(1.75rem, 1.35vw + 1.42rem, 2.6rem); the max caps at
   41.6px around 1420px so it never crosses 42.
   The EMBLEM IS UNTOUCHED and stays dominant: --em is 133px @1280, so FORTIFIED
   still renders ~103px cap-height against a 40px H1 — a 2.6x lead.

   ---- V3.1e SECTION-BREAK PASS --------------------------------------------
   H2 moved to 41.6px @1280, so a 40px home H1 would have been outranked by its
   own section headings again (the exact defect SB-2 fixed). Retargeted on the
   same two-anchor method:
       @390px  -> 31.0px   (was 28.0)
       @1280px -> 48.0px   (was 40.0)
   = clamp(1.9rem, 1.91vw + 1.47rem, 3.1rem), capping at 49.6px around 1365px.
   The emblem is STILL untouched and still dominant: --em 133px @1280 puts
   FORTIFIED at ~103px cap height against a 48px H1, a 2.15x lead. */
.hero-ace h1 {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.9rem, 1.91vw + 1.47rem, 3.1rem);
  line-height: 1.2; letter-spacing: -0.014em; color: var(--paper);
  max-width: 42ch; margin: clamp(24px, 3.2vh, 38px) auto 0; text-wrap: balance;
}
.hero-ace-sub {
  font-size: clamp(1.04rem, 0.56vw + 0.88rem, 1.19rem);
  color: #BFC5D2; max-width: 46ch; margin: 13px auto 0;
}

/* ---------- THE ONE ACTION: service picker ---------- */
.hero-action { width: min(100%, 600px); margin: clamp(26px, 3.6vh, 44px) auto 0; }
.action-label {
  display: block; font-family: var(--font-display); font-weight: 700;
  font-size: 1.02rem; color: var(--paper); margin-bottom: 11px;
}
/* V3.3 (2026-08-24) — phone is now the only required field in the hero
   capture form; this marks the job-description label as optional without
   introducing an asterisk convention the rest of the build doesn't use. */
.action-optional {
  font-family: var(--font-body); font-weight: 400; font-size: 0.86rem;
  color: rgba(247,242,233,0.6);
}
.action-form { display: flex; gap: 10px; align-items: stretch; }
.combo { position: relative; flex: 1 1 auto; min-width: 0; }
#svcInput {
  width: 100%; height: 60px; padding: 0 16px; border-radius: var(--radius);
  border: 2px solid rgba(247,242,233,0.24); background: var(--paper-3);
  color: var(--ink); font-size: 1.02rem; font-family: var(--font-body);
}
/* P-10: #8A9099 on --paper-3 measured 3.02:1. #6B7280 takes it to 4.71:1. */
#svcInput::placeholder { color: #6B7280; }
/* P-9: this was the ONLY interactive element on the site without the 2px solid
   ring — it substituted a 25%-opacity glow that vanished against the navy hero.
   Keep the glow, add the ring. */
#svcInput:focus-visible { border-color: var(--rust); box-shadow: 0 0 0 4px rgba(181,80,30,0.25); outline: 2px solid var(--rust); outline-offset: 2px; }
#svcInput:focus:not(:focus-visible) { border-color: var(--rust); box-shadow: 0 0 0 4px rgba(181,80,30,0.25); outline: none; }
.btn-go {
  height: 60px; flex: 0 0 auto; padding: 0 clamp(20px, 2.6vw, 34px);
  background: var(--rust); color: #fff; border: 0; border-radius: var(--radius);
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(0.94rem, 0.9vw + 0.5rem, 1.1rem); white-space: nowrap; cursor: pointer;
  transition: background 160ms var(--ease), transform 160ms var(--ease), box-shadow 160ms var(--ease);
}
.btn-go:hover { background: #C75C24; transform: translateY(-2px); box-shadow: 0 12px 26px rgba(181,80,30,0.38); }
.btn-go:active { transform: translateY(0); }

.sugg {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: 40;
  background: var(--paper-3); border: 1px solid rgba(27,35,51,0.16); border-radius: var(--radius);
  box-shadow: 0 20px 44px rgba(9,12,20,0.48);
  max-height: 322px; overflow-y: auto; text-align: left; padding: 5px; list-style: none;
}
.sugg[hidden] { display: none; }
.sugg li {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 11px 12px; border-radius: 4px; color: var(--ink); cursor: pointer;
  font-size: 0.98rem; line-height: 1.3;
}
.sugg li .cat {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-soft); flex: 0 0 auto;
}
.sugg li mark { background: var(--rust-soft); color: var(--ink); border-radius: 2px; padding: 0 1px; }
.sugg li[aria-selected="true"], .sugg li:hover { background: var(--rust); color: #fff; }
.sugg li[aria-selected="true"] .cat, .sugg li:hover .cat { color: rgba(255,255,255,0.72); }
.sugg li[aria-selected="true"] mark, .sugg li:hover mark { background: rgba(255,255,255,0.26); color: #fff; }
/* The no-match row is a SENTENCE, not a two-column suggestion row. Without the
   display override it inherits `.sugg li { display:flex; justify-content:
   space-between }` and its inline <strong> becomes a third flex column, which
   shredded the message into three space-between chunks. */
.sugg .empty { display: block; padding: 13px 12px; color: var(--ink-soft); font-size: 0.92rem; line-height: 1.5; cursor: default; }
.sugg .empty:hover { background: transparent; color: var(--ink-soft); }

.action-note {
  margin-top: 12px; font-family: var(--font-mono);
  font-size: 0.7rem; letter-spacing: 0.09em; text-transform: uppercase; color: #9AA3B5;
}
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ==========================================================================
   V3.2 — HERO CAPTURE FORM  (A1, 2026-08-24)
   The hero action grew from one field to three. Everything below reuses the
   tokens and the two control shapes the hero already had (#svcInput's 60px
   paper-3 field and .btn-go's rust block) — no new colours, no new radii, no
   new type ramp. See the comment above the form in index.html for why this is
   a prefilling handoff and not a direct POST.
   ========================================================================== */
.action-fields {
  display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;
}
.action-field { min-width: 0; }
/* Same box as #svcInput, declared once so the three fields cannot drift apart. */
.action-field input {
  width: 100%; height: 60px; padding: 0 16px; border-radius: var(--radius);
  border: 2px solid rgba(247,242,233,0.24); background: var(--paper-3);
  color: var(--ink); font-size: 1.02rem; font-family: var(--font-body);
}
.action-field input::placeholder { color: #6B7280; }
.action-field input:focus-visible {
  border-color: var(--rust); box-shadow: 0 0 0 4px rgba(181,80,30,0.25);
  outline: 2px solid var(--rust); outline-offset: 2px;
}
.action-field input:focus:not(:focus-visible) {
  border-color: var(--rust); box-shadow: 0 0 0 4px rgba(181,80,30,0.25); outline: none;
}
.action-field.has-error input { border-color: #FF8A80; }

/* The submit is its own row now that it sits under three fields rather than
   beside one. .btn-go keeps every other property. */
.btn-go--block { width: 100%; margin-top: 10px; }

/* Validation copy. On the navy hero #B3261E is unreadable, so the error text
   uses the sand-family warm tint the hero already uses for .photo-label .ph. */
.action-error {
  margin: 10px 0 0; font-size: 0.88rem; font-weight: 600; line-height: 1.45;
  color: #F6C9A9; text-align: left;
}
.action-error[hidden] { display: none; }

/* ---------- A4: the one real job photo ---------- */
.proof-shot { margin: 0 0 22px; }
.proof-shot img {
  display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--radius-lg); border: 1px solid var(--hair);
  box-shadow: var(--shadow-card); background: var(--navy-2);
}
.proof-shot figcaption {
  margin-top: 9px; font-family: var(--font-mono); font-size: 0.72rem;
  letter-spacing: 0.06em; color: var(--ink-soft);
}
/* If initPhotoSlots() removed a broken image, do not leave a caption describing
   a photo that is not there. */
.proof-shot:not(:has(img)) { display: none; }

@media (max-width: 860px) {
  /* On one column the pricing section would otherwise read: heading, five
     bullets of model explanation, and only THEN the numbers and the photo.
     The numbers are the reason the section moved up. They go first on a phone. */
  #pricing .grid--2 > div:first-child { order: 2; }
  #pricing .grid--2 > div:last-child  { order: 1; }
  #pricing .grid--2 { gap: 30px; }
  .proof-shot img { aspect-ratio: 3 / 2; }
}

@media (max-width: 640px) {
  .action-form { flex-direction: column; }
  .btn-go { width: 100%; }
  /* One column: three stacked 60px fields plus a 60px button is the whole point
     of the section, so the fields go full width rather than sharing a row. */
  .action-fields { grid-template-columns: 1fr; gap: 10px; }

  /* ---- FOLD BUDGET, 390x844 (A1) --------------------------------------
     The capture form adds ~140px to the hero (two 60px fields + gaps) and the
     submit button has to land ABOVE the sticky bar, i.e. above y=770. The
     budget is bought back from vertical padding and from the sub-line's
     breathing room, NOT from the emblem: --em is untouched at
     clamp(58px, 18vw, 104px), so the lockup is exactly the size the owner
     approved on 2026-08-02. Measured with a headless 390x844 viewport, not
     estimated — see screenshots/ in this folder. */
  .hero-ace { --peek: 0px; min-height: auto; }
  .hero-ace-inner { padding: 22px 20px 26px; }
  .hero-ace h1 { margin-top: 16px; }
  .hero-ace-sub { margin-top: 9px; }
  .hero-action { margin-top: 18px; }
  .action-label { margin-bottom: 8px; }
  .action-note { margin-top: 10px; }
}
/* Mobile: the lockup STAYS horizontal — it is one unit and breaking it into two
   tiers on a phone is exactly the thing this revision removed. It just gets
   smaller and centers. The photo panel becomes a full-width background wash
   behind the content rather than a left column. */
@media (max-width: 640px) {
  .hero-lockup { --em: clamp(58px, 18vw, 104px); }
  .lockup { gap: calc(var(--em, 180px) * 0.11); }
  /* smaller --em means fewer real pixels per glyph, so the sub takes a slightly
     larger ratio to stay legible. Still well under the 0.2429 ceiling that would
     make the sub, not FORTIFIED, define the stack width. */
  .lockup-sub { font-size: calc(var(--em, 180px) * 0.195); margin-top: calc(var(--em, 180px) * 0.03); }
  .hero-photo { width: 100%; opacity: 0.45; }
  .hero-photo::after { background:
    radial-gradient(ellipse 74% 46% at 40% 24%, rgba(181,80,30,0.40), transparent 68%),
    radial-gradient(ellipse 80% 60% at 70% 92%, rgba(181,80,30,0.14), transparent 66%); }
}

/* ---------- Trust strip: natural end of the hero ---------- */
.trust-strip {
  background: var(--navy-2); border-top: 1px solid rgba(247,242,233,0.10);
  padding: 20px clamp(18px, 4vw, 48px);
}
.trust-inner {
  max-width: 1140px; margin: 0 auto;
  display: flex; align-items: center; justify-content: center;
  gap: clamp(12px, 2.6vw, 34px); flex-wrap: wrap;
  /* P-2: these are the REAL credentials and they were rendering at 11.5px in a
     grey that the rust-outlined warranty placeholder beneath them beat on every
     axis. Up to 13px, tracking relaxed, color lifted to the paper family. */
  font-family: var(--font-mono); font-size: 0.815rem; letter-spacing: 0.04em;
  text-transform: uppercase; color: #DCE0E8; font-weight: 600;
}
.trust-inner .sep { color: var(--rust); opacity: 0.9; }
.trust-inner a { color: var(--paper); }
.trust-inner a:hover { color: var(--rust-soft); }

/* TRUST PROMISES — second row of the strip (V3.1b, ACE_GAP_CHECK §2).
   Row 1 is credential chips; this row is the two trust SENTENCES. The on-my-way
   promise is live copy; the warranty line is a reserved, visibly-empty position.
   Both are sentence-case body type, not mono chips, because a promise that reads
   like a badge reads like decoration. */
.trust-promises {
  max-width: 1140px; margin: 16px auto 0; padding-top: 15px;
  border-top: 1px solid rgba(247,242,233,0.10);
  display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center;
}
.trust-promises .promise {
  display: inline-flex; align-items: center; gap: 10px;
  color: var(--paper); font-size: clamp(0.95rem, 1.4vw, 1.06rem); font-weight: 700;
  letter-spacing: -0.005em; margin: 0;
}
.trust-promises .promise .dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--rust); flex: none;
}
.warranty-slot {
  display: flex; align-items: center; justify-content: center; gap: 11px; flex-wrap: wrap;
  margin: 0; max-width: 74ch;
  font-size: 0.88rem; line-height: 1.45; color: #AEB5C4;
}
/* P-2: the placeholder sits at the same optical weight as the sentence next to
   it now — no hatch fill, hairline border, no uppercase shout. */
.warranty-slot .ph {
  color: #E3B492; border-color: rgba(201,122,72,0.55); background: none;
  font-size: 0.72rem; letter-spacing: 0.06em; padding: 4px 9px; font-weight: 500;
}
@media (max-width: 560px) {
  .trust-promises { gap: 12px; }
  .warranty-slot { font-size: 0.83rem; gap: 9px; }
}

/* ---------- Coordinated-work row (the 13th row that is not a service) ---------- */
.svc-row--coord {
  border-top: 1px dashed var(--rust);
  background: repeating-linear-gradient(45deg, rgba(181,80,30,0.045) 0 10px, transparent 10px 20px);
}
.svc-row--coord .num { color: var(--rust); }

/* ---------- "From $[PRICE — Connor]" anchor (locked decision #3) ---------- */
.price-anchor {
  display: inline-flex; align-items: baseline; gap: 7px; flex-wrap: wrap;
  font-family: var(--font-display); font-weight: 700; font-size: 0.92rem; color: var(--ink-soft);
}
.section--band .price-anchor { color: var(--band-soft); }

/* ---------- Estimate page: "Asking about:" confirmation receipt ---------- */
.req-summary {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 22px; padding: 14px 18px;
  border: 1px solid var(--rust);   /* P-6: no left stripe — full hairline + tint */
  border-radius: var(--radius); background: rgba(181,80,30,0.07);
}
.req-summary[hidden] { display: none; }
.req-summary-label {
  font-size: 0.68rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--rust-deep);
}
.req-summary-value { font-family: var(--font-display); font-weight: 800; font-size: 1.06rem; color: var(--ink); }
.req-summary-note { font-size: 0.86rem; color: var(--ink-soft); flex-basis: 100%; }

/* ==========================================================================
   OPENING ANIMATION — "COLLAPSE TO SHIELD" v2  (homepage only)
   transform / opacity ONLY. contain:strict isolates it from page layout, so it
   can never reflow the hero underneath it and can never become the LCP element.
   ========================================================================== */
/* FAIL-OPEN DEFAULT. The overlay is display:none until the inline controller on
   the homepage decides to play it and adds .armed. If that script is blocked,
   throws, or never runs, the visitor sees the hero — never a navy field stuck
   over the site. This is the single most important rule in this block. */
#fas-intro { display: none; position: fixed; inset: 0; z-index: 900; overflow: hidden; contain: strict; pointer-events: none; }
#fas-intro.armed { display: block; }
#fas-intro.done { display: none !important; }

.ov-field {
  position: absolute; inset: -1px 0;
  background: radial-gradient(125% 82% at 50% 48%, #232C3E 0%, var(--navy) 46%, #121724 100%);
  transform: translate3d(0,0,0);
}
.ov-field::after {
  content: ''; position: absolute; inset: 0; opacity: 0.5;
  background-image: linear-gradient(90deg, rgba(181,80,30,0.16) 1px, transparent 1px),
                    linear-gradient(0deg, rgba(181,80,30,0.16) 1px, transparent 1px);
  background-size: 56px 56px;
  -webkit-mask-image: radial-gradient(ellipse 68% 58% at 50% 50%, #000 28%, transparent 76%);
          mask-image: radial-gradient(ellipse 68% 58% at 50% 50%, #000 28%, transparent 76%);
}

/* Two nested jolt layers. They MUST be separate elements: two animations on one
   element both targeting `transform` with fill-mode:both would have the later
   one's backwards-fill silently override the earlier one for the whole timeline. */
.ov-stage { position: absolute; inset: 0; will-change: transform; }
.ov-jolt  { position: absolute; inset: 0; will-change: transform; }

.ov-words {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  text-align: center; white-space: nowrap;
}
.ov-words .w {
  display: block; font-family: var(--font-display); font-weight: 800;
  font-size: clamp(2.1rem, 9.2vw, 6.4rem); line-height: 0.94; letter-spacing: -0.03em;
  color: var(--paper); opacity: 0;
}
.ov-words .w:nth-child(2) { color: #E3DED2; }
.ov-words .w:nth-child(3) { color: #D2CDC2; }
.ov-words .w b { font-weight: 800; display: inline-block; will-change: transform, opacity; }

.ov-settle { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); will-change: transform; }
.ov-lockup {
  /* deliberately larger than the hero lockup so the final glide scales DOWN into
     place (~0.67x at 1280). Capped for the ~4.0x-wide horizontal build so the
     whole unit — including FORTIFIED, which is invisible but already occupying
     layout width during the collapse — stays inside the viewport at every size. */
  --em: clamp(120px, 15.5vw, 230px);
  transform: translate(var(--sx, 0px), var(--sy, 0px));
  will-change: transform;
}
@media (max-width: 640px) { .ov-lockup { --em: clamp(74px, 23vw, 132px); } }

.ov-lockup .emblem { opacity: 0; transform: scale(0.55); will-change: transform, opacity; }
.ov-lockup .emblem-fas i { opacity: 0; }
/* FORTIFIED reveals rightward out of the shield, so its origin is its own left
   edge — the edge that sits against the emblem in the horizontal build. */
.ov-lockup .lockup-word {
  opacity: 0; transform: translateX(calc(var(--em, 180px) * -0.17)) scale(0.84);
  transform-origin: left center; will-change: transform, opacity;
}
.ov-lockup .lockup-sub i { opacity: 0; transform: translateY(calc(var(--em, 180px) * 0.026)); }
.ov-lockup .lockup-rule { opacity: 0; transform: scaleX(0); transform-origin: left center; }

/* impact ring is a SIBLING of the emblem, not a child, so the stamp's own
   scale-overshoot doesn't multiply into the ring's expansion */
.emblem-wrap { position: relative; display: flex; flex: 0 0 auto; }
.ov-ring {
  position: absolute; left: 50%; top: 50%; width: var(--em); aspect-ratio: 120 / 140;
  transform: translate(-50%, -50%) scale(0.6); opacity: 0; pointer-events: none;
}
.ov-ring svg { width: 100%; height: 100%; }

.snap-fx { position: absolute; left: 0; right: 0; bottom: 0; height: 0; pointer-events: none; }
.snap-flash {
  position: absolute; left: 0; right: 0; top: -4px; height: 9px;
  background: linear-gradient(90deg, transparent, #FFF8EA 18%, #FFF8EA 82%, transparent);
  opacity: 0; transform: scaleY(0.3);
}
.snap-dust { position: absolute; left: 0; right: 0; top: 0; height: 0; }
.snap-dust i {
  position: absolute; top: 0; left: var(--x); width: 4px; height: 4px; border-radius: 50%;
  background: var(--rust-soft); opacity: 0;
}
.snap-dust i.d2 { background: #FFF3E0; width: 3px; height: 3px; }
.snap-dust i.d3 { background: var(--rust); width: 5px; height: 5px; }

.skip-hint {
  position: absolute; left: 0; right: 0; bottom: clamp(22px, 5vh, 50px);
  text-align: center; font-family: var(--font-mono); font-size: 0.66rem;
  letter-spacing: 0.2em; text-transform: uppercase; color: rgba(247,242,233,0.42); opacity: 0;
}

@keyframes ov-wordin { from { opacity: 0; transform: translateY(26px) scale(0.97); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes ov-flykey { 0% { opacity: 1; transform: translate(0,0) scale(1); } 72% { opacity: 1; } 100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(var(--s)); } }
@keyframes ov-flydead { 0% { opacity: 1; transform: translate(0,0) scale(1); } 55% { opacity: 0.55; } 100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(0.14); } }
@keyframes ov-wordfade { from { opacity: 1; } to { opacity: 0; } }
@keyframes ov-stamp { 0% { opacity: 0; transform: scale(0.52); } 45% { opacity: 1; transform: scale(1.07); } 72% { transform: scale(0.985); } 100% { opacity: 1; transform: scale(1); } }
@keyframes ov-ringout { 0% { opacity: 0; transform: translate(-50%,-50%) scale(0.72); } 14% { opacity: 0.85; } 100% { opacity: 0; transform: translate(-50%,-50%) scale(1.62); } }
@keyframes ov-fasin { from { opacity: 0; } to { opacity: 1; } }
@keyframes ov-jolt { 0%,100% { transform: translate3d(0,0,0); } 14% { transform: translate3d(0,4px,0); } 32% { transform: translate3d(-3px,-2px,0); } 52% { transform: translate3d(2px,2px,0); } 76% { transform: translate3d(-1px,-1px,0); } }
@keyframes ov-jolt2 { 0%,100% { transform: translate3d(0,0,0); } 18% { transform: translate3d(0,3px,0); } 44% { transform: translate3d(2px,-2px,0); } 72% { transform: translate3d(-1px,1px,0); } }
@keyframes ov-recenter { from { transform: translate(var(--sx,0px), var(--sy,0px)); } to { transform: translate(0px, 0px); } }
@keyframes ov-wordsettle { 0% { opacity: 0; transform: translateX(calc(var(--em, 180px) * -0.17)) scale(0.84); } 62% { opacity: 1; } 100% { opacity: 1; transform: translateX(0) scale(1); } }
@keyframes ov-subletter { from { opacity: 0; transform: translateY(calc(var(--em, 180px) * 0.026)); } to { opacity: 1; transform: translateY(0); } }
@keyframes ov-chalksnap { 0% { opacity: 0; transform: scaleX(0) translateY(0); } 26% { opacity: 1; transform: scaleX(1) translateY(0); } 44% { transform: scaleX(1) translateY(-10px); } 60% { transform: scaleX(1) translateY(6px); } 78% { transform: scaleX(1) translateY(-2px); } 100% { opacity: 1; transform: scaleX(1) translateY(0); } }
@keyframes ov-flashout { 0% { opacity: 0; transform: scaleY(0.3); } 22% { opacity: 1; transform: scaleY(1); } 100% { opacity: 0; transform: scaleY(0.4); } }
@keyframes ov-dustout { 0% { opacity: 0; transform: translate(0,0) scale(1); } 16% { opacity: 0.95; } 100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(var(--s)); } }
@keyframes ov-hintin { from { opacity: 0; } to { opacity: 1; } }
@keyframes ov-settleout { from { transform: translate(-50%,-50%) translate(0,0) scale(1); } to { transform: translate(-50%,-50%) translate(var(--fx), var(--fy)) scale(var(--fs)); } }
@keyframes ov-fieldout { from { opacity: 1; } to { opacity: 0; } }
@keyframes ov-lockfade { from { opacity: 1; } to { opacity: 0; } }

/* ==========================================================================
   TIMELINE — 2.98s total.  REVISED 2026-08-02 (V3.1a) for the horizontal lockup.
   The concept is unchanged (three words collapse, FAS lands in the shield); what
   changed is the ASSEMBLY ORDER, which now builds the horizontal unit left to
   right and snaps the chalk line BEFORE the sub-line resolves under it — the
   owner's requested reading order.

     0.06 - 0.67   FORTIFIED / ASSET / SOLUTIONS rise in, stacked, 90ms stagger
     0.78 - 1.25   COLLAPSE — every letter flies to the dead center of the
                   viewport; F, A and S survive and land as the monogram, the
                   other twenty implode and fade
     1.15 - 1.50   Shield STAMPS in around them, DEAD CENTER (overshoot 1.07 +
                   expanding impact ring + 300ms jolt through the stage)
     1.45 - 1.84   FORTIFIED reveals to the RIGHT of the shield and the lockup
                   re-centers around the finished horizontal unit
     1.80 - 2.12   CHALK LINE SNAP directly under FORTIFIED — taut, pulled 10px,
                   snapped 6px past level, settles. Flash + 16 dust particles +
                   a second jolt. Still the best 300ms in the sequence.
     2.04 - 2.50   HANDYMAN SERVICES resolves under the line, letter by letter
                   (15ms stagger, opacity+translate only — never letter-spacing)
     2.48 - 2.88   The whole formed lockup GLIDES from center to its final hero
                   position, center-right. Measured landing, not eyeballed.
     2.84 - 2.98   Navy field cross-fades ONLY after the lockup has landed —
                   fading earlier double-images the two lockups for ~250ms
   ========================================================================== */
#fas-intro.run .ov-words .w { animation: ov-wordin 0.43s cubic-bezier(.16,.85,.3,1) both; animation-delay: calc(0.06s + var(--w) * 0.09s); }
#fas-intro.run .ov-words .w b.key  { animation: ov-flykey 0.47s cubic-bezier(.62,.02,.3,1) 0.78s both; }
#fas-intro.run .ov-words .w b.dead { animation: ov-flydead 0.47s cubic-bezier(.62,.02,.3,1) both; animation-delay: calc(0.78s + var(--j) * 0.012s); }
#fas-intro.run .ov-words { animation: ov-wordfade 0.10s linear 1.24s both; }
#fas-intro.run .ov-lockup .emblem { animation: ov-stamp 0.35s cubic-bezier(.22,.9,.28,1.05) 1.15s both; }
#fas-intro.run .ov-ring { animation: ov-ringout 0.42s cubic-bezier(.2,.7,.3,1) 1.15s both; }
#fas-intro.run .ov-lockup .emblem-fas i { animation: ov-fasin 0.09s linear 1.19s both; }
#fas-intro.run .ov-stage { animation: ov-jolt 0.30s cubic-bezier(.3,.7,.4,1) 1.16s both; }
#fas-intro.run .ov-lockup { animation: ov-recenter 0.39s cubic-bezier(.2,.8,.2,1) 1.45s both; }
#fas-intro.run .ov-lockup .lockup-word { animation: ov-wordsettle 0.39s cubic-bezier(.2,.85,.25,1.02) 1.45s both; }
#fas-intro.run .ov-lockup .lockup-rule { animation: ov-chalksnap 0.32s cubic-bezier(.3,.72,.36,1) 1.80s both; }
#fas-intro.run .ov-jolt  { animation: ov-jolt2 0.20s cubic-bezier(.3,.7,.4,1) 1.96s both; }
#fas-intro.run .snap-flash { animation: ov-flashout 0.30s ease-out 1.94s both; }
#fas-intro.run .snap-dust i { animation: ov-dustout 0.54s cubic-bezier(.2,.7,.3,1) both; animation-delay: calc(1.96s + var(--d)); }
#fas-intro.run .ov-lockup .lockup-sub i { animation: ov-subletter 0.22s var(--ease) both; animation-delay: calc(2.04s + var(--k) * 0.015s); }
#fas-intro.run .skip-hint { animation: ov-hintin 0.34s ease-out 1s both; }

/* Settle onto the hero's own lockup. Both animations live on .ov-settle because
   they target DIFFERENT properties (transform vs opacity) so they compose. The
   fade must NOT go on .ov-lockup: that element still holds the `recenter`
   transform via fill-mode, and a second animation there drops it back to its
   pre-recenter offset — a visible jump on the last beat. */
/* The GLIDE. Both animations live on .ov-settle because they target DIFFERENT
   properties (transform vs opacity) so they compose instead of clobbering. The
   fade must NOT go on .ov-lockup: that element is still holding the `recenter`
   transform via fill-mode, and a second animation there would drop it back to
   its pre-recenter offset — a visible jump on the last beat. */
#fas-intro.out .ov-settle {
  animation: ov-settleout 0.40s cubic-bezier(.5,.03,.2,1) both,
             ov-lockfade 0.12s linear 0.38s both;
}
#fas-intro.out .ov-field { animation: ov-fieldout 0.14s linear 0.36s both; }
#fas-intro.out .skip-hint { opacity: 0; }
#fas-intro.out .snap-fx { opacity: 0; }

/* Reduced motion: never render, never auto-play. Straight to the hero. */
@media (prefers-reduced-motion: reduce) {
  #fas-intro { display: none !important; }
}

/* ---------- V3.1 narrow-viewport corrections ------------------------------
   Two defects found in the 320px sweep during the V3.1 pass. Both predate this
   build; fixed here because the pass added a wider element to one of them.
   1. Grid children default to min-width:auto, so a long dotted-leader spec row
      could push its track past the container and force a 2px horizontal scroll
      on the homepage pricing band. min-width:0 lets the track shrink.
   2. The estimate page's two-column form/sidebar split was set with an INLINE
      grid-template-columns, which outranks the 860px collapse rule — so it
      stayed two columns all the way down. It is now a class.
   -------------------------------------------------------------------------- */
.grid > * { min-width: 0; }
.grid--form { grid-template-columns: 1.25fr 0.75fr; }
@media (max-width: 860px) { .grid--form { grid-template-columns: 1fr; } }
@media (max-width: 560px) {
  .spec { flex-wrap: wrap; }
  .spec .lead { min-width: 8px; }
  .price-anchor { font-size: 0.86rem; }
  .price-anchor .ph--price { font-size: 0.8rem; padding: 6px 9px; }
}
@media (max-width: 380px) {
  /* mono section indices are wide by design; let the longest ones wrap rather
     than force a horizontal scroll on a 320px phone */
  .rule-head { flex-wrap: wrap; }
  .rule-head .idx { white-space: normal; }
}


/* ==========================================================================
   V3.1e LAYER — owner punchlist ruling wave  (2026-08-02)
   Additive. New components only; every edit to an existing rule above this line
   is annotated in place. Reverting V3.1e = delete from here down plus the
   annotated edits.
   ========================================================================== */

/* ---------- A2 · SERVICE TILE GRID (homepage §02) --------------------------
   Replaces the 12-row list the homepage duplicated verbatim from /services/.
   Six tiles, one custom inline icon each, hairline-divided.
   The dividers are the GRID GAP showing the container's background through, not
   per-tile borders — so the same rule produces a correct grid at 3, 2 or 1
   columns with no :nth-child arithmetic to get wrong at a breakpoint. */
.tile-grid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 1px; background: var(--hair);
  border: 1px solid var(--hair); border-radius: var(--radius-lg); overflow: hidden;
  box-shadow: var(--shadow-card);
}
.tile {
  display: flex; flex-direction: column; align-items: flex-start; gap: 18px;
  background: var(--paper-3); color: var(--ink);
  padding: 30px 26px 26px; min-height: 176px; position: relative;
  transition: background 180ms var(--ease);
}
.tile:hover, .tile:focus-visible { background: var(--paper-2); }
.tile-ico { display: block; width: 48px; height: 48px; flex: none; }
/* One icon system: 48px grid, 2px ink strokes, exactly one rust element each
   (.ac stroked, .ac-f filled). No clip-art, no emoji, no icon font. */
.tile-ico svg {
  display: block; width: 48px; height: 48px;
  fill: none; stroke: var(--ink); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.tile-ico .ac { stroke: var(--rust); }
.tile-ico .ac-f { fill: var(--rust); stroke: none; }
.tile-name {
  font-family: var(--font-display); font-weight: 800; font-size: 1.06rem;
  line-height: 1.25; letter-spacing: -0.01em;
}
.tile-go {
  margin-top: auto;                 /* pin the affordance to the bottom edge, so
                                       the name stays with the icon it labels */
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--rust-text);
  display: inline-flex; align-items: center; gap: 7px;
  transition: transform 180ms var(--ease);
}
.tile:hover .tile-go, .tile:focus-visible .tile-go { transform: translateX(5px); }
.tile-more { margin-top: 26px; font-weight: 600; }
@media (max-width: 900px) {
  .tile-grid { grid-template-columns: repeat(2, 1fr); }   /* tablet: 2 x 3 */
}
@media (max-width: 520px) {
  .tile { padding: 22px 16px 20px; min-height: 152px; gap: 14px; }
  .tile-name { font-size: 0.97rem; }
  .tile-ico, .tile-ico svg { width: 40px; height: 40px; }
}

/* ---------- SF-5 · PACKAGE COMPARISON STRIP -------------------------------
   /packages/ opened with five ~780px cards and the price at the bottom of each,
   so "help me pick one" cost 3,900px of scrolling. The strip puts the five
   names, times and from-prices in one glance above the detail cards. */
.cmp-wrap { margin-bottom: 40px; }
.cmp { width: 100%; border-collapse: collapse; }
.cmp th, .cmp td {
  text-align: left; padding: 15px 14px 15px 0; vertical-align: top;
  border-bottom: 1px solid var(--hair); font-size: 0.95rem; color: var(--ink-soft);
}
.cmp thead th {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--rust-text); font-weight: 600;
  border-bottom: 1px solid var(--rust-soft); padding-bottom: 11px;
}
.cmp tbody tr:last-child th, .cmp tbody tr:last-child td { border-bottom: 0; }
.cmp tbody th {
  font-family: var(--font-display); font-weight: 800; font-size: 1rem; color: var(--ink);
  white-space: normal;
}
.cmp tbody th a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--rust-soft); text-underline-offset: 4px; }
.cmp tbody th a:hover { color: var(--rust-text); text-decoration-color: var(--rust); }
.cmp .cmp-time { font-family: var(--font-mono); font-size: 0.85rem; white-space: nowrap; }
.cmp .cmp-price { white-space: nowrap; }
.cmp .cmp-for { font-size: 0.9rem; }
@media (max-width: 780px) {
  /* Drop the prose column rather than side-scroll the table — name, time and
     price are the three that make the comparison. */
  .cmp .cmp-for, .cmp .cmp-for-h { display: none; }
  .cmp th, .cmp td { padding: 13px 10px 13px 0; }
}
@media (max-width: 620px) {
  /* Measured: with three columns the strip's own minimum is 566px (the
     from-price chip does not wrap and the package names are two words), so the
     420px cut-off left a 421-566px band that pushed the page into horizontal
     scroll. Name + price is still the comparison; the times are restated at the
     top of every card below. */
  .cmp .cmp-time, .cmp .cmp-time-h { display: none; }
}

/* ---------- P-5 · FAQ CLUSTERS + CATEGORY RAIL ----------------------------
   18 flat accordions in one column with ~40% of the desktop viewport empty to
   the right. Four labelled clusters, and the void becomes a sticky index. */
.faq-layout { display: grid; grid-template-columns: minmax(0, 1fr) 232px; gap: 52px; align-items: start; }
.faq-group + .faq-group { margin-top: 46px; }
.faq-group > h2 {
  font-size: 1.5rem; margin-bottom: 4px; scroll-margin-top: 90px;
}
.faq-group > .faq-group-note { font-size: 0.92rem; margin-bottom: 18px; }
.faq-rail { position: sticky; top: 96px; }
.faq-rail-title {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--rust-text); font-weight: 600;
  padding-bottom: 12px; border-bottom: 1px solid var(--hair); display: block;
}
.faq-rail-links { display: flex; flex-direction: column; }
.faq-rail-links a {
  padding: 13px 0; border-bottom: 1px solid var(--hair-2);
  font-size: 0.94rem; font-weight: 600; color: var(--ink);
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.faq-rail-links a:hover { color: var(--rust-text); }
.faq-rail-links .n { font-family: var(--font-mono); font-size: 0.76rem; color: var(--ink-soft); }
@media (max-width: 900px) {
  .faq-layout { grid-template-columns: 1fr; gap: 26px; }
  .faq-rail { position: static; }
  .faq-rail-links { flex-direction: row; flex-wrap: wrap; gap: 9px; }
  .faq-rail-links a {
    padding: 9px 14px; border: 1px solid var(--hair); border-radius: 999px;
    background: var(--paper-3); font-size: 0.88rem;
  }
}

/* ---------- A4 · CONTACT METHOD CARDS ------------------------------------
   /contact/ is no longer a second copy of the estimate form. The three method
   cards carry it, and one full-width action sends everyone to the one form. */
.contact-action {
  margin-top: 40px; padding: 30px 26px; border-radius: var(--radius-lg);
  border: 1px solid var(--hair); background: var(--paper-3); box-shadow: var(--shadow-card);
  display: flex; flex-direction: column; align-items: flex-start; gap: 14px;
}
.contact-action h2 { font-size: 1.6rem; }
.contact-action .btn { margin-top: 6px; }
@media (min-width: 861px) {
  .contact-action { align-items: center; text-align: center; }
  .contact-action p { margin-left: auto; margin-right: auto; }
}

/* ---------- SF-13 · single-action sticky bar ------------------------------
   .sticky-cta a is already flex:1, so one child fills the bar. This exists to
   name the state in the markup and to widen the tap target's label. */
.sticky-cta--single .btn { width: 100%; }

/* REAL HERO PHOTO (V3.2, 2026-08-03). The hero panel now ships with Connor's
   real photo (assets/photos/connor-hero.jpg — Connor at his equipment trailer,
   shot 8/3). Everything below is gated on :has(.hero-photo-img) so the
   fail-open contract holds: if main.js removes a failed image, the label,
   marks and original rust-wash treatment all come back untouched. */
.hero-photo:has(.hero-photo-img) .photo-label { display: none; }
.hero-photo:has(.hero-photo-img)::after {
  /* photo replaces the placeholder texture, so drop the rust washes (they
     muddy a real photograph) and keep only the right-edge blend into the
     navy hero plus a light navy tint for type harmony. Needs z-index now —
     the img sits at z:1 and pseudo-elements default under it. */
  z-index: 2;
  background:
    linear-gradient(90deg, transparent 55%, var(--navy) 99%),
    linear-gradient(0deg, rgba(18,23,36,0.14), rgba(18,23,36,0.14));
}

/* OWNER LETTER (V3.2). Personal note from Connor at the bottom of the
   homepage, above the final CTA band. Photo is the REAL 8/3 shot (square
   crop of the trailer photo); swap src for the edited headshot when Drew
   delivers it — layout doesn't change. */
.owner-letter {
  display: grid; grid-template-columns: 320px 1fr; gap: 40px; align-items: start;
}
.owner-letter-photo img {
  width: 100%; height: auto; display: block; border-radius: 10px;
  border: 1px solid var(--line); box-shadow: 0 18px 40px -18px rgba(18,23,36,0.35);
}
.owner-letter-photo figcaption {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.08em;
  color: var(--grey); margin-top: 10px;
}
.owner-letter-body h2 { margin-bottom: 18px; }
.owner-letter-body p { max-width: 62ch; }
.owner-letter-body p + p { margin-top: 14px; }
.owner-letter-sig {
  margin-top: 22px; font-family: var(--font-display); font-weight: 800; font-size: 1.15rem;
}
.owner-letter-sig span {
  display: block; font-family: var(--font-mono); font-weight: 500; font-size: 0.78rem;
  letter-spacing: 0.08em; color: var(--grey); margin-top: 4px;
}
@media (max-width: 760px) {
  .owner-letter { grid-template-columns: 1fr; gap: 26px; }
  .owner-letter-photo { max-width: 380px; }
}

/* ---------- Build credit: Summit Intake Systems (footer bottom-right) ----------
   Sits as the third item in .footer-bottom, so on desktop the row reads
   copyright / legal / credit and the credit lands in the bottom-right corner.
   Below 560px the flex row wraps and it stacks under the legal links. */
.footer-credit a {
  display: inline-flex; align-items: center; gap: 7px;
  color: #8E96A8; font-size: 0.8rem; white-space: nowrap;
}
.footer-credit a:hover, .footer-credit a:focus-visible { color: #fff; }
.sis-mark { width: 15px; height: 13px; flex: none; fill: currentColor; }


/* ==========================================================================
   V3.2 — GHL FORM SLOT: LAZY + A LOADING STATE  (A3, 2026-08-24)
   The slot used to be a 1,286px-tall empty off-white card with a hairline and a
   drop shadow, and nothing else, for as long as three origins took to resolve
   (fortified-assets.com -> api.leadconnectorhq.com -> GHL's own CSS/JS/fonts).
   Measured 3.5s P50 / 5.9s P75 on three DESKTOP loads; never measured on mobile
   4G. A visitor on a phone saw about 1.5 screens of blank rectangle with no
   indication anything was happening.

   NOT A CLICK GATE, deliberately. /contact/ is the A2P campaign's registered
   opt-in URL and /estimate/ is the primary intake; putting either behind a
   button risks reading as a hidden opt-in form to a carrier reviewer — the exact
   shape of the 30909 rejection logged in the A2P canon. The iframe therefore
   loads on INTERSECTION with a generous rootMargin (it is effectively immediate
   on both pages), and this skeleton fills the reserved box until it paints.
   ========================================================================== */
.form-slot { position: relative; }
.form-skel {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; min-height: 320px; padding: 40px 24px; text-align: center;
  color: var(--ink-soft);
}
.form-skel[hidden] { display: none; }
.form-skel .spin {
  width: 30px; height: 30px; border-radius: 50%;
  border: 3px solid var(--rust-soft); border-top-color: var(--rust);
  animation: fas-spin 900ms linear infinite;
}
@keyframes fas-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  /* A spinner that cannot spin is a dot. Give reduced-motion users the same
     information as a static ring instead. */
  .form-skel .spin { animation: none; border-top-color: var(--rust-soft); }
}
.form-skel .skel-title {
  font-family: var(--font-display); font-weight: 700; font-size: 1.02rem; color: var(--ink);
}
.form-skel .skel-note { font-size: 0.9rem; max-width: 40ch; line-height: 1.5; }
.form-skel .skel-note a { font-family: var(--font-mono); }
/* Before the iframe is asked to load, the slot does not need to reserve 1,286px
   of empty page. It claims the space the moment the src is set, which is also
   the moment the skeleton is showing, so nothing shifts under the reader. */
.form-slot iframe:not([src]) { min-height: 0 !important; height: 0 !important; }
