/* ============================================================
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-14  CDT
 * Summary:  Darkened the trust-badge cards a second step. .badge background
 *           opacity raised from .375 to .47 (rgba(20,25,33,.47)) — another
 *           ~25% darker over the hero photo (cumulative from the original
 *           .30). Colour unchanged; only the overlay opacity moved. White
 *           title / #d5dae1 subtitle stay readable. Nothing else changed.
 * ============================================================ */
/* ---- Prior session header ----========================================================
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-14  CDT
 * Summary:  Darkened the trust-badge cards. .badge background opacity
 *           raised from .30 to .375 (rgba(20,25,33,.375)) so the eight
 *           cards read ~25% darker over the hero photo. Colour unchanged;
 *           only the overlay opacity moved. Nothing else changed.
 * ============================================================ */
/* ---- Prior session header ----========================================================
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-11  10:40 PM CDT
 * Summary:  Added the .confp-* rules for the NEW standalone confirmation
 *           page (contact-submitted.php), right after the existing .conf-*
 *           block. Both quote forms now redirect to that page on success
 *           instead of swapping a panel in place, so the panel sits on the
 *           site's DARK --navy body background — colours are set for a dark
 *           surface (light text) on a raised --navy-800 card, every value
 *           contrast-checked on --navy (all pass AA; secure line #948e83 at
 *           4.85:1, lifted from the forms' #6f6a60 which failed at 2.94:1).
 *           SEPARATE prefix from the forms' .conf-* on purpose. These
 *           selectors are class-scoped and match nothing on the home page
 *           (verified: the .hero-band cascade is byte-identical before/after
 *           this change). Nothing else changed.
 * ============================================================ */
/* ---- Prior session header ----========================================================
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-11  07:55 PM CDT
 * Summary:  Added the .conf-* rules for the redesigned handyman post-submit
 *           confirmation panel (see app.js this session). Placed immediately
 *           after the legacy .sent rules. The panel renders inside #formWrap,
 *           which keeps the WHITE .form card, so every colour here is set for
 *           a WHITE background: .conf-h uses --ink-dark, .conf-p uses #6b665d,
 *           the call box border is #e6e0d6, the secure line is #9a948a, and
 *           the check-ring / accents use --orange. Class-namespaced .conf-*
 *           so nothing leans on .form / .sent inheritance. One media query at
 *           <=520px drops the headline to 26px. Nothing else changed; the old
 *           .sent rules are left in place (still referenced nowhere now, but
 *           harmless -- can be pruned later).
 * ============================================================ */
/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-11  06:40 PM CDT
 * Summary:  Added a .zip-city rule for the handyman form's ZIP -> city
 *           hint (#zip_city, filled by app.js from api/lookup-zip.php).
 *
 *           WHY IT WAS NEEDED: the element had no rule and rendered WHITE
 *           on the white .form card -- invisible. Root cause: --ink is
 *           defined as #fff, and body{color:var(--ink)} makes white the
 *           document default; a bare <p> inside the white form inherited
 *           it. (The form's inputs dodge this by explicitly setting
 *           color:var(--ink-dark); a class-less <p> did not.)
 *
 *           FIX: .zip-city{color:#7a746a; font-size:12.5px; margin-top:6px}
 *           -- #7a746a is the same muted body grey as .form-note (4.63:1
 *           on white, AA-pass), so both form hints read as one family;
 *           12.5px matches .field-err. Single muted colour for BOTH the
 *           "City, County County" and "Zip not found" states by design.
 *           .zip-city:empty{display:none} collapses it (mirrors
 *           .field-err:empty) so there is no gap before a ZIP is typed.
 *           Contrast measured with the WCAG formula on the real #fff card;
 *           placed immediately after .field-err, which it sits below in
 *           the markup. Nothing else changed.
 * ============================================================ */
/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-11  01:05 PM CDT
 * Summary:  CORRECTED the quote-form field spacing properly. The real
 *           bug: a plain descendant `.form .field{margin-bottom:12px}`
 *           (added earlier this session) matched ALL SIX .field divs --
 *           including the four nested INSIDE the two .frow grids -- so
 *           each paired-row input carried a phantom 12px below it, making
 *           the vertical gaps uneven. Fixed by switching to the CHILD
 *           combinator: `.form > .field{margin-bottom:12px}` matches only
 *           the two STANDALONE rows (timing <select>, textarea), never the
 *           nested grid-item .fields. The redundant `.form .field-last`
 *           rule was removed (the textarea now gets its 12px from
 *           .form > .field); the .field-last class stays in the markup but
 *           carries no spacing.
 *
 *           Result: every row gap is a single uniform 12px from exactly
 *           one source -- .frow for pairs, .form > .field for standalones.
 *           Proven with lxml+cssselect against the real form DOM: the
 *           child combinator matches ONLY timing + textarea; the four
 *           nested fields get no row margin.
 *
 *           Also earlier this session: .why-grid 2 -> 3 columns. See
 *           prior headers.
 * ============================================================ */
/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-11  12:48 PM CDT
 * Summary:  Made ALL quote-form field gaps uniform at 12px. Dropped
 *           .form .field-last from 16px to 12px so the textarea->button
 *           gap matches every other row (name/phone, zip/service, timing
 *           all sit at .frow/.field 12px). The whole form now has one
 *           consistent vertical rhythm. .field-last now equals .form
 *           .field, so its earlier tie-break rationale is moot; the class
 *           is kept (removing it would touch the form markup) and its
 *           comment updated. Cascade re-resolved with cssutils: every gap
 *           (frow, frow, .field, .field-last) = 12px.
 *
 *           Earlier this session: .form .field{margin-bottom:12px} added
 *           for standalone fields; .why-grid 2 -> 3 columns. See prior
 *           headers below.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-11  12:34 PM CDT
 * Summary:  Two changes this session.
 *
 *           (1) Fixed the quote form's field spacing. A standalone .field
 *               (the new timing <select>, and the textarea) had NO
 *               margin-bottom -- only .frow (12px) and .field-last (16px)
 *               did -- so the timing row sat tight against the textarea
 *               while every paired row had a 12px gap. Added
 *               .form .field{margin-bottom:12px}, placed BEFORE
 *               .field-last (the two tie at 0,0,2,0, so source order
 *               decides; .field-last's 16px must still win for the
 *               textarea). Verified by resolving the cascade with
 *               cssutils: standalone .field -> 12px, .field-last -> 16px.
 *
 *           (2) [earlier] .why-grid base rule 2 -> 3 columns for the
 *               six-card 'why' section. See prior header below.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-11  11:31 AM CDT
 * Summary:  .why-grid base rule changed from repeat(2,1fr) to
 *           repeat(3,1fr) so the 'why us' section renders 3 columns x
 *           2 rows on desktop, now that content.php has six why cards
 *           (was four). ONLY the base (>960px) rule changed; the
 *           <=960px rule (1fr, single column) and the <=767px rule
 *           (horizontal scroll-snap carousel) are untouched, so tablet
 *           and mobile are unaffected.
 *
 *           Verified with real Inter metrics at the desktop column
 *           width (~245px of text per card): every title fits on one
 *           line, blurbs wrap to 2-3 lines, and no single word
 *           overflows. Headless Chromium was unavailable this session
 *           (browser download blocked), so fit was confirmed by
 *           cssutils cascade resolution + Pillow/Inter text measurement
 *           rather than a screenshot.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-10  11:56 AM CDT
 * Summary:  Two white icons turned orange, matching the site pattern
 *           where an icon uses currentColor and the svg carries the
 *           colour (see .phone svg, .foot-phone svg, .county svg).
 *
 *           1. .quote-phone svg -> var(--orange). The quote section's
 *              "Prefer to talk?" phone icon was inheriting the #fff set
 *              on .quote-phone. Now the ICON is orange and the NUMBER
 *              stays white -- same split as the nav's .phone.
 *
 *           2. .foot-brand .brand svg -> var(--orange). The footer
 *              wrench in the PROHandyUSA mark was inheriting white.
 *              SCOPED to .foot-brand on purpose: the nav uses the same
 *              .brand class, and an unscoped rule would recolour the
 *              nav wordmark's wrench too.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----========================================================
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-10  10:58 AM CDT
 * Summary:  Footer restyle for FAQ/footer separation.
 *
 *           1. .footer background var(--dark) -> var(--navy-950)
 *              (#141921). The footer now sits DARKER than the navy
 *              FAQ section above it; that tone step is what marks the
 *              boundary, replacing the removed orange line.
 *
 *           2. Deleted the .foot-accent rule (the 4px orange gradient
 *              bar at the top of the footer). Its markup <div> is
 *              removed from footer.php in the same change.
 *
 *           .footer keeps position:relative. It was there only to
 *           anchor the absolute .foot-accent; harmless now, left in.
 * ------------------------------------------------------------ */
/* ============================================================
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-10  03:11 AM CDT
 * Summary:  Added .form-note -- "All fields required." under the quote
 *           form's heading.
 *
 * ---- The heading's 20px is redistributed ----------------------------
 *
 *           `.form h3{margin-bottom:20px}` held the heading off the first
 *           .frow. A paragraph now sits between them, so leaving the 20px
 *           there would push the NOTE away from the heading it belongs
 *           to, and the note would still need a gap of its own beneath.
 *
 *           The h3's margin goes to 0; the note carries 6px above and
 *           14px below. It reads as part of the heading, which is what
 *           it is.
 *
 *           THE FORM GETS TALLER, by the note's line box (~18px) plus
 *           the extra 0px of margin. THAT IS UNAVOIDABLE: a visible line
 *           of text occupies the height of a visible line of text. There
 *           is no margin trick that hides it, and margin COLLAPSING does
 *           not help -- collapsing merges adjacent MARGINS, and does
 *           nothing about an element with height sitting between them.
 *           (An earlier draft of this comment claimed otherwise. It was
 *           wrong.)
 *
 *           SAFE FOR THE CONFIRMATION SCREEN. `.form h3` also matches
 *           `.sent h3` -- .sent renders inside #formWrap, which keeps
 *           its .form class. But `.sent h3` sets the `margin` SHORTHAND,
 *           which overrides margin-bottom outright, and at (0,0,1,1) it
 *           ties `.form h3` and wins on source order (line 573 vs 526).
 *           Checked, not assumed.
 *
 * ---- The colour is #7a746a, NOT .form-fine's #9a948a ----------------
 *
 *           #9a948a on white measures 3.01:1. That FAILS 4.5:1 for
 *           normal text at any size the note would use. It is the grey
 *           `.form-fine` already uses for the "No spam, ever" line --
 *           A PRE-EXISTING PROBLEM ON THAT ELEMENT, not introduced here,
 *           and left alone because it was out of scope. It is in the
 *           to-do list.
 *
 *           #7a746a is 4.63:1 and is already the site's body grey
 *           (.svc p, .cab-points p). Copying the failing token into a
 *           new element would have doubled the bug.
 *
 *           12.5px, matching .field-err, so the two secondary lines in
 *           the form agree.
 * ============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800;900&family=Inter:wght@400;500;600;700&display=swap');
:root{
  /* Pro Mount USA palette. --dark and --cream both resolve to navy so the
     site reads as one flat surface; they are kept as names because 9 rules
     still reference them. --char was unused and has been removed. */
  --navy:#1a2332;        /* navy-900 — the single section background */
  --navy-800:#202938;    /* navy-800 — raised panels */
  --navy-950:#141921;    /* navy-950 — deepest, used sparingly */
  --dark:var(--navy);
  --cream:var(--navy);
  --orange:#FF6B35;      /* Pro Mount orange-500 */
  --orange-dk:#FD5815;
  --orange-lt:#FF744D;
  --teal:#2EA1CB;
  --ink:#ffffff;         /* text on navy; was near-black on cream */
  --ink-dark:#232019;    /* text inside the white cards */
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
@media (prefers-reduced-motion: reduce){html{scroll-behavior:auto}}
body{background:var(--cream);color:var(--ink);font-family:'Inter',system-ui,sans-serif}
.wrap{max-width:1180px;margin:0 auto;padding:0 24px}
.wrap.narrow{max-width:820px}
h1,h2,h3,h4,.wm{font-family:'Poppins',sans-serif}
button{font-family:inherit;color:inherit;cursor:pointer;border:none;background:none}  /* color:inherit — without it buttons take the UA default (near-black),
     which made the nav wrench and burger invisible on navy. Every other
     button (.btn-*, .svc-link, .faqi>button) sets its own color. */
a{cursor:pointer;text-decoration:none;color:inherit}
svg{display:block}

.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;font-weight:700;border-radius:11px;padding:11px 20px;font-size:15px;transition:transform .15s ease,box-shadow .2s ease,background .2s ease;white-space:nowrap}
.btn.lg{padding:15px 26px;font-size:16px}
.btn.full{width:100%}
.btn-orange{background:linear-gradient(135deg,var(--orange-dk),var(--orange-lt));color:#fff;box-shadow:0 8px 22px rgba(255,107,53,.32)}
.btn-orange:hover{transform:translateY(-2px);box-shadow:0 12px 30px rgba(255,107,53,.42)}
.btn-ghost{background:rgba(255,255,255,.06);color:#fff;border:1px solid rgba(255,255,255,.18)}
.btn-ghost:hover{background:rgba(255,255,255,.12)}
.btn-dark{background:var(--dark);color:#fff}
.btn-dark:hover{transform:translateY(-2px)}
/* The first button variant designed to sit on a WHITE surface. btn-ghost is
   white-on-translucent and vanishes there.

   Orange on white is 2.84:1 -- under even the 3:1 non-text bar -- so orange
   is used ONLY for the border, never for a label. The resting label is
   --ink-dark (16.25:1 on white).

   ON HOVER THE LABEL STAYS --ink-dark. It does NOT flip to white. Contrast
   is symmetric, so white-on-orange is the same 2.84:1 as orange-on-white and
   fails the 3:1 large-text bar. --ink-dark on orange is 5.73:1 and passes at
   any size. Do not "fix" this to white.

   Border is 1.5px, and is carried at rest, so hover changes colour only and
   nothing shifts. */
.btn-outline{background:transparent;color:var(--ink-dark);border:1.5px solid var(--orange)}
.btn-outline:hover{background:var(--orange);color:var(--ink-dark)}

/* Offset in-page scroll targets so the fixed nav doesn't cover them.
   #top is excluded on purpose — the hero should sit flush at the top.
   #how, #why and #area are no longer in the nav but are still reachable
   from the footer, so they keep their offset. */
#services,#cabinets,#how,#why,#reviews,#area,#faq,#quote{scroll-margin-top:86px}

.nav{position:fixed;top:0;left:0;right:0;z-index:50;background:var(--navy);transition:box-shadow .25s ease;border-bottom:1px solid transparent}
.nav.scrolled{box-shadow:0 6px 24px rgba(0,0,0,.28);border-bottom:1px solid rgba(255,255,255,.06)}  /* background is already solid navy */
.navrow{display:flex;align-items:center;justify-content:space-between;height:70px}
.brand{display:flex;align-items:center;gap:9px}
.ic-static svg{color:var(--orange)}   /* nav wrench */
.wm{font-weight:800;font-size:19px;letter-spacing:.3px;color:#fff}
.wm em{color:var(--orange);font-style:normal}
.links{display:flex;gap:28px}
.links a{color:rgba(255,255,255,.82);font-size:14.5px;font-weight:500;transition:color .15s}
/* Hover: orange + underline. --orange-lt, not --orange — against navy the
   lighter tone clears the 4.5:1 small-text contrast bar (4.71:1) where
   --orange does not (4.26:1). text-underline-offset keeps the rule off
   the descenders. */
.links a:hover{color:var(--orange-lt);text-decoration:underline;text-underline-offset:4px}
.navcta{display:flex;align-items:center;gap:16px}
/* Caption + number form one column; "Book now" sits beside the pair.
   The phone glyph stays inline with the number (see .phone), matching
   the Pro Mount reference — it is not part of the caption row. */
.phone-stack{display:flex;flex-direction:column;align-items:flex-end;gap:3px;line-height:1.1}
.cta-cap{font-size:10.5px;font-weight:600;letter-spacing:.8px;text-transform:uppercase;color:#9aa1ad;white-space:nowrap}
.phone{display:flex;align-items:center;gap:7px;color:#fff;font-weight:700;font-size:19px}
.phone svg{color:var(--orange)}                 /* icon orange, number stays white */
.phone:hover{color:var(--orange-lt)}
.phone:hover svg{color:var(--orange-lt)}        /* icon and number move together */
/* The burger holds both glyphs stacked in one grid cell, so swapping them
   never resizes the button. .nav.is-open decides which is visible — the
   menu's state, not the click, because go() also closes the menu. */
.burger{display:none;place-items:center}
.burger>span{grid-area:1/1;display:flex;transition:opacity .18s ease,transform .18s ease}
.burger-x{opacity:0;transform:rotate(-90deg) scale(.7);pointer-events:none}
.nav.is-open .burger-menu{opacity:0;transform:rotate(90deg) scale(.7);pointer-events:none}
.nav.is-open .burger-x{opacity:1;transform:none;pointer-events:auto}
@media (prefers-reduced-motion: reduce){
  .burger>span{transition:none}
  .burger-x,.nav.is-open .burger-menu{transform:none}
}
/* Tap-to-call in the collapsed mobile bar, left of the burger. Icon only —
   the number would crowd the wordmark at 380px. Hidden on desktop, where
   .navcta already shows the full number. */
.nav-call{display:none;align-items:center;justify-content:center;width:44px;height:44px;border-radius:10px;color:var(--orange);transition:background .15s}  /* 44px = minimum comfortable tap target */
.nav-call:hover,.nav-call:active{background:rgba(255,107,53,.12)}
.mobile{display:none;flex-direction:column;gap:4px;padding:12px 24px 22px;background:var(--dark);border-top:1px solid rgba(255,255,255,.08)}
.mobile.open{display:flex}
.mobile a{color:#fff;padding:12px 4px;font-weight:600;border-bottom:1px solid rgba(255,255,255,.06);transition:color .15s}
/* Same treatment as the desktop row. .mobile-phone is excluded rather than
   left to fight this rule — its colour is already !important, and it sits
   below so the win is plain rather than accidental. */
.mobile a:not(.mobile-phone):hover{color:var(--orange-lt);text-decoration:underline;text-underline-offset:4px}
.mobile-phone{display:flex;align-items:center;gap:8px;color:var(--orange-lt)!important}
.mobile .btn{margin-top:12px}

/* ============================================================
   .hero-band — the container wrapping <hero> + <badges>.
   The photo lives here so it runs continuously behind both, instead of
   stopping at the hero's edge and leaving a seam across the badge cards.

   Three background layers, painted front to back:
     1. the scrim   — navy: 70% at the crown, 82% at 45%, then EASING
                      OFF to 62% / 55% so the photo stays visible through
                      the badge strip. The top two stops are load-bearing
                      for the headline's contrast; the bottom two are what
                      you change to make the photo more or less present.
     2. the photo   — WebP where supported, JPEG otherwise
     3. navy        — background-color, shown while loading / on 404
   ============================================================ */
.hero-band{
  position:relative;
  background-color:var(--navy);
  background-image:linear-gradient(to bottom,rgba(26,35,50,.70) 0%,rgba(26,35,50,.82) 45%,rgba(26,35,50,.62) 70%,rgba(26,35,50,.55) 100%),url('../img/hero.jpg');
  background-image:linear-gradient(to bottom,rgba(26,35,50,.70) 0%,rgba(26,35,50,.82) 45%,rgba(26,35,50,.62) 70%,rgba(26,35,50,.55) 100%),-webkit-image-set(url('../img/hero.webp') type('image/webp'),url('../img/hero.jpg') type('image/jpeg'));
  background-image:linear-gradient(to bottom,rgba(26,35,50,.70) 0%,rgba(26,35,50,.82) 45%,rgba(26,35,50,.62) 70%,rgba(26,35,50,.55) 100%),image-set(url('../img/hero.webp') type('image/webp'),url('../img/hero.jpg') type('image/jpeg'));
  background-size:cover;
  background-position:center 40%;
  background-repeat:no-repeat;
  overflow:hidden;   /* was on .hero; clips .hero-glow */
}

.hero{
  /* Background lives on .hero-band (it spans the hero AND the badges).
     This section keeps position:relative so .hero-glow can anchor to it. */
  position:relative;
  padding:120px 0 70px;
}

.hero-glow{position:absolute;top:-140px;right:-120px;width:520px;height:520px;border-radius:50%;background:radial-gradient(circle,rgba(255,107,53,.12),transparent 62%);pointer-events:none}
.hero-grid{display:grid;grid-template-columns:1fr;gap:52px;align-items:center;position:relative}
.hero-copy{text-align:center;max-width:780px;margin:0 auto}
.hero-copy .lede{margin-left:auto;margin-right:auto}
.hero-btns{justify-content:center}
.hero-trust{justify-content:center}
.eyebrow{display:inline-flex;align-items:center;gap:9px;color:#c3cbd6;font-size:13px;font-weight:600;letter-spacing:.4px;text-transform:uppercase;margin-bottom:20px}
.eyebrow .dot{width:8px;height:8px;border-radius:50%;background:var(--orange);box-shadow:0 0 0 4px rgba(255,107,53,.22)}
.hero h1{color:#fff;font-size:56px;line-height:1.03;font-weight:900;letter-spacing:-.5px}
.hero h1 .o{color:var(--orange)}
.lede{color:#b9c2cf;font-size:18px;line-height:1.6;margin:20px 0 30px;max-width:520px}
.hero-btns{display:flex;gap:14px;flex-wrap:wrap}
.hero-trust{display:flex;align-items:center;gap:16px;margin-top:28px;color:#dde3ea;font-size:14px;font-weight:600;flex-wrap:wrap}
.hero-trust span{display:inline-flex;align-items:center;gap:7px}
.hero-trust .sep{width:1px;height:16px;background:rgba(255,255,255,.2)}

.badges-wrap{background:transparent;padding:0 0 8px}  /* photo shows through from .hero-band */
.badges{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;transform:translateY(-30px)}
/* A white wash, not a navy panel. Fitted from the Pro Mount render:
   alpha 0.10 over white reproduced their interiors to 3.5/255; a navy
   overlay missed by 19.7. The photo reads THROUGH the card. Shadow is
   softened because a translucent card casting a hard shadow reads as a
   pane of glass sitting proud of the page rather than a tint on it. */
/* A navy tint, not a white wash. Solved against the real hero.jpg: the
   badge strip lands on the workbench and tool wall, whose specular
   highlights reach rgb(108,109,115) after the scrim. A white wash lifts
   that further and no subtitle colour survives it. .30 navy pulls it
   down to a median of rgb(37,37,40) while keeping the photo legible
   (luminance sigma 0.0202 vs 0.0447 for the white wash). */
.badge{background:rgba(20,25,33,.47);border:1px solid rgba(255,255,255,.28);border-radius:14px;padding:18px 14px;text-align:center;box-shadow:0 10px 26px rgba(0,0,0,.22);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}
.badge .bi{display:flex;justify-content:center;color:var(--orange)}
.badge .bt{color:#fff;font-weight:700;font-size:14px;margin-top:9px;font-family:'Poppins'}
/* #d5dae1, not #8b95a5. On the tinted card the old grey sat at 2.5:1 —
   invisible over the bright half of the photo. This clears 4.5:1 on
   99.998% of the pixels under the cards. Do not darken it back toward
   the greys used elsewhere on flat navy; this text sits on a photo. */
.badge .bs{color:#d5dae1;font-size:12px;margin-top:3px}
/* The mobile check-disc. Emitted by badge.php on every badge, hidden here
   and revealed only inside the <=767px block. Without this rule it would
   inherit div's display:block and show a stray orange disc on every
   desktop card. */
.badge .bx{display:none}

/* 37px. Padding does NOT collapse, so two adjacent sections stack to 2x this:
   the gap between sections is 74px. Cut twice: 84 -> 56 -> 37, a 56% total
   reduction from the original.
   .quote is NOT a .section (quote-form.php uses class="quote") and carries
   its own copy of this number -- change them together or the quote form
   becomes the tallest block on the page.
   Note .head{margin-bottom:44px} was deliberately NOT cut, so a heading now
   sits 44px from its own cards while sections sit 74px apart. Chosen. */
.section{padding:37px 0}
/* Centered. max-width:640px alone does NOT center the box -- without the auto
   margins it stays pinned left and only the text inside moves. Both are
   needed. Centers within the containing .wrap, so the FAQ head (in
   .wrap.narrow, 772px of content) centers over its accordion, while the
   rest center in 1132px. services.php used to carry this inline; that
   duplicate is now removed. */
.head{max-width:640px;margin:0 auto 44px;text-align:center}
/* The cabinets headline is 719.5px in real Poppins ExtraBold at 38px (with
   letter-spacing:-.4px, which CSS adds after every character including the
   last). The global 640px cap forced it to two lines while .wrap still had
   412.5px spare -- the room existed, the box refused it.

   760px = the headline + 40.5px of slack. The slack absorbs the disagreement
   between a measuring library and a real rasterizer; a heading that fits by
   half a pixel on paper clips on a screen.

   Scoped at (0,0,2,0) vs .head's (0,0,1,0), so it wins on specificity and
   moves nothing else. Safe to widen ONLY because .cabinets has no .sub -- the
   box holds a .kick and an h2, so there is no paragraph measure to stretch.

   >> 760px IS DERIVED FROM ONE STRING, and that string is still PLACEHOLDER
      COPY awaiting sign-off. Change the headline, re-derive this number.
      Nothing keeps them in sync. */
.cabinets .head{max-width:760px}
.kick{color:var(--orange);font-weight:700;font-size:13px;letter-spacing:1.5px;text-transform:uppercase;margin-bottom:12px}
.head h2{font-size:38px;line-height:1.1;font-weight:800;letter-spacing:-.4px;color:var(--ink)}
.head h2.nowrap-desktop{white-space:nowrap}
.sub{color:#b9c2cf;font-size:17px;line-height:1.6;margin-top:14px}

.svc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
.svc{background:#fff;border:1px solid transparent;border-radius:18px;padding:26px;transition:transform .2s ease,box-shadow .2s ease,border-color .2s}

.svc-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
.svc-ic{width:46px;height:46px;border-radius:12px;background:rgba(255,107,53,.1);display:flex;align-items:center;justify-content:center;color:var(--orange)}
.svc-from{color:var(--orange);font-weight:800;font-size:14px;background:rgba(255,107,53,.09);padding:5px 11px;border-radius:20px}
.svc h3{font-size:19px;font-weight:700;margin-bottom:8px;color:var(--ink-dark)}
.svc p{color:#7a746a;font-size:14px;line-height:1.55;margin-bottom:16px}
.svc ul{list-style:none;margin-bottom:18px}
.svc ul li{display:flex;align-items:center;gap:9px;color:#4a463e;font-size:14px;padding:5px 0}

/* ---- cabinets ------------------------------------------------------------
   Two columns, even split. .wrap gives 1132px of content; at gap:48 each
   column is 542px.

   THE PHOTO IS CROPPED, ON PURPOSE. It is 1344x784 (ratio 1.714) and the
   card beside it measures 353.6px tall -- so the photo's natural height at
   542px is 316.2px, 37.4px short. Widening the photo column does not fix
   this: it narrows the card, whose text rewraps TALLER, and the deficit
   never closes (minimum +36.0px across every split from 460 to 580px).
   object-fit:cover absorbs it by trimming 71px from each side -- 10.6% of
   the width, all of it blank wall. Verified against the real file.

   Card height is a function of the copy in content.php. Change a point's
   text and the crop silently adjusts; that is the point of cover here.

   .cab-card takes NO hover. It is a static panel, not a grid item and not
   clickable -- unlike .svc/.why/.rev/.step, which lift and glow. Hover
   feedback on a non-interactive element is noise. */
.cab-grid{display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:stretch}

/* White surface -> --ink-dark. This is the --ink split: colour follows the
   SURFACE, not the section. The .kick and h2 above sit on navy and stay light. */
.cab-card{background:#fff;border-radius:18px;padding:32px;display:flex;flex-direction:column}
.cab-points{list-style:none;display:flex;flex-direction:column;gap:22px}
.cab-points li{display:block}
.cp-head{display:flex;align-items:center;gap:10px;margin-bottom:8px}
/* flex:none so a two-line heading cannot squash the tick. */
.cp-ic{color:var(--orange);display:flex;flex:none}
.cp-head h3{font-size:15.5px;font-weight:700;line-height:1.35;color:var(--ink-dark)}
/* 28px = the 18px tick + the 10px gap, so the body text hangs under the
   heading's first letter rather than under the tick. */
.cab-points p{margin-left:28px;color:#7a746a;font-size:14.5px;line-height:1.6}

/* ONE button now, not two. margin-top:auto still pins it to the bottom of the
   card, so the card stays composed if a point is added or a line rewraps.

   flex-direction:column, not row. With a single child the default
   align-items:stretch makes it full-width for free -- no width:100% needed,
   and no <=767px override needed either. gap and flex-wrap left with the
   second button. */
.cab-cta{display:flex;flex-direction:column;margin-top:auto;padding-top:26px}

/* Lets the long label wrap. It measures 251.6px in real Inter Bold at 15px and
   the button gives it 438px at 1440px, so ON DESKTOP THIS RULE IS INERT --
   one line, already centred. It earns its place at <=375px, where the button
   is down to 247px and the label must break. MEASURED; a first guess that it
   wrapped on desktop was wrong by 186px.

   Deliberately NOT inside a media query: the wrap point is a function of the
   LABEL, and the label lives in content.php. Lengthen it and the breakpoint
   moves. Three properties, each load-bearing:

     white-space:normal   .btn hard-sets `nowrap`. WITHOUT THIS the label
                          overflows the card at 375px instead of wrapping.
                          The only .btn on the site allowed to wrap.
     text-align:center    .btn's justify-content:center centres the flex LINE
                          BOX; it does not align a second line inside it.
                          Without this, line 2 rags left.
     line-height:1.35     .btn sets none, so two lines would collide.
                          Matches .cp-head h3.

   The old `.cab-cta .btn:not(.btn-outline){border:1.5px solid transparent}` is
   REMOVED. It reserved a border so the borderless <a> matched the bordered
   <button> when the row stacked. There is no <button> here any more, and one
   element cannot mismatch itself.

   >> THE TRAP IT PATCHED IS NOT FIXED. box-sizing:border-box does NOT absorb a
      border when height:auto. THE HERO still pairs a bordered <button> with a
      borderless <a> and will show the same 2px the moment that row wraps. Do
      not read this deletion as the fix. */
.cab-cta .btn{white-space:normal;text-align:center;line-height:1.35}

/* height:100% needs a definite parent height; align-items:stretch on the grid
   supplies it. Without the stretch, 100% resolves against auto and collapses. */
.cab-photo{border-radius:18px;overflow:hidden;line-height:0}
.cab-photo picture{display:block;height:100%}
.cab-photo img{width:100%;height:100%;object-fit:cover;display:block}

.svc ul li .ck{color:var(--teal);flex:none}
.svc-link{display:inline-flex;align-items:center;gap:6px;color:var(--orange);font-weight:700;font-size:14.5px;transition:gap .15s}
.svc-link:hover{gap:11px}
/* The "just ask" line. Same #b9c2cf as the Pro Mount footnote below it (8.78:1
   on navy) -- the two are one register. Emphasis is carried by the orange
   phone link, not by the sentence. An earlier version set this to --ink; white
   shouted, and the link was already doing the work.

   34px is the gap the Pro Mount line used to own. It now binds THIS line to
   the card grid above it. */
.svc-ask{text-align:center;margin-top:34px;color:#b9c2cf;font-size:15px;line-height:1.6}

/* The tel: link. --orange is 5.57:1 on navy, which PASSES 4.5:1 for 15px
   normal text. (The doc's note that orange fails is about SMALL text --
   4.26:1 at 13px -- which is why the nav hover uses --orange-lt. Here the
   resting colour can be plain --orange.) Hover matches every other link.
   The icon inherits currentColor and so stays orange too, deliberately.

   NOT inline-flex. It sits INLINE, MID-SENTENCE, and an inline-flex box takes
   its baseline from its FIRST FLEX ITEM. That item is the <svg>, which has no
   baseline, so the box synthesizes one from its bottom margin edge -- dropping
   the number exactly one descent (3.62px at 15px Inter) below the sentence.
   As plain inline text the link's baseline IS the number's baseline, which IS
   the sentence's. Exact at any size.
     >> Do NOT "fix" this with vertical-align:middle on the <a>. That leaves
        1.56px of error and ties the result to the font's x-height.
     >> Do NOT copy .foot-phone / .phone here. They are inline-flex, but they
        are flex ITEMS inside flex containers, so no surrounding text ever
        tests their baseline. Different case.

   nowrap because the whole line wraps below 606px (it measures 557.1px in real
   Inter against 1132px of .wrap), and without this the phone icon can be
   orphaned from the number across the break. The link stays atomic -- this
   never depended on flex. */
.ask-phone{white-space:nowrap;color:var(--orange);font-weight:600;transition:color .15s}
.ask-phone:hover{color:var(--orange-lt)}

/* The icon rides the DIGITS, not the x-height. Inter's figures are lining:
   they span baseline..cap-height (1490/2048 upem = 10.91px at 15px). Centring
   a 16px box on that band gives 10.91/2 - 16/2 = -2.54px. Centring on the
   x-height instead would give -3.91px and sink the icon against the numbers.
   Read from the font binary, not eyeballed.

   `svg{display:block}` is global; vertical-align only applies to inline-level
   boxes, so display:inline is required here or the icon drops to its own line.
   margin-right replaces the flex `gap` it no longer has. */
.ask-phone svg{display:inline;vertical-align:-2.5px;margin-right:6px}

/* Was margin-top:34px off the grid; that 34px moved to .svc-ask. 26px here
   holds the footnote APART from the ask line rather than merely lower on the
   page -- two steps, not one number. */
.svc-tv{text-align:center;margin-top:26px;color:#b9c2cf;font-size:15px}
/* The Pro Mount USA link carries a two-tone wordmark, so the anchor sets no
   colour of its own — .wm-inline paints PRO/USA white, Mount orange. The
   previous rule (color:var(--orange) + a border-bottom underline) was
   removed: it would have flattened the wordmark to one colour, and no
   other link on the site is underlined. The global a{} already handles
   colour:inherit and text-decoration:none. */
.svc-tv a:hover .wm-inline,.svc-tv a:hover .wm-inline em{color:var(--orange-lt)}

/* Inline sibling of .wm. Cannot reuse .wm itself — that hard-sets
   color:#fff and font-size:19px, which would tower over this 15px line.
   Inherits its size from the sentence; white on navy, orange mid-word. */
.wm-inline{font-family:'Poppins',sans-serif;font-weight:800;letter-spacing:.3px;color:var(--ink);transition:color .15s}
.wm-inline em{color:var(--orange);font-style:normal;transition:color .15s}

.how{background:var(--dark)}
.how .head h2{color:#fff}
.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}
.step{background:var(--navy-800);border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:30px 26px;transition:transform .2s ease,box-shadow .2s ease,border-color .2s}
/* The dark card. Identical hover values to the white ones. The GLOW is cast
   outside the card, onto the same navy, so it is literally the same glow --
   measured peak +0.0398 either way. What .step lacks is separation between the
   glow and its own edge (1.61:1, against 9.08:1 for a white card). The ORANGE
   BORDER covers that, taking the edge from 1.27:1 (the rgba(255,255,255,.08)
   hairline over navy-800) to 5.16:1. So .step needs no special-casing, but it
   does need the border. Do not drop it, and do not swap it for a brighter white. */

.step-n{font-family:'Poppins';font-weight:900;font-size:40px;color:transparent;-webkit-text-stroke:1.5px rgba(255,107,53,.55);margin-bottom:14px;line-height:1}
.step h3{color:#fff;font-size:20px;font-weight:700;margin-bottom:10px}
.step p{color:#aeb8c6;font-size:14.5px;line-height:1.6}

.why-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}
.why{display:flex;gap:18px;background:#fff;border:1px solid transparent;border-radius:16px;padding:26px;transition:transform .2s ease,box-shadow .2s ease,border-color .2s}

.why-ic{flex:none;width:48px;height:48px;border-radius:12px;background:rgba(255,107,53,.1);display:flex;align-items:center;justify-content:center;color:var(--orange)}
.why h3{font-size:18px;font-weight:700;margin-bottom:7px;color:var(--ink-dark)}
.why p{color:#7a746a;font-size:14.5px;line-height:1.6}

.reviews{background:var(--navy)}
.rev-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
.rev{background:#fff;border:1px solid transparent;border-radius:18px;padding:28px;transition:transform .2s ease,box-shadow .2s ease,border-color .2s}

/* Hover lives behind hover:hover. On a touch screen :hover LATCHES after a tap
   and persists until you tap elsewhere -- inside the mobile snap scrollers that
   left a card lifted and glowing while you swiped past it. The query asks
   whether the PRIMARY pointer can hover: true for a mouse, false for touch.
   This gates hover at every width, not only <=767px, which is right. */
@media (hover:hover){
  .svc:hover,.why:hover,.rev:hover,.step:hover{transform:translateY(-8px);box-shadow:0 24px 32px rgba(255,107,53,.35);border-color:var(--orange)}
}

.stars{display:flex;gap:3px;margin-bottom:16px;color:var(--orange)}
.rev blockquote{color:#3a362f;font-size:15.5px;line-height:1.65;font-weight:500;margin-bottom:18px}
.rev figcaption{color:var(--ink-dark);font-weight:700;font-size:14px}
.rev figcaption span{color:#9a948a;font-weight:500}

.area{background:var(--dark)}
/* start, not center. The text column is shorter than .area-card, so centring
   floated it. Tops are FLUSH -- deliberately not optically aligned, because
   .area-card has no heading to align to: its first child is an .ac-row (icon
   + 15px body text) whose text cap sits 47px down. Matching the .kick to that
   would align a 13px label to a body line, which reads as nothing; matching
   the h2 to it needs 9.1px of NEGATIVE padding. Flush is the honest answer. */
.area-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:48px;align-items:start}
.area h2{color:#fff}
.area .sub{color:#b9c2cf}
.counties{display:flex;flex-wrap:wrap;gap:10px;margin:24px 0}
.county{display:inline-flex;align-items:center;gap:7px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#fff;padding:9px 15px;border-radius:30px;font-weight:600;font-size:14px}
.county svg{color:var(--orange)}
.area-note{color:#aeb8c6;font-size:14.5px}
.area-note a{color:var(--orange-lt);font-weight:700}
.area-card{background:var(--navy-800);border:1px solid rgba(255,255,255,.09);border-radius:20px;padding:28px;box-shadow:0 24px 60px rgba(0,0,0,.4)}
.ac-row{display:flex;align-items:center;gap:13px;color:#dde3ea;font-size:15px;font-weight:500;padding:13px 0;border-bottom:1px solid rgba(255,255,255,.07)}
.ac-row svg{color:var(--orange);flex:none}
.ac-row:last-of-type{border-bottom:none}

.quote{background:var(--navy);padding:37px 0}   /* keep in step with .section */
/* start, not center. The copy column is much shorter than .form, so centring
   floated 'Get your free quote' far below 'Request a quote'. */
.quote-grid{display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:start}
/* 32px, NOT .form's 30px padding. The two headings are different sizes, so
   their caps sit at different depths inside their own line boxes: the 36px/1.1
   h2's cap is 6.30px down (negative half-leading pulls it up), the 22px/normal
   h3's is 8.00px down. Card top -> h3 cap = 30 + 8.00 = 38.00px. Column top ->
   h2 cap = 6.30px. Difference 31.70px. Padding by 30px would leave them 1.7px
   apart. Measured with the real Poppins metrics, not assumed.
   TRACKS .form's padding: change one, re-derive the other. */
.quote-copy{padding-top:32px}
.quote-copy h2{color:#fff;font-size:36px;font-weight:800;line-height:1.1;letter-spacing:-.4px}
.quote-copy p{color:#b9c2cf;font-size:17px;line-height:1.6;margin:16px 0 24px}
.quote-list{list-style:none;margin-bottom:26px}
.quote-list li{display:flex;align-items:center;gap:11px;color:#fff;font-weight:600;font-size:15px;padding:7px 0}
.quote-list li svg{color:var(--orange);flex:none}
.quote-phone{display:inline-flex;align-items:center;gap:9px;color:#fff;font-weight:700;font-size:16px;border-bottom:2px solid var(--orange);padding-bottom:3px}
.quote-phone svg{color:var(--orange)}   /* icon orange, number stays white */
.form{background:#fff;border-radius:20px;padding:30px;box-shadow:0 30px 70px rgba(0,0,0,.3)}
/* margin-bottom was 20px, holding the heading off the first .frow. The
   .form-note now sits between them and carries the spacing: 6px above, 14px
   below. The form is ~18px taller than before, which is the height of the line
   of text that was added. Margin collapsing does not avoid that -- it merges
   adjacent MARGINS and says nothing about an element with height between them.

   This does NOT disturb the confirmation screen. `.form h3` also matches
   `.sent h3` (.sent renders inside #formWrap, which keeps .form), but .sent h3
   sets the `margin` SHORTHAND, ties this rule at (0,0,1,1), and wins on source
   order. Verified. */
.form h3{font-size:22px;font-weight:800;margin-bottom:0;color:var(--ink-dark)}

/* "All fields required." -- the only VISIBLE cue that the controls are
   mandatory. One line, because ALL five are: an asterisk on each would
   distinguish nothing from nothing, and these fields carry placeholders rather
   than <label>s, so there is nowhere for a `*` to live that does not vanish
   the moment the user types.

   #7a746a, the site's body grey, at 4.63:1 on white. NOT .form-fine's #9a948a,
   which is 3.01:1 and fails 4.5:1 -- a pre-existing problem on that element,
   left alone, logged. Do not "unify" the two by copying the failing one here.

   12.5px matches .field-err, so the form's two secondary lines agree. */
.form-note{color:#7a746a;font-size:12.5px;line-height:1.4;margin:6px 0 14px}
.frow{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
/* Row spacing. Paired rows use .frow (margin-bottom:12px). The two STANDALONE
   fields (timing <select>, textarea) are DIRECT children of .form, so the child
   combinator gives them the same 12px WITHOUT also hitting the four .field divs
   nested inside .frow grids -- a plain descendant `.form .field` did hit those,
   adding phantom margin below each input inside the paired rows and making the
   gaps uneven. Every row-level gap is now a single uniform 12px from exactly
   one source: .frow for pairs, .form > .field for standalones. */
.form > .field{margin-bottom:12px}
.form input,.form select,.form textarea{width:100%;border:1.5px solid #e4ddd2;border-radius:11px;padding:13px 14px;font-size:15px;font-family:inherit;color:var(--ink-dark);background:#fdfcfa;transition:border-color .15s,box-shadow .15s;outline:none}
/* The textarea's row gap now comes from `.form > .field` above, same as the
   timing row -- no separate rule needed. The `.field-last` class is still on the
   textarea's wrapper in the markup but no longer carries spacing; left in place
   to avoid touching the form markup for a pure CSS fix. (:has(textarea) was
   avoided originally because it fails in Firefox before 121; moot now.) */
.form textarea{resize:vertical}
.form input:focus,.form select:focus,.form textarea:focus{border-color:var(--orange);box-shadow:0 0 0 3px rgba(255,107,53,.14)}

/* ---- error state --------------------------------------------------------
   RED, NOT ORANGE. Orange is already the :focus colour, so an invalid field
   looked exactly like a focused one -- and orange measures 2.77:1 against the
   input's #fdfcfa, under even the 3:1 non-text bar. #c0392b is 5.30:1 on the
   input and 5.44:1 on white, and app.js already used it for its network-error
   line.

   THIS BLOCK MUST STAY BELOW :focus. A field is routinely focused AND invalid
   at once, because app.js focuses the first bad field. `.form input.is-err` and
   `.form input:focus` are BOTH (0,0,2,1) -- an exact tie, decided by source
   order. Move this up and the red turns orange the instant the field is
   focused. Same trap as `.area h2` and `.cab-photo img`.

   Toggled as a CLASS by app.js. The old code set el.style.borderColor inline,
   which outranked every stylesheet rule and was never cleared: fix the field,
   and it stayed red until reload. */
.form input.is-err,.form select.is-err,.form textarea.is-err{border-color:#c0392b;background:#fffafa}
.form input.is-err:focus,.form select.is-err:focus,.form textarea.is-err:focus{border-color:#c0392b;box-shadow:0 0 0 3px rgba(192,57,43,.14)}

/* Always in the DOM, empty until app.js writes to it. NOT inserted on demand:
   an element that appears would reflow the form and slide the submit button
   under a cursor that is already moving toward it. Empty, a <p> with no margin
   generates no line box and costs no height. */
.field-err{color:#c0392b;font-size:12.5px;line-height:1.4;margin-top:6px}
.field-err:empty{display:none}

/* ZIP -> city hint, filled by app.js after a 5-digit ZIP resolves against
   api/lookup-zip.php ("City, County County", or "Zip not found"). It sits on
   the WHITE .form card, so it needs a DARK colour -- the site's --ink is #fff
   (white), which a bare <p> would inherit and render invisible on white. Uses
   #7a746a, the same muted body grey as .form-note (4.63:1 on white, AA-pass),
   so both form hints read as one family. 12.5px matches .field-err. Single
   muted colour for BOTH states by design (found and not-found look the same).
   :empty collapses it so there is no stray gap before a ZIP is entered. */
.zip-city{color:#7a746a;font-size:12.5px;line-height:1.4;margin-top:6px}
.zip-city:empty{display:none}

/* The generic failure line, when the server rejects for a reason no single
   field owns. Was an inline style.cssText in app.js. */
.form-err{color:#c0392b;font-size:13px;text-align:center;margin-top:10px}

.form-fine{color:#9a948a;font-size:12px;text-align:center;margin-top:12px}
.sent{text-align:center;padding:20px 4px}
.sent .sic{display:flex;justify-content:center;color:var(--orange)}
.sent h3{margin:14px 0 8px;font-size:22px}
.sent p{color:#6b665d;font-size:15px;line-height:1.6;margin-bottom:20px}

/* Post-submit confirmation panel (handyman). Renders inside #formWrap, which
   keeps the white .form card, so all colours are set for a WHITE background.
   Class-namespaced .conf-* so nothing here leans on .form/.sent inheritance. */
.conf{text-align:center;padding:10px 4px}
.conf-ic{width:78px;height:78px;border:3px solid var(--orange);border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--orange);margin:0 auto 22px}
.conf-h{font-family:'Poppins','Inter',sans-serif;font-weight:800;font-size:32px;line-height:1.1;margin:0 0 18px;color:var(--ink-dark)}
.conf-h span{color:var(--orange)}
.conf-p{color:#6b665d;font-size:16px;line-height:1.6;margin:0 auto 26px;max-width:440px}
.conf-p b{color:var(--orange);font-weight:700}
.conf-call{border:1px solid #e6e0d6;border-radius:14px;padding:18px;margin:0 auto 26px;max-width:420px}
.conf-call-l{color:#8a8478;font-size:13px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;margin-bottom:6px}
.conf-call a{color:var(--orange);font-size:20px;font-weight:800;text-decoration:none}
.conf-call a:hover{text-decoration:underline}
.conf-btn{display:inline-flex;gap:9px;font-size:16px;padding:15px 34px}
.conf-sec{margin-top:24px;color:#9a948a;font-size:12.5px;display:flex;align-items:center;justify-content:center;gap:6px}
.conf-sec svg{flex:none}
@media(max-width:520px){ .conf-h{font-size:26px} }

/* Standalone confirmation page (contact-submitted.php). SEPARATE prefix
   (.confp-*) from the forms' .conf-*: this panel sits on the site's DARK
   --navy body background, not the white form card or the raised cabinet
   card, so colours are set for a dark surface (light text). Contrast on
   --navy (#1a2332), measured: headline #fff 15.78:1, body #b6b0a6 7.33:1,
   accents --orange-lt 5.91:1, call-label #8f8a80 4.60:1, secure #948e83
   4.85:1 — all pass AA. (The forms' secure grey #6f6a60 is only 2.94:1 on
   this flat navy and fails, hence the lift here.) .confp-wrap clears the
   70px fixed nav and vertically centres the card. These selectors are
   class-scoped and match NOTHING on the home page. */
.confp-wrap{display:flex;align-items:center;justify-content:center;padding:144px 20px 104px}
.confp{text-align:center;max-width:640px;width:100%;background:var(--navy-800);border:1px solid rgba(255,255,255,.06);border-radius:20px;padding:52px 40px}
.confp-ic{width:84px;height:84px;border:3px solid var(--orange);border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--orange);margin:0 auto 26px}
.confp-h{font-family:'Poppins','Inter',sans-serif;font-weight:800;font-size:36px;line-height:1.1;margin:0 0 20px;color:#fff}
.confp-h span{color:var(--orange)}
.confp-p{color:#b6b0a6;font-size:16px;line-height:1.6;margin:0 auto 28px;max-width:460px}
.confp-p b{color:var(--orange-lt);font-weight:700}
.confp-call{border:1px solid rgba(255,255,255,.12);border-radius:14px;padding:18px;margin:0 auto 28px;max-width:440px}
.confp-call-l{color:#8f8a80;font-size:13px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;margin-bottom:6px}
.confp-call a{color:var(--orange-lt);font-size:20px;font-weight:800;text-decoration:none}
.confp-call a:hover{text-decoration:underline}
.confp-btn{display:inline-flex;align-items:center;gap:9px;font-size:16px;padding:15px 34px}
.confp-sec{margin-top:26px;color:#948e83;font-size:12.5px;display:flex;align-items:center;justify-content:center;gap:6px}
.confp-sec svg{flex:none}
@media(max-width:520px){ .confp{padding:40px 22px} .confp-h{font-size:28px} }

.faq{background:var(--cream)}
/* Three columns, two rows. Five items means row 2 is short: 3 + 2, third
   column empty. Deliberate.
   align-items:stretch (the grid default, stated for the record) is what makes
   the two CLOSED cards in a row grow to match an OPEN sibling -- empty space
   under their questions, as in the Pro Mount reference. Do not set `start`
   here or the row goes ragged.
   The max-height ease still works: grid row height is recomputed each frame
   from the tallest child, and that child's height IS easing, so the row
   follows it. Nothing jumps. */
.faqs{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;align-items:stretch}
/* A column, so the question pins to the TOP of a stretched card and the empty
   space falls below it. Without this the button centres itself vertically in
   whatever height the row hands down. */
.faqi{background:#fff;border:1px solid transparent;border-radius:14px;overflow:hidden;display:flex;flex-direction:column}
.faqi>button{width:100%;flex:none;display:flex;justify-content:space-between;align-items:flex-start;gap:16px;padding:20px 22px;text-align:left;font-family:'Poppins';font-weight:600;font-size:16px;color:var(--ink-dark)}   /* flex:none -- the column must not stretch the button */
/* margin-top nudges the chevron onto the first line's centre now that the
   button is align-items:flex-start and a question may wrap to two lines. */
.faqi .chev{transition:transform .2s ease;color:var(--orange);flex:none;margin-top:2px}
.faqi.open .chev{transform:rotate(180deg)}
.faqa{max-height:0;overflow:hidden;transition:max-height .28s ease}
/* 400px, not 240px. Nothing today needs it -- the tallest answer runs 139.6px
   in a 369px column, measured with the real font. But this is a magic number
   that CLIPS SILENTLY when someone writes a longer answer, and the failure is
   invisible in review. 400px is roughly 3x the current worst case and still
   short enough that the .28s ease does not crawl. If an answer ever needs
   more, raise this -- do not remove the transition. */
.faqi.open .faqa{max-height:400px}
.faqa p{padding:0 22px 20px;color:#6b665d;font-size:14.5px;line-height:1.65}

.footer{background:var(--navy-950);position:relative;padding:56px 0 30px}
.tagline{text-align:center;font-family:'Poppins';font-weight:800;font-size:22px;margin-bottom:40px}
.foot-grid{display:grid;grid-template-columns:1.5fr 1fr 1fr 1fr;gap:34px;padding-bottom:34px;border-bottom:1px solid rgba(255,255,255,.09)}
.foot-brand .brand svg{color:var(--orange)}   /* footer wrench orange; nav brand unaffected */
.foot-brand p{color:#9aa4b3;font-size:14px;line-height:1.6;margin:16px 0}
.foot-phone{display:inline-flex;align-items:center;gap:8px;color:#fff;font-weight:700;font-size:16px}
.foot-phone svg{color:var(--orange)}
.foot-col h4{color:#fff;font-size:14px;font-weight:700;margin-bottom:14px;font-family:'Poppins'}
.foot-col a,.foot-county{display:block;color:#9aa4b3;font-size:14px;padding:6px 0;transition:color .15s}
.foot-col a:hover{color:var(--orange-lt)}
.foot-bottom{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;padding-top:22px;color:#77808e;font-size:13px}

/* Scroll dots. app.js only builds these below 767px, but they are hidden here
   too: if the breakpoint ever moves, or JS runs before the media query is
   evaluated, a stray row of dots must never appear under a desktop grid. */
.dots{display:none}

.reveal{opacity:0;transform:translateY(22px);transition:opacity .6s ease,transform .6s ease}
.reveal.in{opacity:1;transform:none}
@media (prefers-reduced-motion: reduce){.reveal{opacity:1;transform:none;transition:none}}

/* Drop the LIFT, keep the colour and the shadow. A translate on hover is exactly
   the motion this query exists to suppress; the border and shadow still say
   'hovered' without moving anything. Matches the treatment already given to
   .reveal and .burger. */
@media (prefers-reduced-motion: reduce){
  .why,.rev,.svc,.step{transition:box-shadow .2s ease,border-color .2s}
  .why:hover,.rev:hover,.svc:hover,.step:hover{transform:none}
}

/* Between 1100px and ~1140px the full nav still shows but the caption is the
   widest thing in the CTA column, and the row runs out of room. Trimming the
   caption's size and tracking buys back ~29px — enough to hold the layout
   without collapsing the links a breakpoint early. Contrast is unaffected
   (#9aa1ad on navy is 6.07:1, clear of the 4.5:1 small-text bar). */
@media(max-width:1140px){
  .cta-cap{font-size:9.5px;letter-spacing:.4px}
  /* The number must shrink with the caption. At 19px it is ~182px wide —
     wider than the shrunk caption (~177px) — so it, not the caption,
     would drive the column and the shrink would buy nothing. */
  .phone{font-size:17px}
  .links{gap:22px}
}

/* The nav collapses EARLIER than the page layout. The .cta-cap caption is
   wider than the phone number it sits above (~206px vs ~144px), so it sets
   the CTA column's width and the row overflows below roughly 1140px — well
   before the old 960px burger breakpoint. These four nav rules move to
   1100px; everything else stays at 960px so the page doesn't reflow early. */
@media(max-width:1100px){
  .links,.navcta{display:none}
  .burger{display:grid}
  /* .navrow is space-between; with .links and .navcta hidden its children are
     [brand] [nav-call] [burger]. Without margin-left:auto the call icon would
     be stranded mid-bar. This absorbs the free space so the two sit as a pair
     on the right, with the burger flush to the edge. */
  .nav-call{display:inline-flex;margin-left:auto;margin-right:2px}
}

@media(max-width:960px){
  .hero-grid,.area-grid,.quote-grid{grid-template-columns:1fr;gap:36px}
  /* Stacked, the photo has no card to match, so nothing constrains its
     height. Release it: height:auto restores the natural 1.714 ratio and
     object-fit:cover has nothing left to crop. A fixed height here would
     stretch-crop below ~520px, where the natural height is already under
     any cap worth setting. The card's own height goes auto with it.

     TRAP: .cab-photo img here TIES the base rule at (0,0,1,1) -- media
     queries add no specificity. height:auto wins on SOURCE ORDER alone.
     Move this block above the base rule, or append another base
     .cab-photo img anywhere below, and height:100% comes back at every
     width and the stacked photo stretch-crops. Same trap as .area h2. */
  .cab-grid{grid-template-columns:1fr;gap:36px}
  /* Hand the heading cap back to the global 640px. This is the right place
     because `.head h2{font-size:30px}` is in THIS SAME BLOCK -- the heading
     shrinks here, the grid collapses here, the cap returns here. The section
     restacks as one unit.

     At 30px the headline is 564.6px, well inside 640px, so it stays on ONE
     LINE down to 767px and only wraps below ~610px where .wrap binds.

     >> If `.head h2{font-size:30px}` is ever moved to the 767px block, MOVE
        THIS WITH IT. A 38px headline at 768px has 0.5px of slack against
        .wrap -- one hinting difference from a clipped heading. */
  .cabinets .head{max-width:640px}
  .cab-photo picture,.cab-photo img{height:auto}
  .hero{padding:104px 0 56px}
  .hero h1{font-size:44px}
  .badges{grid-template-columns:repeat(2,1fr);transform:translateY(-24px)}
  .svc-grid,.steps,.rev-grid{grid-template-columns:1fr}
  .why-grid{grid-template-columns:1fr}
  /* FAQ 3 -> 2 columns. Measured: at 960px three columns leave 216px of text
     and the longest question wraps to two lines; two columns give 370px and it
     fits on one. Rides the site's existing ladder -- no new breakpoint. */
  .faqs{grid-template-columns:repeat(2,1fr)}
  .foot-grid{grid-template-columns:1fr 1fr;gap:26px}
  .head h2{font-size:30px}
  .head h2.nowrap-desktop{white-space:normal}
  .quote-copy h2{font-size:28px}
  /* One column now: the form sits BELOW the copy, so there is nothing to the
     right to align with and the 32px is just a stray gap under the heading. */
  .quote-copy{padding-top:0}
}
/* ============================================================
   <=767px — the badge strip stops being cards and becomes a list.
   Mirrors Pro Mount USA: two columns, one orange check-disc per row,
   bold title only, sitting directly on the hero photo.

   Three things happen together here, and they depend on each other:
     1. .badge sheds its background, border, radius, shadow and blur.
     2. .bs and .bi are hidden; .bx (the check-disc) appears.
     3. .hero-band's scrim deepens at the bottom, because (1) removed
        the tint that was carrying the text's contrast.
   Undo any one of them and the strip breaks. See the file header.
   ============================================================ */
@media(max-width:767px){

  /* The scrim, deepened at the bottom TWO stops only. The top two are
     the headline's and are deliberately identical to the desktop rule.
     Repeated in full (with the same three-tier image-set fallback) because
     background-image is one property -- there is no way to restyle the
     gradient layer without restating the photo layer beneath it. */
  .hero-band{
    background-image:linear-gradient(to bottom,rgba(26,35,50,.70) 0%,rgba(26,35,50,.82) 45%,rgba(26,35,50,.70) 70%,rgba(26,35,50,.78) 100%),url('../img/hero.jpg');
    background-image:linear-gradient(to bottom,rgba(26,35,50,.70) 0%,rgba(26,35,50,.82) 45%,rgba(26,35,50,.70) 70%,rgba(26,35,50,.78) 100%),-webkit-image-set(url('../img/hero.webp') type('image/webp'),url('../img/hero.jpg') type('image/jpeg'));
    background-image:linear-gradient(to bottom,rgba(26,35,50,.70) 0%,rgba(26,35,50,.82) 45%,rgba(26,35,50,.70) 70%,rgba(26,35,50,.78) 100%),image-set(url('../img/hero.webp') type('image/webp'),url('../img/hero.jpg') type('image/jpeg'));
  }

  /* Two columns, held all the way down. Row gap is larger than the column
     gap: rows separate by whitespace alone now that no card edge does it. */
  .badges{grid-template-columns:repeat(2,1fr);column-gap:12px;row-gap:18px;transform:translateY(-24px)}

  /* The card, dismantled. `all` is not used -- it would also reset the
     .reveal transition and strand every badge at opacity:0 if the
     IntersectionObserver never fires. */
  .badge{
    background:none;
    border:none;
    border-radius:0;
    padding:0;
    box-shadow:none;
    backdrop-filter:none;
    -webkit-backdrop-filter:none;
    /* icon and title on one row, icon pinned to the first text line */
    display:flex;
    align-items:flex-start;
    gap:10px;
    text-align:left;
  }

  .badge .bi{display:none}            /* category icon: desktop only */
  .badge .bs{display:none}            /* subtitle dropped, per the reference */

  /* The check-disc. flex:none so it never squashes when the title wraps
     to two or three lines. margin-top nudges it onto the cap-height of
     the first line rather than its ascender box. */
  .badge .bx{display:flex;flex:none;color:var(--orange);margin-top:1px}

  /* margin-top:9px was spacing the title BELOW a centered icon. It now
     sits beside one, so it must go, or every row hangs low. */
  .badge .bt{margin-top:0;line-height:1.35}

  /* ---- area + quote-form headings ----------------------------------
     These two sections have NO .head, so the global centering rule does
     not reach them. Their headings sit in two-column grids beside a card
     (.area-card, .form), which is why they are left-aligned on desktop.
     Both grids collapse to one column at 960px; we centre at 767px, with
     the badge list, to keep one mobile breakpoint.

     What centres differs by section, deliberately:
       area        .kick + h2   ("SERVICE AREA" / "Serving five...")
       quote-form  h2 only      ("Get your free quote") -- it has no .kick

     What does NOT centre: .sub, .quote-copy p, .counties, .quote-list,
     .area-note, .quote-phone. So a centred heading sits above left-aligned
     prose. That is a deliberate choice, not an oversight.
     >> IF IT READS BADLY ON A PHONE: add `.area .sub` and `.quote-copy p`
        to the selectors below. One line, and the group closes up.

     Cards are untouched. Safe because .area-card holds only .ac-row divs
     and an <a> -- no h2, no .kick -- so `.area h2` and `.area .kick` each
     match exactly one element. The text column in area.php has no class of
     its own (it is a bare <div class="reveal">), which is why we select
     through .area rather than through a wrapper.

     Specificity note: `.area h2` here TIES the base `.area h2{color:#fff}`
     at 0,1,1 -- a media query adds nothing. It wins on source order alone,
     because this block sits below it. Same for `.quote-copy h2`. Move
     either rule above its base and the centring silently stops. */
  .area .kick,
  .area h2,
  .quote-copy h2{text-align:center}

  /* FAQ 2 -> 1 column. Two columns stay one-line down to ~800px and break at
     768px; this is the first breakpoint below that. One column returns the FAQ
     to the stacked accordion it has always been on a phone -- and there the
     stretch behaviour is moot, since every row holds a single card. */
  .faqs{grid-template-columns:1fr}
  /* The card loses 12px of padding on a phone; at 360px that is 24px of text
     width back, which keeps every point heading on one line -- and buys the
     CTA label 24px too, though at <=375px it wraps anyway.

     The old `.cab-cta{flex-direction:column;gap:10px}` and
     `.cab-cta .btn{width:100%}` are GONE. With one button .cab-cta is a column
     at every width and align-items:stretch already makes it full-width.
     Nothing left to override. */
  .cab-card{padding:20px}

  /* ---- horizontal card scrollers ------------------------------------
     services, why, reviews and how-it-works stop stacking and become
     one-card-per-screen snap scrollers. Measured card heights at 360px:
     svc 503px, rev 251px, step 218px, why 197px -- all inside a ~640px
     viewport, so no card scrolls vertically inside itself and there is no
     gesture conflict between swiping sideways and scrolling down.

     Cards are 100% wide, so NOTHING PEEKS at the right edge and the only
     cue that the section scrolls is the swipe itself. That was chosen. If
     it reads as a plain stack on a real phone, give .scroll-x a small
     right gutter (e.g. flex-basis:calc(100% - 14px)) and a sliver of the
     next card appears. One number.

     grid -> flex. The four selectors keep their names; only their display
     changes, so the desktop grid rules above are untouched. */
  .svc-grid,.why-grid,.rev-grid,.steps{
    display:flex;
    /* -24px cancels .wrap's padding so card 1 starts flush at the screen
       edge; the matching scroll-padding puts the snap line there too.
       Without both, the scroller is inset 24px and every snap lands short. */
    margin-left:-24px;
    margin-right:-24px;
    padding-left:24px;
    padding-right:24px;
    scroll-padding-left:24px;
    gap:12px;
    overflow-x:auto;
    scroll-snap-type:x mandatory;
    overscroll-behavior-x:contain;   /* a sideways swipe must not trigger browser back */
    -webkit-overflow-scrolling:touch;
    scrollbar-width:none;            /* Firefox */
    -ms-overflow-style:none;         /* old Edge */
  }
  .svc-grid::-webkit-scrollbar,
  .why-grid::-webkit-scrollbar,
  .rev-grid::-webkit-scrollbar,
  .steps::-webkit-scrollbar{display:none}

  /* flex:0 0 100% -- do not let a short card shrink, do not let a tall one grow.
     `width` alone would be overridden by flex-basis:auto. */
  .svc-grid>*,.why-grid>*,.rev-grid>*,.steps>*{
    flex:0 0 100%;
    scroll-snap-align:start;
    scroll-snap-stop:always;         /* one card per swipe, never skip two */
  }

  /* .reveal must not run on a card inside a scroller. Two reasons, and the
     second is the one that bites:
       1. transform:translateY(22px) shifts where the browser thinks the snap
          edge is, until .in lands.
       2. Cards 2..n are clipped by the scroller at load, so they only get .in
          when you swipe to them -- every card would fade up from 22px below
          ON ARRIVAL. A 0.6s animation triggered by the swipe that revealed it.
     Neutralised in CSS, not by adding .in from JS: this way the cards are
     visible even if JS never runs. Same shape as the reduced-motion rule.
     The section HEADINGS still reveal -- they sit outside the scroller. */
  .svc-grid .reveal,.why-grid .reveal,.rev-grid .reveal,.steps .reveal{
    opacity:1;
    transform:none;
    transition:none;
  }

  /* The dots. app.js inserts .dots as the scroller's next sibling, so it sits
     INSIDE .wrap and is not affected by the scroller's negative margins. */
  .dots{display:flex;justify-content:center;gap:2px;margin-top:18px}

  /* The button is the tap target (24px); the dot is what you see (8px). Growing
     the dot to meet the 24px guideline would give six fat circles; padding the
     button instead keeps the row light and still comfortably tappable. */
  .dot{
    width:24px;height:24px;
    display:flex;align-items:center;justify-content:center;
    padding:0;border-radius:50%;
    -webkit-tap-highlight-color:transparent;
  }
  .dot::before{
    content:'';
    width:8px;height:8px;border-radius:50%;
    background:rgba(255,255,255,.28);
    transition:background .2s ease,transform .2s ease;
  }
  .dot.on::before{background:var(--orange);transform:scale(1.15)}

  /* A button that cannot show focus is a regression. Ring, not outline, so it
     traces the circle. :focus-visible keeps it off mouse clicks. */
  .dot:focus-visible{outline:none;box-shadow:0 0 0 2px var(--orange-lt)}

  @media (prefers-reduced-motion: reduce){
    .dot::before{transition:none}
    .dot.on::before{transform:none}
  }
}
@media(max-width:520px){
  .frow{grid-template-columns:1fr}
  /* .badges{grid-template-columns:1fr} REMOVED. The <=767px list is two
     columns all the way down; this rule came LATER in source order and,
     at equal specificity, silently won. */
  .foot-grid{grid-template-columns:1fr}
  .hero h1{font-size:38px}
  .foot-bottom{flex-direction:column;gap:6px}
}
/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-09  08:36 PM CDT
 * Summary:  TWO changes this session. (2) is new since 03:18 PM; take
 *           THIS copy of the file, not the earlier one.
 *
 * ---- (1) Mobile badge strip, <=767px --------------------------------
 *
 *           The badge strip stops being cards and becomes a Pro Mount
 *           USA-style two-column LIST: no cards, no subtitles, one
 *           repeated orange check-disc per row, text directly on the
 *           hero photo.
 *
 *           Removing the card removes the thing that made the text
 *           legible. Measured, not guessed: hero.jpg composited exactly
 *           as the browser renders it (cover, center 40%) at 767 / 520 /
 *           420 / 380 / 360px, the scrim applied, and the pixels under
 *           each of the eight rows sampled.
 *
 *           With the desktop scrim (.62/.55 at the bottom), white text
 *           on the bare photo FAILS: 13-20% of pixels under the text
 *           fall below 4.5:1, bottoming at 3.69:1. The failure is not
 *           spread out. Rows 1-3 pass everywhere; ROW 4 fails at 56%.
 *           Row 4 sits at 92-99% of the band, where the scrim is at its
 *           thinnest AND the photo is at its brightest -- the pale wall
 *           and light tools, raw mean rgb(178,169,163), saturating to
 *           white. The scrim's own bottom stop is the problem.
 *
 *           Two fixes were swept. A text-shadow, modelled honestly with
 *           the real glyph mask and gaussian falloff (not as a flat
 *           local scrim, which flatters it ~2x), tops out around
 *           4.5-4.9:1 -- and a shadow strong enough to be safe is
 *           strong enough to halo on eight short lines. Deepening the
 *           scrim's bottom two stops wins on every axis.
 *
 *           Landed on .70 @70% and .78 @100%, mobile only:
 *             worst cell 5.97:1 (Free Quotes @360px), 4.5:1 bar
 *             zero failing pixels at any tested width
 *             photo sigma 0.0244-0.0254, ABOVE the desktop cards' 0.0202
 *
 *           The top two stops (.70 @0%, .82 @45%) are NOT touched. They
 *           are load-bearing for the headline, which measures 7.34:1
 *           before and after. Do not move them.
 *
 *           Also: the .badges rule was deleted from the 520px block. It
 *           set one column, and being LAST in source order it would have
 *           overridden the new 767px two-column rule at equal
 *           specificity. Two columns now hold all the way down.
 *
 *           One trap worth naming: .bx is emitted on EVERY badge by
 *           badge.php, so it needs a base .badge .bx{display:none} outside
 *           the media query. Without it, div's display:block wins and a
 *           stray orange disc appears on every desktop card.
 *
 *           IF THE HERO IMAGE EVER CHANGES, re-derive every number in
 *           this header and in the badge-card block below. Contrast here
 *           is a property of the photo, not of the CSS.
 *
 * ---- (2) Centered headings, tighter sections ------------------------
 *
 *           .head is now centered: margin:0 auto and text-align:center.
 *           max-width:640px alone does NOT center it -- without the auto
 *           margins the box stays pinned left and only its text moves.
 *
 *           This centers all FIVE .head sections at once: why, reviews,
 *           services, how, faq. services.php carried these exact
 *           declarations inline and now does not; the inline copy would
 *           have won on specificity and quietly ignored this rule.
 *           area and quote-form have no .head -- their headings sit in
 *           two-column grids beside a card, and stay left-aligned.
 *
 *           Each .head centers within its own .wrap: 1132px of content
 *           for four of them, 772px for the FAQ (which is in
 *           .wrap.narrow). The FAQ heading therefore centers over its
 *           accordion, which is the content it labels. Correct.
 *
 *           .section padding is now 37px, and .quote with it. Cut TWICE this
 *           session: 84 -> 56 -> 37, a 56% total reduction.
 *           PADDING DOES NOT COLLAPSE, so two adjacent sections stack to
 *           twice this: every section-to-section gap falls from 168px to
 *           74px. .quote is NOT a .section (quote-form.php uses
 *           class="quote") and keeps its own copy of the number, so the two
 *           must be changed together or the quote form becomes the tallest
 *           block on the page.
 *
 *           .head{margin-bottom:44px} was deliberately NOT cut. A heading
 *           therefore sits 44px from its own cards while whole sections sit
 *           74px apart -- a 1.7x ratio. That is a deliberate choice, made
 *           with the tradeoff named. If the page reads as one undifferentiated
 *           column, cut .head's margin to ~30px before touching .section again.
 *
 *           Knock-on: .footer keeps padding:56px 0 30px, so its top padding
 *           is now the LARGEST single gap on the page. Left alone on purpose.
 *
 *           .hero and .footer set their own padding and are untouched.
 *           No media query overrides .head, .section or .quote, so this
 *           carries to mobile unchanged.
 *
 * ---- (3) area + quote-form headings, <=767px ------------------------
 *
 *           These two sections have no .head, so (2) does not reach them.
 *           Their headings sit beside a card in a two-column grid, which
 *           is why they are left-aligned on desktop. Both grids collapse
 *           at 960px; we centre at 767px to keep one mobile breakpoint.
 *
 *           area centres .kick + h2. quote-form centres h2 only -- it has
 *           no .kick. Nothing else centres: .sub, .quote-copy p, the
 *           county pills, .quote-list, .area-note and .quote-phone all
 *           stay left, so a centred heading sits above left-aligned prose.
 *           Deliberate. The escape hatch, if it reads badly on a phone, is
 *           to add .area .sub and .quote-copy p to that selector.
 *
 *           The rules live INSIDE the (1) media block -- one 767px block,
 *           not two. `.area h2` there TIES the base `.area h2{color:#fff}`
 *           at 0,1,1, since a media query adds no specificity; it wins on
 *           SOURCE ORDER only. Same for `.quote-copy h2`. Verified with a
 *           real CSS parser, not by reading. Move either block above its
 *           base rule and the centring silently stops.
 *
 * ---- (4) Card hover: why / reviews / services / how ------------------
 *
 *           All four card types lift 8px, cast an ORANGE GLOW, and take an
 *           orange border on hover. .svc already had a lift and a shadow;
 *           .why, .rev and .step had no hover and no transition at all.
 *
 *           The shadow is orange because a BLACK one cannot glow on this
 *           palette, and that is not a matter of taste. Measured on navy
 *           (linear luminance 0.0165, JND ~0.004): the old black shadow moved
 *           it -0.0061 -- barely past threshold, and DOWNWARD into a colour
 *           that has almost no room left to darken. The orange glow at the
 *           same geometry moves it +0.0398, six times the magnitude, in the
 *           direction the eye notices. This is why it "didn't glow".
 *
 *           Alpha .35 and blur 32px are both bleed-limited, not taste. The
 *           grids sit 20px (.svc-grid, .rev-grid) and 22px (.why-grid, .steps)
 *           apart. At the OLD 48px blur, every alpha spilled onto the
 *           neighbouring card's edge (+0.005 to +0.008, all above the 0.004
 *           threshold) -- the old geometry was already too loose, and the
 *           black shadow only hid it by being invisible. 32px is the knee:
 *           clean at both gaps up to alpha .35, and still +0.033 of lift.
 *
 *           dy stays 24px. A card lifted L px wants a shadow offset of about
 *           2-3x L; at the old -4px, dy:24 was 6.0x -- detached, part of why
 *           it read as a smudge. At -8px it is 3.0x. Blur 32 / offset 24 =
 *           1.33x keeps the penumbra tucked under the card, not fanning past.
 *
 *           Fixed a dead declaration: .svc:hover set border-color:transparent,
 *           the same value as its base rule, so it did nothing -- while sitting
 *           in the transition list. Someone meant to write a colour there.
 *
 *           No layout shift on any of them: all four already carry a 1px
 *           border (transparent on the white cards, a faint white hairline on
 *           .step), so only border-COLOR changes. Never use the border
 *           shorthand here.
 *
 *           .step needs no special-casing. The glow is cast OUTSIDE the card,
 *           onto the same navy, so it is literally the same glow (+0.0398
 *           either way). What .step lacks is separation between the glow and
 *           its own edge (1.61:1 vs 9.08:1 for a white card) -- and the orange
 *           border covers that, taking the edge from 1.27:1 to 5.16:1.
 *
 *           Border colour is --orange, not --orange-lt or --orange-dk. Its
 *           weakest edge is against the white cards (2.84:1), which needs only
 *           to be VISIBLE, not legible -- the 4.5:1 text bar does not apply to
 *           a border. On the two edges that matter it is strongest: 5.57:1 on
 *           navy, 5.16:1 on navy-800.
 *
 *           prefers-reduced-motion drops the LIFT and keeps the glow and
 *           border -- a translate on hover is exactly the motion that query
 *           exists to suppress. One trap: every card also carries .reveal,
 *           whose rule in that same block sets transition:none. .reveal and
 *           .why TIE at 0,1,0, so the card rule wins on SOURCE ORDER only.
 *           Reorder that block and the hover stops easing. Verified with a
 *           real CSS parser.
 *
 * ---- (5) Top-align the two text/card grids ---------------------------
 *
 *           .quote-grid and .area-grid both used align-items:center, which
 *           floated the shorter text column against a taller card. Both are
 *           now align-items:start.
 *
 *           quote-form gets OPTICAL alignment: .quote-copy{padding-top:32px}
 *           puts "Get your free quote" on the same cap line as "Request a
 *           quote". 32px, NOT .form's 30px padding -- the two headings are
 *           different sizes and their caps sit at different depths inside
 *           their own line boxes. The 36px/lh-1.1 h2's cap is 6.30px down
 *           (negative half-leading pulls it up); the 22px/normal h3's is
 *           8.00px. Card top -> h3 cap is 30 + 8.00 = 38.00. Column top ->
 *           h2 cap is 6.30. Difference 31.70, rounded to 32, leaving the
 *           caps 0.3px apart. The card's own 30px would have left 1.7px.
 *           Measured against the real Poppins metrics. THIS NUMBER TRACKS
 *           .form's padding and both heading sizes -- change any of them
 *           and re-derive it.
 *
 *           area gets FLUSH TOPS, not optical alignment, and that is
 *           deliberate. .area-card has no heading to align to: its first
 *           child is an .ac-row (18px icon + 15px body text) whose text cap
 *           sits 47px below the card top. Aligning the .kick to that would
 *           put a 13px uppercase label on a body line -- nothing reads as
 *           aligned. Aligning the h2 to it needs 9.1px of NEGATIVE padding.
 *           Flush is the only honest option, and the columns are close
 *           enough in height that it already looked nearly right.
 *
 *           Both grids collapse to one column at 960px, where the card sits
 *           BELOW the copy and there is nothing to align with. .quote-copy's
 *           padding is reset to 0 there, or it is a stray gap.
 *
 * ---- (6) FAQ as a three-column grid ---------------------------------
 *
 *           .faqs goes from a flex column to grid, 3 columns at 12px gap,
 *           matching the Pro Mount reference. Five items means row 2 is
 *           short: 3 + 2, third column empty. Cards stay WHITE.
 *
 *           faq.php moves from .wrap.narrow to .wrap, and that is required
 *           rather than cosmetic. Measured with the real Poppins metrics:
 *           narrow gives 772px, so three columns leave each question 169px
 *           of text and "Do I need to supply the materials?" wraps to THREE
 *           lines. The full .wrap gives 1132px, columns of 369px, text of
 *           289px -- all five questions fit on one line.
 *
 *           align-items:stretch is stated explicitly, though it is the grid
 *           default, because it is load-bearing: it is what makes the two
 *           CLOSED cards in a row grow to match an OPEN sibling, leaving
 *           empty space under their questions exactly as in the reference.
 *           Setting `start` here would leave the row ragged.
 *
 *           .faqi therefore becomes display:flex;flex-direction:column, so
 *           the question pins to the TOP of a stretched card and the space
 *           falls below it. Its button takes flex:none, or the column
 *           stretches the button instead. The button also moves to
 *           align-items:flex-start (questions can wrap now), and .chev gains
 *           margin-top:2px to sit on the first line's centre.
 *
 *           The max-height ease SURVIVES the grid. Grid row height is
 *           recomputed each frame from the tallest child; that child's
 *           height is itself easing, so the row follows it and nothing jumps.
 *           This was the one thing worth checking before committing to the
 *           layout.
 *
 *           max-height goes 240px -> 400px. Nothing needs it today: the
 *           tallest answer runs 139.6px in a 369px column. But 240px is a
 *           magic number that CLIPS SILENTLY when someone writes a longer
 *           answer, and the failure is invisible in review. If an answer
 *           ever exceeds 400px, raise it -- do not remove the transition.
 *
 *           Breakpoints ride the site's existing ladder, no new ones:
 *             <=960px  3 -> 2 columns  (3-col leaves 216px of text; wraps)
 *             <=767px  2 -> 1 column   (2-col holds to ~800px, breaks at 768)
 *           At one column it is the stacked accordion it has always been on
 *           a phone, and the stretch behaviour is moot there.
 *
 *           app.js: toggleFaq() is now an exclusive accordion -- see that
 *           file. Opening one card closes any other; clicking an open card
 *           still closes it.
 *
 * ---- (7) Mobile card scrollers, <=767px ------------------------------
 *
 *           services, why, reviews and how-it-works stop stacking and
 *           become one-card-per-screen horizontal snap scrollers. The
 *           four selectors keep their names; only display changes, so
 *           every desktop grid rule above is untouched. At 960-767px they
 *           are still a single-column grid -- scroller is phone-only.
 *
 *           Card heights were measured first, because a card taller than
 *           the viewport would put a vertical scroll INSIDE a horizontal
 *           one. At 360px: svc 503px, rev 251px, step 218px, why 197px --
 *           all inside a ~640px viewport. No gesture conflict. This is why
 *           .svc-grid could be included despite nine list items per card.
 *
 *           Cards are 100% wide, so NOTHING PEEKS at the right edge and
 *           the only cue that the section scrolls is the swipe itself.
 *           Chosen deliberately. If it reads as a plain stack on a real
 *           phone, give the children flex-basis:calc(100% - 14px) and a
 *           sliver of the next card appears. One number.
 *
 *           Two rules are load-bearing and easy to delete by accident:
 *
 *           1. NEGATIVE MARGINS. -24px cancels .wrap's padding so card 1
 *              starts flush at the screen edge, with matching padding and
 *              scroll-padding so the snap line lands there too. Drop
 *              either and the scroller is inset 24px and every snap falls
 *              short. Safe because no ancestor clips: .wrap and .section
 *              set no overflow, and the only overflow:hidden on the page
 *              is .hero-band, which contains none of these sections.
 *
 *           2. .reveal IS NEUTRALISED INSIDE THE SCROLLERS. Cards 2..n are
 *              clipped by the scroller at load, so they only receive .in
 *              when you swipe to them -- every card would fade up from
 *              22px below ON ARRIVAL, a 0.6s animation triggered by the
 *              very swipe that revealed it. Its transform:translateY(22px)
 *              also shifts the snap edge until .in lands. Neutralised in
 *              CSS rather than by adding .in from JS, so the cards are
 *              visible even if JS never runs. Section HEADINGS still
 *              reveal -- they sit outside the scroller.
 *
 *           scroll-snap-stop:always means one card per swipe, never two.
 *           overscroll-behavior-x:contain stops a sideways swipe from
 *           triggering browser back-navigation.
 *
 * ---- (8) Card hover is gated behind hover:hover -----------------------
 *
 *           .svc/.why/.rev/.step:hover moved inside @media (hover:hover).
 *           On a touch screen :hover LATCHES after a tap and persists
 *           until you tap elsewhere; inside a snap scroller that left a
 *           card lifted 8px and glowing while you swiped past it.
 *
 *           The query asks whether the PRIMARY pointer can hover -- true
 *           for a mouse, false for touch. This gates hover at EVERY width,
 *           not only <=767px, which is a wider change than the scrollers
 *           alone but the correct one.
 *
 *           The prefers-reduced-motion block still sets transform:none on
 *           those same hovers and still sits BELOW this block, so it wins
 *           where both apply. Media queries add no specificity; order does.
 *
 * ---- (9) Scroll dots --------------------------------------------------
 *
 *           A row of dots under each mobile scroller: one per card, the
 *           active one orange, each a real <button> that scrolls to its
 *           card. This is the affordance the 100%-wide cards lacked --
 *           nothing peeks at the right edge, so at rest a scroller looked
 *           exactly like a single stacked card.
 *
 *           app.js builds them (see that file), so the count can never
 *           drift from the cards actually rendered. .dots{display:none} is
 *           declared at BASE and only lifted inside the 767px block. app.js
 *           already refuses to build them above 767px, so this is belt and
 *           braces: if the breakpoint moves, or JS runs before matchMedia
 *           settles, a stray row must never appear under a desktop grid.
 *
 *           The button is the 24px tap target; the dot is an 8px ::before.
 *           Growing the dot itself to meet the tap-target guideline would
 *           give six fat circles. .dot:focus-visible gets a ring rather
 *           than an outline, so it traces the circle -- a button that
 *           cannot show focus is a regression.
 *
 *           .dots is inserted as the scroller's NEXT SIBLING, so it lives
 *           inside .wrap and is untouched by the scroller's -24px margins.
 *           Negative margins pull their own box; they do not inherit.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-09  10:04 AM CDT
 * Summary:  Two nav changes.
 *
 *           1. Nav links underline in orange on hover, desktop row and
 *           mobile menu alike. The colour is --orange-lt, not --orange:
 *           against navy, #FF744D measures 4.71:1 — clear of the 4.5:1
 *           bar WCAG AA sets for small text — where #FF6B35 sits at
 *           4.26:1 and would fail at the 14.5px the links render at.
 *           text-underline-offset:4px keeps the rule clear of the
 *           descenders in "Contact Us". .mobile-phone is excluded from
 *           the mobile rule and moved below it: its colour is already
 *           !important, and leaving it above would have the two rules
 *           fighting on touch-hold rather than one plainly winning.
 *
 *           2. #faq added to the scroll-margin-top list. The nav gained
 *           an FAQ link and faq.php gained the matching id; without the
 *           offset the heading would land under the fixed 70px nav.
 *           #how, #why and #area STAY in that list even though the nav
 *           no longer links to them — the footer still calls go('how')
 *           and go('why'), and both sections remain on the page.
 *
 *           Nav width, revisited: dropping "How it works" and "Service
 *           area" frees roughly 200px in the desktop row, well past the
 *           ~78px "Home" costs. The 1140/1100 ladder is untouched and
 *           has slack again.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-09  01:26 AM CDT
 * Summary:  The nav's phone stack is now right-justified
 *           (.phone-stack align-items:flex-end) and the number grows
 *           from 14.5px to 19px, matching the Pro Mount reference. The
 *           glyph goes to 17px in nav.php to scale with it.
 *
 *           Growing the number costs no extra width: the caption is
 *           ~206px and the 19px number ~184px, so the caption still
 *           drives the column. The number fills space the column had
 *           already claimed — which is precisely what right-justifying
 *           makes visible.
 *
 *           One knock-on, fixed: at the 1140px shrink step the caption
 *           drops to ~177px, and a 19px number (~184px) would overtake
 *           it and become the column driver, so shrinking the caption
 *           alone would buy nothing. The step now shrinks the number to
 *           17px as well. Verified: positive slack at every width from
 *           1400px down to the 1100px burger breakpoint.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-09  12:58 AM CDT
 * Summary:  The desktop nav phone is now a two-line stack — a small
 *           uppercase caption (.cta-cap) above the number, inside a
 *           .phone-stack column, with "Book now" beside the pair. Number
 *           size unchanged at 14.5px. Caption is #9aa1ad, sampled from
 *           the Pro Mount reference; 6.07:1 on navy, clear of the 4.5:1
 *           small-text bar.
 *
 *           The breakpoint work is the load-bearing part. The caption is
 *           WIDER than the number it sits above (~206px vs ~144px), so
 *           it sets the CTA column's width and the 70px row overflowed
 *           below roughly 1140px — well before the old 960px burger
 *           breakpoint. Fixed with a ladder: at 1140px the caption's
 *           size and tracking shrink and .links tightens its gap,
 *           buying back ~65px; at 1100px the nav collapses to the
 *           burger. The four NAV rules moved out of the 960px block
 *           into their own 1100px block — the 960px block still governs
 *           page layout (hero, badges, grids, footer), which must not
 *           reflow 140px early.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-09  12:24 AM CDT
 * Summary:  Pro Mount USA link wired to the live URL, styled as a
 *           .wm-inline wordmark (PROMountUSA). Removed .svc-tv a's
 *           blanket orange and its underline.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  11:52 PM CDT
 * Summary:  Mobile menu button swaps hamburger -> X while open.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  11:18 PM CDT
 * Summary:  color:inherit on the button reset — buttons were taking the
 *           UA default (near-black). New .nav-call tap-to-call.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  10:41 PM CDT
 * Summary:  Hero photo onto .hero-band spanning hero + badges, four-stop
 *           scrim. Nav permanently opaque.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  10:04 PM CDT
 * Summary:  Added the hero background photo, flat 65% scrim. Superseded.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  09:12 PM CDT
 * Summary:  Repainted in the Pro Mount USA palette. Flat navy-900,
 *           orange -> #FF6B35, --ink white with --ink-dark in cards.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  08:26 PM CDT
 * Summary:  picker.php merged into services.php.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  07:58 PM CDT
 * Summary:  scroll-margin-top:86px on the in-page scroll targets.
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  07:34 PM CDT
 * Summary:  Tightened the heading-to-grid gap. (Superseded 08:26 PM.)
 * ------------------------------------------------------------ */

/* ---- Earlier session header --------------------------------
 * Updated:  2026-07-08  07:05 PM CDT
 * Summary:  Added .nowrap-desktop; removed orphaned picker-card CSS.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-09  09:47 PM CDT
 * Summary:  THREE changes, all for the new cabinets section.
 *
 * ---- (1) .btn-outline -----------------------------------------------
 *
 *           The first button variant designed to sit on a WHITE surface.
 *           btn-ghost is white-on-translucent and disappears there.
 *
 *           Orange on white is 2.84:1, under even the 3:1 non-text bar,
 *           so orange is used ONLY for the border. The label is
 *           --ink-dark (16.25:1 on white) AND STAYS --ink-dark ON HOVER.
 *           Contrast is symmetric: white-on-orange is the same 2.84:1
 *           and would fail. --ink-dark on orange is 5.73:1.
 *           The border is carried at rest, so hover changes colour only
 *           and nothing shifts.
 *
 * ---- (2) #cabinets joins the scroll-margin-top list ------------------
 *
 *           go('cabinets') would otherwise land under the fixed nav.
 *
 * ---- (3) The cabinets section ---------------------------------------
 *
 *           Two columns, even split: at gap:48 each is 542px of .wrap's
 *           1132px content box.
 *
 *           THE PHOTO IS CROPPED, ON PURPOSE. It is 1344x784 (ratio
 *           1.714); the card beside it measures 353.6px tall, so the
 *           photo's natural height at 542px is 316.2px -- 37.4px short.
 *           Widening the photo column does NOT fix this. It narrows the
 *           card, whose text rewraps taller, and the deficit never
 *           closes: the minimum is +36.0px across every split from 460
 *           to 580px. object-fit:cover absorbs it, trimming 71px from
 *           each side -- 10.6% of the width, all of it blank wall.
 *           Checked against the real file, not estimated.
 *
 *           Card height is a function of the copy in content.php. Edit a
 *           point and the crop adjusts silently. That is what cover is
 *           for here; it is not idle.
 *
 *           At <=960px the grid collapses and the photo is released to
 *           height:auto -- stacked, it has no card to match. See the
 *           source-order TRAP noted at that rule.
 *
 *           .cab-card takes NO hover. It is a static panel, not a grid
 *           item and not clickable, unlike .svc/.why/.rev/.step.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-10  12:18 AM CDT
 * Summary:  THREE changes, all inside .cab-cta, all consequences of the
 *           cabinets section dropping from two buttons to one.
 *
 * ---- (1) The single button stretches full-width ----------------------
 *
 *           .cab-cta is still flex, but holds one child. Switched to
 *           flex-direction:column, and the default align-items:stretch
 *           then makes that child full-width for free -- no width:100%
 *           needed, and it makes the <=767px override redundant.
 *           gap and flex-wrap go with the second button.
 *
 * ---- (2) The label wraps ONLY ON NARROW PHONES ----------------------
 *
 *           "Get your cabinet installation quote" measures 251.6px in
 *           the REAL Inter Bold at 15px. MEASURED, not estimated -- an
 *           earlier guess that it wrapped on desktop was wrong by 186px.
 *
 *           Button text width by viewport (full-width button, minus
 *           .btn's 20px padding each side):
 *
 *             1440px   438.0px    one line
 *             1100px   398.0px    one line
 *              961px   328.5px    one line   <- narrowest 2-col width
 *              960px   808.0px    one line   <- grid collapses, card
 *                                               takes the full .wrap
 *              420px   292.0px    one line
 *              390px   262.0px    one line
 *              380px   252.0px    one line   <- LAST width that fits
 *              375px   247.0px    TWO LINES  <- iPhone SE / mini / 12 mini
 *              360px   232.0px    TWO LINES
 *              320px   192.0px    TWO LINES
 *
 *           So it is a one-liner everywhere except phones at or below
 *           375px. Note the jump at 960px: the grid collapses to one
 *           column, the card gets the whole .wrap, and the button gets
 *           WIDER, not narrower. The tight band is 961-1180px, not the
 *           small end -- until 375px, where .cab-card's 20px padding is
 *           already helping and the label still runs out of room.
 *
 *           Wrapping there was chosen over shrinking the type. At 375px
 *           it breaks "Get your cabinet installation" / "quote"; at
 *           320px "Get your cabinet" / "installation quote". Neither
 *           line overflows.
 *
 *           Three properties make it work, each load-bearing:
 *
 *             white-space:normal   .btn hard-sets `nowrap`. WITHOUT THIS
 *                                  the label overflows the card at 375px
 *                                  instead of wrapping. This is the only
 *                                  .btn on the site that is allowed to
 *                                  wrap.
 *
 *             text-align:center    .btn's justify-content:center centres
 *                                  the flex LINE BOX. It does NOT align a
 *                                  second line of text inside it. Without
 *                                  this, line 2 rags left.
 *
 *             line-height:1.35     .btn sets none, so two lines would
 *                                  collide. Matches .cp-head h3.
 *
 *           On desktop all three are inert -- one line, already centred.
 *           They cost nothing and are not scoped to a media query,
 *           because the wrap point depends on the LABEL, and the label
 *           lives in content.php. Edit it and the breakpoint moves.
 *
 * ---- (3) The <a>/<button> border reservation is REMOVED --------------
 *
 *           `.cab-cta .btn:not(.btn-outline){border:1.5px solid transparent}`
 *           existed for ONE reason: .btn-orange is an <a> with no border,
 *           .btn-outline was a <button> with a 1.5px one, and the two
 *           disagreed by 2px whenever the row stacked into a column.
 *
 *           .btn-outline is gone from the markup. One element cannot
 *           mismatch itself, and a transparent border on a lone button is
 *           3px of dead space. Removed. The <=767px override
 *           (.cab-cta{flex-direction:column;gap:10px} and
 *           .cab-cta .btn{width:100%}) is collapsed for the same reason:
 *           both are now unconditional. Only .cab-card{padding:20px}
 *           survives there.
 *
 *           >> THE UNDERLYING TRAP IS NOT FIXED. box-sizing:border-box
 *              does NOT absorb a border when height:auto. THE HERO still
 *              pairs a bordered <button> with a borderless <a> and will
 *              show the same 2px the moment that row wraps. Do not read
 *              this deletion as the fix. See the backlog.
 *
 * ---- .btn-outline IS NOW UNUSED, AND IS KEPT ANYWAY ------------------
 *
 *           It was written for the button just deleted and is referenced
 *           NOWHERE in the markup. It stays because it is the only button
 *           variant designed for a WHITE surface -- .btn-ghost is
 *           white-on-translucent and vanishes there -- and because its
 *           contrast findings are expensive to rediscover:
 *
 *             orange on white       2.84:1  -> border only, never a label
 *             white on orange       2.84:1  -> contrast is SYMMETRIC
 *             --ink-dark on orange  5.73:1  -> hover label stays DARK
 *
 *           If it is ever deleted, delete this note with it. If it is
 *           ever reused, DO NOT "fix" the hover label to white.
 *
 * ---- card height, re-measured ---------------------------------------
 *
 *           Old .cab-cta: two buttons side by side in a flex ROW, 43px.
 *           New .cab-cta: one button, one line, round(15*1.35)+22 = 42px.
 *
 *           The card is ~1px SHORTER. Not taller. The photo crop
 *           (object-fit:cover, ~71px trimmed per side) absorbs 1px
 *           without comment -- see the unchanged note below.
 *
 * ---- unchanged, and still true --------------------------------------
 *
 *           THE CABINETS PHOTO IS CROPPED, ON PURPOSE. 1344x784 (ratio
 *           1.714) in a 542px column. The card beside it is taller than
 *           the photo's natural height at every split from 460 to 580px
 *           (minimum +36.0px), so widening the photo column never closes
 *           the gap -- it narrows the card, whose text rewraps taller.
 *           object-fit:cover absorbs it, trimming ~71px per side, all of
 *           it blank wall.
 *
 *           .cab-card takes NO hover. It is a static panel, not a grid
 *           item and not clickable, unlike .svc/.why/.rev/.step.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-10  01:02 AM CDT
 * Summary:  Added .svc-ask and .ask-phone, and pushed .svc-tv lower.
 *
 * ---- .svc-ask -- the new "just ask" line ----------------------------
 *
 *           Sits between the service-card grid and the Pro Mount USA
 *           footnote. It is a CALL TO ACTION; .svc-tv is a FOOTNOTE.
 *           They are deliberately not the same weight:
 *
 *             .svc-ask   --ink (#fff)   15.78:1 on navy
 *             .svc-tv    #b9c2cf         8.78:1 on navy
 *
 *           Both pass 4.5:1 at 15px. Measured, not recalled.
 *
 * ---- .svc-tv MOVED LOWER --------------------------------------------
 *
 *           Was margin-top:34px off the grid. The 34px now belongs to
 *           .svc-ask, and .svc-tv takes 26px off .svc-ask.
 *
 *           SPACED IN TWO STEPS, NOT ONE. Piling both gaps onto a single
 *           margin would drop the footnote without separating it from
 *           the ask line -- the two would read as one paragraph set
 *           adrift. 34px binds the ask line to the grid it belongs to;
 *           26px then holds the footnote apart from it. Net drop of the
 *           Pro Mount line is 26px plus the ask line's own height.
 *
 * ---- .ask-phone -- the tel: link ------------------------------------
 *
 *           --orange, 5.57:1 on navy. That PASSES 4.5:1 for 15px normal
 *           text. The context doc's warning that orange "fails" is about
 *           SMALL text (4.26:1 at the nav's 13px), which is why the nav
 *           hover uses --orange-lt. Here the resting colour can be plain
 *           --orange; the hover still goes --orange-lt (5.91:1) to match
 *           every other link on the site.
 *
 *           THREE PROPERTIES, EACH LOAD-BEARING:
 *
 *             display:inline-flex  icon() emits a raw <svg>, and the
 *                                  global `svg{display:block}` would drop
 *                                  it onto its own line. Same fix as
 *                                  .foot-phone and .phone.
 *
 *             align-items:center   vertically centres the 16px glyph
 *                                  against the 15px text.
 *
 *             white-space:nowrap   THE WHOLE LINE WRAPS BELOW 606px, and
 *                                  without this the icon can be orphaned
 *                                  from the number across the break. The
 *                                  link must stay atomic. Measured: the
 *                                  full line is 557.1px in real Inter
 *                                  (419px question+call, 22px icon+gap,
 *                                  116px bold number) against 1132px of
 *                                  .wrap at 1440px -- one line down to
 *                                  606px, wrapping on every phone.
 *
 *           The number is BOLD (600) so it reads as the actionable part
 *           of the sentence. The rest of the line is body weight.
 *
 * ---- both paragraphs are OUTSIDE the mobile scroller -----------------
 *
 *           At <=767px .svc-grid becomes a horizontal snap scroller with
 *           -24px side margins and its own scroll-padding. .svc-ask and
 *           .svc-tv are SIBLINGS of that grid, not children, so they
 *           inherit none of it and stay within .wrap's 24px padding.
 *           Nothing to override, and no rule was added for them at
 *           <=767px. Confirmed by resolving the cascade, not by eye.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-10  01:41 AM CDT
 * Summary:  TWO fixes to the .svc-ask line added earlier tonight.
 *
 * ---- (1) Colour: .svc-ask now matches .svc-tv ------------------------
 *
 *           var(--ink) (#fff, 15.78:1) -> #b9c2cf (8.78:1).
 *
 *           The two lines now read as one register. The PHONE LINK STAYS
 *           --orange and ITS ICON STAYS ORANGE -- the actionable part of
 *           the sentence keeps its emphasis. Only the sentence text
 *           changed. Still passes 4.5:1 at 15px by a wide margin.
 *
 *           This REVERSES the hierarchy argument in the previous header,
 *           quoted at the bottom of this file, which made .svc-ask white
 *           because it is a call to action and .svc-tv a footnote. The
 *           orange link carries that job on its own; the white sentence
 *           was shouting.
 *
 * ---- (2) The phone link sat 3.62px too low. Fixed. -------------------
 *
 *           .ask-phone was display:inline-flex. That was WRONG for an
 *           element sitting INLINE, MID-SENTENCE, and the error was
 *           exactly one descent:
 *
 *             An inline-flex box gives the line its FIRST FLEX ITEM's
 *             baseline. The first item here is the <svg>, which has no
 *             baseline. The box therefore synthesizes one from its
 *             BOTTOM MARGIN EDGE. The bottom edge lands on the parent's
 *             baseline, so the NUMBER's baseline ends up one full
 *             descent below it.
 *
 *             Inter at 15px: ascent 14.54px, descent 3.62px, content-box
 *             18.16px. Predicted drop 18.16 - 14.54 = 3.62px. That is
 *             the gap in the screenshot.
 *
 *           THE FIX IS TO STOP BEING A FLEX BOX, not to nudge it.
 *           .ask-phone is now ordinary inline text, so its baseline IS
 *           the number's baseline, which IS the sentence's baseline.
 *           Exact by construction, at any font size, forever.
 *
 *           vertical-align:middle on the link was considered and
 *           REJECTED: it leaves 1.56px of residual error and makes the
 *           result depend on the font's x-height. Do not "simplify" back
 *           to it.
 *
 *           The flex machinery is replaced piece by piece:
 *             gap:6px            -> margin-right:6px on the svg
 *             align-items:center -> vertical-align:-2.5px on the svg
 *             white-space:nowrap -> kept; it never needed flex
 *
 *           AND ONE THING FLEX WAS SILENTLY DOING FOR US. Whitespace
 *           between flex items is not rendered, so the literal space in
 *           services.php's markup --
 *               <?= icon('phone',16) ?> <?= $biz['phone'] ?>
 *                                       ^ here
 *           -- was being DISCARDED. As inline text it renders, adding
 *           4.22px on top of margin-right:6px for a 10.22px gap. THE
 *           SPACE HAS BEEN DELETED FROM services.php. The gap now lives
 *           entirely in this file.
 *             >> footer.php still has that space, and still needs it
 *                gone if .foot-phone ever stops being flex.
 *
 *           WHY -2.5px, AND WHY NOT -4px. Read from the font binary, not
 *           guessed. Inter, 2048 upem: x-height 1118 units (8.19px at
 *           15px), cap-height 1490 units (10.91px).
 *
 *             centre on x-height  -> 8.19/2 - 16/2 = -3.91px
 *             centre on cap-height-> 10.91/2 - 16/2 = -2.54px
 *
 *           The glyphs beside the icon are DIGITS -- (469) 647-9148 --
 *           and Inter's figures are LINING: they span baseline to
 *           cap-height, not x-height. Centring on x-height would sink the
 *           icon against the very numbers it sits beside. Hence -2.5px.
 *
 *           The phone glyph itself is centred in its 24x24 viewBox (off
 *           by 0.04 units) and fills 91.4% of it, so the 16px <svg> box
 *           is a fair proxy for the ink. No further correction.
 *
 * ---- .foot-phone and .phone ARE NOT A PRECEDENT FOR THIS -------------
 *
 *           The previous header claimed .ask-phone used "the same fix as
 *           .foot-phone and .phone". IT DOES NOT, and that note was
 *           wrong. Both of those are inline-flex, but both are FLEX ITEMS
 *           inside flex containers (.phone inside .phone-stack). A flex
 *           item's baseline is never tested against surrounding text,
 *           because there is no surrounding text. The bug cannot surface
 *           there.
 *
 *           .ask-phone is the FIRST caller of icon() placed inline in a
 *           running sentence. If a second one is ever added, copy THIS
 *           rule, not .foot-phone. And do not "restore" inline-flex here
 *           for consistency with the other two -- consistency is what
 *           caused this.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-10  02:07 AM CDT
 * Summary:  The cabinets headline no longer wraps on desktop.
 *
 * ---- The cause was .head, not .wrap ---------------------------------
 *
 *           .head{max-width:640px} caps EVERY section heading. The
 *           cabinets headline measures 719.5px in the real Poppins
 *           ExtraBold at 38px with letter-spacing:-.4px -- 79.5px over
 *           that cap, so it broke to two lines. Inside .wrap (1132px of
 *           content) it had 412.5px to spare. The room was there; the
 *           box would not let it have it.
 *
 *           MEASURED IN THE REAL 800 WEIGHT, fetched from
 *           github.com/google/fonts. The local Poppins ships only up to
 *           Bold(700), which underestimates this string by 9.2px --
 *           exactly the wrong direction for a does-it-fit question. Do
 *           not measure this with the installed font.
 *
 *           letter-spacing was included: CSS adds it after EVERY
 *           character, the last one included.
 *
 * ---- The fix: widen ONLY this section's .head -----------------------
 *
 *             .cabinets .head{max-width:760px}
 *
 *           Scoped, at (0,0,2,0) against the global .head's (0,0,1,0),
 *           so it wins regardless of source order and moves nothing else.
 *
 *           760px = 719.5px + 40.5px of slack. The slack is deliberate:
 *           PIL and a browser will not agree to the pixel on hinting and
 *           rasterization, and a heading that fits by 0.5px in a
 *           measurement is a heading that clips in the wild.
 *
 *           >> THE 760px IS DERIVED FROM ONE SPECIFIC STRING, and that
 *              string -- "Cabinets installed right, the first time" --
 *              IS STILL PLACEHOLDER COPY pending sign-off (the mockup
 *              read "<tag line here>"). CHANGE THE HEADLINE AND
 *              RE-DERIVE THIS NUMBER. There is no rule that keeps them
 *              in sync.
 *
 *           .cabinets has NO .sub, so widening its .head cannot stretch
 *           any paragraph's measure. The box holds a .kick and an h2 and
 *           nothing else. This is why the section can be widened safely
 *           and `why` / `services` could not be.
 *
 * ---- Why .nowrap-desktop was NOT used -------------------------------
 *
 *           services solves the same problem that way, and it works
 *           there by a hair: its headline is 657.8px, so nowrap pushes
 *           it 17.8px past .head -- 8.9px each side, centred, invisible.
 *
 *           Cabinets would overflow by 79.5px, 39.7px each side. Still
 *           invisible, but it is OVERFLOW, not fit: the box would be
 *           lying about its contents, and nothing would stop a longer
 *           headline from running into the section edges. Widening the
 *           box is the honest version of the same result.
 *
 * ---- The revert goes in the EXISTING 960px block --------------------
 *
 *           `.head h2{font-size:30px}` ALSO lives at <=960px -- not at
 *           767px, where the rest of the phone layout happens. So 960 is
 *           the width at which this heading changes size, the width at
 *           which .cab-grid collapses to one column, and now the width
 *           at which the cap returns to 640px. The section restacks as a
 *           unit. No new breakpoint.
 *
 *           Below 960px the h2 is 30px and the headline measures 564.6px
 *           -- comfortably inside the 640px cap. Net effect: THE HEADLINE
 *           IS ONE LINE FROM 1440px DOWN TO 767px, and only wraps below
 *           ~610px where .wrap itself becomes the binding constraint.
 *
 *           Widths where it wraps, h2 at 30px, cap 640px:
 *             767px -> one line (wrap 719px)
 *             600px -> two lines
 *             375px -> two lines, balanced
 *             320px -> three lines
 *
 *           Note the size is NOT monotonic in the viewport: two lines
 *           would appear between 768 and 960 IF the h2 were still 38px
 *           there. It is not. Had the 30px drop been at 767px instead,
 *           768px would have left 0.5px of slack against .wrap -- one
 *           hinting difference from a clipped heading. It shares the
 *           960px breakpoint, so that hazard does not arise. IF THE SIZE
 *           DROP IS EVER MOVED TO 767px, MOVE THIS REVERT WITH IT.
 * ------------------------------------------------------------ */

/* ---- Prior session header ----------------------------------
 * File:     assets/css/styles.css
 * Location: public_html/assets/css/
 * Updated:  2026-07-10  02:44 AM CDT
 * Summary:  Form error states, for the now-mandatory quote-form fields.
 *
 * ---- .is-err and .field-err are RED, not orange ---------------------
 *
 *           The old validator painted a failed field orange. TWO
 *           PROBLEMS, both measured:
 *
 *             1. --orange IS ALREADY THE :focus COLOUR. An invalid field
 *                and a focused field were indistinguishable.
 *             2. --orange on the input's #fdfcfa background is 2.77:1 --
 *                under even the 3:1 bar for non-text. As error TEXT on
 *                white it is 2.84:1, against a 4.5:1 requirement.
 *
 *           #c0392b measures 5.30:1 on the input and 5.44:1 on white. It
 *           is not a new colour: app.js already used it for the
 *           network-failure message. Now it is a token in one place.
 *
 * ---- .is-err MUST OUTRANK :focus ------------------------------------
 *
 *           A field can be focused AND invalid at the same time -- it is
 *           the normal case, because app.js focuses the first bad field.
 *           `.form input:focus` is (0,0,2,1); `.form input.is-err` is
 *           (0,0,2,1) too -- AN EXACT TIE. It wins on SOURCE ORDER
 *           ALONE, so the .is-err block sits BELOW the :focus block.
 *           MOVE IT ABOVE AND THE RED SILENTLY TURNS ORANGE ON FOCUS.
 *           Same shape as the .area h2 and .cab-photo img traps.
 *
 * ---- .field, a new wrapper ------------------------------------------
 *
 *           quote-form.php now wraps each control in
 *           <div class="field">, so the error <p> has somewhere to live
 *           that is not inside the input.
 *
 *           .frow IS A TWO-COLUMN GRID and its children used to BE the
 *           inputs. They are now the wrappers. The grid is unchanged --
 *           it still sees two children -- but two things had to move:
 *
 *             .form textarea{margin-bottom:16px} moved to the wrapper,
 *               or the gap would sit between the textarea and its own
 *               error message rather than below the pair. The wrapper
 *               carries an explicit `.field-last` class -- NOT
 *               `:has(textarea)`, which is unsupported in Firefox below
 *               121 and would fail by silently dropping the margin.
 *
 *             .frow{margin-bottom:12px} now has to clear the tallest
 *               cell including its error line. It already did; the error
 *               <p> is empty and zero-height until JS writes to it.
 *
 *           THE ERROR <p> IS ALWAYS PRESENT AND USUALLY EMPTY. It is not
 *           inserted on demand: an element that appears would reflow the
 *           form and push the submit button under the cursor mid-click.
 *           Empty, it has no margin and no line box, so it costs nothing.
 *
 * ---- role="alert" is on the <p>, not aria-live ----------------------
 *
 *           role="alert" carries an implicit aria-live="assertive". Do
 *           not add aria-live as well; some screen readers announce the
 *           message twice.
 * ------------------------------------------------------------ */