/* ===========================================================================
   GYM JUNKSTERS — src/assets/styles.css
   Round four. Codename CLEARANCE. Implements DESIGN-PLAN.md, which REPLACES
   DESIGN-SYSTEM.md. Where the two disagree, DESIGN-PLAN.md wins.

   THREE IDEAS CARRY THIS ROUND. Every rule in this file serves one of them:

     1. THE FOLD IS THE JOB.  The home page opens on a full-bleed photograph
        you drag to clear. Not a headline about hauling — the hauling, under
        your thumb. §08.
     2. THE PAGE CLEARS AS YOU SCROLL.  Padding grows, rules thin, gaps open,
        the measure widens and — the mechanism nobody expects — the display
        letterforms get WIDER band by band, 105% to 125%. One six-step ladder,
        `.cl-1` … `.cl-6`, drives all of it. §07.
     3. THE TYPE IS THE CLIENT'S.  Teko for headlines, in SENTENCE CASE, never
        condensed, never uppercase. Its width axis IS the mechanism for (2), so
        the typographic decision and the structural one are one decision.

   READING ORDER
     00  @property, @font-face
     01  Tokens + field context
     02  Reset
     03  Base typography
     04  Utilities
     05  Controls — buttons
     06  Chrome — header, nav, mobile panel, breadcrumbs
     07  Band system + the clearance ladder
     08  The fold + the .ba drag-to-compare slider      <- the signature
     09  Home bands 2–11
     10  Inner pages — hero, quick answer, prose, tables, figures
     11  Blog — post, aside, related, zips
     12  Forms
     13  Footer + call bar
     14  Motion
     15  Print

   AUTHORING RULES (DESIGN-PLAN §6.8)
     - Selector specificity stays flat: one class, optionally compounded with
       an `.is-*` state or a field / clearance modifier.
     - No element-type selectors for LAYOUT. Element selectors appear only for
       typography defaults, where any component class outranks them.
     - Band padding is set in exactly ONE place: `.band{padding-block:var(--pad)}`.
       No component sets its own band padding. That is how round three's
       paddings started cancelling each other out.
     - Mobile-first. Every media query is `min-width`, at 560 / 720 / 900 /
       1100 / 1400 (plus the 821 complement of the call bar's ≤820).
     - Every animation and transition sits inside
       `@media (prefers-reduced-motion: no-preference)`.
     - PROGRESSIVE ENHANCEMENT IS ONE-DIRECTIONAL. The no-JS state is the
       DEFAULT here. Everything JS-dependent is nested under `html.js-ba`,
       `html.js-chrome` or `html.js-reveal` — flags set by a ~120-byte inline
       <head> script (js-ba / js-chrome / is-top) and by site.js (js-reveal).
       Never the reverse: a stylesheet that hides content and waits for a
       script to reveal it is a stylesheet that can lose the page.

   THE PALETTE IS THE CLIENT'S AND IT IS LOCKED. tools/palette-audit.mjs fails
   the build on any hex outside §01. Because it is locked — and because
   near-black-plus-one-hot-accent is itself one of the three looks AI design
   defaults to — LAYOUT, TYPE and THE SIGNATURE carry 100% of the
   distinctiveness. Nothing below may lean on colour to be interesting.

   And the other two defaults, guarded explicitly:
     - Cream + high-contrast serif + terracotta: THERE IS NO SERIF IN THIS
       FILE. Not one fallback stack contains one.
     - Broadsheet hairlines, radius 0, dense columns: held off by a
       photographic full-bleed fold, expanded sentence-case display type, no
       prose ever set in newspaper columns (the measure never drops below
       58ch and no prose block is multi-column), and a bottom third that is
       mostly empty space.
   =========================================================================== */


/* ===========================================================================
   00  @property + @font-face
   =========================================================================== */

/* --ba-pos is the whole slider. ONE registered custom property drives the
   clip-path on .ba-clip AND the transform on .ba-seam, which is what lets the
   seam nudge (§14) interpolate it as a real percentage instead of stepping
   between two strings. `inherits:true` is required: the runtime writes it on
   .ba-frame and both consumers are descendants.

   Without @property the slider still works — clip-path and transform read the
   unregistered value fine. Only the nudge degrades, to a jump. */
@property --ba-pos {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 50%;
}

/* ---------------------------------------------------------------------------
   FONTS — the client's three, self-hosted, latin subsets, all OFL.

     Headlines    Teko, 700          h1, h2, .band-h, .fold-lede, .close-tel
     Subheadings  Montserrat, 600    h3, h4, labels, eyebrows, buttons
     Body         Inter, 400         everything that is a sentence

   These were specified by the client, not chosen here, and the previous
   Saira / Asap / Martian Mono set was replaced wholesale. 123KB for all three
   against 165KB for the outgoing set, so the swap is also lighter.

   THERE IS NO FOURTH FAMILY. The old sheet used a mono face for labels,
   eyebrows, data rails and BEFORE / AFTER. Montserrat at --t-micro, 600, with
   the letterspacing in --tr-mono does that job, and a fourth family nobody
   asked for would work against a three-face brief. The token is --ff-sub; it
   was called --ff-mono when a monospace actually filled the role.

   THE WIDTH LADDER IS GONE. Saira had a wdth axis and `font-stretch: var(--dw)`
   widened the display face from 105% to 125% down the clearance ladder, which
   made "the page opens up as you scroll" a typographic fact and not just a
   padding change. None of these three families has a width axis, so the
   declaration would be dead CSS pretending to work. The ladder still runs on
   padding, gap, rule weight and measure — see §07.

   TEKO STOPS AT 700. Its weight axis is 300..700, so a display rule asking for
   800 silently clamps. Every --ff-display rule here sets 700 explicitly.

   ONE url() PER FACE. The round-three sheet listed a filename that did not
   exist first; a `src` list does fall through, so it resolved and looked fine
   while burning a real request on a 404 on every cold load and swapping the
   display face in late. These three strings must match PRELOAD_FONTS in
   layout.mjs character for character.
   ------------------------------------------------------------------------- */

@font-face {
  font-family: 'Teko';
  src: url('/assets/fonts/teko-var.woff2') format('woff2-variations');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Montserrat';
  src: url('/assets/fonts/montserrat-var.woff2') format('woff2-variations');
  font-weight: 300 800;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-var.woff2') format('woff2-variations');
  font-weight: 300 800;
  font-style: normal;
  font-display: swap;
}

/* CLS insurance. `swap` paints in the fallback on frame one; these two are
   metric-matched so the fold's headline does not reflow when the real face
   lands. The LCP element is a photograph, but a headline that jumps two lines
   400ms in reads as broken whatever the metric says.

   MEASURED, NOT ESTIMATED — tools/font-metrics.mjs, run against the real files
   with this site's own h1 as the sample string, because what is being matched
   is where THAT headline wraps. size-adjust is the number that matters: every
   text element here carries a NUMERIC line-height, so the line box comes from
   the number and the ascent/descent overrides only shift the baseline inside
   it, but size-adjust changes glyph width and a changed wrap point relayouts
   everything under it.

   Arial is the target rather than a closer-but-rarer face. Machines that have
   Arial Narrow would already be near Teko; machines that do not fall through to
   Arial anyway, so Arial is the case that actually happens.

   Teko is 19% narrower than Arial at the same size, which is most of what makes
   it read as a display face. */
@font-face {
  font-family: 'Teko Fallback';
  src: local('Arial'), local('Helvetica Neue');
  size-adjust: 81.3%;
  ascent-override: 96.0%;
  descent-override: 48.0%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Montserrat Fallback';
  src: local('Arial'), local('Helvetica Neue');
  size-adjust: 108.7%;
  ascent-override: 97.0%;
  descent-override: 25.0%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial'), local('Helvetica Neue');
  size-adjust: 96.7%;
  ascent-override: 97.0%;
  descent-override: 24.0%;
  line-gap-override: 0%;
}

/* THREE THINGS FONTS.MD MEASURED THAT THIS FILE HAS TO RESPECT — see
   src/assets/fonts/FONTS.md §4 for the measurements.

   1. EVERY RULE THAT SETS --ff-display ALSO SETS font-weight. A variable font
      renders at its fvar default when no weight is given, which is not the
      weight anybody intends. Keep it that way.

   2. TEKO'S NATURAL LINE BOX IS 1.440em (ascent 96% + descent 48%). Any element
      set in Teko that inherits `line-height: normal` opens up badly. Every rule
      here that sets --ff-display also sets an explicit line-height.

   3. `→` (U+2192) AND `▸` (U+25B8) ARE NOT IN THE LATIN SUBSET of any of the
      three files. Nothing in this stylesheet types either one: the .ask marker
      and the .ba-grip chevrons are drawn from rotated borders, and the crumb
      separator is an ASCII slash. Templates filling .index-go and .band-more
      must use `›` (U+203A, present in all three), an inline SVG from
      ICON_PATHS, or nothing — a bare arrow falls back to a system font and
      renders as tofu on some Android builds. */


/* ===========================================================================
   01  TOKENS + FIELD CONTEXT
   =========================================================================== */

:root {
  /* --- the client's five. LOCKED. May not be altered. --------------------- */
  --ink:    #000501;
  --orange: #FF713D;
  --cream:  #FFF8EA;
  --grey:   #6F6F6F;
  --peach:  #FFAE82;

  /* --- derived, because three of the five fail contrast where they'd sit ---
     Measured, not estimated. Do not re-derive these.
       --orange on --cream is 2.59:1  ->  --orange-ink   5.04:1
       --grey   on --ink   is 4.09:1  ->  --muted-ink    7.03:1
       --grey   on --cream is passable ->  --muted-cream 5.51:1 and safer
     NEVER AS TEXT: orange on cream · peach on cream · cream on orange · grey
     on ink (large / UI only). Buttons are an orange fill with INK text.

     --orange-ink IS MEASURED AGAINST --cream-2, NOT --cream. There are two
     cream grounds, and the derived value only ever got checked against the
     lighter one. It was #BA522D: 4.59:1 on --cream, which passes, and 4.20:1 on
     --cream-2, which does not. Table headers are `color: var(--mark)` on
     `background: var(--panel)`, and --panel resolves to --cream-2 inside a
     .band-cream — so every table on the site shipped accent text below AA.
     Deriving against the DARKER ground makes the token safe on both:
       #B04D2A  5.04:1 on --cream · 4.62:1 on --cream-2
     It is only ever text or a hairline on a cream field (--accent and --mark
     are defined nowhere else), so nothing sits on top of it and no fill ratio
     moves with it. */
  --orange-ink:  #B04D2A;
  --muted-ink:   #979797;
  --muted-cream: #656565;

  /* --- surfaces and hairlines --------------------------------------------- */
  --ink-2:      #0A0F0B;
  --line-dark:  #1C2320;
  --cream-2:    #F6EEDD;
  --line-light: #E4DAC6;

  /* The thinned hairline the bottom of the page uses. Alpha rather than a new
     hex, because the rule has to disappear INTO whichever field it is drawn
     on, and at cl-5/cl-6 the point is that you can barely see it. */
  --line-dark-soft:  rgba(255, 248, 234, .10);
  --line-light-soft: rgba(0, 5, 1, .09);

  /* Two pre-computed fills.
       --orange-dim   --orange at 14% over --ink. Ghost-button hover ground on
                      ink; --orange reads off it at 6.52:1.
       --orange-flat  the hover value for an orange fill on engines with no
                      color-mix(). --ink on it is 5.68:1. Cream never is. */
  --orange-dim:  #241409;
  --orange-flat: #DB6235;

  --focus: #FF713D;

  /* rgb triplet of --ink, for the fold's gradient scrims (§08). */
  --scrim: 0, 5, 1;

  /* --- families. NO SERIF ANYWHERE, including in the fallback stacks. ----- */
  --ff-display: 'Teko', 'Teko Fallback', system-ui, sans-serif;
  --ff-sub:     'Montserrat', 'Montserrat Fallback', system-ui, sans-serif;
  --ff-body:    'Inter', 'Inter Fallback', system-ui, sans-serif;

  /* --- type scale — 9 steps, fluid 360 -> 1440 ---------------------------- */
  --t-mega:  clamp(2.75rem, 1.17rem + 7.04vw, 7.5rem);    /*  44 -> 120px  .close phone, 404 */
  --t-fold:  clamp(2.5rem, 1.4rem + 4.6vw, 6rem);         /*  40 ->  96px  home h1 ONLY */
  --t-h1:    clamp(1.75rem, 1.25rem + 2.22vw, 3.25rem);   /*  28 ->  52px  inner-page h1 */
  --t-h2:    clamp(1.5rem, 1.17rem + 1.48vw, 2.5rem);     /*  24 ->  40px  band headings */
  --t-h3:    clamp(1.1875rem, 1.08rem + 0.46vw, 1.5rem);  /*  19 ->  24px  row titles */
  --t-lead:  clamp(1.125rem, 1.04rem + 0.37vw, 1.375rem); /*  18 ->  22px  band sub, quick answer */
  --t-body:  clamp(1.03rem, 1rem + 0.14vw, 1.125rem);     /* 16.5 ->  18px paragraphs */
  --t-small: 0.9375rem;                                   /*  15px         captions, meta, legal */
  --t-micro: clamp(0.72rem, 0.7rem + 0.09vw, 0.78rem);    /* 11.5 -> 12.5px mono labels */

  --lh-display: .95;
  --lh-h2:      1.04;
  --lh-h3:      1.22;
  --lh-lead:    1.45;
  --lh-body:    1.62;
  --lh-micro:   1.2;

  /* Sentence case needs far less negative tracking than a wall of caps did. */
  --tr-display: -0.015em;

  /* 0.14em is the label letterspacing and it applies from 560px up.
     Below that it is 0.10em, and the reason is measured rather than aesthetic:
     the shipped file is the SemiExpanded cut (requesting only the wght axis
     from the css2 endpoint pinned wdth at its 112.5% default, so there is no
     width axis to move and font-stretch does nothing to it). Its glyphs are
     Montserrat is a geometric face with wide round forms, so a long label
     like "Licensed & insured · free quotes on site" eats horizontal space
     fast on a 360px screen. FONTS.MD §4.2: THE LEVER IS letter-spacing, NOT
     font-size — --t-micro is already compensating for the cap height and must
     not be nudged. */
  --tr-mono: 0.10em;

  /* --- the measure ladder — it WIDENS down the page (§07) ----------------- */
  --measure-1: 58ch;
  --measure-2: 62ch;
  --measure-3: 66ch;
  --measure-4: 70ch;

  /* --- clearance defaults, for anything that lives outside a band --------- */
  --pad:     clamp(3rem, 2rem + 3.4vw, 4.5rem);
  --gap:     1.5rem;
  --rule-w:  1px;
  --measure: var(--measure-3);

  /* --- containers --------------------------------------------------------- */
  --wrap:        1240px;
  --wrap-narrow: 720px;
  --wrap-wide:   1440px;
  --gut:         clamp(1.25rem, 4vw, 3rem);

  --hdr-h: 64px;

  /* --- the slider -------------------------------------------------------- */
  --ba-pos: 50%;
  --ba-grip-y: 50%;

  /* --- motion, declared once so §14 has one place to tune ---------------- */
  --ease-out:   cubic-bezier(.22, .75, .3, 1);
  --ease-rise:  cubic-bezier(.2, .8, .25, 1);
  --ease-nudge: cubic-bezier(.4, 0, .2, 1);
  --t-color: 120ms;
  --t-move:  160ms;

  /* The page ground is near-black, so native UI — scrollbars, date pickers,
     the select caret we do not draw — should render dark. Light fields flip
     it back below. */
  color-scheme: dark;
}

@media (min-width: 560px) {
  :root { --tr-mono: 0.14em; }
}

@media (min-width: 900px) {
  :root { --hdr-h: 76px; }
}

/* ---------------------------------------------------------------------------
   FIELD CONTEXT

   Components are written against these variables rather than against a colour,
   so identical markup rules itself correctly on any of the three fields. This
   is the only reason §09 and §10 contain no `.band-cream .index-row {}`
   overrides at all.
   ------------------------------------------------------------------------- */

.band-cream,
.band-cream2,
.crumbs {
  --bg: var(--cream);
  --fg: var(--ink);
  --muted: var(--muted-cream);
  --rule-strong: var(--line-light);
  --rule-soft: var(--line-light-soft);
  --panel: var(--cream-2);
  --accent: var(--orange-ink);
  --accent-fill: var(--orange);
  --mark: var(--orange-ink);
  /* An orange focus ring on cream is 2.59:1 against the band — invisible
     exactly when it matters. The declared rule in §03 is still literally
     `outline: 3px solid var(--focus)`; the field re-points the variable at
     something that clears 3:1 by a mile. */
  --focus: var(--ink);
  --field-bg: transparent;
  --field-bd: var(--grey);
  color-scheme: light;
}

.band-cream2,
.crumbs {
  --bg: var(--cream-2);
  --panel: var(--cream);
}


/* ===========================================================================
   02  RESET
   Small and modern. Not normalize — the handful of defaults that are actively
   wrong for this layout.
   =========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Anchors land below the header instead of under it. Not motion — this
     applies whatever the motion preference is. */
  scroll-padding-top: calc(var(--hdr-h) + 1rem);
}

body,
h1, h2, h3, h4, h5, h6,
p, figure, blockquote,
dl, dd, ol, ul,
fieldset, legend { margin: 0; }

ul[class], ol[class] { padding: 0; list-style: none; }

/* Replaced content is block-level and never wider than its column. This one
   rule is what stops a 1536px job photo scrolling the page sideways. */
img, picture, svg, video, canvas, iframe {
  display: block;
  max-width: 100%;
}
img { height: auto; }

input, button, textarea, select {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}

button { background: none; border: 0; }

table { border-collapse: collapse; border-spacing: 0; }

summary::-webkit-details-marker { display: none; }

[hidden] { display: none !important; }


/* ===========================================================================
   03  BASE TYPOGRAPHY
   =========================================================================== */

body {
  background: var(--ink);
  color: var(--cream);
  font-family: var(--ff-body);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: var(--lh-body);
  /* Greyscale AA specifically because this site is dark-first: on a near-black
     ground subpixel AA fattens the stems and the page reads muddy. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Room for the fixed call bar. Cancelled at 821px in §13. */
  padding-block-end: env(safe-area-inset-bottom, 0px);
  /* Belt and braces against a sideways scroll — every wide block already has
     its own overflow container. `clip`, not `hidden`: `hidden` makes the body
     a scroll container in some engines, which silently kills sticky inside. */
  overflow-x: clip;
}

/* The header only becomes fixed once the chrome flag is set, so the offset it
   needs only exists then. With JS off the header is in normal flow and this
   padding would be a 64px hole at the top of every page. */
html.js-chrome body { padding-block-start: var(--hdr-h); }

/* THE HOME PAGE KEEPS THE HEADER OFFSET. It used to be zeroed here, because the
   old fold was a full-bleed photograph that deliberately ran under the
   transparent header. The fold is now a contained carousel, so zeroing the
   offset just slid the h1 up underneath the header — "Same-day" was clipped at
   390 and the whole first line overlapped at 768. Nothing bleeds to the top
   edge any more, so nothing needs the offset removed. */

/* --- display headings ---------------------------------------------------
   Teko, 700, SENTENCE CASE. There is no
   `text-transform: uppercase` on a heading anywhere in this file, at any size,
   on any page. The only uppercase on the site is Montserrat at --t-micro.

   The display face no longer widens down the page: --dw and font-stretch are
   gone with Saira, because none of the client's three families has a width
   axis. What is left of the ladder is padding, gap, rule weight and measure.
   For the record, --dw ran 105% in the fold and 125% in the
   last band, set once per band by the clearance ladder (§07) and inherited
   from there. Nobody will name the mechanism; they will say the page opens up.

   Sentence case is also what keeps a headline readable over photography — a
   wall of caps at 76px turns into a grey brick, while ascenders and descenders
   keep a silhouette the eye can still read shapes off. */
h1, h2,
.band-h, .h2 {
  font-family: var(--ff-display);
  font-weight: 700;
  letter-spacing: var(--tr-display);
  line-height: var(--lh-h2);
  text-wrap: balance;
  color: var(--fg);
}

h1 { font-size: var(--t-h1); max-width: 20ch; line-height: var(--lh-display); }
h2, .h2 { font-size: var(--t-h2); max-width: 26ch; }
/* Subheadings. Montserrat SemiBold, per the client's type list. */
h3, h4, .h3 {
  font-family: var(--ff-sub);
  font-weight: 600;
  letter-spacing: -0.005em;
}
h3, .h3 { font-size: var(--t-h3); max-width: 30ch; line-height: var(--lh-h3); }
h4 { font-size: 1.05rem; max-width: 32ch; line-height: var(--lh-h3); }

/* h5 / h6 are the mono label level. Data and labels, never a sentence. */
h5, h6 {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--muted);
}

p { text-wrap: pretty; }

/* PROSE FLOW.
   The reset zeroes margin on everything, which is right for components that
   own their rhythm and wrong for running copy. Service sections and blog
   bodies put <p>, <ul> and <h3> straight inside .prose or .wrap with no
   wrapper, so without these they render flush against each other and a
   sub-heading reads as a bold sentence.

   SPECIFICITY IS DELIBERATE: every selector here is element-only, (0,0,2), so
   any component rule with a class in it still wins. Sibling combinators mean
   component headings are untouched — a heading that is a first child never
   matches one of these. */
:is(p, ul, ol, dl, blockquote, figure, table) + :is(p, ul, ol, dl, blockquote, figure, table) {
  margin-block-start: 1rem;
}
:is(p, ul, ol, dl, blockquote, figure, table) + :is(h2, h3, h4) {
  margin-block-start: 2.5rem;
}
:is(h2, h3, h4) + :is(p, ul, ol, dl, blockquote, figure, table) {
  margin-block-start: 1rem;
}

/* Loose prose emitted straight into a width container still gets a measure.

   `:not([class])` IS LOAD-BEARING, not tidiness. Several components are a
   classed <ul>/<ol>/<dl> sitting directly inside .wrap — .plate is a <dl>,
   .run is an <ol> — and without the guard this rule capped those grids at 58ch
   and left the four-fact plate occupying 563px of a 1144px band while the
   numbered steps collapsed into a column two words wide. A component knows its
   own measure; this rule is only for the markup that has nobody to set one. */
.wrap > p:not([class]),
.wrap > ul:not([class]),
.wrap > ol:not([class]),
.wrap > dl:not([class]) { max-width: var(--measure); }

strong, b { font-weight: 600; }
small { font-size: .875em; }

/* --- links ---------------------------------------------------------------
   Inline prose links inherit their colour and carry an accent underline.
   Orange is reserved for "act now", so a link inside a paragraph is not
   painted in it — but the underline is, which keeps the accent doing its one
   job and means a link is never signalled by colour alone. */
a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

p a,
li a:not([class]),
dd a,
.prose a,
.ask-a a,
.callout a,
.answer a,
.post-body a { text-decoration-color: var(--accent); }

p a:hover,
li a:not([class]):hover,
dd a:hover,
.prose a:hover,
.ask-a a:hover,
.callout a:hover,
.answer a:hover,
.post-body a:hover {
  color: var(--accent);
  text-decoration-thickness: 2px;
}

/* --- focus ---------------------------------------------------------------
   3px solid, offset 2px, on every interactive element, never removed without
   a replacement, and NEVER transitioned — a focus ring that fades in is a
   focus ring you miss. --focus is field-scoped (§01): orange on ink,
   near-black on cream, --ink on an orange fill. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* #main carries tabindex="-1" so the skip link has somewhere to land. It is a
   landmark, not a control, and a ring round the whole page after a skip is
   noise rather than information. */
#main:focus,
#main:focus-visible { outline: none; }

::selection {
  background: var(--orange);
  color: var(--ink);
}

li::marker { color: var(--mark); }


/* ===========================================================================
   04  UTILITIES
   =========================================================================== */

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

/* THE NARROW WRAP SHARES THE WIDE WRAP'S LEFT EDGE.
   Centring a 720px column inside a page whose other bands are 1240px puts its
   left edge 260px further in at 1440px, so a service page steps left, right,
   left, right four times on the way down. Measured on /hot-tub-removal/: six
   bands started at 148px and four started at 408px. That is what the client
   was describing as the spacing being strange — it is not the gaps, it is the
   margin moving.

   The inset is the wide wrap's own centring margin, so the two line up at every
   width and collapse to the same gutter below 1240px. max() rather than a media
   query because the number is the same arithmetic the browser is already doing
   for .wrap. */
.wrap.is-narrow {
  max-width: var(--wrap-narrow);
  margin-inline: max(0px, (100% - var(--wrap)) / 2) auto;
}

.wrap.is-wide   { max-width: var(--wrap-wide); }

/* Standalone divider. Takes BOTH its weight and its colour from the clearance
   ladder, so the same element is a 2px line at the top of the page and a
   barely-there hairline at the bottom of it. */
.rule {
  border: 0;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
  height: 0;
  margin: 0;
}

/* Inline SVG on the 24px grid, stroke following currentColor. Surviving keys
   only: check, x, phone, chat, pin. Every other icon in ICON_PATHS went with
   the service cards. */
.ico {
  width: 1.5rem;
  height: 1.5rem;
  flex: none;
  fill: none;
  stroke: currentColor;
}

/* Force the mono face on a fragment — a phone number inside a sentence. */
.u-mono {
  font-family: var(--ff-sub);
  font-size: .92em;
  letter-spacing: .02em;
}

.u-tab { font-variant-numeric: tabular-nums; }

/* Visually hidden, still announced. `.vh` is the legacy alias site.js uses for
   its live region; both must exist and neither may ever become visible. */
.sr-only,
.vh {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link. Fixed so it clears the header, and moved off-screen by transform
   rather than display:none — a display:none link is not focusable and the
   whole affordance quietly stops working. */
.skip {
  position: fixed;
  top: .5rem;
  left: .5rem;
  z-index: 200;
  transform: translateY(calc(-100% - 1.5rem));
  background: var(--orange);
  color: var(--ink);
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  text-decoration: none;
  padding: .9rem 1.1rem;
}

.skip:focus { transform: translateY(0); }
/* The ring would vanish into the orange fill it sits on. */
.skip:focus-visible { outline-color: var(--ink); }


/* ===========================================================================
   05  CONTROLS — BUTTONS
   Orange fill, INK text. Never cream on orange: that pair is 2.59:1 and it is
   the single most tempting mistake in this palette.

   RADIUS 0 ON EVERY CONTROL. The only round object on this entire site is the
   drag grip in §08 — so the one thing you are meant to touch is the one thing
   that looks touchable.
   =========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .55rem;
  min-height: 48px;
  padding: .8rem 1.4rem;
  border: 2px solid var(--orange);
  border-radius: 0;
  background: var(--orange);
  color: var(--ink);
  /* Montserrat SemiBold. A button is a control, not a headline, and Teko is a
     condensed display face that stops being comfortable at button size. */
  font-family: var(--ff-sub);
  font-weight: 600;
  font-size: 1rem;
  line-height: 1.15;
  letter-spacing: .02em;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
}

.btn .ico { width: 1.15em; height: 1.15em; stroke-width: 2; }

/* Stated explicitly rather than inherited from .btn, so a future change to the
   base can never silently repaint every solid button. */
.btn-solid {
  background: var(--orange);
  border-color: var(--orange);
  color: var(--ink);
}

.btn-solid:hover {
  background: var(--orange-flat);
  border-color: var(--orange-flat);
}

/* The ring has to switch on an orange fill — an orange ring on an orange
   button is not a ring. */
.btn:focus-visible,
.btn-solid:focus-visible { outline-color: var(--ink); }

/* Ghost — INK FIELDS ONLY. 1px orange border, orange text (7.52:1 on ink). */
.btn-ghost {
  background: transparent;
  border: 1px solid var(--orange);
  color: var(--orange);
  padding: .84rem 1.4rem;
}

.btn-ghost:hover {
  background: var(--orange-dim);
  border-color: var(--orange);
  color: var(--orange);
}

.btn-ghost:focus-visible { outline-color: var(--focus); }

/* Defensive: orange text on cream is 2.59:1. If a page module ever drops a
   ghost button on a light field it degrades into the .btn-line treatment
   rather than shipping unreadable. */
.band-cream .btn-ghost,
.band-cream2 .btn-ghost {
  color: var(--ink);
  border-color: var(--ink);
}

.band-cream .btn-ghost:hover,
.band-cream2 .btn-ghost:hover {
  background: var(--ink);
  color: var(--cream);
}

/* Line — the CREAM-FIELD secondary. */
.btn-line {
  background: transparent;
  border: 1px solid var(--ink);
  color: var(--ink);
  padding: .84rem 1.4rem;
}

.btn-line:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--cream);
}

/* On an ink field the same modifier flips to the light hairline. --grey is
   4.09:1 on ink: fine for a 1px control border, which needs 3:1, and never
   used for the label. */
.band-ink .btn-line,
.fold .btn-line,
.phero .btn-line,
.ftr .btn-line {
  color: var(--cream);
  border-color: var(--grey);
}

.band-ink .btn-line:hover,
.fold .btn-line:hover,
.phero .btn-line:hover,
.ftr .btn-line:hover {
  background: var(--cream);
  border-color: var(--cream);
  color: var(--ink);
}

.btn-lg {
  min-height: 56px;
  padding: 1rem 1.85rem;
  font-size: 1.08rem;
}

/* Smaller box, same 48px tap floor. There are no exceptions to 48px. */
.btn-sm {
  min-height: 48px;
  padding: .5rem 1.05rem;
  font-size: .9rem;
}

.btn-block {
  display: flex;
  width: 100%;
}


/* ===========================================================================
   06  CHROME — HEADER, NAV, MOBILE PANEL, BREADCRUMBS

   THE NO-JS DEFAULT IS WRITTEN FIRST, ON PURPOSE. Without `html.js-chrome`:
     - the header sits in normal flow, so there is no fixed offset to
       compensate for and nothing overlaps,
     - the mobile panel is OPEN, because it ships open in the markup and only
       the enhanced state closes it — a visitor whose JavaScript never runs
       gets the full navigation instead of a dead hamburger,
     - the burger is hidden, because there is nothing for it to toggle.
   =========================================================================== */

.hdr {
  position: relative;
  z-index: 100;
  background: var(--ink);
  color: var(--cream);
  border-bottom: 1px solid var(--line-dark);
}

html.js-chrome .hdr {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}

/* TRANSPARENT OVER THE FOLD, AND NOWHERE ELSE. Compounded three ways on
   purpose: the page must be the home page, the capability script must have
   run, and the visitor must not have scrolled. site.js drops `is-top` off
   <html> past the first screen and the header goes solid again. */
html.is-top body.p-home .hdr {
  background: transparent;
  border-bottom-color: transparent;
}

.hdr-in {
  display: flex;
  align-items: center;
  gap: .75rem;
  min-height: var(--hdr-h);
  /* Same geometry as .wrap. The bar itself stays full-bleed — it is fixed and
     its background has to span the viewport — but what is inside it lines up
     with the page content, so the logo sits on the same left edge as every h1
     below it instead of 20px to the left of them. */
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gut);
}

/* --- brand --------------------------------------------------------------- */
.hdr-brand {
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  margin-inline-end: auto;
  /* The lockup is a wide raster; without a min tap box it becomes a 30px-tall
     sliver of a link. */
  min-height: 48px;
  min-width: 0;
  padding-block: .25rem;
  color: var(--cream);
  text-decoration: none;
  font-family: var(--ff-display);
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: var(--tr-display);
  line-height: 1;
}

/* THE CLASS IS ON A <picture>, NOT ON THE <img>. Sizing the <picture> does
   nothing to the image inside it — the img keeps its natural 600x112 and
   pushes the page sideways on a phone, which is exactly what the overflow
   audit caught. The constraint has to land on the img.

   max-height caps the lockup on a wide screen; max-width:100% is what makes it
   scale DOWN proportionally instead of overflowing at 360px, and it only works
   because width and height are both auto so the aspect ratio is preserved.
   min-width:0 on the link lets flexbox actually shrink it — a flex item
   defaults to min-width:auto and refuses to go below its content. */
.hdr-mark { display: block; min-width: 0; }

.hdr-mark img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 30px;
}

@media (min-width: 720px)  { .hdr-mark img { max-height: 34px; } }
@media (min-width: 1100px) { .hdr-mark img { max-height: 40px; } }

/* --- desktop nav ---------------------------------------------------------
   Hidden until 900px, which is exactly where the burger goes away. */
.hdr-nav { display: none; }

/* The flex lives on `.hdr-nav` itself. layout.mjs emits the links as direct
   children of the <nav> — there is no <ul> — so a `.hdr-nav ul` rule matched
   nothing and every desktop viewport rendered the nav as one run-on line:
   "Services Areas we serve Before & after Blog". */
.hdr-nav {
  align-items: center;
  gap: clamp(.9rem, 1.6vw, 1.75rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.hdr-link {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding-block: .25rem;
  border-bottom: 2px solid transparent;
  color: var(--cream);
  text-decoration: none;
  font-family: var(--ff-display);
  font-weight: 600;
  font-size: .95rem;
  /* Explicit, because Teko's natural line box is 1.440em and this is a flex
     item inside a 64px header (FONTS.md §4.4). */
  line-height: 1.2;
  letter-spacing: .01em;
}

/* Hover previews the rule the current page wears, in the soft secondary
   rather than the accent, so "where I am" and "where I could go" never look
   alike. */
.hdr-link:hover {
  color: var(--peach);
  border-bottom-color: var(--grey);
}

/* Current page carries a WEIGHT change as well as a rule and a colour, so it
   is never signalled by colour alone. */
.hdr-link[aria-current='page'],
.hdr-link.is-active {
  color: var(--orange);
  border-bottom-color: var(--orange);
  font-weight: 800;
}

/* --- header phone --------------------------------------------------------
   Orange fill, INK text, always visible, ≥48px. On a junk removal site the
   number IS the conversion, so it gets the loudest shape in the chrome. */
.hdr-tel {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  min-height: 48px;
  padding-inline: .8rem;
  background: var(--orange);
  color: var(--ink);
  text-decoration: none;
  font-family: var(--ff-sub);
  font-size: clamp(.7rem, .55rem + .5vw, .82rem);
  font-weight: 600;
  letter-spacing: .04em;
  white-space: nowrap;
}

.hdr-tel:hover { background: var(--orange-flat); }
.hdr-tel:focus-visible { outline-color: var(--ink); }
.hdr-tel .ico { width: 1.1rem; height: 1.1rem; stroke-width: 2; }

/* --- burger --------------------------------------------------------------
   The three bars are drawn as backgrounds, so this renders correctly whether
   or not the template puts <span>s inside it. Any empty span it does emit is
   hidden; a non-empty one (an .sr-only label) survives. */
.hdr-menu {
  position: relative;
  display: none;
  width: 48px;
  height: 48px;
  flex: none;
  padding: 0;
  border: 1px solid var(--line-dark);
  background-color: transparent;
  /* Only the MIDDLE bar is painted by the background. The outer two are the
     ::before and ::after below, which is what lets them rotate into the X
     rather than the button swapping one picture for another. */
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 22px 2px;
  background-repeat: no-repeat;
  background-position: center;
  color: var(--cream);
  cursor: pointer;
}

.hdr-menu > span:empty { display: none; }

html.js-chrome .hdr-menu { display: inline-block; }

.hdr-menu:hover { border-color: var(--orange); color: var(--orange); }

/* The two outer bars, present in both states. Closed they sit 8px above and
   below the middle one; open they collapse onto it and cross. */
.hdr-menu::before,
.hdr-menu::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 22px;
  height: 2px;
  margin: -1px 0 0 -11px;
  background: currentColor;
}

.hdr-menu::before { transform: translateY(-8px); }
.hdr-menu::after  { transform: translateY(8px); }

.hdr-menu.is-open,
.hdr-menu[aria-expanded='true'] {
  border-color: var(--orange);
  color: var(--orange);
}

/* The middle bar is the background image, so it fades rather than rotating. */
.hdr-menu.is-open,
.hdr-menu[aria-expanded='true'] { background-image: none; }

.hdr-menu.is-open::before,
.hdr-menu[aria-expanded='true']::before { transform: rotate(45deg); width: 24px; margin-left: -12px; }
.hdr-menu.is-open::after,
.hdr-menu[aria-expanded='true']::after { transform: rotate(-45deg); width: 24px; margin-left: -12px; }

@media (min-width: 900px) {
  .hdr-nav { display: flex; }   /* flex, not block — the gap above only applies to a flex container */
  html.js-chrome .hdr-menu { display: none; }
  .hdr-in { gap: 1.5rem; }
  .hdr-tel { padding-inline: 1.1rem; font-size: .82rem; }
}

/* --- mobile panel --------------------------------------------------------
   Ships OPEN in the markup. Only `html.js-chrome` closes it, so with JS off
   this is a plain block of links in normal flow: full navigation, no dead
   button, nothing to toggle. */
.m-nav {
  background: var(--ink-2);
  border-bottom: 1px solid var(--line-dark);
  color: var(--cream);
}

.m-nav-in { padding-block: 1rem 1.5rem; }

html.js-chrome .m-nav {
  position: fixed;
  top: var(--hdr-h);
  left: 0;
  right: 0;
  z-index: 99;
  max-height: calc(100dvh - var(--hdr-h));
  overflow-y: auto;
  overscroll-behavior: contain;

  /* Shut. visibility keeps it out of the tab order and the accessibility tree
     exactly as display:none did, but unlike display it can be transitioned. */
  visibility: hidden;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
}

html.js-chrome .m-nav.is-open {
  visibility: visible;
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

@media (prefers-reduced-motion: no-preference) {
  html.js-chrome .m-nav {
    transition:
      opacity 200ms var(--ease-out),
      transform 260ms var(--ease-out),
      visibility 0s linear 260ms;
  }
  html.js-chrome .m-nav.is-open {
    transition:
      opacity 180ms var(--ease-out),
      transform 260ms var(--ease-out),
      visibility 0s;
  }

  /* The links follow the panel in. --nav-i is stamped by site.js so the stagger
     survives the list changing length. Capped at six so the last item never
     waits long enough to feel broken. */
  html.js-chrome .m-nav .m-nav-link,
  html.js-chrome .m-nav .btn-block {
    opacity: 0;
    transform: translateY(-4px);
  }
  html.js-chrome .m-nav.is-open .m-nav-link,
  html.js-chrome .m-nav.is-open .btn-block {
    opacity: 1;
    transform: none;
    transition:
      opacity 220ms var(--ease-out) calc(90ms + var(--nav-i, 0) * 30ms),
      transform 260ms var(--ease-out) calc(90ms + var(--nav-i, 0) * 30ms);
  }

  /* The bars turn into the X instead of being replaced by it. */
  .hdr-menu::before,
  .hdr-menu::after {
    transition:
      transform 260ms var(--ease-out),
      opacity 140ms var(--ease-out);
  }
}

.m-nav ul {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line-dark);
}

.m-nav li { border-bottom: 1px solid var(--line-dark); }

.m-nav-link {
  display: flex;
  align-items: center;
  min-height: 52px;
  padding: .5rem 0 .5rem .75rem;
  border-left: 2px solid transparent;
  color: var(--cream);
  text-decoration: none;
  font-family: var(--ff-display);
  font-weight: 600;
  font-size: 1.05rem;
  line-height: 1.2;   /* Teko's natural box is 1.440em — never inherit normal */
  letter-spacing: var(--tr-display);
}

.m-nav-link:hover {
  color: var(--orange);
  border-left-color: var(--orange);
}

.m-nav-link[aria-current='page'],
.m-nav-link.is-active {
  color: var(--orange);
  border-left-color: var(--orange);
  font-weight: 800;
}

.m-nav .btn { margin-top: 1rem; }

/* Above 900px the panel is never the navigation, whatever state it was left
   in — including the JS-off case, where the desktop nav has appeared. */
@media (min-width: 900px) {
  .m-nav,
  .m-nav.is-open,
  html.js-chrome .m-nav,
  html.js-chrome .m-nav.is-open { display: none; }
}

/* --- breadcrumbs ---------------------------------------------------------
   Mono on cream-2, between the header and <main>. Breadcrumbs are a path,
   which is data, which is what the mono face is for. */
.crumbs {
  background: var(--cream-2);
  color: var(--ink);
  border-bottom: 1px solid var(--line-light);
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  line-height: var(--lh-micro);
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
}

.crumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .25rem .5rem;
  margin: 0;
  padding-block: .9rem;
  list-style: none;
}

/* THE ELEMENT NAMED `.crumb` IS THE LINK, NOT THE LIST ITEM.
   layout.mjs emits `<li><a class="crumb">` / `<li><span class="crumb" aria-current>`.
   Three rules here assumed the opposite and all three were dead:
     · `.crumb + .crumb::before` — crumbs are never siblings, they sit in
       separate <li>s, so the site shipped with NO separators ("HOME CONTACT")
     · `.crumb a` — the .crumb IS the a, so parent links got no colour or hover
     · `.crumb [aria-current]` — aria-current is ON .crumb, so the current page
       was never distinguished
   Separator moves to the <li>; the other two lose a descendant step. */
.crumbs li { display: flex; align-items: center; gap: .5rem; }

.crumbs li + li::before {
  content: '/';
  color: var(--grey);
}

.crumb {
  color: var(--muted-cream);
  text-decoration: none;
  padding-block: .35rem;
}

a.crumb:hover {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--orange);
}

.crumb[aria-current='page'] { color: var(--ink); }


/* ===========================================================================
   07  BAND SYSTEM + THE CLEARANCE LADDER

   "The page clears as you scroll." A job starts with a room you cannot walk
   through and ends with a swept floor; the page does the same thing. Band one
   is the densest object on the site — a photograph filling the viewport with a
   wall of debris in it. Band eleven is one sentence and a phone number in a
   field of black. In between, EVERY measurable quantity moves in one
   direction, and all of them come from one six-step ladder that each band
   carries exactly once. There are no per-band overrides anywhere in this file.

     step   --pad          --gap     --rule-w  --rule-c  --dw   --measure
     cl-1    36 ->  52px   1px       2px       strong    105%   58ch
     cl-2    48 ->  72px   1.25rem   2px       strong    108%   58ch
     cl-3    64 -> 100px   2rem      1px       strong    112%   62ch
     cl-4    80 -> 136px   2.75rem   1px       strong    116%   66ch
     cl-5    96 -> 176px   3.5rem    1px       SOFT      120%   70ch
     cl-6   120 -> 224px   4.5rem    1px       SOFT      125%   70ch

   Top of page: things touch, rules are 2px, four things fit across,
   letterforms are near-normal width. Bottom of page: nothing touches, gaps are
   4.5rem, one thing per row, letterforms are 125% wide.

   --rule-c resolves through the FIELD's two hairline weights (§01), so the
   same clearance step thins correctly on ink and on cream. Custom-property
   references resolve at use time, so the order of the two single-class rules
   below does not matter.
   =========================================================================== */

.band {
  /* THE ONLY PLACE band padding is set, anywhere in this file. */
  padding-block: var(--pad);
  background: var(--bg);
  color: var(--fg);
}

.band-ink    { background: var(--ink);     color: var(--cream); }
.band-cream  { background: var(--cream);   color: var(--ink); }
.band-cream2 { background: var(--cream-2); color: var(--ink); }

.cl-1 { --pad: clamp(2.25rem, 1.6rem + 2.4vw, 3.25rem); --gap: 1px;     --rule-w: 2px; --measure: var(--measure-1); }
.cl-2 { --pad: clamp(3rem,    2rem + 3.4vw,  4.5rem);   --gap: 1.25rem; --rule-w: 2px; --measure: var(--measure-1); }
.cl-3 { --pad: clamp(4rem,    2.4rem + 5vw,  6.25rem);  --gap: 2rem;    --rule-w: 1px; --measure: var(--measure-2); }
.cl-4 { --pad: clamp(5rem,    2.6rem + 7vw,  8.5rem);   --gap: 2.75rem; --rule-w: 1px; --measure: var(--measure-3); }
.cl-5 { --pad: clamp(6rem,    2.8rem + 9vw,  11rem);    --gap: 3.5rem;  --rule-w: 1px; --measure: var(--measure-4); }
.cl-6 { --pad: clamp(7.5rem,  3rem + 12vw,   14rem);    --gap: 4.5rem;  --rule-w: 1px; --measure: var(--measure-4); }

.cl-1, .cl-2, .cl-3, .cl-4 { --rule-c: var(--rule-strong); }
.cl-5, .cl-6               { --rule-c: var(--rule-soft); }

/* --- band heads ----------------------------------------------------------
   The heading and its deck are ONE unit: one reveal target per band (§14),
   and on most pages the only thing that reveals at all. */
.band-head {
  margin-block-end: clamp(1.75rem, 1rem + 2.4vw, 3rem);
  max-width: var(--measure);
}

/* Mono label above the heading. Present ONLY where it carries information the
   heading does not. Round three's `data-key` chip above every h2 was a label
   labelling a label, and it is gone. */
.band-eyebrow {
  display: block;
  margin-block-end: .75rem;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--mark);
}

.band-h {
  font-size: var(--t-h2);
  max-width: 26ch;
}

.band-sub {
  margin-block-start: .9rem;
  max-width: var(--measure);
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
  color: var(--muted);
}

/* Trailing link row under a band. */
.band-more {
  margin-block-start: clamp(1.75rem, 1rem + 2.4vw, 3rem);
  padding-block-start: 1.25rem;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
}

/* `:not([class*="btn"])` is load-bearing, not tidiness. This rule is (0,1,1) and
   `.btn-solid` is (0,1,0), so a bare `.band-more a` WINS over a button class and
   repaints it — which is how the primary Call button rendered as orange text on
   an orange fill, an invisible control, on five pages. Narrowing it here means
   this selector can never silently take over a control again. */
.band-more a:not([class*='btn']) {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--accent);
}

.band-more a:not([class*="btn"]):hover { text-decoration: underline; text-underline-offset: .3em; }

/* ---------------------------------------------------------------------------
   THE GRID-ITEM MINIMUM-SIZE GUARD

   A grid item defaults to `min-width: auto`, which means it REFUSES to shrink
   below the min-content width of what is inside it. One wide thing — the
   `.tbl` inside a `.tbl-scroll`, which carries `min-width: 34rem` so its
   columns do not hyphenate — therefore forces its whole grid column to 544px
   and scrolls the PAGE sideways on a phone, defeating the very
   `overflow-x: auto` container that was supposed to contain it. The overflow
   audit caught exactly this on all five blog posts.

   `min-width: 0` restores the obvious behaviour: the item takes the track's
   width and the wide content scrolls inside its own box. Every component below
   that is both a grid child and able to contain arbitrary content carries it.
   ------------------------------------------------------------------------- */
.post-body,
.post-side,
.johnny-copy,
.johnny-media,
.takes-col,
.map-col,
.ftr-col,
.plate-cell,
.index-row,
.read-row,
.related-row,
.job,
.quote,
.run-step,
.phero-rail { min-width: 0; }


/* ===========================================================================
   08  THE FOLD + THE .ba DRAG-TO-COMPARE SLIDER
   ---------------------------------------------------------------------------
   THE SIGNATURE. One memorable element; everything else on the site stays
   quiet and disciplined so this one lands.

   The home page opens on a full-bleed photograph of the scrap-lumber job.
   Left of the seam, a wall of timber over grass. Right of it, bare swept
   ground. The seam drags. The headline and the phone number sit fixed over
   the top of it. Before a visitor reads a word they have cleared a yard with
   their thumb — the business happening under their hand instead of a sentence
   describing it.

   THE RISK, TAKEN DELIBERATELY: type on photography is harder to keep legible
   and harder on LCP than a flat colour block. It is paid for with a measured
   scrim floor (below), a preload-and-re-encode plan in layout.mjs, and a
   stacked no-JS fallback — not with hope.

   ONE COMPONENT, TWO SKINS: `.ba` on service and gallery pages, `.ba.ba-bleed`
   inside `.fold` on home. One implementation, one JS handler on [data-ba].

   MARKUP CONTRACT (DESIGN-PLAN §4.2):

     <section class="fold">
       <figure class="ba ba-bleed" data-ba data-ba-start="50"
               data-ba-start-wide="62" data-ba-nudge>
         <div class="ba-frame">
           <picture class="ba-after">…</picture>       <- base, LCP candidate
           <div class="ba-clip"><picture class="ba-before">…</picture></div>
           <div class="ba-seam" aria-hidden="true"><span class="ba-grip"></span></div>
           <p class="ba-lab is-before" aria-hidden="true">Before</p>
           <p class="ba-lab is-after"  aria-hidden="true">After</p>
           <input class="ba-range" type="range" min="0" max="100" value="50" …>
         </div>
         <figcaption class="sr-only">…</figcaption>
       </figure>
       <div class="fold-scrim" aria-hidden="true"></div>
       <div class="fold-scrim-dirty" aria-hidden="true"></div>
       <div class="fold-copy"> kick / h1 / sub / actions / trust / hint </div>
     </section>

   THE <input type="range"> IS THE CONTROL. It is transparent, its thumb is
   invisible, and because it is a real range the keyboard, the screen reader
   and touch all work with no script at all. `.ba-seam` is the visible bar and
   is decorative.

   NO TEXT-SHADOW ANYWHERE IN THIS SECTION. It is the cheap fix for type on
   photography and it looks it. The scrim floor is the fix.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   8.1  THE NO-JS DEFAULT — written first, on purpose.

   Without `html.js-ba` there is no slider, so this stylesheet must not draw
   one. `.ba-frame` becomes a plain grid of the two halves: two rows below
   720px, two columns at and above it, both photographs shown IN FULL. No
   orphan download, no frozen affordance, and no text over an unscrimmed photo.

   Grid placement is explicit because the markup order is after-then-before
   (the after half is the base layer and the LCP candidate when the slider IS
   live). In the static pair the BEFORE half has to read first.
   ------------------------------------------------------------------------- */

.ba {
  position: relative;
  margin: 0;
}

.ba-frame {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  overflow: hidden;
  background: var(--ink);
}

.ba-clip  { grid-area: 1 / 1; position: relative; }
.ba-after { grid-area: 2 / 1; position: relative; }

.ba-after,
.ba-before,
.ba-after img,
.ba-before img { width: 100%; }

/* The seam, the grip, the range and the dirty scrim ARE the interactive layer.
   A slider that invites a drag and does nothing is worse than no slider, so
   with no JS they are not on the page at all. */
.ba-seam,
.ba-grip,
.ba-range,
.fold-hint,
.fold-scrim,
.fold-scrim-dirty { display: none; }

/* The BEFORE / AFTER labels STAY in the static pair — they are what makes it
   legible. They go only on the split halves; NEVER over the job-*.jpg
   composites, which have the words burned into the frame already. */
.ba-lab {
  position: absolute;
  z-index: 4;
  margin: .75rem;
  padding: .4rem .7rem;
  background: var(--ink);
  color: var(--cream);
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  pointer-events: none;
}

/* ALL FOUR GRID LINES, NOT TWO. `.ba-lab` is absolutely positioned, and for an
   abspos grid child an `auto` end-line does NOT mean "span one track" the way
   it does for an in-flow item — it resolves to the grid container's PADDING
   EDGE. `grid-area: 1 / 1` therefore gave both labels the full height of the
   frame, and `align-self: end` stacked them on top of each other at the very
   bottom, so the no-JS pair rendered with one label and one unlabelled photo. */
.ba-lab.is-before { grid-area: 1 / 1 / 2 / 2; align-self: end; justify-self: start; }
.ba-lab.is-after  { grid-area: 2 / 1 / 3 / 2; align-self: end; justify-self: start; }

.ba-cap {
  margin-block-start: 1rem;
  padding-block-start: .75rem;
  border-top: 2px solid var(--mark);
  max-width: 52ch;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--muted);
}

@media (min-width: 720px) {
  .ba-frame { grid-template-columns: 1fr 1fr; }
  .ba-clip  { grid-area: 1 / 1; }
  .ba-after { grid-area: 1 / 2; }
  .ba-lab.is-before { grid-area: 1 / 1 / 2 / 2; }
  .ba-lab.is-after  { grid-area: 1 / 2 / 2 / 3; }
}

/* .fold with no slider: a stacked block on ink. The photographs first, the
   copy under them at IDENTICAL type sizes — same h1, same phone button, just
   stacked. Nothing is lost. */
.fold {
  background: var(--ink);
  color: var(--cream);
  padding-block: clamp(2rem, 1rem + 3vw, 3.5rem);
}

/* ---------------------------------------------------------------------------
   THE FOLD IS CONTAINED, NOT BLED.
   The previous version stretched a 761x899 portrait half edge to edge with
   object-fit: cover — a 3.4x upscale at 2560, showing about a fifth of the
   picture. Capping the width and containing the image is what makes the
   photograph legible; the letterbox is the same ink as the band, so a portrait
   photo just reads as a narrower picture rather than a cropped one.
   --------------------------------------------------------------------------- */
.fold-in {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gut);
  display: grid;
  gap: clamp(1.75rem, 1rem + 3vw, 3rem);
}

@media (min-width: 960px) {
  .fold-in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    align-items: center;
    gap: clamp(2rem, 1rem + 3.5vw, 4rem);
  }
}

/* --- the carousel ------------------------------------------------------- */
/* Not a list: see src/pages/home.mjs. Each slide is a role="group"
   aria-roledescription="slide", which an <li> may not be. */
.show-track { margin: 0; padding: 0; }

/* Fixed frame, contained image. Every job keeps its own proportions inside it
   and none of them is ever cut. 4:3 on a wide screen, 1:1 on a phone — the two
   ratios that waste least space across a square, a portrait and a tall
   portrait, which is what the three real jobs are. */
.ba-frame {
  position: relative;
  background: var(--ink-2);
  border: 1px solid var(--line-dark);
  overflow: hidden;
}

.ba-after img,
.ba-clip img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.show-cap {
  margin-block-start: .85rem;
  font-size: var(--t-small);
  color: var(--muted-ink);
}

.show-cap b { color: var(--cream); font-weight: 600; }

.show-bar {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-block-start: 1rem;
}

.show-nav {
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: none;
  background: transparent;
  color: var(--cream);
  border: 1px solid var(--line-dark);
  cursor: pointer;
  font-size: 1rem;
}

.show-nav:hover { border-color: var(--orange); color: var(--orange); }

.show-dots { display: flex; gap: .5rem; flex: 1; }

/* The count is the phone control; the dots are the pointer one. Nine 44px tap
   targets are 460px of dots before the arrows are counted, which does not fit a
   phone and cannot be shrunk without dropping under the minimum a thumb can
   hit. Above 720px they fit and a mouse can use them, so they stay. */
.show-count {
  flex: 1;
  margin: 0;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  line-height: var(--lh-micro);
  letter-spacing: var(--tr-mono);
  text-align: center;
  color: var(--muted-ink);
}

.show-count b { color: var(--cream); font-weight: 600; }

@media (min-width: 720px) { .show-count { display: none; } }
@media (max-width: 719.98px) { .show-dots { display: none; } }

/* 44px hit area, 10px visual — the tap target is the button, the dot is drawn
   by the ::before so it can stay small without being hard to press. */
.show-dot {
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}

.show-dot::before {
  content: '';
  position: absolute;
  inset: 50% auto auto 50%;
  translate: -50% -50%;
  width: 10px;
  height: 10px;
  border: 1px solid var(--muted-ink);
}

.show-dot[aria-current='true']::before { background: var(--orange); border-color: var(--orange); }
.show-dot:hover::before { border-color: var(--orange); }

.fold-copy > * + * { margin-block-start: 1rem; }
.fold .wrap,
.fold-copy { position: relative; }

.fold-kick {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--cream);
  opacity: .86;
}

.fold-h {
  font-size: var(--t-fold);
  line-height: var(--lh-display);
  max-width: 16ch;
  /* Last resort against a long compound word at 360px. */
  overflow-wrap: anywhere;
}

.fold-sub {
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
  max-width: 46ch;
  color: var(--cream);
}

/* ---------------------------------------------------------------------------
   THE CYCLING LEDE. "We haul away [the hot tub]" — the one moving thing in the
   fold, and the only place on the page where a word is allowed to change.
   It answers "what does this business actually do" with a concrete noun rather
   than a category, which is the thing the h1 above it cannot do without losing
   the ranking phrase.
   --------------------------------------------------------------------------- */
/* Set at heading size, not lead size. This line is the one that says what the
   business actually removes, and at 18px under a 34px headline it read as a
   caption. It is now the second-loudest thing in the fold after the h1 —
   which is the right order, because "we haul away the hot tub" is more use to
   a stranger than any adjective on the page. */
.fold-lede {
  font-family: var(--ff-display);
  font-size: clamp(1.75rem, 1.2rem + 2.6vw, 3rem);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--cream);
  margin-block-start: 1rem;
  text-wrap: balance;
}

/* inline-grid stacks the outgoing and incoming word in the same cell, so the
   line never reflows as the word length changes — "the fridge" and "a garage
   full of it" are very different widths and a shifting baseline would drag the
   buttons below it up and down. */
/* THE WORD ALWAYS GETS ITS OWN LINE.
   Two separate shifts had to be killed here, and only one of them was obvious.
   The inline-grid stacks the outgoing and incoming word in a single cell, so
   the WORD's own box never changes width mid-swap. But the LINE was still
   rewrapping: "the hot tub" fitted beside "We haul away" and "a garage full of
   it" did not, so the paragraph flipped between one line and two and every
   element under it — including both buttons — jumped every 2.6 seconds.
   Making it display:block means the block is always exactly two lines tall
   regardless of which noun is showing. Nothing below it can move. */
.swap {
  display: grid;
  justify-content: start;
  overflow: hidden;
  color: var(--orange);
}

.swap-w {
  grid-area: 1 / 1;
  white-space: nowrap;
}

.fold-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  margin-block-start: 1.5rem;
}

.fold-trust,
.fold-hint {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: 1.7;
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  /* --cream at 0.86, NEVER --muted-ink. Muted grey over photography is exactly
     where legibility actually fails, and this line has to survive both the
     scrimmed photo and the flat ink field. */
  color: var(--cream);
  opacity: .86;
}

/* PHONES: TWO BUTTONS, SIDE BY SIDE, SMALLER.
   They used to stack full-width at 56px tall each, which put two slabs and a
   trust line between the headline and the photograph — and there is already a
   permanent Call / Text / Free quote bar fixed to the bottom of every phone
   screen, plus a call button in the header. Three copies of the same action, the
   biggest of which was pushing the actual work below the fold.
   Side by side at 48px keeps both within the tap-target floor, halves the
   vertical cost, and lets the hero get to the photograph sooner. */
/* ---------------------------------------------------------------------------
   PHONES: THE COPY OWNS THE FIRST SCREEN, THE PHOTOS START BELOW IT.
   Landing on a half-cropped photograph reads as a page that has already started
   without you. The copy block now fills the first viewport — headline, what we
   haul, the two buttons, the trust line — and the carousel begins underneath,
   so the photos are something you scroll INTO rather than something you arrive
   mid-way through.

   svh, not vh: on a phone `vh` is the height with the browser chrome HIDDEN, so
   sizing to it puts the bottom of the block under the address bar on first
   paint and shifts everything the moment the user scrolls. svh is the smallest
   viewport, which is the one actually visible when the page loads.
   The two bars are subtracted because both are fixed and neither ever scrolls
   away: the header at the top and the call bar at the bottom.
   --------------------------------------------------------------------------- */
@media (max-width: 719.98px) {
  .fold-copy {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: calc(100svh - var(--hdr-h) - 3.5rem);
    padding-block-end: .5rem;
  }

  /* A little more air between the parts now that they have a screen to
     themselves — the cramped rhythm was most of what read as unpolished. */
  .fold-copy > * + * { margin-block-start: 1.15rem; }
  .fold-actions { margin-block-start: 1.75rem; }
}

@media (max-width: 559.98px) {
  .fold-actions {
    display: grid;
    /* The call button gets the wider column: it carries a phone number and the
       other one carries two words. An even split broke "Call (954) 774-0277"
       across two lines, which makes the primary action look like an accident. */
    grid-template-columns: 1.25fr 1fr;
    gap: .5rem;
  }

  .fold-actions .btn {
    width: 100%;
    min-height: 48px;
    padding: .7rem .5rem;
    font-size: .9rem;
    text-align: center;
    white-space: nowrap;
  }
}

/* Below ~360px there is no honest way to fit a formatted phone number beside a
   second button, so the pair stacks again rather than shrinking the type past
   readable. The fixed call bar at the bottom of the screen means neither is
   ever the only way to ring. */
@media (max-width: 359.98px) {
  .fold-actions { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------------
   8.2  THE SLIDER, LIVE — everything below is behind html.js-ba, which the
   inline <head> capability script sets BEFORE FIRST PAINT:

     if (CSS.supports('clip-path','inset(0 50% 0 0)') && 'PointerEvent' in window)
       document.documentElement.className += ' js-ba js-chrome is-top'

   Before paint matters: if the flag arrived at DOMContentLoaded the fold would
   paint stacked and then jump to overlaid — a layout shift on the LCP element,
   which is the single worst place on the site to have one.
   ------------------------------------------------------------------------- */

/* THE FRAME TAKES THE PHOTOGRAPH'S OWN RATIO.
   A single shared ratio cannot serve these three jobs — they are 1.010, 0.846
   and 0.403 — so any fixed frame either crops the tall one savagely or
   letterboxes the square one. `--ba-ar` is written inline per slide from the
   real pixel dimensions (src/pages/home.mjs), so the frame always matches its
   image exactly: nothing is cropped, nothing is letterboxed, and `cover` below
   is a no-op that still guarantees both layers fill identical boxes, which is
   what keeps the seam from jittering as you drag.

   Height is the constant, not width. Fixing the HEIGHT and letting width follow
   the ratio means advancing the carousel never changes the band's height — a
   varying height would be a layout shift on every auto-advance. On narrow
   screens max-width takes over and the height reduces instead.

   Both halves also carry width/height attributes, so the box is reserved before
   a byte of image arrives. */
/* WIDTH LEADS, HEIGHT FOLLOWS, AND THE CAP IS EXPRESSED AS A WIDTH.
   Driving this off `height` with `width: auto` sized the frame from the
   viewport height, which on a 390x800 phone made it ~464px wide inside a 390px
   column — the page overflowed sideways and clipped the h1. So width is 100%
   (it can never exceed its column) and aspect-ratio derives the height.

   The cap used to be a max-height, and the comment here used to claim that when
   it bit, `contain` letterboxed invisibly against a near-black site. That was
   true of the original photographs, which were near-square. It is not true of
   the owner's own 1400x1867 portraits: a 0.75 photo in a frame whose height was
   capped to 480px while its width stayed at the full column produced a
   landscape box with 200px of black down each side. The client's words were
   "those black bars that do appear on the photos".

   A max-height and an aspect-ratio cannot both win. So the cap is now a
   MAX-WIDTH derived from the same ratio — the frame is never taller than the
   old limit, but it gets there by being narrower rather than by going landscape
   and letterboxing. aspect-ratio is honoured at every width, so `contain`
   never has anything to letterbox and nothing is cropped either. */
html.js-ba .ba-frame {
  display: block;
  gap: 0;
  width: 100%;
  aspect-ratio: var(--ba-ar, 4 / 5);
  max-width: calc(min(64vh, 30rem) * (var(--ba-ar, 4 / 5)));
  margin-inline: auto;
}

@media (min-width: 560px) {
  html.js-ba .ba-frame { max-width: calc(min(66vh, 34rem) * (var(--ba-ar, 4 / 5))); }
}

html.js-ba .ba-after,
html.js-ba .ba-clip {
  position: absolute;
  inset: 0;
  display: block;
}

/* IDENTICAL BOX, IDENTICAL CROP. A one-pixel difference between the two layers
   shows as jitter at the seam while you drag, which reads as a fault in the
   photograph rather than in the code. */
html.js-ba .ba-after,
html.js-ba .ba-before,
html.js-ba .ba-after img,
html.js-ba .ba-before img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: 50% 50%;
}

/* The wipe. A RIGHT inset, so the surviving strip is the LEFT one and the left
   of the frame is always the BEFORE. One custom property drives it, and
   clip-path composites — a drag touches no layout property and forces no
   reflow. */
html.js-ba .ba-clip {
  clip-path: inset(0 calc(100% - var(--ba-pos)) 0 0);
}

/* The seam is full-width and TRANSLATED, not positioned with `left`:
   translateX is a compositor transform and `left` is layout. It is full-width
   because a percentage translate resolves against the ELEMENT's own width, so
   a 2px bar translated 62% would move 1.24px. The visible line is drawn on its
   leading edge instead. */
html.js-ba .ba-seam {
  display: block;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  z-index: 3;
  transform: translateX(var(--ba-pos));
  pointer-events: none;
}

html.js-ba .ba-seam::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -1px;
  width: 2px;
  background: var(--cream);
}

/* THE ONLY ROUND OBJECT ON THE SITE, and it is the one thing you are meant to
   touch. Everything else on every page is radius 0. Cream with an ink border
   so it holds up over a white sky and over a black truck bed alike — a border,
   not a shadow, because it is doing a border's job. */
html.js-ba .ba-grip {
  display: grid;
  grid-auto-flow: column;
  place-content: center;
  gap: 5px;
  position: absolute;
  left: 0;
  top: var(--ba-grip-y);
  width: 48px;
  height: 48px;
  margin-left: -24px;
  margin-top: -24px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--cream);
  pointer-events: none;
}

html.js-ba .ba-grip::before,
html.js-ba .ba-grip::after {
  content: '';
  width: 7px;
  height: 7px;
  border-top: 2px solid var(--ink);
  border-right: 2px solid var(--ink);
}

html.js-ba .ba-grip::before { transform: rotate(-135deg); }  /* points left */
html.js-ba .ba-grip::after  { transform: rotate(45deg); }    /* points right */

/* ---------------------------------------------------------------------------
   DRAG VERSUS VERTICAL SCROLL ON A PHONE — the failure mode that kills drag
   heroes, so it is specified precisely rather than hoped about.

   The range is NOT full-bleed. It is a 96px-tall full-width strip centred on
   the grip; above and below it the fold does not capture touches at all. And
   `touch-action: pan-y` hands vertical panning back to the browser, so a page
   scroll that happens to start on the strip still scrolls the page. The
   runtime never calls preventDefault on a touch event and never registers a
   non-passive touch listener — it listens to `input`, which is all touch
   needs. A visitor stuck mid-page is worse off than a visitor who never had a
   slider at all, and most of the traffic here is a phone.
   ------------------------------------------------------------------------- */
html.js-ba .ba-range {
  display: block;
  position: absolute;
  left: 0;
  top: var(--ba-grip-y);
  z-index: 4;
  width: 100%;
  height: 96px;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  transform: translateY(-50%);
  touch-action: pan-y;
  cursor: ew-resize;
  pointer-events: auto;
}

html.js-ba .ba-range::-webkit-slider-runnable-track {
  height: 96px;
  border: 0;
  background: transparent;
}
html.js-ba .ba-range::-moz-range-track {
  height: 96px;
  border: 0;
  background: transparent;
}
html.js-ba .ba-range::-moz-range-progress { background: transparent; }

/* The thumb is invisible, but it is what a finger and a cursor grab, so it is
   sized for a thumb rather than for a pixel. */
html.js-ba .ba-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 48px;
  height: 48px;
  border: 0;
  background: transparent;
  cursor: ew-resize;
}
html.js-ba .ba-range::-moz-range-thumb {
  width: 48px;
  height: 48px;
  border: 0;
  background: transparent;
  cursor: ew-resize;
}

/* The ring marks the STRIP, which is genuinely useful information: it shows a
   keyboard user exactly where the drag zone is. Inset so the frame's overflow
   cannot clip it. */
html.js-ba .ba-range:focus-visible { outline: none; }

/* The ring lands on the grip instead — the thing the eye is already on. The
   frame is the :has() host because .ba-seam precedes .ba-range in the markup,
   so a sibling combinator cannot reach backwards to it. */
html.js-ba .ba-frame:has(.ba-range:focus-visible) .ba-grip {
  box-shadow: 0 0 0 3px var(--focus);
}

/* Without :has() there is no way to style the grip from the input's state, and
   no focus ring at all is worse than an ugly one. Restore the outline there. */
@supports not selector(:has(*)) {
  html.js-ba .ba-range:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: -3px;
  }
}

/* Standalone .ba (service + gallery pages): corner labels at the bottom, grip
   centred, and the whole frame grabbable on a fine pointer only — a mouse
   cannot confuse a horizontal drag with a scroll; a finger can. */
html.js-ba .ba-lab.is-before,
html.js-ba .ba-lab.is-after {
  grid-area: auto;
  bottom: .75rem;
  margin: 0;
}
html.js-ba .ba-lab.is-before { left: .75rem; right: auto; }
html.js-ba .ba-lab.is-after  { right: .75rem; left: auto; }

@media (pointer: fine) {
  html.js-ba .ba-frame { cursor: ew-resize; }
}

/* ---------------------------------------------------------------------------
   8.3  THE FOLD, LIVE
   ------------------------------------------------------------------------- */

html.js-ba .fold {
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  padding-block: 0;
  /* svh, deliberately. `vh` overshoots under the mobile URL bar and pushes the
     copy off-screen; `dvh` resizes the fold while you scroll, which makes the
     seam and the headline drift under the reader's thumb. */
  min-height: clamp(560px, 86vh, 820px);
  min-height: clamp(560px, 86svh, 820px);
  overflow: hidden;
  isolation: isolate;
  contain: layout paint;
}

/* The full-bleed fold skin (.ba-bleed, .fold-scrim, .fold-scrim-dirty and the
   .fold-copy gradient) was removed when the fold became a contained carousel.
   It cropped a 761px portrait source to about a fifth of itself at desktop
   widths. The .ba component keeps its one remaining skin, defined above. */

/* MOBILE: BUY BACK THE PHOTOGRAPH.
   On a 636px-tall phone fold the copy row — kicker, three-line h1, three-line
   sub, two buttons, trust line, drag hint — ran to about 470px of it. Stack the
   three scrims over that and roughly a fifth of the "full-bleed photograph" was
   actually photograph; the wall of scrap lumber, which is the entire reason
   this pair was chosen, sat under the copy scrim.
   The kicker and the drag hint are the two dispensable lines: the kicker repeats
   what the header and the trust line already say, and the hint describes an
   affordance the seam and grip already advertise. Dropping them below 720px
   returns roughly 120px of picture — about a quarter of the fold — to the only
   thing in it that sells the work. Chanel's mirror: remove one accessory. */
/* Both of these came back when the copy got a screen of its own. They were cut
   while the fold was a cropped photograph fighting the copy for room; that
   constraint is gone, and the kicker is the line that names Coconut Creek and
   the two counties — the most locally useful sentence above the fold. */


/* ===========================================================================
   09  HOME BANDS 2–11

   Adjacency proof — every neighbouring pair differs in BOTH field and column
   count, so no two bands can read as one undifferentiated field:
     1 ink/1 -> 2 cream-2/4 -> 3 ink/3 -> 4 cream/1 -> 5 cream-2/3 ->
     6 ink/2 -> 7 cream/4 -> 8 ink/2 -> 9 cream/1 -> 10 cream-2/3 -> 11 ink/1
   =========================================================================== */

/* ---------------------------------------------------------------------------
   BAND 2 — .plate.  Four hard facts in mono, hairline-divided, riveted tight
   under the photograph. NO NUMERALS AND NO ICONS.

   The first draft of this band was four big numbers with small labels — the
   exact shape the design brief names as THE default hero-support move, and
   dishonest here besides: voice.md forbids invented statistics and there are
   no real ones (no job count, no years, no crew size). Any big number would
   have been fake. These are four COMMITMENTS at mono label size instead,
   packed tight against the fold so the band reads as a riveted spec plate
   rather than a dashboard. The absence of numbers is the honest design.
   ------------------------------------------------------------------------- */
.plate {
  display: grid;
  gap: 0;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
}

.plate-cell {
  padding-block: 1rem;
  border-top: 1px solid var(--rule-c, var(--rule-strong));
}

.plate-cell:first-child { border-top: 0; }

.plate-k {
  display: block;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--mark);
}

.plate-v {
  display: block;
  margin-block-start: .4rem;
  max-width: 30ch;
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--fg);
}

@media (min-width: 560px) {
  .plate { grid-template-columns: 1fr 1fr; }
  .plate-cell {
    padding: 1rem 1.25rem;
    border-top: 0;
    border-left: 1px solid var(--rule-c, var(--rule-strong));
  }
  /* 2x2: cells 1 and 3 start a row, so they lose the vertical, and the second
     row keeps a horizontal to separate it from the first. */
  .plate-cell:nth-child(odd) { border-left: 0; padding-left: 0; }
  .plate-cell:nth-child(n + 3) { border-top: 1px solid var(--rule-c, var(--rule-strong)); }
}

@media (min-width: 900px) {
  .plate { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  /* Undoes the second-row horizontal from the 560 block at the same
     specificity, rather than reaching for !important. */
  .plate-cell { border-top: 0; border-left: 1px solid var(--rule-c, var(--rule-strong)); }
  .plate-cell:nth-child(odd) { border-left: 1px solid var(--rule-c, var(--rule-strong)); }
  .plate-cell:nth-child(n + 3) { border-top: 0; }
  .plate-cell:first-child { border-left: 0; padding-left: 0; }
}

/* ---------------------------------------------------------------------------
   BAND 3 — .index.  All eleven services as a ruled index: name, then what it
   actually covers, one line each. The densest text on the page.

   NO ICONS, NO CARDS, NO 01/02/03. Eleven services are not a sequence, so
   numbering them was decoration pretending to be structure. This is denser,
   faster to scan, and puts eleven internal links one screen below the fold
   instead of nine. Reused verbatim for related services on inner pages.
   ------------------------------------------------------------------------- */
.index {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: var(--gap);
  row-gap: 0;
}

.index-row {
  position: relative;
  display: block;
  padding: .95rem 2rem .95rem 1rem;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
  color: var(--fg);
  text-decoration: none;
}

/* THE hover rule for every ruled row on this site: a 3px orange bar grows from
   the leading edge and the title shifts 6px. No lift, no scale, no shadow
   bloom — anywhere, on anything. */
.index-row::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--accent-fill);
  transform: scaleX(0);
  transform-origin: left;
}

.index-row:hover::before,
.index-row:focus-visible::before { transform: scaleX(1); }

.index-name {
  display: block;
  font-family: var(--ff-sub);
  font-weight: 800;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--tr-display);
  color: var(--fg);
}

.index-row:hover .index-name,
.index-row:focus-visible .index-name { color: var(--accent); }

.index-note {
  display: block;
  margin-block-start: .2rem;
  max-width: 46ch;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--muted);
}

/* U+2192 IS NOT IN THE LATIN SUBSET of Teko, Montserrat or Inter (Google's
   latin subset carries U+2191 and U+2193 but not U+2192
   §4.3), so the `&rarr;` the templates emit here renders from whatever system
   font the device falls back to — a visible mismatch on desktop and a tofu box
   on some Android builds. Every instance of it on the site is inside an
   aria-hidden element, so collapsing the glyph costs assistive tech nothing,
   and a real chevron is drawn from two rotated borders — the same construction
   the .ask marker and the .ba grip already use. */
.index-go {
  position: absolute;
  right: 0;
  top: 1.2rem;
  font-size: 0;
  line-height: 0;
  color: var(--accent);
}

.index-go::after,
.job-more span[aria-hidden]::after {
  content: '';
  display: inline-block;
  width: .4rem;
  height: .4rem;
  border-top: 2px solid currentColor;
  border-right: 2px solid currentColor;
  transform: rotate(45deg);
}

.job-more span[aria-hidden] {
  font-size: 0;
  line-height: 0;
}

.job-more span[aria-hidden]::after {
  margin-left: .45rem;
  vertical-align: .1em;
}

@media (min-width: 720px)  { .index { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .index { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* ---------------------------------------------------------------------------
   BAND 4 — .run.  How a job runs, four steps.

   THE ONLY NUMBERED STRUCTURE ON THE HOME PAGE, because this is the one place
   where order genuinely carries information the reader needs. The numeral
   hangs in the left gutter in mono, and it is a real <ol>.
   ------------------------------------------------------------------------- */
.run {
  display: grid;
  gap: var(--gap);
}

/* THREE children, TWO columns — so the placement has to be explicit. A step is
   <span class="run-n">, <h3 class="run-h">, <p class="run-p">, and left to
   auto-flow the paragraph drops into row two of the NUMERAL gutter and sets
   itself two words wide. The numeral hangs; the prose is one column. */
.run-step {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr);
  column-gap: 1rem;
  align-items: start;
  max-width: var(--measure);
}

.run-n { grid-column: 1; grid-row: 1; }
.run-h { grid-column: 2; grid-row: 1; }
.run-p { grid-column: 2; grid-row: 2; }

.run-n {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: 1.9;
  letter-spacing: var(--tr-mono);
  font-variant-numeric: tabular-nums;
  color: var(--mark);
}

.run-h {
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
  max-width: 26ch;
}

.run-p {
  margin-block-start: .4rem;
  max-width: var(--measure);
  color: var(--muted);
}

@media (min-width: 720px) {
  .run-step { grid-template-columns: 4rem minmax(0, 1fr); column-gap: 1.5rem; }
}

/* ---------------------------------------------------------------------------
   BAND 5 — .jobs.  The two remaining real before/after composites, plus a
   third cell that is only a link to the full gallery.

   NOTHING OVERLAYS THESE IMAGES. site.transformations[].img is the original
   side-by-side composite and it already has BEFORE and AFTER burned into the
   frame by whoever shot it. A drawn label here doubles the word on screen, and
   a cover-crop eats the burned-in one — which is why these keep their natural
   proportion and the cells are top-aligned. Uneven column bottoms are the
   correct outcome, not something to fix with object-fit.
   ------------------------------------------------------------------------- */
.jobs {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap);
  align-items: start;
}

.job { margin: 0; }

.job-media img {
  width: 100%;
  border: 1px solid var(--rule-c, var(--rule-strong));
}

.job-cap {
  margin-block-start: 1rem;
  padding-block-start: .75rem;
  border-top: 2px solid var(--mark);
}

.job-cap h3 { margin-block-end: .3rem; }

.job-cap p {
  max-width: 46ch;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--muted);
}

/* The third cell is only a link — a flat panel, no photo — so the row reads as
   "two jobs and the way to the rest" rather than as three equal things. */
.job-more {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: .75rem;
  min-height: 12rem;
  padding: 1.25rem;
  border: 1px solid var(--rule-c, var(--rule-strong));
  background: var(--panel);
  color: var(--fg);
  text-decoration: none;
  font-family: var(--ff-display);
  font-weight: 700;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--tr-display);
}

.job-more:hover { color: var(--accent); border-color: var(--accent-fill); }

@media (min-width: 720px)  { .jobs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .jobs { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* ---------------------------------------------------------------------------
   BAND 6 — .johnny.  team-flex.jpg and the founder story: who is going to be
   standing in your garage. Doubles as the generic two-column media+prose
   block; .johnny-rev flips the sides.
   ------------------------------------------------------------------------- */
.johnny {
  display: grid;
  gap: var(--gap);
  align-items: start;
}

/* TWO PHOTOGRAPHS, SIDE BY SIDE — not stacked.
   The band holds two portraits of the same person. Stacked, that is roughly a
   thousand pixels of Johnny before the copy starts, and since the band moved to
   third on the home page it is the first thing under the fold. As a pair it
   reads as "here he is" and takes half the height. The sizes attribute in
   home.mjs has always said 22vw — half the 44vw column — so this is the layout
   the markup was written for. */
.johnny-media {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: .5rem;
  align-items: start;
}

/* The pair layout is conditional on there being a pair. The home page band
   holds two portraits; every city and service page holds one, and forcing two
   columns left those single photos at half width beside an empty column.
   Where :has() is unsupported the rule simply does not apply and a two-photo
   band stacks, which is what it did before — a safe way to fail. */
.johnny-media:has(> :nth-child(2)) {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.johnny-media img {
  width: 100%;
  border: 1px solid var(--rule-c, var(--rule-strong));
  aspect-ratio: 3 / 4;
  object-fit: cover;
  object-position: 50% 22%;
}

.johnny-copy > * + * { margin-block-start: 1rem; }

/* The donation band is two sentences against a tall portrait, so top-aligning
   the copy the way every other .johnny band does leaves a column of empty ink
   beside the photograph. Centred, the short column reads as deliberate. Scoped
   to this band: everywhere else the copy is long enough that start is right. */
@media (min-width: 900px) {
  #donation .johnny-copy { align-self: center; }
}

.johnny-copy p {
  max-width: var(--measure);
  color: var(--muted);
}

/* The first paragraph after the heading carries the weight of a deck. */
.johnny-copy h2 + p {
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
  color: var(--fg);
}

@media (min-width: 900px) {
  .johnny { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .johnny-media img { aspect-ratio: 3 / 4; object-position: 50% 22%; }
  /* The media is always first in the markup so the phone layout leads with the
     photograph; the reversal is a desktop-only concern. */
  .johnny-rev .johnny-media { order: 2; }
}

/* ---------------------------------------------------------------------------
   BAND 7 — .said.  The four real reviews as four columns of plain text with a
   hairline between. No cards, NO STAR GLYPHS — we publish no rating and ship
   no Review schema, so drawn stars would be a claim we are not making.
   ------------------------------------------------------------------------- */
.said {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}

/* The four reviews are four columns of unequal length; without this the mono
   attributions sit at four different heights and the band reads as unformatted
   paste rather than a composition. One line each. */
.quote { display: flex; flex-direction: column; }
.quote-by { margin-top: auto; }

.quote {
  margin: 0;
  padding-block: 1.5rem;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
}

.quote-t {
  max-width: 34ch;
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--fg);
}

.quote-by {
  display: block;
  margin-block-start: .9rem;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  font-style: normal;
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--mark);
}

.quote-job {
  display: block;
  margin-block-start: .3rem;
  font-size: var(--t-small);
  color: var(--muted);
}

@media (min-width: 720px) {
  .said { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--gap); }
}

@media (min-width: 1100px) {
  .said { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---------------------------------------------------------------------------
   BAND 8 — .map.  The fifteen city pages as a ruled index, ZIPs hanging right
   in mono. A ZIP list is data, so it is the one thing here in the mono face —
   a directory line, the way a printed one sets.
   ------------------------------------------------------------------------- */
.map {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: var(--gap);
  row-gap: 0;
}

.map-col { display: block; }

.map-row {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .35rem 1rem;
  padding: .85rem 0 .85rem 1rem;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
  color: var(--fg);
  text-decoration: none;
}

.map-row::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--accent-fill);
  transform: scaleX(0);
  transform-origin: left;
}

.map-row:hover::before,
.map-row:focus-visible::before { transform: scaleX(1); }

.map-city {
  font-family: var(--ff-sub);
  font-weight: 800;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--tr-display);
}

.map-row:hover .map-city,
.map-row:focus-visible .map-city { color: var(--accent); }

.map-zips {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  letter-spacing: var(--tr-mono);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  text-align: right;
}

/* The page you are on: a rule and a colour together, never colour alone. The
   markup pairs this with aria-current on the link. */
.map-row.is-active::before { transform: scaleX(1); }
.map-row.is-active .map-city { color: var(--accent); }

@media (min-width: 720px) { .map { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ---------------------------------------------------------------------------
   BAND 9 — .asked.  Six FAQs as native <details>: they open with JavaScript
   blocked, they are findable by in-page search, and they need no widget code.
   Narrow measure, generous air.
   ------------------------------------------------------------------------- */
.asked { max-width: var(--measure); }

.ask { border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong)); }

.ask:last-child { border-bottom: var(--rule-w) solid var(--rule-c, var(--rule-strong)); }

.ask-q {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding-block: 1.15rem;
  list-style: none;
  cursor: pointer;
  font-family: var(--ff-display);
  font-weight: 700;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--tr-display);
  color: var(--fg);
}

.ask-q::-webkit-details-marker { display: none; }
.ask-q::marker { content: ''; }

/* The marker is drawn, not typed, so it is never read aloud or copied. */
.ask-q::after {
  content: '';
  flex: none;
  width: .55rem;
  height: .55rem;
  margin-top: .45em;
  margin-left: auto;
  border-top: 2px solid var(--accent-fill);
  border-right: 2px solid var(--accent-fill);
  transform: rotate(45deg);
  transform-origin: 55% 45%;
}

.ask[open] > .ask-q::after { transform: rotate(135deg); }

.ask-q:hover { color: var(--accent); }

.ask-a {
  padding-block: 0 1.35rem;
  max-width: var(--measure);
  color: var(--muted);
}

.ask-a > * + * { margin-block-start: .9rem; }

/* ---------------------------------------------------------------------------
   BAND 10 — .read.  Three latest posts: mono date, title, one line.
   ------------------------------------------------------------------------- */
.read {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: var(--gap);
  row-gap: 0;
}

.read-row {
  position: relative;
  display: block;
  padding: 1.1rem 0 1.1rem 1rem;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
  color: var(--fg);
  text-decoration: none;
}

.read-row::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--accent-fill);
  transform: scaleX(0);
  transform-origin: left;
}

.read-row:hover::before,
.read-row:focus-visible::before { transform: scaleX(1); }

.read-date {
  display: block;
  margin-block-end: .5rem;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  font-variant-numeric: tabular-nums;
  color: var(--mark);
}

.read-h {
  display: block;
  max-width: 24ch;
  font-family: var(--ff-sub);
  font-weight: 800;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--tr-display);
}

.read-row:hover .read-h,
.read-row:focus-visible .read-h { color: var(--accent); }

.read-x {
  display: block;
  margin-block-start: .4rem;
  max-width: 44ch;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--muted);
}

@media (min-width: 720px)  { .read { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .read { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* --- the teaser thumbnail -------------------------------------------------
   The row was a ruled block of text and it stays one; the photo sits above the
   date as a band of colour, not as a card. 3:2 is reserved with aspect-ratio so
   the row never reflows when the image lands. */
.read-thumb {
  display: block;
  margin-block-end: .95rem;
  overflow: hidden;
  background: var(--rule-strong);
}

.read-thumb img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

@media (prefers-reduced-motion: no-preference) {
  .read-thumb img {
    transition: transform 420ms var(--ease-out), filter 420ms var(--ease-out);
    filter: saturate(.8);
  }
  .read-row:hover .read-thumb img,
  .read-row:focus-visible .read-thumb img {
    transform: scale(1.035);
    filter: saturate(1);
  }
}

/* --- the post hero photograph ---------------------------------------------
   Stock, and captioned as such. NOT faded in: it is the LCP element on a post
   page and Chrome does not count a fully transparent element as painted. */
.post-fig {
  margin: clamp(1.75rem, 4vw, 2.75rem) 0 0;
}

.post-fig img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

.post-fig-cap {
  margin-block-start: .7rem;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  line-height: var(--lh-micro);
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  color: var(--muted);
}

.post-fig-cap a { color: inherit; }
.post-fig-cap a:hover { color: var(--accent); }

/* ---------------------------------------------------------------------------
   BAND 11 — .close.  One sentence, the phone number at --t-mega in Teko, one text-a-photo link. Nothing else. The page ends cleared.
   ------------------------------------------------------------------------- */
.close { max-width: 24ch; }

.close-h {
  font-size: var(--t-h2);
  line-height: var(--lh-h2);
  max-width: 18ch;
}

.close-tel {
  display: block;
  margin-block-start: clamp(1.25rem, .5rem + 2.4vw, 2.5rem);
  font-family: var(--ff-display);
  font-weight: 700;
  /* --t-mega, held back only where it would not fit. At 360px the number is
     about 7.6em wide in the display face, which overruns a 320px column at 44px;
     min() drops it to ~37px there and never bites above ~1200px. */
  font-size: min(var(--t-mega), calc(8vw + 0.5rem));
  line-height: var(--lh-display);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
}

.close-tel:hover { color: var(--accent); }

.close-note {
  margin-block-start: clamp(1.25rem, .75rem + 1.6vw, 2rem);
  max-width: 40ch;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: 1.9;
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--muted);
}

.close-note a { color: var(--accent); text-decoration-color: var(--accent); }

@media (min-width: 900px) {
  .close { max-width: none; }
  .close-h { max-width: 20ch; }
}


/* ===========================================================================
   10  INNER PAGES — HERO, QUICK ANSWER, PROSE, TABLES, FIGURES
   =========================================================================== */

/* --- inner hero ----------------------------------------------------------
   Ink, copy only, NO photograph. Shorter than the fold on purpose: a visitor
   arrived here from a search result and wants the answer, not another poster. */
.phero {
  background: var(--ink);
  color: var(--cream);
}

.phero-in {
  display: grid;
  gap: clamp(1.5rem, 1rem + 2vw, 2.5rem);
}

.phero-h {
  font-size: var(--t-h1);
  line-height: var(--lh-display);
  max-width: 18ch;
}

.phero-sub {
  margin-block-start: 1rem;
  max-width: var(--measure);
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
  color: var(--muted);
}

/* Mono data rail: service area, response time, phone. Data only; it never sets
   a sentence. Below 900px it is a plain stacked list under the deck. */
.phero-rail {
  display: grid;
  gap: .75rem;
  margin: 0;
  padding-block-start: 1.25rem;
  border-top: 1px solid var(--line-dark);
}

.phero-rail-k {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--mark);
}

.phero-rail-v {
  margin: 0;
  font-family: var(--ff-sub);
  font-size: var(--t-small);
  line-height: 1.4;
  color: var(--cream);
}

.phero-rail-v a { color: var(--orange); text-decoration-color: var(--orange); }

@media (min-width: 900px) {
  .phero-in { grid-template-columns: minmax(0, 1.35fr) minmax(0, .65fr); align-items: end; }

  /* Four children auto-place into two rows: h1 | deck, then buttons | rail.
     align-items: end is right for the first row — the headline and the deck
     settle onto a shared bottom edge, which is the point. It is wrong for the
     second, because the data rail is about 80px taller than a button and
     bottom-aligning the buttons inside that row opens 80px of empty black
     between the headline and the only two controls in the hero. Both second-row
     items start instead, so the buttons sit directly under the headline. */
  .phero-in > .fold-actions,
  .phero-in > .phero-rail { align-self: start; }
  /* The two-column hero is built for a copy block plus a data rail — exactly two
     children. A blog post hero has four loose ones (meta, h1, excerpt, photo),
     and auto-placement dealt them alternately into the two columns: the headline
     landed in the narrow right column and wrapped to eight lines while the date
     sat stranded at the bottom of the left one. An article hero stacks. */
  .phero-in.is-stack { grid-template-columns: minmax(0, 1fr); align-items: start; gap: 0; }
  .phero-in.is-stack > * { max-width: none; }
  .phero-in.is-stack .phero-h { max-width: 20ch; }
  .phero-in.is-stack .phero-sub { max-width: var(--measure); }
  /* The hairline between the two columns lands exactly on the grid line rather
     than being drawn near it. */
  .phero-rail {
    border-top: 0;
    border-left: 1px solid var(--line-dark);
    padding-block-start: 0;
    padding-left: clamp(1.5rem, 3vw, 2.5rem);
  }
}

/* --- THE QUICK ANSWER ----------------------------------------------------
   40–60 words, first on every service and city page, behind a 2px orange left
   rule. THIS IS THE BLOCK AN AI ANSWER ENGINE LIFTS. It stays first and it
   stays intact, and nothing decorative goes near it — no panel fill competing
   with the sentence, no icon, no chip. */
.answer {
  max-width: var(--measure);
  padding-left: clamp(1rem, 3vw, 1.5rem);
  border-left: 2px solid var(--accent-fill);
}

.answer-k {
  display: block;
  margin-block-end: .6rem;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--accent);
}

.answer p {
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
  color: var(--fg);
}

.answer p + p { margin-block-start: .8rem; }

/* --- prose --------------------------------------------------------------- */
.prose { max-width: var(--measure); }

.prose > * + * { margin-block-start: 1rem; }
.prose > * + :is(h2, h3, h4) { margin-block-start: 2.5rem; }

.prose p { color: var(--fg); }

.prose-lead {
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
  color: var(--fg);
}

.prose ul:not([class]),
.prose ol:not([class]) { padding-left: 1.25rem; }

.prose li + li { margin-block-start: .5rem; }

/* Inline emphasis block for the sentence worth stopping on.

   IT IS NEVER position:sticky AND NEVER POSITIONED. This component exists
   because the blog renderer once used a sidebar card for this job, and that
   card was sticky above 960px — so an inline callout in the middle of an
   article pinned itself under the header and every paragraph after it scrolled
   away behind it. Words disappeared behind a box. There is deliberately no
   `position` declaration in this block; do not add one. */
.callout {
  margin-block: 1.75rem;
  padding: 1.25rem clamp(1rem, 3vw, 1.5rem);
  background: var(--panel);
  border-left: 2px solid var(--accent-fill);
  max-width: var(--measure);
}

.callout-h {
  margin-block-end: .5rem;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
}

.callout > * + * { margin-block-start: .75rem; }

/* Two-up list with an inline orange check glyph. --accent-fill on the glyph:
   it is a mark, not a letterform. */
.checks {
  display: grid;
  gap: .6rem 1.5rem;
  margin-block-start: 1rem;
  padding: 0;
  list-style: none;
}

.checks li {
  display: grid;
  grid-template-columns: 1.35rem minmax(0, 1fr);
  gap: .6rem;
  align-items: start;
  font-size: var(--t-small);
  line-height: 1.5;
}

.checks .ico {
  width: 1.15rem;
  height: 1.15rem;
  margin-top: .18rem;
  stroke-width: 2.2;
  color: var(--accent-fill);
}

@media (min-width: 720px) { .checks { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* --- what we take / what we cannot --------------------------------------- */
.takes {
  display: grid;
  gap: var(--gap);
}

.takes-col {
  padding-block-start: 1.25rem;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
}

.takes-h {
  margin-block-end: .9rem;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
}

.takes-list {
  display: grid;
  gap: .55rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.takes-list li {
  display: grid;
  grid-template-columns: 1.35rem minmax(0, 1fr);
  gap: .6rem;
  align-items: start;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--fg);
}

.takes-list .ico { width: 1.15rem; height: 1.15rem; margin-top: .18rem; stroke-width: 2.2; }

@media (min-width: 720px) {
  .takes { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* TWO COLUMNS ONLY WHEN THERE ARE TWO COLUMNS.
     This band was a yes-list beside a no-list. The no-list is gone — the client
     asked for the refusals to come off the site — so every service page now has
     a single child sitting in the left half of a two-column grid with the right
     half empty. Same bug the .johnny-media pair had, same fix: let the grid
     collapse when there is nothing to pair with. Without :has() it stays
     two-column, which is what it does today, so nothing regresses. */
  .takes:not(:has(> :nth-child(2))) { grid-template-columns: minmax(0, 1fr); }
}

/* --- tables --------------------------------------------------------------
   Any wide table scrolls inside its OWN container so the page never does.
   Markup contract: <div class="tbl-scroll" tabindex="0" role="region"
   aria-label="…"><table class="tbl">…</table></div>. The tabindex is what
   makes a scrollable region reachable from a keyboard.

   .price-tbl is deleted along with every price on this site. */

/* --- the booking embed --------------------------------------------------
   A Calendly scheduling page in a plain <iframe>. No third-party script runs
   on this origin, so there is nothing to allow in the CSP and nothing to
   execute before the frame is scrolled to.

   THE HEIGHT IS RESERVED ON THE WRAPPER, NOT THE FRAME. A lazy iframe has no
   intrinsic size, so without this the band collapses to nothing and then snaps
   to full height when the embed lands — a layout shift on the one page whose
   whole job is a confident click. `min-height` holds the space from first
   paint. The value is what Calendly's month view needs before it starts
   scrolling inside itself. */
.booker {
  min-height: 42rem;
  margin-block: 1.5rem;
  border: 1px solid var(--rule-c, var(--rule-strong));
  background: var(--panel);
}

.booker-frame {
  display: block;
  width: 100%;
  /* Matches .booker so the frame fills the reserved box exactly rather than
     leaving a strip of panel showing under it. */
  height: 42rem;
  border: 0;
}

@media (min-width: 700px) {
  .booker { min-height: 46rem; }
  .booker-frame { height: 46rem; }
}

/* The way out for anyone who does not want a calendar. Quiet, but present — a
   scheduler with no phone number beside it loses the callers who never trusted
   one in the first place. */
.booker-alt {
  margin-block-start: 1rem;
  font-family: var(--ff-sub);
  font-size: var(--t-small);
  color: var(--muted);
}

.tbl-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  margin-block: 1.5rem;
  border: 1px solid var(--rule-c, var(--rule-strong));
}

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

.tbl {
  width: 100%;
  /* Below this the columns start hyphenating, which is worse than a swipe. */
  min-width: 34rem;
  border-collapse: collapse;
  font-size: var(--t-small);
}

.tbl th,
.tbl td {
  padding: .8rem 1rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--rule-c, var(--rule-strong));
}

.tbl th {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--mark);
  background: var(--panel);
}

.tbl tr:last-child td { border-bottom: 0; }

/* --- standalone figure ---------------------------------------------------- */
.figure { margin: 0; }

.figure img {
  width: 100%;
  border: 1px solid var(--rule-c, var(--rule-strong));
}

.figure-cap {
  margin-block-start: .75rem;
  max-width: 52ch;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--muted);
}


/* ===========================================================================
   11  BLOG — POST, ASIDE, RELATED, ZIPS
   =========================================================================== */

.post {
  display: grid;
  gap: clamp(2.5rem, 1.5rem + 4vw, 4rem);
}

.post-body { max-width: var(--measure-4); }

.post-body > * + * { margin-block-start: 1rem; }
.post-body > * + :is(h2, h3, h4) { margin-block-start: 2.75rem; }

.post-body ul:not([class]),
.post-body ol:not([class]) { padding-left: 1.25rem; }
.post-body li + li { margin-block-start: .5rem; }

.post-body blockquote {
  margin-block: 1.75rem;
  padding-left: 1.25rem;
  border-left: 2px solid var(--accent-fill);
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
}

.post-body img { width: 100%; border: 1px solid var(--rule-c, var(--rule-strong)); }

.post-body figcaption {
  margin-block-start: .75rem;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  line-height: 1.6;
  letter-spacing: .06em;
  color: var(--muted);
}

.post-body hr {
  border: 0;
  border-top: 1px solid var(--rule-c, var(--rule-strong));
  margin-block: 2.5rem;
}

.post-body code {
  font-family: var(--ff-sub);
  font-size: .88em;
  padding: .1em .35em;
  background: var(--panel);
}

.post-meta {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem .9rem;
  margin-block-end: 1.5rem;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--muted);
}

.post-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  justify-content: space-between;
  margin-block-start: 2.5rem;
  padding-block-start: 1.25rem;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
}

.post-nav a {
  max-width: 26ch;
  font-family: var(--ff-sub);
  font-weight: 800;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--tr-display);
  text-decoration: none;
}

.post-nav a:hover { color: var(--accent); }

/* Sidebar card. STICKY ONLY inside .post-side at >= 960px, and nowhere else on
   the site — see the note on .callout above for what happens otherwise. */
.aside {
  padding: 1.25rem clamp(1rem, 3vw, 1.5rem);
  background: var(--panel);
  border-left: 2px solid var(--accent-fill);
}

.aside > * + * { margin-block-start: .75rem; }

.aside-h {
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
}

.aside p { font-size: var(--t-small); line-height: 1.55; color: var(--muted); }

.aside ul {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--rule-c, var(--rule-strong));
}

.aside li { border-bottom: 1px solid var(--rule-c, var(--rule-strong)); }

.aside li a {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding-block: .5rem;
  font-size: var(--t-small);
  color: var(--fg);
  text-decoration: none;
}

.aside li a:hover { color: var(--accent); }

.post-side {
  display: grid;
  gap: 1.5rem;
  align-content: start;
}

@media (min-width: 960px) {
  .post { grid-template-columns: minmax(0, 1fr) minmax(0, 19rem); }
  .post-side .aside {
    position: sticky;
    top: calc(var(--hdr-h) + 1.5rem);
  }
}

/* --- end-of-page sibling links -------------------------------------------- */
.related {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: var(--gap);
  row-gap: 0;
}

.related-row {
  position: relative;
  display: block;
  padding: .9rem 0 .9rem 1rem;
  border-top: var(--rule-w) solid var(--rule-c, var(--rule-strong));
  color: var(--fg);
  text-decoration: none;
  font-family: var(--ff-display);
  font-weight: 700;
  font-size: var(--t-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--tr-display);
}

.related-row::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--accent-fill);
  transform: scaleX(0);
  transform-origin: left;
}

.related-row:hover::before,
.related-row:focus-visible::before { transform: scaleX(1); }
.related-row:hover { color: var(--accent); }

@media (min-width: 720px)  { .related { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .related { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* --- the ZIP index on /service-areas/ ------------------------------------- */
.zips {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem .5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.zip {
  padding: .3rem .55rem;
  border: 1px solid var(--rule-c, var(--rule-strong));
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  letter-spacing: var(--tr-mono);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}


/* ===========================================================================
   12  FORMS
   Labels are ALWAYS visible. No placeholder-as-label anywhere on this site: a
   placeholder vanishes the moment someone starts typing, which is exactly when
   a person filling a quote form on a phone outdoors needs to check what field
   they are in.
   =========================================================================== */

.form {
  display: grid;
  gap: 1.25rem;
  max-width: 40rem;
}

.field { display: grid; gap: .4rem; }

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

@media (min-width: 560px) {
  .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.field-lab {
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--mark);
}

/* "Optional" is stated in words inside the label, not signalled by a red
   asterisk nobody has a legend for. */
.field-lab small {
  font-weight: 400;
  text-transform: none;
  letter-spacing: .02em;
  color: var(--muted);
}

.field-in {
  width: 100%;
  min-height: 48px;
  padding: .7rem .85rem;
  border: 1px solid var(--field-bd);
  border-radius: 0;
  background: var(--field-bg);
  color: var(--fg);
  font-family: var(--ff-body);
  /* Never below 16px — iOS zooms the viewport under it. */
  font-size: 1rem;
  line-height: 1.4;
}

textarea.field-in { min-height: 8rem; resize: vertical; line-height: 1.55; }

.field-in::placeholder { color: var(--muted); opacity: 1; }

.field-in:hover { border-color: var(--fg); }
.field-in:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

.field-note {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--muted);
}

/* :user-invalid, not :invalid — a required field is invalid the instant the
   page loads, and painting an error on an untouched form is hostile.
   .field.is-invalid is the same state written by site.js when it takes over
   validation; the two are styled identically so the form never changes shape
   depending on whether the script ran. */
.field-in:user-invalid,
.field.is-invalid .field-in {
  border-color: var(--accent-fill);
  border-width: 2px;
}

/* Injected by site.js under the input it belongs to. There is no red in this
   palette and inventing one would be a sixth colour — the accent is what says
   "look here", which is what it says everywhere else on the site. The message
   is words; the colour is never carrying it alone. */
.field-err {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--accent);
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  align-items: center;
}

/* What happens after you press the button. */
.form-note {
  max-width: var(--measure-1);
  padding-block-start: 1rem;
  border-top: 1px solid var(--rule-c, var(--rule-strong));
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--muted);
}

.form-note strong { color: var(--fg); }

/* The two panels site.js swaps in for a delivered form and for the SMS
   fallback. Same flat block as .callout, so an answered form looks like part
   of the page rather than like a browser dialog. */
.form-ok,
.form-sms {
  max-width: 40rem;
  margin-block: 1.5rem;
  padding: 1.25rem clamp(1rem, 3vw, 1.5rem);
  background: var(--panel);
  border-left: 2px solid var(--accent-fill);
  color: var(--fg);
  font-size: var(--t-small);
  line-height: 1.55;
}

.form-ok > * + *,
.form-sms > * + * { margin-block-start: .75rem; }

.form-ok a,
.form-sms a { text-decoration-color: var(--accent); }


/* ===========================================================================
   13  FOOTER + CALL BAR
   =========================================================================== */

.ftr {
  background: var(--ink);
  color: var(--cream);
  border-top: 1px solid var(--line-dark);
  padding-block: clamp(3rem, 2rem + 4vw, 5.5rem) clamp(2rem, 1.5rem + 2vw, 3rem);
}

.ftr-in {
  display: grid;
  gap: clamp(2rem, 1.5rem + 2.4vw, 3rem);
  /* Same geometry as .wrap — see .hdr-in. Without this the NAP plate sat hard
     against the left edge of the phone screen. */
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gut);
}

/* THIS IS A COLUMN, NOT A LOCKUP LINK. layout.mjs puts .ftr-brand on the whole
   first footer cell: the logo link, the tagline, the NAP plate, the hours line
   and the social row. Laying it out as a horizontal flex row — which is what
   the class name suggests and what the first draft did — put five blocks side
   by side and made the page 528px wide on a 360px phone. */
.ftr-brand {
  display: grid;
  gap: 1rem;
  align-content: start;
  min-width: 0;
}

.ftr-brand > a { justify-self: start; }

.ftr-brand .hdr-mark img { max-height: 40px; }

.ftr-brand p {
  max-width: 34ch;
  font-size: var(--t-small);
  color: var(--muted-ink);
}

.ftr-h {
  margin-block-end: 1rem;
  padding-block-end: .5rem;
  border-bottom: 1px solid var(--line-dark);
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: var(--lh-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--mark);
  max-width: none;
}

.ftr-link {
  display: block;
  padding-block: .3rem;
  font-size: var(--t-small);
  color: var(--cream);
  text-decoration: none;
}

.ftr-link:hover { color: var(--orange); }

/* THE NAP BLOCK — name, address, phone. Mono, because it is data, and it is
   what local search reads. It has to match the Google Business Profile
   character for character, so it is set as a ruled ledger rather than as a
   card: nothing about it should read as marketing. */
.ftr-plate {
  display: grid;
  gap: .65rem;
  margin: 0;
  padding-block-start: 1.25rem;
  border-top: 1px solid var(--line-dark);
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  line-height: 1.7;
  letter-spacing: .06em;
}

/* Each term/definition pair is wrapped in its own <div> by layout.mjs, so the
   two-column ledger has to be built INSIDE the wrapper. Putting
   grid-template-columns on the <dl> would lay the pairs out two-across
   instead. */
.ftr-plate > div { display: grid; gap: .1rem; min-width: 0; }

.ftr-plate dt {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
  color: var(--mark);
}

.ftr-plate dd {
  margin: 0;
  color: var(--cream);
  /* A mono NAP value in a ~180px column: nothing here may push the page. */
  overflow-wrap: anywhere;
}

.ftr-plate a { color: var(--orange); text-decoration-color: var(--orange); }

@media (min-width: 560px) {
  .ftr-plate > div {
    grid-template-columns: 7.5rem minmax(0, 1fr);
    gap: .75rem;
    align-items: baseline;
  }
}

.ftr-social {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem 1.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ftr-social a {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 44px;
  color: var(--cream);
  text-decoration: none;
  font-family: var(--ff-sub);
  font-size: var(--t-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tr-mono);
}

.ftr-social a:hover { color: var(--orange); }

.ftr-legal {
  margin-block-start: clamp(2rem, 1.5rem + 2vw, 3rem);
  padding-block-start: 1.25rem;
  border-top: 1px solid var(--line-dark);
  font-size: var(--t-small);
  color: var(--muted-ink);
}

.ftr-legal a { color: var(--muted-ink); }
.ftr-legal a:hover { color: var(--cream); }

@media (min-width: 720px) {
  .ftr-in { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1100px) {
  .ftr-in { grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr)); }
}

/* --- the call bar --------------------------------------------------------
   Fixed to the bottom, phones only. <body> reserves its height in §03, and
   both are cancelled together at 821px — the complement of the design plan's
   "≤820px", written as a min-width so this file only ever queries in one
   direction.





/* ===========================================================================
   14  MOTION

   THE BUDGET IS ONE MOMENT. Round three shipped 109 reveal hooks; scattered
   motion is the loudest generated-design tell there is. This round spends the
   whole budget on THE SEAM NUDGE — a 900ms demonstration that the picture
   changes when you drag it, where the thing being demonstrated IS the service
   — and rations everything else to almost nothing. If a reviewer can name a
   second "moment", the effect has been scattered.

   NO SCROLL-LINKED EFFECTS, DELIBERATELY. A parallax on the fold photo and a
   scroll-strength scrim were both considered and both cut. The concept — the
   page clearing — is carried by STATIC spacing, rule weight, measure and
   letterform width, so it is legible in a screenshot, on a printout, and to
   someone with reduced motion switched on. A scroll-linked effect would also
   compete with the nudge for the role of "the moment". If a concept only works
   while moving, it is not a concept.

   Still banned: scroll-jacking, autoplay video, infinite marquees (also a WCAG
   2.2.2 failure), anything that fights the scrollbar.

   EVERYTHING below sits inside prefers-reduced-motion: no-preference. Nothing
   outside this block moves, transitions, or hides.
   =========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  html { scroll-behavior: smooth; }

  /* --- PAGE LOAD, STEPS 2–3 ---------------------------------------------
     NO OPACITY ANIMATION ANYWHERE IN THE FOLD. Chrome does not count a fully
     transparent element as painted, so fading the photo or the headline in
     would push LCP back by the length of the animation. The words move on
     transform only, inside their own overflow:hidden mask.

     MARKUP: <span class="rise"><span>word</span></span>, one per word, with
     real spaces between the outer spans so the accessible name is still one
     continuous sentence. If a template ever emits a bare `.rise` with no
     element inside it, `.rise > *` simply matches nothing and the word renders
     in its final position — the failure mode is "no animation", never "no
     words". This is a CSS load animation, so it also runs with site.js
     blocked, and it cannot strand content off-screen because it ends in the
     natural position. */
  .fold-h .rise {
    display: inline-block;
    overflow: hidden;
    vertical-align: bottom;
    /* A descender (the y in County) would be shaved off by overflow:hidden, so
       the mask is padded and the padding is pulled back with a negative
       margin. Without this the animation looks right and the finished heading
       has its tails cut. */
    padding-block-end: .12em;
    margin-block-end: -.12em;
  }

  /* Stagger 60ms, CAPPED AT FIVE STEPS. A tenth word waiting 540ms is a bug,
     not a flourish.

     Two sources for the step index, in priority order. home.mjs writes
     `style="--i:N"` on each inner span with the cap already applied, and that
     wins. If a template ever emits a bare .rise with no --i, the nth-child
     ladder below supplies the same numbers, so the stagger survives either
     markup and neither can silently flatten to zero. */
  .fold-h .rise:nth-child(1) > * { --rise-i: 0; }
  .fold-h .rise:nth-child(2) > * { --rise-i: 1; }
  .fold-h .rise:nth-child(3) > * { --rise-i: 2; }
  .fold-h .rise:nth-child(4) > * { --rise-i: 3; }
  .fold-h .rise:nth-child(n + 5) > * { --rise-i: 4; }

  /* html.js-chrome IS LOAD-BEARING — do not simplify this selector away.
     `animation-fill-mode: both` paints the FROM state before the animation
     starts, and keeps painting it forever if the animation never runs. The FROM
     state here is translateY(105%) — the word sitting fully below its own mask.
     Unscoped, any failure that stops the animation (and there are more than
     reduced-motion) leaves the h1 with one word visible and the kicker, sub and
     trust line at opacity 0. That is what shipped, and Google's renderer, social
     card renderers and Lighthouse all capture early enough to see it.
     Scoping to the flag the inline <head> script sets means no JS, no hidden
     state — exactly how .reveal already works in this same file. */
  html.js-chrome .fold-h .rise > * {
    display: inline-block;
    animation: gj-rise 460ms var(--ease-rise) both;
    animation-delay: calc(var(--i, var(--rise-i, 0)) * 60ms);
  }

  html.js-chrome .fold-kick,
  html.js-chrome .fold-sub,
  html.js-chrome .fold-trust { animation: gj-settle 320ms var(--ease-out) 140ms both; }

  /* STEP 4: .fold-actions has NO animation and is deliberately absent from
     every list above. It is present and pressable on frame one. A control that
     is still moving is a control you cannot press. */

  /* --- THE ORCHESTRATED MOMENT: THE SEAM NUDGE -------------------------
     site.js adds .is-nudging 520ms after DOMContentLoaded, once per page load,
     fold only, and removes it on animationend. It animates the registered
     --ba-pos, which drives clip-path on ONE element and a transform on the
     grip, so the whole thing runs on the compositor and touches no layout.

     It cancels instantly on the first pointerdown or input — the user's hand
     always wins. will-change is set for the duration only and dropped with the
     class, because a permanent will-change on a full-bleed image layer is a
     permanent extra compositor layer. */
  .ba.is-nudging .ba-frame,
  .ba-frame.is-nudging { animation: gj-nudge 900ms var(--ease-nudge) both; }

  .ba.is-nudging .ba-clip,
  .ba-frame.is-nudging .ba-clip { will-change: clip-path; }

  /* --- SCROLL REVEALS — rationed hard -----------------------------------
     One reveal per band, on the band head only, plus at most one
     [data-reveal-group] per page. Hard cap: 12 revealed elements per page.

     THE HIDDEN STATE IS APPLIED BY JS, NEVER BY THIS STYLESHEET ALONE. It sits
     behind html.js-reveal, which site.js adds only when it has an
     IntersectionObserver to un-hide with. That is two independent locks on the
     same door: no JS means the class never arrives and every .reveal is simply
     visible; reduced motion means this whole block does not exist. There is no
     state of this file in which content is hidden and nothing is coming to
     un-hide it.

     Opacity and transform only, so a reveal is a compositor job and never a
     layout one. Never in the fold, never on anything interactive, and nothing
     ever re-hides on the way back up. */
  html.js-reveal .reveal {
    opacity: 0;
    transform: translateY(16px);
  }

  html.js-reveal .reveal.is-in {
    opacity: 1;
    transform: none;
    transition:
      opacity 420ms var(--ease-out) var(--reveal-delay, 0ms),
      transform 420ms var(--ease-out) var(--reveal-delay, 0ms);
  }

  /* ---------------------------------------------------------------------
     THREE MORE SCROLL BEHAVIOURS, ADDED AT THE CLIENT'S REQUEST.
     The design plan cut scroll effects by name and the runtime header still
     says so; the brief overrides the plan — "I feel like it's just kind of a
     bland website, it doesn't really say much."

     They are variants of the SAME system, not a second one: each is a class on
     top of `.reveal`, so all three inherit the guarantees that already hold —
     hidden state only under html.js-reveal, never under reduced motion, never
     without IntersectionObserver, force-shown by the three-second rescue, and
     reset to visible for print.

     Each one does something the subject actually does, rather than decorating:
       .reveal-wipe  a photograph is uncovered left to right, the way a space
                     is cleared. clip-path only — no layout, no repaint.
       .reveal-rule  a hairline draws itself across, so a ruled index assembles
                     rather than appearing.
       .reveal-lift  a row rises slightly further than the default, for the
                     handful of places where a single element should feel like
                     it lands rather than fades.
     --------------------------------------------------------------------- */
  html.js-reveal .reveal-wipe {
    opacity: 1;                        /* the wipe IS the reveal; no fade */
    transform: none;
    clip-path: inset(0 100% 0 0);
  }

  html.js-reveal .reveal-wipe.is-in {
    clip-path: inset(0 0 0 0);
    transition: clip-path 760ms var(--ease-out) var(--reveal-delay, 0ms);
  }

  html.js-reveal .reveal-rule {
    opacity: 1;
    transform: scaleX(0);
    transform-origin: left center;
  }

  html.js-reveal .reveal-rule.is-in {
    transform: scaleX(1);
    transition: transform 620ms var(--ease-out) var(--reveal-delay, 0ms);
  }

  html.js-reveal .reveal-lift { transform: translateY(30px); }

  /* --- STAGGERED ROWS -----------------------------------------------------
     A container carries data-rise; site.js observes the CONTAINER and stamps
     --rise-i on each child. One observer target, however many rows. Nothing is
     hidden until site.js has added .is-armed, so with JavaScript off — or the
     script blocked, or an exception thrown before initRise() — every row is
     simply visible. That ordering is the whole safety argument.

     transform and opacity only, so a fifteen-row list costs the compositor and
     nothing else. --------------------------------------------------------- */
  html.js-reveal [data-rise].is-armed > * {
    opacity: 0;
    transform: translateY(22px);
  }

  html.js-reveal [data-rise].is-armed.is-in > * {
    opacity: 1;
    transform: none;
    transition:
      opacity 560ms var(--ease-out) calc(var(--rise-i, 0) * 60ms),
      transform 620ms var(--ease-out) calc(var(--rise-i, 0) * 60ms);
  }

  /* --- THE MAP DRAWS ITSELF ----------------------------------------------
     Spokes run out from the yard, then the pins land. site.js measures each
     line and sets its own dasharray, because one dash value cannot draw thirty
     lines of different lengths at the same rate. .is-armed is added by the
     script, so the default state is a finished map. */
  .smap-svg.is-armed .sm-spoke,
  .smap-svg.is-armed .sm-lead {
    transition: stroke-dashoffset 900ms var(--ease-out);
  }

  .smap-svg.is-armed.is-drawn .sm-spoke,
  .smap-svg.is-armed.is-drawn .sm-lead {
    stroke-dashoffset: 0 !important;
  }

  .smap-svg.is-armed .sm-dot,
  .smap-svg.is-armed .sm-label {
    opacity: 0;
    transition: opacity 420ms var(--ease-out) 520ms;
  }

  .smap-svg.is-armed .sm-dot { transform-box: fill-box; transform-origin: center; }

  .smap-svg.is-armed.is-drawn .sm-dot,
  .smap-svg.is-armed.is-drawn .sm-label { opacity: 1; }

  /* The yard lands first and on its own — it is the thing the rest radiates
     from, so it arrives before the cities rather than with them. */
  .smap-svg.is-armed .sm-dot.is-base { transition-delay: 120ms; }

  /* --- PRESS FEEDBACK -----------------------------------------------------
     A tap that produces no visible change reads as a tap that missed. Applied
     on :active only, so it costs nothing until a finger is down, and kept
     small enough that it does not move a 44px target out from under one. */
  .btn:active,
  .show-nav:active,
  .show-dot:active { transform: scale(.97); }

  .btn { transition: transform 90ms var(--ease-out), background-color 160ms var(--ease-out), color 160ms var(--ease-out), border-color 160ms var(--ease-out); }

  /* The word swap. Out goes up and fades, in arrives from below — the same
     upward motion as every other entrance on the site, so it reads as one
     vocabulary rather than a separate effect. Transform and opacity only.
     Outside this query nothing moves: reduced motion gets one static noun,
     and site.js never starts the timer. */
  .swap-w {
    transition: transform 420ms var(--ease-out), opacity 300ms var(--ease-out);
  }

  .swap-w.is-out {
    transform: translateY(-0.9em);
    opacity: 0;
  }

  .swap-w.is-in {
    transform: translateY(0.9em);
    opacity: 0;
  }

  /* --- HOVER / FOCUS MICRO-INTERACTIONS ---------------------------------
     Compositor-only. No lift, no scale on a card, no shadow bloom, anywhere. */
  .index-row::before,
  .map-row::before,
  .read-row::before,
  .related-row::before { transition: transform 140ms ease-out; }

  .index-name,
  .map-city,
  .read-h { transition: transform var(--t-move) ease-out, color var(--t-color) ease; }

  .index-row:hover .index-name,
  .index-row:focus-visible .index-name,
  .map-row:hover .map-city,
  .map-row:focus-visible .map-city,
  .read-row:hover .read-h,
  .read-row:focus-visible .read-h { transform: translateX(6px); }

  .btn,
  .btn-solid,
  .btn-ghost,
  .btn-line,
  .hdr-tel,
  .field-in {
    transition:
      background-color var(--t-color) ease,
      border-color var(--t-color) ease,
      color var(--t-color) ease;
  }

  .hdr,
  .hdr-link,
  .hdr-menu,
  .m-nav-link,
  .ftr-link,
  .ftr-social a,
  .crumb a,
  .close-tel,
  .job-more,
  .post-nav a,
  .related-row,
  .aside li a,
  .band-more a,
  .ask-q,
  a {
    transition:
      background-color 200ms ease,
      border-color 200ms ease,
      color var(--t-color) ease,
      text-decoration-color var(--t-color) ease;
  }

  html.js-ba .ba-grip { transition: transform 140ms ease-out; }
  html.js-ba .ba-frame:hover .ba-grip { transform: scale(1.08); }

  /* The marker rotates; the PANEL does not animate. Height is layout, and
     <details> has no interoperable way to animate it well. */
  .ask-q::after { transition: transform 160ms ease; }

  .skip { transition: transform 160ms ease-out; }

  /* The focus ring is deliberately absent from every transition list above.
     A focus ring that fades in is a focus ring you miss. */
}

@keyframes gj-rise {
  from { transform: translateY(105%); }
  to   { transform: translateY(0); }
}

@keyframes gj-settle {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* 50 -> 68 -> 50. Out and back, once, ever. */
@keyframes gj-nudge {
  0%   { --ba-pos: 50%; }
  42%  { --ba-pos: 68%; }
  100% { --ba-pos: 50%; }
}

/* At >= 900px the seam rests at 62% so it clears the copy column, so the nudge
   travels from there instead — the same 18-point wipe, without a jolt back to
   50% before it starts. */
@keyframes gj-nudge-wide {
  0%   { --ba-pos: 62%; }
  42%  { --ba-pos: 80%; }
  100% { --ba-pos: 62%; }
}

@media (prefers-reduced-motion: no-preference) and (min-width: 900px) {
  .ba-bleed.is-nudging .ba-frame,
  .ba-bleed .ba-frame.is-nudging { animation-name: gj-nudge-wide; }
}


/* ===========================================================================
   15  PRINT
   Somebody printing this is printing a service page to put the number on the
   fridge. Give them the number, the words and the photographs, and nothing
   else. Chrome goes, colour goes, and external links say where they point —
   a printed page has no hover and no address bar.
   =========================================================================== */

@media print {
  :root {
    --pad: 0;
    --gap: 1rem;
    --measure: 100%;
   
  }

  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    background: transparent !important;
    color: #000 !important;
    box-shadow: none !important;
    animation: none !important;
    transition: none !important;
  }

  body {
    background: #fff;
    font-size: 11pt;
    line-height: 1.45;
    padding: 0 !important;
  }

  html.js-chrome body,
  html.js-chrome.js-ba body.p-home { padding-block-start: 0; }

  .skip,
  .hdr,
  .m-nav,
  .crumbs,
  .ftr-social,
  .ftr-legal,
  .band-more,
  .fold-actions,
  .fold-hint,
  .fold-scrim,
  .fold-scrim-dirty,
  .ba-seam,
  .ba-grip,
  .ba-range,
  .index-go,
  .post-side,
  .form,
  .job-more { display: none !important; }

  /* A reveal that never fired must not print as a blank block. */
  html.js-reveal .reveal,
  html.js-reveal .reveal-wipe,
  html.js-reveal .reveal-rule { opacity: 1 !important; transform: none !important; clip-path: none !important; }

  .band { padding-block: .55cm; border-bottom: 1px solid #999; }

  /* The fold prints as what it actually is on paper: two photographs and the
     words, side by side, both labelled. A printed before/after with no words
     on it is a puzzle, so .ba-lab is NOT hidden above. */
  html.js-ba .fold {
    display: block;
    min-height: 0;
    contain: none;
  }

  html.js-ba .ba-bleed,
  html.js-ba .ba-bleed .ba-frame {
    position: static;
    inset: auto;
  }

  html.js-ba .ba-frame {
    display: grid;
    grid-template-columns: 1fr 1fr;
    aspect-ratio: auto;
    overflow: visible;
  }

  html.js-ba .ba-after,
  html.js-ba .ba-clip {
    position: static;
    clip-path: none;
  }

  html.js-ba .ba-after,
  html.js-ba .ba-before,
  html.js-ba .ba-after img,
  html.js-ba .ba-before img {
    position: static;
    height: auto;
    object-fit: contain;
  }

  html.js-ba .ba-clip  { grid-area: 1 / 1; }
  html.js-ba .ba-after { grid-area: 1 / 2; }

  html.js-ba .fold-copy {
    position: static;
    max-width: none;
    padding: .4cm 0 0;
    background: none;
    pointer-events: auto;
  }

  .ba-lab {
    position: static;
    margin: 0 0 .1cm;
    padding: 0;
    font-size: 8pt;
  }

  .fold-h, h1 { font-size: 20pt; }
  h2, .h2, .band-h { font-size: 15pt; }
  h3, .h3 { font-size: 12pt; }
  .close-tel { font-size: 22pt; }

  .tbl-scroll { overflow: visible !important; }
  .tbl { min-width: 0; }

  /* A <details> the visitor left closed still has to print its answer — six
     questions with no answers is worthless on paper. */
  .ask > .ask-a { display: block !important; }
  .ask-q::after { display: none; }

  /* Where an external link actually goes. Internal hrefs stay bare: the reader
     already has the site, and an index of forty links would turn a readable
     page into a column of addresses. */
  a { text-decoration: underline; }

  a[href^='http']::after {
    content: ' (' attr(href) ')';
    font-family: monospace;
    font-size: 8pt;
    word-break: break-all;
  }

  a[href^='tel:']::after,
  a[href^='sms:']::after,
  a[href^='mailto:']::after { content: '' !important; }

  img { max-width: 100% !important; }

  .quote,
  .index-row,
  .map-row,
  .read-row,
  .run-step,
  .job,
  .ask,
  .aside,
  .callout,
  .figure,
  .ba,
  figure,
  tr { break-inside: avoid; page-break-inside: avoid; }

  h1, h2, h3, .band-h { break-after: avoid; page-break-after: avoid; }
}

/* ===========================================================================
   THE SERVICE-AREA MAP  (.smap)
   Inline SVG built from real city coordinates — see src/lib/servicemap.mjs for
   why this is not a Google Maps embed, and why there are two of them.
   Everything here is a token, so the map inherits the palette and prints.
   =========================================================================== */
.smap {
  margin: 0 0 clamp(1.75rem, 4vw, 2.75rem);
}

.smap-svg {
  display: block;
  width: 100%;
  height: auto;
  background: var(--ink);
  border: 1px solid var(--line-dark);
}

/* SVG text scales with the viewBox, so the wide artwork is capped near its own
   860-unit width. Past that the labels start growing instead of the map. */
.smap-svg.is-wide {
  max-width: 54rem;
  /* LEFT EDGE, NOT CENTRED. Capped at 54rem inside a 1144px figure, `auto`
     margins put the plate 140px to the right of the caption directly beneath it
     and of every heading on the page — measured at 1440px: plate 288, everything
     else 148. One left edge per page is the rule the rest of the site now holds
     to, and a map that floats right of it is the same misalignment the client
     reported in the footer, pointing the other way. Slack goes on the right. */
  margin-inline: 0 auto;
}

/* One is always hidden. `display: none` and not `visibility` — a hidden SVG
   still lays out and still costs paint. */
.smap-svg.is-tall { display: none; }

@media (max-width: 719.98px) {
  .smap-svg.is-wide { display: none; }

  /* Capped, because the tall artwork is a 1.67:1 portrait and the breakpoint
     runs up to 720px: let it fill a 640px tablet column and it becomes a
     1067px-tall black slab with 29px labels in it. 27rem keeps the plate the
     shape a phone gets — the labels stop growing at about 18px and the extra
     width becomes margin. A phone never reaches the cap, so nothing about the
     390px case changes. */
  .smap-svg.is-tall {
    display: block;
    max-width: 27rem;
    margin-inline: auto;
  }
}

/* The 5-mile grid. Barely there on purpose — --line-dark against --ink is about
   1.2:1, which is a whisper, and a whisper is all a coordinate field should be.
   It gives the black plate structure and scale without competing with anything
   that carries a name. */
.sm-grid {
  fill: none;
  stroke: var(--line-dark);
  stroke-width: 1;
}

/* NO `stroke` HERE. The spokes are painted by a userSpaceOnUse radial gradient
   declared per canvas in servicemap.mjs and applied as a presentation attribute
   on their group, so they fade out as they travel away from the yard. A CSS
   `stroke` on this selector would beat that attribute and flatten all fourteen
   back to the hairlines they used to be. */
.sm-spoke {
  stroke-width: 1;
  stroke-linecap: round;
}

/* Both gradient stops are the one orange; only the opacity moves. Declaring the
   colour here rather than in the markup is what keeps the palette audit able to
   see it. */
.sm-ray-grad stop { stop-color: var(--orange); }

/* A lead only exists on a label the placer could not fit flush against its own
   dot. It is the only thing tying that name to that place, so it has to be
   visible — quieter than a name, louder than the grid. Drawn in the neutral, not
   the accent, because it is wayfinding and not a call to action. */
.sm-lead {
  stroke: var(--muted-ink);
  stroke-width: 1;
  opacity: .5;
}

.sm-dot {
  fill: var(--cream);
  stroke: var(--ink);
  stroke-width: 2;
}

.sm-dot.is-base { fill: var(--orange); }
.sm-pin.is-base .sm-label { fill: var(--orange); }

/* The yard, and the only ring on the map. It is what the eye lands on first,
   and it is the reason the divider and the grid gave up their orange: with one
   accent in one place the hierarchy needs no other help. Declared after
   .is-base so the fill is overridden rather than fought. */
.sm-dot.is-hub {
  fill: none;
  stroke: var(--orange);
  stroke-width: 1.5;
  opacity: .5;
}

/* The halo is what lets a label sit on top of the field and stay readable — it
   is what buys the phone render the freedom to name every city instead of three.
   Order matters: stroke first, then fill, so the outline sits behind the
   letterform instead of eating into it. */
.sm-label {
  font-family: var(--ff-sub);
  font-weight: 600;
  letter-spacing: .02em;
  fill: var(--cream);
  stroke: var(--ink);
  stroke-width: 4;
  paint-order: stroke fill;
}

/* Dashed because the line is honest about being approximate: it sits in the gap
   between the southernmost Palm Beach city and the northernmost Broward one,
   which is a claim the coordinates support, and it is not a surveyed boundary. */
.sm-divide line {
  stroke: var(--muted-ink);
  stroke-width: 1;
  stroke-dasharray: 2 7;
  opacity: .5;
}

.sm-divide text,
.sm-rose text,
.sm-legend {
  font-family: var(--ff-sub);
  font-weight: 600;
  letter-spacing: .14em;
  fill: var(--muted-ink);
  stroke: var(--ink);
  stroke-width: 3;
  paint-order: stroke fill;
}

/* Map furniture: which way is up, and how far apart these places are. Both are
   set at the volume of a margin note, because that is what they are. */
.sm-rose path {
  fill: var(--muted-ink);
  stroke: none;
}

/* The phone canvas is 540 units wide and renders at about 350, so a "1 unit"
   hairline lands on six tenths of a device pixel and greys away to nothing —
   the grid in particular disappeared entirely. Every line on that canvas is
   scaled up to land on a whole pixel instead. Mobile is 80% of the traffic;
   this is not a rounding detail. */
.smap-svg.is-tall .sm-grid,
.smap-svg.is-tall .sm-spoke,
.smap-svg.is-tall .sm-lead,
.smap-svg.is-tall .sm-divide line { stroke-width: 1.6; }

.smap-svg.is-tall .sm-dot.is-hub { stroke-width: 2; }

.smap-cap {
  margin-block-start: .85rem;
  font-size: var(--t-small);
  color: var(--muted-cream);
  max-width: 62ch;
}

/* The same figure ships on a cream band (/service-areas/) and on an ink one
   (the home page), and --muted-cream is only 3.7:1 against --ink — it was
   failing on the home page and had been for a while. --muted-ink is the token
   measured for exactly this, at 7.03:1. */
.band-ink .smap-cap { color: var(--muted-ink); }

/* Hover dims the field so the one you are pointing at stands out. Pointer-only
   and motion-gated: on a phone there is no hover and nothing is lost. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .sm-pin { transition: opacity 180ms var(--ease-out); }
  .smap-svg:hover .sm-pin { opacity: .35; }
  .smap-svg .sm-pin:hover { opacity: 1; }
}
