/* ============================================================================
   TREVOR'S LAWN AND TREE SERVICES — styles.css
   Built by Traffel Media. Loaded by every page from /assets/styles.css.
   Lifted verbatim out of the old single-file index.html on 2026-08-10 when the
   site became sixteen real pages: one cached request instead of the same 58 KB
   inlined sixteen times. System font stack only — nothing is loaded from a
   network, here or anywhere else on this site.
   ========================================================================== */
/* ============================================================================
   1. VARIABLES
   Green-dominant by design. Brick red is the ACTION color only — buttons,
   the estimate submit, the occasional underline. If red ever starts reading
   like wallpaper, pull it back.

   CONTRAST — every pairing below was checked against WCAG AA (4.5:1 body):
     green-deep on cream  12.35   cream on green-deep  12.35
     green-deep on beige  10.39   beige on green-deep  10.39
     green      on cream   7.74   cream on green        7.74
     green      on beige   6.51   beige on green        6.51
     green-mid  on cream   6.07   green-mid on beige    5.11
     charcoal   on cream  13.74   charcoal on beige    11.57
     ink-mid    on cream   8.30   ink-mid on beige      6.99
     white      on red     5.71   white on red-dark     7.97
     red        on cream   5.29   red-dark on beige     6.21
     green-pale on green-deep  6.23

   FAILS — do not use these combinations for body text:
     --green-bright (#2E7D4F) on beige = 3.80  -> large display / fills only
     --green-pale  (#8FB89A) on --green = 3.90 -> only on --green-deep
     --red (#B33A2B) as TEXT on beige   = 4.45 -> use --red-dark on beige
   ========================================================================== */
:root{
  /* Green — primary, carries the site */
  --green-deep:#14331F;   /* large surfaces, footer, hero, headings on light */
  --green:#1E5631;        /* primary green, safe for body text on cream/beige */
  --green-mid:#266741;    /* eyebrows and labels, safe on cream AND beige */
  --green-bright:#2E7D4F; /* decorative fills, icons, large display ONLY */
  --green-pale:#8FB89A;   /* muted text on --green-deep ONLY */
  --green-veil:rgba(20,51,31,.08);

  /* Beige / cream — secondary, warm not gray */
  --beige:#E8DFC9;
  --beige-deep:#DDD2B7;
  --cream:#F7F2E7;
  --white:#FDFBF6;        /* tinted near-white; pure #FFF never appears outdoors */

  /* Brick red — ACCENT ONLY. CTAs and the estimate submit. */
  --red:#B33A2B;
  --red-dark:#8F2C20;

  /* Ink */
  --charcoal:#23261F;
  --ink-mid:#3F4A40;

  /* Type — system stack only, no external font loading anywhere on this site */
  --font-body:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --font-display:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --font-cond:"Arial Narrow","Helvetica Neue Condensed",Arial,sans-serif;

  --shadow-sm:0 3px 12px rgba(20,51,31,.08);
  --shadow-md:0 14px 38px rgba(20,51,31,.14);
  --shadow-lg:0 26px 64px rgba(20,51,31,.22);
  --shadow-red:0 14px 34px rgba(179,58,43,.32);

  --t:cubic-bezier(.22,.61,.36,1);
  --radius:6px;
  /* WIDENED 2026-08-07 from 1180px. At 1180 the whole site sat in a narrow
     centred column — on a ~2000px screen that left ~410px of dead space on
     each side, so the logo was nowhere near the top-left of the screen and
     every section floated in the middle. Edward wanted it corner to corner.
     Two components depended on the old 1180 cap and are now capped
     individually instead: .ba-set (image upscaling) and .hero-photo. */
  --maxw:1680px;

  /* FORM CONTROL BORDER — WCAG 1.4.11 non-text contrast.
     The form borders used to be --beige-deep (#DDD2B7), which is 1.45:1
     against the --white (#FDFBF6) field it sits on, against a 3:1 minimum.
     These two forms carry every lead on the site, so the field edge has to be
     visible to somebody with low vision or a screen in daylight. Same warm
     hue, darkened until it passes: 3.27:1. Used ONLY by .field controls —
     --beige-deep still draws every hairline elsewhere, so nothing else on the
     site changes appearance. */
  --field-border:#948B76;
}

/* ============================================================================
   2. RESET & BASE
   ========================================================================== */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
html{
  scroll-behavior:smooth;-webkit-text-size-adjust:100%;text-size-adjust:100%;
  /* The header is sticky and its height is (logo height + 22px of chrome).
     Without this, a hash jump or a Tab-focused element can land UNDER the
     header. Keep the clamp in sync with .wordmark.has-img .wordmark-img. */
  scroll-padding-top:calc(clamp(54px,7.4vw,76px) + 34px);
}
body{
  font-family:var(--font-body);
  font-size:17px;
  line-height:1.65;
  color:var(--charcoal);
  background:var(--cream);
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  overflow-x:hidden;
}
/* overflow-wrap only fires when a single word genuinely cannot fit the box,
   so it changes nothing at normal widths. It is the safety net that stops a
   long place name ("Christiansburg,") from pushing the page sideways at 320px. */
h1,h2,h3,h4,h5,h6{font-family:var(--font-display);font-weight:800;line-height:1.12;letter-spacing:-.015em;color:var(--green-deep);overflow-wrap:break-word;}
h1{font-size:clamp(2.1rem,5.2vw,3.6rem);line-height:1.05;}
h2{font-size:clamp(1.7rem,3.6vw,2.7rem);}
h3{font-size:clamp(1.2rem,2vw,1.55rem);}
h4{font-size:1.05rem;}
p{font-size:1.0625rem;line-height:1.72;color:var(--ink-mid);overflow-wrap:break-word;}
a{color:inherit;text-decoration:none;transition:color .22s var(--t);}

/* IMAGES — GLOBAL RULE. Read before touching any <img> on this site.
   height:auto is the important part: every <img> here carries width/height
   ATTRIBUTES so the browser can reserve the box, and without height:auto a
   constrained width would leave the attribute height in place and squash the
   picture. Any image that genuinely needs a fixed height (.wordmark-img,
   .ba-img, .photo-frame img) overrides this with a class selector, which
   always wins over this element selector.

   align-self/justify-self are the defensive half. A flex or grid container
   stretches its children on the cross axis by default, and an <img> stretched
   that way is distorted — that is exactly the bug that was squashing the header
   logo before .wordmark.has-img was switched to display:block. Pinning the
   default self-alignment to centre means no image on this site can ever be
   stretched by its parent's alignment, wherever it gets placed later.
   No image is a flex/grid item today, so this changes nothing right now; it is
   here so the bug cannot come back somewhere new. */
img{max-width:100%;height:auto;display:block;align-self:center;justify-self:center;}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit;}
ul,ol{list-style:none;}
strong{color:var(--green-deep);font-weight:700;}
::selection{background:var(--green-deep);color:var(--cream);}

/* Focus: brick red ring on light surfaces, beige ring on the dark green ones. */
*:focus{outline:none;}
*:focus-visible{outline:3px solid var(--red);outline-offset:3px;border-radius:2px;}
.on-dark *:focus-visible,.site-footer *:focus-visible,.hero *:focus-visible{outline-color:var(--beige);}

@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto !important;}
  .reveal{opacity:1 !important;transform:none !important;transition:none !important;}
  /* The three lines below were added 2026-08-10 with the staggered groups and
     the drawn-in rule. The blanket * rule underneath flattens DURATIONS but not
     DELAYS, so without these the fifth card in a group still waited 280ms
     before appearing, and a child of a .reveal-stagger stayed at opacity 0
     until an IntersectionObserver got round to it. Neither is acceptable for
     somebody who has asked for no motion: the content has to be simply there.
     transition-delay is zeroed globally for the same reason. */
  .reveal-stagger>*{opacity:1 !important;transform:none !important;transition:none !important;}
  .reveal .rule,.reveal.in .rule{transform:none !important;transition:none !important;}
  .marquee-track{animation:none !important;}
  *{
    transition-duration:.01ms !important;
    transition-delay:0ms !important;
    animation-duration:.01ms !important;
    animation-delay:0ms !important;
    animation-iteration-count:1 !important;
    scroll-behavior:auto !important;
  }
}

/* Skip link — keyboard users land here first */
.skip-link{
  position:absolute;left:-9999px;top:0;z-index:400;
  background:var(--green-deep);color:var(--cream);
  padding:14px 22px;font-weight:700;
}
.skip-link:focus{left:0;}

/* ============================================================================
   3. LAYOUT HELPERS
   ========================================================================== */
/* Gutters shrink smoothly instead of at a breakpoint. 16px at 320px buys back
   12px of usable width on the narrowest phones, which is where the long town
   names in .area-card were closest to spilling. Reaches the original 22px by
   440px and stays there. */
.container{max-width:var(--maxw);margin:0 auto;padding:0 clamp(16px,5vw,22px);}
/* Measure: was 820px, which is roughly 88 characters of 17px body copy — too
   long to track comfortably. 700px gives ~69ch where the padding applies (home
   FAQ, privacy, terms) and ~74ch on the five location pages, which override the
   padding to 0 inline. Both land in the 70-75 character band.
   This is the one change in this pass that is visible on a desktop screen. */
.container-narrow{max-width:700px;margin:0 auto;padding:0 clamp(16px,5vw,22px);}
/* Section rhythm ramps instead of jumping at 760px. The clamp endpoints are
   tuned to land on the old values at the old breakpoint, so nothing shifts on
   a phone; only the 760-1140px band is now a gradient rather than a step. */
.section{padding:clamp(56px,7.4vw,84px) 0;}
.section-sm{padding:clamp(40px,5.5vw,56px) 0;}
.bg-cream{background:var(--cream);}
.bg-beige{background:var(--beige);}
.bg-white{background:var(--white);}
.bg-green{background:var(--green-deep);}
.bg-green .eyebrow{color:var(--green-pale);}
.bg-green h1,.bg-green h2,.bg-green h3,.bg-green h4{color:var(--cream);}
.bg-green p,.bg-green li{color:var(--beige);}

.eyebrow{
  font-family:var(--font-body);
  font-size:.78rem;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:var(--green-mid);margin-bottom:14px;display:block;
}
.rule{width:64px;height:4px;background:var(--red);margin:20px 0 0;border-radius:2px;}
.rule.centered{margin:20px auto 0;}
.lede{font-size:1.15rem;line-height:1.66;color:var(--ink-mid);max-width:64ch;}
.bg-green .lede{color:var(--beige);}
.center{text-align:center;}
.section-head{margin-bottom:44px;max-width:70ch;}
.section-head.centered{margin-left:auto;margin-right:auto;text-align:center;}
.stack>*+*{margin-top:18px;}
.prose h3{margin-top:36px;margin-bottom:10px;}
.prose h2{margin-top:44px;margin-bottom:12px;}
/* HEADING LEVEL vs HEADING SIZE.
   Splitting the site into real pages on 2026-08-10 gave every page its own
   <h1>, which made the body headings underneath it second-level content. They
   were <h3> with no <h2> above them, so every one of those pages skipped a
   level. They are <h2> now. .h-sub holds the h3 SIZE they were designed at:
   the outline changed, the page did not. */
.h-sub{font-size:clamp(1.2rem,2vw,1.55rem);}
.prose h2.h-sub{margin-top:36px;margin-bottom:10px;}
.prose p+p{margin-top:16px;}
.prose ul{margin:16px 0 0;}
.prose ul li{
  position:relative;padding-left:28px;margin-bottom:11px;
  color:var(--ink-mid);font-size:1.0625rem;line-height:1.65;
}
.prose ul li::before{
  content:"";position:absolute;left:2px;top:.62em;
  width:9px;height:9px;border-radius:50%;background:var(--green-bright);
}
.bg-green .prose ul li{color:var(--beige);}
.bg-green .prose ul li::before{background:var(--green-pale);}

/* ============================================================================
   4. BUTTONS — red is the action color, and only the action color
   ========================================================================== */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:10px;
  padding:16px 30px;
  font-family:var(--font-body);font-size:1rem;font-weight:800;letter-spacing:.02em;
  border-radius:var(--radius);
  cursor:pointer;text-align:center;
  transition:transform .25s var(--t),background .25s var(--t),color .25s var(--t),box-shadow .25s var(--t),border-color .25s var(--t);
}
.btn .arrow{display:inline-block;transition:transform .25s var(--t);}
.btn:hover .arrow{transform:translateX(5px);}
.btn-red{background:var(--red);color:var(--white);}
.btn-red:hover{background:var(--red-dark);color:var(--white);transform:translateY(-2px);box-shadow:var(--shadow-red);}
.btn-green{background:var(--green-deep);color:var(--cream);}
.btn-green:hover{background:var(--green);color:var(--white);transform:translateY(-2px);box-shadow:var(--shadow-md);}
.btn-ghost{background:transparent;color:var(--green-deep);border:2px solid var(--green-deep);}
.btn-ghost:hover{background:var(--green-deep);color:var(--cream);transform:translateY(-2px);}
.btn-ghost-light{background:transparent;color:var(--cream);border:2px solid var(--beige);}
.btn-ghost-light:hover{background:var(--beige);color:var(--green-deep);transform:translateY(-2px);}
.btn-sm{padding:11px 20px;font-size:.9rem;}
.btn-block{width:100%;}
.btn-row{display:flex;flex-wrap:wrap;gap:14px;margin-top:28px;}
.btn-row.centered{justify-content:center;}

.textlink{
  color:var(--green);font-weight:700;
  border-bottom:2px solid var(--red);padding-bottom:1px;
}
.textlink:hover{color:var(--red-dark);}

/* ============================================================================
   5. HEADER / NAV
   Cream bar sitting on top of the deep green hero. Phone visible at all times,
   "Free Estimate" pinned in the nav (Valley Tree pattern).
   ========================================================================== */
.nav-wrap{
  position:sticky;top:0;left:0;right:0;z-index:120;
  background:var(--cream);
  border-bottom:4px solid var(--green-deep);
  box-shadow:0 3px 18px rgba(20,51,31,.07);
}
.nav-inner{
  max-width:var(--maxw);margin:0 auto;
  display:flex;align-items:center;
  /* Vertical padding trimmed from 12px to 9px on 2026-08-07 so the larger
     logo can grow without the sticky header eating too much of the viewport.
     The logo's own height now sets the bar height.
     Horizontal padding and gap now ramp instead of stepping at 520px — same
     end values, no jump. TOTAL HEADER HEIGHT = logo height + 22px (9+9+4px
     border). The .mobile-panel max-height below depends on that arithmetic. */
  gap:clamp(10px,3vw,18px);
  padding:9px clamp(14px,5vw,22px);
}

/* ---- WORDMARK --------------------------------------------------------------
   RESOLVED 2026-08-07. The corrected logo art arrived reading "LAWN AND TREE
   SERVICES" in the right order, so the image is live in the header and the
   About feature slot.

   This CSS text mark is now the FALLBACK, not the primary. It stays in the DOM
   and is visually hidden while .wordmark has .has-img. If assets/logo.jpg ever
   fails to load, the img's onerror strips .has-img and this text mark comes
   back — the header is never empty.

   The footer keeps the text mark permanently: it sits on deep green, where the
   logo's multiply blend would darken it into the background.

   If a vector version of the logo ever arrives, drop it at assets/logo.svg and
   change BOTH the preload in <head> and the header <img> src together.
   -------------------------------------------------------------------------- */
.wordmark{display:inline-flex;flex-direction:column;line-height:1;flex-shrink:0;position:relative;}
.wordmark-top{
  font-family:var(--font-cond);font-stretch:condensed;
  font-size:clamp(1.35rem,3.1vw,1.85rem);font-weight:800;
  letter-spacing:.045em;color:var(--green-deep);text-transform:uppercase;
}
.wordmark-sub{
  font-family:var(--font-body);
  font-size:clamp(.5rem,1.35vw,.615rem);font-weight:700;
  letter-spacing:.155em;color:var(--green-mid);text-transform:uppercase;
  margin-top:4px;white-space:nowrap;
}
/* LOGO IMAGE — live since 2026-08-07 (corrected art, right word order).
   When .wordmark carries .has-img the real logo shows and the CSS text mark is
   hidden. If the image 404s, its onerror strips .has-img and the text mark
   comes back automatically, so the header is never empty. */
.wordmark-img{display:none;}

/* When the logo image is showing, drop the flex context entirely.
   .wordmark is a COLUMN flexbox for the text mark, and a column flexbox
   stretches its children on the cross axis (width) by default — that was
   squashing the logo wider than its natural ratio. display:block removes the
   stretch, and height + width:auto then preserves the aspect ratio cleanly. */
.wordmark.has-img{display:block;line-height:0;}
.wordmark.has-img .wordmark-img{
  display:block;
  height:clamp(54px,7.4vw,76px);
  width:auto;              /* ratio-preserving — never set both dimensions */
  max-width:none;          /* beat any global img{max-width:100%} squeeze */
  object-fit:contain;      /* belt and braces if a parent ever constrains it */
  /* The art is a wide horizontal lockup sitting on white. Multiply drops the
     white into the cream header bar so there's no visible rectangle. */
  mix-blend-mode:multiply;
}
.wordmark.has-img .wordmark-top,
.wordmark.has-img .wordmark-sub{
  position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
}
/* The footer sits on deep green — multiply would darken the logo into the
   background there, so the footer keeps the text mark regardless. */
.site-footer .wordmark.has-img .wordmark-img{display:none;}
.site-footer .wordmark.has-img .wordmark-top,
.site-footer .wordmark.has-img .wordmark-sub{
  position:static;width:auto;height:auto;overflow:visible;
  clip:auto;clip-path:none;white-space:normal;
}
.site-footer .wordmark-top{color:var(--cream);}
.site-footer .wordmark-sub{color:var(--green-pale);}

.nav-links{display:flex;align-items:center;gap:26px;margin-left:auto;}
.nav-link{
  font-size:.94rem;font-weight:700;letter-spacing:.02em;
  color:var(--green-deep);position:relative;cursor:pointer;
  background:none;border:none;font-family:var(--font-body);
  /* 8px -> 10px vertical, plus min-height, gets these to a 44px touch target.
     They are only visible above 1080px, but that includes touch laptops and
     iPad Pro in landscape, so the target size still matters. */
  padding:10px 2px;min-height:44px;display:inline-flex;align-items:center;
}
.nav-link::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:3px;
  background:var(--red);transform:scaleX(0);transform-origin:right;
  transition:transform .28s var(--t);border-radius:2px;
}
.nav-link:hover::after,.nav-link.active::after{transform:scaleX(1);transform-origin:left;}
.nav-link.active{color:var(--red-dark);}

/* Dropdowns (Services / Areas) */
.nav-item{position:relative;}
.nav-caret{display:inline-block;margin-left:5px;font-size:.62em;transition:transform .22s var(--t);}
.nav-item.open .nav-caret{transform:rotate(180deg);}
.nav-drop{
  position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%) translateY(-6px);
  background:var(--white);border:2px solid var(--green-deep);border-radius:var(--radius);
  box-shadow:var(--shadow-md);
  min-width:250px;padding:8px;opacity:0;visibility:hidden;transition:all .22s var(--t);z-index:130;
}
.nav-item.open .nav-drop{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);}
/* The dropdown was role="menu" with role="menuitem" children and none of the
   arrow-key handling an ARIA menu contracts for, which is worse for a screen
   reader than plain markup. It is a <ul> of links inside a disclosure now, so
   these two rules carry the list. */
.nav-drop ul{display:block;}
.nav-drop li{display:block;}
.nav-drop a{
  display:block;padding:10px 14px;border-radius:4px;
  font-size:.95rem;font-weight:600;color:var(--green-deep);
}
.nav-drop a:hover{background:var(--beige);color:var(--red-dark);}

/* Header call element — one of only three tel: links on the whole site */
/* min-height + centring gives the two-line call block a real 44px tap target;
   the label and number were only ~30px tall combined.

   flex-shrink was 0. That mattered once TODO [PHONE] gets filled in: applyPhone()
   swaps the label to the longer "Call for a free estimate", and at 320px an
   unshrinkable block that wide pushed the header row past the viewport. With
   flex-shrink:1 the label wraps to two lines instead and the row fits.
   min-width is deliberately NOT set to 0 — the automatic minimum size then
   falls back to .nav-call-num's max-content, so the phone number itself can
   never be squeezed or clipped, only the label re-wraps. */
.nav-call{
  display:flex;flex-direction:column;align-items:flex-end;justify-content:center;
  line-height:1.15;flex-shrink:1;min-height:44px;
}
/* Both sizes ramp rather than stepping at 520px — same end values as the old
   media query, no jump, and no risk of the number wrapping on a narrow phone.
   text-align:right keeps a wrapped label flush with the number above it. */
.nav-call-label{font-size:clamp(.56rem,1.6vw,.63rem);font-weight:800;letter-spacing:.13em;text-transform:uppercase;color:var(--green-mid);text-align:right;}
.nav-call-num{font-size:clamp(.9rem,2.6vw,1.02rem);font-weight:800;color:var(--green-deep);white-space:nowrap;}
.nav-call:hover .nav-call-num{color:var(--red-dark);}

.burger{
  display:none;width:44px;height:44px;flex-shrink:0;
  border:2px solid var(--green-deep);border-radius:var(--radius);
  align-items:center;justify-content:center;flex-direction:column;gap:5px;
  background:var(--white);margin-left:auto;
}
.burger span{display:block;width:20px;height:2.5px;background:var(--green-deep);border-radius:2px;transition:transform .25s var(--t),opacity .25s var(--t);}
.burger[aria-expanded="true"] span:nth-child(1){transform:translateY(7.5px) rotate(45deg);}
.burger[aria-expanded="true"] span:nth-child(2){opacity:0;}
.burger[aria-expanded="true"] span:nth-child(3){transform:translateY(-7.5px) rotate(-45deg);}

.mobile-panel{
  display:none;background:var(--green-deep);border-top:4px solid var(--red);
  overflow-y:auto;
  /* WAS calc(100vh - 78px), a hardcoded header height. The header is actually
     (logo clamp + 22px), i.e. 76-98px, so at wide-but-touch sizes the panel
     could run past the bottom of the screen. Derived from the same clamp now.
     100dvh accounts for mobile browser chrome; the 100vh line is the fallback
     for engines without dvh and is deliberately listed first. */
  max-height:calc(100vh - (clamp(54px,7.4vw,76px) + 22px));
  max-height:calc(100dvh - (clamp(54px,7.4vw,76px) + 22px));
  overscroll-behavior:contain; /* stop the page scrolling behind the open menu */
}
.mobile-panel.open{display:block;}
.mobile-panel .mp-inner{padding:22px;max-width:var(--maxw);margin:0 auto;}
.mobile-panel .mp-group+.mp-group{margin-top:24px;padding-top:22px;border-top:1px solid rgba(232,223,201,.2);}
.mobile-panel .mp-title{
  font-size:.72rem;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:var(--green-pale);margin-bottom:12px;
}
/* 12px padding + a 1.05rem line puts every mobile nav row at ~52px, comfortably
   over the 44px touch minimum with real separation between adjacent rows. */
.mobile-panel a{display:block;padding:12px 0;font-size:1.05rem;font-weight:700;color:var(--cream);}
.mobile-panel a:hover{color:var(--green-pale);}
.mobile-panel .btn{margin-top:18px;}

@media(max-width:1080px){
  .nav-links{display:none;}
  .nav-call{margin-left:auto;}
  .burger{display:flex;margin-left:0;}
  .nav-cta-desktop{display:none;}
}
/* The old @media(max-width:520px) block that lived here — nav-inner padding/gap
   and the two .nav-call font sizes — is gone. All four now use clamp() with the
   same end values, so the header scales continuously instead of snapping at
   520px, and the header height stays exactly (logo + 22px) at EVERY width,
   which is what .mobile-panel's max-height calc depends on. */

/* ============================================================================
   6. PAGE HERO
   The .page / .page.active / .page.visible rules that used to head this block
   are gone. The site was one document with sixteen <section class="page">
   blocks shown and hidden by showPage(); on 2026-08-10 it became sixteen real
   HTML files, so there is nothing left to show or hide and every page renders
   its own content with no JavaScript involved.
   ========================================================================== */
.page-hero{
  background:var(--green-deep);
  /* Ramped, not stepped. Lands on the old 48/44 values at 760px and the old
     72/66 by ~1130px, so both endpoints match what was already signed off. */
  padding:clamp(48px,6.4vw,72px) 0 clamp(44px,5.9vw,66px);
  position:relative;overflow:hidden;
}
.page-hero .container{position:relative;z-index:2;}
.page-hero h1{color:var(--cream);max-width:20ch;}
.page-hero p{color:var(--beige);max-width:62ch;margin-top:18px;font-size:1.12rem;}
.page-hero .eyebrow{color:var(--green-pale);}
/* Soft contour lines — decorative only, sits behind the copy */
.page-hero::after{
  content:"";position:absolute;right:-8%;bottom:-45%;width:620px;height:620px;
  border-radius:50%;
  background:radial-gradient(circle at 35% 35%,rgba(143,184,154,.16),rgba(143,184,154,0) 62%);
  pointer-events:none;
}

/* ============================================================================
   7. HOME HERO
   ========================================================================== */
.hero{
  background:var(--green-deep);
  /* Trimmed 2026-08-07. Was clamp(48,6.9vw,78) / clamp(56,7.4vw,84), which
     left large empty green areas above and below the content — most visible
     in the corners, where the 16:9 photo is shorter than the copy column so
     the gaps stack up. Roughly 20% tighter top and bottom. */
  padding:clamp(38px,5.4vw,62px) 0 clamp(44px,5.8vw,66px);
  position:relative;overflow:hidden;
}
.hero::after{
  content:"";position:absolute;right:-10%;top:-30%;width:760px;height:760px;border-radius:50%;
  background:radial-gradient(circle at 40% 40%,rgba(46,125,79,.30),rgba(46,125,79,0) 62%);
  pointer-events:none;
}
.hero .container{position:relative;z-index:2;}

/* ---- HERO TWO-COLUMN -------------------------------------------------------
   Copy left, owner photo right. 1fr / 0.82fr keeps the text column dominant —
   the headline and CTA are the job, the photo is support.
   Collapses to one column at 900px; the photo is AFTER the copy in source
   order, so on a phone it lands below the buttons with no CSS reordering and
   no reading-order/tab-order mismatch. */
.hero-grid{
  display:grid;
  /* Photo column is deliberately DOMINANT. It went 0.82fr -> 1.06fr -> 1.28fr;
     Edward wanted it plainly bigger, not incrementally bigger. The owner shot
     is the whole differentiator on this site, so it outweighs the copy column.
     The h1 carries max-width:16ch and already wraps to two lines, so the
     narrower copy column costs nothing. */
  /* Ratio derived from a rectangle Edward drew over a screenshot 2026-08-07.
     Target on a ~2000px viewport: photo 930px wide starting at x=890, which
     works out to a 420px copy column against a 678px photo column, plus the
     bleed below. Do not "tidy" these numbers — they hit a measured target. */
  /* DEFAULT (901px - 1399px): a gentler split. The 1.61fr target below was
     measured on a ~2000px screen; applying it here squeezed the copy column
     to ~38% of a narrow container, which wrapped the h1 onto three lines and
     left large empty green areas around a short 16:9 frame.
     Roughly even columns keep the mid-size layout balanced. */
  grid-template-columns:1fr 1.08fr;
  gap:clamp(26px,3vw,38px);
  /* Back to centre. Stretching the photo to the copy column's height made it
     nearly square and Edward preferred the original landscape proportion —
     the fix for "too small" was a wider column, not a taller frame. */
  align-items:center;
}
@media (max-width:900px){
  .hero-grid{grid-template-columns:1fr;gap:32px;align-items:start;}
}

.hero-photo{
  margin:0;position:relative;
  border:3px solid rgba(232,223,201,.22);
  box-shadow:0 18px 44px rgba(0,0,0,.28);
  overflow:hidden;
  /* 16:9. The rectangle Edward drew measures ~930x505 (1.84:1); 16/9 is the
     nearest standard ratio and lands within ~18px of it.
     Wider and shorter than the source's native 4:3, so cover trims some top
     and bottom — object-position keeps Trevor centred. 19:6 was tried and was
     far too severe; this is the middle ground he actually drew. */
  aspect-ratio:16/9;
}

/* ---- HERO PHOTO BLEED ------------------------------------------------------
   The container caps at --maxw (1180px), which boxed the photo in at ~726px.
   To get it genuinely bigger without squeezing the copy column below a
   readable width, the photo breaks out of the container and runs to the RIGHT
   EDGE OF THE VIEWPORT.

   The negative margin equals the distance from the container's content edge to
   the screen edge:  (100vw - maxw) / 2 + the container's own side padding.
   max(22px, …) keeps it correct below 1180px, where that expression would go
   negative and pull the photo the wrong way.

   Capped at 900px so it doesn't become absurd on an ultrawide monitor — the
   cap preserves the 4:3 ratio rather than clipping height, and margin-left:auto
   keeps it pinned right when the cap bites.

   The right border is dropped: a border on an edge that runs off-screen reads
   as a mistake. Safe against horizontal scroll because .hero already carries
   overflow:hidden. */
/* ---- HERO PHOTO: PARTIAL BLEED --------------------------------------------
   The photo needs to be wider than the container allows. At the target width
   (~865px) it cannot fit inside --maxw without crushing the copy column to
   roughly 200px, so it breaks out to the RIGHT — but only partway.

   Key distinction from the version that was rejected: this bleeds
   HORIZONTALLY ONLY. No negative top/bottom margin, no full-height stretch.
   The 4:3 ratio, the centred float, and green breathing room above and below
   are all preserved. Running it edge-to-edge and floor-to-ceiling read as far
   too big; stopping short keeps it a card.

   The bleed is a SHARE of the available gutter (60%), capped at 230px — not
   the whole gutter. Taking the full gutter made the photo touch the viewport
   edge at 1280 and 1440px, which killed the framed-card look. Sixty percent
   always leaves green to the right at every width, so all four borders stay
   visible and it reads as a photo, not a background.
   max(0px, …) keeps it harmless below --maxw, where there is no gutter. */
/* WIDE SCREENS ONLY (1400px+). This is where the measured target applies:
   photo ~930px on a ~2000px viewport, matching the rectangle Edward drew.
   Gated behind 1400px because below that there isn't enough container width
   to give the photo that share without starving the copy column. */
@media (min-width:1400px){
  /* minmax(465px, …) is load-bearing, not decorative. A plain 1fr let the copy
     column fall to ~421px, and the button row ("Get a Free Estimate" plus the
     phone button, ~430px together) wrapped onto two lines. The floor keeps
     them side by side.
     The bleed multiplier was raised from 0.31 to 0.40 to give the photo back
     the width the floor took, so it still lands near the 930px target. */
  .hero-grid{grid-template-columns:minmax(465px,1fr) 1.61fr;gap:38px;}
  .hero-photo{
    /* --maxw is now 1680px, so the container itself is wide enough — the old
       negative-margin bleed is no longer needed and would overshoot badly.
       Capped at the 930px Edward approved and pinned to the right of its
       column so it sits against the container edge, not floating mid-column. */
    max-width:930px;
    justify-self:end;
    margin-right:0;
  }
}
.hero-photo img{
  width:100%;height:100%;
  object-fit:cover;
  /* Back to a centred vertical crop now the frame is 4:3 again — at the
     source's native ratio nothing is cropped, so the 34% upward bias that
     19:6 needed would only push the framing off. 38% horizontal keeps him
     centred as the column narrows on smaller desktops. */
  object-position:38% 50%;
  display:block;
}
.hero-photo figcaption{
  position:absolute;left:0;right:0;bottom:0;
  background:linear-gradient(to top,rgba(10,26,16,.88),rgba(10,26,16,0));
  color:var(--beige);
  font-size:.82rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  padding:26px 16px 12px;
}
/* Single column below 900px: the stretch behaviour no longer applies, so give
   the frame an explicit ratio again. 16:10 keeps it from running very tall at
   full phone width, and drops the min-height that only makes sense beside a
   copy column. */
@media (max-width:900px){
  .hero-photo{aspect-ratio:16/10;min-height:0;}
  .hero-photo img{object-position:38% 42%;}
}
.hero-eyebrow{
  display:inline-flex;align-items:center;gap:9px;
  background:rgba(232,223,201,.12);border:1px solid rgba(232,223,201,.3);
  border-radius:100px;padding:7px 16px;
  font-size:.75rem;font-weight:800;letter-spacing:.11em;text-transform:uppercase;
  color:var(--beige);margin-bottom:24px;
}
/* DESCENDER FIX 2026-08-07 — the global h1 rule sets line-height:1.05, which
   is tighter than this display face's descenders need. Combined with
   .hero{overflow:hidden} the tails of "p" and "y" in "whole property." were
   being clipped at the bottom edge. 1.08 gives the line box enough room, and
   the small padding-bottom guarantees the final line's descenders clear the
   overflow boundary at every clamp size. Do not take line-height back below
   1.08 on a heading that can end in a descender. */
.hero h1{
  color:var(--cream);
  max-width:16ch;
  font-size:clamp(2.3rem,6vw,4.1rem);
  line-height:1.08;
  padding-bottom:.06em;
}
.hero-sub{color:var(--beige);font-size:1.16rem;line-height:1.62;max-width:56ch;margin-top:22px;}
.hero-serving{
  margin-top:24px;padding-top:20px;border-top:1px solid rgba(232,223,201,.22);
  color:var(--green-pale);font-size:.98rem;font-weight:600;max-width:60ch;
}
.hero-serving strong{color:var(--beige);}

/* ============================================================================
   8. MARQUEE — service-area strip
   ========================================================================== */
.marquee{
  background:var(--red);overflow:hidden;padding:13px 0;
  border-top:3px solid var(--green-deep);border-bottom:3px solid var(--green-deep);
}
.marquee-track{display:flex;width:max-content;animation:marquee 34s linear infinite;}
.marquee-track span{
  color:var(--white);font-size:.86rem;font-weight:800;letter-spacing:.17em;text-transform:uppercase;
  padding:0 26px;white-space:nowrap;
}
@keyframes marquee{from{transform:translateX(0);}to{transform:translateX(-50%);}}

/* ============================================================================
   9. CARDS
   ========================================================================== */
.grid{display:grid;gap:24px;}
.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:980px){.grid-3,.grid-4{grid-template-columns:repeat(2,1fr);}}
@media(max-width:680px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr;}}

/* FIVE SERVICE CARDS, NOT SIX. Stump grinding came off the service list on
   2026-08-07, so both service grids (home is 3-up, the services page is 2-up)
   now hold five cards. Any TWO-column layout leaves the fifth card alone on the
   last row, hard against the left edge, which reads like one failed to load.
   This keeps that card at its normal column width and centres it on the row.
   It deliberately does NOT stretch the card across the row — a full-width card
   would set its body copy on a ~100-character line, well outside the measure
   the rest of the site holds to (see .container-narrow).
   The selector matches only a .card that is BOTH the fifth child and the last
   one, so a grid holding any other number of cards is untouched. The 3-up home
   grid above 980px is 3 + 2, which needs nothing. Below 680px every grid is a
   single column and the width override hands it back to 100%. */
.grid-2>.card:nth-child(5):last-child{grid-column:1/-1;justify-self:center;width:calc(50% - 12px);}
@media(max-width:980px){.grid-3>.card:nth-child(5):last-child{grid-column:1/-1;justify-self:center;width:calc(50% - 12px);}}
@media(max-width:680px){.grid-2>.card:nth-child(5):last-child,.grid-3>.card:nth-child(5):last-child{width:100%;}}

.card{
  background:var(--white);border:1px solid var(--beige-deep);border-radius:var(--radius);
  padding:28px 26px;display:flex;flex-direction:column;
  border-top:4px solid var(--green-bright);
  transition:transform .25s var(--t),box-shadow .25s var(--t),border-color .25s var(--t);
}
a.card,.card[role="link"]{cursor:pointer;}
.card:hover{transform:translateY(-4px);box-shadow:var(--shadow-md);border-top-color:var(--red);}
.card h3,.card h2{margin-bottom:10px;}
/* HOVER WEIGHT — added 2026-08-10. The lift above is generic on its own, so a
   red bar now wipes across the top edge from left to right on hover instead of
   the border simply changing colour. It sits exactly over the 4px
   --green-bright top border (top:-4px is the border area; the -1px sides cover
   the card's own 1px side borders) so nothing moves and nothing reflows.
   Gated behind (hover:hover) on purpose: a :hover transform that sticks after a
   tap is worse than no effect at all on a phone. */
@media (hover:hover){
  .card{position:relative;}
  .card::after{
    content:"";position:absolute;left:-1px;right:-1px;top:-4px;height:4px;
    background:var(--red);border-radius:var(--radius) var(--radius) 0 0;
    transform:scaleX(0);transform-origin:left center;
    transition:transform .42s var(--t);pointer-events:none;
  }
  .card:hover::after{transform:scaleX(1);}
  /* The colour swap is redundant now the bar wipes over it, and running both
     made the top edge flash. The lift is trimmed to match the smaller gesture. */
  .card:hover{transform:translateY(-3px);border-top-color:var(--green-bright);}
  .card-dark:hover{border-top-color:var(--green-pale);}
  .card:hover .card-more .arrow,.card:hover .card-more>span[aria-hidden]{transform:translateX(4px);}
  .card-more>span[aria-hidden]{display:inline-block;transition:transform .25s var(--t);}
}
.card p{font-size:1rem;flex:1;}
.card-more{
  margin-top:18px;font-weight:800;font-size:.92rem;color:var(--green);
  display:inline-flex;align-items:center;gap:7px;
}
.card:hover .card-more{color:var(--red-dark);}
.card-icon{width:46px;height:46px;margin-bottom:16px;}
.card-icon svg{width:100%;height:100%;display:block;}

.card-dark{background:rgba(232,223,201,.07);border:1px solid rgba(232,223,201,.22);border-top:4px solid var(--green-pale);}
.card-dark h3{color:var(--cream);}
.card-dark p{color:var(--beige);}
.card-dark:hover{border-top-color:var(--red);}

/* Numbered "why" columns */
.why-col{padding:0;}
.why-num{
  font-family:var(--font-cond);font-stretch:condensed;
  font-size:2.6rem;font-weight:800;color:var(--red);line-height:1;margin-bottom:12px;display:block;
}
.bg-green .why-num{color:var(--green-pale);}

/* ============================================================================
   10. BEFORE / AFTER COMPONENT
   Real component, built now, running on placeholders.

   HOW THE PHOTO SWAP WORKS — read before touching this:
   Every slot has BOTH an <img> pointing at the final filename AND an inline
   SVG placeholder sitting behind it. The <img> has onerror="this.remove()",
   so while the file is missing the image element deletes itself and the SVG
   shows through. The moment the real .jpg is dropped into assets/ with the
   matching filename, the photo loads and covers the placeholder.
   => Adding the photos is a pure file drop. ZERO markup changes needed.

   ---------------------------------------------------------------------------
   ASPECT RATIO — CORRECTED 2026-08-07. Read this before changing a number.

   The frame was a flat 3/2 for every slot. The photos that actually arrived
   are not 3/2, and two of the four pairs are PORTRAIT:

     pair 1  01-brush-clearing-before  1100x825  landscape 4:3
             01-brush-clearing-after    721x536  landscape ~4:3
     pair 2  02-shrub-trimming-*        825x1100 PORTRAIT  3:4
     pair 3  03-tree-line-clearing-*    825x1100 PORTRAIT  3:4
     pair 4  04-storm-debris-*         1100x825  landscape 4:3

   A 3:4 photo cover-fitted into a 3:2 frame keeps only half its height — the
   shrub and the tree line were being sliced through the middle of the subject.
   So the frame now matches the photography: 4/3 by default, and 3/4 on the two
   pairs carrying .is-portrait. Both are EXACT matches to the source files, so
   the crop is now zero in every one of the eight slots. Nothing is stretched
   and nothing is letterboxed — object-fit:cover simply has nothing to cut.

   Both images inside any one pair share an orientation, so the two frames in a
   row are always the same height and the 2px hairline gap stays straight.

   If a future pair mixes orientations, give that pair its own ratio rather
   than reverting to a one-size frame.

   UPSCALING — CORRECTED 2026-08-10. The old sum here subtracted the
   .container gutters from .ba-set, which it must not: .ba-set carries its own
   max-width and sits well inside the container, so the gutters never touch it.
   The real frame width is (.ba-set max-width - 2px .ba-pair border - 2px grid
   gap) / 2. At the old 1480px cap that was 738 CSS px against a 721px source
   file, i.e. every "after" in pair 1 was being displayed about 2.4% larger
   than it exists. The cap is now 1444px, which puts each frame at 720 CSS px
   — just inside the 721px file. Raise it ONLY if that file is replaced with a
   higher-resolution original, and redo this sum when you do.

   The #ph-before / #ph-after placeholders use preserveAspectRatio="slice", so
   they still fill the frame at either ratio and still occupy exactly the same
   box as the photo. The file-drop swap mechanism is untouched.
   ========================================================================== */
/* Capped independently of --maxw. Frame width = (this - 2px .ba-pair border
   - 2px .ba-grid gap) / 2, so 1444px puts every frame at 720 CSS px. The
   lowest-resolution source on the site is 721px wide
   (01-brush-clearing-after.jpg, recovered from a phone screenshot), so 720
   is the ceiling. See the UPSCALING note above before changing this. */
.ba-set{display:grid;gap:34px;max-width:1444px;margin-inline:auto;}
.ba-pair{
  background:var(--white);border:1px solid var(--beige-deep);border-radius:var(--radius);
  overflow:hidden;box-shadow:var(--shadow-sm);
}
.ba-grid{display:grid;grid-template-columns:1fr 1fr;gap:2px;background:var(--beige-deep);}
@media(max-width:680px){.ba-grid{grid-template-columns:1fr;}}
.ba-item{position:relative;background:var(--beige);}
.ba-frame{position:relative;aspect-ratio:4/3;overflow:hidden;background:var(--beige);}
/* Pairs 2 and 3 are portrait. See the ratio note above. */
.ba-pair.is-portrait .ba-frame{aspect-ratio:3/4;}
.ba-ph{position:absolute;inset:0;width:100%;height:100%;display:block;z-index:1;}
/* width+height 100% is safe here ONLY because object-fit:cover is on the same
   rule — the frame crops, the picture never distorts. Do not remove object-fit
   from this rule without also removing the fixed height. */
.ba-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:50% 50%;z-index:2;}
.ba-tag{
  position:absolute;left:0;top:0;z-index:3;
  font-size:.74rem;font-weight:800;letter-spacing:.17em;text-transform:uppercase;
  padding:8px 16px;
}
.ba-tag-before{background:var(--charcoal);color:var(--cream);}
.ba-tag-after{background:var(--green-deep);color:var(--cream);}
.ba-caption{padding:20px 24px;}
.ba-caption h3{font-size:1.2rem;margin-bottom:6px;}
.ba-caption p{font-size:.98rem;margin:0;}
.ba-where{
  display:inline-block;margin-top:10px;
  font-size:.76rem;font-weight:800;letter-spacing:.11em;text-transform:uppercase;color:var(--green-mid);
}

/* ---- SINGLE-PHOTO FRAME -----------------------------------------------------
   Same swap mechanism as the before/after set: an inline SVG placeholder and
   the <img> both sit absolutely inside a ratio-locked box, so the placeholder
   and the real photo occupy IDENTICAL space and nothing reflows when a file
   lands. The frame's aspect-ratio is what reserves the box, which is why an
   <img> in here cannot cause layout shift even without width/height attributes.

   FOUR VARIANTS — pick the one that matches the photo, do not force one shape:
     .square   1/1   trevor-thomas-owner.jpg is natively 800x800 -> zero crop
     .wide     3/2   the not-yet-shot slots, spec'd at 1200x800 in
                     BUILD-NOTES.md. Also holds 01-brush-clearing-after.jpg
                     and 04-storm-debris-after.jpg (both ~4:3), which take a
                     mild, unremarkable ~11% centre crop top and bottom.
                     Pair it with .cap-721 when the file is the 721px one.
     .tall     3/4   for PORTRAIT source photos. Added 2026-08-07 because the
                     trimming page was showing 02-shrub-trimming-after.jpg
                     (825x1100, portrait) in a 3/2 frame, which threw away half
                     the picture and cut straight through the shrub.
     .logo     native ratio + object-fit:contain. See the note on .logo below.
   -------------------------------------------------------------------------- */
.photo-frame{
  position:relative;overflow:hidden;border-radius:var(--radius);
  background:var(--beige);border:1px solid var(--beige-deep);
}
.photo-frame.square{aspect-ratio:1/1;}
.photo-frame.wide{aspect-ratio:3/2;}
.photo-frame.tall{aspect-ratio:3/4;}
.photo-frame .ph{position:absolute;inset:0;width:100%;height:100%;display:block;z-index:1;}
/* As with .ba-img: the fixed 100%/100% is only safe alongside object-fit:cover. */
.photo-frame img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:2;}

/* Trevor's portrait is 800x800 in a 1/1 frame, so there is nothing to crop and
   this is a no-op today. It is here as a guard: if the frame ratio is ever
   changed, a centre crop would take the top of his head off, and biasing the
   focal point upward keeps the face in frame automatically. */
.photo-frame.square img{object-position:50% 38%;}

/* LOGO FRAME — this fixed a real crop bug.
   logo.jpg is 1000x542 (1.845:1), a very wide horizontal lockup with the
   business NAME set in it. It was sitting in a .wide 3/2 frame under
   object-fit:cover, which cropped the left and right edges clean off the
   wordmark. Anything containing text must be contain, never cover.
   The frame now carries the artwork's own ratio, so contain and cover would
   agree — contain is stated anyway so a future logo of a different shape
   letterboxes instead of losing its ends. Background is --white because the
   art sits on white; any letterbox band is therefore invisible. */
/* CAP — 01-brush-clearing-after.jpg is 721x536, the lowest-resolution file on
   the site. In a .grid-2 column inside the 1680px container it would render
   ~796 CSS px, about 10% above its own pixels, so the container is capped to
   the file instead. Below roughly a 1520px viewport the column is already
   narrower than this and the cap does nothing. Remove it the day a
   higher-resolution version of that photo arrives. */
.photo-frame.cap-721{max-width:721px;margin-inline:auto;}
/* Same idea as cap-721, for the owner portrait on the Contact page. The file is
   800x800; a square frame at the full ~800px column width would tower over the
   estimate form beside it, so it is held to 340px and left-aligned with the
   info blocks under it. Well inside its own pixels either way. */
.photo-frame.cap-340{max-width:340px;}
.photo-frame.logo{aspect-ratio:1000/542;background:var(--white);}
.photo-frame.logo img{object-fit:contain;}
.photo-note{
  margin-top:12px;font-size:.85rem;color:var(--ink-mid);font-style:italic;
}
.bg-green .photo-note{color:var(--green-pale);}

/* OWNER PORTRAIT + SITE QR, side by side. Added 2026-08-11 for the Contact page.
   The portrait is capped at 340px and left-aligned, which left a large empty
   square to its right in the ~800px column; the QR tile fills it. Each figure
   reuses .photo-frame.cap-340 unchanged, so both tiles are the same size. The
   row wraps to stacked once the column is narrower than two tiles (tablet, and
   again when .grid-2 collapses to one column at 680px). */
.owner-row{display:flex;flex-wrap:wrap;gap:24px;align-items:flex-start;}
/* flex:1 1 240px keeps the two tiles SIDE BY SIDE — they share the column width
   and shrink together — down to phone widths, where 2x240 no longer fits and the
   QR drops below the portrait. max-width holds each to the portrait's own 340px
   cap so neither tile ever towers over the form. */
.owner-row>figure{margin:0;flex:1 1 240px;max-width:340px;min-width:0;}
/* The QR carries its own quiet zone; keep it uncropped on a clean field. */
.owner-row .qr-frame{background:var(--white);}
.owner-row .qr-frame img{object-fit:contain;}

/* ============================================================================
   11. CHECKLIST — "What's included in every visit"
   ========================================================================== */
.checklist{display:grid;grid-template-columns:repeat(2,1fr);gap:18px 34px;}
@media(max-width:760px){.checklist{grid-template-columns:1fr;}}
.check-item{display:flex;gap:14px;align-items:flex-start;}
.check-mark{
  flex-shrink:0;width:26px;height:26px;border-radius:50%;
  background:var(--green-bright);color:var(--white);
  display:flex;align-items:center;justify-content:center;
  font-size:.8rem;font-weight:900;margin-top:2px;
}
.bg-green .check-mark{background:var(--green-pale);color:var(--green-deep);}
.check-item p{margin:0;font-size:1.02rem;}
.check-item strong{display:block;margin-bottom:2px;}
.bg-green .check-item strong{color:var(--cream);}

/* ============================================================================
   12. PROCESS STRIP
   ========================================================================== */
/* position:relative is what the mowing track in section 22 is measured and
   positioned against. Without it the track would escape to the viewport. */
.process{display:grid;grid-template-columns:repeat(4,1fr);gap:0;counter-reset:step;position:relative;}
@media(max-width:900px){.process{grid-template-columns:repeat(2,1fr);}}
@media(max-width:560px){.process{grid-template-columns:1fr;}}
.step{padding:30px 26px;border-left:1px solid rgba(232,223,201,.22);position:relative;}
.step:first-child{border-left:none;}
@media(max-width:900px){.step{border-left:none;border-top:1px solid rgba(232,223,201,.22);}}
.step-num{
  font-family:var(--font-cond);font-stretch:condensed;
  display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;border-radius:50%;
  border:2px solid var(--green-pale);color:var(--green-pale);
  font-size:1.25rem;font-weight:800;margin-bottom:16px;
  /* The three properties below were added 2026-08-10 for the mowing track.
     background is the SAME colour this circle already sat on, so it looks
     identical — it just makes the circle opaque, which lets the track and the
     mower pass BEHIND it instead of crossing the numeral. z-index keeps it on
     top of them. Nothing about the circle's size or position changed. */
  background:var(--green-deep);
  position:relative;z-index:2;
  transition:border-color .3s var(--t),color .3s var(--t),background .3s var(--t),transform .3s var(--t);
}
/* Set by site.js as the mower reaches each circle. Passed steps read as done,
   the one the mower is parked at reads as active. Existing palette only. */
.step.is-done .step-num{background:var(--green-pale);border-color:var(--green-pale);color:var(--green-deep);}
.step.is-current .step-num{border-color:var(--beige);color:var(--beige);transform:scale(1.07);}
/* The steps are given tabindex="0" by site.js — and only by site.js, so with
   scripting off there are no tab stops on content that would not do anything.
   The ring is drawn round the whole step, which is what has just been focused. */
.step:focus-visible{outline:3px solid var(--beige);outline-offset:-3px;border-radius:var(--radius);}
.step h3{font-size:1.1rem;color:var(--cream);margin-bottom:8px;}
.step p{font-size:.98rem;color:var(--beige);}

/* ============================================================================
   13. SERVICE AREA
   ========================================================================== */
.area-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;}
@media(max-width:1000px){.area-grid{grid-template-columns:repeat(3,1fr);}}
@media(max-width:620px){.area-grid{grid-template-columns:repeat(2,1fr);}}
/* Below ~380px a two-up card is about 94px of content, and "Christiansburg" set
   at 1.05rem bold is wider than that — the word would push out past the card.
   Going one-up is the only fix that keeps the word whole.
   !important is required and deliberate: the .area-grid on the About page
   carries an inline style="grid-template-columns:repeat(2,1fr)", and an inline
   style beats a stylesheet rule at any specificity. If that inline style is
   ever moved into a class, drop the !important with it. */
@media(max-width:380px){.area-grid{grid-template-columns:1fr !important;}}
.area-card{
  background:var(--white);border:1px solid var(--beige-deep);border-radius:var(--radius);
  padding:22px 18px;text-align:center;cursor:pointer;
  transition:transform .22s var(--t),box-shadow .22s var(--t),background .22s var(--t);
}
.area-card:hover{transform:translateY(-3px);box-shadow:var(--shadow-sm);background:var(--beige);}
.area-card .pin{width:26px;height:26px;margin:0 auto 10px;}
/* Matches the .card-icon svg rule. An <svg> with a viewBox but no width/height
   is sized by the UA, and pinning it to the 26px box removes any chance of it
   resolving to the 300x150 replaced-element default and blowing out the card. */
.area-card .pin svg{width:100%;height:100%;display:block;}
.area-card h3{font-size:1.05rem;margin-bottom:4px;}
.area-card span{font-size:.85rem;color:var(--ink-mid);}

/* ============================================================================
   14. FAQ — two columns, native <details>/<summary> disclosures
   ============================================================================
   REBUILT 2026-08-10. It was eight <h3> + <p> pairs stacked in a 700px centred
   column with every answer open at once, which read as one undifferentiated
   wall. It is now an anchor column on the left and the questions on the right,
   and each Q&A is a native <details>/<summary>.

   NATIVE DISCLOSURE, NOT A SCRIPTED ONE. Everything a homemade accordion has to
   earn back by hand is free here and already correct: Tab reaches every
   question, Enter and Space toggle it, a screen reader announces a button with
   an expanded or collapsed state, and it all still works with JavaScript
   switched off. There is NO JavaScript behind this section — site.js does not
   know it exists, and nothing here depends on it running.

   THE FAQPage SCHEMA STILL DESCRIBES THE PAGE. Google asks that the answer be
   present for a visitor, and text inside a closed <details> is in the DOM and
   in the served HTML — it is not injected, not fetched and not display:none'd
   by a class. The first item ships open so the section never reads as empty.

   MEASURE. .faq-layout caps itself at 1280px inside the 1680px .container, the
   same way .ba-set carries its own cap. At that width the answer column sets
   body copy at roughly 73 characters, inside the 70-75 band the rest of the
   site holds to (see .container-narrow). Widening the cap makes the answers
   harder to read, not easier.

   NO STICKY, AS OF 2026-08-10. The left column used to stick below the header
   from 901px up. The testimonial block moving into the bottom of that column
   is what ended it: the rail roughly doubled in height, and a sticky box
   taller than the viewport hides its own bottom permanently. The full sum is
   under the media query at the foot of this section — read it before putting
   sticky back.

   .reveal is still on the PIECES INSIDE .faq-aside and never on .faq-aside
   itself. That started as a sticky requirement: .reveal settles at
   transform:translateY(0), which is still a transform, and a transformed
   ancestor becomes the containing block and silently kills sticky on
   everything inside it. Sticky is gone so the constraint is lifted, but four
   small fades down the rail read better than one big one, so it stays.

   CONTRAST — computed for this pass, sRGB, WCAG 2.x, AA needs 4.5 for body
   text and 3.0 for a control's own boundary:
     green-deep #14331F on white #FDFBF6   question text          13.33
     ink-mid    #3F4A40 on white #FDFBF6   answer text             8.97
     green-deep on beige #E8DFC9           question, row hovered  10.39
     green-mid  #266741 badge ring on white                        6.55
     green-mid  badge ring on beige (hovered row)                  5.11
     green-deep chevron on the beige badge                        10.39
     green-deep chevron on the white badge (hovered)              13.33
     cream      chevron on the green-deep badge (open)            12.35
   ========================================================================== */
/* STACKED (up to 900px) the whole block holds 700px — the same measure as
   .container-narrow, which is what this section used to sit in. Without this
   cap the single column runs the full container and sets the answers at 89
   characters on a 900px tablet, which is wider than the wall of text this pass
   was meant to fix. The 1280px two-column cap is set in the media query below.
   .faq-foot carries the same numbers so the two buttons line up with the
   columns above them rather than with the 1680px container. */
.faq-layout{
  display:grid;grid-template-columns:1fr;gap:34px;
  max-width:700px;margin-inline:auto;
}
.faq-foot{max-width:700px;margin-inline:auto;}

/* ---- LEFT COLUMN — the anchor ---------------------------------------------
   Heading, one line of lead-in, and one quiet card. It supports the questions;
   it is not a second call to action competing with them, which is why the card
   carries the GHOST button and the red one stays where it already was, at the
   foot of the section. One red primary per section. */
.faq-aside .section-head{margin-bottom:0;max-width:none;}
.faq-lead{margin-top:20px;}
.faq-cta{
  margin-top:26px;
  background:var(--white);border:1px solid var(--beige-deep);
  border-radius:var(--radius);padding:24px 24px 26px;
}
.faq-cta h3{font-size:1.08rem;margin-bottom:8px;}
.faq-cta p{font-size:1rem;margin:0;}
.faq-cta .btn{margin-top:18px;}
/* (0,2,0) — beats .faq-cta p above on class count, so the margin lands. */
.faq-cta .faq-cta-call{margin-top:14px;font-size:.95rem;}
/* The number renders as TEXT here, not a tel: link. Traffel house rule: tel:
   exists in exactly three places on this site (header, hero, footer). */
.faq-cta-num{white-space:nowrap;}

/* ---- RIGHT COLUMN — the questions ---------------------------------------- */
.faq-list{display:grid;gap:12px;}
.faq-item{
  background:var(--white);
  border:1px solid var(--beige-deep);
  border-left:4px solid var(--green-bright);
  border-radius:0 var(--radius) var(--radius) 0;
  transition:border-color .25s var(--t),box-shadow .25s var(--t);
}
.faq-item[open]{border-left-color:var(--green-deep);box-shadow:var(--shadow-sm);}

/* THE ROW. The whole thing is the click target — 60px tall minimum, well over
   the 44px touch floor, and the padding is part of the target rather than dead
   space beside it.
   Three marker resets, and all three are needed: display:flex drops the
   list-item box in Chrome and Firefox, list-style covers Firefox if the display
   value is ever changed back, and ::-webkit-details-marker is the only thing
   Safari listens to. */
.faq-summary{
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  cursor:pointer;list-style:none;
  padding:16px clamp(16px,2vw,22px);min-height:60px;
  border-radius:0 var(--radius) var(--radius) 0;
  transition:background .2s var(--t);
}
.faq-summary::-webkit-details-marker{display:none;}
.faq-summary::marker{content:"";}
.faq-item[open] .faq-summary{border-radius:0 var(--radius) 0 0;}

/* min-width:0 is load-bearing on a flex item: without it the automatic minimum
   size is the longest word, and a long question would push the badge off the
   panel instead of wrapping. */
.faq-q{flex:1 1 auto;min-width:0;font-size:1.06rem;line-height:1.35;margin:0;}

.faq-summary:hover,
.faq-summary:focus-visible{background:var(--beige);}
/* (0,2,0) beats the global *:focus-visible (0,1,0). The offset is NEGATIVE so
   the ring is drawn inside the panel — at the global +3px it would be painted
   outside the card and clipped by the neighbour above. Same reasoning as
   .step:focus-visible. */
.faq-summary:focus-visible{outline:3px solid var(--red);outline-offset:-3px;border-radius:0 var(--radius) var(--radius) 0;}

/* THE INDICATOR. A chevron drawn from two borders on a rotated box — no glyph,
   so it cannot be resized or shifted by whichever system font is resolved.
   The ring is what carries the 3:1 the badge itself cannot: a --beige fill on a
   --white panel is 1.28:1, and this is the affordance that says the row opens. */
.faq-mark{
  flex:0 0 auto;width:30px;height:30px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:var(--beige);border:2px solid var(--green-mid);color:var(--green-deep);
  transition:transform .3s var(--t),background .2s var(--t),border-color .2s var(--t),color .2s var(--t);
}
.faq-mark::before{
  content:"";display:block;width:7px;height:7px;margin-top:-3px;
  border-right:2.5px solid currentColor;border-bottom:2.5px solid currentColor;
  transform:rotate(45deg);
}
.faq-summary:hover .faq-mark,
.faq-summary:focus-visible .faq-mark{background:var(--white);}
.faq-item[open] .faq-mark{
  transform:rotate(180deg);
  background:var(--green-deep);border-color:var(--green-deep);color:var(--cream);
}

.faq-a{
  padding:16px clamp(16px,2vw,22px) 20px;
  border-top:1px solid var(--beige-deep);
}
.faq-a p{font-size:1.02rem;margin:0;}
.faq-a p+p{margin-top:12px;}

/* Native <details> cannot transition its own height without script, and this
   site is not adding any. So the ANSWER fades and settles instead — one
   animation, opacity and transform only, nothing that can reflow the page. */
.faq-item[open] .faq-a{animation:faq-reveal .26s var(--t) both;}
@keyframes faq-reveal{
  from{opacity:0;transform:translateY(-6px);}
  to  {opacity:1;transform:translateY(0);}
}
/* The blanket block in section 2 already flattens both of these to nothing.
   They are stated again here so the intent is readable at the component. */
@media (prefers-reduced-motion: reduce){
  .faq-item[open] .faq-a{animation:none;}
  .faq-mark{transition:none;}
}

/* Two columns from 901px up — the same breakpoint the hero uses, so the page
   changes shape once rather than twice. */
@media (min-width:901px){
  .faq-layout,.faq-foot{max-width:1280px;}
  .faq-layout{
    grid-template-columns:minmax(280px,1fr) minmax(0,1.42fr);
    gap:clamp(30px,3.2vw,56px);
    /* start, not stretch. This began life as a sticky requirement — a stretched
       grid item fills the whole row and has nowhere to stick to. Sticky is gone
       and it earns its place anyway: with the default stretch the shorter
       column is dragged to the height of the taller one, and any background or
       border added to it later would draw a box round a lot of empty space. */
    align-items:start;
  }
}

/* ---- WHY THERE IS NO position:sticky ON .faq-aside ANY MORE ----------------
   REMOVED 2026-08-10, together with its @media (max-height:740px) guard, when
   the testimonial block moved into the bottom of this column.

   The rule was  .faq-aside{position:sticky;top:calc(clamp(54px,7.4vw,76px) + 42px)}
   and the guard turned it off under 740px of viewport height. Both are gone.

   THE PROBLEM. A sticky element with only a `top` offset pins its TOP edge and
   lets everything past the bottom of the viewport fall off the screen and stay
   there. If the aside is taller than (viewport height - top offset), the bottom
   of it is unreachable no matter how far the visitor scrolls. Here that bottom
   is the estimate button and Trevor's phone number.

   THE SUM, computed from this stylesheet at the 1280px cap (left rail 506px,
   17px root, widest h2 clamp):
     heading block   eyebrow 34.6 + h2 two lines 96.8 + rule 24        155.4
     lead-in         20 margin + three lines at 32.4                   117.3
     contact card    26 margin + 2 border + 50 padding + h3 27.4
                     + p two lines 58.5 + button 78.1 + call line 41.8 283.8
     ---------------------------------------------------------------------
     as it shipped this morning                                        556.5  (the old note said "roughly 554")
     quotes heading  38 margin + h2 29.5 + 16 margin                    83.5
     three quote cards at 252.2, plus two 12px gaps                    780.6
     ---------------------------------------------------------------------
     the rail now                                                     1420.6

   Top offset is clamp(54px,7.4vw,76px) + 42 = 118px on any screen over
   1027px wide. So sticky would need 1539px of VIEWPORT HEIGHT to be safe, or
   1275px if only two quotes are ever published.

   Nothing normal is that tall. A 1080p monitor gives about 950px of usable
   viewport after browser chrome, and a 1440p monitor about 1290px — so even
   the two-quote case fails on a 1440p screen. Keeping sticky would have meant
   raising the guard from 740px to ~1540px, i.e. shipping a rule that is
   switched off on essentially every landscape desktop in use. A rule that
   never fires is not a feature, it is a trap for whoever reads it next.

   So the column scrolls with the page, like every other column on this site.

   IF THE QUOTES ARE EVER CUT BACK OUT of this rail and you want sticky again:
   restore both the rule and a max-height guard, and recompute the guard from
   whatever the rail actually measures. Do not restore the old 740px — it was
   derived from a 554px rail that no longer exists.
   -------------------------------------------------------------------------- */

/* ============================================================================
   14b. TESTIMONIALS — BUILT AND STYLED, NOT IN USE ANYWHERE
   ============================================================================
   Nothing on this site carries these classes today. The markup they belong to
   sits COMMENTED OUT on the home page, in the LEFT COLUMN of the FAQ section
   directly under the "Still have a question?" card, with the full explanation
   of why. Short version: there are no reviews yet, and we are not writing any
   — a fabricated testimonial is a violation of the FTC's Rule on the Use of
   Consumer Reviews and Testimonials (16 CFR Part 465) and the liability lands
   on Trevor. No Review or AggregateRating JSON-LD goes anywhere near this site
   until real, permissioned quotes are actually rendering on the page.

   The styling is finished so that uncommenting the block and pasting in real,
   permissioned quotes is the whole job.

   REBUILT 2026-08-10 FOR THE RAIL. It used to be a three-across grid on a
   full-width deep-green band of its own. Trevor asked for the reviews to sit
   next to the questions, so it is a vertical stack in a narrow column on cream
   now, and almost none of the old rules survived the move:

     DELETED  .tq-grid — repeat(auto-fit,minmax(290px,1fr)) with a 1180px cap.
              In a rail that measures 342px at a 901px viewport and 511px at
              its widest there is only ever one column, so auto-fit had nothing
              to fit and the cap was three times wider than the box. Replaced
              by .tq-stack: one column in the rail, and a single deliberate
              two-up rule for the STACKED layout below 901px, where the column
              is up to 700px wide and one-up would run very long.
     CHANGED  every colour. The card was --beige at 7% over --green-deep with
              cream and beige text on it, which is close to invisible on a
              cream section. It is a --white card with dark text now.
     CHANGED  the skeleton, from .card-dark's 4px top border to .faq-item's 4px
              LEFT border and matching radius, so the quotes down the left rail
              and the questions down the right read as one component rather
              than two.
     CHANGED  every size down. Padding 26/24 -> 18/20, quote mark 2.4rem ->
              1.8rem, gaps 24px -> 12px. The old numbers were drawn for a
              ~375px card in a wide band with air all round it; this one is
              load-bearing content in a column with a page beside it.

   .faq-quotes and .faq-quotes-title are named for the FAQ but live here rather
   than in section 14, because they exist only to carry this block. If the
   quotes are ever cut from the page, everything below this comment goes with
   them and section 14 needs no edit at all.

   CONTRAST — computed for this pass, sRGB, WCAG 2.x. AA needs 4.5 for body
   text and 3.0 for a meaningful non-text boundary. Card is --white #FDFBF6:
     ink-mid      #3F4A40 quote text                              8.97
     green-deep   #14331F customer name                          13.33
     green-mid    #266741 town / job type                         6.55
     green-bright #2E7D4F 4px card edge, on the --cream section   4.52
     green-bright #2E7D4F quote glyph, on the --white card        4.88
   The last two are decoration and are aria-hidden or a border, so neither has
   to pass anything; both clear the 4.5 body-text bar anyway. The 1px
   --beige-deep hairlines are 1.45 on --white, which is exactly what .faq-cta
   and .faq-item already draw beside them — a seam, not a boundary anything is
   conveyed by.
   ========================================================================== */

/* The block as a whole. 38px is enough of a gap under the contact card that the
   rail reads as two separate things rather than one long stack of cards. */
.faq-quotes{margin-top:38px;}
/* The heading is an <h2 class="h-sub"> — right level, h3 size. .h-sub sets the
   size in section 3; this only has to space it. */
.faq-quotes-title{margin-bottom:16px;}

/* A STACK, NOT A GRID. One column, always, at every width this rail exists at.
   The two-up rule further down applies only in the STACKED layout below 901px,
   where the column is up to 700px wide and one-up would run very long. */
.tq-stack{display:grid;gap:12px;}

/* Same skeleton as .faq-item across the gap: one green edge, one hairline, the
   same radius, so the left rail and the right rail read as one component. */
.tq-card{
  background:var(--white);
  border:1px solid var(--beige-deep);
  border-left:4px solid var(--green-bright);
  border-radius:0 var(--radius) var(--radius) 0;
  padding:18px clamp(16px,1.6vw,20px);
  display:flex;flex-direction:column;
}
/* Decorative, aria-hidden in the markup. Sized to mark the card without taking
   a whole line of the column: at 1.8rem on a .62 line-height it costs 19px. */
.tq-open{
  display:block;font-family:var(--font-cond);font-stretch:condensed;
  font-size:1.8rem;line-height:.62;color:var(--green-bright);margin-bottom:10px;
}
/* flex:1 does nothing in the one-column rail, where cards are their own height.
   It earns its place in the two-up stacked layout below 901px, where the grid
   makes both cards in a row the same height and this is what keeps the two
   attribution lines level with each other instead of floating mid-card. */
.tq-quote{margin:0;flex:1;}
.tq-quote p{font-size:.99rem;line-height:1.62;margin:0;}
.tq-quote p+p{margin-top:10px;}
.tq-attrib{margin-top:14px;padding-top:12px;border-top:1px solid var(--beige-deep);}
.tq-name{display:block;font-weight:800;font-size:.96rem;color:var(--green-deep);}
.tq-meta{
  display:block;margin-top:2px;
  font-size:.72rem;font-weight:800;letter-spacing:.11em;text-transform:uppercase;
  color:var(--green-mid);
}

/* STACKED LAYOUT ONLY (up to 900px). The two columns become one 700px column,
   the aside is no longer a rail, and three quote cards one under another put
   roughly 800px between the "Still have a question?" card and the first
   question. Two across halves that wherever there is room for it: 700px gives
   two 344px cards, and a 375px phone falls back to one. This is the only place
   auto-fit belongs in this component. */
@media (max-width:900px){
  .faq-quotes{margin-top:32px;}
  .tq-stack{grid-template-columns:repeat(auto-fit,minmax(268px,1fr));}
  /* THE ODD LAST CARD. Two columns and three quotes leaves the third one alone
     against the left edge of its row, which is the same "looks like one failed
     to load" problem .grid-2>.card:nth-child(5):last-child solves in section 9,
     and this is the same fix: normal column width, centred on the row, never
     stretched across it. calc(50% - 6px) is one column when the gap is 12px.
     The selector reads: the LAST card, at an ODD position, from the third card
     on. So it catches 3 quotes and 5, and deliberately does not catch 1 — a
     single quote has a collapsed empty track beside it and is already full
     width, and halving it would be a bug rather than a fix. */
  .tq-stack>.tq-card:last-child:nth-child(odd):nth-child(n+3){
    grid-column:1/-1;justify-self:center;width:calc(50% - 6px);
  }
  /* Below ~592px of viewport the column is narrower than 2x268+12, so auto-fit
     is already down to one track and the rule above must hand the width back. */
}
@media (max-width:591px){
  .tq-stack>.tq-card:last-child:nth-child(odd):nth-child(n+3){width:100%;}
}

/* ============================================================================
   15. FORM
   ========================================================================== */
.form-card{
  background:var(--cream);border:1px solid var(--beige-deep);border-radius:var(--radius);
  padding:34px 32px;box-shadow:var(--shadow-sm);
}
.form-card.on-green{background:var(--cream);}
@media(max-width:560px){.form-card{padding:24px 20px;}}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:18px;}
@media(max-width:620px){.form-row{grid-template-columns:1fr;}}
.field{display:flex;flex-direction:column;margin-bottom:18px;}
.field label{
  font-size:.83rem;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
  color:var(--green-deep);margin-bottom:7px;
}
.field .opt{font-weight:600;text-transform:none;letter-spacing:0;color:var(--ink-mid);font-size:.8rem;}
.field input,.field select,.field textarea{
  /* 1.0625rem = 17px. iOS Safari zooms the viewport on focus for any field
     under 16px; 1rem sat exactly on that line, this clears it and matches the
     body copy size. Do not drop this below 1rem. */
  font-family:var(--font-body);font-size:1.0625rem;color:var(--charcoal);
  background:var(--white);border:2px solid var(--field-border);border-radius:var(--radius);
  padding:13px 14px;transition:border-color .2s var(--t),box-shadow .2s var(--t);
  width:100%;
}
.field textarea{min-height:130px;resize:vertical;}
.field select{cursor:pointer;appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--green-deep) 50%),linear-gradient(135deg,var(--green-deep) 50%,transparent 50%);background-position:calc(100% - 20px) 55%,calc(100% - 14px) 55%;background-size:6px 6px,6px 6px;background-repeat:no-repeat;padding-right:42px;}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--green);box-shadow:0 0 0 3px rgba(30,86,49,.14);}
/* Placeholder text is text, so it needs the full 4.5:1. #8A9188 was 3.13:1 on
   the --white field. #6B7069 is the same green-grey hue at 4.90:1, still
   clearly lighter than the --charcoal a visitor types. */
.field input::placeholder,.field textarea::placeholder{color:#6B7069;}
.form-msg{
  margin-top:16px;padding:14px 16px;border-radius:var(--radius);
  font-size:.98rem;font-weight:600;display:none;
}
.form-msg.show{display:block;}
.form-msg.ok{background:rgba(46,125,79,.13);color:var(--green-deep);border:1px solid var(--green-bright);}
.form-msg.err{background:rgba(179,58,43,.10);color:var(--red-dark);border:1px solid var(--red);}
.form-fine{font-size:.86rem;color:var(--ink-mid);margin-top:16px;}

/* ============================================================================
   16. CONTACT / INFO BLOCKS
   ========================================================================== */
.info-block{
  background:var(--white);border:1px solid var(--beige-deep);border-radius:var(--radius);
  padding:24px 26px;border-left:4px solid var(--green-bright);
}
.info-block h3{font-size:1.08rem;margin-bottom:8px;}
.info-block p{font-size:1rem;margin:0;}
.info-big{font-size:1.5rem;font-weight:800;color:var(--green-deep);letter-spacing:.01em;}

/* ============================================================================
   17. FOOTER
   ========================================================================== */
.site-footer{background:var(--green-deep);padding:64px 0 0;}
.footer-top{display:grid;grid-template-columns:1.5fr 1fr 1fr 1.2fr;gap:38px;}
@media(max-width:980px){.footer-top{grid-template-columns:1fr 1fr;gap:34px;}}
@media(max-width:560px){.footer-top{grid-template-columns:1fr;}}
.footer-blurb{color:var(--beige);font-size:.98rem;margin-top:18px;max-width:38ch;}
.footer-est{
  display:inline-block;margin-top:16px;padding:5px 12px;border:1px solid rgba(232,223,201,.32);border-radius:100px;
  font-size:.7rem;font-weight:800;letter-spacing:.13em;text-transform:uppercase;color:var(--green-pale);
}
.footer-col h4{
  font-size:.76rem;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:var(--green-pale);margin-bottom:16px;
}
/* Footer links were ~26px tall with a 10px gap — a 36px pitch, under the 44px
   touch minimum. The padding moved off the <li> and onto the <a> so the whole
   target is tappable rather than being dead space between links.
   CORRECTED 2026-08-10: 9px of padding only reached 43.6px
   (.97rem x 16px root = 15.52px, x 1.65 line-height = 25.6px, + 18px), which
   is a fraction under the minimum. 10px clears it at 45.6px. */
.footer-col ul li{margin-bottom:0;}
.footer-col a{color:var(--beige);font-size:.97rem;display:inline-block;padding:10px 0;}
.footer-col a:hover{color:var(--white);}
.footer-call{display:block;margin-bottom:6px;}
.footer-call .lbl{font-size:.7rem;font-weight:800;letter-spacing:.13em;text-transform:uppercase;color:var(--green-pale);display:block;}
.footer-call .num{font-size:1.32rem;font-weight:800;color:var(--cream);}
.footer-call:hover .num{color:var(--green-pale);}
.footer-bottom{
  margin-top:52px;border-top:1px solid rgba(232,223,201,.2);
  padding:22px 0 28px;
  display:flex;flex-wrap:wrap;gap:14px 26px;align-items:center;justify-content:space-between;
}
.footer-bottom p,.footer-bottom a{color:var(--green-pale);font-size:.86rem;margin:0;}
/* Same touch-target reasoning as .footer-col a. CORRECTED 2026-08-10: at
   .86rem these were 42.7px with 10px of padding; 11px reaches 44.7px. */
.footer-bottom a{display:inline-block;padding:11px 0;}
.footer-bottom a:hover{color:var(--beige);}
.footer-legal{display:flex;gap:4px 20px;flex-wrap:wrap;align-items:center;}

/* ============================================================================
   18. REVEAL ON SCROLL
   ========================================================================== */
/* NO-JS GUARD — load-bearing since the site became real pages on 2026-08-10.
   .reveal starts at opacity:0 and only becomes visible when site.js adds .in.
   While the whole site was one JavaScript-routed document that was harmless,
   because nothing rendered without JavaScript anyway. Every page is meant to
   render on its own now, and without this rule a visitor with JavaScript off
   would get the header, the footer, and thirty-odd invisible blocks in
   between. <html> ships with class="no-js" and a one-line inline script in
   every <head> removes it before first paint, so this rule only ever applies
   when that script did not run. */
.no-js .reveal{opacity:1;transform:none;transition:none;}
.reveal{opacity:0;transform:translateY(18px);transition:opacity .6s var(--t),transform .6s var(--t);}
.reveal.in{opacity:1;transform:translateY(0);}
.reveal-d1{transition-delay:.09s;}
.reveal-d2{transition-delay:.18s;}
.reveal-d3{transition-delay:.27s;}

/* ---- THE RULE DRAWS ITSELF IN -----------------------------------------------
   Added 2026-08-10. The 64px brick-red bar under a section heading used to
   simply appear with the block. It now grows out from the heading's edge a
   beat after the words arrive, so the heading lands and then gets underlined.

   transform only — the bar's box never changes size, so nothing below it moves.
   Only .rule elements INSIDE a .reveal are touched; a .rule anywhere else is
   left exactly as it was, so there is no way for one to end up stuck at zero
   width. The .no-js line is the same guard the block above relies on.
   -------------------------------------------------------------------------- */
.no-js .reveal .rule{transform:none;transition:none;}
.reveal .rule{transform:scaleX(0);transform-origin:left center;transition:transform .5s var(--t) .16s;}
.reveal .rule.centered{transform-origin:center center;}
.reveal.in .rule{transform:scaleX(1);}

/* ---- STAGGERED GROUPS -------------------------------------------------------
   Added 2026-08-10. A container marked .reveal-stagger reveals its OWN
   CHILDREN one after another instead of the whole block arriving at once.

   Deliberately used ONLY on groups of tiles and cards that a visitor scans —
   the service cards, the six promises, the five town cards. It is never put on
   a block of body copy: text a visitor is trying to read must not be held back
   waiting for its turn. The whole cascade is capped at .42s so the last item is
   never far behind the first.

   .reveal-stagger is NOT itself a .reveal — the container stays visible and
   only the children move, which avoids the double fade you get from nesting
   the two. site.js observes both class names.
   -------------------------------------------------------------------------- */
.no-js .reveal-stagger>*{opacity:1;transform:none;transition:none;}
.reveal-stagger>*{opacity:0;transform:translateY(14px);transition:opacity .55s var(--t),transform .55s var(--t);}
.reveal-stagger.in>*{opacity:1;transform:translateY(0);}
.reveal-stagger.in>*:nth-child(2){transition-delay:.07s;}
.reveal-stagger.in>*:nth-child(3){transition-delay:.14s;}
.reveal-stagger.in>*:nth-child(4){transition-delay:.21s;}
.reveal-stagger.in>*:nth-child(5){transition-delay:.28s;}
.reveal-stagger.in>*:nth-child(6){transition-delay:.35s;}
.reveal-stagger.in>*:nth-child(n+7){transition-delay:.42s;}

/* ---- PHOTOGRAPHY, ON HOVER --------------------------------------------------
   A slow 3% push into the picture. The frames all carry overflow:hidden and the
   images are absolutely inset at 100%/100% with object-fit:cover, so this
   crops very slightly inward and cannot move a single pixel of layout.
   .logo is excluded: that frame is object-fit:contain around artwork with the
   business name set in it, and zooming a wordmark looks like a mistake.
   Behind (hover:hover) so it never sticks after a tap. */
@media (hover:hover){
  .photo-frame:not(.logo) img,.ba-frame .ba-img{transition:transform .75s var(--t);}
  .photo-frame:not(.logo):hover img,.ba-item:hover .ba-img{transform:scale(1.03);}
  .photo-frame:not(.logo),.ba-frame{transition:border-color .3s var(--t);}
  .photo-frame:not(.logo):hover{border-color:var(--green-bright);}
}

/* ============================================================================
   19. LEGAL PROSE
   ========================================================================== */
.legal-prose h3,.legal-prose h2{margin-top:32px;margin-bottom:8px;font-size:1.15rem;}
.legal-prose p{margin-top:12px;}
.legal-prose ul{margin-top:12px;}

/* ============================================================================
   20. LOCATION PAGE BLOCKS
   Three small pieces used ONLY by the five service-area pages
   (#page-christiansburg, -blacksburg, -roanoke, -floyd, -new-river-valley).

   Why they need their own rules: a location page is a SINGLE narrow reading
   column — .container-narrow.prose carrying an inline style="padding:0" — so it
   can't borrow the two-column photo slot the service pages use. These are the
   column-width equivalents. Everything below is built from existing variables
   and existing components; .photo-frame, .photo-note and .textlink are reused
   exactly as they are, with no new colors and no new JavaScript.
   ========================================================================== */

/* ---- QUICK FACTS -----------------------------------------------------------
   A scannable strip under the intro paragraph. Every line on every page is
   drawn from copy already written on that page — no response times, no job
   counts, no years, no distances that the page doesn't already state.

   SPECIFICITY: the .prose prefix is load-bearing, not decoration. .prose ul
   (0,1,1) and .prose ul li (0,1,2) already set a green bullet dot, a 28px
   indent, a larger size and a bottom margin. .prose .quick-facts (0,2,0) and
   .prose .quick-facts li (0,2,1) beat them. Drop the prefix and the bullets
   and the indent come straight back.
   -------------------------------------------------------------------------- */
.prose .quick-facts{
  display:grid;
  /* auto-fit: four across on a full-width column, two mid, one on a phone,
     with no media query. It also never strands a lone item on its own row the
     way a fixed four-column grid would when a page only has three facts. */
  grid-template-columns:repeat(auto-fit,minmax(168px,1fr));
  /* A 1px gap over a --beige-deep background draws the hairlines BETWEEN cells
     without every cell carrying its own border and doubling up. */
  gap:1px;background:var(--beige-deep);
  border:1px solid var(--beige-deep);border-radius:var(--radius);
  overflow:hidden;margin:26px 0 0;
}
.prose .quick-facts li{
  background:var(--white);
  margin:0;padding:15px 17px;
  font-size:.95rem;line-height:1.5;color:var(--ink-mid);
}
.prose .quick-facts li::before{display:none;}
.quick-facts strong{
  display:block;margin-bottom:4px;
  font-size:.68rem;font-weight:800;letter-spacing:.13em;text-transform:uppercase;
  color:var(--green-mid);
}

/* ---- LOCATION PHOTO --------------------------------------------------------
   One photo per location page, sitting in the reading column beside the section
   it illustrates. .photo-frame does all the real work (ratio box, inline SVG
   placeholder underneath, onerror swap); this only decides how wide it gets.

   NEVER let one of these render wider than its source file:
     column max width = 700px (.container-narrow, padding zeroed inline)
     landscape files used here = 1100px and 721px wide -> safe at full column
     portrait files used here  = 825x1100 -> capped at 400px below
   400px also stops a 3/4 frame from running ~930px tall in the middle of a
   page. If a wider column is ever introduced, re-check the 721px file first.
   -------------------------------------------------------------------------- */
.loc-photo{margin:30px 0 4px;}
.loc-photo.portrait{max-width:400px;margin-inline:auto;}

/* ---- NEARBY AREAS ----------------------------------------------------------
   A quiet row of links to the other service-area pages at the foot of each one.
   This is navigation, not a feature: a hairline and some text, no card, no box.
   The vertical padding on the links is what gets each one to a ~44px touch
   target without adding any visible chrome.
   -------------------------------------------------------------------------- */
.nearby{
  display:flex;flex-wrap:wrap;align-items:center;gap:2px 20px;
  margin-top:26px;padding-top:16px;
  border-top:1px solid var(--beige-deep);
}
.nearby-label{
  font-size:.72rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
  color:var(--green-mid);
}
.nearby a{
  color:var(--green);font-weight:700;font-size:.95rem;
  padding:12px 0;display:inline-block;
  border-bottom:2px solid transparent;
}
.nearby a:hover{color:var(--red-dark);border-bottom-color:var(--red);}

/* ============================================================================
   21. UTILITY
   ========================================================================== */
.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}
.mt-0{margin-top:0;}
.mt-32{margin-top:32px;}
.mt-44{margin-top:44px;}
.divider{height:1px;background:var(--beige-deep);border:none;margin:44px 0;}

/* ============================================================================
   22. THE MOWING TRACK — "Four steps, no runaround" on the home page
   ============================================================================
   A stripe running through the centres of the four numbered circles, and a
   push mower that is pushed along it. Behind the mower the stripe reads CUT;
   ahead of it, UNCUT. Hovering or focusing a step sends the mower to that
   step's circle and the stripe follows it exactly.

   HOW THE TWO HALVES SPLIT
     CSS owns the look and the resting geometry.
     site.js owns the measurements and the movement.
   The left/top/width/height below are DEFAULTS, and they are what a visitor
   with JavaScript switched off sees: a plain, correctly placed connecting line
   through the four circles, and no mower. site.js overwrites all four inline
   with real measured pixels the moment it runs, and re-measures on resize, so
   the line is exact whatever the type or the container does.

   WHY 48px AND 48px
     left 48 = .step's 26px left padding + half of the 44px circle.
     75%     = three of the four equal columns, i.e. circle 1's centre across
               to circle 4's.
     top 48  = .step's 30px top padding + 22px (half the circle) - 4px (half of
               this 8px box). Measured against a real render at several widths:
               the circle centres land at y=52, so the box starts at 48.
     Only a visitor with JavaScript off ever sees these three; site.js replaces
     all of them with measured pixels.

   TWO WIDTH BANDS, ON PURPOSE
     above 900px  four steps across   -> horizontal stripe, mower runs on it
     900px down   steps stack (2x2,
                  then one column)    -> NO stripe and no mower. The section
                                         keeps exactly the hairline separators
                                         it already had, and site.js fills the
                                         four numbered circles in order instead
                                         when the section comes into view.

   A VERTICAL STRIPE WAS BUILT AND THEN TAKEN OUT. In the stacked layout the
   circles sit at x=48 and the paragraph under each one starts at x=26, so a
   line drawn through the circle centres ran straight down through every line
   of body copy. The only way to keep it would have been to indent all four
   steps to open a lane for it, which is a redesign of the section rather than
   a piece of motion. The circles filling in sequence says the same thing and
   collides with nothing. Do not re-add a vertical track without first solving
   the text collision.

   EVERYTHING THAT MOVES IS transform OR opacity. No width, left or top is ever
   animated, there is no requestAnimationFrame loop anywhere in this feature,
   and under prefers-reduced-motion the global block at the top of this file
   flattens every transition and animation named here to nothing.
   ========================================================================== */
.mow{
  position:absolute;
  left:48px;right:calc(25% - 48px);top:48px;height:8px;
  pointer-events:none;   /* never steals a hover from the step it sits on */
  z-index:0;
}
/* Only the four-across layout gets it. See the note above. */
@media(max-width:900px){.mow{display:none;}}

/* UNCUT — thinner, dimmer, and visibly tufted. The repeating gradient is the
   texture that makes "not mown yet" read at a glance; it is 1.5px of a faint
   --green-pale against near-nothing, repeated every 9px. */
.mow-uncut{
  position:absolute;left:0;right:0;top:2px;height:4px;border-radius:2px;
  background:
    repeating-linear-gradient(90deg,rgba(143,184,154,.34) 0 2px,rgba(143,184,154,0) 2px 9px),
    rgba(232,223,201,.10);
}
/* CUT — taller, solid, definite, with a lighter top edge so the stripe catches
   the light the way a mown pass does. transform-origin is the START of the run,
   so scaleX is literally "how far along the mower has got".
   No border-radius: a rounded end scaled to 0.02 looks broken. */
.mow-cut{
  position:absolute;left:0;top:0;width:100%;height:8px;
  background:linear-gradient(to bottom,rgba(232,223,201,.85) 0 2px,var(--green-pale) 2px);
  transform:scaleX(0);transform-origin:left center;
  transition:transform 0ms linear;
}

/* THE MOWER.
   Positioned so its BOTTOM EDGE sits on the centre line of the stripe, because
   that is where the wheels are drawn — the machine stands on the ground rather
   than floating over it. Horizontal travel is set inline by site.js as a single
   translate3d; the bob and the wheels are separate animations on the elements
   inside, so the two never fight over one transform. */
/* The SVG's viewBox is 51x41. 56x45 is that shape at a single uniform 1.098
   scale — check both numbers together if you ever change one, a non-uniform
   pair would stretch the machine. site.js works the rear wheel out as a
   FRACTION of this width, so it follows the size automatically. */
.mow-mower{
  position:absolute;left:0;
  /* The wheels are drawn flush with the bottom of the box, so this is what
     puts them on the ground. The extra 2px sits them slightly INTO the stripe
     rather than balanced on its centre line — at bottom:50% exactly the machine
     read as hovering just above the grass. */
  bottom:calc(50% - 2px);
  width:56px;height:45px;
  transform:translate3d(0,0,0);
  transition:transform 0ms linear;
  z-index:1;                 /* under .step-num, which is z-index:2 */
}
.mow-mower svg{width:100%;height:100%;display:block;overflow:visible;}
/* .is-driving is on for exactly as long as a run lasts and is taken off the
   moment the mower stops, so neither of these animations is ever left ticking
   on a parked mower, an off-screen section or a hidden tab. */
.mow.is-driving .mow-mower svg{animation:mow-bob .42s ease-in-out infinite;}
.mow.is-driving .mow-wheel{animation:mow-wheel .5s linear infinite;}
/* view-box, not fill-box: the origins below are stated in the SVG's own user
   units, which is what view-box resolves against. Swapping to fill-box would
   silently reinterpret them against each wheel's own bounding box and the
   wheels would orbit instead of spin. */
.mow-wheel{transform-box:view-box;}
.mow-wheel-r{transform-origin:24px 37.2px;}
.mow-wheel-f{transform-origin:46px 37.7px;}
@keyframes mow-bob{
  0%,100%{transform:translate3d(0,0,0) rotate(0deg);}
  50%    {transform:translate3d(0,-1px,0) rotate(-.7deg);}
}
@keyframes mow-wheel{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}

/* GRASS CLIPPINGS. A handful of flecks thrown out behind the deck, which
   settle and fade. They are dropped into the track (which does not move), not
   into the mower (which does), so each one stays where it landed instead of
   being carried along. site.js positions each with an inline translate3d on the
   outer span and then restarts the animation on the <i> inside it.
   Three variants, so they do not all fly the same arc. */
.mow-clip{position:absolute;left:0;top:50%;width:5px;height:3px;margin-top:-1px;}
.mow-clip>i{
  display:block;width:100%;height:100%;border-radius:1px;
  background:var(--green-pale);opacity:0;
  --fx:-22px;--fy:11px;
}
.mow-clip.v2>i{--fx:-31px;--fy:6px;}
.mow-clip.v3>i{--fx:-15px;--fy:14px;}
.mow-clip.go>i{animation:mow-fleck .62s var(--t) forwards;}
.mow-clip.v2.go>i{animation-duration:.8s;}
.mow-clip.v3.go>i{animation-duration:.52s;}
@keyframes mow-fleck{
  0%  {opacity:.85;transform:translate3d(0,0,0) rotate(0deg);}
  70% {opacity:.5;}
  100%{opacity:0;transform:translate3d(var(--fx),var(--fy),0) rotate(160deg);}
}

