/* ==========================================================================
   UPRIGHT ELECTRIC — main.css
   The single sitewide stylesheet.
   Layers: 1. Tokens  2. Base  3. Type & labels  4. Buttons  5. Structure
           6. Header  7. Hero  8. Sections  9. Footer  10. Motion  11. Utilities
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. FONTS — self-hosted IBM Plex Sans (WOFF2 from IBM/plex, OFL)
   Weights match what the stylesheet uses. Italic is declared for completeness
   but unused in markup today, so browsers will not fetch that file.
   -------------------------------------------------------------------------- */
@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/fonts/IBMPlexSans-Regular.woff2") format("woff2");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}
@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/fonts/IBMPlexSans-Medium.woff2") format("woff2");
	font-weight: 500;
	font-style: normal;
	font-display: swap;
}
@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/fonts/IBMPlexSans-SemiBold.woff2") format("woff2");
	font-weight: 600;
	font-style: normal;
	font-display: swap;
}
@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/fonts/IBMPlexSans-Bold.woff2") format("woff2");
	font-weight: 700;
	font-style: normal;
	font-display: swap;
}
@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/fonts/IBMPlexSans-Italic.woff2") format("woff2");
	font-weight: 400;
	font-style: italic;
	font-display: swap;
}

/* --------------------------------------------------------------------------
   1. DESIGN TOKENS
   -------------------------------------------------------------------------- */
:root {
	/* Color — surfaces */
	--bg: #eef1f4;            /* ambient gray-blue page background */
	--surface: #ffffff;       /* white planes on --bg */
	--navy: #0a1f44;          /* dark sections */
	--navy-rgb: 10, 31, 68;   /* same navy, for gradients that need alpha */
	--navy-deep: #050d21;     /* footer, deepest planes */
	/* Charcoal overlay for photo scrims. Navy on a light photo reads as a blue cast. */
	--scrim-rgb: 3, 8, 20;
	--ink: #0e1a2e;           /* text on light */
	--ink-muted: #55637a;     /* secondary text on light */

	/* Color — brand */
	--blue: #276ccd;          /* primary actions, links */
	--blue-hot: #1f46d1;
	--red: #ed4b00;           /* small accents only */
	--red-hot: #c93f00;
	--amber: #f5a623;         /* review stars; mixed into --mark for quote highlights */
	/* Marker wash on a quoted phrase. Mixed from --amber so it stays in the
	   review color family without painting the star fill onto the type. */
	--mark: color-mix(in srgb, var(--amber) 36%, var(--surface));

	/* Color — on dark */
	--white: #ffffff;
	--mist: #aebcd2;

	/* Hairlines */
	--line: #d5dbe3;
	--line-strong: #b9c2cd;
	--line-dark: rgba(255, 255, 255, 0.14);

	/* Type */
	--font: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
					Roboto, "Helvetica Neue", Arial, sans-serif;
	--font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;

	/* Sized ~one step up for Plex's smaller x-height vs system UI fonts. */
	--text-xs: 0.75rem;
	--text-sm: 0.9375rem;
	--text-base: 1.0625rem;
	--text-lg: 1.25rem;
	--text-xl: clamp(1.3125rem, 1.15rem + 0.65vw, 1.625rem);
	--text-2xl: clamp(1.7rem, 1.4rem + 1.25vw, 2.25rem);
	--text-3xl: clamp(2.125rem, 1.5rem + 2.5vw, 3.4rem);
	/* Interior page titles only. The hero sets its own size, one step above
	   this at every width, because it is a two- or three-word poster over a
	   photograph and this is a sentence in open page. It used to run to
	   5.75rem, which put every interior title above the hero and made a
	   sentence shout louder than the poster. Lowered August 2026: keep it
	   under the hero's clamp if either one moves. */
	--text-display: clamp(2.3rem, 1.7rem + 2.55vw, 4rem);
	--tracking-caps: 0.14em;

	/* Space */
	--sp-1: 0.25rem;
	--sp-2: 0.5rem;
	--sp-3: 0.75rem;
	--sp-4: 1rem;
	--sp-5: 1.5rem;
	--sp-6: 2rem;
	--sp-7: 3rem;
	--sp-8: 4.5rem;
	--section-pad: clamp(4.5rem, 3rem + 6vw, 9rem);
	/* For a section bracketed by strong edges (a full-bleed bar, a color
	   change) rather than by open page. A full pad against either reads as a
	   hole instead of a rhythm. Kept as a ratio of --section-pad so the two
	   stay locked together across the whole viewport range. */
	--section-pad-tight: calc(var(--section-pad) * 0.7);

	/* Shape & elevation — sharp by default */
	--radius: 4px;

	/* Elevation is rare here, so the few shadows that do exist are all built
	   the same way: a ramp of layers whose offset and blur double each step,
	   at one low alpha per layer, rather than a single wide blur. A lone blur
	   fades evenly outward from the edge, which is what makes a shadow read as
	   a gray halo. Stacked, each layer adds its own falloff, so density is
	   highest at the contact edge and thins fast, and the tail reaches far
	   from the surface without ever showing as an edge.

	   Alpha is identical in every layer of every ramp: it compounds where the
	   layers overlap, so depth comes from how far the ramp runs, not from
	   darkening the near layers. It has to stay very low, since these surfaces
	   are white on near-white — six layers at 0.04 already reach ~0.22 at the
	   contact edge.

	   The tint is --navy-deep, not black: neutral gray goes muddy over the
	   cool grays of --bg. It repeats here as a raw triplet because rgba()
	   cannot take a hex.

	   --shadow-bar and --shadow-bar-lifted are the same ramp one step apart:
	   the resting depth for the header bar, and one step further out for the
	   privacy notice floating over content at the viewport's bottom edge. */
	--shadow-rgb: 5, 13, 33;

	/* Header bar, floating just clear of the page */
	--shadow-bar:
		0 1px 1px rgba(var(--shadow-rgb), 0.04),
		0 2px 2px rgba(var(--shadow-rgb), 0.04),
		0 4px 4px rgba(var(--shadow-rgb), 0.04),
		0 8px 8px rgba(var(--shadow-rgb), 0.04);
	/* One step further out: the privacy notice bar */
	--shadow-bar-lifted:
		0 2px 2px rgba(var(--shadow-rgb), 0.04),
		0 4px 4px rgba(var(--shadow-rgb), 0.04),
		0 8px 8px rgba(var(--shadow-rgb), 0.04),
		0 16px 16px rgba(var(--shadow-rgb), 0.04);
	/* Full surface overlapping another surface: the whole ramp */
	--shadow-panel:
		0 2px 2px rgba(var(--shadow-rgb), 0.04),
		0 4px 4px rgba(var(--shadow-rgb), 0.04),
		0 8px 8px rgba(var(--shadow-rgb), 0.04),
		0 16px 16px rgba(var(--shadow-rgb), 0.04),
		0 32px 32px rgba(var(--shadow-rgb), 0.04),
		0 64px 64px rgba(var(--shadow-rgb), 0.04);

	/* Layout */
	--container: 88rem;
	/* The single alignment spine: .container, the hero panel, and any full-bleed
	   bar take their left edge from this value. The 3vw term keeps mid-size
	   desktops off the screen edge in the range where the viewport is narrower
	   than --container. */
	--gutter: max(1.5rem, 3vw, calc((100vw - var(--container)) / 2));

	/* Floating inset header bar. --bar-inset is set so that bar padding lands
	   the logo exactly on --gutter, keeping the header on the page spine. */
	--header-h: 4.5rem;
	--header-pad: 0.75rem;
	--header-total: calc(var(--header-h) + var(--header-pad) * 2);
	/* Lockup height in the header, the 36rem crop, and the footer. The
	   subtracted --sp-1 is leftover room in the bar, split above and
	   below by align-items: center, not clearance on each side. */
	--logo-h: calc(var(--header-h) - 2px - var(--sp-1));
	--bar-pad: min(1.25rem, calc(var(--gutter) - 1rem));
	--bar-inset: calc(var(--gutter) - var(--bar-pad));
	--radius-bar: 6px;

	/* Motion */
	--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
	--dur: 0.7s;
}

/* --------------------------------------------------------------------------
   2. BASE
   -------------------------------------------------------------------------- */
*,
*::before,
*::after { box-sizing: border-box; }

html {
	scroll-behavior: smooth;
	scroll-padding-top: calc(var(--header-total) + 1rem);
	-webkit-text-size-adjust: 100%;
}

body {
	margin: 0;
	font-family: var(--font);
	font-size: var(--text-base);
	line-height: 1.65;
	color: var(--ink);
	background: var(--bg);
	-webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
	margin: 0 0 var(--sp-4);
	line-height: 1.04;
	letter-spacing: -0.035em;
	font-weight: 800;
	text-wrap: balance;
}
h1 { font-size: var(--text-display); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-lg); letter-spacing: -0.015em; }

p { margin: 0 0 var(--sp-4); }
p:last-child { margin-bottom: 0; }

a { color: var(--blue); text-decoration: none; }
a:hover { color: var(--blue-hot); }

ul, ol { margin: 0 0 var(--sp-4); padding-left: 1.25rem; }
img { max-width: 100%; height: auto; display: block; }
strong { font-weight: 700; }
figure { margin: 0; }
blockquote { margin: 0; }

::selection { background: var(--blue); color: var(--white); }

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

.skip-link {
	position: absolute;
	top: -100%;
	left: var(--sp-4);
	z-index: 200;
	padding: var(--sp-3) var(--sp-5);
	background: var(--navy);
	color: var(--white);
	font-weight: 600;
}
.skip-link:focus { top: 0; color: var(--white); }

/* --------------------------------------------------------------------------
   3. TYPE & TECHNICAL LABELS
   -------------------------------------------------------------------------- */
.container { width: 100%; padding-inline: var(--gutter); }

.icon {
	width: 1.25em;
	height: 1.25em;
	flex: none;
	vertical-align: -0.25em;
}

/* Monospace microtext — coordinates, annotations, figure captions */
.microtext {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 500;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--ink-muted);
}

/* Eyebrow — red tick + mono label */
.eyebrow {
	display: flex;
	align-items: flex-start;
	gap: var(--sp-3);
	margin: 0 0 var(--sp-5);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: var(--tracking-caps);
	text-transform: uppercase;
	color: var(--ink-muted);
}
/* The accent tick, shared with the section name in .rule-head below. Geometry
   lives here for both of them; only the vertical alignment differs, since the
   two labels sit in different formatting contexts. Both strips are --text-xs
   mono, so one width reads as one device across the page. */
.eyebrow::before,
.rule-head span:first-child::before {
	content: "";
	width: 1.25rem;
	height: 2px;
	background: var(--red);
}
/* Pinned to the first line, so a label that wraps indents past the tick. The
   offset centers it on the cap height rather than the taller line box. */
.eyebrow::before {
	margin-top: 0.7em;
	flex: none;
}
.eyebrow--on-dark { color: var(--mist); }

/* Section rule-head: hairline + mono section name left / meta right */
.rule-head {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--sp-5);
	padding-top: var(--sp-4);
	margin-bottom: clamp(2.5rem, 2rem + 2vw, 4rem);
	border-top: 1px solid var(--line-strong);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: var(--tracking-caps);
	text-transform: uppercase;
	color: var(--ink-muted);
}
/* The section name is the strongest thing in the strip and the meta label
   opposite it stays muted. The name is set in ink rather than the accent:
   red-orange over a phrase this long stops reading as an accent and starts
   reading as a warning, so the accent is carried by the tick instead. */
.rule-head span:first-child { color: var(--ink); font-weight: 700; }
/* Set inline rather than as a flex item, so the tick rides the section name's
   own baseline. The gap matches the flex gap .eyebrow uses. */
.rule-head span:first-child::before {
	display: inline-block;
	margin-right: var(--sp-3);
	vertical-align: 0.3em;
}
.section--dark .rule-head,
.section--deepest .rule-head { border-top-color: var(--line-dark); color: var(--mist); }
.section--dark .rule-head span:first-child,
.section--deepest .rule-head span:first-child { color: var(--white); }
/* Link in the rule head meta slot. */
.rule-head a {
	display: inline-flex;
	align-items: center;
	gap: var(--sp-2);
}
.rule-head a .icon { width: 1em; height: 1em; }
@media (prefers-reduced-motion: no-preference) {
	.rule-head a .icon { transition: transform 0.2s var(--ease-out); }
}
.rule-head a:hover .icon,
.rule-head a:focus-visible .icon { transform: translateX(4px); }
/* Meta labels only. A link in this slot is the action, so it stays. */
@media (max-width: 36rem) { .rule-head span:last-child { display: none; } }

/* Breadcrumb strip — the interior-page counterpart to .rule-head, same mono
   microtext, never on the same page (two label strips stack as clutter). No
   accent tick: the tick marks a section name and this is navigation. No
   hairline either: a rule-head's line divides a section from what came
   before, and a breadcrumb under the header has nothing to divide. The gap
   beneath is one step past .sec-head--tight: a caption on the page title
   that still needs a beat of air before a display-size H1. */
.crumbs {
	margin-bottom: var(--sp-6);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: var(--tracking-caps);
	text-transform: uppercase;
	color: var(--ink-muted);
}
.crumbs ol {
	display: flex;
	flex-wrap: wrap;
	gap: var(--sp-3);
	margin: 0;
	padding: 0;
	list-style: none;
}
.crumbs li + li::before {
	content: "/";
	margin-right: var(--sp-3);
	color: var(--line-strong);
}
.crumbs [aria-current] { color: var(--ink); font-weight: 700; }

/* Lead paragraph — one step over body copy, muted. Defined at the top level
   rather than under .sec-head because a lead also opens a column of copy that
   is not a section head, and the two need to match. */
.lead { font-size: var(--text-lg); color: var(--ink-muted); }
.section--dark .lead,
.section--deepest .lead { color: var(--mist); }

/* Arrow link */
.arrow-link {
	display: inline-flex;
	align-items: center;
	gap: var(--sp-2);
	font-weight: 700;
	font-size: var(--text-sm);
	letter-spacing: 0.02em;
}
.arrow-link .icon { width: 1em; height: 1em; transition: transform 0.2s var(--ease-out); }
.arrow-link:hover .icon { transform: translateX(4px); }

/* Review stars */
.stars { display: inline-flex; gap: 2px; color: var(--amber); }
.stars .icon { width: 1rem; height: 1rem; }

/* --------------------------------------------------------------------------
   4. BUTTONS — sharp, uppercase, deliberate
   -------------------------------------------------------------------------- */
.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--sp-3);
	padding: 1.05rem 2rem;
	border: 0;
	border-radius: var(--radius);
	font: 700 0.875rem var(--font);
	letter-spacing: 0.09em;
	text-transform: uppercase;
	line-height: 1;
	white-space: nowrap;
	cursor: pointer;
	transition: background-color 0.18s, color 0.18s, box-shadow 0.18s;
}
.btn .icon { width: 1.05em; height: 1.05em; }

.btn--primary { background: var(--blue); color: var(--white); }
.btn--primary:hover { background: var(--blue-hot); color: var(--white); }

.btn--accent { background: var(--red); color: var(--white); }
.btn--accent:hover { background: var(--red-hot); color: var(--white); }

.btn--ghost {
	background: transparent;
	color: var(--ink);
	box-shadow: inset 0 0 0 1px var(--line-strong);
}
.btn--ghost:hover { color: var(--blue); box-shadow: inset 0 0 0 1px var(--blue); }

/* Frosted glass on imagery/dark surfaces */
.btn--ghost-dark {
	background: rgba(8, 14, 28, 0.32);
	-webkit-backdrop-filter: blur(10px);
	backdrop-filter: blur(10px);
	color: var(--white);
	box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.28);
}
.btn--ghost-dark:hover {
	background: rgba(8, 14, 28, 0.5);
	box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
	color: var(--white);
}

/* --------------------------------------------------------------------------
   5. STRUCTURE — sections, hairline grids, duotone media
   -------------------------------------------------------------------------- */
.section { position: relative; padding-block: var(--section-pad); }

/* A section that hands its bottom gap to whatever follows, used where the
   next element sets its own approach distance. */
.section--flush-bottom { padding-bottom: 0; }
/* The same on the way in, for a section whose first child is a full-bleed
   plane meant to touch the color above it rather than clear it. Type cannot
   sit on this edge, so whatever leads the section carries its own inset. */
.section--flush-top { padding-top: 0; }

/* Tight against a strong edge above. Pair with .section--flush-bottom when
   the next block is itself an edge (a color change, a bar). */
.section--tight { padding-top: var(--section-pad-tight); }
/* Same token on the way out. Use this instead of flush-bottom when the last
   line of type would otherwise sit on a color join. */
.section--tight-bottom { padding-bottom: var(--section-pad-tight); }
/* Hairline above a section that shares a plane with the one before it.
   Used when stacked chapters sit on one wash and need a divide without a
   color change. */
.section--divide { border-top: 1px solid var(--line); }
/* One-row mid-page band (service CTA + Google rating). --tight is still a
   chapter approach; this is clearance around a single control row. */
.section--strip { padding-block: var(--sp-7); }

/* First section on an interior page, opening directly under the header bar.
   Sized to clear the header's own --header-pad frame and land the
   breadcrumb, which is always the first mark on these pages. A full section
   pad (or even --section-pad-tight) against the floating bar reads as a hole
   rather than as caption clearance. */
.section--head { padding-top: var(--sp-5); }
/* An interior page title is a sentence, where the hero's is a two- or
   three-word poster stack, so it needs the full container to break well.
   .sec-head caps its measure at 58rem, which is right for a heading sitting
   inside a section's content but wrong at the top of a page: it ends the line
   well before the page's own width runs out and forces an extra line. */
.section--head .sec-head { max-width: none; }
/* The standfirst under it widens as well, but nothing like as far. It is body
   copy, so its measure is set by how far the eye can travel and still find
   the start of the next line, not by the column it sits in: 44rem breaks a
   two-sentence lead into three lines, and past roughly 54rem the return sweep
   starts costing more than the saved line is worth. */
.section--head .sec-head .lead { max-width: 54rem; }

/* White plane on --bg. A run of ambient gray makes long type look unfinished. Pair with a gray or navy neighbour so the edge reads. */
.section--surface { background: var(--surface); }

.section--dark { background: var(--navy); color: var(--mist); }
.section--dark h2, .section--dark h3 { color: var(--white); }

.section--deepest { background: var(--navy-deep); color: var(--mist); }
.section--deepest h2, .section--deepest h3 { color: var(--white); }

/* Duotone media — grayscale + navy/blue wash, sharp corners */
.duotone {
	position: relative;
	overflow: hidden;
}
.duotone img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	filter: grayscale(100%) contrast(1.06) brightness(0.96);
}
.duotone::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(120deg, rgba(10, 31, 68, 0.5), rgba(45, 95, 255, 0.32));
	mix-blend-mode: multiply;
	pointer-events: none;
}

/* Figure annotation — technical caption under media */
.figcap {
	display: block;
	padding-top: var(--sp-3);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 500;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--ink-muted);
}

/* Asymmetric split */
.split {
	display: grid;
	grid-template-columns: minmax(0, 6fr) minmax(0, 6fr);
	gap: clamp(2rem, 1rem + 4vw, 5rem);
	align-items: center;
}
.split--wide-left { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
/* A heading beside a taller column (FAQ list, not a checklist of similar
   height). Center alignment would float the H2 in the middle of the
   questions. */
.split--top { align-items: start; }
.split--top .sec-head {
	max-width: none;
	margin-bottom: 0;
}
.split--top > .faq { max-width: none; }
@media (max-width: 56rem) {
	.split, .split--wide-left { grid-template-columns: 1fr; }
}

/* Landscape job photo in a split. The figure stretches to the copy column
   so the top stays locked to the heading; the img is then taller than the
   figure by the section's own bottom pad plus the next section's tight top
   pad. That clears the white under the checklist (which keeps its pad) and
   stops at the next heading, on the right, short of the catalog. A negative
   margin on the figure cannot produce this hang: when copy and photo are
   the same height the row is already that tall, so the margin is a no-op.
   The img carries --shadow-panel because it is the overlapping surface, the
   same job as .about-panel. People stay natural color. */
.split:has(.split-photo) {
	align-items: stretch;
}
.split-photo {
	position: relative;
	z-index: 1;
	margin: 0;
	min-height: 0;
	align-self: stretch;
}
.split-photo img {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: calc(100% + var(--section-pad) + var(--section-pad-tight));
	object-fit: cover;
	object-position: var(--split-photo-pos, 82% 48%);
	box-shadow: var(--shadow-panel);
}
/* Standing portraits (a person at a panel) keep the head and the work
   in frame when the figure is a landscape crop of a tall file. */
.split-photo--upper { --split-photo-pos: 50% 28%; }
/* Kneeling or mid-frame portraits: the torso and the work, not
   the sky that --upper keeps for a standing head. */
.split-photo--mid { --split-photo-pos: 50% 40%; }
/* Landscape files whose subject sits on the left. Default 82% is for a
   right-parked subject and would crop the person out of a tall hanging well. */
.split-photo--left { --split-photo-pos: 18% 42%; }
/* No hang into the next section. Use when FAQ or other full-width type
   follows and there is no left-column heading for the overhang to clear. */
.split-photo--flush img {
	height: 100%;
}
/* In-flow type would sit at the top of the well and vanish under the
   absolute img. Park the caption at the overflow edge instead. */
.split-photo .figcap {
	position: absolute;
	left: 0;
	right: 0;
	top: calc(100% + var(--section-pad) + var(--section-pad-tight));
}
.split-photo--flush .figcap {
	top: 100%;
}
.section:has(.split-photo) {
	z-index: 1;
}
@media (max-width: 56rem) {
	.split-photo { aspect-ratio: 4 / 3; }
	.split-photo img {
		position: static;
		height: 100%;
	}
	/* A caption in the figure has to leave the crop, or it sits inside
	   the well and the aspect-ratio fights the type. */
	.split-photo:has(.figcap) { aspect-ratio: auto; }
	.split-photo:has(.figcap) img {
		aspect-ratio: 4 / 3;
		height: auto;
	}
	.split-photo .figcap { position: static; }
	/* Guide hangings: hide rather than stack a landscape job shot under
	   the chapters. Collapse the tracks so copy is not stuck in 7/12. */
	.split:has(.split-photo--desktop) {
		grid-template-columns: 1fr;
	}
	.split-photo--desktop { display: none; }
}

/* --------------------------------------------------------------------------
   6. HEADER
   -------------------------------------------------------------------------- */
/* Floating inset bar: the header itself is a transparent frame; the white
   bar is .site-header__inner, which also anchors the mega-menu. The bar was
   translucent with a backdrop blur until August 2026: the logo lockup is
   opaque on a white ground, and any alpha in the bar showed that ground as a
   patch whose seam moved with whatever scrolled beneath. The bar and the
   logo plate therefore share --surface, so the two can never drift apart. */
.site-header {
	position: sticky;
	top: 0;
	z-index: 100;
	padding: var(--header-pad) var(--bar-inset);
	background: transparent;
}
.site-header__inner {
	position: relative;
	display: flex;
	align-items: center;
	gap: var(--sp-2);
	height: var(--header-h);
	padding-inline: var(--bar-pad);
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--radius-bar);
	box-shadow: var(--shadow-bar);
}

.logo { display: inline-flex; }
/* Client raster lockup on opaque white. Header sits on --surface so the
   file's ground disappears. Footer uses logo-light.png. If --surface leaves
   white, the GIF shows as a patch. */
.logo__mark {
	display: block;
	width: auto;
	flex: none;
}
/* The height is --logo-h rather than a percentage of the bar: a replaced
   element sized by percentage height can be collapsed by the flex layout
   around it, because the base img max-width clamp recomputes its height
   through the aspect ratio whenever the bar's contents squeeze the
   anchor. Fixed height, flex: none on the anchor, and max-width: none
   make the size a constant. The file is cropped to its ink and carries
   no padding of its own, so the bar's inline padding lands the
   artwork's left edge on the gutter spine. */
.site-header .logo { flex: none; }
.site-header .logo__mark {
	height: var(--logo-h);
	max-width: none;
}

.site-nav { margin-left: auto; }
.site-nav__list {
	display: flex;
	align-items: center;
	gap: var(--sp-6);
	margin: 0;
	padding: 0;
	list-style: none;
}
/* A half step above --text-xs: at 0.75rem the uppercase labels read as
   fine print next to the logo and the phone number instead of as the bar's
   primary controls. */
.site-nav__list a {
	color: var(--ink);
	font-size: 0.8125rem;
	font-weight: 700;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	white-space: nowrap;
}
.site-nav__list a:hover { color: var(--blue); }

/* Top-level links only (mega and mobile links restyle themselves): padding
   grows the hit target inside the fixed-height bar, and a hairline slides in
   under the label on hover. currentColor keeps the underline on the link's
   own color through the hover flip to blue. */
@media (min-width: 80rem) {
	.site-nav__list > li > a {
		position: relative;
		display: inline-block;
		padding: var(--sp-2) 0;
	}
	.site-nav__list > li > a::after {
		content: "";
		position: absolute;
		left: 0;
		right: 0;
		bottom: 0;
		height: 2px;
		background: currentColor;
		transform: scaleX(0);
		transform-origin: left;
	}
	.site-nav__list > li > a:hover::after,
	.site-nav__list > li > a:focus-visible::after,
	.site-nav__list > .has-mega.is-open > a::after,
	.site-nav__list > .has-menu.is-open > a::after { transform: scaleX(1); }
}
@media (min-width: 80rem) and (prefers-reduced-motion: no-preference) {
	.site-nav__list > li > a::after { transition: transform 0.25s var(--ease-out); }
}

/* Parent links that open a panel (Electrical Services, Company) carry a
   chevron so they do not read as peers of Reviews. The mark is the sprite,
   sized to the uppercase label. Shown only in the full nav row. Hidden
   by default so the 80rem overlap (both min-width and max-width match)
   cannot flash a mark beside a link that will not open a panel. */
.site-nav__list > .has-mega > a > .icon,
.site-nav__list > .has-menu > a > .icon { display: none; }
@media (min-width: 80rem) {
	.site-nav__list > .has-mega > a,
	.site-nav__list > .has-menu > a {
		display: inline-flex;
		align-items: center;
		gap: var(--sp-1);
	}
	.site-nav__list > .has-mega > a > .icon,
	.site-nav__list > .has-menu > a > .icon {
		display: block;
		width: 1em;
		height: 1em;
	}
	.site-nav__list > .has-mega.is-open > a > .icon,
	.site-nav__list > .has-menu.is-open > a > .icon { transform: rotate(180deg); }
}
@media (min-width: 80rem) and (prefers-reduced-motion: no-preference) {
	.site-nav__list > .has-mega > a > .icon,
	.site-nav__list > .has-menu > a > .icon { transition: transform 0.2s var(--ease-out); }
}

.header-cta { display: flex; align-items: center; gap: var(--sp-5); }
/* Hairline between the nav links and the contact cluster, so Company and
   the phone number stop reading as one run of small type */
@media (min-width: 80rem) {
	.header-cta {
		margin-left: var(--sp-5);
		padding-left: var(--sp-6);
		border-left: 1px solid var(--line);
		transition: border-color 0.25s;
	}
}
.header-cta__phone {
	display: inline-flex;
	align-items: center;
	gap: var(--sp-2);
	color: var(--ink);
	font-family: var(--font-mono);
	font-size: var(--text-sm);
	font-weight: 700;
	white-space: nowrap;
}
.header-cta__phone .icon { width: 0.9rem; height: 0.9rem; color: var(--red); }
.header-cta .btn { padding: 0.8rem 1.4rem; }

/* Mega-menu — white panel dropping below the floating bar, matching its width.
   Opened by hover/focus (main.js toggles .is-open + body.mega-open). */
.mega {
	position: absolute;
	top: calc(100% + var(--sp-2));
	left: 0;
	right: 0;
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--radius-bar);
	box-shadow: var(--shadow-panel);
	/* Bottom pad is 0: .mega__all is the panel's bottom edge. overflow clips
	   that bar's navy hover to the panel's --radius-bar. */
	padding-block: var(--sp-6) 0;
	overflow: hidden;
	opacity: 0;
	visibility: hidden;
	transform: translateY(-10px);
	pointer-events: none;
}
.has-mega.is-open .mega {
	opacity: 1;
	visibility: visible;
	transform: none;
	pointer-events: auto;
}
@media (prefers-reduced-motion: no-preference) {
	.mega { transition: opacity 0.2s var(--ease-out), transform 0.24s var(--ease-out), visibility 0s 0.24s; }
	.has-mega.is-open .mega { transition: opacity 0.24s var(--ease-out), transform 0.24s var(--ease-out), visibility 0s; }
}

.mega__grid {
	display: grid;
	grid-template-columns: repeat(5, 1fr);
	gap: var(--sp-2);
	padding-inline: var(--bar-pad);
}

/* Reset the uppercase nav-link styling inherited inside the panel.
   white-space: normal is required: .site-nav__list a is nowrap so bar
   labels never break, and that would clip a wrapping lede inside the
   photo overflow. Titles and .mega__all opt back into nowrap. */
.site-nav__list .mega a {
	display: block;
	padding: 0;
	font-size: var(--text-sm);
	font-weight: 500;
	letter-spacing: 0.01em;
	text-transform: none;
	white-space: normal;
	color: var(--ink);
}
.site-nav__list .mega a:hover { color: var(--blue); }

/* Image-led category cards — same photo language as the services gallery */
.mega-card { min-width: 0; }
.site-nav__list .mega a.mega-card__tile {
	position: relative;
	overflow: hidden;
	/* Tall enough for a two-line lede in the bottom scrim. */
	height: clamp(12rem, 15vw, 14.5rem);
	background: var(--navy);
	color: var(--white);
}
.mega-card__media { position: absolute; inset: 0; margin: 0; }
.mega-card__media img { width: 100%; height: 100%; object-fit: cover; }
.mega-card__media::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(to top,
		rgba(var(--scrim-rgb), 0.9) 0%,
		rgba(var(--scrim-rgb), 0.68) 34%,
		rgba(var(--scrim-rgb), 0.12) 70%,
		rgba(var(--scrim-rgb), 0.28) 100%);
}
.mega-card__title {
	display: flex;
	align-items: center;
	gap: 0.4rem;
	color: var(--white);
	font-size: var(--text-base);
	font-weight: 700;
	letter-spacing: -0.01em;
	white-space: nowrap;
	line-height: 1.2;
}
.mega-card__title .icon { width: 0.85em; height: 0.85em; }
/* Title + flavor line stacked on the photo. */
.mega-card__copy {
	position: absolute;
	left: var(--sp-4);
	right: var(--sp-4);
	bottom: calc(var(--sp-3) + 3px);
	z-index: 1;
	display: flex;
	flex-direction: column;
	/* Do not set align-items: flex-start. That sizes the lede to the
	   string instead of the tile, and overflow:hidden on the photo zoom
	   clips the last word. */
	gap: var(--sp-1);
}
/* Flavor of the work, not a child-page list: three links under a tile
   read as the complete offering. One pixel under --text-sm: there is
   no token between that and --text-xs. */
.mega-card__lede {
	min-width: 0;
	width: 100%;
	font-size: calc(var(--text-sm) - 1px);
	font-weight: 500;
	letter-spacing: 0;
	text-transform: none;
	line-height: 1.35;
	color: var(--mist);
}
@media (prefers-reduced-motion: no-preference) {
	.mega-card__media img { transition: transform 0.5s var(--ease-out); }
	.mega-card__title .icon { transition: transform 0.2s var(--ease-out); }
}
.mega-card__tile:hover .mega-card__media img,
.mega-card__tile:focus-visible .mega-card__media img { transform: scale(1.05); }
.mega-card__tile:hover .mega-card__title .icon,
.mega-card__tile:focus-visible .mega-card__title .icon { transform: translateX(3px); }

/* Full-bleed so the hit area is the panel width, not the label. --bg at rest
   draws the bar; --navy on hover is the dark-blue state. The catalog line
   sits left, the action right: one row, one destination. */
.site-nav__list .mega a.mega__all {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sp-4);
	margin-top: var(--sp-5);
	padding: var(--sp-4) var(--bar-pad);
	border-top: 1px solid var(--line);
	font-weight: 700;
	white-space: nowrap;
	color: var(--blue);
	background: var(--bg);
}
.site-nav__list .mega a.mega__all .microtext { min-width: 0; }
.site-nav__list .mega a.mega__all .mega__all-label {
	display: inline-flex;
	align-items: center;
	gap: var(--sp-2);
	flex: none;
}
.site-nav__list .mega a.mega__all .icon { width: 1em; height: 1em; }
.site-nav__list .mega a.mega__all:hover,
.site-nav__list .mega a.mega__all:focus-visible {
	color: var(--white);
	background: var(--navy);
}
.site-nav__list .mega a.mega__all:hover .microtext,
.site-nav__list .mega a.mega__all:focus-visible .microtext { color: var(--mist); }
.site-nav__list .mega a.mega__all:focus-visible { outline-offset: -2px; }
@media (prefers-reduced-motion: no-preference) {
	.site-nav__list .mega a.mega__all {
		transition: background-color 0.18s, color 0.18s;
	}
	.site-nav__list .mega a.mega__all .microtext { transition: color 0.18s; }
	.site-nav__list .mega a.mega__all .icon { transition: transform 0.2s var(--ease-out); }
	.site-nav__list .mega a.mega__all:hover .icon,
	.site-nav__list .mega a.mega__all:focus-visible .icon { transform: translateX(4px); }
}

/* Compact Company list. Same surface as the mega (white, hairline,
   --shadow-panel) but sized to the trigger, not the bar. No page frost:
   six text links are not a takeover. left: 0 so the panel hangs from the
   start of Company, not the chevron. There is no /company/ page, so the
   trigger goes to /about/. */
.has-menu { position: relative; }
.menu {
	position: absolute;
	top: calc(100% + var(--sp-2));
	left: 0;
	z-index: 1;
	min-width: 12rem;
	margin: 0;
	padding: var(--sp-3) 0;
	list-style: none;
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--radius-bar);
	box-shadow: var(--shadow-panel);
	opacity: 0;
	visibility: hidden;
	transform: translateY(-10px);
	pointer-events: none;
}
.has-menu.is-open .menu {
	opacity: 1;
	visibility: visible;
	transform: none;
	pointer-events: auto;
}
@media (prefers-reduced-motion: no-preference) {
	.menu { transition: opacity 0.2s var(--ease-out), transform 0.24s var(--ease-out), visibility 0s 0.24s; }
	.has-menu.is-open .menu { transition: opacity 0.24s var(--ease-out), transform 0.24s var(--ease-out), visibility 0s; }
}
.site-nav__list .menu a {
	display: block;
	font-size: var(--text-sm);
	font-weight: 500;
	letter-spacing: 0.01em;
	text-transform: none;
	color: var(--ink);
}
.site-nav__list .menu a:hover { color: var(--blue); }
@media (min-width: 80rem) {
	.site-nav__list .menu a { padding: var(--sp-2) var(--sp-5); }
}
/* Desktop: the image mega is the Services panel. This list is the
   collapsed-nav counterpart and stays out of the full row. */
.has-mega > .menu { display: none; }

/* Dim + blur the page behind an open mega-menu. Covers the full viewport,
   including the margin around the floating bar; the header's higher z-index
   keeps the bar itself crisp. */
body::after {
	content: "";
	position: fixed;
	inset: 0;
	z-index: 90;
	background: rgba(5, 13, 33, 0.38);
	-webkit-backdrop-filter: blur(5px);
	backdrop-filter: blur(5px);
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
}
body.mega-open::after { opacity: 1; visibility: visible; }
@media (prefers-reduced-motion: no-preference) {
	body::after { transition: opacity 0.24s var(--ease-out), visibility 0s 0.24s; }
	body.mega-open::after { transition: opacity 0.24s var(--ease-out), visibility 0s; }
}

.nav-toggle {
	display: none;
	flex-direction: column;
	justify-content: center;
	gap: 5px;
	width: 2.75rem;
	height: 2.75rem;
	padding: 0 0.6rem;
	/* flex: none because a squeezed toggle collapses its bars' width to
	   nothing and leaves an empty outline in the bar. */
	flex: none;
	background: none;
	border: 1px solid var(--line-strong);
	border-radius: var(--radius);
	cursor: pointer;
}
.nav-toggle__bar {
	height: 2px;
	background: var(--ink);
	transition: transform 0.25s var(--ease-out), opacity 0.2s;
}

/* The bar collapses to the toggle pattern at 80rem, well above the site's
   56rem stacking point: with the full-size nav labels, the divider, the
   phone number, and the CTA, one row only fits above ~1280px, and no header
   label is ever allowed to wrap to a second line. */
@media (max-width: 80rem) {
	/* Drops from the bar and matches its width, like the mega panel */
	.site-nav {
		position: absolute;
		top: calc(100% + var(--sp-2));
		left: 0;
		right: 0;
		margin: 0;
		background: var(--surface);
		border: 1px solid var(--line);
		border-radius: var(--radius-bar);
		box-shadow: var(--shadow-panel);
		padding: var(--sp-5);
		display: none;
	}
	.site-nav.is-open { display: block; }
	.site-nav__list { flex-direction: column; align-items: stretch; gap: 0; }
	.site-nav__list a {
		display: block;
		padding: var(--sp-4);
		font-size: var(--text-sm);
		border-bottom: 1px solid var(--line);
	}
	.site-nav__list > li:last-child > a { border-bottom: 0; }
	/* At exactly 80rem the full-row link treatment (inline padding, hover
	   underline) also matches; these overrides must win so the open panel's
	   links keep the panel's spacing and drop the slide-in underline */
	.site-nav__list > li > a {
		display: block;
		padding: var(--sp-4);
	}
	.site-nav__list > li > a::after { content: none; }
	/* Image mega pairs with the full nav row. Collapsed, only the five
	   service categories stay as a text list; Company does not, because
	   both lists together overran a small screen. Chevrons go either way:
	   a mark would imply a toggle that is not there. */
	.site-nav__list > .has-mega > a > .icon,
	.site-nav__list > .has-menu > a > .icon { display: none; }
	.mega { display: none; }
	.has-mega,
	.has-menu { position: static; }
	.has-mega > .menu { display: block; }
	.has-menu > .menu { display: none; }
	.menu {
		position: static;
		z-index: auto;
		min-width: 0;
		padding: 0;
		border: 0;
		border-radius: 0;
		box-shadow: none;
		opacity: 1;
		visibility: visible;
		transform: none;
		pointer-events: auto;
		transition: none;
	}
	/* One step further left than the section labels so the five names
	   read as children. Right pad stays --sp-4 so the hairline ladder
	   still shares one trailing edge. The arrow sits after the word
	   the way .arrow-link does; space-between parked it on the far
	   edge and left a hole next to a short name. */
	.site-nav__list .menu a {
		display: flex;
		align-items: center;
		gap: var(--sp-2);
		padding: var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-6);
		font-weight: 600;
		letter-spacing: 0.01em;
		text-transform: none;
		color: var(--ink-muted);
	}
	.site-nav__list .menu a::after {
		content: "\2192";
		color: var(--line-strong);
	}
	.site-nav__list .menu a:hover,
	.site-nav__list .menu a:focus-visible { color: var(--blue); }
	.site-nav__list .menu a:hover::after,
	.site-nav__list .menu a:focus-visible::after { color: var(--blue); }
	@media (prefers-reduced-motion: no-preference) {
		.site-nav__list .menu a::after { transition: color 0.15s, transform 0.15s var(--ease-out); }
		.site-nav__list .menu a:hover::after,
		.site-nav__list .menu a:focus-visible::after { transform: translateX(3px); }
	}
	/* The bar's own gap is minimal; the toggle carries its clearance from
	   the contact cluster itself */
	.nav-toggle { display: flex; margin-left: auto; margin-right: var(--sp-4); }
	/* At exactly 80rem this block and the min-width divider rule both match;
	   this later block must win so the toggle never shows beside the divider */
	.header-cta {
		margin-left: 0;
		padding-left: 0;
		border-left: 0;
	}
	.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) { transform: translateY(7px) rotate(45deg); }
	.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2) { opacity: 0; }
	.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
}
/* Small-screen ladder. Every label in the bar is nowrap, so nothing can
   shrink: instead the bar sheds pieces in value order as the room runs out.
   The thresholds are where the next-widest arrangement stops fitting. */
@media (max-width: 44rem) {
	/* The number goes first; the icon alone stays tappable */
	.header-cta__phone span { display: none; }
	.header-cta { gap: var(--sp-3); }
}
@media (max-width: 36rem) {
	/* Crop the lockup to its mark. Width is --logo-h times the mark's share of
	   the file: ink spans the first 232 columns of the 800x311 canvas
	   (wordmark starts at column 314). Re-measure if the file changes. */
	.site-header .logo__mark {
		width: calc(var(--logo-h) * 232 / 311);
		object-fit: cover;
		object-position: left center;
	}
}
@media (max-width: 22.5rem) {
	/* Smallest screens: the phone link leaves the bar entirely — the number
	   is still one tap away in the menu, the hero, and the footer */
	.header-cta__phone { display: none; }
}

/* --------------------------------------------------------------------------
   7. HERO — one full-bleed photo directly below the header bar. Copy sits
   low-left on a charcoal scrim; a frosted spec bar overlays the bottom edge.
   -------------------------------------------------------------------------- */
.hero {
	position: relative;
	display: flex;
	flex-direction: column;
	/* Content-sized: .hero__content's padding sets the height. The negative
	   margin pulls the photo up behind the sticky bar's transparent frame,
	   so the bar floats over the hero instead of stacking a page-colored
	   band above it; the bar's z-index keeps it on top. */
	margin-top: calc(-1 * var(--header-total));
	background: var(--navy);
	overflow: hidden;
}

.hero__media { position: absolute; inset: 0; }
.hero__media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	/* Default parks the subject on the right so the headline sits on the quieter side. Hubs override --hero-pos. */
	object-position: var(--hero-pos, 85% 50%);
}
/* Legibility scrim, three washes under one grain layer, all charcoal.
   Navy in the top band against charcoal from the left was two hues in the
   same corner and read as a clash. One color, three directions: a short
   band under the header, a lift under the copy, and a left-to-right cast
   that has to reach the H1. That heading is 20ch of display type, which
   on a wide screen sits past 50% of the hero, so the old 62% fade-out
   left the last words on the photo. */
.hero__media::after {
	content: "";
	position: absolute;
	inset: 0;
	pointer-events: none;
	background-image:
		url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.07'/%3E%3C/svg%3E"),
		linear-gradient(to bottom,
			rgba(var(--scrim-rgb), 0.55) 0%,
			rgba(var(--scrim-rgb), 0.22) 10%,
			rgba(var(--scrim-rgb), 0) 22%),
		linear-gradient(to top,
			rgba(var(--scrim-rgb), 0.88) 0%,
			rgba(var(--scrim-rgb), 0.5) 32%,
			rgba(var(--scrim-rgb), 0) 64%),
		linear-gradient(to right,
			rgba(var(--scrim-rgb), 0.72) 0%,
			rgba(var(--scrim-rgb), 0.55) 32%,
			rgba(var(--scrim-rgb), 0.38) 50%,
			rgba(var(--scrim-rgb), 0.18) 68%,
			rgba(var(--scrim-rgb), 0) 84%);
}

.hero__content {
	position: relative;
	z-index: 1;
	/* The hero is content-sized, so this padding is what sets its height and
	   proportions: the top term clears the bar floating over the photo before
	   adding breathing room, the bottom holds the copy off the spec bar. */
	padding-top: calc(var(--header-total) + var(--sp-8));
	padding-bottom: clamp(3.5rem, 2.5rem + 4vw, 7rem);
}
.hero__content h1 {
	color: var(--white);
	font-size: clamp(2.6rem, 1.3rem + 3.6vw, 4.5rem);
	/* Wraps the headline to two to three stacked lines, poster-style,
	   instead of one long strip across the photo. */
	max-width: 20ch;
	margin-bottom: var(--sp-4);
}
.hero__content h1 .accent-dot { color: var(--red); }
.hero__lead {
	font-size: var(--text-lg);
	line-height: 1.55;
	color: rgba(255, 255, 255, 0.8);
	max-width: 34rem;
	margin-bottom: var(--sp-6);
}
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
/* The charcoal scrim is the same value as the frosted ghost fill, so the
   phone button disappeared into the overlay. On the hero it is a hairline
   ghost: no fill, mist edge, still secondary to the blue CTA. The frosted
   fill stays on the navy CTA band, where the ground is flat. */
.hero .btn--ghost-dark {
	background: transparent;
	-webkit-backdrop-filter: none;
	backdrop-filter: none;
	box-shadow: inset 0 0 0 1px var(--mist);
}
.hero .btn--ghost-dark:hover {
	background: rgba(255, 255, 255, 0.08);
	box-shadow: inset 0 0 0 1px var(--white);
}

/* Frosted spec bar riding on the photo's bottom edge */
.hero__bar {
	position: relative;
	z-index: 1;
	background: rgba(4, 10, 24, 0.25);
	-webkit-backdrop-filter: blur(16px);
	backdrop-filter: blur(16px);
	border-top: 1px solid var(--line-dark);
}
.hero__bar-grid {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
}
.spec { padding: var(--sp-5) var(--sp-6); }
/* Outer cells sit flush to the gutter so the bar shares the page's left edge */
.spec:first-child { padding-left: 0; }
.spec:last-child { padding-right: 0; }
.spec + .spec { border-left: 1px solid var(--line-dark); }
.spec__value {
	display: flex;
	align-items: center;
	gap: var(--sp-2);
	font-size: var(--text-lg);
	font-weight: 800;
	letter-spacing: -0.01em;
	color: var(--white);
	margin-bottom: var(--sp-1);
}
.spec__label {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 500;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--mist);
}
@media (max-width: 56rem) {
	.hero__bar-grid { grid-template-columns: repeat(2, 1fr); }
	.spec { padding: var(--sp-4) var(--sp-5); }
	.spec:nth-child(odd) { border-left: 0; }
	.spec:nth-child(n+3) { border-top: 1px solid var(--line-dark); }
}

/* Hub heroes: breadcrumbs ride the photo scrim, so they invert. */
.hero .crumbs {
	margin-bottom: var(--sp-5);
	color: var(--mist);
}
.hero .crumbs a { color: var(--mist); }
.hero .crumbs a:hover { color: var(--white); }
.hero .crumbs [aria-current] { color: var(--white); }
	.hero .crumbs li + li::before { color: var(--line-dark); }

/* Hero pages park the subject with --hero-pos so the headline sits on the quieter side of the photo. */
.hero--generators { --hero-pos: 80% 50%; }
/* Centered 24kW pad unit: favor the left of the file so the generator sits right of the scrim. */
.hero--generator-service { --hero-pos: 35% 45%; }
/* Portable unit and inlet: keep the orange generator right of the scrim. */
.hero--portable { --hero-pos: 68% 58%; }
.hero--residential { --hero-pos: 58% 82%; }
/* Aerial lake houses: keep water and shoreline on the right of the headline. */
.hero--lakefront { --hero-pos: 62% 48%; }
/* New panel on a black backer: panel center, quiet framing on the left. */
.hero--panel { --hero-pos: 58% 48%; }
/* Hallway cans and living-room fixtures: subject mid-frame, not the floor. */
.hero--hallway { --hero-pos: 55% 38%; }
.hero--living { --hero-pos: 58% 40%; }
.hero--commercial { --hero-pos: 72% 48%; }
/* Control cabinet and trench/van: park the gear or truck right of the headline. */
.hero--cabinet { --hero-pos: 55% 48%; }
.hero--trench { --hero-pos: 78% 42%; }
.hero--ev-chargers { --hero-pos: 72% 45%; }
/* Wall connector on a blank wall: keep the unit centered in the crop. */
.hero--ev-wall { --hero-pos: 50% 42%; }
.hero--construction { --hero-pos: 68% 48%; }
/* Framing and outlet rough-in: boxes mid-frame, not the workbench. */
.hero--framing { --hero-pos: 62% 45%; }
.hero--outlet-rough { --hero-pos: 50% 38%; }
/* Disconnect on siding: the box sits just right of center with the whip on
   the left, so a centered crop keeps both and the headline covers the whip. */
.hero--disconnect { --hero-pos: 55% 50%; }
/* Deck under a lit soffit, portrait file: aim the band at the railing and
   windows so the soffit cans ride the top edge and the sky falls out. */
.hero--deck { --hero-pos: 50% 58%; }
/* Rooftop fan and feeder spools: kneeling or standing technician mid-frame,
   with enough headroom that the crop does not take the head. */
.hero--roof { --hero-pos: 48% 50%; }
.hero--spool { --hero-pos: 50% 40%; }
/* Shingled gable end: keep the right window and the lit soffit line in the
   crop and let the snow at the bottom of the file fall out. */
.hero--shingle { --hero-pos: 70% 45%; }

/* --------------------------------------------------------------------------
   8. PAGE SECTIONS
   -------------------------------------------------------------------------- */

/* ----- Section head block ----- */
/* Headings get the wider measure; the lead stays narrow enough to read comfortably */
.sec-head { position: relative; z-index: 1; max-width: 58rem; margin-bottom: clamp(2.5rem, 2rem + 2vw, 4rem); }
/* For a head whose next element belongs to it, rather than opening the section's
   content: the standard clearance would read as a break between the two. */
.sec-head--tight { margin-bottom: var(--sp-6); }
.sec-head .lead { max-width: 44rem; }
/* Chapter label beside a taller column (FAQ groups on /faqs/).
   Red sprite + H2 midway between --text-2xl and the default H2. */
.sec-head--icon {
	--sec-head-label: calc((var(--text-2xl) + var(--text-3xl)) / 2);
	display: flex;
	align-items: center;
	gap: var(--sp-3);
}
.sec-head--icon .sec-head__icon {
	flex: none;
	width: calc(var(--sec-head-label) * 0.72);
	height: calc(var(--sec-head-label) * 0.72);
	color: var(--red);
}
.sec-head--icon h2 {
	margin: 0;
	font-size: var(--sec-head-label);
}

/* ----- Services: full-bleed expanding image panels ----- */
.expander {
	display: flex;
	gap: var(--sp-3);
	height: clamp(28rem, 62vh, 37rem);
	width: 100%;
	--exp-dur: 0.35s;
	--exp-ease: cubic-bezier(0.4, 0, 0.2, 1);
}
.expander__panel {
	position: relative;
	flex: 1 1 0;
	overflow: hidden;
	display: block;
	color: var(--white);
}
@media (prefers-reduced-motion: no-preference) {
	.expander__panel { transition: flex-grow var(--exp-dur) var(--exp-ease); }
}
.expander__panel:hover,
.expander__panel:focus-visible { flex-grow: 2.6; color: var(--white); }
.expander__panel:focus-visible { outline-offset: -2px; }

.expander__media { position: absolute; inset: 0; }
.expander__media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
}
@media (prefers-reduced-motion: no-preference) {
	.expander__media img { transition: transform var(--exp-dur) var(--exp-ease); }
}
.expander__panel:hover .expander__media img { transform: scale(1.04); }
.expander__media::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(to bottom,
		rgba(3, 8, 20, 0.82) 0%,
		rgba(3, 8, 20, 0.18) 55%,
		rgba(3, 8, 20, 0.3) 100%);
}

.expander__body {
	position: absolute;
	left: 0;
	right: 0;
	top: 0;
	z-index: 1;
	padding: var(--sp-5);
}
/* Titles sit on the same top edge; the collapsed reveal below them takes zero height */
.expander__body h3 {
	color: var(--white);
	font-size: var(--text-xl);
	margin: 0;
	white-space: nowrap;
}

/* Height-animated reveal: 0fr (no layout space) -> 1fr on hover/focus */
.expander__reveal {
	display: grid;
	grid-template-rows: 0fr;
}
.expander__reveal > div { overflow: hidden; min-height: 0; }
.expander__body p {
	width: 20rem;
	max-width: 100%;
	font-size: var(--text-base);
	color: rgba(255, 255, 255, 0.85);
	margin: var(--sp-3) 0;
	opacity: 0;
}
.expander__go {
	display: block;
	margin-bottom: var(--sp-1);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--white);
	opacity: 0;
}
/* Go line sits outside .expander__reveal (direct child of .expander__body)
   so panels read as links at rest. The description still reveals on hover. */
.expander__body > .expander__go {
	opacity: 1;
	margin-top: var(--sp-3);
}
@media (prefers-reduced-motion: no-preference) {
	.expander__reveal { transition: grid-template-rows var(--exp-dur) var(--exp-ease); }
	.expander__body { transition: opacity 0.25s var(--exp-ease); }
	/* Delay only on the way in, so copy is not still opaque while collapsing. */
	.expander__body p,
	.expander__go { transition: opacity 0.2s var(--exp-ease); }
	.expander__panel:hover .expander__body p,
	.expander__panel:hover .expander__go,
	.expander__panel:focus-visible .expander__body p,
	.expander__panel:focus-visible .expander__go {
		transition: opacity 0.28s var(--exp-ease) 0.1s;
	}
}

/* On narrower desktops, compressed panels can't fit their titles —
   fade out labels of the non-active panels while one is expanded */
@media (min-width: 56rem) and (max-width: 78rem) {
	.expander:hover .expander__panel:not(:hover) .expander__body,
	.expander:focus-within .expander__panel:not(:focus-visible) .expander__body {
		opacity: 0;
	}
}
.expander__panel:hover .expander__reveal,
.expander__panel:focus-visible .expander__reveal { grid-template-rows: 1fr; }
.expander__panel:hover .expander__body p,
.expander__panel:hover .expander__go,
.expander__panel:focus-visible .expander__body p,
.expander__panel:focus-visible .expander__go { opacity: 1; }

/* Stack on small screens; descriptions always visible (no hover on touch) */
@media (max-width: 56rem) {
	.expander { flex-direction: column; height: auto; gap: var(--sp-2); }
	.expander__panel { min-height: 13rem; }
	.expander__reveal { grid-template-rows: 1fr; }
	.expander__body p, .expander__go { opacity: 1; }
	/* Text hugs the left edge here, so the scrim runs left-to-right and heavier */
	.expander__media::after {
		background: linear-gradient(to right,
			rgba(3, 8, 20, 0.92) 0%,
			rgba(3, 8, 20, 0.78) 45%,
			rgba(3, 8, 20, 0.25) 100%);
	}
}

/* Placed third-party marks, never recolored. White plane because every mark
   ships on opaque white. Distinct from the drawn .badge seals. */
.creds {
	/* Scaled rather than fixed: four cells plus their padding leave the widest
	   mark about 185px at the low end of the four-across range, and it needs
	   191px at full size. The ramp reaches the 5.75rem maximum at --container
	   and hands back roughly 6px of mark for every 100px of lost viewport. */
	--cred-size: clamp(4.5rem, 1.9rem + 4.375vw, 5.75rem);
	margin-top: var(--sp-3);
	background: var(--surface);
	border-block: 1px solid var(--line);
}
.creds__row {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	margin: 0;
	padding: 0;
	list-style: none;
}
.cred {
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--sp-6) var(--sp-5);
}
.cred + .cred { border-left: 1px solid var(--line); }

/* Sized to balanced optical mass, not equal height and not equal width. These
   marks run from 2.94:1 to 4.08:1, so at a shared height the widest covers
   about a third more of the row than the narrowest, and matching widths
   inverts the same problem. Two measurements set each height: the aspect ratio
   of the mark's ink box, and how much of that box is actually ink (58% for the
   Chamber block, 31% for Kohler's open type). Equalising box area alone leaves
   the sparse marks looking small; equalising ink area alone blows them up past
   the solid ones. Each factor is the geometric mean of the two, normalised so
   Generac holds still, and CSS sqrt() is too new to depend on so the roots are
   folded in. Adjust one mark against the rest with its own factor; scale the
   whole row with --cred-size.
   The three local files are cropped to their ink so their canvas and ink box
   are the same rectangle. The BBB seal cannot be cropped, and its 8px inset
   on every side is why its factor is not comparable to the other three. */
.cred img {
	width: auto;
	max-width: 100%;
	height: calc(var(--cred-size) * var(--cred-f));
	/* Guards the aspect ratio if max-width ever wins over the computed width */
	object-fit: contain;
}
/* Ink 4.08:1 at 39% density, but the factor drives the 3.50:1 canvas, which
   runs 25% taller than the ink box. Both corrections push the same way, which
   is why this number sits so far above the others. */
.cred--bbb { --cred-f: 0.648; }
.cred--generac { --cred-f: 0.545; }  /* 3.37:1 at 47% */
.cred--kohler { --cred-f: 0.559; }   /* 3.92:1 at 31%, the sparsest mark */
.cred--chamber { --cred-f: 0.553; }  /* 2.94:1 at 58%, the densest */

/* Two of the four link out. The lift is the whole affordance: the marks cannot
   be tinted, and the cell cannot invert behind artwork on a white ground. */
.cred__link { display: flex; }
@media (prefers-reduced-motion: no-preference) {
	.cred__link { transition: transform 0.2s var(--ease-out); }
}
.cred__link:hover,
.cred__link:focus-visible { transform: translateY(-2px); }

/* Below roughly 200px a cell the marks start crowding their dividers */
@media (max-width: 62rem) {
	.creds { margin-top: var(--sp-2); }
	.creds__row { grid-template-columns: repeat(2, 1fr); }
	.cred:nth-child(odd) { border-left: 0; }
	.cred:nth-child(n + 3) { border-top: 1px solid var(--line); }
}
@media (max-width: 36rem) {
	.creds { margin-top: var(--sp-2); }
}
/* One column on the narrowest phones. The BBB seal needs 163px at the floor of
   --cred-size, and two cells stop supplying that just under 28rem. Past there
   only that one mark would shrink, which is the unevenness the row exists to
   fix, so the row drops to one column instead. */
@media (max-width: 28rem) {
	.creds__row { grid-template-columns: 1fr; }
	.cred + .cred { border-left: 0; border-top: 1px solid var(--line); }
}

/* Two-mark variant. Three equal cells: kicker, brand names, link.
   Rules sit after the base row's media queries so they win on order. */
.creds--makers { margin-top: 0; }
.creds--makers .creds__row { grid-template-columns: repeat(3, 1fr); }
.creds__note {
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: flex-start;
	gap: var(--sp-2);
	padding: var(--sp-6) var(--sp-5) var(--sp-6) var(--gutter);
}
.creds__note .microtext { margin: 0; }
.creds__brands {
	margin: 0;
	font-size: var(--text-xl);
	font-weight: 800;
	letter-spacing: -0.02em;
	line-height: 1.1;
	color: var(--ink);
}
.creds--makers .cred { border-left: 1px solid var(--line); border-top: 0; }

@media (max-width: 62rem) {
	.creds--makers .creds__row { grid-template-columns: 1fr 1fr; }
	.creds__note {
		grid-column: 1 / -1;
		padding-right: var(--gutter);
		border-bottom: 1px solid var(--line);
	}
	.creds--makers .cred:nth-child(2) { border-left: 0; }
}
@media (max-width: 28rem) {
	.creds--makers .creds__row { grid-template-columns: 1fr; }
	.creds--makers .cred { border-left: 0; border-top: 1px solid var(--line); }
}

/* ----- Financing CTA — a shadowed plate between two dark sections -----
   Not a .section: no --section-pad, no rule-head. The brand cell is navy because
   Wisetack's lockup is white-only and is never recolored. */
.fin-cta { padding: clamp(3rem, 2.5rem + 2vw, 4.5rem) var(--gutter); }
/* overflow keeps the brand cell's navy inside the rounded corners */
.fin-cta__plate {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr) auto;
	grid-template-rows: 1fr auto;
	overflow: hidden;
	background: var(--surface);
	border-radius: var(--radius);
	box-shadow: var(--shadow-panel);
}
/* Spans both rows, so the navy runs the plate's full height and anchors it */
.fin-cta__brand {
	grid-row: 1 / -1;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--sp-6) var(--sp-7);
	background: var(--navy);
}
.fin-cta__logo { height: 1.75rem; width: auto; }
.fin-cta__copy { padding: var(--sp-6) var(--sp-7); }
/* Tracking loosened from the h2 default, which is drawn for display sizes */
.fin-cta__copy h2 {
	margin: 0 0 var(--sp-4);
	font-size: var(--text-3xl);
	letter-spacing: -0.02em;
}
/* h2 + p so the pitch alone is styled: the eyebrow is also a p in this cell */
.fin-cta__copy h2 + p {
	max-width: 72ch;
	margin: 0;
	font-size: var(--text-base);
	color: var(--ink-muted);
}
.fin-cta__actions {
	display: flex;
	flex-direction: column;
	justify-content: center;
	gap: var(--sp-3);
	padding: var(--sp-6) var(--sp-7);
	border-left: 1px solid var(--line);
}
.fin-cta__actions .arrow-link { justify-content: center; }
/* Wisetack's short disclosure, a footnote row under its own hairline. Links
   stay in the running color so the legal line reads as one quiet block; blue
   at this size looks like two more CTAs. */
.fin-cta__fine {
	grid-column: 2 / -1;
	margin: 0;
	padding: var(--sp-4) var(--sp-7);
	border-top: 1px solid var(--line);
	font-size: var(--text-xs);
	line-height: 1.6;
	color: var(--ink-muted);
}
.fin-cta__fine a {
	color: inherit;
	text-decoration: underline;
	text-decoration-color: var(--line-strong);
	text-underline-offset: 0.15em;
}
.fin-cta__fine a:hover { color: var(--blue); text-decoration-color: currentColor; }

/* Stacked: navy cap, copy, actions in a row, footnote at the foot */
@media (max-width: 62rem) {
	.fin-cta__plate { grid-template-columns: 1fr; grid-template-rows: none; }
	.fin-cta__brand { grid-row: auto; padding: var(--sp-4) var(--sp-6); }
	.fin-cta__logo { height: 1.5rem; }
	.fin-cta__copy { padding: var(--sp-6); }
	.fin-cta__actions {
		flex-direction: row;
		flex-wrap: wrap;
		align-items: center;
		padding: 0 var(--sp-6) var(--sp-6);
		border-left: 0;
	}
	.fin-cta__fine { grid-column: 1 / -1; padding: var(--sp-4) var(--sp-6); }
}

/* Interior financing ledger. Navy cap because the lockup is white-only.
   Long disclosure is --text-sm: smaller and a 700-character paragraph drops below readable size. */
.fin-offer {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	overflow: hidden;
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--radius);
}
.fin-offer__brand,
.fin-offer__fine { grid-column: 1 / -1; }
.fin-offer__brand {
	display: flex;
	align-items: center;
	padding: var(--sp-5) var(--sp-7);
	background: var(--navy);
}
.fin-offer__logo { height: 1.75rem; width: auto; }
.fin-offer__body {
	display: flex;
	flex-direction: column;
	justify-content: center;
	padding: var(--sp-6) var(--sp-7) var(--sp-7);
}
.fin-offer__media {
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--sp-6) var(--sp-7);
	border-left: 1px solid var(--line);
	/* Slightly off --surface so the phone mockup's white ground keeps an edge. */
	background: #fbfbfb;
}
/* Display width only; height follows the file's ratio. */
.fin-offer__preview { width: 18rem; }
/* --text-2xl rather than the section-poster h2: four short spec lines cannot
   carry a --text-3xl heading inside this ledger. */
.fin-offer__body h2 {
	margin: 0 0 var(--sp-5);
	font-size: var(--text-2xl);
	letter-spacing: -0.02em;
}
.fin-offer__body > p { max-width: 54rem; }
.fin-offer__list {
	margin: 0 0 var(--sp-6);
	padding: 0;
	list-style: none;
	border-top: 1px solid var(--line);
}
.fin-offer__list li {
	margin: 0;
	padding: var(--sp-4) 0;
	border-bottom: 1px solid var(--line);
	font-size: var(--text-lg);
	font-weight: 600;
	letter-spacing: -0.015em;
}
.fin-offer__actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--sp-4) var(--sp-6);
}
.fin-offer__fine {
	margin: 0;
	padding: var(--sp-5) var(--sp-7);
	border-top: 1px solid var(--line);
	font-size: var(--text-sm);
	line-height: 1.6;
	color: var(--ink-muted);
}
.fin-offer__fine a {
	color: inherit;
	text-decoration: underline;
	text-decoration-color: var(--line-strong);
	text-underline-offset: 0.15em;
}
.fin-offer__fine a:hover { color: var(--blue); text-decoration-color: currentColor; }

@media (max-width: 62rem) {
	.fin-offer { grid-template-columns: 1fr; }
	.fin-offer__brand { padding: var(--sp-4) var(--sp-6); }
	.fin-offer__logo { height: 1.5rem; }
	.fin-offer__body { padding: var(--sp-6); }
	.fin-offer__media { display: none; }
	.fin-offer__fine { padding: var(--sp-5) var(--sp-6); }
}

/* ----- Credential seals -----
   A four-strong badge family: round stamp, shield, hex plate, spec plate.
   One stroke language (1.5 frame / 0.7 hairline on a 120-unit grid), one
   accent per seal. SVG type is sized in user units, so it scales with the box. */
.badges {
	position: relative;
	z-index: 1;
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	border-block: 1px solid var(--line-dark);
	margin-bottom: clamp(2.5rem, 2rem + 2vw, 4rem);
}
/* Where the seals are the last thing in a section, the section's own padding is the gap and a bottom margin would double it. */
.badges--solo { margin-bottom: 0; }
.badge-cell {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--sp-5);
	padding: var(--sp-7) var(--sp-4);
	text-align: center;
	border-left: 1px solid var(--line-dark);
}
.badge-cell:first-child { border-left: 0; }
.badge-cell p {
	margin: 0;
	max-width: 34ch;
	color: var(--mist);
}

.badge {
	/* Sized off the cell, not the viewport, so mid-size screens don't shrink it */
	width: min(90%, 13.5rem);
	height: auto;
	color: var(--white);
}
.badge__frame { fill: none; stroke: currentColor; stroke-width: 1.5; }
.badge__hair { fill: none; stroke: currentColor; stroke-width: 0.7; opacity: 0.5; }
.badge__tick { fill: currentColor; stroke: none; }
.badge__type { fill: currentColor; font-size: 8px; font-weight: 700; letter-spacing: 1.6px; }
.badge__word { fill: currentColor; font-size: 11px; font-weight: 800; letter-spacing: 0.4px; }
.badge__fine { fill: currentColor; font-size: 6px; font-weight: 700; letter-spacing: 1.4px; opacity: 0.75; }
.badge__num { fill: currentColor; font-size: 22px; font-weight: 800; letter-spacing: -0.6px; }
.badge__accent { color: var(--red); }
.badge__star { color: var(--amber); }

@media (max-width: 62rem) {
	.badges { grid-template-columns: repeat(2, 1fr); }
	.badge-cell:nth-child(odd) { border-left: 0; }
	.badge-cell:nth-child(n + 3) { border-top: 1px solid var(--line-dark); }
}

/* ----- Why choose: hairline rows on dark, each led by a sprite icon ----- */
.features {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: var(--sp-7) var(--sp-8);
	position: relative;
	z-index: 1;
}
/* Icon and heading share one line; the paragraph wraps to a full-width row below */
.feature {
	--feature-icon: calc(var(--text-xl) * 1.05);
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	column-gap: var(--sp-3);
	border-top: 1px solid var(--line-dark);
	padding-top: var(--sp-5);
}
/* The top row tucks under the badge plate, whose own rule already divides it */
.features > :nth-child(-n + 2) { border-top: 0; padding-top: 0; }

@media (max-width: 56rem) {
	.features { grid-template-columns: 1fr; gap: var(--sp-6); }
	/* One column: only the very first row sits against the plate */
	.features > :nth-child(2) { border-top: 1px solid var(--line-dark); padding-top: var(--sp-5); }
}
/* Sized off the heading scale so icon and title grow together */
.feature__icon {
	width: var(--feature-icon);
	height: var(--feature-icon);
	color: var(--red);
}
.feature h3 { font-size: var(--text-xl); margin: 0; }
/* Indent past the icon and the gap so the copy starts at the heading's text edge */
.feature p {
	flex-basis: 100%;
	padding-left: calc(var(--feature-icon) + var(--sp-3));
	font-size: var(--text-base);
	margin: var(--sp-2) 0 0;
}
/* Scope disclaimer under the guarantee claim. It has to be readable, so it
   drops one size step and goes italic rather than dimming toward the navy. */
.feature p.feature__fine {
	font-size: var(--text-sm);
	font-style: italic;
}
/* --blue fails contrast on navy, so an inline link here keeps the surrounding
   text color and is marked by its underline instead. */
.feature a {
	color: inherit;
	text-decoration: underline;
	text-decoration-color: var(--mist);
	text-underline-offset: 0.15em;
}
.feature a:hover {
	color: var(--white);
	text-decoration-color: currentColor;
}

/* ----- About: owner photo (natural color) + overlapping white panel ----- */
.about-grid {
	display: grid;
	grid-template-columns: minmax(0, 6fr) minmax(0, 6fr);
	align-items: center;
}
/* The portrait always fills its column, upscaling past the file's native
   width if it must: the composition needs a photo taller than the panel so
   the panel reads as overlaying it. The 4:5 crop, not the file, sets the
   proportions. */
.about-grid__media figure {
	aspect-ratio: 4 / 5;
	overflow: hidden;
}
.about-grid__media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: var(--about-photo-pos, 50% 50%);
}
/* Standing portrait in the 4:5 well: Y keeps the head and the work,
   not the floor clutter a 50% crop would still show. */
.about-grid__media--upper { --about-photo-pos: 50% 22%; }
.about-panel {
	position: relative;
	z-index: 2;
	background: var(--surface);
	padding: clamp(2rem, 1.5rem + 2vw, 4rem);
	margin-left: calc(-1 * clamp(2rem, 1rem + 3vw, 4.5rem));
	box-shadow: var(--shadow-panel);
}
@media (max-width: 56rem) {
	.about-grid { grid-template-columns: 1fr; }
	.about-panel {
		margin-left: var(--sp-5);
		margin-right: var(--sp-5);
		margin-top: -4rem;
	}
	/* The panel's -4rem overlap sits on top of the caption when stacked */
	.about-grid__media .figcap { display: none; }
}
/* One hairline and a row of mono facts closing a block of copy. .sign-off is
   the same device under a first-person letter, where the facts are who wrote
   it rather than what the panel claims. */
.about-panel__creds,
.sign-off {
	display: flex;
	flex-wrap: wrap;
	gap: var(--sp-2) var(--sp-6);
	margin-top: var(--sp-6);
	padding-top: var(--sp-5);
	border-top: 1px solid var(--line);
}

/* A spec list inside the overlapping panel, used on service pages that reuse
   this construction. The panel's own padding is the outer gap, so the list
   does not add another --sp-6 on top. */
.about-panel .checks { margin-top: var(--sp-5); }
/* Arrow under a checklist. Same clearance as .about-panel; the last
   .checks row already draws the hairline, so the link needs approach
   distance or it sits on that rule. */
.checks + .arrow-link,
.about-panel .arrow-link { margin-top: var(--sp-5); }

/* ----- Photo mosaic: six frames beside a column of copy -----
   Two offset columns on a 6x8 grid of square tracks, so no frame shares a
   horizontal edge with the one across the seam from it. The aspect is fixed
   rather than stretched to the copy beside it: 1fr rows would let the length
   of the text decide every crop, and a tall column turns the two-row frames
   into slivers. Frames hold people, so they stay natural color and take no
   .duotone wash. The gap matches .gallery, where a 1px hairline vanished
   against the photos. */
.mosaic {
	display: grid;
	grid-template-columns: repeat(6, 1fr);
	grid-template-rows: repeat(8, 1fr);
	gap: var(--sp-3);
	aspect-ratio: 3 / 4;
}
.mosaic__cell {
	position: relative;
	overflow: hidden;
	background: var(--navy);
}
.mosaic__cell img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: var(--mosaic-pos, 50% 50%);
}
/* Areas are keyed to source order, because the composition is a fixed six
   frames and a class per cell would only restate the number. The left column
   runs 4-2-2 track rows against the right column's 2-3-3. */
.mosaic__cell:nth-child(1) { grid-area: 1 / 1 / 5 / 4; }
.mosaic__cell:nth-child(2) { grid-area: 1 / 4 / 3 / 7; }
.mosaic__cell:nth-child(3) { grid-area: 3 / 4 / 6 / 7; }
.mosaic__cell:nth-child(4) { grid-area: 5 / 1 / 7 / 4; }
.mosaic__cell:nth-child(5) { grid-area: 6 / 4 / 9 / 7; }
.mosaic__cell:nth-child(6) { grid-area: 7 / 1 / 9 / 4; }
/* A standing subject in a square or landscape frame: keep the head and the
   work, not the floor. Same job as .split-photo--upper / --mid. */
.mosaic__cell--upper { --mosaic-pos: 50% 20%; }
.mosaic__cell--mid { --mosaic-pos: 50% 40%; }

/* Stacked under the copy the desktop composition is the wrong shape, and so is
   a plain two-column stack of it: six 4:5 frames run three tall rows, which is
   most of a screen of photographs arriving after a long letter. The frames
   recompose into three visual columns on the same six tracks instead, the
   portrait at double scale with five thumbnails around it, which holds all six
   in roughly half the scroll. A 1/1 container is what makes those visual cells
   square: three row tracks plus their two gaps come out to exactly the
   container width. Keep the aspect and the row count together if either moves. */
@media (max-width: 56rem) {
	.mosaic {
		grid-template-rows: repeat(3, 1fr);
		aspect-ratio: 1 / 1;
		gap: var(--sp-2);
	}
	.mosaic .mosaic__cell:nth-child(1) { grid-area: 1 / 1 / 3 / 5; }
	.mosaic .mosaic__cell:nth-child(2) { grid-area: 1 / 5 / 2 / 7; }
	.mosaic .mosaic__cell:nth-child(3) { grid-area: 2 / 5 / 3 / 7; }
	.mosaic .mosaic__cell:nth-child(4) { grid-area: 3 / 1 / 4 / 3; }
	.mosaic .mosaic__cell:nth-child(5) { grid-area: 3 / 3 / 4 / 5; }
	.mosaic .mosaic__cell:nth-child(6) { grid-area: 3 / 5 / 4 / 7; }
}

/* ----- Reviews: heading, one rating strip, then a snap row of plates -----
   There are no photographs of clients to place here, so the section's visual
   interest comes from structure and metadata rather than portraits: per-review
   stars, a sprite avatar in place of a face, and hairline plates. The quotes
   sit in one horizontal row so the section is one plate tall; a stacked wall
   of the same six ran longer than the trust content above it. The heading is
   the only display-scale element in the section. */

/* Rating strip. The score was set at display size beside the heading and the
   two read as competing headlines, so it moved into one hairline row below it:
   the heading leads, and the numeral, stars, count, and link sit together as
   the single line of evidence under it. Full width, so it also ties the head
   of the section to the rail of plates beneath. */
.rating-strip {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	align-items: center;
	gap: var(--sp-4) var(--sp-6);
	padding-block: var(--sp-5);
	margin-bottom: clamp(2rem, 1.5rem + 1.5vw, 3rem);
	border-block: 1px solid var(--line-dark);
	position: relative;
	z-index: 1;
}
.rating-strip__score {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--sp-3) var(--sp-4);
}
.rating-strip__mark {
	display: flex;
	align-items: center;
	gap: var(--sp-3) var(--sp-4);
	flex-wrap: nowrap;
}
.rating-strip strong {
	font-size: var(--text-2xl);
	font-weight: 800;
	line-height: 1;
	letter-spacing: -0.03em;
	color: var(--white);
}
/* Sized up from the 1rem default to hold their own beside the numeral */
.rating-strip .stars .icon { width: 1.15rem; height: 1.15rem; }
.rating-strip .microtext { color: var(--mist); }
/* The reviews page seats the estimate/call pair in this row so the
   hairlines frame the score and the actions together. The count lives
   in the score cluster, not beside the buttons: wrapping it with the
   actions left a hole beside the numeral. Homepage and contact keep
   the Read-all-reviews link as the second child. */
.rating-strip__end {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: flex-end;
	gap: var(--sp-4) var(--sp-6);
	margin-left: auto;
}
.rating-strip .head-actions { margin-top: 0; }
.rating-strip--light .rating-strip__score {
	flex: 1 1 14rem;
	min-width: 0;
	justify-content: space-between;
}
.rating-strip--light .microtext {
	white-space: nowrap;
	text-align: right;
	color: var(--ink-muted);
}
.rating-strip--light .rating-strip__end {
	flex: 0 0 auto;
	flex-wrap: nowrap;
	max-width: 100%;
}
.rating-strip--light .head-actions { flex-wrap: nowrap; }
@media (max-width: 35.98rem) {
	.rating-strip--light .microtext { white-space: normal; }
	.rating-strip--light .rating-strip__end,
	.rating-strip--light .head-actions { flex-wrap: wrap; }
}
/* Ink, --line, muted microtext. Margin drops out: the next section
   already pays its own top pad. Does not replace the default, which is
   for dark sections. */
.rating-strip--light {
	margin-bottom: 0;
	border-block-color: var(--line);
}
.rating-strip--light strong { color: var(--ink); }
/* Mid-page service CTA: primary left, linked Google score right. Score
   stays a compact cluster (not the flex-grow left rail on /reviews/).
   No hairlines and no strip pad: the section’s --tight pad is the approach,
   same job as .head-actions--flush on resource guides. */
.rating-strip--cta {
	margin-bottom: 0;
	padding-block: 0;
	border-block: none;
}
.rating-strip--cta > .btn { flex: 0 0 auto; }
.rating-strip--cta > a.rating-strip__score {
	flex: 0 1 auto;
	margin-left: auto;
	justify-content: flex-end;
	text-decoration: none;
	color: inherit;
}
.rating-strip--cta > a.rating-strip__score:hover .microtext {
	color: var(--blue);
}

/* Snap rail. --reviews-seen is how many whole plates fit in the track. A
   fractional peek was tried and read as a clipped card rather than a hint
   that the row continues; the pager is that signal instead. The 1 / 2 / 3
   steps match the widths where the old quote wall dropped from three
   columns to two to one, so the rail and the rest of the page break
   together. */
.reviews-rail {
	--review-gap: clamp(1rem, 0.75rem + 1vw, 1.75rem);
	--reviews-seen: 1;
	position: relative;
	z-index: 1;
}
@media (min-width: 48rem) {
	.reviews-rail { --reviews-seen: 2; }
}
@media (min-width: 62rem) {
	.reviews-rail { --reviews-seen: 3; }
}
.reviews-rail__track {
	display: flex;
	align-items: stretch;
	gap: var(--review-gap);
	overflow-x: auto;
	overflow-y: hidden;
	overscroll-behavior-x: contain;
	scroll-snap-type: x mandatory;
	-webkit-overflow-scrolling: touch;
}
/* The scrollbar is hidden only after main.js has wired the buttons, so a
   visitor without script still has a way to move the track. */
.reviews-rail.is-ready .reviews-rail__track {
	scrollbar-width: none;
	-ms-overflow-style: none;
}
.reviews-rail.is-ready .reviews-rail__track::-webkit-scrollbar { display: none; }

.quote-cell {
	flex: 0 0 calc((100% - var(--review-gap) * (var(--reviews-seen) - 1)) / var(--reviews-seen));
	min-width: 0;
	scroll-snap-align: start;
	display: flex;
}
/* Each review is a closed plate: one hairline the whole way around, at the
   weight the rest of the page divides with. Corners stay square. Every plate
   is in the flex row, including the ones off-screen, so the rail's height is
   the tallest quote and does not jump as the track scrolls. Short quotes pin
   their attribution to the bottom of the plate so the leftover space sits
   between the words and the name rather than looking like a clipped card. */
.quote {
	display: flex;
	flex-direction: column;
	flex: 1;
	min-width: 0;
	padding: var(--sp-6);
	background-color: rgba(255, 255, 255, 0.05);
	border: 1px solid var(--line-dark);
}

.quote > .stars { display: flex; margin-bottom: var(--sp-5); }
.quote blockquote p {
	font-size: var(--text-lg);
	line-height: 1.5;
	letter-spacing: -0.01em;
	font-weight: 500;
	color: var(--white);
}
.quote blockquote p::before { content: "\201C"; }
.quote blockquote p::after { content: "\201D"; }

.quote figcaption {
	display: flex;
	align-items: center;
	gap: var(--sp-4);
	margin-top: auto;
	padding-top: var(--sp-5);
}
/* Letter disc in place of a Google profile photo. Round so it reads as a
   face mark, not a card. The hairline lives on the name so the disc can
   be a closed circle. Eight mixes, all from brand tokens, so a row of
   quotes does not share one silhouette. --N is keyed to the reviewer. */
.quote__avatar {
	display: inline-flex;
	flex: none;
	align-items: center;
	justify-content: center;
	width: 1.75rem;
	height: 1.75rem;
	border-radius: 50%;
	font-size: var(--text-xs);
	font-weight: 700;
	line-height: 1;
	letter-spacing: 0;
	color: var(--white);
	background: var(--blue);
}
.quote__avatar--1 { background: var(--blue); }
.quote__avatar--2 { background: var(--red); }
.quote__avatar--3 { background: color-mix(in srgb, var(--blue) 52%, var(--red)); }
.quote__avatar--4 { background: color-mix(in srgb, var(--blue) 58%, var(--amber)); }
.quote__avatar--5 { background: color-mix(in srgb, var(--red) 58%, var(--amber)); }
.quote__avatar--6 { background: color-mix(in srgb, var(--blue) 62%, var(--navy)); }
.quote__avatar--7 { background: color-mix(in srgb, var(--red) 62%, var(--navy)); }
.quote__avatar--8 { background: color-mix(in srgb, var(--ink-muted) 40%, var(--blue)); }
.quote__who {
	display: flex;
	flex-direction: column;
	padding-left: var(--sp-4);
	border-left: 1px solid var(--line-dark);
}
.quote__who b {
	font-size: var(--text-sm);
	font-weight: 600;
	letter-spacing: 0;
	color: var(--white);
}
.quote__who .microtext { color: var(--mist); }

/* Two square hairline buttons sharing one frame. Separate ghost buttons
   read as a pair of chips; one split box reads as a pager. */
.reviews-rail__nav {
	display: none;
	margin-top: var(--sp-5);
	border: 1px solid var(--line-dark);
}
.reviews-rail.is-ready .reviews-rail__nav { display: inline-flex; }
.reviews-rail__btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	padding: 0;
	border: 0;
	background: transparent;
	color: var(--white);
	cursor: pointer;
}
.reviews-rail__btn + .reviews-rail__btn { border-left: 1px solid var(--line-dark); }
.reviews-rail__btn .icon { width: 1.15rem; height: 1.15rem; }
.reviews-rail__btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.05); }
.reviews-rail__btn:disabled {
	opacity: 0.28;
	cursor: default;
}
.reviews-rail__btn:focus-visible { outline-offset: -2px; }

/* The plates narrow before a second one fits, and the box padding eats into
   the measure, so the quote type steps down one notch first. */
@media (max-width: 72rem) {
	.quote blockquote p { font-size: var(--text-base); }
}
@media (max-width: 48rem) {
	.quote { padding: var(--sp-5); }
}

/* ----- Reviews page: masonry wall of light plates -----
   CSS columns and grid-template-rows: masonry both leave leftover height
   in the shorter columns and insert it between plates (the uneven gap).
   main.js spans each plate over 1px tracks and bakes --quote-gap into the
   span so the gutter is the same in every column. Hairline plates, no
   shadow. Attribution stays in normal flow: auto-margin on figcaption
   only fires when a flex parent is stretched, which this wall does not. */
.quote-wall {
	--quote-gap: clamp(1rem, 0.75rem + 1vw, 1.75rem);
	display: flex;
	flex-direction: column;
	gap: var(--quote-gap);
}
.quote-wall > .quote {
	width: 100%;
	flex: none;
	height: auto;
	margin: 0;
	background-color: color-mix(in srgb, var(--bg) 65%, var(--surface));
	border-color: var(--line);
}
.quote-wall.is-packed {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	grid-auto-flow: dense;
	grid-auto-rows: 1px;
	align-items: start;
	align-content: start;
	gap: 0 var(--quote-gap);
}
@media (max-width: 61.98rem) {
	.quote-wall.is-packed { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 47.98rem) {
	.quote-wall.is-packed { grid-template-columns: minmax(0, 1fr); }
}
.quote-wall .quote blockquote p { color: var(--ink); }
.quote-wall .quote__who { border-left-color: var(--line); }
.quote-wall .quote__who b { color: var(--ink); }
.quote-wall .quote__who .microtext { color: var(--ink-muted); }
/* The wash stops short of the line box so it reads as a marker stroke
   rather than a filled chip. clone keeps the stroke on every wrapped line. */
.quote-wall mark {
	background-color: transparent;
	background-image: linear-gradient(
		transparent 0.12em,
		var(--mark) 0.12em,
		var(--mark) calc(100% - 0.08em),
		transparent calc(100% - 0.08em)
	);
	color: inherit;
	padding-inline: 0.18em;
	box-decoration-break: clone;
	-webkit-box-decoration-break: clone;
}

/* ----- Link list: hairline names with a CSS arrow.
   Default two columns is the map sidebar (ten towns, smaller type).
   --trio is twelve names on /service-areas/. Overflow children under
   a trio catalog are .link-grid--continue, not a third list. ----- */
.link-list {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: 0 var(--sp-6);
	padding: 0;
	margin: var(--sp-6) 0;
	list-style: none;
}
.link-list a {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: var(--sp-3);
	min-width: 0;
	padding: 0.6rem 0;
	border-bottom: 1px solid var(--line);
	color: var(--ink);
	font-size: var(--text-sm);
	font-weight: 600;
	letter-spacing: 0.02em;
}
.link-list a::after {
	content: "\2192";
	flex: none;
	color: var(--line-strong);
	transition: color 0.15s, transform 0.15s var(--ease-out);
}
.link-list a:hover { color: var(--blue); }
.link-list a:hover::after { color: var(--blue); transform: translateX(3px); }
/* Three columns on the service-area hub, where the list is the section
   rather than a sidebar beside the map. Twelve items fill four rows.
   Two columns at the same 48rem step as .link-grid--trio so the last
   row stays even. */
.link-list--trio {
	grid-template-columns: repeat(3, 1fr);
	margin: 0;
}
.link-list--trio a { font-size: var(--text-base); }
@media (max-width: 48rem) {
	.link-list--trio { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 36rem) {
	.link-list--trio { grid-template-columns: 1fr; }
}

/* Service-area map — Google Maps fills this shell.
   Full-bleed left: the panel runs off the page edge while the copy beside it
   stays on the container spine. */
/* The split centers its items; the map fills the row. Padding on this
   column is the overhang, so the map reads ~100px past the type on each end. */
.map-copy { padding-block: 0 5.5rem; }
/* Where the section is flush at the top the panel meets the color above it, so
   the copy supplies its own top inset too and the map overhangs the type at
   both ends rather than only below. */
.section--flush-top .map-copy { padding-block: 5.5rem; }
.map-panel {
	position: relative;
	align-self: stretch;
	min-height: 30rem;
	margin-left: calc(-1 * var(--gutter));
	/* Matches the Maps basemap ground color while tiles load */
	background: #2a2e35;
	overflow: hidden;
}
.map-panel,
.map-panel > div {
	height: 100%;
	min-height: inherit;
}
/* Stacked: nothing sits beside it, so it bleeds to both edges */
@media (max-width: 56rem) {
	/* Nothing sits beside the panel now, so the copy's overhang inset goes,
	   including the symmetric one a flush-top section gave it. */
	.map-copy,
	.section--flush-top .map-copy { padding-block: 0; }
	.map-panel { margin-inline: calc(-1 * var(--gutter)); }
	/* The town list is the bottom edge once the map stacks above the copy.
	   :has(.map-copy) is what scopes this to a map section, so a catalog that
	   is also flush-bottom does not grow a second pad above the next gray
	   chapter. The top edge needs nothing: stacked, the panel is still the
	   first thing in the section and is still what meets the color above. */
	.section--flush-bottom:has(.map-copy) { padding-bottom: var(--section-pad-tight); }
}

/* Full-bleed map on the service-area hub. Reset the base negative gutter so
   a full-width panel does not shift off the left edge. Height after the
   grouped height:100% rule so that percentage does not win. */
.map-panel--hero {
	align-self: auto;
	margin-left: 0;
	width: 100%;
	height: min(70vh, 48rem);
	min-height: 28rem;
}
@media (max-width: 56rem) {
	.map-panel--hero { margin-inline: 0; }
}

/* ----- CTA band ----- */
.cta-band .container {
	display: grid;
	grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
	gap: clamp(2.5rem, 2rem + 3vw, 5rem);
	align-items: center;
}
@media (max-width: 56rem) { .cta-band .container { grid-template-columns: 1fr; } }

.cta-band h2 { font-size: var(--text-3xl); max-width: 18ch; }
.cta-band .lead { margin: 0; }
.cta-band__actions {
	display: flex;
	flex-direction: column;
	gap: var(--sp-4);
	align-items: stretch;
}
.cta-band__actions .btn { justify-content: space-between; }
.cta-band__sub {
	display: flex;
	justify-content: space-between;
	gap: var(--sp-4);
	margin-top: var(--sp-2);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--mist);
}
.cta-band__sub a { color: var(--white); }
/* Seals under the estimate copy in the same navy field. Span the two
   CTA columns so the hairline is the full measure. */
.cta-band--seals .badges { grid-column: 1 / -1; }

/* ----- Service pages -----
   The kit the interior service pages are built from. Everything here is
   written for reuse across the whole services tree rather than for the page
   that happened to need it first, because the alternative is forty pages each
   inventing their own grid. -------------------------------------------------- */

/* Button row under an interior page title. The head section is type only
   above it, so the row sets its own approach distance rather than inheriting
   the gap that follows a paragraph. */
.head-actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--sp-4);
	margin-top: var(--sp-6);
}
/* Sole child of a section (a mid-page service CTA strip): the section pad
   is the approach, so the title clearance would double the top space. */
.head-actions--flush { margin-top: 0; }
/* A single control under a centered block, not the page-head pair. */
.head-actions--center { justify-content: center; }

/* Checklist inside a column of copy. Divided by hairlines rather than marked
   with bullets, so a list of what a job includes reads as a spec sheet and not
   as prose with dots in front of it. The tick is the sprite check in the
   accent, which is the same job the red tick does on .eyebrow. */
.checks {
	margin: var(--sp-6) 0 0;
	padding: 0;
	list-style: none;
	border-top: 1px solid var(--line);
}
.split > .checks { margin-top: 0; }
.checks li {
	display: flex;
	align-items: flex-start;
	gap: var(--sp-3);
	padding: var(--sp-3) 0;
	border-bottom: 1px solid var(--line);
}
.checks .icon {
	flex: none;
	/* Centers the tick on the cap height of the first line, not on its line box */
	margin-top: 0.2em;
	color: var(--red);
}
.section--dark .checks,
.section--dark .checks li { border-color: var(--line-dark); }
/* Footnote under a spec list. The last .checks row already draws the
   hairline; body `p` has no top margin, so without this the next
   paragraph sits on that rule at body size and reads as more of the
   list. Gap plus a size step and muted ink make it the limit on the
   list. No second border: the list already supplied the rule. */
.fine {
	margin-top: var(--sp-5);
	padding-top: var(--sp-4);
	font-size: var(--text-sm);
	line-height: 1.6;
	color: var(--ink-muted);
}

/* Education limit under a guide catalog. Not a footnote on a checklist
   (.fine): copy left, ghost CTA right, hairline above. Stacks below
   36rem so the button is not squeezed beside the scope line. */
.edu-note {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sp-5) var(--sp-7);
	margin-top: var(--sp-6);
	padding-top: var(--sp-5);
	border-top: 1px solid var(--line);
}
@media (max-width: 36rem) {
	.edu-note {
		flex-direction: column;
		align-items: flex-start;
	}
}
.edu-note__copy { min-width: 0; flex: 1; }
.edu-note__lead {
	margin: 0 0 var(--sp-2);
	font-size: var(--text-base);
	font-weight: 600;
	line-height: 1.4;
	color: var(--ink);
}
.edu-note__lead + p {
	margin: 0;
	font-size: var(--text-sm);
	line-height: 1.6;
	color: var(--ink-muted);
}
.edu-note__copy a { text-decoration: underline; text-underline-offset: 0.15em; }
.edu-note > .btn { flex-shrink: 0; }

/* Editorial body on a resource guide. Continuous H3 chapters under the
   page H1 (no section H2: a second poster heading under the hero read as
   a double title). Not hairline rows and not a .checks / .feature service
   pattern. Sits in a .split beside .split-photo--desktop so the right
   column carries the job photo on desktop and drops it on small screens. */
.guide-body > p:first-child { margin-top: 0; }
.guide-body h3 {
	margin: var(--sp-6) 0 var(--sp-3);
	font-size: var(--text-xl);
}
.guide-body > p:first-child + h3 { margin-top: var(--sp-6); }
.guide-body .fine {
	margin-top: var(--sp-6);
	padding-top: var(--sp-5);
	border-top: 1px solid var(--line);
}

/* ----- Link grid — the sitewide index of other pages -----
   Three jobs, one component: a parent page's index of its own child services,
   a cross-link strip on a child page, and a related-guides strip on the
   resource pages. Cells are divided by a top hairline and the grid gap, the
   way .feature rows are, rather than by a box around each cell: a bordered
   cell leaves a visible hole wherever the item count does not fill the last
   row. auto-fit is the default. Even three- and six-item catalogs lock
   to three columns with .link-grid--trio, because auto-fit at mid widths
   can still leave a hanging cell. Four-item catalogs lock to two columns
   with .link-grid--pair so they cannot hang at three-across or collapse
   into one skinny row on a wide screen. .link-grid--cards is the plane
   form of a child-service catalog: it drops the hairline and paints a
   wash so the cells read as targets. Compact related strips stay hairline. */
.link-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
	gap: var(--sp-6) var(--sp-7);
}
.link-grid__item {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--sp-3);
	padding-top: var(--sp-5);
	border-top: 1px solid var(--line);
	color: inherit;
}
.link-grid__item h3 {
	margin: 0;
	font-size: var(--text-xl);
	transition: color 0.18s;
}
.link-grid__item p {
	margin: 0;
	font-size: var(--text-base);
	color: var(--ink-muted);
}
.link-grid__icon {
	width: calc(var(--text-xl) * 1.05);
	height: calc(var(--text-xl) * 1.05);
	color: var(--red);
}
/* Pushed to the bottom of the cell so the arrows line up across a row whose
   descriptions run to different lengths. */
.link-grid__go {
	display: inline-flex;
	align-items: center;
	gap: var(--sp-2);
	margin-top: auto;
	padding-top: var(--sp-2);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: var(--tracking-caps);
	text-transform: uppercase;
	color: var(--blue);
}
.link-grid__go .icon { width: 1em; height: 1em; }
@media (prefers-reduced-motion: no-preference) {
	.link-grid__go .icon { transition: transform 0.2s var(--ease-out); }
}
.link-grid__item:hover h3,
.link-grid__item:focus-visible h3 { color: var(--blue); }
.link-grid__item:hover .link-grid__go .icon,
.link-grid__item:focus-visible .link-grid__go .icon { transform: translateX(4px); }

.section--dark .link-grid__item { border-top-color: var(--line-dark); }
.section--dark .link-grid__item p { color: var(--mist); }

/* Cross-link strip: one row per link, title and arrow on one line.
   Descriptions stay out of the markup. Icons are optional: related-guides
   strips omit them; topic catalogs on /resources/ keep them for scanning. */
.link-grid--compact .link-grid__item {
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
	gap: var(--sp-4);
}
.link-grid--compact .link-grid__icon {
	flex-shrink: 0;
	width: calc(var(--text-lg) * 1.05);
	height: calc(var(--text-lg) * 1.05);
}
.link-grid--compact .link-grid__item h3 {
	flex: 1;
	min-width: 0;
	font-size: var(--text-lg);
}
.link-grid--compact .link-grid__go { margin-top: 0; padding-top: 0; flex-shrink: 0; }

/* Overflow under a trio catalog. Compact rows, three columns. Three
   names fill one even row. Do not also add .link-grid--trio: that
   class drops to two columns at 48rem, and three names hang. This
   strip stays three columns until 36rem. */
.link-grid--continue {
	grid-template-columns: repeat(3, 1fr);
	gap: 0 var(--sp-7);
	margin-top: var(--sp-6);
}
.link-grid--continue .link-grid__item {
	min-width: 0;
	padding: var(--sp-4) 0;
}
.link-grid--continue .link-grid__item h3 { font-size: var(--text-base); min-width: 0; }
@media (max-width: 36rem) {
	.link-grid--continue { grid-template-columns: 1fr; }
}

/* Four-item catalogs. Two columns at every width until they stack, so a
   hanging third-row cell and a four-across strip are both off the table. */
.link-grid--pair {
	grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 36rem) {
	.link-grid--pair { grid-template-columns: 1fr; }
}

/* Even catalogs (three or six) lock their column count so auto-fit cannot
   leave a hanging cell. Odd counts get a real sixth item, or they use the
   compact row form. Two columns at the tablet step keeps six items even. */
.link-grid--trio {
	grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 48rem) {
	.link-grid--trio { grid-template-columns: repeat(2, 1fr); }
	/* Two columns leave a hanging cell when there are exactly three items.
	   Cards make that a leftover plane, so those catalogs stack instead. */
	.link-grid--trio:has(> .link-grid__item:nth-of-type(3):last-of-type) {
		grid-template-columns: 1fr;
	}
}
@media (max-width: 36rem) {
	.link-grid--trio { grid-template-columns: 1fr; }
}

/* A --surface wash over --bg is the resting plane. Full --surface plus a
   --line edge and the lifted bar ramp on hover is the lift. --radius
   stays 2px; a larger corner would read as a product card. The hover
   shadow is an existing token, not a fourth ramp. --continue needs a
   row gap once the hairline is gone, or the two rows sit on each other. */
.link-grid--cards {
	gap: var(--sp-5);
}
.link-grid--cards .link-grid__item {
	padding: var(--sp-5);
	border-top: 0;
	border-radius: var(--radius);
	background: color-mix(in srgb, var(--surface) 65%, transparent);
}
/* A --surface wash on --surface is invisible. Mix --bg in so the plane still
   reads on a white section. */
.section--surface .link-grid--cards .link-grid__item {
	background: color-mix(in srgb, var(--bg) 65%, var(--surface));
}
@media (prefers-reduced-motion: no-preference) {
	.link-grid--cards .link-grid__item {
		transition: background 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out);
	}
}
.link-grid--cards .link-grid__item:hover,
.link-grid--cards .link-grid__item:focus-visible {
	background: var(--surface);
	box-shadow: 0 0 0 1px var(--line), var(--shadow-bar-lifted);
}
.link-grid--cards.link-grid--continue {
	gap: var(--sp-3) var(--sp-5);
}
.link-grid + .link-grid--cards.link-grid--continue {
	margin-top: var(--sp-5);
}
.link-grid--cards.link-grid--continue .link-grid__item {
	align-items: center;
	padding: var(--sp-4) var(--sp-5);
}
.link-grid--cards.link-grid--continue .link-grid__item h3 { font-size: var(--text-base); }
/* Compact pair catalogs: same cell pad as --continue; columns from --pair.
   Leftover count must be even at two columns. Margin only when stacked
   under another grid, so a pair that opens a section does not double
   the sec-head clearance. */
.link-grid--cards.link-grid--compact.link-grid--pair {
	gap: var(--sp-3) var(--sp-5);
}
.link-grid + .link-grid--cards.link-grid--compact.link-grid--pair {
	margin-top: var(--sp-5);
}
.link-grid--cards.link-grid--compact.link-grid--pair .link-grid__item {
	align-items: center;
	padding: var(--sp-4) var(--sp-5);
}
.link-grid + .arrow-link,
.fine + .arrow-link {
	margin-top: var(--sp-5);
}

/* A labelled strip of links sitting under a block of content rather than
   opening a section: an .eyebrow over a .link-grid. The grid carries no outer
   margin of its own, since it also has to be able to open a section, so the
   approach distance lives here. */
.related { margin-top: clamp(2.5rem, 2rem + 2vw, 4rem); }

/* ----- Spec table — two options compared row by row -----
   Hairline rows and a hairline between the two answers. The axis is mono
   microtext; the column heads carry the weight, because the heads are the
   two things being chosen between. The frame is a plane so the table does
   not dissolve into a white section. Below 48rem the axis sits above a
   two-column pair: a 44rem min-width plus overflow-x left the third column
   off-screen behind a scrollbar, which is not a comparison. */
.spec-table {
	width: 100%;
	border-collapse: collapse;
	font-size: var(--text-sm);
	line-height: 1.5;
	text-align: left;
}
.spec-table th,
.spec-table td {
	padding: var(--sp-4) var(--sp-5);
	border-top: 1px solid var(--line);
	vertical-align: top;
}
.spec-table thead th {
	padding-top: var(--sp-5);
	padding-bottom: var(--sp-5);
	border-top: 0;
	border-bottom: 1px solid var(--line-strong);
	background: var(--bg);
	font-size: var(--text-lg);
	font-weight: 800;
	letter-spacing: -0.02em;
	color: var(--ink);
}
.spec-table thead th .microtext {
	display: block;
	margin-bottom: var(--sp-2);
}
/* The axis column. Narrow and mono so the eye runs down it as a list of
   properties rather than reading it as a third answer. */
.spec-table tbody th {
	width: 10rem;
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--ink-muted);
	background: var(--bg);
}
.spec-table td { border-left: 1px solid var(--line); }
.spec-table thead th:not(:first-child) { border-left: 1px solid var(--line); }
.spec-table__scroll {
	border: 1px solid var(--line-strong);
	background: var(--surface);
}
@media (max-width: 48rem) {
	.spec-table,
	.spec-table thead,
	.spec-table tbody {
		display: block;
	}
	.spec-table thead tr,
	.spec-table tbody tr {
		display: grid;
		grid-template-columns: 1fr 1fr;
	}
	.spec-table thead th,
	.spec-table tbody th,
	.spec-table td {
		display: block;
		width: auto;
		padding: var(--sp-4);
	}
	.spec-table thead th:first-child { display: none; }
	.spec-table thead th {
		font-size: var(--text-base);
	}
	.spec-table thead th:nth-child(2),
	.spec-table td:first-of-type {
		border-left: 0;
	}
	.spec-table tbody th {
		grid-column: 1 / -1;
		padding-bottom: var(--sp-2);
	}
}

/* ----- Project gallery -----
   Four photo cells, captions overlaid so the section does not grow a caption row.
   Lives outside .container. Uses the expander's gap because a 1px hairline vanished against the photos. */
.gallery {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: var(--sp-3);
}
.gallery figure {
	position: relative;
	margin: 0;
	aspect-ratio: 4 / 5;
	background: var(--navy);
	overflow: hidden;
}
.gallery img {
	width: 100%;
	height: 100%;
	object-fit: cover;
}
/* Padding-top is the fade height, not type inset: the letters sit in the
   last --sp-4, and the ramp has to start well above them to stay readable
   on a light unit. */
.gallery .figcap {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 1;
	padding: clamp(3.5rem, 2.5rem + 4vw, 5.5rem) var(--sp-5) var(--sp-4);
	background: linear-gradient(to top,
		rgba(var(--scrim-rgb), 0.86) 0%,
		rgba(var(--scrim-rgb), 0.62) 28%,
		rgba(var(--scrim-rgb), 0) 100%);
	color: var(--white);
}
@media (max-width: 56rem) {
	.gallery { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 28rem) {
	.gallery { grid-template-columns: 1fr; }
}

/* ----- FAQ — native details rows, no script -----
   Stacked, the list is capped at a reading measure so a full-container FAQ
   does not turn every question into a poster line. Beside a heading in
   .split--top the column is the measure, so that cap is lifted. */
.faq {
	max-width: 46rem;
	border-top: 1px solid var(--line);
}
.faq details { border-bottom: 1px solid var(--line); }
.faq summary {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sp-5);
	padding: var(--sp-4) 0;
	font-size: var(--text-lg);
	font-weight: 700;
	line-height: 1.3;
	cursor: pointer;
	list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary h3 { margin: 0; font: inherit; letter-spacing: -0.015em; }
.faq summary .icon { flex: none; color: var(--red); }
@media (prefers-reduced-motion: no-preference) {
	.faq summary .icon { transition: transform 0.2s var(--ease-out); }
}
.faq details[open] summary .icon { transform: rotate(180deg); }
.faq__body { padding-bottom: var(--sp-4); }
.faq__body > :first-child { margin-top: 0; }
.faq__body > :last-child { margin-bottom: 0; }

/* ----- Legal documents ----- */
/* A rail of first-party key points, and the framed document beside it. The
   rail holds while the frame scrolls, so the opt-out instruction stays on
   screen no matter how deep into the terms the reader is. */
.legal {
	display: grid;
	grid-template-columns: minmax(0, 19rem) minmax(0, 1fr);
	gap: clamp(2.5rem, 2rem + 3vw, 5rem);
	align-items: start;
}
.legal__key {
	position: sticky;
	top: calc(var(--header-total) + var(--sp-5));
	padding-top: var(--sp-4);
	border-top: 1px solid var(--line-strong);
}
.legal__key .microtext { margin: 0; }
.legal__key ul { list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.legal__key li {
	padding-block: var(--sp-3);
	border-top: 1px solid var(--line);
	font-size: var(--text-sm);
	line-height: 1.5;
}
.legal__key li:first-child { border-top: 0; padding-top: 0; }
.legal__aside {
	margin: var(--sp-5) 0 0;
	padding-top: var(--sp-4);
	border-top: 1px solid var(--line);
	font-size: var(--text-xs);
	line-height: 1.6;
	color: var(--ink-muted);
}

/* The documents held in this repo, beside the rail, in the same grid the
   framed pages use: the thin rail, and everything else to the document.

   The document carries no max-width, and that is the whole point. An earlier
   build capped it at a reading measure inside the 1fr column, which ended the
   text well short of the page on a wide screen and left a hole beside it.
   Widening the rail to absorb that space was tried next and made the rail
   absurd for four short facts. So the document fills the column instead. The
   line runs long at the top of the range, and that is the accepted trade for
   two columns that reach both edges of the page. */

/* The rule across the top belongs to the column, not to whatever leads it. The
   privacy policy opens on a paragraph and the other two open on a heading, and
   only the column is there on all three. Padding matches the rail's, so the
   two columns hang off one line drawn straight across the grid. */
.legal-doc {
	padding-top: var(--sp-4);
	border-top: 1px solid var(--line-strong);
}
/* Whatever leads the document gives up its own top spacing, including the rule
   an h2 would otherwise draw a few pixels under the one above it. */
.legal-doc > :first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* Long prose on a site whose other sections are short bursts, so the heading
   ramp is compressed: h2 is a --text-3xl section poster everywhere else, and
   twenty-one numbered clauses set that way is a wall of posters. Sections are
   divided by the same hairline the rest of the site uses for structure. */
.legal-doc h2 {
	margin: var(--sp-8) 0 var(--sp-4);
	padding-top: var(--sp-5);
	border-top: 1px solid var(--line-strong);
	font-size: var(--text-xl);
	letter-spacing: -0.02em;
}
.legal-doc h3 { margin: var(--sp-6) 0 var(--sp-3); font-size: var(--text-lg); }
/* The fourth level is a run-in label over a single paragraph. It carries
   weight and nothing else, because another size step would land on top of h3. */
.legal-doc h4 {
	margin: var(--sp-5) 0 var(--sp-2);
	font-size: var(--text-base);
	font-weight: 700;
	letter-spacing: 0;
	line-height: 1.4;
}
.legal-doc li { margin-bottom: var(--sp-2); }
.legal-doc li:last-child { margin-bottom: 0; }
.legal-doc code {
	font-family: var(--font-mono);
	font-size: 0.92em;
	letter-spacing: 0.02em;
	color: var(--ink);
}

/* The state-law categories grid in the privacy policy is the only table in the
   set. Hairline rows, no fill and no vertical rules, so it reads as the same
   grid the rest of the site builds out of borders. */
.legal-doc table {
	width: 100%;
	margin: 0 0 var(--sp-5);
	border-collapse: collapse;
	font-size: var(--text-sm);
	line-height: 1.5;
	text-align: left;
}
.legal-doc th,
.legal-doc td {
	padding: var(--sp-3) var(--sp-4) var(--sp-3) 0;
	border-top: 1px solid var(--line);
	vertical-align: top;
}
.legal-doc th {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--ink-muted);
}
.legal-doc tr:first-child th { border-top: 0; padding-top: 0; }
/* The last column is a one-word verdict on the row. Pinned narrow at the right
   rather than left to size itself against two columns of prose. */
.legal-doc th:last-child,
.legal-doc td:last-child { width: 4.5rem; padding-right: 0; }
.legal-doc td:last-child { font-family: var(--font-mono); font-size: var(--text-xs); }
/* Three columns of prose will not compress to a phone, so the table scrolls
   sideways in its own box instead of stretching the page under it. */
.legal-doc__scroll { margin: 0 0 var(--sp-5); overflow-x: auto; }
.legal-doc__scroll table { min-width: 34rem; margin: 0; }

/* Cross-origin frame sends no height, so it scrolls inside a viewport-relative box. */
.legal__embed {
	height: min(85vh, 55rem);
	background: var(--surface);
	border: 1px solid var(--line-strong);
}
.legal__embed iframe {
	display: block;
	width: 100%;
	height: 100%;
	border: 0;
}
/* Form frame is tall enough to show the submit button. Cross-origin, so the height is fixed rather than measured. */
.legal__embed--form { height: 66rem; }

@media (max-width: 62rem) {
	.legal { grid-template-columns: 1fr; }
	/* Unpinned once it sits above the frame, where following the scroll would
	   park the rail over the document instead of beside it. */
	.legal__key { position: static; }
	/* A nested scroll region inside a short viewport is hard to escape by
	   touch, so the frame gives back enough page around it to scroll past. */
	.legal__embed { height: min(70vh, 40rem); }
	/* The form is the exception: narrower means its labels and fields stack
	   taller, so it needs more room here, not less. */
	.legal__embed--form { height: 76rem; }
}

/* ----- Contact: estimate form + NAP rail -----
   Opening is .section--head with a hanging portrait; this grid is the
   form and facts on --surface. Gutter is padding on the form and the
   rail, not a grid gap, so the hairline sits in that inset. The hanging
   portrait uses the same inset so its left edge matches the facts. The
   rail is shorter than the form, so it does not stick. */
.contact-layout,
.split-photo--contact {
	--contact-rail-pad: clamp(2rem, 1.5rem + 3vw, 4.5rem);
}
.contact-layout {
	display: grid;
	grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
	align-items: start;
}
.estimate-form {
	padding-right: var(--contact-rail-pad);
}
.contact-aside {
	/* Clears the hanging portrait's --shadow-panel (32px step) so the
	   first fact does not sit in the shadow. The hang still covers the
	   section pad; this is extra space below the photo. */
	padding-top: var(--sp-7);
	padding-left: var(--contact-rail-pad);
	border-left: 1px solid var(--line);
	container-type: inline-size;
}
/* Absolute img ignores padding on the figure; margin moves the well. */
.split-photo--contact {
	margin-left: var(--contact-rail-pad);
}
/* Opening is tight-bottom into a tight form section, not a full pad. */
.split-photo--contact img {
	height: calc(100% + var(--section-pad-tight) + var(--section-pad-tight));
}
/* The opening split’s gap would sit this figure left of the padded
   facts. Drop it only while the two-column rail is in play; stacked
   layouts still need the split’s row gap. */
@media (min-width: 62rem) {
	.split:has(.split-photo--contact) { gap: 0; }
}
@media (max-width: 62rem) {
	.contact-layout {
		--contact-rail-pad: 0;
		grid-template-columns: 1fr;
	}
	.estimate-form { padding-bottom: var(--sp-7); }
	.contact-aside {
		padding-top: var(--sp-7);
		border-left: 0;
		border-top: 1px solid var(--line);
	}
	/* Hidden, not stacked: a one-column split with display:none still
	   keeps the 7/5 tracks, so the copy would sit in 7/12. */
	.split:has(.split-photo--contact),
	.split--wide-left:has(.split-photo--contact) {
		grid-template-columns: 1fr;
	}
	.split-photo--contact { display: none; }
}

/* Sentence case and a hairline so this is not read as the first field label. */
.estimate-form__req {
	margin: 0 0 var(--sp-5);
	padding-bottom: var(--sp-3);
	border-bottom: 1px solid var(--line);
	font-family: var(--font);
	font-size: var(--text-sm);
	letter-spacing: normal;
	text-transform: none;
	color: var(--red-hot);
}
.estimate-form__row {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--sp-5);
}
@media (max-width: 36rem) {
	.estimate-form__row { grid-template-columns: 1fr; }
}
.estimate-form__field { margin: 0 0 var(--sp-5); }
.estimate-form fieldset.estimate-form__field {
	margin: 0 0 var(--sp-5);
	padding: 0;
	border: 0;
	min-width: 0;
}
.estimate-form legend.estimate-form__label { float: none; width: 100%; }
.estimate-form__label {
	display: block;
	margin: 0 0 var(--sp-2);
	padding: 0;
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: var(--tracking-caps);
	text-transform: uppercase;
	color: var(--ink-muted);
}
.estimate-form input[type="text"],
.estimate-form input[type="email"],
.estimate-form input[type="tel"],
.estimate-form textarea {
	display: block;
	width: 100%;
	margin: 0;
	padding: 0.85rem 1rem;
	border: 1px solid var(--line);
	border-radius: var(--radius);
	background: var(--bg);
	color: var(--ink);
	font: 500 var(--text-base) / 1.4 var(--font);
	appearance: none;
}
.estimate-form textarea {
	min-height: 9rem;
	resize: vertical;
}
.estimate-form input::placeholder,
.estimate-form textarea::placeholder { color: var(--ink-muted); }
.estimate-form input:focus-visible,
.estimate-form textarea:focus-visible { border-color: var(--blue); }

/* Three equal cells sharing one hairline frame. Not chips: the selected
   state is a --bg wash, not a fill or a pill. */
.estimate-form__choice-row {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	border: 1px solid var(--line);
	border-radius: var(--radius);
	overflow: hidden;
}
.estimate-form__choice {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--sp-2);
	margin: 0;
	padding: 0.85rem var(--sp-3);
	cursor: pointer;
	font-weight: 600;
	font-size: var(--text-sm);
	letter-spacing: 0.02em;
}
.estimate-form__choice + .estimate-form__choice { border-left: 1px solid var(--line); }
.estimate-form__choice:has(:checked) { background: var(--bg); }
.estimate-form__choice input {
	flex: none;
	accent-color: var(--blue);
}
@media (max-width: 28rem) {
	.estimate-form__choice-row { grid-template-columns: 1fr; }
	.estimate-form__choice + .estimate-form__choice {
		border-left: 0;
		border-top: 1px solid var(--line);
	}
}

.estimate-form__note {
	margin: 0 0 var(--sp-5);
	font-size: var(--text-sm);
	color: var(--ink-muted);
}
.estimate-form__agree {
	display: flex;
	align-items: flex-start;
	gap: var(--sp-3);
	margin: 0 0 var(--sp-5);
	padding: var(--sp-5) 0;
	border-top: 1px solid var(--line);
	cursor: pointer;
	font-size: var(--text-sm);
}
.estimate-form__agree input {
	flex: none;
	width: 1.15rem;
	height: 1.15rem;
	margin-top: 0.2em;
	accent-color: var(--blue);
}
.estimate-form__actions {
	margin: 0;
	padding-top: var(--sp-5);
}
.estimate-form__actions .btn { width: 100%; justify-content: space-between; }
.estimate-form__status {
	margin: var(--sp-5) 0 0;
	padding: var(--sp-4) var(--sp-5);
	border: 1px solid var(--line);
	background: var(--bg);
	font-size: var(--text-sm);
	color: var(--ink-muted);
}
/* Revealed by fragment when the form posts without a script. An author
   display beats the UA rule for hidden. */
.estimate-form__status:target { display: block; }
.estimate-form__status--sent {
	border-color: var(--blue);
	color: var(--ink);
}
.estimate-form .btn:disabled {
	opacity: 0.6;
	cursor: default;
}

.contact-facts {
	list-style: none;
	margin: 0;
	padding: 0;
}
.contact-facts li {
	margin: 0;
	padding-block: var(--sp-4);
	border-top: 1px solid var(--line);
}
.contact-facts li:first-child { padding-top: 0; border-top: 0; }
.contact-facts li:last-child { border-top: 0; }
.contact-facts__label {
	display: block;
	margin-bottom: var(--sp-2);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--ink-muted);
}
.contact-facts__phone {
	font-size: var(--text-base);
	font-weight: 700;
	color: var(--ink);
}
.contact-facts__phone:hover { color: var(--blue); }
.contact-facts address {
	margin: 0;
	font-style: normal;
	font-weight: 600;
	line-height: 1.45;
}
.contact-facts address a { color: inherit; }
.contact-facts address a:hover { color: var(--blue); }
/* Compact location pin in the NAP address cell. Hairline and --radius
   only; the overlapping-panel shadow is not for this. */
.contact-map {
	width: 100%;
	/* 4/3 of the rail content box, then 3.75rem (~60px) shorter. */
	height: calc(100cqi * 3 / 4 - 3.75rem);
	margin-top: var(--sp-4);
	overflow: hidden;
	border: 1px solid var(--line);
	border-radius: var(--radius);
	/* Matches the Maps basemap ground color while tiles load */
	background: #2a2e35;
}
.contact-map > div { height: 100%; }
.contact-facts strong { display: block; }
.contact-facts__sub {
	display: block;
	margin-top: var(--sp-1);
	font-size: var(--text-sm);
	color: var(--ink-muted);
}

/* --------------------------------------------------------------------------
   9. FOOTER
   -------------------------------------------------------------------------- */
/* The footer sits directly below the dark CTA band, and --navy-deep against
   --navy is too close a step to register as a new surface on its own. */
.site-footer {
	background: var(--navy-deep);
	color: var(--mist);
	border-top: 1px solid var(--line-dark);
	padding-bottom: var(--sp-6);
	font-size: var(--text-sm);
}
.site-footer a { color: var(--mist); }
.site-footer a:hover { color: var(--white); }

/* Columns are separated by hairlines rather than by empty gap, built the same
   way as the hero spec bar: no column gap, each cell carries the spacing as
   padding, and the outer cells go flush so the row shares the page's edges.
   Without the rules the four blocks read as text floating at four different
   heights, since their content lengths have nothing to do with each other.

   This padding sets how far the text sits off each rule, and a divider needs
   clearance on both sides, so the row pays for it six times over: once at each
   inner edge of the two middle cells, once inside each outer cell. That is why
   it is fluid rather than a flat token. The ceiling lets the columns breathe on
   a full-width window; the floor is what the narrow end of the four-column
   range can afford before the cells drop under the labels they carry. */
.site-footer__grid {
	display: grid;
	grid-template-columns: 1.35fr 1fr 1fr 1.05fr;
	padding-block: var(--sp-8) var(--sp-7);
}
.footer-col { padding-inline: clamp(var(--sp-5), 2.4vw, var(--sp-7)); }
.footer-col:first-child { padding-left: 0; }
.footer-col:last-child { padding-right: 0; }
.footer-col + .footer-col { border-left: 1px solid var(--line-dark); }

/* Stacking moves each divider from the cell's left edge to its top edge.
   The four-column row is dropped while the container can still afford the
   rules: under about 76rem a link column holds less than "Residential
   Electrical" on one line, and the logo lockup in the brand cell cannot wrap
   at all. Two wide columns read better than four cramped ones, and the header
   already treats this range as a transition, collapsing to the hamburger at
   80rem. */
@media (max-width: 76rem) {
	.site-footer__grid { grid-template-columns: 1fr 1fr; }
	.footer-col { padding-inline: var(--sp-6); }
	.footer-col:nth-child(odd) { padding-left: 0; border-left: 0; }
	.footer-col:nth-child(even) { padding-right: 0; }
	.footer-col:nth-child(n+3) {
		margin-top: var(--sp-6);
		padding-top: var(--sp-6);
		border-top: 1px solid var(--line-dark);
	}
}
@media (max-width: 36rem) {
	.site-footer__grid { grid-template-columns: 1fr; }
	.footer-col { padding-inline: 0; }
	.footer-col:nth-child(n+2) {
		margin-top: var(--sp-6);
		padding-top: var(--sp-6);
		border-top: 1px solid var(--line-dark);
		border-left: 0;
	}
}

.site-footer h3 {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: var(--tracking-caps);
	text-transform: uppercase;
	color: var(--white);
	margin-bottom: var(--sp-4);
}
.site-footer ul { list-style: none; padding: 0; margin: 0; }
/* Tighter than the body leading and spaced further apart, so a link long
   enough to wrap still reads as one item rather than as two. */
.site-footer li { margin-bottom: var(--sp-3); line-height: 1.4; }
.site-footer strong { color: var(--white); }

.footer-brand .logo { margin-bottom: var(--sp-4); }
/* Light lockup: same 800x311 composition as the header GIF, white type,
   transparent ground, so it sits bare on navy. Height is --logo-h so
   this stays locked to the header. max-width: none because the base
   img clamp would squash a height-sized raster the moment the brand
   column is narrower than the file's aspect. */
.footer-brand .logo__mark {
	height: var(--logo-h);
	max-width: none;
}

/* Veteran-owned strip. The claim and the discount are split onto two lines,
   one mono label over one sans detail, so neither has to wrap mid-phrase. */
.footer-flag {
	display: flex;
	align-items: center;
	gap: var(--sp-4);
	margin-top: var(--sp-6);
	padding-top: var(--sp-5);
	border-top: 1px solid var(--line-dark);
}
.footer-flag img { width: 4rem; opacity: 0.75; }
.footer-flag p {
	display: flex;
	flex-direction: column;
	gap: 0.2rem;
	margin: 0;
	font-size: var(--text-xs);
	line-height: 1.45;
}
.footer-flag b {
	font-family: var(--font-mono);
	font-weight: 600;
	letter-spacing: var(--tracking-caps);
	text-transform: uppercase;
	color: var(--white);
}

/* Contact column is a reference block, not a second call to action: the CTA
   band immediately above already carries the buttons, so repeating them here
   would compete with it. Rows are label-over-value, hairline-divided, which
   also gives the shortest column enough vertical structure to hold its own
   against the two link lists beside it. */
.footer-contact li {
	margin: 0;
	padding-block: var(--sp-4);
	border-top: 1px solid var(--line-dark);
}
.footer-contact li:first-child { padding-top: 0; border-top: 0; }
.footer-contact li:last-child { padding-bottom: 0; }
.footer-contact a { color: var(--white); }
.footer-contact__label {
	display: block;
	margin-bottom: var(--sp-2);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--mist);
}
/* One step over the column's --text-sm is enough to lead the block. Anything
   larger competes with the phone button in the CTA band directly above. */
.footer-contact__phone {
	font-size: var(--text-base);
	font-weight: 700;
}

.footer-meta {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	align-items: center;
	gap: var(--sp-4) var(--sp-6);
	padding-top: var(--sp-5);
	border-top: 1px solid var(--line-dark);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: 0.04em;
}
/* Two groups, so the row has something to space between. The legal set grew to
   a page per document plus the opt-in form, and five mono links no longer fit
   beside the license line at most laptop widths. Grouped this way they take
   the next row whole and start on the page's left edge, because that is where
   space-between leaves a lone item on a line. Three loose spans with
   margin-left:auto on the last one stranded them mid-row instead, and pinned
   them against the license text in the band just before they wrapped. */
.footer-meta__id,
.footer-meta__legal { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5); }
.footer-meta__id { column-gap: var(--sp-6); }

/* --------------------------------------------------------------------------
   9b. PRIVACY NOTICE — floating inset bar (injected by main.js)
   Same geometry family as the header: --bar-inset, --bar-pad, --radius-bar,
   and --shadow-bar-lifted. Built and ready; shown only when NOTICE_ENABLED is
   true in main.js. A short acknowledgment of cookies and similar technologies
   (operation, security, usage measurement), not a preference center.
   -------------------------------------------------------------------------- */
.privacy-notice {
	position: fixed;
	z-index: 200;
	left: var(--bar-inset);
	right: var(--bar-inset);
	bottom: max(var(--header-pad), env(safe-area-inset-bottom, 0px));
}
.privacy-notice__inner {
	display: flex;
	align-items: center;
	gap: var(--sp-5) var(--sp-6);
	padding: var(--sp-4) var(--bar-pad);
	background: rgba(255, 255, 255, 0.94);
	backdrop-filter: blur(16px);
	-webkit-backdrop-filter: blur(16px);
	border: 1px solid var(--line);
	border-radius: var(--radius-bar);
	box-shadow: var(--shadow-bar-lifted);
}
.privacy-notice__text {
	flex: 1 1 auto;
	min-width: 0;
}
.privacy-notice__text .microtext {
	margin: 0 0 var(--sp-2);
	color: var(--ink);
}
.privacy-notice__text p:last-child {
	margin: 0;
	font-size: var(--text-sm);
	line-height: 1.5;
	color: var(--ink-muted);
}
.privacy-notice__text a {
	color: var(--blue);
	text-decoration: underline;
	text-decoration-color: rgba(39, 108, 205, 0.35);
	text-underline-offset: 0.15em;
}
.privacy-notice__text a:hover { text-decoration-color: currentColor; }
/* Compact primary: the bar is a footnote, not a CTA band */
.privacy-notice__ok {
	flex: 0 0 auto;
	padding: 0.8rem 1.4rem;
	white-space: nowrap;
}
@media (max-width: 36rem) {
	.privacy-notice__inner {
		flex-direction: column;
		align-items: stretch;
		gap: var(--sp-4);
	}
	.privacy-notice__ok { width: 100%; }
}
@media (prefers-reduced-motion: no-preference) {
	.privacy-notice {
		opacity: 0;
		transform: translateY(0.75rem);
		transition: opacity 0.35s var(--ease-out), transform 0.35s var(--ease-out);
	}
	.privacy-notice.is-visible {
		opacity: 1;
		transform: none;
	}
	.privacy-notice.is-leaving {
		opacity: 0;
		transform: translateY(0.75rem);
	}
}

/* --------------------------------------------------------------------------
   10. MOTION — scroll reveal (activated by main.js)
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
	.reveal {
		opacity: 0;
		transform: translateY(20px);
		transition: opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
		transition-delay: var(--reveal-delay, 0s);
	}
	.reveal.is-visible { opacity: 1; transform: none; }
	.reveal-d1 { --reveal-delay: 0.08s; }
	.reveal-d2 { --reveal-delay: 0.16s; }
	.reveal-d3 { --reveal-delay: 0.24s; }
}

/* --------------------------------------------------------------------------
   11. UTILITIES
   -------------------------------------------------------------------------- */
.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0 0 0 0);
	white-space: nowrap;
	border: 0;
}
