/* D&D Decking and More: theme styles.
   Locked brand palette (Grey Option One) and IBM Plex Sans per the 2026-07-29 brand guidelines.
   Layers: tokens -> base -> layout -> components. */

@layer tokens, base, components;

@layer tokens {
	:root {
		/* Brand palette, locked hexes from the approved guidelines. Do not drift. */
		--paper: #F4F1EA;
		--fog: #DAD8D1;
		--concrete: #97948B;
		--slate: #474B43;
		--ink: #23241F;
		--evergreen: #2F4A38;

		/* Derived roles */
		--body-ink: #3A3B35;            /* body text on paper, per guidelines */
		--paper-on-dark: rgba(244, 241, 234, 0.82);
		--evergreen-press: #263C2D;

		--sans: 'IBM Plex Sans', 'Segoe UI', system-ui, -apple-system, Helvetica, Arial, sans-serif;

		/* Fluid type scale (320 to 1440). Max stays under 2.5x min for zoom safety. */
		--step--1: clamp(0.8125rem, 0.79rem + 0.12vw, 0.875rem);
		--step-0: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
		--step-1: clamp(1.1875rem, 1.1rem + 0.45vw, 1.5rem);
		--step-2: clamp(1.5rem, 1.35rem + 0.75vw, 2.0625rem);

		/* Space */
		--gutter: clamp(1.25rem, 5vw, 4.5rem);
		--header-h: 76px;
		--space-s: clamp(0.75rem, 1.5vw, 1.25rem);
		--space-m: clamp(1.5rem, 3vw, 2.5rem);
		--space-l: clamp(3rem, 7vw, 6rem);
		--seam: clamp(0.3rem, 0.5vw, 0.55rem);   /* the gap between two boards; every photo run uses it */

		--ease-solid: cubic-bezier(0.2, 0.8, 0.2, 1);
	}
}

@layer base {
	@font-face {
		font-family: 'IBM Plex Sans';
		src: url('../fonts/ibm-plex-sans-latin.woff2') format('woff2');
		font-weight: 100 700;
		font-display: swap;
	}

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

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

	body {
		font-family: var(--sans);
		font-size: var(--step-0);
		line-height: 1.6;
		background: var(--paper);
		color: var(--body-ink);
		-webkit-font-smoothing: antialiased;
	}

	h1, h2, h3 { color: var(--ink); line-height: 1.05; text-wrap: balance; }
	p { text-wrap: pretty; }

	img, svg { max-width: 100%; height: auto; display: block; }

	a { color: inherit; }

	:focus-visible { outline: 3px solid currentColor; outline-offset: 3px; }

	.skip-link {
		position: absolute; left: -999px; top: 0; z-index: 100;
		background: var(--evergreen); color: var(--paper);
		padding: 0.6em 1em; font-weight: 600; text-decoration: none;
	}
	.skip-link:focus-visible { left: 0; }

	@media (prefers-reduced-motion: reduce) {
		*, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
	}
}

@layer components {

	/* ---------- Buttons ---------- */
	.button {
		display: inline-block;
		border: 0;               /* <button> carries a UA border that <a> never had */
		cursor: pointer;
		font-family: inherit;
		background: var(--evergreen);
		color: var(--paper);
		/* Sized up one step at the client's ask (round 1, 2026-08-16). Kept in em so the
		   padding grows with the label rather than drifting out of proportion. The header
		   CTA still clears --header-h (76px) with room, so the sticky header does not move
		   and nothing computed off --header-h shifts. */
		font-size: 1.0625rem;
		font-weight: 600;
		letter-spacing: 0.14em;
		text-transform: uppercase;
		text-decoration: none;
		padding: 1.05em 1.9em;
		/* Hover: a darker plane slides across the face, the site's planes-crossing gesture
		   at button scale. background-size keeps the paint under the label, no extra markup. */
		background-image: linear-gradient(var(--evergreen-press), var(--evergreen-press));
		background-repeat: no-repeat;
		background-size: 0% 100%;
		background-position: left center;
		transition: background-size 0.22s var(--ease-solid);
	}
	.button:hover, .button:focus-visible { background-size: 100% 100%; }
	.button--lg { font-size: 1.15rem; padding: 1.2em 2.2em; }

	/* ---------- Header / nav ---------- */
	.site-header {
		background: var(--paper);
		border-bottom: 1px solid rgba(35, 36, 31, 0.14);
	}
	@media (min-width: 861px) {
		.site-header { position: sticky; top: 0; z-index: 10; }
	}
	.site-header__inner {
		display: flex;
		align-items: center;
		gap: var(--space-m);
		min-height: var(--header-h);
		/* Same rail every page title and the footer sit on. A flat gutter here put the
		   logo ~65px left of the h1 directly beneath it, on every page, for the whole
		   sticky scroll: the one fixed anchor on the site, out of register with itself. */
		padding-inline: max(var(--gutter), (100vw - 76rem) / 2);
		flex-wrap: wrap;
	}
	.site-header__brand { flex-shrink: 0; display: flex; align-items: center; }
	/* The lockup SVG carries 18 units of empty box to the left of the D (the mark's own
	   clear space, baked into the artwork), which at 46px tall is 8.3px. That pushed the
	   mark's visible left edge 8px inside the rail every page title sits on, which is the
	   loose left gutter the client called out. Pulling the brand back by that exact amount
	   lands the D on the rail optically, without moving the rail itself. */
	.site-header__brand { margin-inline-start: -8px; }
	.site-header__brand img { height: 46px; width: auto; }

	/* Right-aligning the header used to be nav's job via margin-inline-start: auto. That only
	   ever aligned the row the nav was on. Between 861px and ~1054px the brand, nav, phone and
	   quote button do not fit on one row, so the actions cluster wraps, and the wrapped line
	   landed hard against the LEFT gutter, divider rule and all. (.site-header__actions carried
	   justify-self: end, which is a grid property and has never done anything in this flex row.)
	   Aligning the CONTAINER to the end and letting the brand's auto margin hold the left edge
	   right-aligns EVERY line, wrapped or not, with no breakpoint to keep in sync. */
	@media (min-width: 861px) {
		.site-header__inner { justify-content: flex-end; }
		.site-header__brand { margin-inline-end: auto; }
	}
	.site-nav__list {
		display: flex;
		gap: clamp(1rem, 2.2vw, 2.2rem);
		list-style: none;
	}
	.site-nav__list a {
		font-size: 1.25rem;
		font-weight: 500;
		color: var(--ink);
		text-decoration: none;
		padding-block: 0.35em;
		border-bottom: 2px solid transparent;
		transition: border-color 0.18s var(--ease-solid);
	}
	.site-nav__list a:hover { border-bottom-color: var(--evergreen); }

	.site-header__actions {
		display: flex;
		align-items: center;
		gap: clamp(0.9rem, 1.8vw, 1.6rem);
	}
	.site-header__actions::before {
		content: "";
		width: 1px;
		height: 26px;
		background: rgba(35, 36, 31, 0.28);
	}
	.site-header__phone {
		/* Below 861px the quote button hides and this becomes the mobile action, so it
		   needs a box, not just its own text height. */
		padding-block: 0.5rem;
		font-weight: 700;
		font-size: 1.25rem;
		color: var(--evergreen);
		text-decoration: none;
		white-space: nowrap;
		font-variant-numeric: tabular-nums;
	}
	.site-header__phone:hover { color: var(--evergreen-press); text-decoration: underline; text-underline-offset: 4px; }

	/* The one-row header needs about 1080px to hold brand + nav + phone + quote. Below that it
	   drops the nav onto its own line, which is the layout the theme already shipped for phones.
	   This used to switch at 860px, which left 861-1079px running the desktop header at a width
	   it does not fit: the actions cluster wrapped and stranded the phone and quote button on a
	   near-empty second row. Found 2026-08-09 on an unfolded Z Fold. */
	@media (max-width: 1079px) {
		.site-header__inner { padding-block: 0.6rem; row-gap: 0.4rem; min-height: 0; }
		/* min-width: 0 is load-bearing, not tidying. A flex item defaults to min-width: auto,
		   which refuses to shrink below its content, so the nav pushed the header wider than
		   the screen and the whole DOCUMENT scrolled sideways on a phone: 443px of content in
		   a 371px viewport. The overflow-x: auto below was already there and was never able
		   to engage. Predates today's work; found while verifying it. */
		.site-nav { order: 3; flex-basis: 100%; margin-inline-start: 0; min-width: 0; }
		.site-nav__list { justify-content: flex-start; gap: 1.4rem; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
	}
	/* Five items, not four. Adding Gallery pushed the row to 453px in a 390px viewport, and
	   because this strip scrolls with its scrollbar hidden, the item that fell off the end was
	   Contact, with nothing on screen to suggest it was still there. Tightening the gap and the
	   label size below 520px fits all five without scrolling. The scroll stays as the backstop
	   for a narrower phone or a larger text setting, and the fade tells you it is there. */
	@media (max-width: 520px) {
		.site-nav__list { gap: 0.85rem; }
		.site-nav__list a { font-size: 1.05rem; }
		/* 0.85rem fits 390 and 430 but still put Contact's edge 3px past a 360px screen, which
		   is a real width on older Androids. Measured, not guessed at. */
		@media (max-width: 380px) {
			.site-nav__list { gap: 0.6rem; }
			.site-nav__list a { font-size: 1rem; }
		}
		.site-nav { position: relative; }
		.site-nav::after {
			content: "";
			position: absolute;
			inset-block: 0;
			right: 0;
			width: 1.5rem;
			pointer-events: none;
			background: linear-gradient(to right, rgba(244, 241, 234, 0), var(--paper));
		}
	}
	/* Phones only. They get the sticky bottom action bar, so the header's quote button would be
	   the third copy of the same CTA; tablets and small laptops have no bottom bar and keep it. */
	@media (max-width: 860px) {
		.site-header__cta { display: none; }  /* phone is the mobile action; quote CTA returns in the hero */
		.site-header__actions::before { display: none; }
	}
	/* The header is two rows here AND still sticky (sticky starts at 861px), so --header-h has to
	   describe the taller header or .flyover__stage's `top` and the hero's min-height both do
	   their maths against a header that is no longer 76px tall. */
	@media (min-width: 861px) and (max-width: 1079px) {
		:root { --header-h: 115px; }  /* measured: two rows with the quote button still shown */
	}

	/* ---------- Hero ---------- */
	.hero {
		position: relative;
		isolation: isolate;   /* keeps the hero's internal z-order from outranking later sections */
		display: grid;
		min-height: min(calc(100svh - var(--header-h) - 1px), 820px);
		background: var(--slate);
	}
	.hero__media, .hero__scrim, .hero__content { grid-area: 1 / 1; }
	.hero__media { z-index: 1; }
	.hero__scrim { z-index: 2; }
	.hero__content { z-index: 3; }

	.hero__media { position: relative; overflow: hidden; }
	.hero__poster, .hero__video {
		position: absolute; inset: 0;
		width: 100%; height: 100%;
		object-fit: cover;
	}
	/* .hero__poster is a <picture> now (art-directed portrait crop on phones), and object-fit
	   does not apply to the picture box itself, only to the replaced element inside it. */
	.hero__poster { display: block; }
	.hero__poster img { width: 100%; height: 100%; object-fit: cover; }
	.hero__video {
		opacity: 0;
		transition: opacity 0.6s var(--ease-solid);
	}
	.hero__video.is-playing { opacity: 1; }

	/* Two gradients, not one. The old single bottom-up wash was tuned to a dark poster; the
	   current photograph is a bright sunlit deck, and the eyebrow line crossed a white window
	   frame. The vertical wash carries the general legibility floor and the left-hand wash
	   darkens only the corner the copy occupies, so the deck on the right stays as bright as
	   it was shot.
	   HOW THE RATIOS BELOW WERE TAKEN, because the method is the whole claim: the poster is
	   redrawn to canvas exactly as object-fit: cover lays it out, both gradients are composited
	   over it, and then the WORST (brightest) single pixel under each line of text is measured
	   against that text's own colour. "Under the text" means the Range client rects of the
	   glyph runs, not the element's block box, which for a full-width <p> would sample empty
	   space the letters never touch.
	   An earlier version of this comment claimed 6.10 for the desktop eyebrow. That number came
	   from an 18-point sparse grid that stepped straight over the window frame; the same line
	   measures 3.55 when every glyph pixel is checked, i.e. it was failing. Sparse sampling on
	   a photographic background is not measurement. Current, all-pixels method:
	     desktop  eyebrow 6.06   h1 5.83   sub 9.00   phone link 11.59
	     phone    eyebrow 6.87   h1 8.04   sub 10.60  phone link 14.20
	   Floors: 4.5 for the small copy, 3.0 for the display h1. */
	.hero__scrim {
		background:
			linear-gradient(to right, rgba(35, 36, 31, 0.74) 0%, rgba(35, 36, 31, 0.46) 42%, rgba(35, 36, 31, 0) 72%),
			linear-gradient(to top, rgba(35, 36, 31, 0.86) 0%, rgba(35, 36, 31, 0.5) 45%, rgba(35, 36, 31, 0.14) 100%);
	}
	/* Paper, not fog, at every width. Fog leaves this line at 3.74 on desktop even with the
	   deeper wash above, and darkening the photograph further to rescue a 13px label is the
	   wrong trade. */
	.hero__eyebrow { color: var(--paper); }
	/* On a phone the copy spans the full width, so a left-hand wash buys nothing and the
	   vertical one has to do all the work: the eyebrow sits over the bright chairs in the
	   portrait crop and needs real cover. This is the same wash, deeper and reaching
	   further up. */
	@media (max-width: 859px) {
		/* A phone hero is tall and the copy occupies the bottom half, so the wash is shaped
		   to that instead of covering the frame evenly. A flat 0.42 at the top passed the
		   contrast check but visibly dulled the video across the whole hero; holding the
		   darkness under the text and letting it clear by three-quarters height keeps the
		   footage bright where nothing is written over it. */
		.hero__scrim {
			background:
				linear-gradient(to top,
					rgba(35, 36, 31, 0.94) 0%,
					rgba(35, 36, 31, 0.90) 38%,
					/* The eyebrow sits right about here and it is the smallest text in the
					   hero, so this stop is set by its contrast, not by taste. */
					rgba(35, 36, 31, 0.78) 58%,
					rgba(35, 36, 31, 0.28) 78%,
					rgba(35, 36, 31, 0.04) 100%);
		}
	}

	.hero__content {
		align-self: end;
		padding: var(--space-m) var(--gutter) clamp(2.25rem, 5vh, 3.5rem);
		max-width: 62rem;
	}
	.hero__eyebrow {
		font-size: var(--step--1);
		font-weight: 600;
		letter-spacing: 0.22em;
		text-transform: uppercase;
		/* Colour lives with the scrim above, where the contrast reasoning is. */
		margin-bottom: 1.1rem;
	}
	/* The line gained "Decks and fences" on 2026-08-17 and no longer fits one phone line.
	   Left to wrap on its own it broke at "25 miles around / Sherwood, Oregon", splitting a
	   phrase across lines. Each half is nowrap instead, so the only legal break is the space
	   between them: one whole phrase per line on a phone, still one line on a desktop, and no
	   breakpoint to keep in sync. */
	.hero__eyebrow span { white-space: nowrap; }
	/* ...but below ~385px it does not fit at all, and nowrap made the DOCUMENT wider than
	   the screen: the whole page scrolled sideways on a 320px phone (WCAG 1.4.10 reflow)
	   and the hero media stretched to the overflowed width with it.

	   Breakpoint measured, not guessed: swept 378-396px against the live page. "25 miles
	   around Sherwood, Oregon" renders 345px with nowrap on, needs 20px of gutter each
	   side, and the document still overflowed at 382, 383 and 384 before coming clean at
	   385. Note the span's own width is fluid (--step--1), so it is 271px once it is
	   allowed to wrap and 344-345px when it is not; sizing this off a single measurement
	   at one width is how the first attempt landed 3px short. 390px, the common phone,
	   keeps the nowrap with 45px to spare. */
	@media (max-width: 384px) {
		.hero__eyebrow span { white-space: normal; }
	}
	.hero__content h1 {
		/* The page's largest headline by design: nothing below outranks the thesis. */
		font-size: clamp(2.6rem, 2rem + 3.2vw, 5.75rem);
		font-weight: 700;
		letter-spacing: -0.025em;
		line-height: 0.98;
		color: var(--paper);
	}
	.hero__sub {
		font-size: var(--step-1);
		line-height: 1.45;
		color: var(--paper-on-dark);
		max-width: 44ch;
		margin-top: 1.1rem;
	}
	.hero__actions {
		display: flex;
		align-items: center;
		flex-wrap: wrap;
		gap: 1.1rem 1.6rem;
		margin-top: 1.9rem;
	}
	.hero__phone {
		color: var(--paper);
		font-weight: 600;
		text-decoration-color: var(--concrete);
		text-underline-offset: 4px;
		white-space: nowrap;
	}
	.hero__phone:hover { text-decoration-color: var(--paper); }

	/* ---------- Reviews: the 5.0 as architecture, the quotes on a paper plate ----------
	   Three moves, all flat (no gradient, no shadow, no texture, per Gage's "flat is fine"):
	   1. the 5.0 runs off the left edge of the screen the way the services photos bleed,
	   2. the quotes ride a Paper panel that crosses up into the hero and off the right edge,
	      so the band reads as two planes meeting instead of one filled rectangle,
	   3. each review's own opening sentence is pulled to display scale, so the eye has four
	      places to land instead of one number and a column of grey.
	   Quote text stays verbatim from the live Google listing; the lead is just the first
	   sentence set larger, never a paraphrase. */
	.reviews {
		background: var(--evergreen);
		padding-block: clamp(3.5rem, 7vw, 6rem);
		overflow-x: clip;      /* contains the numeral's bleed without killing the panel's rise */
		overflow-y: visible;
		position: relative;    /* .hero is positioned, so the panel needs its own layer to rise over it */
		z-index: 1;
	}
	/* Visually hidden, still announced. .reviews__sr predates this and kept its own copy of
	   the same declarations; both names point at one rule now so they cannot drift apart. */
	.sr-only,
	.reviews__sr {
		position: absolute; width: 1px; height: 1px; overflow: hidden;
		clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
	}
	.reviews__inner {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 30rem) minmax(0, 46rem) [content-end]
			minmax(0, 1fr) [full-end];
		align-items: center;
	}
	.reviews__stat {
		grid-column: full-start / 3;
		padding-right: clamp(1rem, 2vw, 1.75rem);
		container-type: inline-size;   /* so the numeral can be sized off its own column */
	}
	.reviews__big {
		/* Was bled off the left edge at 77cqi with a -0.135em shift. It read as a rendering
		   bug rather than a crop: Derek's first note on the band was that the 5 looks cut
		   off, and once someone says that you cannot unsee it. The numeral now sits fully
		   on the same left rail as the stars, the meta line and the link below it, so the
		   whole stat column reads as one flush-left stack. Still the biggest thing in the
		   band, just no longer amputated. */
		font-size: clamp(5rem, 18vw, 15rem);   /* fallback: cqi in min() drops the whole line */
		font-size: min(56cqi, 15rem);
		font-weight: 700;
		letter-spacing: -0.055em;
		line-height: 0.78;
		color: var(--paper);
		font-variant-numeric: tabular-nums;
		margin-inline-start: var(--gutter);
		/* Pull the glyph's own left side bearing off so the "5" optically aligns with the
		   flush-left text below it instead of sitting a hair inboard of the rail. */
		text-indent: -0.045em;
	}
	.reviews__field {
		display: flex;
		gap: clamp(0.5rem, 1.2vw, 0.95rem);
		margin-top: 2.2rem;
		margin-inline-start: var(--gutter);
	}
	.reviews__field svg { width: clamp(22px, 2.6vw, 32px); height: auto; fill: rgba(244, 241, 234, 0.92); }
	.reviews__meta {
		margin-top: 1.5rem;
		margin-inline-start: var(--gutter);
		font-size: var(--step-0);
		line-height: 1.5;
		color: rgba(244, 241, 234, 0.85);
		max-width: 24ch;
	}
	.reviews__link {
		display: inline-block;
		margin-top: 1.5rem;
		margin-inline-start: var(--gutter);
		font-weight: 600;
		color: var(--paper);
		text-decoration: none;
		border-bottom: 2px solid rgba(244, 241, 234, 0.55);   /* 0.35 sat at 2.46:1, under the 3:1 UI floor */
		padding-bottom: 2px;
		transition: border-color 0.18s var(--ease-solid);
	}
	.reviews__link:hover { border-color: var(--paper); }

	/* The panel used to be one flat cream rectangle with three quotes stacked down its left
	   third: no divisions, no colour, and a dead half on the right that the 52ch measure
	   never reached. Derek called it a giant white block, which it was. It is now three
	   boards: the feature quote on paper across the top, the two shorter ones on fog below
	   it, side by side. The gaps between them are --seam, the same board gap the photo runs
	   use, so the division is the site's own device rather than a borrowed card style. */
	.reviews__quotes {
		grid-column: 3 / full-end;
		/* The panel's own ground is the band's green, and it is visible only in the seams
		   between the boards. Three boards with the green showing through the gaps is the
		   deck itself, and it puts the brand's one hue INSIDE the block instead of only
		   around it, which is what "it needs colour" actually needed. */
		background: var(--evergreen);
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: var(--seam);
		align-items: stretch;
	}
	.quote {
		background: var(--fog);
		padding: clamp(1.5rem, 2.4vw, 2.25rem) clamp(1.5rem, 2.4vw, 2.25rem) clamp(1.6rem, 2.4vw, 2.25rem);
		display: flex;
		flex-direction: column;
	}
	/* The feature runs the full width and stays on paper, so the eye lands on it first and
	   the two fog boards below read as the supporting pair. */
	.quote--feature {
		grid-column: 1 / -1;
		background: var(--paper);
		padding-inline: clamp(1.75rem, 3vw, 3rem);
		padding-block: clamp(1.75rem, 3vw, 2.75rem);
	}
	/* The panel only rises into the hero where there is room to its left: below this the
	   hero's own CTA row reaches far enough right that the two would collide. */
	@media (min-width: 1024px) {
		.reviews__quotes { margin-top: calc(-1 * clamp(3.5rem, 7vw, 6rem) - clamp(2rem, 4vw, 3.5rem)); }
	}
	.quote__lead {
		display: block;
		font-size: clamp(1.5rem, 2.9vw, 2.5rem);
		font-weight: 400;
		line-height: 1.1;
		letter-spacing: -0.025em;
		/* Evergreen, not ink: it carries the brand hue into the largest type in the block
		   and separates the lifted sentence from the body copy by colour as well as size. */
		color: var(--evergreen);
		margin-bottom: 0.7rem;
		text-wrap: balance;
		/* Hang the opening curly quote so the lead's first letter sits on the same flush
		   left edge as the body copy and caption below it. Measured offset is 0.47em, the
		   glyph's own advance. hanging-punctuation is Safari-only, so do it by hand. */
		text-indent: -0.47em;
	}
	.quote--feature .quote__lead { font-size: clamp(1.75rem, 3.4vw, 2.9rem); }
	.quote blockquote {
		font-size: var(--step-0);
		line-height: 1.6;
		color: var(--body-ink);
		text-wrap: pretty;
		max-width: 52ch;
	}
	/* The feature board is full-panel width, so a 52ch measure left a third of it empty.
	   64ch is still inside the comfortable reading range and fills the board. */
	.quote--feature blockquote { max-width: 64ch; }
	/* Pushed to the bottom of the board so the two fog quotes bottom-align with each other
	   even though their copy runs to different lengths. */
	.quote figcaption {
		margin-top: auto;
		padding-top: clamp(1rem, 1.6vw, 1.4rem);
		font-size: var(--step--1);
		color: var(--slate);
		display: flex;
		align-items: center;
		flex-wrap: wrap;
		gap: 0.1rem 0.6rem;
	}
	/* Derek's ask: every review carries its own stars, not just the summary up the column.
	   Evergreen is the only hue the brand has, and against fog it is the strongest colour
	   move available without inventing a gold that is not in the guidelines. */
	.quote__stars {
		display: flex;
		gap: 0.2rem;
		flex-basis: 100%;
		margin-bottom: 0.55rem;
	}
	.quote__stars svg { width: 15px; height: 15px; fill: var(--evergreen); }
	.quote--feature .quote__stars svg { width: 17px; height: 17px; }
	.quote__who { font-weight: 600; color: var(--ink); }
	/* The source is the quieter half of the credit: same line, one step back, separated by a
	   rule rather than a comma so the name reads as the name. Slate, not concrete: concrete
	   measures 2.13:1 on fog and 2.69:1 on paper, both well under the 4.5 floor for text this
	   size. Slate gives 6.85:1 on fog and 8.02:1 on paper and still reads as the quieter half,
	   because the weight and the rule are doing that job, not the value. */
	.quote__src {
		color: var(--slate);
		padding-left: 0.6rem;
		border-left: 1px solid rgba(71, 75, 67, 0.35);
	}

	@media (max-width: 860px) {
		.reviews { padding-block: clamp(3rem, 8vw, 4rem) 0; }
		.reviews__inner { grid-template-columns: 1fr; }
		.reviews__stat { grid-column: 1; padding-right: 0; }
		.reviews__big { font-size: clamp(4rem, 20vw, 9rem); font-size: min(46cqi, 9rem); }
		.reviews__meta { max-width: 40ch; }
		.reviews__quotes {
			grid-column: 1;
			margin-top: clamp(2.5rem, 7vw, 4rem);
			grid-template-columns: 1fr;   /* the pair stacks; the seam keeps dividing them */
			/* The three-boards rework dropped the old padding-inline here and nothing put it
			   back, so the cards sat flush against both screen edges while the 5.0, the stars,
			   the meta line and the link directly above them all kept their gutter. Padding,
			   not margin: the panel's own ground is the green that shows through the seams, and
			   it should keep showing along the outside edges too. */
			padding-inline: var(--gutter);
		}
		.quote--feature { grid-column: 1; }
	}

	/* ---------- Services: aerial-led photo bands ---------- */
	/* Breakout grid: content aligns to the same 76rem line as everything else;
	   photos escape it to the viewport edge. Spacing rhythm: 8px base, 96px structural. */
	.services {
		background: var(--paper);
		padding-block: clamp(4rem, 9vw, 7rem) clamp(3rem, 7vw, 5rem);
		overflow: clip;   /* clip, not hidden: hidden makes this a scroll container and kills the flyover's sticky stage */
	}
	.services.is-flyover { padding-bottom: 0; }
	.services.is-flyover .svc-row,
	.services.is-flyover .svc-cta { display: none; }

	/* The screen-reader equivalent of the theater's chapters (see .svc-sr in front-page.php).
	   Present in the DOM on every tier, but only EXPOSED when the theater is actually on:
	   when the static .svc-row blocks are visible they are already the equivalent, and
	   announcing both would read every service twice. */
	.svc-sr { display: none; }
	.services.is-flyover .svc-sr {
		display: block;
		position: absolute; width: 1px; height: 1px; overflow: hidden;
		clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
	}
	.svc-head,
	.svc-row {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 38rem) minmax(0, 38rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
	}
	.svc-head > * { grid-column: content-start / content-end; }
	.svc-head h1,
	.svc-head h2 {
		font-size: clamp(2.8rem, 6.5vw, 5.5rem);   /* under the hero H1; hierarchy runs downhill from the thesis */
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 0.95;
		color: var(--ink);
	}
	.svc-head__sub {
		margin-top: 1.5rem;
		font-size: var(--step-1);
		line-height: 1.45;
		color: var(--body-ink);
		max-width: 46ch;
	}
	.svc-head { margin-bottom: clamp(3rem, 6vw, 6rem); }

	.svc-row { align-items: center; }
	.svc-row + .svc-row { margin-top: clamp(3rem, 6vw, 6rem); }

	/* The text block is a paper panel that crosses the photo's top corner (an L-step bite),
	   the same planes-meeting gesture as the reviews panel rising into the hero. On the paper
	   band the panel's own field is invisible; its edge exists only where it bites the photo. */
	.svc-row__text {
		position: relative;
		z-index: 1;
		background: var(--paper);
		align-self: start;
		margin-top: calc(-1 * clamp(1.5rem, 3.5vw, 3rem));   /* the panel starts above the photo's top edge */
	}
	.svc-row--photo-right .svc-row__text {
		grid-column: content-start / 3;
		grid-row: 1;
		padding: clamp(1.75rem, 3vw, 2.75rem) clamp(1.75rem, 3.5vw, 3.25rem) clamp(1.75rem, 3vw, 2.75rem) 0;
		margin-right: calc(-1 * clamp(2.5rem, 6vw, 6rem));
	}
	.svc-row--photo-right .svc-row__media { grid-column: 3 / full-end; grid-row: 1; }
	.svc-row--photo-left .svc-row__media { grid-column: full-start / 3; grid-row: 1; }
	.svc-row--photo-left .svc-row__text {
		grid-column: 3 / content-end;
		grid-row: 1;
		padding: clamp(1.75rem, 3vw, 2.75rem) 0 clamp(1.75rem, 3vw, 2.75rem) clamp(1.75rem, 3.5vw, 3.25rem);
		margin-left: calc(-1 * clamp(2.5rem, 6vw, 6rem));
	}

	.svc-row__media img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		aspect-ratio: 16 / 10;
		max-height: 420px;
	}
	.svc-row--flagship .svc-row__media img { aspect-ratio: 16 / 9; min-height: clamp(360px, 34vw, 600px); max-height: none; }

	.svc-row__text h3,
	.svc-row__text h2 {
		font-size: clamp(2rem, 4.2vw, 3.5rem);
		font-weight: 700;
		letter-spacing: -0.025em;
		line-height: 1;
		color: var(--ink);
	}
	.svc-row--flagship .svc-row__text h3 { font-size: clamp(2.4rem, 5.2vw, 4.5rem); }
	.svc-row__text p {
		margin-top: 1rem;
		font-size: var(--step-0);
		line-height: 1.55;
		color: var(--body-ink);
		max-width: 38ch;
	}
	/* :not(.button) throughout. The services chapters promoted this link to a real button
	   on 2026-08-17, and `.svc-row__text a` (0,1,1) out-specifies `.button` (0,1,0), so
	   without the exclusion the text-link rules win and repaint the button: an underline
	   across its bottom edge, and on the ink chapter a paper label on a paper face. The
	   homepage flyover still uses the plain text link, which is why these rules stay. */
	.svc-row__text a:not(.button) {
		display: inline-block;
		margin-top: 1.5rem;
		font-weight: 600;
		color: var(--evergreen);
		text-decoration: none;
		border-bottom: 2px solid rgba(47, 74, 56, 0.75);
		padding-bottom: 2px;
		transition: border-color 0.18s var(--ease-solid);
	}
	.svc-row__text a:not(.button):hover { border-color: var(--evergreen); }
	/* The button needs its own top gap: the rule above supplied it, and .button has none. */
	.svc-row__text .button { margin-top: 1.75rem; }

	.svc-cta {
		margin-top: clamp(3rem, 6vw, 6rem);
		padding-inline: var(--gutter);
		max-width: 76rem;
		margin-inline: auto;
	}
	.svc-cta a {
		font-size: var(--step-1);
		font-weight: 600;
		color: var(--evergreen);
		text-decoration: none;
		border-bottom: 2px solid rgba(47, 74, 56, 0.75);
		padding-bottom: 3px;
	}
	.svc-cta a:hover { border-color: var(--evergreen); }

	/* ---------- The flyover: pinned theater, mounted-print edition ----------
	   380vh of scroll runway; the stage stays pinned a full viewport tall. The media is a
	   large canvas mounted inside the paper band (not full-bleed), scroll scrubs a slow
	   authored camera move across each still: push-in, drift, track. The paper panel bites
	   the canvas's bottom-right corner, the site's L-gesture at theater scale. */
	.flyover { height: 380vh; margin-top: clamp(2rem, 5vw, 4rem); }
	/* CSS backstop for the JS width gate: if the window is resized across 1024px after load,
	   the class state is stale (the gate is computed once); these rules force the correct
	   tier regardless, so no viewport is ever stranded theater-less rows-less. */
	@media (max-width: 1023px) {
		.flyover { display: none; }
		.services.is-flyover .svc-row,
		.services.is-flyover .svc-cta { display: grid; }
		.services.is-flyover .svc-cta { display: block; }
		.services.is-flyover { padding-bottom: clamp(3rem, 7vw, 5rem); }
		/* Same backstop for the screen-reader block: this media query puts the static rows
		   BACK when the is-flyover class is stale after a resize across 1024px, and the rows
		   are the equivalent, so exposing .svc-sr as well would read every service twice. */
		.services.is-flyover .svc-sr { display: none; }
	}
	.flyover__stage {
		position: sticky;
		top: var(--header-h);                     /* the sticky header owns the strip above */
		height: calc(100svh - var(--header-h));
		overflow: clip;
		background: var(--paper);
	}
	.flyover__scene {
		position: absolute;
		inset: clamp(1.5rem, 4vh, 3rem) var(--gutter) clamp(4rem, 9vh, 6.5rem);
		visibility: hidden;
		opacity: 0;
		transition: opacity 0.65s var(--ease-solid), visibility 0s 0.65s;
	}
	.flyover__scene.is-active {
		visibility: visible;
		opacity: 1;
		transition: opacity 0.65s var(--ease-solid);
		z-index: 1;
	}
	.flyover__media {
		position: absolute;
		inset: 0;
		overflow: clip;
		background: var(--ink);
	}
	.flyover__media img {
		position: absolute;
		inset: 0;
		width: 100%;
		height: 100%;
		object-fit: cover;
		will-change: transform;
	}
	.flyover__scrim {
		position: absolute;
		inset: 0;
		background:
			linear-gradient(to bottom, rgba(35, 36, 31, 0.4) 0%, rgba(35, 36, 31, 0) 20%),
			linear-gradient(to top, rgba(35, 36, 31, 0.66) 0%, rgba(35, 36, 31, 0.2) 36%, rgba(35, 36, 31, 0.06) 100%);
	}
	.flyover__title {
		position: absolute;
		left: clamp(1.75rem, 3vw, 3rem);
		bottom: clamp(1.75rem, 4.5vh, 3.5rem);
		max-width: 56%;
		font-size: clamp(3rem, 7.5vw, 7.5rem);
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 0.92;
		color: var(--paper);
		text-wrap: balance;
		transform: translateY(0.4em);
		opacity: 0;
		transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s ease-out;
	}
	.flyover__scene.is-active .flyover__title { transform: none; opacity: 1; transition-delay: 0.15s; }
	/* Evergreen, not paper: the reviews band inverted (there an evergreen band carries a
	   paper panel; here the paper stage carries an evergreen one). */
	.flyover__panel {
		position: absolute;
		right: clamp(1.75rem, 4vw, 4rem);
		bottom: calc(-1 * clamp(2rem, 4.5vh, 3rem));   /* crosses the canvas's bottom edge onto the paper */
		width: min(26rem, 38%);
		background: var(--evergreen);
		padding: clamp(1.5rem, 2.5vw, 2.25rem);
		transform: translateX(2.5rem);
		opacity: 0;
		transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s ease-out;
	}
	.flyover__scene.is-active .flyover__panel { transform: none; opacity: 1; transition-delay: 0.25s; }
	.flyover__panel p {
		font-size: var(--step-0);
		line-height: 1.55;
		color: var(--paper);
	}
	.flyover__panel a {
		display: inline-block;
		margin-top: 1rem;
		font-weight: 600;
		color: var(--paper);
		text-decoration: none;
		border-bottom: 2px solid rgba(244, 241, 234, 0.55);
		padding-bottom: 2px;
		transition: border-color 0.18s var(--ease-solid);
	}
	.flyover__panel a:hover { border-color: var(--paper); }
	.flyover__rail {
		position: absolute;
		top: calc(clamp(1.5rem, 4vh, 3rem) + clamp(1.5rem, 3vh, 2.5rem));
		right: calc(var(--gutter) + clamp(1.5rem, 3vw, 2.5rem));
		z-index: 2;
		display: grid;
		gap: 0.6rem;
		justify-items: end;
	}
	.flyover__rail span {
		font-size: var(--step--1);
		font-weight: 600;
		letter-spacing: 0.14em;
		text-transform: uppercase;
		color: rgba(244, 241, 234, 0.45);
		border-bottom: 2px solid transparent;
		padding-bottom: 2px;
		transition: color 0.3s ease-out, border-color 0.3s ease-out;
	}
	.flyover__rail span.is-here { color: var(--paper); border-color: rgba(244, 241, 234, 0.7); }

	/* ---------- About: the counter-shot ----------
	   Services is the drone view; this is deck height. One quiet composition on the Fog
	   ground: text block left-set in the content column, then the wide crew photo held
	   INSIDE the grid rails (the one photo on the page that does not bleed), caption under.
	   Flat, framed, album-page register. */
	/* The dark room: ink ground bookends the dark hero, and the crew photos carry the
	   page's color (warm cedar, amber string lights, garden green) the way the brand
	   system intends: neutral chrome, chromatic photography. */
	.about {
		background: var(--ink);
		padding-block: clamp(4rem, 8vw, 6.5rem);
	}
	.about .about__inner h2 { color: var(--paper); }
	.about .about__body p { color: var(--paper-on-dark); }
	.about .about__link {
		color: var(--paper);
		border-bottom-color: rgba(244, 241, 234, 0.55);
	}
	.about .about__link:hover { border-color: var(--paper); }
	.about__inner {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 38rem) minmax(0, 38rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
	}
	/* Editorial split: the headline anchors the left column, the body copy rides the top of
	   the right one, so the band's first row is full even before the photo lands. */
	.about__inner h2 {
		grid-column: content-start / 3;
		font-size: clamp(2.4rem, 5.4vw, 5rem);
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 0.95;
		color: var(--ink);
		padding-right: clamp(1.5rem, 4vw, 4rem);
	}
	.about__body { grid-column: 3 / content-end; padding-top: 0.35em; }
	.about__body p {
		font-size: var(--step-1);
		line-height: 1.45;
		color: var(--body-ink);
		max-width: 44ch;
	}
	.about__link {
		display: inline-block;
		margin-top: 1.5rem;
		font-weight: 600;
		color: var(--evergreen);
		text-decoration: none;
		border-bottom: 2px solid rgba(47, 74, 56, 0.75);
		padding-bottom: 2px;
		transition: border-color 0.18s var(--ease-solid);
	}
	.about__link:hover { border-color: var(--evergreen); }
	@media (max-width: 860px) {
		.about__inner { grid-template-columns: [full-start] 0 [content-start] 1fr 0 [content-end] 0 [full-end]; }
		.about__inner h2, .about__body { grid-column: content-start / content-end; }
		.about__inner { padding-inline: var(--gutter); }
		.about__inner h2 { padding-right: 0; }
		.about__body { padding-top: 1.25rem; }
	}

	/* ---------- The crew collage: the page's still moment ----------
	   Three ground-level frames composed on the fog: a wide anchor, a tall frame riding
	   higher than it, and a small paper-matted print sitting on the anchor's corner, the
	   site's plane-overlap gesture held static. No motion; the theater above earns its
	   motion by being the only one. */
	/* Rails, not scatter: the name at display scale, then a strict triptych locked to the
	   same content rails as the type. Columns are proportioned to each photo's aspect
	   ratio (1.6 / 1.333 / 0.8), so all three render at ONE exact shared height and the
	   row always fills the text's width precisely. One baseline, aligned captions. */
	.collage {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 76rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
		margin-top: clamp(2rem, 4vw, 3.5rem);
	}
	.collage__row {
		grid-column: content-start / content-end;
		display: grid;
		grid-template-columns: 1.6fr 1.333fr 0.8fr;
		gap: clamp(1rem, 2vw, 2rem);
	}
	.collage figure { margin: 0; }
	.collage img { width: 100%; height: auto; object-fit: cover; }
	.collage__anchor img { aspect-ratio: 16 / 10; }
	.collage__mid img { aspect-ratio: 4 / 3; }
	.collage__tall img { aspect-ratio: 4 / 5; }
	.collage figcaption {
		margin-top: 0.7rem;
		font-size: var(--step--1);
		font-weight: 600;
		color: var(--concrete);   /* on the ink ground; 5.5:1 */
	}
	@media (max-width: 860px) {
		.collage__row { display: block; }
		.collage figure + figure { margin-top: 1.75rem; }
	}

	/* ---------- Closing CTA: the ask, in numerals ---------- */
	.close {
		background: var(--evergreen);
		padding-block: clamp(4rem, 8vw, 7rem);
	}
	.close__inner {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 54rem) minmax(0, 22rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
		align-items: end;
	}
	.close__lead {
		grid-column: content-start / 3;
		container-type: inline-size;   /* the numeral sizes off its own column, learned on the reviews 5.0 */
		padding-right: clamp(1.5rem, 3vw, 3rem);
	}
	.close__lead h2 {
		font-size: clamp(2.2rem, 3.4vw, 3.25rem);
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 1;
		color: var(--paper);
	}
	.close__phone {
		display: inline-block;
		margin-top: 1.25rem;
		font-size: clamp(2.8rem, 7vw, 6.5rem);   /* fallback: cqi line below wins where supported */
		font-size: min(13.8cqi, 8.5rem);
		font-weight: 700;
		letter-spacing: -0.025em;
		line-height: 1;
		color: var(--paper);
		text-decoration: none;
		font-variant-numeric: proportional-nums;   /* tabular slots read gap-toothed at display scale */
		white-space: nowrap;
		border-bottom: 2px solid rgba(244, 241, 234, 0.5);
		padding-bottom: 0.06em;
		transition: border-color 0.18s var(--ease-solid);
	}
	.close__phone:hover { border-color: var(--paper); }
	.close__side {
		grid-column: 3 / content-end;
		padding-bottom: 0.35em;   /* seats the button's base on the numeral's baseline zone */
	}
	.close__side p {
		font-size: var(--step-0);
		line-height: 1.55;
		color: var(--paper);
		max-width: 30ch;
	}
	.close__side .button { margin-top: 1.5rem; }
	.button--paper {
		background-color: var(--paper);
		background-image: linear-gradient(var(--fog), var(--fog));
		color: var(--evergreen);
	}

	@media (max-width: 860px) {
		.close__inner { grid-template-columns: 1fr; padding-inline: var(--gutter); }
		.close__lead, .close__side { grid-column: 1; }
		.close__phone { font-size: clamp(2.1rem, 9.5vw, 3.4rem); }
		.close__side { padding-bottom: 0; margin-top: 1.75rem; }
	}

	/* Scroll motion, JS-driven for universal support (CSS animation-timeline is still
	   Chrome-only; Firefox/Safari users must not get a dead section). The .js-motion class
	   lands on <html> ONLY when JS runs AND the user has no reduced-motion preference, so
	   no-JS, old browsers, and reduced-motion all get the plain static layout. Two gestures:
	   1. drift: the aerial photos pan slowly inside their frames as you scroll, the drone
	      still hovering. JS sets translate each frame; 1.12 overscan hides the travel.
	   2. dock: each paper panel slides its last 2.5rem into the photo bite when the row
	      enters (IntersectionObserver adds .is-in once), the planes-meeting gesture performed. */
	.js-motion .svc-row__media { overflow: clip; }
	.js-motion .svc-row__media img { scale: 1.12; }
	.js-motion .svc-row__text {
		opacity: 0.001;
		transform: translateX(-2.5rem);
		transition:
			transform 0.7s cubic-bezier(0.22, 1, 0.36, 1),
			opacity 0.5s ease-out;
	}
	.js-motion .svc-row--photo-left .svc-row__text { transform: translateX(2.5rem); }
	.js-motion .svc-row__text.is-in { opacity: 1; transform: none; }

	@media (max-width: 860px) {
		.svc-row { grid-template-columns: 1fr; }
		.svc-row--photo-right .svc-row__text,
		.svc-row--photo-left .svc-row__text {
			grid-column: 1;
			grid-row: auto;
			padding: 1.5rem var(--gutter) 0;
			margin: 0;
			background: none;
			align-self: auto;
		}
		.svc-row--photo-right .svc-row__media,
		.svc-row--photo-left .svc-row__media {
			grid-column: 1;
			grid-row: auto;
		}
		.svc-row__media { order: -1; }   /* photo leads every row on mobile; one consistent scan rhythm */
		.svc-row--flagship .svc-row__media img { min-height: 0; }
		.svc-head { padding-inline: 0; }
		.services { padding-block-start: 2rem; }   /* the reviews panel above carries paper padding already; don't stack two blanks */
		/* Honest tap targets. Excludes .button, which already ships a 1.05em block padding
		   and clears 44px on its own; adding to it would just inflate the chapter buttons. */
		.svc-row__text a:not(.button), .svc-cta a { padding-block: 0.4rem; }
	}

	/* ---------- Services page: three chapters, three grounds ---------- */
	/* The value staircase: build on paper, repair on ink, fences on fog, then the
	   evergreen close. Ink sits MID-page on purpose: #23241F against the close band's
	   #2F4A38 is a near-zero-value seam, so the darkest chapter cannot be the last one.
	   Each chapter is a full-bleed band; the content inside stays on the same 76rem rails,
	   so the ground changes and the measure never does. */
	.services--page {
		padding-block: clamp(3rem, 6vw, 5rem) 0;   /* the last chapter's band padding runs straight into the close */
	}
	/* The page title takes the whole measure: headline left, the sub sitting on its
	   baseline at the right. Stacked in one column it left an L of blank paper beside
	   itself, which is exactly the emptiness the bands were meant to cure. */
	.services--page .svc-head {
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 46rem) minmax(0, 30rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
		align-items: end;
		margin-bottom: clamp(2rem, 3.5vw, 3rem);   /* the band edge should read as a decision, not a collision */
	}
	.services--page .svc-head h1 {
		grid-column: content-start / 3;
		font-size: clamp(2.8rem, 7vw, 6rem);
	}
	.services--page .svc-head__sub {
		grid-column: 3 / content-end;
		margin-top: 0;
		padding-bottom: 0.6rem;
		max-width: 32ch;
	}
	.svc-chapter {
		scroll-margin-top: calc(var(--header-h) + 1rem);
		padding-block: clamp(4rem, 8vw, 7rem);
	}
	.svc-chapter--fog { background: var(--fog); }
	.svc-chapter--ink { background: var(--ink); }

	/* The corner-bite panel takes its band's own ground, so the gesture stays what it
	   always was (two planes meeting, edge visible only where it crosses the photo) and
	   never degrades into a floating white card on a colored field. */
	.svc-chapter--fog .svc-row__text { background: var(--fog); }
	.svc-chapter--ink .svc-row__text { background: var(--ink); }

	/* "Fix weird image gaps" (client, round 1, 2026-08-16).
	   The bite works on the homepage, where the panel is short and wide against a wide
	   photo, so the overlap reads as one plane crossing another. On these chapter rows
	   the panel is TALL (title, lede, detail, link) and the overlap is only ~84px, so
	   the same rule cut a narrow full-height notch down the left edge of every main
	   photo and stepped the photo out beneath it. Three chapters, three notches, each
	   reading as a piece of the photograph missing rather than as a composition.
	   Here the two planes simply meet: the panel keeps its ground, the photo keeps a
	   clean rectangle, and the seam is the whole gesture. Scoped to .svc-chapter so the
	   homepage flyover's own rows are untouched. */
	.svc-chapter .svc-row__text { margin-top: 0; align-self: center; }
	.svc-chapter .svc-row--photo-right .svc-row__text { margin-right: 0; }
	.svc-chapter .svc-row--photo-left .svc-row__text { margin-left: 0; }

	/* Ink chapter: same type, inverted. Matches the about band's treatment exactly. */
	.svc-chapter--ink .svc-row__text h2 { color: var(--paper); }
	.svc-chapter--ink .svc-row__text p { color: var(--paper-on-dark); }
	.svc-chapter--ink .svc-row__lede { color: var(--paper); }
	.svc-chapter--ink .svc-row__text a:not(.button) {
		color: var(--paper);
		border-bottom-color: rgba(244, 241, 234, 0.5);
	}
	.svc-chapter--ink .svc-row__text a:not(.button):hover { border-bottom-color: var(--paper); }

	/* The whole chapter argument lives in the bite panel: title, lede, the specifics,
	   the ask. One block against one photo, no orphan paragraph sitting under a column
	   of dead air. */
	.svc-chapter .svc-row__lede {
		font-size: var(--step-1);
		line-height: 1.45;
		color: var(--ink);
		max-width: 34ch;
	}
	.svc-chapter .svc-row__text p + p { margin-top: 0.9rem; max-width: 42ch; }
	.svc-chapter .svcpage__gallery { margin-top: clamp(2.25rem, 4.5vw, 3.5rem); }
	@media (max-width: 860px) {
		.services--page .svc-head h1,
		.services--page .svc-head__sub { grid-column: content-start / content-end; }
		.services--page .svc-head__sub { margin-top: 1.25rem; padding-bottom: 0; }
	}

	/* ---------- Service pages: photo-led, on the rails ---------- */
	.svcpage { padding-block: clamp(3rem, 6vw, 5rem) clamp(4rem, 8vw, 7rem); }
	.svcpage__head,
	.svcpage__detail {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 46rem) minmax(0, 30rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
	}
	.svcpage__head h1 {
		grid-column: content-start / 3;
		font-size: clamp(2.6rem, 5.4vw, 4.75rem);
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 0.98;
	}
	.svcpage__head p {
		grid-column: content-start / 3;
		margin-top: 1.25rem;
		font-size: var(--step-1);
		line-height: 1.45;
		color: var(--body-ink);
		max-width: 44ch;
	}
	.svcpage__hero {
		margin: clamp(2.5rem, 5vw, 4rem) auto 0;
		max-width: 76rem;
		padding-inline: var(--gutter);
	}
	.svcpage__hero img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
	.svcpage__detail { margin-top: clamp(2rem, 4vw, 3rem); }
	.svcpage__detail p {
		grid-column: content-start / 3;
		font-size: var(--step-1);
		line-height: 1.5;
		color: var(--body-ink);
		max-width: 48ch;
	}
	.svcpage__detail a {
		grid-column: 3 / content-end;
		justify-self: start;
		align-self: center;
		font-weight: 600;
		color: var(--evergreen);
		text-decoration: none;
		border-bottom: 2px solid rgba(47, 74, 56, 0.75);
		padding-bottom: 2px;
		transition: border-color 0.18s var(--ease-solid);
	}
	.svcpage__detail a:hover { border-color: var(--evergreen); }
	/* The evidence, laid out like decking: parallel runs of unequal width, edge to edge,
	   separated by a seam the width of a real board gap. A four-square grid is what every
	   contractor template does; a board run is what only this client's page can do. */
	.svcpage__gallery {
		display: grid;
		grid-template-columns: 1.15fr 0.8fr 1.05fr 0.9fr;
		gap: var(--seam);
		margin-top: clamp(2.5rem, 5vw, 4rem);
	}
	.svcpage__gallery figure { margin: 0; min-width: 0; overflow: clip; }
	.svcpage__gallery img {
		display: block;
		width: 100%;
		height: clamp(280px, 27vw, 440px);
		object-fit: cover;
	}
	@media (max-width: 860px) {
		.svcpage__head h1, .svcpage__head p,
		.svcpage__detail p { grid-column: content-start / content-end; }
		.svcpage__detail a { grid-column: content-start / content-end; justify-self: start; margin-top: 1rem; }
		/* Two runs instead of four: the board rhythm survives, the crops stay readable. */
		.svcpage__gallery { grid-template-columns: 1.1fr 0.9fr; }
		.svcpage__gallery img { height: clamp(200px, 42vw, 300px); }
	}

	/* ---------- Projects: the wall ---------- */
	/* Same board logic as the services strips, scaled up: every run bleeds edge to edge,
	   the column widths inside a run are deliberately unequal, and the seam between any
	   two photos is constant, so the whole section reads as one laid surface rather than
	   a gallery of framed pictures. The run VARIANT changes down the page (3 wide, 2 tall,
	   4 short) which is what keeps a 28-photo wall from turning into wallpaper. */
	/* The whole page is wall: no paper margin above it, no trailing band below it, and the
	   two sections butt directly so the ground change IS the section break. */
	.work.services--page { padding-block: 0; }
	.work .svc-chapter { padding-block: 0; }

	/* The text tile: a cell of the wall that happens to hold words. Its copy sits on the
	   same 76rem rail as the rest of the site even though the cell itself bleeds to the
	   viewport edge, so the type never looks like it slid off the grid. */
	.wall__tile {
		display: flex;
		flex-direction: column;
		justify-content: center;
		gap: 1.1rem;
		padding: clamp(1.75rem, 3.5vw, 3rem) clamp(1.5rem, 3vw, 2.75rem);
		padding-inline-start: max(var(--gutter), (100vw - 76rem) / 2);
	}
	/* h1 and h2 used to share this rule, so 'The work.' read as one more caption
	   instead of the page arriving. h1 gets its own rung below. */
	.wall__tile h1,
	.wall__tile h2 {
		font-size: clamp(2.1rem, 4.6vw, 3.75rem);
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 0.95;
		color: var(--ink);
	}
	/* The page title has to outrank the section break inside the same page. Every other
	   page here holds about a 1.7x jump between the two; sharing one rule made it 1.0x. */
	.wall__tile h1 { font-size: clamp(2.6rem, 5.8vw, 5rem); }
	.wall__tile p {
		font-size: var(--step-0);
		line-height: 1.5;
		color: var(--body-ink);
		max-width: 34ch;
	}
	.svc-chapter--ink .wall__tile h1,
	.svc-chapter--ink .wall__tile h2 { color: var(--paper); }
	.svc-chapter--ink .wall__tile p { color: var(--paper-on-dark); }

	.wall__run {
		display: grid;
		gap: var(--seam);
		margin-top: var(--seam);
	}
	.wall__run:first-child { margin-top: 0; }
	.wall__run figure { margin: 0; min-width: 0; overflow: clip; }
	.wall__run img {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}
	/* grid-auto-rows, NOT height on the container: with an auto row the img's height:100%
	   has no definite basis, so it falls back to intrinsic height and spills past the run
	   by exactly the seam, painting over the gap between runs. */
	/* minmax, so a tile carrying real copy (the about page) grows the row instead of
	   overflowing it, while a one-line tile (projects) still gets the full height. */
	.wall__run--h3 { grid-template-columns: 1.35fr 0.9fr 0.75fr; grid-auto-rows: minmax(clamp(360px, 34vw, 560px), auto); }
	.wall__run--h4 { grid-template-columns: 1.3fr 0.62fr 0.56fr 0.52fr; grid-auto-rows: minmax(clamp(320px, 30vw, 500px), auto); }
	.wall__run--a { grid-template-columns: 1.25fr 0.85fr 1.05fr; grid-auto-rows: clamp(300px, 30vw, 500px); }
	.wall__run--b { grid-template-columns: 0.85fr 1.35fr; grid-auto-rows: clamp(340px, 36vw, 600px); }
	.wall__run--c { grid-template-columns: 1fr 0.75fr 1.1fr 0.85fr; grid-auto-rows: clamp(220px, 21vw, 350px); }
	.wall__run--d { grid-template-columns: 0.9fr 1.2fr 0.8fr; grid-auto-rows: clamp(280px, 27vw, 450px); }
	.wall__run--e { grid-template-columns: 1.3fr 0.8fr; grid-auto-rows: clamp(320px, 32vw, 530px); }

	@media (max-width: 860px) {
		/* Every run collapses to the same two columns: the wall keeps its seam and its
		   full bleed, and the varied heights still give it rhythm on a phone. */
		.wall__run--h3, .wall__run--h4,
		.wall__run--a, .wall__run--b, .wall__run--c, .wall__run--d, .wall__run--e {
			grid-auto-rows: auto;
		}
		/* Alternate which side is wide instead of collapsing every run to the same
		   1.15/0.85 pair. The desktop wall's whole argument is that the runs are edited
		   rather than dumped, and one repeated ratio down a phone is the wallpaper the
		   varied desktop shapes exist to avoid. */
		.wall__run--h3, .wall__run--a, .wall__run--d { grid-template-columns: 1.15fr 0.85fr; }
		.wall__run--h4, .wall__run--b, .wall__run--e { grid-template-columns: 0.85fr 1.15fr; }
		.wall__run--c { grid-template-columns: 1fr 1fr; }
		.wall__run img { height: clamp(150px, 34vw, 260px); }
		.wall__run--b img, .wall__run--e img { height: clamp(190px, 44vw, 330px); }
		/* The tile takes the full width and reads as a normal headline block; below it the
		   photos pair up as usual. */
		.wall__tile { grid-column: 1 / -1; padding-inline: var(--gutter); }
		/* Three photos in two columns leaves a hole. The odd one takes the full width
		   instead, which turns the leftover into the run's feature shot. */
		.wall__run--a figure:nth-child(3),
		.wall__run--h4 figure:nth-child(4),
		.wall__run--d figure:nth-child(3) { grid-column: 1 / -1; }
		.wall__run--a figure:nth-child(3) img,
		.wall__run--h4 figure:nth-child(4) img,
		.wall__run--d figure:nth-child(3) img { height: clamp(190px, 46vw, 340px); }
	}

	/* ---------- The album: the full gallery under the edited wall ----------
	   The wall above is deliberately uneven, because an edited selection has to
	   look edited. This is the opposite job: eighty-odd photos someone is
	   scanning for one that resembles their own yard, so every tile is the same
	   size and the eye can move fast. Same seam, same full bleed, so it still
	   reads as the same wall, just laid flat. auto-fill rather than a fixed
	   column count: the tile size is the constant and the count follows the
	   viewport, which is what keeps it working from a phone to a wide desktop
	   with no breakpoint to maintain. */
	.album { padding-block: clamp(4rem, 8vw, 7rem); }
	.album__head { margin-bottom: clamp(2rem, 4vw, 3.5rem); }
	/* Round 2 cut the grid, so this band is now heading + copy + button. Its bottom margin was
	   the gap to a grid that is no longer there, and the close band supplies its own top edge:
	   left in, it read as a dropped section rather than a deliberate stop. The .album__grid
	   rules below are now dead (nothing renders that class) and are kept only so restoring the
	   grid is one template edit rather than two; they match nothing, so they cost a parse and
	   change no pixel. */
	.album--link .album__head { margin-bottom: 0; }
	.album__cta { margin-top: clamp(1.75rem, 3vw, 2.5rem); }
	.album__grid {
		display: grid;
		grid-template-columns: repeat(auto-fill, minmax(clamp(150px, 19vw, 300px), 1fr));
		gap: var(--seam);
	}
	.album__grid figure { margin: 0; min-width: 0; overflow: clip; }
	.album__grid figure a { display: block; height: 100%; cursor: pointer; }
	.album__grid img {
		display: block;
		width: 100%;
		height: clamp(150px, 19vw, 300px);
		object-fit: cover;
		/* The tile is square-ish and the photos are a mix of landscape drone
		   frames and a few portrait crops, so pull the crop toward the middle
		   rather than the top, where a landscape frame is mostly sky. */
		object-position: 50% 55%;
	}
	/* Phones get three columns, not the two that auto-fill would pick at this tile
	   size. Eighty-odd photos two-up is forty-two rows of scrolling; three-up halves
	   that, and a 120px tile is still big enough to decide whether to tap it, which
	   is the only decision this grid asks for. */
	@media (max-width: 860px) {
		.album__grid { grid-template-columns: repeat(3, 1fr); }
		.album__grid img { height: clamp(105px, 30vw, 200px); }
	}

	/* ---------- About: few and large, the inverse of the projects wall ---------- */
	.about-page { background: var(--paper); }
	.about-lead {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 40rem) minmax(0, 36rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
		align-items: end;
		padding-block: clamp(3rem, 6vw, 5.5rem) clamp(2.5rem, 5vw, 4rem);
	}
	.about-lead h1 {
		grid-column: content-start / 3;
		font-size: clamp(2.6rem, 6.4vw, 5.25rem);
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 0.95;
		color: var(--ink);
	}
	.about-lead__body {
		grid-column: 3 / content-end;
		padding-bottom: 0.4rem;
		padding-left: clamp(0rem, 2vw, 2.5rem);
	}
	/* Reading size, not caption size: this is the only page whose argument is the words. */
	.about-lead__body p {
		font-size: var(--step-1);
		line-height: 1.5;
		color: var(--body-ink);
		max-width: 40ch;
	}
	.about-lead__body p + p { margin-top: 1rem; }

	.about-shot { margin: 0; }
	.about-shot a { display: block; cursor: pointer; overflow: clip; }
	.about-shot img {
		display: block;
		width: 100%;
		height: clamp(280px, 46vw, 680px);
		object-fit: cover;
	}
	.about-shot--last img { height: clamp(260px, 38vw, 560px); }

	/* The spec block: labels and values, no photographs. A builder reads a spec sheet, and
	   it is the one device on the site that is pure type. */
	.about-spec {
		background: var(--ink);
		padding-block: clamp(3rem, 6vw, 5rem);
	}
	.about-spec h2,
	.about-spec dl {
		width: min(76rem, 100% - var(--gutter) * 2);
		margin-inline: auto;
	}
	.about-spec h2 {
		font-size: clamp(1.9rem, 3.6vw, 3rem);
		font-weight: 700;
		letter-spacing: -0.025em;
		line-height: 1;
		color: var(--paper);
	}
	.about-spec dl {
		margin-top: clamp(2rem, 4vw, 3rem);
		display: grid;
		/* 20rem, not 16: six facts land as three-by-two instead of four-plus-an-orphan. */
		grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
		gap: clamp(1.75rem, 3.5vw, 3rem) clamp(1.5rem, 3vw, 3rem);
	}
	.about-spec dt {
		font-size: var(--step--1);
		font-weight: 600;
		letter-spacing: 0.2em;
		text-transform: uppercase;
		color: var(--concrete);
	}
	.about-spec dd {
		margin: 0.6rem 0 0;
		font-size: var(--step-1);
		font-weight: 600;
		line-height: 1.25;
		color: var(--paper);
	}

	@media (max-width: 860px) {
		.about-lead {
			grid-template-columns: [full-start] 0 [content-start] 1fr [content-end] 0 [full-end];
			padding-inline: var(--gutter);
		}
		.about-lead h1,
		.about-lead__body { grid-column: content-start / content-end; }
		.about-lead__body { padding: 1.25rem 0 0; }
	}

	/* ---------- Long-form page: the legal/prose fallback template ---------- */
	/* Anything that comes out of the WP editor lands here. Narrow measure, generous
	   leading, one type scale: this is the only page on the site nobody wants to look at,
	   so its whole job is being easy to read and easy to search. */
	.page-plain {
		background: var(--paper);
		padding-block: clamp(2.5rem, 5vw, 4.5rem) clamp(4rem, 8vw, 7rem);
		/* On the same rail as every other page, not centered in the viewport: the title
		   should start where the logo starts. */
		width: min(76rem, 100% - var(--gutter) * 2);
		margin-inline: auto;
	}
	.page-plain > article { max-width: 46rem; }
	/* The one page nobody wants to read, so measure matters most: 46rem of 18px ran
	   about 90 characters a line. A ch cap holds it near 68 no matter the font size. */
	.flow p,
	.flow li { max-width: 68ch; }
	.page-plain h1 {
		font-size: clamp(2.2rem, 5vw, 3.5rem);
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 1;
		color: var(--ink);
		margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
	}
	.flow h2 {
		margin-top: clamp(2.25rem, 4vw, 3rem);
		margin-bottom: 0.6rem;
		font-size: var(--step-2);
		font-weight: 700;
		letter-spacing: -0.02em;
		line-height: 1.1;
		color: var(--ink);
	}
	.flow h3 {
		margin-top: 1.75rem;
		margin-bottom: 0.4rem;
		font-size: var(--step-1);
		font-weight: 600;
		color: var(--ink);
	}
	.flow p,
	.flow li {
		font-size: var(--step-0);
		line-height: 1.65;
		color: var(--body-ink);
	}
	.flow p { margin-top: 0.9rem; }
	.flow ul { margin: 0.9rem 0 0; padding-left: 1.25rem; }
	.flow li { margin-top: 0.5rem; }
	.flow li::marker { color: var(--concrete); }
	.flow a {
		color: var(--evergreen);
		text-decoration: none;
		border-bottom: 2px solid rgba(47, 74, 56, 0.75);
		padding-bottom: 1px;
		transition: border-color 0.18s var(--ease-solid);
	}
	.flow a:hover { border-color: var(--evergreen); }
	.flow strong { font-weight: 600; color: var(--ink); }
	.flow code {
		font-family: inherit;
		font-weight: 600;
		background: var(--fog);
		padding: 0.05em 0.35em;
	}

	/* ---------- Photo viewer ---------- */
	/* The photo itself is the affordance: it lifts a hair and brightens on hover, which
	   is the whole hint a homeowner needs. No magnifier icon pinned to a corner, and
	   no zoom-in cursor either: the OS magnifier glyph is 2005-era chrome and reads
	   cheap. A clickable thing gets the pointer, same as every other link on the site. */
	.wall__run figure a,
	.svcpage__gallery figure a {
		display: block;
		height: 100%;
		cursor: pointer;
		/* The focus offset used to be set here at +3px and was clipped away by the figure.
		   It now lives with the :focus-visible rule below, at a negative offset. */
	}
	.wall__run img,
	.svcpage__gallery img {
		transition: transform 0.45s var(--ease-solid), filter 0.45s var(--ease-solid);
	}
	@media (hover: hover) {
		.wall__run figure a:hover img,
		.svcpage__gallery figure a:hover img {
			transform: scale(1.035);
			filter: brightness(1.06);
		}
	}
	/* The ring has to be drawn INSIDE the tile. Every photo figure on this site is
	   `overflow: clip`, and the global focus style offsets the outline +3px, i.e. into
	   exactly the band the clip removes: the ring was being painted and then thrown away,
	   so tabbing through a gallery showed nothing at all. A negative offset puts it just
	   inside the photo edge, where it survives the clip and reads against any image.
	   Applies to all three photo grids, not only the album added in round 1: the wall and
	   the service-page rows had the same defect, and a keyboard visitor on the projects
	   page meets all of them on one screen. */
	.wall__run figure a:focus-visible,
	.svcpage__gallery figure a:focus-visible,
	.album__grid figure a:focus-visible {
		outline: 3px solid var(--evergreen);
		outline-offset: -3px;
	}
	.svc-chapter--ink .wall__run figure a:focus-visible { outline-color: var(--paper); }

	html.is-zoomed { overflow: hidden; }
	.zoom {
		position: fixed;
		inset: 0;
		z-index: 100;
		background: rgba(35, 36, 31, 0.94);
		display: grid;
		place-items: center;
		padding: clamp(1rem, 4vw, 3rem);
	}
	.zoom[hidden] { display: none; }
	.zoom__frame {
		margin: 0;
		max-width: min(96vw, 1400px);
		display: flex;
		flex-direction: column;
		align-items: center;
		/* No `gap` here: the frame held an <img> + <figcaption> until the caption was removed, and
		   a gap with one child does nothing. Re-add it if a second child ever comes back. */
	}
	.zoom__frame img {
		display: block;
		max-width: 100%;
		/* Two caps, and the second one matters more than it looks. 68svh leaves room for the
		   controls row at ordinary window shapes. But the photo is CENTRED in the full viewport
		   height while the row is pinned to the bottom, so the two are measured off different
		   origins: on a short viewport the centred photo grows down into the row. Because the
		   photo is centred, clearing a row that occupies the bottom ~5rem costs TWICE that in
		   height, hence 10rem. Verified no collision at 800x360, 740x400, 932x430 (phone
		   landscape), 1024x600, 1280x720 and 412x915. */
		max-height: min(68svh, calc(100svh - 10rem));
		width: auto;
		height: auto;
		object-fit: contain;
	}
	.zoom__btn {
		position: absolute;
		border: 0;
		background: rgba(244, 241, 234, 0.14);
		border: 1px solid rgba(244, 241, 234, 0.4);
		color: var(--paper);
		font-family: inherit;
		font-size: 1.6rem;
		line-height: 1;
		width: 3rem;
		height: 3rem;
		cursor: pointer;
		transition: background-color 0.18s var(--ease-solid);
	}
	.zoom__btn:hover { background: rgba(244, 241, 234, 0.3); }
	.zoom__btn:focus-visible { outline: 3px solid var(--paper); outline-offset: 2px; }
	.zoom__close { top: clamp(0.75rem, 2vw, 1.5rem); right: clamp(0.75rem, 2vw, 1.5rem); }
	.zoom__count {
		margin: 0;
		font-size: var(--step--1);
		letter-spacing: 0.16em;
		color: var(--concrete);
	}
	/* prev / count / next sit in a row UNDER the photo at EVERY width.
	   They were edge-pinned above 700px on the assumption that a wide screen leaves dark margin
	   beside the photo for them. Measured, that assumption only holds once the image stops being
	   width-bound and starts being capped by its max-height, which is around 1290px. So 768px to
	   ~1250px, every tablet and a Z Fold unfolded, still had the arrows sitting ON the photo:
	   exactly the bug the row was added to fix. Rather than chase a breakpoint that depends on
	   viewport height and each photo's aspect ratio, the row is now unconditional. Verified with
	   a real gallery photo at 420/768/884/984/1200/1400/1600/1920: zero overlap at any width. */
	.zoom__nav {
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 1.25rem;
		position: absolute;
		left: 0;
		right: 0;
		bottom: clamp(0.75rem, 2vw, 1.5rem);
	}
	/* .zoom__btn is position:absolute for the close button's corner placement. The two arrows are
	   flex items of the row instead, so they have to opt back out or they both collapse onto the
	   same static position and stack on top of each other. */
	.zoom__nav .zoom__btn,
	.zoom__nav .zoom__count {
		position: static;
		transform: none;
	}
	@media (prefers-reduced-motion: reduce) {
		.wall__run img, .svcpage__gallery img, .gal__tile img { transition: none; }
		.wall__run figure a:hover img,
		.svcpage__gallery figure a:hover img,
		.gal__tile a:hover img { transform: none; }
	}

	/* ---------- Contact: the landing for every Get-a-quote ---------- */
	.contact { padding-block: clamp(3rem, 6vw, 5rem) clamp(4rem, 8vw, 7rem); }
	.contact__head,
	.contact__inner {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 46rem) minmax(0, 30rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
	}
	.contact__head h1 {
		grid-column: content-start / 3;
		font-size: clamp(2.6rem, 5.4vw, 4.75rem);
		font-weight: 700;
		letter-spacing: -0.03em;
		line-height: 0.98;
	}
	.contact__head p {
		grid-column: content-start / 3;
		margin-top: 1.25rem;
		font-size: var(--step-1);
		line-height: 1.45;
		color: var(--body-ink);
		max-width: 44ch;
	}
	.contact__inner { margin-top: clamp(2.5rem, 5vw, 4rem); align-items: start; }
	.contact__form { grid-column: content-start / 3; padding-right: clamp(1.5rem, 4vw, 4.5rem); }
	.contact__hp { position: absolute; left: -9999px; }
	.contact__grid {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: 1.5rem clamp(1rem, 2vw, 2rem);
	}
	.contact__field { display: grid; gap: 0.5rem; }
	.contact__field--wide { grid-column: 1 / -1; }
	.contact__field label {
		font-size: var(--step--1);
		font-weight: 600;
		color: var(--ink);
	}
	/* Slate, not concrete. Concrete on paper measures 2.69:1: under the 4.5:1 floor for
	   the "optional" tags and under the 3:1 floor for a field border, and this is the one
	   form on the site. Slate reads 5.4:1 and still sits lighter than the ink labels. */
	.contact__field label span { color: var(--slate); font-weight: 500; }
	.contact__field input,
	.contact__field textarea {
		font: inherit;
		color: var(--ink);
		background: var(--paper);
		border: 1px solid var(--slate);
		padding: 0.8em 0.9em;
		border-radius: 0;
		width: 100%;
	}
	.contact__field input::placeholder,
	.contact__field textarea::placeholder { color: var(--slate); opacity: 1; }
	.contact__field input:focus-visible,
	.contact__field textarea:focus-visible {
		outline: 3px solid var(--evergreen);
		outline-offset: 0;
		border-color: var(--evergreen);
	}
	.contact__form .button { margin-top: 2rem; }
	.contact__error {
		margin-bottom: 1.5rem;
		padding: 0.9em 1em;
		background: var(--fog);
		border-left: 4px solid var(--evergreen);
		font-weight: 600;
		color: var(--ink);
	}
	.contact__sent h2 {
		font-size: clamp(2.2rem, 4vw, 3.5rem);
		letter-spacing: -0.03em;
	}
	.contact__sent p {
		margin-top: 1rem;
		font-size: var(--step-1);
		line-height: 1.5;
		max-width: 40ch;
	}
	.contact__sent a { color: var(--evergreen); font-weight: 700; }

	.contact__rail {
		grid-column: 3 / content-end;
		display: grid;
		gap: clamp(2rem, 4vw, 3rem);
		background: var(--fog);
		padding: clamp(1.75rem, 3vw, 2.5rem);
	}
	.contact__block h2 {
		font-size: var(--step-1);
		font-weight: 700;
		letter-spacing: -0.02em;
	}
	.contact__phone {
		display: inline-block;
		margin-top: 0.5rem;
		font-size: clamp(1.75rem, 3vw, 2.5rem);
		font-weight: 700;
		letter-spacing: -0.025em;
		font-variant-numeric: proportional-nums;
		color: var(--evergreen);
		text-decoration: none;
		border-bottom: 2px solid rgba(47, 74, 56, 0.75);
		padding-bottom: 2px;
	}
	.contact__phone:hover { border-color: var(--evergreen); }
	.contact__block p { margin-top: 0.6rem; line-height: 1.55; color: var(--body-ink); }
	.contact__mail {
		display: inline-block;
		margin-top: 0.5rem;
		font-weight: 600;
		color: var(--evergreen);
		text-decoration: none;
		border-bottom: 2px solid rgba(47, 74, 56, 0.75);
		padding-bottom: 2px;
		overflow-wrap: anywhere;
	}
	.contact__mail:hover { border-color: var(--evergreen); }
	.contact__map { margin-top: 1.25rem; }
	.contact__map iframe {
		width: 100%;
		/* Shorter than 4/3: the rail used to run 365px past the bottom of the form,
		   leaving a dead field of paper beside it on the one page that has to look
		   buttoned-up. */
		aspect-ratio: 16 / 10;
		border: 0;
		display: block;
	}

	@media (max-width: 860px) {
		.contact__head h1, .contact__head p,
		.contact__form, .contact__rail { grid-column: content-start / content-end; }
		.contact__form { padding-right: 0; }
		.contact__grid { grid-template-columns: 1fr; }
		.contact__rail { margin-top: 3rem; }
	}

	/* ---------- Footer: bare close-out ---------- */
	.site-footer {
		background: var(--ink);
		color: var(--paper-on-dark);
		padding-block: clamp(3rem, 6vw, 5rem) clamp(1.75rem, 3vw, 2.5rem);
	}
	.site-footer__inner {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 46rem) minmax(0, 30rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
		row-gap: 2.5rem;
	}
	.site-footer__id { grid-column: content-start / 3; }
	.site-footer__id img { height: 64px; width: auto; }
	.site-footer__area {
		margin-top: 1.5rem;
		font-size: var(--step--1);
		font-weight: 600;
		letter-spacing: 0.22em;   /* matches the hero eyebrow: identical text, identical set */
		text-transform: uppercase;
		color: var(--concrete);
	}
	.site-footer__reach {
		margin-top: 1rem;
		font-style: normal;
		line-height: 1.9;
	}
	.site-footer__reach a {
		color: var(--paper);
		text-decoration: none;
		border-bottom: 1px solid rgba(244, 241, 234, 0.35);
		transition: border-color 0.18s var(--ease-solid);
	}
	.site-footer__reach a:hover { border-color: var(--paper); }
	.site-footer__nav { grid-column: 3 / content-end; justify-self: end; }
	.site-footer__nav ul { list-style: none; columns: 2; column-gap: clamp(2rem, 4vw, 4rem); }
	.site-footer__nav li { margin-bottom: 0.75rem; break-inside: avoid; }
	.site-footer__nav a {
		color: var(--paper);
		text-decoration: none;
		border-bottom: 1px solid transparent;
		transition: border-color 0.18s var(--ease-solid);
	}
	.site-footer__nav a:hover { border-color: var(--paper); }
	.site-footer__legal {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 56rem) minmax(0, 20rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
		margin-top: clamp(2.5rem, 5vw, 4rem);
		padding-top: 1.5rem;
		border-top: 1px solid rgba(244, 241, 234, 0.14);
		font-size: var(--step--1);
		color: var(--concrete);
	}
	.site-footer__legal p { grid-column: content-start / 3; }
	.site-footer__legal a {
		grid-column: 3 / content-end;
		justify-self: end;
		color: var(--concrete);
		text-decoration: none;
		border-bottom: 1px solid rgba(151, 148, 139, 0.4);
		transition: border-color 0.18s var(--ease-solid), color 0.18s var(--ease-solid);
	}
	.site-footer__legal a:hover { color: var(--paper); border-color: var(--paper); }
	/* ---------- Mobile action bar ---------- */
	.action-bar { display: none; }
	@media (max-width: 860px) {
		.action-bar {
			position: fixed;
			inset: auto 0 0 0;
			z-index: 20;
			display: grid;
			grid-template-columns: 1.2fr 1fr;
			border-top: 1px solid rgba(35, 36, 31, 0.2);
			background: var(--paper);
		}
		/* The bar's height must NOT be derived from env(safe-area-inset-bottom). Firefox on
		   Android reports a nonzero bottom inset while its toolbar is on screen and zero once
		   it hides, so anything sized from the inset visibly grows and shrinks as you scroll.
		   Height is therefore constant, and the strip below the bar is covered by the ::after
		   overhang below rather than by padding. */
		.action-bar a {
			position: relative;
			display: grid;
			place-items: center;
			min-height: 3.25rem;
			font-weight: 600;
			/* Label only, matched to the site-wide button bump. The bar's HEIGHT stays
			   constant on purpose: see the note above. */
			font-size: 1.0625rem;
			letter-spacing: 0.06em;
			text-transform: uppercase;
			text-decoration: none;
		}
		/* Each half paints its own colour below the bar. Whatever sits down there (the Android
		   gesture-pill strip, or a gap opened mid-animation by a browser toolbar) shows that
		   half's colour instead of the page background, which is what produced the cream band
		   under the evergreen CALL half. The overhang is inside a position:fixed element, so
		   it does not extend the document's scrollable area. */
		.action-bar a::after {
			content: "";
			position: absolute;
			top: 100%;
			left: 0;
			right: 0;
			height: 4rem;
			background: inherit;
		}
		.action-bar__call { background: var(--evergreen); color: var(--paper); }
		.action-bar__quote { background: var(--paper); color: var(--evergreen); }
		body { padding-bottom: 3.25rem; }
		/* iOS is the one place the inset is both real and stable: the home indicator genuinely
		   overlays the viewport, and the value does not flap with browser chrome. Reserve it
		   there only. The probe matches WebKit, and desktop Safari reports a 0 inset, so this
		   is a no-op everywhere it should not apply. */
		@supports (-webkit-touch-callout: none) {
			.action-bar a {
				min-height: calc(3.25rem + env(safe-area-inset-bottom));
				padding-bottom: env(safe-area-inset-bottom);
			}
			body { padding-bottom: calc(3.25rem + env(safe-area-inset-bottom)); }
		}
	}

	/* ---------- Gallery page (every photo, searchable) ---------- */
	/* Justified rows. Each row's height is solved in JS so the row fills the width exactly;
	   the tile widths come with it. CSS only has to not fight that: no flex-grow, no
	   stretching, because a photo that gets stretched has lost its real proportions and the
	   whole reason for this layout was that this pool is 87.5% landscape and 0.2% square. */
	/* The same gutter-pinned grid .svc-head and .contact__head use, NOT max-width + auto
	   margins. Those two mechanisms agree until the viewport passes the 76rem content cap and
	   then diverge: centring puts the gutter OUTSIDE the centred box, so the padding stacks on
	   top of it. Measured at 1920px, this heading sat 72px right of every other page's h1, and
	   the title visibly jumped when navigating Services -> Every photo. */
	.gal__head {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--gutter), 1fr)
			[content-start] minmax(0, 38rem) minmax(0, 38rem) [content-end]
			minmax(var(--gutter), 1fr) [full-end];
		padding-block: clamp(2.5rem, 6vw, 4.5rem) clamp(1.25rem, 3vw, 2rem);
	}
	.gal__head > * { grid-column: content-start / content-end; }
	.gal__head h1 { font-size: clamp(2.4rem, 6vw, 4.5rem); letter-spacing: -0.025em; line-height: 1; margin: 0 0 0.75rem; }
	.gal__head p { font-size: var(--step-1); line-height: 1.5; color: var(--slate); max-width: 46ch; margin: 0; }

	/* Sticky, because changing your mind 900 photos down is the normal case here. It sits
	   under the header rather than at 0, or it would cover the header on the widths where
	   that is sticky too. */
	.gal__searchwrap {
		position: sticky;
		top: 0;
		z-index: 9;
		background: var(--paper);
		border-block: 1px solid rgba(35, 36, 31, 0.14);
	}
	@media (min-width: 861px) { .gal__searchwrap { top: var(--header-h); } }
	.gal__search {
		max-width: 76rem;
		margin-inline: auto;
		padding: 0.85rem var(--gutter);
		display: flex;
		gap: 1rem;
		align-items: center;
	}
	.gal__search input {
		flex: 1;
		min-width: 0;
		font: inherit;
		font-size: 1.0625rem;
		padding: 0.75em 1em;
		color: var(--ink);
		background: var(--paper);
		/* Slate, not Concrete: Concrete #97948B on Paper #F4F1EA measures about 2.2:1, under
		   the 3:1 WCAG 1.4.11 floor for a control boundary. Slate clears it comfortably. */
		border: 2px solid var(--slate);
		border-radius: 0;
		-webkit-appearance: none;
	}
	/* Same ring as the contact form's inputs and as this page's own tiles and back-to-top
	   button. Dropping the outline for a 2px border-colour shift made the one text input on
	   this page the weakest focus indicator on the site. */
	.gal__search input:focus-visible {
		outline: 3px solid var(--evergreen);
		outline-offset: 0;
		border-color: var(--evergreen);
	}
	.gal__count {
		margin: 0;
		font-size: var(--step--1);
		font-weight: 600;
		letter-spacing: 0.14em;
		text-transform: uppercase;
		color: var(--slate);
		white-space: nowrap;
	}
	@media (max-width: 560px) {
		.gal__search { flex-wrap: wrap; gap: 0.5rem; }
		.gal__count { width: 100%; }
	}
	/* A phone in landscape is under the 860px mobile breakpoint AND short, which is the one
	   shape where the header, this sticky bar and the fixed action bar all compete for the
	   same 375px of height: measured at 666x375 the action bar covered the bottom 27px of the
	   search input and all of the result count, with no photograph visible at all. Every width
	   in the original sweep was portrait-shaped, so none of them found it. Below 480px tall
	   the bar stops sticking and scrolls away with the page, which gives the wall the height
	   back and leaves the search reachable by scrolling up. */
	@media (max-width: 860px) and (max-height: 480px) {
		.gal__searchwrap { position: static; }
		.gal__search { padding-block: 0.5rem; }
		.gal__head { padding-block: 1.25rem 0.75rem; }
	}

	.gal__wrap { padding: 6px var(--gutter) clamp(3rem, 8vw, 6rem); }
	.gal__rows { --seam: 6px; max-width: 100rem; margin-inline: auto; }
	.gal__row { display: flex; gap: var(--seam); margin-bottom: var(--seam); }
	.gal__tile { margin: 0; flex: 0 0 auto; overflow: hidden; background: var(--fog); }
	.gal__tile a { display: block; width: 100%; height: 100%; }
	.gal__tile img { display: block; width: 100%; height: 100%; object-fit: cover; }
	/* Inward, like every other photo figure on this site: the tile is overflow:hidden, so an
	   outward offset is painted into the clipped band and thrown away. */
	.gal__tile a:focus-visible { outline: 3px solid var(--evergreen); outline-offset: -3px; }
	/* The site's established "this photo is clickable" gesture, same scale and easing as the
	   work wall and the service galleries. Its reduced-motion off-switch lives with theirs. */
	.gal__tile img { transition: transform 0.35s var(--ease-solid); }
	.gal__tile a:hover img { transform: scale(1.035); }
	.gal__empty { max-width: 76rem; margin: 2rem auto; padding-inline: var(--gutter); color: var(--slate); font-size: var(--step-1); }
	.gal__empty em { color: var(--ink); font-style: normal; font-weight: 600; }

	/* Bottom-right, clear of the phone action bar. That bar is a deliberate constant height
	   (an inset-derived one grew and shrank while scrolling on Android Firefox), so this is a
	   known offset rather than a guess. */
	.gal__totop {
		position: fixed;
		right: clamp(0.85rem, 2vw, 1.5rem);
		bottom: clamp(0.85rem, 2vw, 1.5rem);
		z-index: 8;
		width: 3rem;
		height: 3rem;
		border: 1px solid rgba(244, 241, 234, 0.4);
		background: var(--ink);
		color: var(--paper);
		font-size: 1.3rem;
		line-height: 1;
		cursor: pointer;
		transition: background-color 0.18s var(--ease-solid);
	}
	.gal__totop:hover { background: var(--evergreen); }
	/* Inward. An outward ring is painted over whatever photograph happens to sit under the
	   button, so its contrast is uncontrolled; inside the ink button it is a known pairing. */
	.gal__totop:focus-visible { outline: 3px solid var(--paper); outline-offset: -5px; }
	.gal__totop[hidden] { display: none; }
	@media (max-width: 860px) {
		/* 3.25rem is the action bar's own constant height, the same literal the bar uses;
		   it is not derived from the safe-area inset for the reason written above it. On iOS
		   the bar reserves the inset as well, so match that or the arrow sits on the CALL
		   half on an iPhone. */
		.gal__totop { bottom: calc(3.25rem + 0.85rem); }
		@supports (-webkit-touch-callout: none) {
			.gal__totop { bottom: calc(3.25rem + env(safe-area-inset-bottom) + 0.85rem); }
		}
	}

	@media (max-width: 860px) {
		.site-footer__inner { grid-template-columns: 1fr; padding-inline: var(--gutter); }
		.site-footer__id, .site-footer__nav { grid-column: 1; justify-self: start; }
		.site-footer__legal { grid-template-columns: 1fr; padding-inline: var(--gutter); row-gap: 0.75rem; }
		.site-footer__legal p, .site-footer__legal a { grid-column: 1; justify-self: start; }
	}
}
