/* ============================================================================
   Inspect This Home Inspections LLC — v2 stylesheet
   Plain CSS. No framework, no build step, no dependencies. Same reasoning as
   v1: eight static pages that change twice a year should not need a toolchain.

   MOBILE FIRST. Every base rule below is the phone layout; min-width queries
   add the desktop. This is deliberate — most people searching "home inspector
   near me" are on a phone, often standing in a house they are about to buy.

   Palette is the actual flag: Old Glory Blue #0A3161, Old Glory Red #B31942.
   The owner is an Army veteran and that matters to him, so the colours carry
   it structurally rather than a badge stuck in the footer.
   ========================================================================= */

/* Metric-matched fallbacks. The ratios are MEASURED from the shipped woff2
   against the local face, not guessed: Public Sans averages .4740em per
   character, Arial .4484em; Fraunces 600 averages .4901em, Georgia .4476em.
   Text therefore lays out ~5.7% (body) and ~9.5% (headings) narrower in the
   fallback and then widens when the webfont lands, so paragraphs gain a line
   and everything below them jumps. In the hero that shift happens inside the
   visible viewport, on top of the LCP element.

   size-adjust is the load-bearing declaration; the *-override values only
   matter if a line-height ever returns to `normal`.

   Note this deliberately moves the swap-window fallback off system-ui: that
   resolves to Segoe UI, SF Pro or Roboto depending on the machine - three
   different width ratios, so it cannot be metric-matched at all. */
@font-face {
  font-family: "Public Sans Fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
  size-adjust: 105.7%;      /* .4740 / .4484 */
  ascent-override: 89.9%;   /* 95.0 / 1.057 */
  descent-override: 21.3%;  /* 22.5 / 1.057 */
  line-gap-override: 0%;
}
@font-face {
  font-family: "Fraunces Fallback";
  src: local("Georgia"), local("Times New Roman"), local("Liberation Serif");
  size-adjust: 109.5%;      /* .4901 / .4476 */
  ascent-override: 89.3%;   /* 97.8 / 1.095 */
  descent-override: 23.3%;  /* 25.5 / 1.095 */
  line-gap-override: 0%;
}

/* ---------------------------------------------------------------- tokens -- */
:root {
  /* Brand — fixed, non-negotiable */
  --navy:       #0a3161;
  --navy-deep:  #06203f;
  --navy-ink:   #041629;
  --red:        #b31942;
  --red-dark:   #8f1435;

  /* Neutrals tuned to the navy so greys never look muddy against it */
  --ink:        #10192a;
  --ink-soft:   #4a5768;
  --ink-faint:  #63738a;   /* 4.76:1 on white — the AA floor. Never go lighter for text. */
  --line:       #dfe6ee;
  --line-soft:  #eef2f7;
  --bg:         #ffffff;
  --bg-soft:    #f5f8fb;

  /* On-navy text. Not pure white for body — slightly cooled so it reads as
     considered rather than harsh, while staying well above 4.5:1. */
  --on-navy:      #ffffff;
  --on-navy-soft: #c8d6e6;

  /* Type. Two faces, both variable, both loaded once.
     Fraunces  — heading face. A modern take on a 19th-century "Scotch" style:
                 institutional, American, built rather than fashionable. Gives
                 a one-man trade business gravitas without looking corporate.
     Public Sans — body face. Literally the US federal government's typeface
                 (USWDS), designed for legibility in dense official documents.
                 Appropriate for a licensed inspector and unusually readable
                 at small sizes on phones. */
  --font-head: "Fraunces", "Fraunces Fallback", Georgia, "Times New Roman", serif;
  --font-body: "Public Sans", "Public Sans Fallback", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Spacing scale — 4px base, standard density */
  --s1: .25rem; --s2: .5rem;  --s3: .75rem; --s4: 1rem;
  --s5: 1.5rem; --s6: 2rem;   --s7: 3rem;   --s8: 4rem;  --s9: 6rem;

  --wrap: 1180px;
  /* 4px, not 12px. A 12px radius on every rectangle is the default look of
     consumer software; drafting sets have square corners. This one token
     change pulls the cards, the report panel, the blueprint stage and the PDF
     viewer toward the engineering language the cutaway already speaks.
     Pills stay at 999px - they read as controls, which is correct. */
  --radius: 4px;
  --radius-sm: 3px;

  /* Shadows built from the brand navy, never neutral black — black shadows
     over a navy palette read grey and cheapen the whole page. */
  --shadow-sm: 0 1px 2px rgba(10,49,97,.07), 0 2px 6px rgba(10,49,97,.05);
  --shadow:    0 2px 4px rgba(10,49,97,.06), 0 12px 32px rgba(10,49,97,.09);
  --shadow-lg: 0 4px 8px rgba(10,49,97,.07), 0 28px 60px rgba(10,49,97,.14);

  --ease: cubic-bezier(.4, 0, .2, 1);
  --dur: 200ms;

  /* Height of the fixed mobile action bar, so content can clear it. */
  --actionbar: 4.25rem;
}

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;   /* stop iOS inflating text in landscape */
  scroll-behavior: smooth;
  /* Anchor targets must clear the sticky header rather than hide under it. */
  scroll-padding-top: 5rem;

  /* Horizontal-overflow guard lives HERE, and uses clip rather than hidden.
     Both details are load-bearing:
       - on <body>, overflow-x:hidden forces overflow-y to compute as auto,
         which makes the body its own scroll container. window.scrollY then
         stays pinned at 0, and anchor links, scroll restoration and
         programmatic scrolling all silently stop working.
       - `clip` does not create a scroll container at all, so it cannot
         repeat that mistake here. `hidden` on <html> would. */
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.0625rem;      /* 17px — above the 16px floor, comfortable on phones */
  line-height: 1.65;
  /* font-optical-sizing: auto is doing real work — it maps Fraunces' opsz axis
     to the used pixel size automatically, so a 54px h1 gets the display cut and
     16px labels get the text cut. Hand-written font-variation-settings would
     FREEZE opsz at one value across the whole clamp() range and defeat it. */
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  /* Inherited widow control for every body block. Headings override with
     `balance` above. One declaration instead of enumerating ten selectors. */
  text-wrap: pretty;
  /* Public Sans figures are proportional: '1' is .407em, '0' is .612em. Phone
     and licence numbers read ragged without tabular figures. */
  font-variant-numeric: tabular-nums;
  /* The fixed bottom bar on phones would otherwise cover the footer. */
  padding-bottom: var(--actionbar);
}
@media (min-width: 800px) { body { padding-bottom: 0; } }

.wrap { width: min(100% - 2rem, var(--wrap)); margin-inline: auto; }
@media (min-width: 640px) { .wrap { width: min(100% - 3rem, var(--wrap)); } }

/* Three utilities that retire ~25 inline style= attributes scattered across
   the pages. A design system overridden inline at every second block is not a
   system. */
.wrap.narrow { max-width: 44rem; }
.center { text-align: center; }
.actions.center { justify-content: center; }

/* The slogan is the one line that is the business's own voice. It gets the
   display tier that nothing else on the site was using: there was previously
   nothing set between the h3 cap of 1.35rem and the h2 cap of 2.6rem. */
.slogan { font-size: clamp(2rem, 5.5vw, 3.1rem); }

/* ------------------------------------------------------------ typography -- */
h1, h2, h3, h4 {
  font-family: var(--font-head);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -.02em;
  margin: 0 0 .5em;
  text-wrap: balance;        /* stops one orphaned word on a line; ignored where unsupported */
}
/* Capped at 3.4rem rather than letting it run to ~4rem. Bigger looked more
   impressive on a screenshot and pushed the call button below the fold on a
   laptop, which is the one thing this page cannot afford: a buyer on a
   contingency clock should never have to scroll to find the number. */
/* `font-variation-settings: "SOFT" 0, "WONK" 0` used to sit on this rule. It
   was dead code twice over: Google axis-subsets the served file so SOFT and
   WONK are not in it, and 0 is the default for both anyway. Requesting those
   axes would have cost +86KB for a declaration that changes nothing. */
h1 { font-size: clamp(2.05rem, 5.6vw, 3.4rem); }
h2 { font-size: clamp(1.7rem, 4.4vw, 2.6rem); }

/* Display figures stay proportional. Tabular is right for a licence number in
   a row of data; it is wrong for the big "68" in the report panel, where even
   spacing reads as a spreadsheet rather than as a headline. */
h1, h2, h3, h4, .reportfacts b, .slogan { font-variant-numeric: proportional-nums; }

/* Tracking has to be a function of RENDERED SIZE, not of heading level. A flat
   -.02em means -0.66px on a 32.8px phone headline and -1.09px on a 54.4px
   desktop one - over-tight small, under-tight large, because em is linear in
   font-size and optimal tracking is not. The em term rides the clamp; the flat
   px term pulls the small end back. No media queries, no JS. */
h1, h2 { letter-spacing: calc(-.038em + .8px); }
h3 { font-size: clamp(1.15rem, 2.4vw, 1.35rem); letter-spacing: -.01em; }
h4 { font-size: 1rem; letter-spacing: 0; }

/* text-wrap: pretty was only on .lead. Every other paragraph on the site -
   centred section intros especially - was free to drop a single word onto its
   own last line, which is the classic template look. One declaration fixes it
   everywhere. */
p { margin: 0 0 var(--s4); text-wrap: pretty; }
.lead { font-size: clamp(1.075rem, 2.4vw, 1.3rem); line-height: 1.6; color: var(--ink-soft); text-wrap: pretty; }
a { color: var(--navy); text-underline-offset: .18em; }
img, svg, video { max-width: 100%; height: auto; display: block; }
strong, b { font-weight: 700; }

.eyebrow {
  display: inline-block; margin-bottom: var(--s3);
  font-family: var(--font-body);
  font-size: .78rem; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--red);
}
.eyebrow.on-dark { color: #ff9eb4; }
/* And automatically on any navy band, because .on-dark being opt-in is a trap:
   the default eyebrow is Old Glory Red, which on Old Glory Blue is 1.92:1 —
   the exact pairing palette.json lists as forbidden. Forgetting the class once
   put unreadable red-on-navy live on /about, and nothing caught it: the
   contrast check verifies that the pairing IS bad, never that the site avoids
   using it. Making the band responsible removes the chance to forget. */
.band.navy .eyebrow { color: #ff9eb4; }

/* Focus. Never removed — a keyboard or switch user has no other way to see
   where they are. Two-tone so it survives on both light and navy grounds. */
:focus-visible {
  outline: 3px solid var(--red);
  outline-offset: 2px;
  border-radius: 3px;
}
/* ACCESSIBILITY FIX. This used to read `.on-navy :focus-visible` — but
   `.on-navy` is a colour TOKEN, not a class that appears anywhere in the HTML.
   So the rule matched nothing, and keyboard focus inside every dark section
   fell back to the default red ring: #b31942 on #0a3161 is 1.93:1, which is
   invisible. A keyboard or switch user had no way to see where they were in
   the cutaway, the report panel or the navy bands. These are the real dark
   containers. */
section.band.navy :focus-visible,
.cutaway :focus-visible,
.reportbox :focus-visible,
.hero :focus-visible,
footer.site :focus-visible {
  outline-color: #ffd0dc;   /* 9.42:1 on navy — asserted in check-contrast.mjs */
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--navy); color: #fff; padding: var(--s3) var(--s4); font-weight: 600;
}
.skip:focus { left: 0; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --------------------------------------------------------------- buttons -- */
/* min-height 48px everywhere: comfortably over the 44px touch-target floor,
   including for a large thumb on a cracked phone in a driveway. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
  min-height: 48px; padding: .8rem 1.4rem;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  font-family: var(--font-body); font-size: 1rem; font-weight: 600; line-height: 1.2;
  text-decoration: none; cursor: pointer;
  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease),
              transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.btn svg { width: 20px; height: 20px; flex: none; }
.btn-primary { background: var(--red); color: #fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--red-dark); transform: translateY(-1px); box-shadow: var(--shadow); }
.btn-outline { border-color: var(--line); background: #fff; color: var(--navy); }
.btn-outline:hover { background: var(--bg-soft); border-color: #c3d0de; }
.btn-ghost { border-color: rgba(255,255,255,.45); color: #fff; }
.btn-ghost:hover { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.7); }
.btn:active { transform: translateY(0); }
.btn-lg { min-height: 54px; padding: .95rem 1.75rem; font-size: 1.05rem; }

.actions { display: flex; flex-wrap: wrap; gap: var(--s3); }
.actions .btn { flex: 1 1 auto; }
@media (min-width: 520px) { .actions .btn { flex: 0 0 auto; } }

/* ----------------------------------------------------------- flag stripe -- */
/* The flag reduced to a 3px rule. Used as a divider throughout. Carries the
   reference everywhere without a single piece of bunting. */
.stripe {
  height: 3px; border: 0; margin: 0;
  background: linear-gradient(90deg, var(--red) 0 34%, #fff 34% 66%, var(--navy) 66% 100%);
}

/* ------------------------------------------------------------- call bar --- */
.callbar {
  background: var(--navy-ink); color: #fff;
  font-size: .875rem; padding: .5rem 0;
}
.callbar .wrap {
  display: flex; align-items: center; justify-content: center;
  gap: .5rem var(--s4); flex-wrap: wrap; text-align: center;
}
.callbar a { color: #fff; font-weight: 600; text-decoration: none; white-space: nowrap; }
.callbar a:hover { text-decoration: underline; }
.callbar .msg { color: var(--on-navy-soft); }
/* This used to be hidden below 560px as "noise". That was backwards: the phone
   is exactly where the audience is - standing in a house, on a contingency
   clock - and hiding the line left them three bare links and no reason to tap
   any of them. It now stays, on its own row, one size down. */
/* The line stays; the STACK does not. With every link column-stacked and each
   one now 44px tall for touch, the bar was eating about 190px of a phone's
   first screen before the hero began — the message and three separate rows.
   The message keeps its own row (flex-basis:100% forces the break) and the
   three actions share the row beneath, which fits comfortably at 320px. Same
   content, same tap targets, roughly 110px of the fold given back. */
@media (max-width: 560px) {
  .callbar .wrap { flex-direction: row; flex-wrap: wrap; gap: .1rem .9rem; }
  .callbar .msg { flex: 0 0 100%; font-size: .78rem; }
}

/* ---------------------------------------------------------------- header -- */
header.site {
  position: sticky; top: 0; z-index: 50;
  background: rgba(255,255,255,.94);
  backdrop-filter: saturate(1.6) blur(10px);
  border-bottom: 1px solid var(--line);
}
header.site .wrap {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s4); padding: .7rem 0;
}
.brand { display: flex; align-items: center; gap: .7rem; text-decoration: none; color: var(--ink); }
.brand .mark { width: 38px; height: 38px; flex: none; }
.brand .txt { display: flex; flex-direction: column; line-height: 1.15; }
.brand .txt b { font-family: var(--font-head); font-size: 1.02rem; font-weight: 600; letter-spacing: -.015em; }
.brand .txt span { font-size: .7rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--red); }
@media (min-width: 400px) { .brand .txt b { font-size: 1.12rem; } }
/* Small phones: the name is long enough to wrap to three cramped lines next to
   the shield. Tightening the mark and the type keeps it to two clean lines
   without dropping the credential line, which is doing real work up here. */
@media (max-width: 430px) {
  .brand { gap: .5rem; }
  .brand .mark { width: 32px; height: 32px; }
  .brand .txt b { font-size: .94rem; }
  .brand .txt span { font-size: .62rem; letter-spacing: .04em; }
  .callbar { font-size: .82rem; }
}

/* Nav toggle. Checkbox + label rather than a <details>: a closed <details>
   collapses its content slot, which laid the desktop nav out at zero width and
   made the whole menu invisible. The <nav> here is always in normal flow. */
.navtoggle { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.navbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; margin-left: auto; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
  color: var(--navy);
}
.navbtn .x { display: none; }
.navtoggle:checked ~ .navbtn .x { display: block; }
.navtoggle:checked ~ .navbtn .bars { display: none; }
/* The real checkbox is off-screen, so its focus ring has to be drawn on the
   label instead — otherwise the control is invisible to keyboard users. */
.navtoggle:focus-visible ~ .navbtn { outline: 3px solid var(--red); outline-offset: 2px; }

.mainnav {
  display: none;
  position: absolute; left: 0; right: 0; top: 100%;
  background: #fff; border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow); padding: var(--s3) 0 var(--s4);
}
.navtoggle:checked ~ .mainnav { display: block; }
.mainnav ul { list-style: none; margin: 0; padding: 0; width: min(100% - 2rem, var(--wrap)); margin-inline: auto; }
.mainnav a {
  display: flex; align-items: center; min-height: 48px; padding: .55rem .25rem;
  border-bottom: 1px solid var(--line-soft);
  color: var(--ink); text-decoration: none; font-weight: 600;
}
.mainnav a[aria-current="page"] { color: var(--red); }
.mainnav .cta { justify-content: center; border-bottom: 0; margin-top: var(--s3); color: #fff; }

@media (min-width: 960px) {
  .navbtn { display: none; }
  .mainnav {
    display: block; position: static; border: 0; box-shadow: none; padding: 0; background: none;
  }
  .mainnav ul { display: flex; align-items: center; gap: .15rem; width: auto; }
  .mainnav a {
    min-height: 40px; padding: .45rem .7rem; border: 0; border-radius: var(--radius-sm);
    font-size: .95rem; font-weight: 500; color: var(--ink-soft);
  }
  .mainnav a:hover { background: var(--bg-soft); color: var(--ink); }
  .mainnav a[aria-current="page"] { color: var(--navy); background: var(--bg-soft); font-weight: 600; }
  .mainnav .cta { margin: 0 0 0 var(--s3); min-height: 44px; color: #fff; }
}

/* ------------------------------------------------------------------ hero -- */
.hero {
  position: relative; isolation: isolate;
  background: var(--navy-ink); color: #fff;
  overflow: hidden;
}
.hero .photo { position: absolute; inset: 0; z-index: 0; background: var(--navy-ink); }
.hero .photo img,
.hero .photo video { width: 100%; height: 100%; object-fit: cover; object-position: 50% 42%; }

/* The hero video and its still are stacked in the same box. The still only
   appears when motion is unwelcome (see the reduced-motion block below). */
.hero .heroposter { position: absolute; inset: 0; display: none; }
.hero .herovid { position: relative; display: block; }
/* Scrim. On a phone the text sits over the middle of the photo, so the wash
   has to be vertical; on desktop the text is in a left column and the wash
   turns horizontal, letting the flag stay bright on the right. */
.hero .scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(4,18,40,.90) 0%, rgba(4,18,40,.80) 45%, rgba(4,18,40,.92) 100%);
}
@media (min-width: 900px) {
  /* Lifted from .96/.90 to .93/.84. At the old values the footage behind the
     headline was effectively black - the page paid 394KB and an autoplay
     element to show almost nothing. Worst case for legibility is white text
     over a white flag stripe at .84 alpha, which composites to roughly
     #2b3749 and still measures ~11.8:1 against white, so this costs no
     contrast at all. Asserted in check-contrast.mjs. */
  .hero .scrim {
    background:
      linear-gradient(90deg, rgba(4,18,40,.93) 0%, rgba(4,18,40,.84) 42%, rgba(4,18,40,.55) 70%, rgba(4,18,40,.22) 100%),
      linear-gradient(180deg, rgba(4,18,40,.32), rgba(4,18,40,.22));
  }
}
.hero .wrap { position: relative; z-index: 2; padding: var(--s7) 0 var(--s8); }
@media (min-width: 900px) {
  .hero .wrap { padding: var(--s8) 0; }
  .hero .inner { max-width: 38rem; }
}
.hero h1 { color: #fff; margin: var(--s4) 0 var(--s4); }
.hero .lead { color: #d9e4f0; max-width: 32rem; margin-bottom: var(--s5); }
.hero .actions { margin-bottom: var(--s5); }

/* Veteran mark — a first-class element, not a footer afterthought. */
.vet {
  display: inline-flex; align-items: center; gap: .55rem;
  padding: .45rem .9rem; border-radius: 999px;
  background: rgba(255,255,255,.10); border: 1px solid rgba(255,255,255,.32);
  color: #fff; font-size: .78rem; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
}
.vet svg { width: 16px; height: 16px; flex: none; }
.vet.on-light { background: #fff; border-color: var(--line); color: var(--navy); box-shadow: var(--shadow-sm); }

/* Inline proof directly under the hero CTAs — licence number visible without
   scrolling, which is the single fact a nervous buyer is looking for. */
/* The comment above says the licence number is the one fact a nervous buyer
   actually wants — and it was then set smaller and fainter than anything else
   in the hero. The digits are now white at full weight and tabular-lined so
   they read as a record rather than as a caption; only the label stays quiet.
   The icon colour was #7fb2ff, a blue that appears nowhere else in the
   palette — now the same lit pink used for every other accent on navy. */
.hero .proof {
  display: flex; flex-wrap: wrap; gap: .6rem 1.6rem;
  padding-top: var(--s4); border-top: 1px solid rgba(255,255,255,.18);
  font-size: .9375rem; color: #fff; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.hero .proof span { display: inline-flex; align-items: center; gap: .45rem; }
.hero .proof svg { width: 17px; height: 17px; color: #ffd0dc; flex: none; }

/* ------------------------------------------------------------- credstrip -- */
/* Navy, not white. This band sits between the navy hero and the navy cutaway,
   so a white strip made the first two screens flicker navy - white - navy, and
   it rendered the four credentials as small grey text at exactly the moment
   trust is being established. Continuing the dark ground gives a tonal stair
   instead, and lets the credentials carry weight. */
.credstrip { background: var(--navy); border-bottom: 1px solid rgba(255,255,255,.12); }
.credstrip ul {
  list-style: none; margin: 0; padding: var(--s5) 0;
  display: grid; gap: var(--s4); grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 760px) { .credstrip ul { grid-template-columns: repeat(4, 1fr); gap: var(--s5); } }
.credstrip li { display: flex; align-items: flex-start; gap: .7rem; }
.credstrip svg { width: 26px; height: 26px; flex: none; color: #ffd0dc; }
.credstrip b { display: block; font-family: var(--font-head); font-size: 1.02rem; font-weight: 600; color: #fff; line-height: 1.2; }
.credstrip span { font-size: .82rem; color: var(--on-navy-soft); }

/* -------------------------------------------------------------- sections -- */
section.band { padding: var(--s7) 0; }
@media (min-width: 760px) { section.band { padding: var(--s9) 0; } }
section.band.soft { background: var(--bg-soft); }
section.band.navy { background: var(--navy); color: #fff; }
section.band.navy h2, section.band.navy h3 { color: #fff; }
section.band.navy p { color: var(--on-navy-soft); }

/* The one red FIELD on the site. Until now Old Glory Red only ever appeared as
   a 2-3px accent - an eyebrow, a tick, a border, a button - which meant the
   section actually asking for the work looked identical to every other band.
   Reserving a full red ground for the closing CTA, and using it exactly once,
   makes it read as the end of the argument rather than as one more stripe. */
section.band.red { background: var(--red); color: #fff; }
section.band.red h2 { color: #fff; }
section.band.red p, section.band.red .lead { color: #ffe0e7; }
section.band.red .vet { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.45); color: #fff; }
section.band.red :focus-visible { outline-color: #fff; }
/* White fill on red: 6.70:1, and the only button on the page that inverts. */
section.band.red .btn-invert { background: #fff; color: var(--red); }
section.band.red .btn-invert:hover { background: #ffe0e7; }

.section-head { max-width: 44rem; margin: 0 auto var(--s6); text-align: center; }
/* 44rem is right for the h2 (~34 characters at 41.6px) but too wide for the
   paragraph under it, which ran to 86 characters — over the 80 ceiling, and
   centred, which makes a long measure harder still. 34rem = 67 characters. */
.section-head p { color: var(--ink-soft); margin: 0 auto; font-size: 1.075rem; max-width: 34rem; }
section.band.navy .section-head p { color: var(--on-navy-soft); }

.grid { display: grid; gap: var(--s4); }
@media (min-width: 640px) { .grid { gap: var(--s5); } }
.grid.c2 { grid-template-columns: 1fr; }
.grid.c3 { grid-template-columns: 1fr; }
@media (min-width: 620px) { .grid.c2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .grid.c3 { grid-template-columns: repeat(3, 1fr); } }

/* Reviews are running prose, not card blurbs. Three columns inside an 1180px
   wrap tops out at ~40 characters per line and collapses to ~26 at 900px -
   under the readable floor at every width, so raising the breakpoint cannot
   fix it. The column count is the problem. Two columns gives ~64 characters.
   :has() scopes this to reviews, leaving the 3-up card grids on services,
   about and schedule untouched; without :has() it degrades to today. */
@media (min-width: 620px) {
  .grid.c3:has(figure.review) { grid-template-columns: repeat(2, 1fr); }
}

.card {
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--s5); box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
/* Only lift cards that actually go somewhere. Most `.card`s on this site are
   static statements - three credentials, three service descriptions - and
   lifting them on hover promises a click that never happens. :has() scopes the
   affordance to cards containing a link; browsers without it simply show no
   hover, which is the correct failure. */
.card:has(a):hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: #cfdae7; }
.card h3 { color: var(--navy); margin-bottom: .35rem; }
.card p { color: var(--ink-soft); margin: 0; font-size: .975rem; }
.card .num {
  display: inline-block; margin-bottom: var(--s3);
  font-size: .72rem; font-weight: 700; letter-spacing: .11em; text-transform: uppercase; color: var(--red);
}
.card .ico { width: 34px; height: 34px; color: var(--red); margin-bottom: var(--s3); }

/* ---------------------------------------------------------------- portrait -- */
/* The owner's photograph on the About page. Held in a plain rectangle with a
   navy rule under it - a record, not a social avatar. */
.portrait { margin: 0; }
.portrait img {
  width: 100%; height: auto; display: block;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.portrait figcaption {
  margin-top: var(--s4); padding-top: var(--s3);
  border-top: 3px solid var(--navy);
}
.portrait figcaption b {
  display: block; font-family: var(--font-head); font-size: 1.25rem;
  font-weight: 600; color: var(--navy); line-height: 1.2;
}
.portrait figcaption span { font-size: .9375rem; color: var(--ink-soft); }

/* ------------------------------------------------------------------ split -- */
.split { display: grid; gap: var(--s5); align-items: center; }
@media (min-width: 860px) { .split { grid-template-columns: 1fr 1fr; gap: var(--s7); } }
.split img { border-radius: var(--radius); box-shadow: var(--shadow); width: 100%; }
.split.flip > :first-child { order: 2; }
@media (max-width: 859px) { .split.flip > :first-child { order: 0; } }

/* Review styling lives under "the record" near the end of this file. The
   de-boxed-card treatment that sat here was replaced wholesale; leaving it
   would have meant two competing definitions of figure.review. */

/* ----------------------------------------------------------------- areas -- */
.areas { display: flex; flex-wrap: wrap; gap: .5rem; list-style: none; padding: 0; margin: 0; justify-content: center; }
.areas li {
  background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.24);
  border-radius: 999px; padding: .45rem 1rem; font-size: .93rem; color: #fff;
}

/* ----------------------------------------------------------------- steps -- */
.steps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s5); counter-reset: step; }
@media (min-width: 860px) { .steps { grid-template-columns: repeat(3, 1fr); gap: var(--s6); } }
.steps li { position: relative; padding-top: var(--s5); counter-increment: step; }
.steps li::before {
  content: counter(step, decimal-leading-zero);
  position: absolute; top: 0; left: 0;
  font-family: var(--font-head); font-size: 2.4rem; font-weight: 600; line-height: 1;
  color: var(--red); opacity: .28;
}
.steps h3 { color: var(--navy); margin-bottom: .3rem; }
.steps p { color: var(--ink-soft); margin: 0; font-size: .975rem; }

/* --------------------------------------------------------------- checks --- */
ul.checks { list-style: none; padding: 0; margin: 0; display: grid; gap: .6rem; }
@media (min-width: 620px) { ul.checks { grid-template-columns: repeat(2, 1fr); gap: .6rem var(--s5); } }
ul.checks li { position: relative; padding-left: 1.7rem; font-size: .975rem; }
ul.checks li::before {
  content: ""; position: absolute; left: 0; top: .5em;
  width: .72rem; height: .4rem;
  border-left: 2.5px solid var(--red); border-bottom: 2.5px solid var(--red);
  transform: rotate(-45deg);
}

/* ================================================================ CUTAWAY ==
   The signature element: a blueprint cross-section of a house where each
   inspected system can be selected to highlight it in the drawing.

   Built on radio inputs and :checked — no JavaScript at all. Radios give
   keyboard arrow-key navigation, screen-reader group semantics and browser
   state management for free, which a div-and-click implementation would all
   have to reimplement badly.

   ponytail: eight fixed systems, hard-coded. A data-driven version would need
   JS and a build step to serve eight items that change roughly never.
   ========================================================================= */
.cutaway { background: var(--navy-ink); color: #fff; overflow: hidden; }
.cutaway .section-head p { color: var(--on-navy-soft); }
.cutaway h2 { color: #fff; }

/* A <fieldset> so the eight radios are announced as one named group. Its
   default border/padding are reset, and position:relative anchors the
   visually-hidden radios so they cannot widen the page. */
.cut-root { position: relative; border: 0; margin: 0; padding: 0; min-width: 0; }

.cut-layout { display: grid; gap: var(--s5); }

/* PHONE ORDER: chips, then the drawing, then the detail panel.
   The markup nests the chips and the panel inside one wrapper div, so ordering
   the layout's direct children could only ever move those two together. An
   earlier pass flipped the wrapper above the drawing to stop the visitor
   meeting a blueprint with no sign anything was tappable — but that put the
   whole detail panel BETWEEN the chips and the drawing they control. Tapping a
   chip changed a picture that was a full checklist further down the page, so
   on a phone the control and its response were never on screen together.
   `display: contents` dissolves the wrapper for the width where it is in the
   way, promoting the chips and the panel to grid items in their own right so
   all three can be sequenced. The wrapper is a plain div with no role, which is
   what makes that safe — there is no semantics for it to drop. */
@media (max-width: 979px) {
  .cut-layout > div:not(.cut-stage) { display: contents; }
  .cut-layout .cut-chips { order: 1; }
  .cut-layout .cut-stage { order: 2; }
  .cut-layout .cut-panel { order: 3; }
  /* The drawing is the thing being answered, so it gets to sit tight under the
     controls rather than a full gap away. */
  .cut-layout .cut-stage { margin-top: calc(var(--s5) * -0.35); }
}

/* Desktop: drawing left, controls right, both in view at once — the wrapper
   comes back so the chips and panel share the right column. */
@media (min-width: 980px) {
  .cut-layout { grid-template-columns: 1.35fr 1fr; gap: var(--s7); align-items: center; }
}

.cut-stage {
  position: relative; border-radius: var(--radius);
  background:
    linear-gradient(rgba(255,255,255,.045) 1px, transparent 1px) 0 0 / 100% 28px,
    linear-gradient(90deg, rgba(255,255,255,.045) 1px, transparent 1px) 0 0 / 28px 100%,
    var(--navy-deep);
  border: 1px solid rgba(255,255,255,.14);
  padding: var(--s4);
}
.cut-stage svg { width: 100%; height: auto; display: block; }

/* Drawing defaults: thin cool lines, like a real blueprint. */
.cut-stage .struct { fill: none; stroke: rgba(180,208,240,.55); stroke-width: 2; stroke-linejoin: round; }
.cut-stage .fillsoft { fill: rgba(140,180,230,.07); stroke: none; }
.cut-stage .hatch { stroke: rgba(180,208,240,.22); stroke-width: 1.2; }
.cut-stage .ground { stroke: rgba(180,208,240,.8); stroke-width: 2.5; }
.cut-stage .lbl { fill: rgba(200,222,246,.75); font-family: var(--font-body); font-size: 13px; font-weight: 600; letter-spacing: .1em; }

/* Every highlightable zone is dimmed until its radio is checked. */
.zone { opacity: 0; transition: opacity 260ms var(--ease); }
.zone .hl-fill { fill: rgba(179,25,66,.30); stroke: none; }
.zone .hl-line { fill: none; stroke: #ff5b83; stroke-width: 3.5; stroke-linejoin: round; stroke-linecap: round; }
.zone .hl-dot  { fill: #ff5b83; }

/* The radios themselves are hidden but focusable — never display:none, which
   would remove them from the tab order entirely. */
.cut-input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }

/* Selection wiring. Each radio lights its zone and its own chip. */
#z-roof:checked      ~ .cut-layout .zone-roof,
#z-exterior:checked  ~ .cut-layout .zone-exterior,
#z-structure:checked ~ .cut-layout .zone-structure,
#z-electrical:checked ~ .cut-layout .zone-electrical,
#z-plumbing:checked  ~ .cut-layout .zone-plumbing,
#z-hvac:checked      ~ .cut-layout .zone-hvac,
#z-interior:checked  ~ .cut-layout .zone-interior,
#z-thermal:checked   ~ .cut-layout .zone-thermal { opacity: 1; }

/* Detail panels: only the selected one is in the flow. */
.cut-detail { display: none; }
#z-roof:checked      ~ .cut-layout .d-roof,
#z-exterior:checked  ~ .cut-layout .d-exterior,
#z-structure:checked ~ .cut-layout .d-structure,
#z-electrical:checked ~ .cut-layout .d-electrical,
#z-plumbing:checked  ~ .cut-layout .d-plumbing,
#z-hvac:checked      ~ .cut-layout .d-hvac,
#z-interior:checked  ~ .cut-layout .d-interior,
#z-thermal:checked   ~ .cut-layout .d-thermal { display: block; }

.cut-chips { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: var(--s5); }
.cut-chips label {
  display: inline-flex; align-items: center; gap: .45rem;
  min-height: 44px; padding: .5rem .9rem;
  border: 1px solid rgba(255,255,255,.26); border-radius: 999px;
  background: rgba(255,255,255,.06); color: #dce8f6;
  font-size: .9rem; font-weight: 600; cursor: pointer;
  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.cut-chips label:hover { background: rgba(255,255,255,.14); color: #fff; }
.cut-chips label svg { width: 17px; height: 17px; flex: none; }

#z-roof:checked      ~ .cut-layout label[for="z-roof"],
#z-exterior:checked  ~ .cut-layout label[for="z-exterior"],
#z-structure:checked ~ .cut-layout label[for="z-structure"],
#z-electrical:checked ~ .cut-layout label[for="z-electrical"],
#z-plumbing:checked  ~ .cut-layout label[for="z-plumbing"],
#z-hvac:checked      ~ .cut-layout label[for="z-hvac"],
#z-interior:checked  ~ .cut-layout label[for="z-interior"],
#z-thermal:checked   ~ .cut-layout label[for="z-thermal"] {
  background: var(--red); border-color: var(--red); color: #fff;
}
/* Keyboard focus has to be visible on the chip, since the real radio is hidden. */
#z-roof:focus-visible      ~ .cut-layout label[for="z-roof"],
#z-exterior:focus-visible  ~ .cut-layout label[for="z-exterior"],
#z-structure:focus-visible ~ .cut-layout label[for="z-structure"],
#z-electrical:focus-visible ~ .cut-layout label[for="z-electrical"],
#z-plumbing:focus-visible  ~ .cut-layout label[for="z-plumbing"],
#z-hvac:focus-visible      ~ .cut-layout label[for="z-hvac"],
#z-interior:focus-visible  ~ .cut-layout label[for="z-interior"],
#z-thermal:focus-visible   ~ .cut-layout label[for="z-thermal"] {
  outline: 3px solid #ffd0dc; outline-offset: 2px;
}

.cut-panel {
  border: 1px solid rgba(255,255,255,.16); border-radius: var(--radius);
  background: rgba(255,255,255,.05); padding: var(--s5);
  min-height: 15rem;
}
.cut-panel h3 { font-size: 1.4rem; margin-bottom: .4rem; color: #fff; }
.cut-panel p { color: var(--on-navy-soft); font-size: .975rem; margin-bottom: var(--s4); }
.cut-panel ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .45rem; }
.cut-panel li { position: relative; padding-left: 1.5rem; font-size: .93rem; color: #dce8f6; }
.cut-panel li::before {
  content: ""; position: absolute; left: 0; top: .5em;
  width: .65rem; height: .36rem;
  border-left: 2.5px solid #ff5b83; border-bottom: 2.5px solid #ff5b83;
  transform: rotate(-45deg);
}

/* ------------------------------------------------------------ report CTA -- */
.reportbox {
  display: grid; gap: var(--s5); align-items: center;
  background: var(--navy); color: #fff; border-radius: var(--radius);
  padding: var(--s5); box-shadow: var(--shadow-lg);
}
.reportbox h2 { color: #fff; }
.reportbox p { color: var(--on-navy-soft); }
.reportbox .eyebrow { color: #ff9eb4; }
@media (min-width: 860px) { .reportbox { grid-template-columns: 1.15fr 1fr; padding: var(--s7); gap: var(--s7); } }

/* Four verifiable facts instead of a decorative image. Each one is checkable
   by opening the sample itself, which is the whole point of the section. */
.reportfacts { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s4); }
@media (min-width: 480px) { .reportfacts { grid-template-columns: repeat(2, 1fr); gap: var(--s4) var(--s5); } }
.reportfacts li {
  padding: var(--s4); border-radius: var(--radius-sm);
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.16);
}
.reportfacts b {
  display: block; font-family: var(--font-head); font-size: 1.5rem; font-weight: 600;
  line-height: 1.1; color: #fff; margin-bottom: .2rem;
}
.reportfacts span { font-size: .88rem; color: var(--on-navy-soft); }

/* --------------------------------------------------------- mobile action -- */
/* Fixed bottom bar. A buyer standing in a house with a contingency clock does
   not scroll looking for a number. Phone-only: on desktop the header CTA and
   the call bar already cover it. env(safe-area-inset-bottom) keeps it clear of
   the iPhone home indicator. */
.actionbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: flex; gap: .5rem;
  padding: .55rem .75rem calc(.55rem + env(safe-area-inset-bottom, 0px));
  background: rgba(255,255,255,.97);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--line);
  box-shadow: 0 -4px 20px rgba(10,49,97,.10);
}
.actionbar .btn { flex: 1; min-height: 50px; font-size: 1rem; }
@media (min-width: 800px) { .actionbar { display: none; } }

/* ------------------------------------------------- inline PDF viewer (reports)
   The report is read in the page, not downloaded on faith. iOS Safari and
   several Android browsers refuse to render a PDF in an iframe and show a
   blank box, so the <object> carries real fallback content — the buttons
   inside only appear when embedding fails. */
.pdfbox {
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  background: var(--bg-soft); box-shadow: var(--shadow);
}
.pdfbox object, .pdfbox iframe { display: block; width: 100%; height: clamp(420px, 78vh, 900px); border: 0; }
.pdfbox .fallback { padding: var(--s6); text-align: center; }
.pdfbox .fallback p { color: var(--ink-soft); }
.pdfbar {
  display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; justify-content: space-between;
  padding: var(--s3) var(--s4); background: #fff; border-bottom: 1px solid var(--line);
}
.pdfbar .meta { font-size: .88rem; color: var(--ink-soft); }
.pdfbar .acts { display: flex; gap: var(--s2); flex-wrap: wrap; }
.pdfbar .acts a { font-size: .9rem; padding: .5rem .9rem; min-height: 44px; }

/* ------------------------------------------------------------------- FAQ -- */
/* A list of questions should look like a list, not like a stack of cards. */
details.faq {
  background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0;
  padding: var(--s4) 0; margin-bottom: 0;
}
details.faq summary {
  cursor: pointer; font-weight: 650; color: var(--navy); list-style: none;
  min-height: 32px; display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
}
details.faq summary::-webkit-details-marker { display: none; }
details.faq summary::after { content: "+"; color: var(--red); font-weight: 800; font-size: 1.3rem; line-height: 1; }
details.faq[open] summary::after { content: "\2212"; }
details.faq p { margin: var(--s3) 0 0; color: var(--ink-soft); }

/* ---------------------------------------------------------------- footer -- */
footer.site { background: var(--navy-ink); color: #a9bdd4; padding: var(--s7) 0 var(--s5); font-size: .95rem; }
footer.site a { color: #fff; text-decoration: none; }
footer.site a:hover { text-decoration: underline; }
footer.site .cols { display: grid; gap: var(--s5); }
@media (min-width: 620px) { footer.site .cols { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 960px) { footer.site .cols { grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: var(--s6); } }
footer.site h3 { color: #fff; font-family: var(--font-body); font-size: .82rem; font-weight: 700; letter-spacing: .11em; text-transform: uppercase; margin: 0 0 var(--s3); }
footer.site ul { list-style: none; margin: 0; padding: 0; }
footer.site li { margin-bottom: .45rem; }
footer.site li a { display: inline-flex; align-items: center; min-height: 32px; }
footer.site .legal {
  border-top: 1px solid rgba(255,255,255,.13); margin-top: var(--s6); padding-top: var(--s4);
  font-size: .84rem; color: #7d92ab;
}

/* -------------------------------------------------------- reveal on scroll */
/* Subtle tier: 12px rise, 350ms. Pure CSS via animation-timeline where it is
   supported; everywhere else the element simply starts visible, which is the
   correct failure mode — never hide content behind an effect that might not
   run. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 42%;
    }
  }
}
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .card:hover, .btn:hover { transform: none; }

  /* A full-bleed looping video is precisely the unrequested motion this
     setting exists to stop - it can trigger nausea and migraine for people
     with vestibular disorders. Swap in the still frame; the hero looks
     identical, it simply holds. */
  .hero .herovid { display: none; }
  .hero .heroposter { display: block; }
}

/* ------------------------------------------------------------------ print */
@media print {
  /* This hid `.navwrap`, a class that no longer exists since the nav was
     rebuilt on a checkbox — so every printed page carried a navigation bar. */
  .callbar, .actionbar, .navbtn, .mainnav, .hero .photo, .hero .scrim,
  .hero .herovid, .hero .heroposter { display: none; }
  body { padding-bottom: 0; color: #000; }
  .hero { background: #fff; color: #000; }
  /* The print rule reset the headline and lead but not the proof line or the
     veteran mark, both of which are near-white for the navy hero. With the
     photo and scrim hidden they printed white-on-white - invisible. The
     licence number is the one fact the print path exists to carry. The navy
     and red bands have the same problem. */
  .hero h1, .hero .lead, .hero .proof, .hero .proof span { color: #000; }
  .hero .proof { border-top-color: #999; }
  .hero .proof svg { color: #000; }
  .vet { color: #000; background: none; border-color: #999; }
  section.band.navy, section.band.navy p, section.band.navy h2,
  section.band.red, section.band.red p, section.band.red h2,
  .credstrip b, .credstrip span, .credstrip svg,
  .cutaway, .cut-panel p, .cut-panel li, .reportbox, .reportbox p { color: #000; }
  section.band.navy, section.band.red, .credstrip, .cutaway, .reportbox { background: #fff; }
  .areas li { color: #000; border-color: #999; background: none; }
}


/* ============================================================ MOTION LAYER ==
   Micro-interaction pass. Everything above this line is untouched.
   Same rules as the rest of the sheet: no JS, no build step.

   Three constraints govern every rule below:

     1. NOTHING IS HIDDEN BY DEFAULT. Every animated element's resting state is
        its final, visible state. If an animation never runs - old browser,
        crawler, reduce-motion - the page is simply the page.
     2. TRANSFORM / OPACITY / CLIP-PATH / STROKE-DASHOFFSET ONLY. The one
        exception is outline, which is defined never to affect layout and so
        cannot reflow.
     3. 150-300ms for anything a person triggered. The two exceptions are the
        cutaway line-draw (360ms) and its fill bloom (420ms), which are the
        signature element and are meant to be watched.
   ========================================================================= */


/* ================================================ 1. THE CUTAWAY (signature)
   Today the whole zone cross-fades: it reads as a slide change. What it should
   read as is a system being energised - the run drawing itself along the
   blueprint, then the terminations lighting.

   Wired without eight duplicated selector lists: each SVG child declares WHICH
   animation it wants (--anim) but nothing runs, because --anim-run is
   undefined and the shorthand falls back to `none`. One selector list copies
   --anim into --anim-run for the live zone. Setting animation-name from `none`
   to a real name is what RESTARTS an animation, so re-selecting a zone you
   already viewed replays it. */
@media (prefers-reduced-motion: no-preference) {
  .zone .hl-line { --anim: zone-draw;  --anim-dur: 360ms; --anim-del: 0ms;   }
  .zone .hl-fill { --anim: zone-bloom; --anim-dur: 420ms; --anim-del: 40ms;  }
  .zone .hl-dot  { --anim: zone-pop;   --anim-dur: 220ms; --anim-del: 200ms; }
  .zone-thermal ellipse { --anim: zone-bloom; --anim-dur: 420ms; --anim-del: 40ms; }

  .zone * {
    animation: var(--anim-run, none) var(--anim-dur, 240ms) var(--ease) var(--anim-del, 0ms) both;
  }
  .zone .hl-dot { transform-box: fill-box; transform-origin: center; }
}

/* Resting alpha nudged .30 -> .38 so that multiplying by the .78 fill-opacity
   the bloom settles on leaves the static appearance unchanged. */
.zone .hl-fill { fill: rgba(179,25,66,.38); fill-opacity: .78; }

#z-roof:checked       ~ .cut-layout :is(.zone-roof *,       label[for="z-roof"]),
#z-exterior:checked   ~ .cut-layout :is(.zone-exterior *,   label[for="z-exterior"]),
#z-structure:checked  ~ .cut-layout :is(.zone-structure *,  label[for="z-structure"]),
#z-electrical:checked ~ .cut-layout :is(.zone-electrical *, label[for="z-electrical"]),
#z-plumbing:checked   ~ .cut-layout :is(.zone-plumbing *,   label[for="z-plumbing"]),
#z-hvac:checked       ~ .cut-layout :is(.zone-hvac *,       label[for="z-hvac"]),
#z-interior:checked   ~ .cut-layout :is(.zone-interior *,   label[for="z-interior"]),
#z-thermal:checked    ~ .cut-layout :is(.zone-thermal *,    label[for="z-thermal"]) {
  --anim-run: var(--anim);   /* undefined --anim (a chip) => stays `none` */
  --chip: 1;                 /* inert on the SVG children */
  background-color: rgba(255,255,255,.06);  /* hand the red to the ::before wipe */
}

/* ponytail: the dash pattern is a flat 800 user units rather than measured per
   path. CEILING: the longest highlight path is ~680 units; anything over 800
   would show a repeat gap mid-draw. UPGRADE PATH: add pathLength="1" to every
   .hl-line and change 800 -> 1 in both keyframes. Welcome side effect of the
   flat value: short branch runs finish before the long roof line, which is how
   a real run would light. */
@keyframes zone-draw {
  from { stroke-dasharray: 800; stroke-dashoffset: 800; }
  to   { stroke-dasharray: 800; stroke-dashoffset: 0; }
}
/* A single bloom that overshoots and settles - a needle finding its reading.
   Deliberately NOT an infinite pulse: looping motion on a page someone is
   reading is restless, and is the first thing a tradesman reads as decoration. */
@keyframes zone-bloom {
  from { fill-opacity: 0; }
  55%  { fill-opacity: 1; }
  to   { fill-opacity: .78; }
}
@keyframes zone-pop {
  from { opacity: 0; transform: scale(.4); }
  to   { opacity: 1; transform: none; }
}

/* The detail panel flips display:none -> block. An element that gains a box
   starts its animations from zero, so the panel animates on every selection
   with no extra state and no JS. */
@media (prefers-reduced-motion: no-preference) {
  .cut-detail    { animation: panel-in 220ms var(--ease) both; }
  .cut-detail li { animation: panel-line 240ms var(--ease) both; }
  .cut-detail li:nth-child(1) { animation-delay:  70ms; }
  .cut-detail li:nth-child(2) { animation-delay:  98ms; }
  .cut-detail li:nth-child(3) { animation-delay: 126ms; }
  .cut-detail li:nth-child(4) { animation-delay: 154ms; }
  .cut-detail li:nth-child(5) { animation-delay: 182ms; }
  .cut-detail li:nth-child(n+6) { animation-delay: 210ms; }
}
@keyframes panel-in   { from { opacity: 0; transform: translateY(6px); }  to { opacity: 1; transform: none; } }
/* Items enter from the left, against the drawing, so the eye is pulled from
   the blueprint to the list rather than dropping down the page. */
@keyframes panel-line { from { opacity: 0; transform: translateX(-7px); } to { opacity: 1; transform: none; } }


/* ==================================================== THE CHIPS (wipe) =====
   A wipe from the leading edge costs the same as the existing cross-fade and
   says something it does not: this control filled. */
.cut-chips label { position: relative; isolation: isolate; }
.cut-chips label::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; background: var(--red);
  clip-path: inset(0 calc(100% - var(--chip, 0) * 100%) 0 0);
  transition: clip-path 240ms var(--ease);
}
/* Restated in full: a second `transition` declaration REPLACES the first. */
.cut-chips label {
  outline: 3px solid transparent; outline-offset: 6px;
  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              outline-color 120ms var(--ease), outline-offset 180ms var(--ease);
}
.cut-chips label svg { transition: transform 200ms var(--ease); }
.cut-chips label:hover svg { transform: translateY(-1px); }


/* ================================================== FOCUS THAT IS AIMED =====
   A ring that pops into existence at final size looks like a browser default.
   A transparent 3px outline held 6px out, then coloured and pulled to 2px on
   focus, makes the ring arrive - it closes onto the control. Nothing moves:
   outline never affects layout, which is why it is the one non-transform
   property animated in this file. */
a, .btn, summary, .navbtn, [tabindex] {
  outline: 3px solid transparent; outline-offset: 6px;
  transition: outline-color 120ms var(--ease), outline-offset 180ms var(--ease);
}
.btn {
  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease),
              transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              outline-color 120ms var(--ease), outline-offset 180ms var(--ease);
}
/* Re-asserted AFTER .btn: both are (0,1,0), so source order decides. Without
   this the transparent outline wins on buttons and the focus ring vanishes. */
:focus-visible { outline: 3px solid var(--red); outline-offset: 2px; border-radius: 3px; }

/* The skip link is the first thing a keyboard user meets, and it animated
   `left` from -9999px - a forbidden property and a 9,999px journey. */
.skip { left: 0; transform: translateY(-120%); transition: transform 160ms var(--ease); }
.skip:focus { transform: none; }


/* ========================================= LINKS, BUTTONS, THE PHONE =======
   Underlines grow from the leading edge on a scaled pseudo-element, so nothing
   about the text box changes. Deliberately NOT on prose links: a pseudo
   underline breaks apart across a line wrap. */
.callbar a, footer.site li a, .mainnav a { position: relative; }
.callbar a:hover, footer.site a:hover { text-decoration: none; }
.callbar a::after, footer.site li a::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1px;
  background: currentColor; pointer-events: none;
  transform: translateY(.72em) scaleX(0); transform-origin: left center;
  transition: transform 200ms var(--ease);
}
.callbar a:hover::after,  .callbar a:focus-visible::after,
footer.site li a:hover::after, footer.site li a:focus-visible::after {
  transform: translateY(.72em) scaleX(1);
}
main p a { text-decoration-thickness: 1px; transition: text-decoration-color 180ms var(--ease); }
main p a:hover { text-decoration-color: var(--red); }

/* Icon nudge - 2px, once, held. Forward motion on a forward action. */
.btn svg { transition: transform 180ms var(--ease); }
.btn:hover svg { transform: translateX(2px); }

/* The phone. A ringing wobble is a toy; this is a handset lifted off the
   cradle - one tilt, held, no oscillation. Every call button is a[href^=tel:]. */
.btn[href^="tel:"]:hover svg,
.btn[href^="tel:"]:focus-visible svg { transform: translateY(-1px) rotate(-9deg); }

.btn:active { transform: scale(.985); }


/* ================================================= CARDS AND REVIEWS =======
   The 3px lift is the most template-looking thing on the page - every site
   does it and none of them mean anything by it. Replaced with the mark the
   site already owns: the flag stripe, drawn across the top edge on approach. */
.card { position: relative; }
.card::after {
  content: ""; position: absolute; top: -1px; left: -1px; right: -1px; height: 3px;
  border-radius: var(--radius) var(--radius) 0 0;
  background: linear-gradient(90deg, var(--red) 0 34%, #fff 34% 66%, var(--navy) 66% 100%);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 240ms var(--ease);
}
.card:has(a):hover::after, .card:focus-within::after { transform: scaleX(1); }
.card:has(a):hover { transform: translateY(-2px); }

/* The review hover-fill that lived here is gone with the card treatment it
   belonged to. ::before on figure.review is now the entry INDEX — see "the
   record" below — and the two cannot share the pseudo-element. */


/* ================================================== HEADER ON SCROLL =======
   The header is sticky with a blur; what it never does is admit the page
   moved. A shadow arriving over the first 90px is the whole effect. Scroll
   driven, so no JS and no scroll listener. @supports-guarded; where it is
   missing the header is exactly what it is today. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    header.site::after {
      content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 14px;
      pointer-events: none; opacity: 0;
      background: linear-gradient(rgba(10,49,97,.14), rgba(10,49,97,0));
      animation: hdr-lift linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 90px;
    }
  }
}
@keyframes hdr-lift { from { opacity: 0; } to { opacity: 1; } }

/* Considered and rejected: a scroll-progress bar. It reads as a blog, implies
   the page is long, and is the kind of decoration this client would clock
   instantly. Left out on purpose, not by oversight. */


/* =============================================== FIRST-PAINT ENTRANCE ======
   @starting-style is the only entrance technique safe here: the resting state
   IS the final state, and it supplies a `from` value that an unsupporting
   browser simply never reads. There is no opacity:0 sitting in the cascade for
   a crawler or reader-mode to trip over.

   The h1 gets ZERO delay on purpose - it is a candidate LCP element, and an
   element at opacity 0 does not count as painted. */
@media (prefers-reduced-motion: no-preference) {
  .hero .inner > *, .enter > * {
    transition: opacity 260ms var(--ease), transform 260ms var(--ease);
  }
  .hero .inner > *:nth-child(3), .enter > *:nth-child(3) { transition-delay:  60ms; }
  .hero .inner > *:nth-child(4), .enter > *:nth-child(4) { transition-delay: 110ms; }
  .hero .inner > *:nth-child(5), .enter > *:nth-child(5) { transition-delay: 150ms; }

  @starting-style {
    .hero .inner > *, .enter > * { opacity: 0; transform: translateY(8px); }
  }
}


/* ====================================== FAQ AND THE MOBILE MENU ============
   Both use the same display:none -> block property the cutaway panel does: no
   box until opened, so animations start on reveal with no extra state. */

/* The marker swaps glyph + to -, which cannot animate. A "+" rotated 45 IS an
   "x", and rotation animates. Overrides the [open] content swap above. */
details.faq summary::after { display: inline-block; transition: transform 200ms var(--ease); }
details.faq[open] summary::after { content: "+"; transform: rotate(45deg); }

@media (prefers-reduced-motion: no-preference) {
  details.faq[open] > :not(summary) { animation: faq-in 200ms var(--ease) both; }

  @media (max-width: 959px) {
    .navtoggle:checked ~ .mainnav a { animation: menu-item 200ms var(--ease) both; }
    .navtoggle:checked ~ .mainnav li:nth-child(1) a { animation-delay:  20ms; }
    .navtoggle:checked ~ .mainnav li:nth-child(2) a { animation-delay:  44ms; }
    .navtoggle:checked ~ .mainnav li:nth-child(3) a { animation-delay:  68ms; }
    .navtoggle:checked ~ .mainnav li:nth-child(4) a { animation-delay:  92ms; }
    .navtoggle:checked ~ .mainnav li:nth-child(5) a { animation-delay: 116ms; }
    .navtoggle:checked ~ .mainnav li:nth-child(n+6) a { animation-delay: 140ms; }
  }
}
@keyframes faq-in    { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@keyframes menu-item { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: no-preference) {
  .navtoggle:checked ~ .navbtn .x { animation: mark-in 180ms var(--ease) both; }
}
@keyframes mark-in { from { opacity: 0; transform: rotate(-45deg); } to { opacity: 1; transform: none; } }


/* ============================================ CROSS-PAGE TRANSITIONS =======
   Eight static pages sharing a header, call bar and footer currently blink
   white between navigations. Naming those elements holds them still while only
   the content cross-fades, so the site behaves like one document.
   220ms cross-fade only - explicitly NOT a slide: a page that slides is a
   phone app pretending, and it delays content a buyer on a clock came for.
   Unsupporting browsers just navigate; there is no fallback to build. */
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 220ms; }
header.site  { view-transition-name: site-header; }
.callbar     { view-transition-name: call-bar; }
.actionbar   { view-transition-name: action-bar; }
footer.site  { view-transition-name: site-footer; }


/* =================================================== REDUCED MOTION ========
   MUST STAY LAST. The earlier block zeroes durations but NOT delays, and a
   zeroed duration with a 210ms delay still means 210ms of invisible content
   for the people who least want to be shown a trick. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-delay: 0s !important;
    transition-delay: 0s !important;
  }

  /* Nothing translates, scales, rotates or wipes. States still change - colour,
     the ring, the underline, the chip fill - they arrive rather than travel.
     Feedback is not the thing being reduced. */
  .card:hover, .card:active, .btn:hover, .btn:active,
  .btn:hover svg, .btn[href^="tel:"]:hover svg,
  .cut-chips label:hover svg { transform: none; }
  .card::after                    { transform: scaleX(1); }
  /* No figure.review::before rule here on purpose: the entry index is drawn by
     .idx::before and must never be scaled to nothing, or the numbers vanish for
     anyone who asked for reduced motion. */
  .callbar a::after,
  footer.site li a::after         { transform: translateY(.72em) scaleX(0); }
  .callbar a:hover::after,
  footer.site li a:hover::after   { transform: translateY(.72em) scaleX(1); }

  /* The cutaway holds its final reading: lines solid, fills at rest. */
  .zone * { animation: none !important; }
  .zone .hl-line { stroke-dasharray: none; stroke-dashoffset: 0; }
  .zone .hl-dot  { opacity: 1; transform: none; }

  @view-transition { navigation: none; }
}

/* ------------------------------------------------------- touch targets -- */
/* The call-bar links render at 22px tall; "Text" is 27x22 and "Email" 35x22.
   WCAG 2.5.8 asks for 44px, and these are the most-tapped controls on the
   site — the audience is standing in a house on a contingency clock, on a
   phone. Measured, not guessed: see the overflow/tap-target sweep run at 320,
   390, 768 and 1024px.

   Two deliberate choices:
   - Two conditions, not one. (pointer:coarse) is the CORRECT axis — a tap
     target is a property of the input device, not the viewport, and it catches
     a touch laptop or a large tablet that no width query would. But a
     coarse-pointer rule cannot be exercised in a desktop browser, so on its
     own it is unverifiable on the machine this was written on. The width arm
     is set at 1024px so it spans phone AND tablet and can actually be
     measured. The cost is a call bar ~20px taller in a narrow desktop window,
     which is worth paying to have the rule provable rather than assumed.
   - min-height, not padding. Padding was tried first and landed the call-bar
     links at 41px and the footer links at 38px: the arithmetic depends on the
     inherited line-height, so it silently misses whenever that changes.
     min-height states the requirement instead of computing it, and
     inline-flex + align-items:center keeps the text optically centred, which
     is what the hover underline (an ::after positioned from the box's centre)
     is measured against. */
@media (max-width: 1024px), (hover: none) and (pointer: coarse) {
  .callbar a,
  footer.site li a,
  .linklist a,
  .score-split dd a {
    display: inline-flex; align-items: center;
    min-height: 44px;
  }
  .callbar a { padding-inline: .55rem; }
  /* Both of these already declare min-height:32px further up; this raises it
     rather than introducing a second sizing mechanism. */
  details.faq summary { min-height: 44px; }
}

/* ------------------------------------------------------------ the record -- */
/* Reviews are the evidence section of this site, so they are set as a RECORD of
   findings rather than as testimonial tiles. The generic pattern — a card, a
   quote glyph, a name, five stars, repeated eight times — flattens eight
   different pieces of proof into eight identical shapes and reads as marketing.
   These quotes each document a specific catch, and the layout says so: an index
   number, the finding, then the client's own words as the evidence beneath it.
   The register is borrowed from the cutaway, which is the site's other forensic
   element.

   The finding line is OUR framing and is typeset to look like it — small,
   letterspaced, navy, in the body face. The quotation is the client's and is
   set in the display serif at reading size. Nobody should be able to mistake
   which of the two the reviewer wrote. */

/* --- the aggregate score, as a figure rather than a sentence --- */
.score {
  display: grid; gap: var(--s5); align-items: end;
  margin: 0 0 var(--s6);
  padding-bottom: var(--s5);
  border-bottom: 1px solid var(--line);
}
@media (min-width: 820px) { .score { grid-template-columns: auto 1fr; gap: var(--s7); } }

 /* Baseline-aligned so the stars sit ON the numeral's baseline rather than
    floating beside it, and tight, so the two read as one lockup. */
.score-fig { margin: 0; display: flex; align-items: baseline; gap: .6rem; }
.score-fig b {
  font-family: var(--font-head);
  font-size: clamp(3.4rem, 9vw, 5rem);
  line-height: .85; color: var(--navy); font-weight: 600;
  letter-spacing: -.02em; font-variant-numeric: lining-nums tabular-nums;
}
.stars { display: inline-flex; gap: .18rem; color: var(--red); }
.stars svg { width: 1.35rem; height: 1.35rem; }

/* Three columns of provenance. A definition list because that is what it is —
   a platform and what it says — and because it keeps the label and the figure
   associated for a screen reader instead of relying on visual proximity. */
.score-split { display: grid; gap: var(--s4); margin: 0; }
@media (min-width: 560px) { .score-split { grid-template-columns: repeat(3, 1fr); gap: var(--s5); } }
.score-split > div { border-top: 2px solid var(--red); padding-top: var(--s3); }
.score-split dt {
  font-size: .7rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-faint); margin-bottom: .3rem;
}
.score-split dd { margin: 0; }
.score-split dd a { color: var(--navy); font-weight: 650; text-underline-offset: .18em; }
.score-split dd span { display: block; font-size: .85rem; color: var(--ink-soft); margin-top: .15rem; }

/* --- the rail --- */
/* Eighteen reviews as a continuous loop rather than a wall. In a 3-wide grid
   this section would be six rows on a desktop and eighteen stacked cards on a
   phone; it is one entry tall at every width and it brings itself to you.

   The loop is a marquee, not a scroller: the track holds the eighteen entries
   TWICE and translates by exactly -50%, so the moment it completes it is back
   where it began with the same entry under the same pixel. Duplication is what
   makes that seamless; the clone is aria-hidden so a screen reader reads
   eighteen, not thirty-six.

   Still no JavaScript. */
.reviewrail {
  overflow: hidden;   /* the marquee moves the track; the box itself does not scroll */
  /* Vertical room for the card shadows. overflow:hidden clips anything outside
     the box, and a 32px-blur shadow with no padding gets sliced flat across the
     top and bottom edges — which reads as a rendering bug, not as elevation. */
  padding-block: var(--s3) var(--s5);
  margin-top: var(--s4);
}

.rail-track { display: flex; width: max-content; }
.rail-half {
  counter-reset: rev;          /* both halves number 01-18, not 01-36 */
  display: flex;
  /* Each entry is as tall as its own quote, and they hang from a common centre
     line. Stretched to a shared height — the flex default — every card was as
     tall as the longest quote in the set, which left a four-line hole between a
     short quote and its attribution. A hole reads as something missing; ragged
     heights on a common axis read as what they are, quotes of different
     lengths. */
  align-items: center;
  gap: var(--s6);
  padding-right: var(--s6);    /* keeps the seam spacing identical to the gaps */
}
.rail-half > .review { width: min(84vw, 21.5rem); flex: none; }
/* The card width is a MEASURE, not a fraction of the screen: 21.5rem holds
   roughly 45 characters of the display serif, which is a comfortable line. It
   deliberately does not grow on a wide monitor — the rail sits in the page's
   1180px column, so a desktop shows three entries and the beginning of a
   fourth, and the fourth is what tells the reader there are more. */

@media (prefers-reduced-motion: no-preference) {
  /* 18 entries at ~21rem plus gaps is roughly 7000px; 210s puts it near 33px/s,
     which is slow enough to read a quote without chasing it. */
  .rail-track { animation: rail-loop 210s linear infinite; }

  /* The two ends, softened. overflow:hidden guillotines the entries at the left
     and right edges, and a card sliced vertically through the middle of a word
     reads as a rendering fault rather than as a carousel continuing off-screen.
     A horizontal mask fades both ends into the band instead, so an entry
     dissolves as it leaves and resolves as it arrives.

     The gradient runs left-to-right ONLY, so the card shadows above and below
     are untouched — the vertical breathing room set by padding-block above is
     still doing its job.

     Only while the rail is actually moving. Under reduced motion it is a
     hand-scrolled rail, where a fade would dim the last card exactly when the
     reader has scrolled to it deliberately; there is no CSS way to tell "more
     to come" from "that's the end", so the fade stays with the loop. */
  .reviewrail {
    --fade: clamp(1rem, 5vw, 6rem);
    -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
            mask-image: linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
  }
  /* PAUSE on hover and on focus. Content that moves for more than five seconds
     with no way to stop it fails WCAG 2.2.2 — and more plainly, a quote that
     slides away the moment you start reading it is worse than no quote. The rail
     is focusable so a keyboard user can stop it too, not only a mouse user. */
  .reviewrail:hover .rail-track,
  .reviewrail:active .rail-track,
  .reviewrail:focus-within .rail-track { animation-play-state: paused; }
  /* :active is there for touch. A phone has no hover, so without it the only
     way to stop the rail on the device most people will read this on is the
     OS-level reduced-motion setting. Touch and hold now pauses it. */
}

@keyframes rail-loop {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* Reduced motion: nothing moves on its own. The rail becomes the ordinary
   scroll-snap row it was before, which is fully usable — the loop is a
   convenience, never the only way to reach an entry. */
@media (prefers-reduced-motion: reduce) {
  .reviewrail {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--line) transparent;
  }
  .rail-track { animation: none; }
  .rail-half > .review { scroll-snap-align: start; }
  /* The duplicate would just be eighteen more entries to scroll past. */
  .rail-half[aria-hidden="true"] { display: none; }
}

/* Focus has to be visible — it is now also the pause control. */
.reviewrail:focus-visible { outline: 3px solid var(--red); outline-offset: 6px; border-radius: 2px; }
/* A mask clips to the BORDER box, and an outline is painted OUTSIDE it — so the
   edge fade above would erase this ring completely, and this ring is not
   decoration, it is how a keyboard user knows they have hold of the pause
   control. Pulled just inside the box, where the mask keeps it: the ring's top
   and bottom rules run the full width of the band and their ends fade out along
   with the entries. Only where the fade exists; under reduced motion the ring
   stays where it was. Must come after the rule above — same specificity. */
@media (prefers-reduced-motion: no-preference) {
  .reviewrail:focus-visible { outline-offset: -3px; }
}

/* --- one entry, as a raised card --- */
/* Built from the same tokens as `.card` elsewhere on the site — white ground,
   hairline border, --radius, --shadow — so this reads as the page's existing
   card and not as a second, nearly-identical component. The elevation is
   --shadow rather than --shadow-sm because these sit on the soft band, where a
   small shadow barely separates the card from its background. */
figure.review {
  counter-increment: rev;
  margin: 0;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--s5);
  display: flex; flex-direction: column;
  position: relative;
}

/* The index and the rating share one rule inside the card. */
figure.review .rev-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3);
  margin: 0 0 var(--s4);
  padding-bottom: .6rem;
  border-bottom: 1px solid var(--line);
}
figure.review .idx::before {
  content: counter(rev, decimal-leading-zero);
  font-size: .72rem; font-weight: 700; letter-spacing: .14em;
  color: var(--red); font-variant-numeric: tabular-nums;
}
/* Per-entry stars. NOT decoration and NOT an assumption: each of the eighteen
   was read off its source profile, where the Angi entries render "Rating: 5 out
   of 5 stars" beside the date shown in the caption and the Google entries a
   five-star row. Smaller than the aggregate row, so 5.0-across-332 stays the
   louder claim. */
figure.review .stars svg { width: .82rem; height: .82rem; }

/* Our framing of what this entry documents. Body face, small, letterspaced —
   deliberately unlike the quotation, so it never reads as the reviewer's words. */
figure.review .find {
  margin: 0 0 var(--s3);
  font-family: var(--font-body);
  font-size: .8rem; font-weight: 700; line-height: 1.35;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--navy);
  text-wrap: balance;
}

/* The client's words, in the display serif. This is the one place on the site
   where someone other than the business is speaking, and the face change is how
   the page says so. No flex-grow here: the card is only as tall as its own
   contents (see .rail-half), so growing the quote would only reopen the gap
   between it and the attribution. */
figure.review blockquote {
  margin: 0 0 var(--s4);
  font-family: var(--font-head);
  font-size: 1.02rem; line-height: 1.5; font-weight: 600;
  color: var(--ink);
}
/* Every quote opens with a left double-quote whose advance is .419em, so the
   first line sits visibly indented while the rest run flush. Safari hangs it
   natively; everywhere else a negative first-line indent does it. */
html { hanging-punctuation: first last; }
@supports not (hanging-punctuation: first) {
  figure.review blockquote { text-indent: -.42em; }
}

figure.review figcaption { font-size: .82rem; color: var(--ink-faint); }
figure.review figcaption b { display: block; color: var(--ink-soft); font-size: .89rem; font-weight: 650; }

/* --- mobile --- */
/* A card needs its padding more than it needs its width: at 390px the previous
   21rem card left ~300px of content, and taking the site's standard --s5 off
   each side of that is a 20-character measure. Narrower card, tighter padding,
   one notch down on the quote. */
@media (max-width: 560px) {
  .rail-half { gap: var(--s4); padding-right: var(--s4); }
  .rail-half > .review { width: min(86vw, 19.5rem); }
  figure.review { padding: var(--s4); }
  figure.review blockquote { font-size: .97rem; }
  figure.review .find { font-size: .76rem; }
}


