/* site.css -- hand-written, no framework, no preprocessor.
 *
 * Mobile-first. The site must be fully usable at 360px wide, INCLUDING WITH
 * JAVASCRIPT OFF. The mobile call bar shows below the breakpoint.
 *
 * THE COLLAPSED NAV IS OPT-IN, NOT THE DEFAULT. Every rule that hides the nav
 * or shows the Menu toggle is gated behind the `js` class that the inline
 * script in _head.html.j2 stamps on <html>. Without JavaScript the nav stays
 * expanded and the toggle -- which nothing could then operate -- stays hidden.
 * The base stylesheet used to hide the nav unconditionally and reveal it only
 * via a class site.js sets, which made six nav links, the header phone button
 * and the header contact CTA unreachable with JS off (Reviewer 2026-08-26).
 *
 * Those gates are written `:where(.js) ...` rather than `.js ...` on purpose.
 * `:where()` contributes nothing to specificity, so the gated rules stay at
 * the same weight as their un-gated counterparts and the `min-width: 48rem`
 * block at the bottom of this file still wins by source order. Writing them as
 * `.js .site-nav` would outrank the desktop rules and leak the mobile layout
 * into desktop. It also keeps the breakpoint in exactly one place: no
 * complementary `max-width` query to drift out of sync.
 *
 * NO WEBFONT HOST. No @import and no @font-face src pointing anywhere off this
 * origin -- not Google's font hosts, not a CDN, not the old Zyro asset host.
 * Nothing is fetched at render time (docs/spec/website-migration.md sec. 4).
 * The hostnames themselves are deliberately not written out here: this file is
 * copied verbatim into dist/, and the sec. 12 check greps the built tree for
 * them.
 * The stack below names Montserrat and Poppins first, matching the live site's
 * typography where a visitor happens to have them, and falls back to a system
 * sans everywhere else. The page must look correct on the fallback.
 * Self-hosting the real woff2 files is open item OI-9.
 *
 * Buttons are real elements, not the live site's text-baked PNGs (User
 * decision 2026-08-26). The pixel-art border treatment below is CSS
 * reproducing that art: a hard 3px dark outline, a bright inner highlight, a
 * flat offset shadow, square-ish corners, no gradients.
 */

:root {
  /* Sampled from the actual brand art. --navy matches qrgen's brand navy. */
  --navy: #1E3A5F;
  --sky: #4A90D9;
  --blue-mid: #346699;
  --orange: #E8712B;
  --orange-bright: #FF823A;
  --orange-dark: #D1520E;
  --ink: #2D2D2D;
  --white: #FFFFFF;

  --pale: #DCEBFA;
  --paper: #F5F8FB;
  --shadow: rgba(45, 45, 45, 0.9);

  --font-head: Montserrat, "Segoe UI", system-ui, -apple-system, sans-serif;
  --font-body: Poppins, "Segoe UI", system-ui, -apple-system, sans-serif;

  --wrap: 68rem;
  /* THE HEADER'S OWN CEILING, WIDER THAN THE BODY'S ON PURPOSE. `.wrap` caps
     body prose at `--wrap` for readability and that is unchanged; the header
     bar is the one `.wrap` that is not prose, and capping it at 68rem was
     what crushed the logo (see the header-width block at the end of this
     file for the measurements). Only `.site-header__bar` may read this. */
  --header-wrap: 88rem;
  --gutter: 1.25rem;
  --radius: 6px;
}

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

/* The `hidden` attribute must actually hide. The UA rule that implements it is
   `display: none` at UA origin, so ANY author rule that sets `display` on a
   matching element silently outranks it -- add `display: flow-root` to
   `.section` one day and the inline contact form site.js hides comes back.
   This is the one place `!important` is right: it does not compete with a
   layout rule, it defends an HTML semantic. Nothing in the built markup ships
   `hidden`; it is set at runtime, by script, and only ever to hide the DUPLICATE
   of something that is already reachable elsewhere. */
[hidden] {
  display: none !important;
}

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

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--ink);
  background: var(--white);
  /* Room for the fixed mobile call bar so it never covers the last line. */
  padding-bottom: 4.5rem;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-head);
  line-height: 1.15;
  margin: 0 0 0.6em;
}

p {
  margin: 0 0 1em;
}

img {
  max-width: 100%;
  height: auto;
}

a {
  color: var(--blue-mid);
}

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

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--navy);
  color: var(--white);
  padding: 0.6rem 1rem;
  z-index: 100;
}

.skip-link:focus {
  left: 0;
}

:focus-visible {
  outline: 3px solid var(--orange);
  outline-offset: 2px;
}

/* ------------------------------------------------------------- buttons -- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.2;
  text-decoration: none;
  text-align: center;
  padding: 0.7rem 1.25rem;
  border: 3px solid var(--ink);
  border-radius: var(--radius);
  box-shadow: 4px 4px 0 var(--shadow);
  cursor: pointer;
  transition: transform 0.06s linear, box-shadow 0.06s linear;
}

.btn:active {
  transform: translate(3px, 3px);
  box-shadow: 1px 1px 0 var(--shadow);
}

.btn--primary {
  background: var(--orange);
  color: var(--white);
  box-shadow: 0 0 0 3px var(--sky), 4px 4px 0 var(--shadow);
}

.btn--primary:hover {
  background: var(--orange-bright);
}

.btn--ghost {
  background: var(--pale);
  color: var(--navy);
  box-shadow: 0 0 0 3px var(--sky), 4px 4px 0 var(--shadow);
}

.btn--ghost:hover {
  background: var(--white);
}

.btn--phone {
  background: var(--white);
  color: var(--navy);
}

.btn--lg {
  font-size: 1.125rem;
  padding: 0.9rem 1.6rem;
}

.btn--submit {
  width: 100%;
}

.btn__icon {
  width: 1.5rem;
  height: 1.5rem;
  flex: none;
  image-rendering: pixelated;
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin: 1.25rem 0;
}

.cta-row__or {
  font-family: var(--font-head);
  font-weight: 600;
  color: var(--blue-mid);
  margin: 0;
}

/* -------------------------------------------------------------- header -- */

.site-header {
  background: var(--navy);
  border-bottom: 4px solid var(--ink);
  z-index: 40;
}

/* Sticky is conditional at mobile for one reason: with JS off the nav is
   expanded, and an expanded nav in a sticky header eats roughly half a 360px
   viewport on every scroll, permanently occluding the page. With JS the nav is
   collapsed and the header is one short bar, so sticky is free. Desktop is
   sticky either way -- see the min-width: 48rem block. */
:where(.js) .site-header {
  position: sticky;
  top: 0;
}

.site-header__bar {
  display: flex;
  /* Base = the no-JS state; see the per-state note below. */
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 0.6rem;
  padding-bottom: 0.6rem;
}

/* WRAPPING IS PER-STATE, NOT GLOBAL. .site-nav is `width: 100%; order: 3` at
   mobile, which only lands on its own row in a wrapping container -- in the
   default `nowrap` it is squeezed onto the logo's line instead. But wrap is
   only WANTED in the two states where something is actually in flow below the
   logo row. The other two states have nothing there, and wrap costs them a
   whole extra row: under `nowrap` the flex line shrinks the logo to fit
   (251px -> 197px at 1280); allowing wrap drops the logo or the toggle to a
   new line instead. Measured on 2026-08-26, this was worth 41% of the desktop
   header height (109px -> 154px at 1280) and took the sticky mobile header
   from 63px to 124px with the nav closed. So:

     no JS               -> wrap    (nav is expanded and in flow)
     JS, nav closed      -> nowrap  (nav is display:none; logo + toggle only)
     JS, nav open        -> wrap    (nav is back in flow)
     >= 48rem, any state -> nowrap  (nav is `width: auto`; one row always)

   The gates match `.site-nav`'s own gates so the two cannot drift apart. */
:where(.js) .site-header__bar {
  flex-wrap: nowrap;
}

:where(.js) .site-header.is-nav-open .site-header__bar {
  flex-wrap: wrap;
}

.site-header__logo img {
  display: block;
  width: auto;
  max-height: 3rem;
}

.nav-toggle {
  /* A button that does nothing must never be shown. */
  display: none;
  align-items: center;
  gap: 0.5rem;
  background: transparent;
  color: var(--white);
  border: 3px solid var(--sky);
  border-radius: var(--radius);
  font-family: var(--font-head);
  font-weight: 700;
  padding: 0.45rem 0.7rem;
  cursor: pointer;
}

:where(.js) .nav-toggle {
  display: inline-flex;
}

.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  display: block;
  width: 18px;
  height: 3px;
  background: var(--white);
  content: "";
}

.nav-toggle__bars {
  position: relative;
}

.nav-toggle__bars::before {
  position: absolute;
  top: -6px;
}

.nav-toggle__bars::after {
  position: absolute;
  top: 6px;
}

.site-nav {
  display: block;
  width: 100%;
  order: 3;
  padding-bottom: 0.75rem;
}

:where(.js) .site-nav {
  display: none;
}

:where(.js) .site-header.is-nav-open .site-nav {
  display: block;
}

.site-nav__list {
  list-style: none;
  margin: 0 0 0.75rem;
  padding: 0;
}

.site-nav__link {
  display: block;
  padding: 0.55rem 0;
  color: var(--white);
  font-family: var(--font-head);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}

.site-nav__link:hover,
.site-nav__link.is-current {
  color: var(--orange-bright);
}

.site-nav__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* Match the mobile call bar's button-sizing treatment (.call-bar__btn below):
   each button grows to share the row equally instead of shrinking to its own
   text width, which is what made the CTA look conspicuously narrower than the
   phone button. `flex: 1 1 0%` only has room to act while `.site-nav__actions`
   is itself a full-width block (mobile, and the no-JS/nav-open flow states);
   at >= 48rem `.site-nav__actions` is a shrink-to-fit flex item beside the nav
   links, so this is a no-op there and the desktop six-link-fit override two
   screens down (`.site-nav__actions .btn` under `@media (min-width: 48rem)`)
   still owns padding/font-size at that width. */
.site-nav__actions .btn {
  flex: 1 1 0%;
  padding: 0.6rem 0.5rem;
  font-size: 0.95rem;
}

/* ---------------------------------------------------------------- hero -- */

/* ONE HERO TREATMENT FOR ALL SIX PAGES (User, 2026-09-06: "The hero banners
 * should all have the same size and opacity, but each page might have a
 * different hero banner"). Size and opacity belong to the BANNER BOX, which is
 * shared; the image inside it is the per-page part. templates/_hero.html.j2 is
 * the only markup that may produce a hero.
 *
 * THIS BLOCK IS EDITED IN PLACE, which rounds 1-3 below deliberately were not.
 * Their append-only rule exists so that adding a component for a NEW page
 * leaves every already-built page byte-identical. This change is the opposite
 * kind of change: it is a cross-page unification whose whole purpose is to
 * alter all six heroes, and three superseded variants had to be deleted rather
 * than shadowed -- a dead `.hero__art { height: auto }` left in place would
 * silently fight the shared box.
 *
 * WHAT "80% OF CURRENT" WAS MEASURED AGAINST. There was no single "current":
 * the three old variants disagreed, and two of them had no CSS height at all.
 * Banner heights as they actually rendered (Chromium, built tree, 2026-09-06):
 *
 *   page                                variant        360px     1280px
 *   index, residential-arvada-hvac      .hero__art     162.0     576.0
 *   commercial-hvac-arvada              .hero__art      89.0     316.5
 *   about-us-hvac, indoor-air-...       .photo--hero   202.5     416.0
 *   duct-cleaning-arvada                (none)            --        --
 *
 * The two `.hero__art` numbers are not a design decision at all -- they are
 * the art's aspect ratio times the viewport width, so they keep growing with
 * the screen (864px at 1920) and cannot be 80%-ed into a shared value.
 * `.photo--hero` was the only variant with a declared box, so it is the
 * baseline: it rendered 202.5px at 360 (bound by its 16/9 ratio, not by its
 * 20rem cap, which never bound at mobile) and 416px from 48rem up (bound by
 * its 26rem cap). 80% of those is 162px and 332.8px, which is what the two
 * values below are -- 10rem is 160px, 2px under the mobile figure, and the
 * rem is worth more than the 2px.
 *
 * That lands as a REDUCTION on every page that had a banner except
 * commercial-hvac-arvada, whose art is a 4:1 letterbox and was anomalously
 * short. That one page GROWS, and the growth is very uneven -- both halves
 * are recorded below, because the desktop half quoted on its own badly
 * understates it.
 *
 * THE COMMERCIAL EXCEPTION, BOTH BREAKPOINTS. Same run as the table above
 * (tenths differ only in the scrollbar allowance):
 *
 *   commercial-hvac-arvada        before      after     change
 *   360px                           88.9      160.0       +80%
 *   1280px                         316.2      332.8        +5%
 *
 * +80% at 360px is a near-doubling, and 360 is the mobile-first breakpoint,
 * so it is the half that matters most -- quoting only "~5% at desktop" would
 * let a reader sign off believing the exception is negligible. It is not.
 *
 * The cause is the art, not the value chosen here. commercial-hero.png is
 * 3150x779 -- 4.04:1 -- where the other `.hero__art` pages carry
 * hero-banner.png at 10000x4500, 2.22:1. With no CSS height each rendered at
 * viewport width / ratio, so the 4:1 page was always the shortest banner on
 * the site at every width. Any single shared height low enough to reduce the
 * 2.2:1 pages is necessarily an increase for the 4:1 one; that is arithmetic,
 * not a mis-tuned number, and no value of `--hero-banner-h` escapes it.
 *
 * The User was shown BOTH figures -- +5% at 1280 and +80% at 360 -- and
 * accepted the trade on 2026-09-06: uniformity wins. So this is a recorded
 * decision, not an oversight. Do not "fix" the mobile growth by special-casing
 * commercial-hvac-arvada; that reintroduces exactly the per-page divergence
 * this block exists to remove.
 */
:root {
  /* 10rem = 160px. The desktop half of this pair is at the end of the file. */
  --hero-banner-h: 10rem;
}

.hero {
  position: relative;
  /* The banner is out of flow, so the hero has to be told not to be shorter
     than it -- otherwise a short hero would let the band bleed into the
     section below. */
  min-height: var(--hero-banner-h);
  background: var(--navy);
  color: var(--white);
  border-bottom: 4px solid var(--ink);
}

/* The banner band. Absolute so the text layer starts at the TOP of the hero
   and overlaps it, which is the "move the hero text up under the header"
   half of this change. No offset from the header is computed anywhere,
   because none could be: the header is 63px at 360 with the nav collapsed,
   118px at 768, 82px at 1280 and 486px at 360 with scripting off. The hero
   follows the header in document flow, so its top edge IS the header's bottom
   edge in every one of those states.

   VERTICALLY CENTRED IN THE HERO, NOT PINNED TO ITS TOP (User, 2026-09-06:
   "the hero banner ... should be center-justified instead of top"). The band
   is a fixed `--hero-banner-h` tall but `.hero` is as tall as its copy, which
   varies per page -- 581px at 1280 on the homepage against a 333px band, so
   the top-pinned band left a 249px gap of bare `--navy` below it. Centring is
   only visible in that difference, which is why it is a band-position change
   and NOT an `object-position` one: the artwork inside was already centred on
   both axes and is deliberately left alone.

   `top: 0; bottom: 0` + a definite `height` + auto block margins is the
   centring mechanism (CSS 2.1 sec. 10.6.4: an over-constrained absolutely
   positioned box with auto margins splits the leftover space equally). A
   `transform: translateY(-50%)` would also centre it, but it would make this
   element a containing block for any descendant that ever goes `fixed`, so
   the margin form is preferred. The band stays out of flow either way, so the
   hero copy still starts at the header -- `HERO_TITLE_GAP_MAX` is untouched
   by this and still measures 16px. */
.hero__media {
  position: absolute;
  inset: 0 0 0 0;
  height: var(--hero-banner-h);
  margin-block: auto;
  overflow: hidden;
  z-index: 0;
  /* Decoration behind live text: it must never eat a click meant for the h1
     or a CTA that overlaps it. */
  pointer-events: none;
}

.hero__media picture {
  display: block;
  height: 100%;
}

/* 20% (User, 2026-09-06) AND THAT NUMBER IS LOAD-BEARING FOR CONTRAST, not
   just for taste. The hero's text sits on top of this, and the backing is
   `--navy`. The lightest thing any banner can composite to is
   0.2*white + 0.8*navy = rgb(75, 97, 127), which is 6.3:1 against the white
   hero text and 5.2:1 against `--pale` (the lead and the "or"). Both clear
   WCAG AA's 4.5:1 with margin, and they do so for ANY image, present or
   future, because white is the ceiling. Raise this opacity or lighten
   `.hero`'s background and that guarantee is gone --
   `test_hero_text_contrast_survives_any_banner_image` recomputes it from the
   browser's own numbers and fails, which is the mechanism that stops a later
   image swap from quietly making the hero unreadable. */
.hero__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0.2;
}

/* Pixel art, and only pixel art, is scaled with hard edges. The photographs
   in the same box must NOT be -- that is why `.photo` has never set this. */
.hero__media .hero__art {
  image-rendering: pixelated;
}

.hero__inner {
  position: relative;
  z-index: 1;
  /* Was 1.75rem, under a banner that was in flow above it. The banner is now
     behind the text, so this padding is the entire gap between the header's
     bottom border and the first line of the hero. */
  padding-top: 1rem;
  padding-bottom: 2rem;
}

.hero__title {
  font-size: clamp(1.9rem, 7vw, 3.25rem);
  text-transform: uppercase;
  letter-spacing: 0.01em;
  text-shadow: 3px 3px 0 var(--ink);
}

.hero__title span {
  display: block;
}

.hero__lead {
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--pale);
}

.hero .cta-row__or {
  color: var(--pale);
}

.hero__offer {
  display: inline-block;
  /* Explicit here because this is an <a> on the homepage and residential
     page since 2026-09-08; the global `p { margin: 0 0 1em; }` rule no
     longer reaches it, and without this the gap to `.cta-row` collapses
     by 1em (16px). */
  margin: 0 0 1em;
  font-family: var(--font-head);
  font-weight: 700;
  background: var(--orange);
  color: var(--white);
  border: 3px solid var(--ink);
  border-radius: var(--radius);
  padding: 0.5rem 0.9rem;
  box-shadow: 4px 4px 0 var(--shadow);
  /* The homepage and residential page turn this into a real <a> (items
     1.3/2.3); the interactive affordances below mirror .btn's idiom so it
     behaves consistently as a link. Harmless as a no-op if the element is
     still a <p>. */
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.06s linear, box-shadow 0.06s linear;
}

.hero__offer:hover,
.hero__offer:focus-visible {
  transform: translate(-1px, -1px);
  box-shadow: 5px 5px 0 var(--shadow);
}

.hero__offer:focus-visible {
  outline: 3px solid var(--orange);
  outline-offset: 2px;
}

.hero__offer:active {
  transform: translate(3px, 3px);
  box-shadow: 1px 1px 0 var(--shadow);
}

/* ------------------------------------------------------------ sections -- */

.section {
  padding: 2.5rem 0;
  border-bottom: 2px solid var(--pale);
}

.section--bundle,
.section--promise {
  background: var(--paper);
}

.section--prevent,
.section--closing-cta {
  background: var(--navy);
  color: var(--white);
}

.section--prevent .section-title,
.section--closing-cta .section-title,
.section--prevent .lead,
.section--closing-cta .cta-row__or {
  color: var(--white);
}

.section--closing-cta .section-title a {
  color: var(--white);
  text-decoration: none;
}

.section--closing-cta .section-title a:hover {
  color: var(--orange-bright);
}

.section-title {
  font-size: clamp(1.5rem, 4.5vw, 2.25rem);
  color: var(--navy);
}

.section-title__kicker {
  display: block;
  /* Enlarged 0.55em -> 0.75em per User request 2026-09-08 (item 4.3, "'Now
     offering' needs to be bigger"). It is an `em` of `.section-title`, whose
     clamp floors at 1.5rem on a phone, so 0.55em rendered 13.2px at 360 --
     smaller than body copy and easy to miss above the heading it labels.
     0.75em is 18px there, i.e. the size of `.subhead` (1.15rem = 18.4px),
     which is the same typographic device: uppercase, letter-spaced,
     --blue-mid. Anchoring to that rather than inventing a number keeps the
     two kicker/subhead treatments reading as one system. Shared by all four
     kickers on the site -- three are the byte-identical "Now Offering:"
     bundle heading and must not diverge; the fourth is residential's "Level
     Up" under its bar art. All four measured at 360 and 1280: still one
     line, still clear of the art. */
  font-size: 0.75em;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--blue-mid);
}

.section-title__main {
  display: block;
}

.subhead {
  font-size: 1.15rem;
  color: var(--blue-mid);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.lead {
  font-size: 1.1rem;
}

.lead--with-icon {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.lead__icon,
.panel__heart {
  /* Enlarged per User mobile review 2026-09-07 (items 1.10/3.3) -- covers the
     commercial "We Protect Your Peak Hours." shield among others. */
  width: 2.75rem;
  height: 2.75rem;
  flex: none;
  image-rendering: pixelated;
}

.lead--peak {
  /* Commercial "We Protect Your Peak Hours." line -- bold + light blue per
     User mobile review 2026-09-07 (item 3.3). --sky measures ~3.3:1 against
     the white .section background here, failing WCAG AA; --blue-mid measures
     ~6.0:1 and passes, so --blue-mid is used (same reasoning as
     .icon-list--accent-sky below). */
  font-weight: 700;
  color: var(--blue-mid);
}

.note {
  font-size: 0.9rem;
  color: var(--blue-mid);
}

/* ----------------------------------------------------------- icon list -- */

.icon-list,
.price-list {
  list-style: none;
  margin: 0 0 1.25rem;
  padding: 0;
}

.icon-list li,
.price-list li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.35rem 0;
  font-weight: 500;
}

.icon-list__icon {
  /* Enlarged per User mobile review 2026-09-07 (items 1.4/2.5/2.8) -- serves
     the homepage bundle shields, the "Our Promise" checkmarks, the
     residential pain-point icons/second-opinion shields, and the commercial
     pain-point icons. */
  width: 2.25rem;
  height: 2.25rem;
  flex: none;
  image-rendering: pixelated;
}

.icon-list--accent-sky li {
  /* Residential pain-point list -- lighter blue per User mobile review
     2026-09-07 (item 2.4). --sky measures ~3.3:1 against the white .section
     background this list sits on (residential-arvada-hvac.html.j2), which
     fails WCAG AA (4.5:1) for body text; --blue-mid measures ~6.0:1 and
     passes, so --blue-mid is used here instead of --sky. */
  color: var(--blue-mid);
}

.icon-list--accent-orange li {
  /* Commercial pain-point list -- orange per User mobile review 2026-09-07
     (item 3.2). */
  color: var(--orange-dark);
}

.price-list li {
  flex-wrap: wrap;
}

.price--was {
  color: var(--blue-mid);
  opacity: 0.75;
}

.price--now {
  color: var(--orange-dark);
  font-family: var(--font-head);
  font-size: 1.2rem;
}

.price-list__combo,
.lead--combo {
  /* Homepage "10% off IAQ test + duct cleaning service combo" line -- orange
     per User mobile review 2026-09-07 (item 1.5). Same token as .price--now
     above so it reads as deliberate.

     `.lead--combo` is the duct page's copy of that same sentence, which the
     User asked to be bold and orange in its own right (request 2026-09-08,
     item 4.4). It shares this declaration block rather than repeating the two
     properties, so the two instances of one sentence cannot drift apart and
     no second orange gets typed. It needs its own class name because
     `.price-list__combo` is an element of `.price-list` and the duct
     occurrence is a `.lead`, not a list item. */
  color: var(--orange-dark);
  font-weight: 700;
}

/* -------------------------------------------------------------- panels -- */

.panel {
  background: var(--sky);
  color: var(--white);
  border: 3px solid var(--ink);
  border-radius: var(--radius);
  box-shadow: 0 0 0 3px var(--orange), 6px 6px 0 var(--shadow);
  padding: 1.25rem;
  margin: 1.5rem 0;
}

.panel p {
  margin: 0 0 0.5rem;
}

.panel p:last-child {
  margin-bottom: 0;
}

.panel__punch {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.15rem;
}

.panel--second-opinion {
  text-align: center;
  background: var(--pale);
  color: var(--navy);
}

.panel__line {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.4rem;
  text-transform: uppercase;
}

.panel__line--free {
  color: var(--orange-dark);
  font-size: 2.2rem;
}

.offer-badge {
  display: block;
  text-align: center;
  background: var(--pale);
  color: var(--orange-dark);
  border: 3px solid var(--ink);
  border-radius: var(--radius);
  box-shadow: 0 0 0 3px var(--sky), 6px 6px 0 var(--shadow);
  padding: 1.25rem;
  margin: 0;
  font-family: var(--font-head);
}

.offer-badge__lead {
  display: block;
  font-size: 2.4rem;
  line-height: 1;
}

.offer-badge__body {
  display: block;
  font-size: 1.4rem;
  font-weight: 700;
}

.offer-badge__terms {
  display: block;
  font-size: 1.05rem;
  font-weight: 600;
}

/* --------------------------------------------------------------- cards -- */

.card-row {
  display: grid;
  gap: 1.25rem;
}

.card-row + .lead {
  /* Breathing room between the commercial "Who we serve" card grid and its
     closing line per User mobile review 2026-09-07 (item 3.11). `.card-row`
     declares no margin and `p` only has a bottom margin, so without this the
     closing <p class="lead"> sat flush against the grid above it. */
  margin-top: 2rem;
}

.card {
  background: var(--white);
  border: 3px solid var(--ink);
  border-radius: var(--radius);
  box-shadow: 6px 6px 0 var(--shadow);
  padding: 1.5rem;
}

.card__title {
  font-size: 1.25rem;
  color: var(--navy);
}

.care-team__icons {
  display: flex;
  gap: 1rem;
  align-items: center;
}

.care-team__icons img {
  image-rendering: pixelated;
}

/* ---------------------------------------------------------------- form -- */

.contact-inline {
  scroll-margin-top: 6rem;
}

.contact-form {
  max-width: 34rem;
}

.field {
  margin: 0 0 1rem;
  border: 0;
  padding: 0;
}

.field label,
.field legend {
  display: block;
  font-family: var(--font-head);
  font-weight: 600;
  margin-bottom: 0.3rem;
}

/* `select` is IN THIS SELECTOR LIST, not in a rule of its own, because the
   requirement is that it look like the text inputs -- and one shared
   declaration block is the only way to say that without two copies of the
   border, padding and colour that would then drift apart. */
.field input[type="text"],
.field input[type="tel"],
.field input[type="email"],
.field select,
.field textarea {
  width: 100%;
  font: inherit;
  padding: 0.6rem 0.7rem;
  border: 3px solid var(--navy);
  border-radius: var(--radius);
  background: var(--white);
  color: var(--ink);
}

.field--group {
  border: 3px solid var(--pale);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
}

.choice {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.15rem 0;
}

.choice label {
  font-family: var(--font-body);
  font-weight: 400;
  margin: 0;
}

/* Honeypot. Off-screen rather than display:none so a naive bot still finds
   and fills it; never focusable, never announced. */
.contact-form__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Was `.contact-form__alt`, a sentence ending in a bare tel: link. It is now a
   real call button below the submit button (User, 2026-09-06). It stays an
   <a href="tel:"> and not a <button>: that is what dials with JavaScript off,
   and it is also what site.js's DELEGATED `a[href^="tel:"]` listener matches,
   so the call_click beacon tracks it with no new JavaScript at all. */
.contact-form__call {
  margin-top: 0.75rem;
}

/* -------------------------------------------------------------- dialog -- */

.contact-dialog {
  width: min(38rem, calc(100vw - 2rem));
  max-height: calc(100vh - 2rem);
  padding: 0;
  border: 4px solid var(--ink);
  border-radius: var(--radius);
  background: var(--white);
  color: var(--ink);
}

.contact-dialog::backdrop {
  background: rgba(30, 58, 95, 0.7);
}

.contact-dialog__inner {
  position: relative;
  padding: 1.5rem;
}

.contact-dialog__close {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  width: 2.25rem;
  height: 2.25rem;
  font-size: 1.4rem;
  line-height: 1;
  background: var(--navy);
  color: var(--white);
  border: 3px solid var(--ink);
  border-radius: var(--radius);
  cursor: pointer;
}

/* ------------------------------------------------------------ call bar -- */

.call-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem;
  background: var(--navy);
  border-top: 4px solid var(--ink);
}

.call-bar__btn {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 0.95rem;
  text-decoration: none;
  padding: 0.6rem 0.5rem;
  border: 3px solid var(--ink);
  border-radius: var(--radius);
}

.call-bar__btn--call {
  background: var(--orange);
  color: var(--white);
}

.call-bar__btn--quote {
  background: var(--pale);
  color: var(--navy);
}

/* -------------------------------------------------------------- footer -- */

.site-footer {
  background: var(--navy);
  color: var(--pale);
  padding: 2.5rem 0 1.5rem;
}

.site-footer a {
  color: var(--white);
}

.site-footer__grid {
  display: grid;
  gap: 1.75rem;
}

.site-footer__heading {
  font-family: var(--font-head);
  font-size: 1.1rem;
  color: var(--white);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.site-footer__phone {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 700;
  text-decoration: none;
}

.site-footer__badges {
  display: flex;
  /* Kept to one row even at 360px per User mobile review 2026-09-07 (item
     1.11) -- the three badges were wrapping 2+1. They shrink instead of
     wrapping; `min-width: 48rem` below restores wrapping once there's room. */
  flex-wrap: nowrap;
  gap: 1rem;
  align-items: center;
}

.site-footer__badges img {
  min-width: 0;
  width: auto;
  max-width: 100%;
  height: auto;
}

.site-footer__legal {
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid rgba(255, 255, 255, 0.2);
  font-size: 0.8rem;
  opacity: 0.85;
}

.site-footer__legal img {
  image-rendering: pixelated;
}

.bbb-badge {
  /* White card behind the BBB logo so it stops blending into the footer's
     dark background per User mobile review 2026-09-07 (item 1.12). Also used
     by .trust-strip on the residential/commercial pages (sibling Writer
     scope) -- deliberately not scoped under .site-footer__legal. */
  background: var(--white);
  border: 3px solid var(--white);
  border-radius: var(--radius);
  padding: 0.35rem;
  image-rendering: pixelated;
}

/* --------------------------------------------------------- breakpoints -- */

@media (min-width: 48rem) {
  body {
    /* The call bar is mobile-only, so the reserved space goes away with it. */
    padding-bottom: 0;
  }

  .site-header {
    /* Unconditional here: the desktop nav is one horizontal row, so it costs
       no meaningful vertical space whether or not JavaScript ran. */
    position: sticky;
    top: 0;
  }

  /* Pin the bar to one row at every desktop state. BOTH selectors are
     required: `:where(.js) .site-header.is-nav-open .site-header__bar` above is
     (0,2,0), so a bare `.site-header__bar` (0,1,0) here would lose to it and a
     nav left open across a resize would keep the wrapped mobile layout. The
     second selector below is also (0,2,0) and later in source order, so it
     wins. Deliberate: the desktop nav is `width: auto` and always fits beside
     the logo, so there is no state where wrapping is correct here. */
  .site-header__bar,
  .site-header.is-nav-open .site-header__bar {
    flex-wrap: nowrap;
  }

  .nav-toggle {
    display: none;
  }

  .site-nav {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 1.5rem;
    width: auto;
    order: 0;
    padding-bottom: 0;
  }

  .site-nav__list {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin: 0;
  }

  .site-nav__link {
    border-bottom: 0;
    padding: 0;
    font-size: 0.95rem;
  }

  .site-nav__actions {
    flex-wrap: nowrap;
  }

  .call-bar {
    display: none;
  }

  .card-row {
    grid-template-columns: 1fr 1fr;
  }

  .site-footer__grid {
    grid-template-columns: 2fr 1fr 1fr;
  }

  .site-footer__badges {
    /* Room to wrap again above the phone breakpoint (item 1.11). */
    flex-wrap: wrap;
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn {
    transition: none;
  }
}

/* ================================================================
 * Phase 1b components.
 *
 * APPENDED, NEVER EDITED IN PLACE. Everything above this banner is Phase 1a
 * and is left byte-identical: the header/nav rules and their `:where(.js)`
 * gates cost three review rounds to settle, and the `min-width: 48rem` block
 * above owns the header pin. The desktop overrides for the components below
 * therefore live in their OWN media query at the end of this file rather than
 * being spliced into that one.
 *
 * These classes serve /residential-arvada-hvac and /commercial-hvac-arvada.
 * The remaining Phase 1b pages reuse them; add to this section, do not fork.
 * ================================================================ */

/* --------------------------------------------------- service catalogue -- */

/* A named group of services: icon + title, an optional intro, then the items.
   `break-inside` is deliberately NOT set anywhere here -- a group grows with
   the data and must be free to fragment. */
.svc-group {
  margin: 0 0 2.25rem;
}

.svc-group:last-child {
  margin-bottom: 0;
}

.svc-group__title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 1.35rem;
  color: var(--navy);
}

.svc-group__icon {
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  image-rendering: pixelated;
}

.svc-group__intro {
  margin: 0 0 1.1rem;
}

/* A <dl> of term/description pairs, one card each. The live pages number the
   terms in their own copy ("1. ...", "2. ..."), so no counter is generated
   here -- generated numbers would fight the transcribed ones. */
.svc-list {
  display: grid;
  gap: 1rem;
  margin: 0;
  padding: 0;
}

.svc-list + .subhead,
.svc-list + .note {
  /* Breathing room between a card grid and the block of text that follows it,
     per User request 2026-09-08 (items 2.6, 3.4 and 3.5 -- one rule, because
     they are one defect). The gap was not merely tight, it was measured at
     exactly 0px in Chromium: `.svc-list` declares `margin: 0` above, and
     `h1..h4` and `p` carry bottom margins only, so nothing separated the last
     card's shadow from the following heading or note.

     KEYED ON THE FOLLOWING ELEMENT, NOT ON `.svc-list`, on the same reasoning
     as `.card-row + .lead`: a bare `margin-bottom` on `.svc-list` would hit
     all 15 instances sitewide, including the six that are followed by a
     mid-page `h3.svc-group__title` repeat that nobody asked to space out, and
     the duct page's `.svc-list` + `.cta-row` pair, which already has 20px.

     2rem matches `.card-row + .lead`, and the cards need at least that: they
     carry a 6px offset shadow below their border box, so 32px of box-to-box
     space reads as ~26px of white. Measured at 360 and 1280 on all three
     pairs. It also lands on the IAQ page's `.svc-list` + `.note` and
     `.svc-list` + `.subhead` pairs (items 3.4 and 3.5 above) and on About
     Us's "the people" card grid before "What It's Like to Work With Us." --
     both the same card-then-text shape. It no longer lands on a step-7
     `.subhead`: per 2026-09-08 User request (spec §2.4) step 7 moved inside
     the `<dl>` as a seventh `.svc-item`, so that adjacency is gone. */
  margin-top: 2rem;
}

.svc-item {
  background: var(--white);
  border: 3px solid var(--ink);
  border-radius: var(--radius);
  box-shadow: 6px 6px 0 var(--shadow);
  padding: 1rem 1.1rem;
}

.svc-item__term {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--navy);
}

.svc-item__icon {
  /* Enlarged 1.75rem -> 2rem per User request 2026-09-08 (item 2.5, "make
     the checkbox next to Anthony's name bigger" -- that icon is `shield.png`
     on the About Us bio card, which is a `.svc-item__icon`). The markup still
     carries width="28" height="28" on all 21 usages; the CSS overrides the
     attributes, same as `.icon-list__icon`, whose markup still says 32.

     BLAST RADIUS, INTENDED: 21 icons across About Us, IAQ, residential and
     commercial -- the three bio icons are a set and Anthony's cannot grow
     alone, and the numbered process/service cards use the same treatment.

     MEASURED COLLATERAL, at 360px: none. 2rem was measured as the largest
     wrap-free step. At 2.25rem exactly two of the 21 terms gained a second
     line -- commercial's "1. Initial System Assessment" and residential's
     "1. A/C Repair & Maintenance" -- so the User chose 2rem on 2026-09-08 to
     keep the collateral at zero. Note this is deliberately SMALLER than
     `.icon-list__icon`'s 2.25rem; that family sits beside shorter labels and
     can afford the extra 4px, this one cannot. Nothing wraps at 1280 and no
     icon overlaps its text at either width. */
  width: 2rem;
  height: 2rem;
  flex: none;
  image-rendering: pixelated;
}

/* A <dd> carries a browser default margin-inline-start of 40px. Reset it, or
   every description hangs off the right of its own card. */
.svc-item__desc {
  margin: 0.45rem 0 0;
}

/* Sections with a paper background need the cards to stay distinguishable. */
.section--promise .svc-item,
.section--bundle .svc-item {
  background: var(--white);
}

/* ---------------------------------------------------------- pull quote -- */

.pull-quote {
  margin: 1.5rem 0;
  padding: 0.25rem 0 0.25rem 1rem;
  border-left: 6px solid var(--orange);
  font-family: var(--font-head);
  font-size: 1.1rem;
  color: var(--navy);
}

.pull-quote p {
  margin: 0;
}

/* ------------------------------------------------------------ tag list -- */

/* The commercial page's industry lists: plain, dense, no bullet art. */
.tag-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.tag-list li {
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--pale);
}

.tag-list li:last-child {
  border-bottom: 0;
}

/* OPT-IN BULLETS, per User request 2026-09-08 (items 3.3, 3.8, 4.2, 4.7 and
   4.9 -- five lists on the IAQ and duct pages that the User asked to be "in a
   bullet list format"). Those lists render with no markers at all because the
   base class above is `list-style: none`.

   A MODIFIER, NOT AN EDIT TO `.tag-list`. The base class has five more users
   the request never mentions -- the commercial page's five industry lists
   inside `.card` boxes -- and the comment above records that those are
   deliberately bullet-free. Changing the base would silently reverse that.

   `outside` (the initial value, restated because it is a decision, not a
   default we inherited): every one of these items wraps to two or three lines
   at 360px, and `inside` would run the wrapped lines back under the marker
   instead of aligning them with the first word.

   The indent is `padding-left` ON THE UL, which is what restores the `padding:
   0` the base class sets. Chromium draws an `outside` marker to the left of
   the li's BORDER box, not inside the li's padding, so putting the indent on
   the li instead was measured to hang the bullets at x=8 on a 360px screen --
   12px to the LEFT of the 20px `.wrap` gutter every other element on the page
   starts at, and 8px from the viewport edge. On the UL the markers land at
   x=28: inside the gutter, and in the same place `.icon-list`'s inline icons
   sit, which is the site's existing pattern for a marker-then-text row. The
   li's `border-bottom` separators indent with the list rather than staying
   flush at 20px, which is the deliberate trade -- each rule then spans
   exactly the text column it separates. */
.tag-list--bulleted {
  list-style-position: outside;
  padding-left: 1.25rem;
}

.tag-list--bulleted li {
  list-style-type: disc;
}

/* Orange for the IAQ page's "Does this sound familiar?" list (item 3.3, "the
   list following it should be orange"). Same --orange-dark token as
   `.icon-list--accent-orange li` and `.price-list__combo`, deliberately: this
   site has exactly one accent orange. The ::marker inherits `color`, so the
   bullets go orange with the text. Only this one list is accented; the other
   four bulleted lists get markers and nothing else.

   MEASURED, AND NOT RESOLVED HERE: --orange-dark (#D1520E) is 4.26:1 against
   the white .section background, i.e. BELOW WCAG AA's 4.5:1 for normal-size
   text. (--orange is worse at 3.08:1; --blue-mid is 5.99:1, which is why the
   two "light blue" rules above cite their numbers.) This is a pre-existing
   site-wide condition, not something introduced here: `.icon-list--accent-
   orange li` already sets body copy in this token on the commercial page.
   Recorded rather than silently re-coloured, because picking a darker orange
   would fork the one accent orange this site has. */
.tag-list--accent-orange li {
  color: var(--orange-dark);
}

/* Orange for the duct page's "Interested in a full system refresh?" sentence
   (spec §4.8, 2026-09-08 User request -- "a like break" is the User's typo
   for "a line break"; the split into its own `<p>` provides that break).
   Colour only: the User asked for orange, not bold, so this does not reuse
   `.lead--combo`'s `font-weight: 700`. Same --orange-dark token as the rules
   above, deliberately -- one accent orange sitewide. Same pre-existing
   4.26:1-against-white contrast noted above; not re-measured or re-litigated
   here. */
.text--accent-orange {
  color: var(--orange-dark);
}

.tag-list--bulleted + p {
  /* Same 0px-gap defect as `.svc-list + .subhead` above, on the User's item
     4.10 ("more space between that list and the text under it"). Keyed on the
     BULLETED modifier rather than on `.tag-list`, which both keeps it off the
     commercial industry lists by construction and confines it to the three
     places it can currently land -- item 4.10's pair plus the duct
     Test/Clean/Compare and IAQ "who is this for" lists, which are the same
     list-then-paragraph shape and are lists THIS change puts bullets on.
     Spacing only 4.10 would leave the other two flush against a list that
     just grew markers, i.e. worse than before this commit. 1.5rem, not the
     2rem used for `.svc-list` above: a `.tag-list` has no card shadow under
     it, and at 2rem the paragraph measured as drifting away from the list it
     belongs to. */
  margin-top: 1.5rem;
}

/* -------------------------------------------------------- trust strip -- */

/* The credential badges where a page repeats them mid-body. The footer keeps
   its own copy; this one wraps rather than overflowing at 360px. */
.trust-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin: 1.5rem 0 0;
}

.trust-strip img {
  image-rendering: pixelated;
}

/* --------------------------------------------- Phase 1b breakpoints -- */

/* Its own block, deliberately. See the banner at the top of this section. */
@media (min-width: 48rem) {
  .svc-list {
    grid-template-columns: 1fr 1fr;
  }

  /* About Us step 7 (spec §2.4, 2026-09-08 User request) -- a seventh
     `.svc-item` that must not land alone in the left column once the grid
     above goes two-up. Modifier on the one item rather than a change to the
     base `.svc-list` grid, which serves 15 instances sitewide. */
  .svc-item--full {
    grid-column: 1 / -1;
  }
}

/* ================================================================
 * Phase 1b round 2 components.
 *
 * APPENDED, NEVER EDITED IN PLACE, exactly as round 1 was: nothing above this
 * banner changed, so the Phase 1a header/nav rules and round 1's components
 * stay byte-identical. Desktop overrides go in the media block at the very
 * end of this file, not spliced into an earlier one.
 *
 * These classes serve /duct-cleaning-arvada and
 * /indoor-air-quality-testing-arvada. Round 2 landed the duct page only and
 * noted here that `.photo--hero` and `.tag-list--sub` were not yet used by
 * any built page; round 2b built the IAQ page, so every rule in this section
 * went live and that caveat stopped applying. `.photo--hero` has since been
 * deleted by the 2026-09-06 hero unification -- see the note where it stood.
 * Round 3 reuses this section; add here, do not fork.
 * ================================================================ */

/* ---------------------------------------------------------- photographs -- */

/* The first real photographs on the site. Deliberately NOT `.hero__art`:
   that class sets `image-rendering: pixelated`, which is right for the
   pixel-art banners and wrong for a camera photo.

   `aspect-ratio` + `object-fit: cover` rather than an intrinsic width/height
   pair, because the sources are a mix of landscape (3630x2160) and portrait
   (1216x2160) and a portrait photo at 100% width would otherwise run nearly
   two screens tall on a phone. The box is fixed, the photo is cropped to it,
   and there is no layout shift while it loads. */
.photo {
  margin: 0 0 1.25rem;
}

.photo img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
  object-position: center;
  background: var(--pale);
}

/* `.photo--hero` WAS HERE and is deleted, not deprecated. It was the second of
   three hero variants; the hero is now one shared box (see `--hero-banner-h`
   in the hero section above) and the two photo pages reach it through
   templates/_hero.html.j2's `photo_name=` argument like everyone else. Left in
   place it would have kept fighting the shared box: its `aspect-ratio` and
   `max-height` sit at the same specificity as `.hero__media img`. Its desktop
   `max-height` override in the round 2 breakpoint block below is gone for the
   same reason. */

/* In-body photographs get the same frame as a card so they read as part of
   the same kit. The 6px shadow sits inside `.wrap`'s gutter, so it never
   causes horizontal overflow at 360px. */
.photo--framed img {
  aspect-ratio: 3 / 2;
  max-height: 22rem;
  border: 3px solid var(--ink);
  border-radius: var(--radius);
  box-shadow: 6px 6px 0 var(--shadow);
}

/* Worn WITH `.photo--framed`, for a source that is taller than it is wide
   (currently one photo: the IAQ page's technician kneeling at a return vent
   with a meter, 1216x2160). In the shared 3/2 box `object-fit: cover` keeps
   only the middle ~37% of a portrait frame's height, which on that photo is a
   band of blank wall -- it throws away the one image on the site that shows
   the work actually being done. A 3/4 box keeps 75% of the height, trimming
   the window above and the carpet below while the technician, the vent and
   the meter all stay in frame.

   The cap is on WIDTH, not height. `.photo--framed`'s `max-height` exists to
   stop a tall image running two screens down a phone, but applied to a
   portrait box it does the cropping all over again: the height clamp
   silently widens the effective ratio back into a letterbox on a wide
   screen. Capping the width bounds the height by the aspect ratio instead,
   so the crop is the same at every viewport. */
.photo--portrait img {
  aspect-ratio: 3 / 4;
  max-width: 22rem;
  max-height: none;
  margin-inline: auto;
}

/* --------------------------------------------------- headings with art -- */

/* A pixel icon set beside a section title, the way `.svc-group__icon` sits
   beside a service-group title. Inline rather than flex so the title still
   wraps as one paragraph of text when the heading is long. */
.section-title__icon {
  width: 2rem;
  height: 2rem;
  margin-right: 0.5rem;
  vertical-align: -0.3em;
  image-rendering: pixelated;
}

/* The residential "Level Up" section title (residential-arvada-hvac.html.j2,
   #second-opinion-heading) treats `text-bar-level-up.png` as a decorative
   banner sitting on its own line above the heading text, per 2026-09-08
   User decision -- the `.section-title__kicker` "Level Up" stays as real,
   selectable text; the art is not a replacement for it.
   The PNG's canvas is 1600x1050 (1.5238:1) but the opaque "LEVEL UP" bar
   inside it is only 1408x407 (~3.46:1), centred with large transparent
   bands above and below -- only ~39% of the canvas height is visible
   artwork. Sizing this like the square `.section-title__icon` (height-only)
   would leave the visible bar tiny, so it is sized by width instead, with
   height:auto to preserve the true aspect ratio. At width 11rem: canvas
   height = 11rem * (1050/1600) = 7.22rem, and the visible bar within that
   canvas = 7.22rem * (407/1050) = ~2.8rem tall. */
.section-title__icon--bar {
  display: block;
  width: 11rem;
  max-width: 100%;
  height: auto;
  margin-right: 0;
  margin-bottom: 0.5rem;
  vertical-align: baseline;
}

/* The live IAQ and duct heroes both close with a line of copy that is a
   heading on the live page ("Schedule an Air Quality Assessment:", "Ready to
   See What's Hiding in Your Ducts? Call Today!"). It stays a heading here,
   one level below the hero's h1, and is sized to sit under it rather than
   compete with it. */
.hero__cta-title {
  margin: 1.5rem 0 0.5rem;
  font-size: 1.2rem;
  color: var(--pale);
}

/* ------------------------------------------------------ nested tag list -- */

/* The IAQ page's pollen-season bullet carries three sub-bullets. They are
   indented and unruled so the parent list's rules still read as the list. */
.tag-list--sub {
  margin: 0.35rem 0 0.15rem 1.1rem;
}

.tag-list--sub li {
  padding: 0.15rem 0;
  border-bottom: 0;
}

/* The sub-list's markers, per item 3.8. `.tag-list--bulleted li` is a
   DESCENDANT selector, so the three pollen seasons already take the parent's
   bullets through it -- the sub-list needs no extra class in the markup,
   which is why none was added. Two things still have to be said here:

   1. `disc` inside `disc` is not legible as nesting. `circle` (hollow) is the
      conventional second level and is the visual distinction the item asks
      for. Verified in Chromium at both widths: filled outer, hollow inner.
   2. The sub-list carries neither `--bulleted` nor `.tag-list`'s indent, so
      it needs its own `padding-left` to give its markers room. 1rem, slightly
      tighter than the parent's 1.25rem, because `.tag-list--sub` above
      already offsets the whole list by a 1.1rem left margin.

   Both selectors are `.tag-list--bulleted .tag-list--sub ...` rather than
   bare `.tag-list--sub ...` so that a future un-bulleted nested list keeps
   the existing marker-free treatment. */
.tag-list--bulleted .tag-list--sub {
  padding-left: 1rem;
}

.tag-list--bulleted .tag-list--sub li {
  list-style-type: circle;
}

/* ----------------------------------------------------------- flow line -- */

/* The duct page's "Testing -> Inspection -> Cleaning -> ..." summary, with the
   live layout's pixel health-bar art either side of it. Wraps to as many rows
   as it needs on a phone instead of overflowing. */
.flow-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  margin: 0 0 1.5rem;
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--navy);
  text-align: center;
}

.flow-line__rule {
  width: 100%;
  max-width: 12rem;
  height: auto;
  image-rendering: pixelated;
}

/* ---------------------------------------- Phase 1b round 2 breakpoints -- */

/* Its own block, deliberately. See the banner at the top of this section. */
@media (min-width: 48rem) {
  .photo--framed img {
    max-height: 26rem;
  }

  /* Re-stated because the `.photo--framed` rule above has the same
     specificity and would otherwise re-impose the height clamp here. */
  .photo--portrait img {
    max-height: none;
  }

  .hero__cta-title {
    font-size: 1.35rem;
  }
}

/* ================================================================
 * Phase 1b round 3.
 *
 * APPENDED, NEVER EDITED IN PLACE, exactly as rounds 1 and 2 were: not one
 * line above this banner is changed. Desktop overrides go in a media block
 * down here, not spliced into an earlier one.
 *
 * TWO PARTS, unlike rounds 1 and 2. First the two components that serve
 * /about-us-hvac, the sixth and last Phase 1 page. Then -- and this is the
 * departure from "components only" -- extra DECLARATIONS on the Phase 1a
 * header selectors, fixing a wrapping defect that has shipped on every page
 * since Phase 1a. They are declarations, not edits: the rules they land on
 * are untouched where they stand, and no gate, no `.is-nav-open` rule and no
 * `flex-wrap` is among them. See the note above that block for why the
 * existing tests could not catch it.
 * ================================================================ */

/* Worn WITH `.photo--framed`, for a source that is as tall as it is wide.
   Both About-page photographs are 2160x2160. `.photo--framed`'s 3/2 box would
   keep only the middle two thirds of a square frame, and its `max-height`
   makes that worse rather than better: with `width: 100%` the height clamp
   widens the EFFECTIVE ratio on a big screen (a 1088px-wide box clamped to
   26rem tall is 2.6:1), so a wide viewport crops harder than a narrow one.
   Same reasoning, and the same fix, as `.photo--portrait` above: cap the
   WIDTH, let the aspect ratio bound the height, and the crop is then identical
   at every viewport. */
.photo--square img {
  aspect-ratio: 1 / 1;
  max-width: 24rem;
  max-height: none;
  margin-inline: auto;
}

/* A piece of illustrative pixel art set inside a section's flow, the way
   `.hero__art` sits at the top of a hero. Centred and width-capped so it
   reads as an illustration beside the copy rather than a full-bleed band, and
   `image-rendering: pixelated` because it IS pixel art -- the same reason
   `.photo` deliberately does not set it. */
.section-art {
  margin: 0 0 1.25rem;
  text-align: center;
}

.section-art img {
  display: inline-block;
  width: 100%;
  max-width: 22rem;
  height: auto;
  image-rendering: pixelated;
}

/* ------------------------------------------------- desktop header fit -- */

/* A PHASE 1A DEFECT, fixed here because the header is shared and it therefore
   shipped on every page. At 1280 the header phone broke across THREE lines
   ("(720)" / "445-" / "5532") and the "Contact Us" label across two, which
   pushed the bar to 109px tall. The layout tests could not see it: they assert
   RELATIONSHIPS ("the nav is on the logo's row") on purpose, so they survive
   font-metric drift -- and a relationship assertion cannot tell a phone number
   on one line from the same number on three.

   NOTHING ABOUT THE WRAP ARCHITECTURE CHANGES HERE. No `:where(.js)` gate, no
   `.is-nav-open` rule and no `flex-wrap` declaration is touched; the
   `min-width: 48rem` block above still owns the one-row pin. These are extra
   declarations on the same selectors, in a later block, so source order
   settles them at equal specificity -- except `.site-nav__actions .btn`, which
   is (0,2,0) deliberately so it outranks the bare `.btn` padding it replaces.

   The break was mid-number and mid-word, so `nowrap` is the fix, and it is
   NOT gated to desktop: at 360 with JavaScript off the same two buttons are in
   flow, `.site-nav__actions` wraps, and they stack instead of overflowing. */
.site-nav__actions .btn--phone span,
.site-nav__actions .btn--primary {
  white-space: nowrap;
}

/* THE NAV FITS ONE ROW ONCE THE HEADER IS WIDE ENOUGH. The 2026-08-27 note
   that used to stand here concluded the opposite -- "six nav links do not fit
   one row at 1280, and cannot be made to without shrinking the text past
   legibility" -- and its arithmetic was right for the box it measured: the
   header bar was a plain `.wrap`, so its content box was 68rem - 2 gutters =
   1048px at EVERY viewport from 1280 to 1920, and one row needed more than
   that. What that note got wrong was the conclusion it drew, that the deficit
   was unfixable. The deficit was the 68rem cap itself, and the User settled
   the design question it was deferring on 2026-09-06: widen the header, keep
   body prose at 68rem. `--header-wrap` does that; the block at the end of this
   file has the re-measured budget. DO NOT restore the old numbers -- 1048px
   is not this header's content box any more.

   What the tightening below is still worth, and why it stays: it is what got
   one row within reach at all. It drops the 768 band from four nav rows /
   168px (where `nowrap` alone lands it, worse than the 128px it started at)
   to three rows / 118px, and it is what makes the six links measure 1019.8px
   on one row instead of 1201px. 0.875rem link text is a 1.2px reduction from
   0.95rem, not a legibility change. */
@media (min-width: 48rem) {
  .site-header__bar {
    gap: 0.75rem;
  }

  .site-header__logo img {
    /* 2.5rem, not 3rem: under `flex-wrap: nowrap` the flex line was already
       squeezing the lockup to 38px tall to make room, so this is a hair
       LARGER than what shipped -- it just stops being an accident.

       THIS IS A CAP, NOT THE RENDERED SIZE. The logo is a shrinkable flex
       item on an over-full line, so between 768 and 1023 it renders well
       BELOW this cap -- 26.8px at 768, 31.6px at 1023 -- and never touches
       40px. Raising this number alone therefore does not resize anything by
       the amount it appears to; what sets the size in this band is the
       `min-width` FLOOR in the header-width block at the end of the file.
       Governs 48rem-64rem only; 64rem up raises the cap to 3.65rem. */
    max-height: 2.5rem;
  }

  .site-nav {
    gap: 1rem;
  }

  .site-nav__list {
    gap: 0.85rem;
    /* The list still wraps -- `flex-wrap` is untouched -- but its lines now
       pack toward the buttons instead of toward the logo, so the sixth link
       lands directly under the fifth and the nav reads as one right-aligned
       block rather than as a full row plus an orphan on the far left. */
    justify-content: flex-end;
  }

  .site-nav__link {
    font-size: 0.875rem;
  }

  .site-nav__actions .btn {
    padding: 0.55rem 0.8rem;
    font-size: 0.9rem;
  }

  .site-nav__actions .btn__icon {
    width: 1.25rem;
    height: 1.25rem;
  }
}

/* A pixel icon trailing a run of body-copy text (16px, not a flex parent),
   per 2026-09-08 User request (spec §2.2) moving the About Us clock out of
   the section title and into the story paragraph, right after "Urgent." None
   of the other three inline-icon rules fit: `.section-title__icon` (2rem) is
   sized against a heading and would read as twice the line's cap height next
   to 16px text; `.lead__icon` and `.icon-list__icon` both assume a flex
   parent, which a plain trailing `<p>` icon does not have. Sized down from
   `.section-title__icon`'s 2rem to 1.5rem so it reads as part of the
   sentence, with margin on the left (not right) since it trails the text
   instead of leading a heading. Lives here, in Phase 1b round 3, rather than
   up in Phase 1a where it first shipped: `.body-icon` serves /about-us-hvac,
   the page round 3 is the designated home for, and Phase 1a above this file's
   first banner is APPENDED, NEVER EDITED IN PLACE. */
.body-icon {
  width: 1.5rem;
  height: 1.5rem;
  margin-left: 0.35rem;
  vertical-align: -0.3em;
  image-rendering: pixelated;
}

/* ================================================================
 * Hero unification breakpoint (2026-09-06).
 *
 * The desktop half of `--hero-banner-h`, in its own block at the end of the
 * file for the same reason rounds 1-3 put theirs here: the breakpoint lives
 * in exactly one place per round and is never spliced into an earlier query.
 * ================================================================ */

@media (min-width: 48rem) {
  /* 20.8rem = 332.8px = 80% of the 26rem box `.photo--hero` declared from
     this breakpoint up. Written as the arithmetic, not as 332.8px, so the
     baseline and the User's 80% are both readable in the value itself and
     neither can be silently re-derived. */
  :root {
    --hero-banner-h: calc(26rem * 0.8);
  }
}

/* ================================================================
 * Header width, and the logo that lives in it (User, 2026-09-06).
 *
 * WHAT WAS ACTUALLY WRONG: the logo was never too small -- THE HEADER WAS TOO
 * NARROW. `.site-header__bar` is a `.wrap`, so it was capped at `--wrap`
 * (68rem = 1088px, a 1048px content box). On a 1990px window that is ~470px
 * of unused space down each side, and inside that squeeze the nav had already
 * wrapped to two rows while the logo -- a shrinkable flex item on an
 * over-full line -- was crushed to 33.7px against a declared 2.5rem cap it
 * never reached. Scaling the logo inside that box could not work: the space
 * it needed was sitting beside the header, not inside it. So the fix is the
 * container, and the logo is then sized against the room that frees up.
 *
 * The three declarations below are one mechanism, in the order the browser
 * applies them: give the bar a wider ceiling, floor the logo so it can never
 * be crushed again, raise its cap so it can use the new room, then stop the
 * nav shrinking once there is provably space for it on one row.
 *
 * THE BUDGET, MEASURED IN CHROMIUM ON THE BUILT TREE, 2026-09-06. Every
 * number here is a `getBoundingClientRect` reading, not a stylesheet value:
 *
 *   Six links on one row .................... 633.8px  (+ 16px nav gap)
 *   Phone button + CTA, `nowrap` ............ 370.0px
 *   => `.site-nav` at max-content .......... 1019.8px
 *   Logo at its 3.65rem cap ................. 304.7px  (+ 12px bar gap)
 *   => one-row content box needed .......... 1336.7px
 *   + 2 x 20px `.wrap` gutters ............. 1376.7px of BORDER box
 *
 * 88rem = 1408px clears that by 31.3px, which is the safety margin: this site
 * ships NO webfont (see the top of this file), so a visitor with Montserrat
 * or Poppins actually installed measures different link widths than the
 * fallback stack does here. 87rem would leave only 15.3px of that margin and
 * 86rem = 1376px is 0.7px short outright.
 *
 * WHY THE ONE-ROW GATE IS 88rem AND NOT 80rem/1280. It is the same number on
 * purpose: THE NAV GOES TO ONE ROW EXACTLY WHEN THE BAR REACHES ITS FULL
 * WIDTH, and below that it is not a free choice but a trade. At 1280 the bar
 * is viewport-bound to a 1240px content box, 96.7px short of the 1336.7px
 * above, so a one-row nav there forces the logo down to 39.9px -- 1.18x the
 * 33.7px baseline, i.e. SMALLER than the 54.1px it gets by leaving the nav on
 * the two rows it already had. Both were measured. Widening gutters, gaps and
 * button padding recovers at most ~48px of the 96.7px, and the rest would
 * have to come out of the link text the 48rem block above already tightened
 * to 14px. So 1280-1407 spends the space on the logo, which is what the User
 * asked to grow, and nothing there is worse than what shipped.
 *
 * A media query cannot read a custom property, so the `88rem` in the gate
 * below and `--header-wrap` at the top of this file are ONE NUMBER written
 * twice. Change them together.
 *
 * RENDERED LOGO HEIGHTS, before -> after (Chromium, both JS states):
 *   768 .... 23.3 -> 26.8px   four nav rows, header 153.5px: both unchanged
 *   1024 ... 31.6 -> 42.8px   two nav rows, header 81.6px: both unchanged
 *   1280 ... 33.7 -> 54.1px   two nav rows, header 81.6px: both unchanged
 *   1920 ... 33.7 -> 58.4px   nav 2 rows -> ONE, header 81.6px unchanged
 * The header is never taller than it was at any width, and no width overflows
 * the viewport. 58.4px is the cap for exactly that reason: it is the height
 * the bar already had (the old two-row nav's 58.4px), so the logo grows into
 * space the header was already paying for and the bar stays 77.6px + 4px
 * border. Anything above 3.65rem makes the header taller, which the User
 * ruled out.
 * ================================================================ */

.site-header__bar {
  /* Later in source order than `.wrap` at equal (0,1,0) specificity, so this
     wins without an `!important` and without touching `.wrap`, which still
     governs every other block on the page. `width: 100%` is unchanged, so
     below 1408px the bar is viewport-bound and this is inert -- the widening
     only ever happens where there was empty space to reclaim. */
  max-width: var(--header-wrap);
}

@media (min-width: 48rem) {
  .site-header__logo {
    /* THE FLOOR, AND THE ONLY THING STOPPING THE CRUSH. `max-height` on the
       img is advisory here: the flex line is over-full, the logo is
       shrinkable, and it lost -- 23.3px rendered against a 40px cap. A
       `min-width` on the flex ITEM is what flex-shrink is not allowed to go
       past, and the img's `max-width: 100%` then carries the height with it.

       8.75rem = 140px wide = 26.8px tall at 768, up from 23.3px. It is a
       floor and not a pin (`flex-shrink: 0`) on purpose, so that 1024 and up
       still grow past it instead of being frozen at the tablet size.

       WHY IT IS NOT LARGER: 768 has a 728px content box, and after the bar
       gap, the nav gap and the 370px phone+CTA pair the link list is left
       with 330px - the logo's width. The list must stay at least 184px wide
       or "Indoor Air Quality Testing", the longest link, breaks across two
       lines and the header grows a row. Measured: 9rem still renders four
       rows / 149.5px, 9.25rem tips it to 171.9px. 8.75rem sits 8px inside
       that cliff -- deliberate headroom, for the same no-webfont reason as
       the 88rem above. */
    min-width: 8.75rem;
  }
}

@media (min-width: 64rem) {
  .site-header__logo img {
    /* 3.65rem = 58.4px, the enlarged cap. Reached at 1408 and above; between
       1024 and 1407 the line is still over-full so the logo renders below it
       (42.8px at 1024, 54.1px at 1280) and grows smoothly with the viewport.

       GATED AT 64rem, NOT 48rem, AND THIS IS NOT COSMETIC: the cap sets the
       flex BASE size, so raising it at 768 makes the logo shrink to a larger
       final width, which steals from the link list and takes the header to
       five nav rows / 211.9px (measured). 768-1023 keeps the 2.5rem cap and
       gets its enlargement from the floor above instead. */
    max-height: 3.65rem;
  }
}

@media (min-width: 88rem) {
  .site-nav {
    /* SIX LINKS, ONE ROW -- the User-visible point of the widening. The nav
       is a flex item whose base size is its max-content width (1019.8px, the
       list unwrapped); refusing to shrink it therefore hands the list exactly
       the width it needs and the wrap never happens. The logo stays
       shrinkable and absorbs whatever is left, which is why it and not the
       nav is what gives way if this ever gets tight again.

       Pinned by test_nav_links_are_one_row_once_the_header_is_full_width,
       together with an overflow assertion: the failure mode of
       `flex-shrink: 0` is a nav that pushes past the viewport instead of
       re-wrapping, and there is 196px of slack before that can happen
       (1368px content box, 140px logo floor, 12px gap). */
    flex-shrink: 0;
  }
}

/* ================================================================
 * Phase 1b round 4 -- PER-PAGE HERO ART FRAMING (User, 2026-09-09).
 * Spec: docs/spec/website-content-updates.md sec. 1.1, extended by sec. 0.2
 * item 1 (which folds sec. 2.1 and sec. 3.1 in: "fix the hero image" on
 * /about-us-hvac and /indoor-air-quality-testing-arvada means the CROP, not a
 * new photograph).
 *
 * APPENDED, NEVER EDITED IN PLACE, exactly as rounds 1-3 were. That matters
 * more than usual here, because the rule this round reframes -- `.hero__media
 * img` at the top of this file -- sits in the Phase 1a region. It is NOT
 * edited. Nor is it shadowed by a same-specificity copy down here that would
 * depend on source order to win: every rule below is written in the COMPOUND
 * form `.hero__media.hero__media--<page> img` -- two class selectors plus a
 * type selector, (0,2,1), against the base rule's (0,1,1) -- so it wins on
 * SPECIFICITY. Move this block anywhere in the file and the rendering is
 * identical. That is deliberate: a cascade-order dependency is invisible in a
 * diff and this is the file where that has bitten before.
 *
 * WRITE THE MODIFIER TWICE, AND HERE IS WHY. `.hero__media--about img` on its
 * own is (0,1,1), not (0,2,1): a BEM `--modifier` is still exactly ONE class.
 * That TIES the base rule `.hero__media img` and wins on source order alone.
 * The tie is not academic for the two `object-position` rules below, which
 * override a property the base rule actually sets (`object-position: center`,
 * with it at the top of this file). Move a single-class version of this block
 * above that rule and the About/IAQ framing silently reverts to centred --
 * nothing in the diff, nothing in the test run, just a wrong-looking page.
 * (The home/residential rules set `transform`, which nothing else sets, so
 * they would survive either form; the claim above has to hold for all four.)
 * The compound form is what makes the paragraph above TRUE rather than merely
 * intended. It shipped single-class once, in this block, and was corrected
 * the same day -- verified by moving the whole block above the base rule,
 * rebuilding, and confirming the computed `object-position` did not move.
 *
 * WHAT IS AND IS NOT FORKED. The shared box is untouched: `--hero-banner-h`
 * (10rem / calc(26rem * 0.8)) and the 0.2 opacity are exactly the values the
 * User fixed on 2026-09-06, and NOTHING here selects `.hero__media` itself --
 * only the img inside it. The 2026-09-06 rationale block above warns "Do not
 * 'fix' the mobile growth by special-casing commercial-hvac-arvada"; that
 * warning is about the box's HEIGHT, and no height is forked here. What is
 * forked is how the art sits inside a box whose size never changes, which is
 * the part `templates/_hero.html.j2` has always called the per-page part.
 *
 * ------------------------------------------------------------------
 * WHY object-position ALONE CANNOT DO THE HOME AND RESIDENTIAL PAGES.
 *
 * Measured in Chromium against the built tree, 2026-09-09, `object-fit:
 * cover` as shipped:
 *
 *   page          360px band          1280px band
 *   home          100%x / 98.8%y      100%x / 57.8%y   of the source visible
 *   residential   100%x / 98.8%y      100%x / 57.8%y
 *
 * 100% of the source WIDTH is visible at both widths on both pages, because
 * the band is wider-ratio (2.25:1 at 360, 3.85:1 at 1280) than the art
 * (2.222:1), so `cover` is width-bound. With no horizontal slack there is
 * nothing for `object-position`'s x to pan across: on these two pages it is
 * INERT at every width, and the y slack only moves an already-centred
 * subject. Neither `contain` nor `fill` helps -- `contain` scales the art
 * DOWN (the subject would shrink from 77.6% of the band to 44.8% at 1280,
 * the opposite of "closer in"), and `fill` stretches pixel art 1.7:1.
 *
 * So the request is not reachable on the `object-fit` axis, and the reason is
 * the art, not the rule. Measured with PIL against the committed sources:
 *
 *   art (10000x4500)                        ink bbox        of the frame
 *   hero-banner.png (home)                  4977 x 2016     49.8%w  44.8%h
 *   hero-banner-vector-20260412...(resi)    3114 x 2016     31.1%w  44.8%h
 *
 * Half to two thirds of each file is EMPTY MARGIN, and that margin is
 * rgb(30, 58, 95) -- byte-identical to `--navy: #1E3A5F`, the colour `.hero`
 * is already painted. So the band was spending most of its area drawing navy
 * onto navy, and the drawn subject sat at x=74.2% / 72.6% of the frame:
 * jammed against the right edge with the left half empty. That is what
 * "double cropped ... should be closer in" and "needs to be filled and closer
 * in on the house icon" describe.
 *
 * THE FIX IS TO FRAME THE BAND ON THE INK, and because the discarded margin
 * is the hero's own navy, clipping it costs nothing visible.
 *
 *   transform: scale(S) translateX(T)      -- translate applies FIRST
 *
 * T moves the ink's centre to the band's centre: T = 50% - <ink centre x>.
 * The whole source width is on screen (above), so source-x% and band-x% are
 * the same axis and one T holds at every width. S then zooms about the band
 * centre, which the translate has just put the ink on, so S and T are
 * independent -- change one without recomputing the other.
 *
 * S IS 90% OF THE LARGEST ZOOM THAT STILL CLIPS NOTHING, per breakpoint:
 *
 *     S = 0.9 * min(100 / ink-width-as-%-of-band, 100 / ink-height-as-%)
 *
 * The 10% is headroom, and it is why the User's "not cropped" survives being
 * zoomed: at every width below the subject ends up at exactly 90% of
 * whichever band axis binds first, so no part of the artwork can reach an
 * edge. The two breakpoints need different S because the band's aspect ratio
 * changes (2.25:1 vs 3.85:1) while the art's does not -- the same reason
 * `--hero-banner-h` itself is forked at 48rem.
 *
 *   page         width   ink in band (before)   S      ink in band (after)
 *   home          360    49.8%w  45.4%h        1.81    90.0%w  82.1%h
 *   home         1280    49.8%w  77.5%h        1.16    57.7%w  89.9%h
 *   residential   360    31.1%w  45.4%h        1.98    61.7%w  89.8%h
 *   residential  1280    31.1%w  77.5%h        1.16    36.1%w  89.9%h
 *
 * Both pages share S at 1280 because both inks are 2016px tall and the
 * height binds there; they differ at 360 because the width binds for home
 * (its ink is 4977px wide) and the height still binds for residential.
 *
 * NO VERTICAL TRANSLATE. Both ink boxes are already centred vertically in
 * their source (y centre 49.7% and 49.5%), which is under 3px of band at
 * 1280. Correcting that would be noise dressed up as precision.
 * ------------------------------------------------------------------ */

.hero__media.hero__media--home img {
  /* ink centre x = (4928 + 9904) / 2 / 10000 = 74.2%, so T = 50 - 74.2. */
  transform: scale(1.81) translateX(-24.2%);
}

.hero__media.hero__media--residential img {
  /* ink centre x = (5702 + 8815) / 2 / 10000 = 72.6%, so T = 50 - 72.6. */
  transform: scale(1.98) translateX(-22.6%);
}

@media (min-width: 48rem) {
  /* The 48rem half, gated on the same breakpoint as `--hero-banner-h` itself
     and for the same reason: the band goes from 2.25:1 to 3.85:1 there while
     the art's ratio does not move, so the axis that binds the zoom changes
     from width to height. Both pages land on the same S because the height
     binds for both and both inks are exactly 2016px tall. T is unchanged --
     the whole source width is on screen at every width, so the translate that
     centres the ink is breakpoint-independent. */
  .hero__media.hero__media--home img {
    transform: scale(1.16) translateX(-24.2%);
  }

  .hero__media.hero__media--residential img {
    transform: scale(1.16) translateX(-22.6%);
  }
}

/* ------------------------------------------------------------------
 * THE PHOTOGRAPH PAGES ARE THE OPPOSITE CASE and are fixed on the
 * `object-position` axis alone, with no transform: their sources are much
 * TALLER-ratio than the band, so `cover` is height-bound, there is a great
 * deal of vertical slack, and the only question is which horizontal slice of
 * the picture the band shows. Measured before this round:
 *
 *   page   source        of the source visible at 360   at 1280
 *   about  2160 x 2160   44.4% of the height            26.0%
 *   iaq    3630 x 2160   74.6%                          43.7%
 *
 * For `object-position: center Y`, the visible source band is
 * [Y*(1-v), Y*(1-v)+v] where v is that visible fraction -- which is how the
 * windows below were chosen rather than nudged.
 * ------------------------------------------------------------------ */

.hero__media.hero__media--about img {
  /* THE STANDING TECHNICIAN WAS DECAPITATED AT DESKTOP. His head occupies
     source y 29-41%; the centred 26% window at 1280 was y[37.0, 63.0], so
     only his chin and below survived. 38% moves the window to y[28.1, 54.1]
     -- his head is whole, and the kneeling technician's head (y 49-62%) keeps
     its top half. Both heads together span 33% of the frame and the desktop
     window is 26% tall, so they CANNOT both fit and this is a choice, not a
     tuning: the standing technician has his hands on the gas-valve wiring and
     is the subject of the alt text. At 360 the window is 44.4% tall and lands
     at y[21.1, 65.5], which holds both heads in full. */
  object-position: center 38%;
}

.hero__media.hero__media--iaq img {
  /* THE SUNSET WAS THE POINT AND IT WAS SITTING ON THE BOTTOM EDGE. The
     mountain ridge runs across source y 66-71% and the glow above it y
     58-69%; the centred 43.7% window at 1280 was y[28.2, 71.9], so the ridge
     landed hard against the bottom of the band and the whole foreground was
     gone. 60% moves the window to y[33.8, 77.5]: the glow and the ridge sit
     inside it with a strip of grassland under them. At 360 the window is
     74.6% tall and barely moves (y[12.7, 87.3] to y[15.2, 89.8]), which is
     correct -- there was nothing wrong with the mobile crop. */
  object-position: center 60%;
}

/* /commercial-hvac-arvada AND /duct-cleaning-arvada ARE DELIBERATELY ABSENT,
 * and their absence is a measured decision rather than an omission.
 *
 * COMMERCIAL asked to "fill the space" (spec sec. 1.1) and already does:
 * commercial-hero.png is 3150x779, a 4.04:1 scene with ink edge to edge and
 * no empty margin at all, and under the shared box `cover` shows 55.6%w/100%h
 * at 360 and 95.1%w/100%h at 1280 -- the band is full on both axes at both
 * widths. The request was raised against the pre-2026-09-06 layout, where
 * that page's band rendered 88.9px at 360; the shared height already answered
 * it (+80%, recorded in the rationale block above). Every fit available here
 * would make it worse: sec. 1.1 works out that `contain` renders this art
 * ~89px tall inside the 160px mobile band, "the exact opposite of fill the
 * space". So the correct change is none.
 *
 * DUCT-CLEANING has no hero image at all. It is BLOCKED on a photograph the
 * User is supplying (spec sec. 0.2 item 3, sec. 4.1) and must not be given a
 * placeholder or another page's art. Its bare `call hero()` passes no
 * `framing`, so nothing here selects it and its empty `.hero__media` renders
 * exactly as before -- which is the point: the band is emitted on that page
 * too, and it is what proves "the same size everywhere".
 * ================================================================ */
