/* ==========================================================================
   The Jenkins Group — design tokens + component styles.
   --accent / --r-hero / --r-card / --r-form are set inline by functions.php
   (jg_theme_token_css) from Site Settings; everything else below is fixed,
   per the approved Chamfered design.
   ========================================================================== */

:root {
	/* Paper twice: as channels and as a colour. Every surface that needs paper
	   at partial alpha — the header's blur, the status pill, the hero pause,
	   the scroller's edge fades, the lightbox chrome — used to restate the
	   literal, which meant eight rules to find and one to miss. Channels are
	   the form a custom property has to take to be usable inside an alpha,
	   and `rgb()` with a slash carries much wider support than `color-mix()`,
	   which matters for the same federal audience the WebP fallback exists for. */
	--paper-rgb: 244 242 237;
	--paper: rgb(var(--paper-rgb));
	--ink: #211f1b;
	--ink-on-dark: #fdfcf9;
	--body-muted: #4a463f;
	--muted: #615d55;
	/* Retuned for WCAG 1.4.3: at #8c887f / #a8a399 these measured 3.16:1 and
	   2.24:1 on paper while carrying 9.5-13px text (inactive nav, the wordmark
	   descriptor, the copyright line). Solved against `card` — the darkest
	   ground either sits on — so both clear 4.5:1 on every surface. */
	--faint: #66635d;
	--fainter: #6e6a64;
	--card: #efece4;
	--white: #ffffff;
	--img-placeholder: #d9d5cc;
	--accent-on-dark: #b79e84;
	/* The one red on the site. It was already here as a literal, as the text
	   colour of the contact form's error notice; naming it stopped the field-level
	   error messages inventing a second one. Deep and slightly warm so it belongs
	   to this palette rather than arriving from a framework, and dark enough to
	   clear AA as small text on paper, on card and on the notice's own tint. */
	--error-ink: #7a2020;
	/* Deeper than `ink` on purpose: the value that clears 4.5:1 against the
	   brightest pixel behind the caption across all five hero slides. Solved on
	   canvas, not guessed, and named so the next scrim cannot invent a third. */
	--hairline: rgba(0, 0, 0, 0.08);
	--hairline-soft: rgba(0, 0, 0, 0.06);
	--ease: cubic-bezier(0.2, 0.7, 0.2, 1);
	/* Height of the sticky header, which is content-driven — a clamped
	   wordmark, a nav that wraps — so no single number is right at every
	   width. `initHeaderHeight` measures the real element and writes this
	   property, which is why three separate offsets could read 96px and 108px
	   against a header that ranges from 76px to 164px and nobody noticed. The
	   values declared here and per breakpoint are the fallback if that never
	   runs: an anchor then lands a little high or low, never behind the header. */
	--header-h: 88px;
}

* { box-sizing: border-box; }
/* Every in-page anchor clears the header, once, on the scroll container —
   rather than each target remembering to hold a `scroll-margin-top`. The
   Work page's sector deep-links (About's What-We-Do grid links straight to
   /work/#sector-0X) landed 68px behind a 164px mobile header because the
   margin they carried was a fixed 96px measured against the desktop one. */
html { scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + 16px); }
body {
	margin: 0;
	background: var(--paper);
	color: var(--ink);
	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
	-webkit-font-smoothing: antialiased;
}
img { display: block; max-width: 100%; }
a { color: inherit; }
button { font-family: inherit; }

/* ===== Focus & browser surfaces =====
   Focus rings, text selection and placeholders otherwise ship with browser
   defaults that belong to no design system. Themed from the palette so the
   parts we didn't draw still read as part of it. Accent on paper is 5.12:1,
   comfortably past the 3:1 that WCAG 1.4.11 asks of a focus indicator. */
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 3px;
}
/* Controls sitting on photography or the lightbox scrim need the light ring. */
:where(.hero-dot, .hero-pause, .jg-lightbox-close, .jg-lightbox-nav, .jg-lightbox-thumb):focus-visible {
	outline-color: var(--ink-on-dark);
}
/* Image wells clip their own overflow, so an outset ring would be cut off. */
.jg-zoom:focus-visible { outline-offset: -3px; }

/* Skip link (WCAG 2.4.1). The header carries five destinations before the
   content starts, and a share of this site's visitors are federal evaluators on
   locked-down machines where keyboard operation is the only option. Off-screen
   until focused rather than display:none, so it stays in the tab order. */
.skip-link {
	position: absolute; left: 16px; top: -100px; z-index: 100;
	padding: 12px 20px; border-radius: var(--r-form);
	background: var(--ink); color: var(--paper);
	font-size: 12px; letter-spacing: 1.6px; text-transform: uppercase; text-decoration: none;
	transition: top .2s var(--ease);
}
/* Plain :focus, not :focus-visible. `:focus-visible` is a heuristic that does
   not reliably match programmatic or assistive focus, and a skip link that
   fails to appear is worse than useless. There is no cost to the mouse user:
   a link cannot be focused by pointer without also being activated. */
.skip-link:focus { top: 16px; }
.skip-link:focus-visible { outline-color: var(--ink-on-dark); }
@media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }

::selection { background: var(--accent); color: var(--paper); }
::placeholder { color: var(--muted); opacity: 1; }

/* padding-left/right are !important because many section classes below share
   .container on the same element and declare their OWN `padding` shorthand
   for vertical spacing (e.g. `padding: 56px 0`) — that shorthand always sets
   all 4 sides, so without !important here it silently zeroes .container's
   horizontal inset. This keeps every section's content flush with a single
   32px (20px on mobile) gutter regardless of what vertical padding it adds. */
.container { max-width: 1280px; margin: 0 auto; padding-top: 0; padding-bottom: 0; padding-left: 32px !important; padding-right: 32px !important; }
.section { padding: 64px 0; }
.section-tight { padding: 56px 0; }
.eyebrow { margin: 0 0 18px; font-size: 12px; font-weight: 400; letter-spacing: 3px; text-transform: uppercase; color: var(--accent); }
/* Inside the dot row the eyebrow's own bottom margin has no job: the row is a
   baseline flex and carries the spacing itself. Four call sites were each
   cancelling it with an inline `style="margin:0"`, which is the component
   asking every caller to remember a detail it can settle once. */
.eyebrow-dot-row { display: flex; align-items: baseline; gap: 14px; margin-bottom: 10px; }
.eyebrow-dot-row .eyebrow { margin: 0; }
/* In the gallery head the row is one half of a flex pair with the shot
   count, which owns the alignment; the row's own bottom margin would push
   the two out of line. Last of the five inline overrides on this component. */
.gallery-head .eyebrow-dot-row { margin-bottom: 0; }
.eyebrow-dot { width: 7px; height: 7px; border-radius: 999px; background: var(--accent); display: inline-block; }

@keyframes jgFade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
@keyframes jgKen { from { transform: scale(1.02); } to { transform: scale(1.1); } }
/* jgFade's opacity-only twin, swapped in under reduced motion: the fade still
   marks the change, the 6px rise does not. */
@keyframes jgFadeFlat { from { opacity: 0; } to { opacity: 1; } }

/* ============================ Evidence band =============================
   The page's one dense moment, and deliberately so: after six generous image
   plates, a band of tight factual typography. That density inversion IS the
   idea -- DESIGN.md says the system reads as confident because it withholds,
   and the critique's charge was that it withheld the wrong thing. This band
   withholds decoration and spends everything on fact.

   Paper ground with a hairline top and bottom, no fill: a card or ink panel
   would read as a promotional callout, and the band's job is to read as
   record. It also keeps the ink testimonial band below as the page's single
   tonal event. No motion and no hover anywhere in here -- hovering facts
   cheapens them.
   ========================================================================== */
/* Top rule only. It used to carry a bottom rule too, which sat directly above
   the next section's own top rule and drew the page's only double hairline. */
.evidence { padding: 72px 0 76px; border-top: 1px solid var(--hairline); }
/* The clamp matches its peers on this page -- `.story-row h2` and `.whatwedo
   h2` -- so the section doing the most corroboration work is not also the
   page's quietest heading. The accent span carries the old "Work that travels"
   line, which is why the two blocks below read as one index rather than two
   sections. The intro owns the gap down to the band, so the head only needs
   the space to its standfirst. */
.evidence-head { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(26px, 2.8vw, 36px); letter-spacing: -0.3px; color: var(--ink); }
.evidence-intro { max-width: 60ch; }
.evidence-standfirst { margin: 16px 0 0; font-size: 16px; line-height: 1.6; color: var(--muted); }

/* Three zones on two tracks. The globe is placed in the top right and spans the
   intro's row and the credentials' -- the corner the eye reaches after the
   headline, and a plate that tall keeps the credentials from reading as a lone
   tower of text. The credentials tuck under the intro in the left track; the
   geography runs the full width beneath both, so the globe's height is absorbed
   by the band rather than left as dead field. Explicit placement rather than
   flow: it is the only way to get an L-shaped wrap around a corner plate.

   Two tracks rather than a 12-column field because the globe wanted a width
   between the sixth and seventh column, and on a twelve-track grid every extra
   column also buys 40px of gap. The ratio is set by the credentials: 1.3 leaves
   them ~240px a column, which is where the longest agency name still sets in
   two lines. */
.evidence-band { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); gap: 52px 40px; align-items: start; }
.evidence-intro { grid-column: 1; grid-row: 1; }
.evidence-plate { grid-column: 2; grid-row: 1 / span 2; }
.evidence-credentials { grid-column: 1; grid-row: 2; }
.evidence-geo { grid-column: 1 / -1; grid-row: 3; }

/* One object, six times. Accent label, hairline above, content beneath --
   agencies, clients, regions and the portfolio all take it, so the band reads
   as a single index instead of a credentials block meeting a geography block. */
/* Stacked, not two-up. Side by side these columns fell to ~236px and all six
   agency lines wrapped -- "NAVFAC — Naval Facilities Engineering Systems
   Command" is 52 characters and needs ~350px to set on one line. Full track
   width, and nothing in the list wraps. The two lists then carry their own
   internal columns, because their item lengths are nothing alike. */
.evidence-credentials { display: grid; grid-template-columns: 1fr; gap: 40px; align-items: start; }
/* Four across, because these are short runs and a two-up would leave the band
   bottom-heavy under a full-width rule. */
.evidence-geo { display: grid; grid-template-columns: repeat(4, 1fr); gap: 34px 40px; align-items: start; }
.evidence-credentials > *, .evidence-geo > * { min-width: 0; }
/* Every group carries its own rule, so the groups in a row line their rules up
   into one continuous hairline across the band. That reads as a ledger ruled
   by row, which is the point -- a vertical divider between two columns would
   say "two lists side by side" again. */
.evidence-group { padding-top: 20px; border-top: 1px solid var(--hairline); }
.evidence-label { margin: 0 0 16px; font-size: 12px; font-weight: 400; letter-spacing: 3px; text-transform: uppercase; color: var(--accent); }
/* The geography's counterpart to `.evidence-list`: place names set as a run
   rather than one per line, because a column of single words is a ladder and
   these are read as a sweep. */
.evidence-run { margin: 0; font-size: 14px; line-height: 1.75; color: var(--body-muted); }

.evidence-list { margin: 0; padding: 0; list-style: none; }
/* Client names are one or two short words; twelve of them in a single column
   is a ladder with 300px of dead space to the right of every rung. */
.evidence-list.is-2up { display: grid; grid-template-columns: 1fr 1fr; gap: 0 32px; align-content: start; }
.evidence-list li { margin: 0 0 9px; font-size: 14px; line-height: 1.5; color: var(--body-muted); }
.evidence-list a { color: inherit; text-decoration: none; border-bottom: 1px solid rgba(0,0,0,0.18); }
.evidence-list a:hover { color: var(--accent); border-color: var(--accent); }
/* Square, because the thing inside it is: the globe sizes itself off the
   smaller of its box's two dimensions, so any other ratio is slack the
   drawing never uses. */
.reach-globe { width: 100%; aspect-ratio: 1; border-radius: var(--r-hero); overflow: hidden; background: var(--paper); }
/* The date and provenance the globe cannot state for itself. Set at the caption
   weight the section's labels use rather than as body copy: it qualifies the
   picture above it, it does not add an argument of its own. */
/* Centred under the plate, and the measure is capped and auto-centred rather
   than left-aligned: a caption sitting under a circle wants to share its axis,
   and a 46ch line ragged-left under a 665px sphere reads as a stray paragraph. */
.reach-note {
	margin: 16px auto 0; max-width: 44ch; text-align: center;
	font-size: 12px; line-height: 1.6; color: var(--faint);
	text-wrap: balance;
}
/* The globe in words. A native <details> because this is exactly what one is
   for, and because it works with no JavaScript at all -- the names stay on the
   page and in the document for anything that reads it, which is the whole
   argument for moving them out of the canvas. Closed by default: 154 names open
   would outweigh the credentials they sit beside. */
.reach-list { margin: 18px auto 0; max-width: 46ch; }
.reach-list summary {
	display: flex; align-items: center; justify-content: center; gap: 7px;
	padding: 7px 0; cursor: pointer; list-style: none;
	font-size: 12px; letter-spacing: 1.4px; text-transform: uppercase;
	color: var(--accent);
}
.reach-list summary::-webkit-details-marker { display: none; }
.reach-list summary:hover { color: var(--ink); }
.reach-list-icon { display: grid; place-items: center; transition: transform .22s var(--ease); }
.reach-list[open] .reach-list-icon { transform: rotate(180deg); }
.reach-list-body { padding-top: 6px; border-top: 1px solid var(--hairline-soft); }
.reach-list-label {
	margin: 16px 0 6px; font-size: 11px; font-weight: 400;
	letter-spacing: 2px; text-transform: uppercase; color: var(--faint);
	display: flex; gap: 8px; justify-content: center;
}
.reach-list-label span { color: var(--accent); }
/* A run, not a column: these are read as a sweep, and 139 names one per line
   would be a ladder taller than the globe it describes. */
.reach-list-run { margin: 0; font-size: 12px; line-height: 1.75; color: var(--body-muted); text-align: center; }
@media (prefers-reduced-motion: reduce) { .reach-list-icon { transition: none; } }

/* ============================== Header / Footer ========================= */
.site-header {
	position: sticky; top: 0; z-index: 50;
	background: rgb(var(--paper-rgb) / 0.82);
	backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
	border-bottom: 1px solid var(--hairline-soft);
}
.nav-row {
	max-width: 1280px; margin: 0 auto; padding: 18px 32px;
	display: flex; align-items: center; justify-content: space-between;
}
.wordmark { display: flex; flex-direction: column; line-height: 1; text-decoration: none; color: var(--ink); }
/* 4.2vw reaches the 37px cap at 881px, so every width at or above the 880px
   breakpoint renders the masthead at its documented size with no step, while
   561-880px scales down instead of wrapping. At 37px fixed, the lockup broke
   into two colliding lines at 720px -- which is 1440px at 200% zoom, exactly
   WCAG 1.4.4's test case. */
.wordmark-name { font-family: "Newsreader", Georgia, serif; font-size: clamp(24px, 4.2vw, 37px); letter-spacing: 0.2px; }
/* 11px is the site's own functional-text floor; this sat at 9.5px, under it,
   on every page. The header is the one place that floor was never applied.
   `text-wrap: balance` is the safety net the raise needs -- see the 320-400px
   note below -- and is a no-op everywhere the line already fits on one row. */
.wordmark-kicker { font-size: 11px; letter-spacing: 3px; text-transform: uppercase; color: var(--faint); margin-top: 5px; text-wrap: balance; }
/* A continuous gap, so the nav narrows through 561-880px instead of wrapping
   to a second row the moment it stops fitting. 34px is held from 1308px up;
   the 16px floor keeps the labels from colliding at the bottom of the band. */
.nav-links { display: flex; gap: clamp(16px, 2.6vw, 34px); align-items: center; }
.nav-link {
	text-decoration: none; padding: 6px 0; font-size: 12px; letter-spacing: 1.6px;
	text-transform: uppercase; color: var(--faint); font-weight: 400;
}
.nav-link.is-active { color: var(--accent); font-weight: 700; }
.nav-ftp { display: flex; align-items: center; gap: 5px; color: var(--faint); }
.nav-ftp:hover, .footer-ftp:hover { color: var(--ink); }
.nav-ftp-arrow { font-size: 11px; line-height: 1; }

/* ============================== Nav subsections ========================= */
/* A section index, not a mega-menu: the same paper ground and hairline the
   evidence band uses, so the panel reads as part of the page rather than as
   browser chrome laid over it. */
.nav-item { position: relative; display: inline-flex; align-items: center; gap: 2px; }
/* The chevron is its own control beside the link, not wrapped around it, so
   "About" stays a destination. 26px square clears the 24px AA target on its
   own; the ::after takes it to 44 without moving the glyph. */
.nav-sub-toggle {
	appearance: none; background: none; border: 0; padding: 0; margin: 0;
	width: 26px; height: 26px; display: grid; place-items: center;
	color: var(--faint); cursor: pointer; position: relative;
	transition: color .18s var(--ease);
}
.nav-sub-toggle::after { content: ""; position: absolute; inset: -9px; }
.nav-sub-toggle:hover { color: var(--ink); }
.nav-sub-chevron { display: grid; place-items: center; transition: transform .22s var(--ease); }
.nav-sub-toggle[aria-expanded="true"] { color: var(--accent); }
.nav-sub-toggle[aria-expanded="true"] .nav-sub-chevron { transform: rotate(180deg); }

.nav-sub {
	position: absolute; top: calc(100% + 12px); left: -14px; z-index: 60;
	min-width: 208px; padding: 10px 0;
	background: var(--paper);
	border: 1px solid var(--hairline);
	border-radius: var(--r-form);
	box-shadow: 0 10px 28px rgb(33 31 27 / 0.10);
}
.nav-sub ul { list-style: none; margin: 0; padding: 0; }
.nav-sub a {
	display: block; padding: 8px 18px;
	font-size: 13px; letter-spacing: 0.2px; text-transform: none;
	color: var(--body-muted); text-decoration: none;
	transition: color .16s var(--ease), background-color .16s var(--ease);
}
.nav-sub a:hover { color: var(--accent); background: var(--card); }
/* Government's agencies, one step in from its five sibling sectors and one
   step down in size -- indentation and scale carry the hierarchy instead of
   a rule or a second background. */
.nav-sub-nested { list-style: none; margin: 2px 0 6px; padding: 0; }
.nav-sub-nested a { padding: 5px 18px 5px 30px; font-size: 12px; }

/* Hidden until the script binds it; below 880 the panel menu takes over. */
.nav-menu-toggle {
	appearance: none; background: none; border: 1px solid var(--hairline);
	border-radius: var(--r-form); padding: 0; width: 42px; height: 42px;
	display: none; place-items: center; color: var(--ink); cursor: pointer;
	transition: border-color .18s var(--ease), color .18s var(--ease);
}
.nav-menu-toggle:hover { border-color: var(--accent); color: var(--accent); }
.nav-menu-icon { display: grid; place-items: center; }

.nav-menu-panel {
	position: fixed; inset: 0; z-index: 200;
	background: var(--paper);
	overflow-y: auto; overscroll-behavior: contain;
}
.nav-menu-inner { max-width: 1280px; margin: 0 auto; padding: 22px 20px 56px; }
.nav-menu-close {
	appearance: none; background: none; border: 1px solid var(--hairline);
	border-radius: var(--r-form); width: 42px; height: 42px; padding: 0;
	display: grid; place-items: center; margin-left: auto; color: var(--ink); cursor: pointer;
}
.nav-menu-close:hover { border-color: var(--accent); color: var(--accent); }
.nav-menu-list { list-style: none; margin: 26px 0 0; padding: 0; }
.nav-menu-list > li + li { margin-top: 26px; padding-top: 26px; border-top: 1px solid var(--hairline-soft); }
.nav-menu-link {
	display: block; font-family: "Newsreader", Georgia, serif; font-weight: 400;
	font-size: 26px; letter-spacing: -0.2px; color: var(--ink); text-decoration: none;
}
.nav-menu-link.is-active { color: var(--accent); }
/* Label and disclosure on one row: the label keeps its link, the chevron owns
   the sections. 44px square so the control is a real target beside a 26px
   serif label, and `justify-content: space-between` puts it at the panel's
   right edge where a thumb already is. */
.nav-menu-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.nav-menu-sub-toggle {
	appearance: none; background: none; border: 0; padding: 0; margin: 0;
	flex: none; width: 44px; height: 44px; display: grid; place-items: center;
	color: var(--faint); cursor: pointer;
	transition: color .18s var(--ease);
}
.nav-menu-sub-toggle:hover { color: var(--accent); }
.nav-menu-sub-toggle[aria-expanded="true"] { color: var(--accent); }
.nav-menu-sub-toggle .nav-sub-chevron { transition: transform .22s var(--ease); }
.nav-menu-sub-toggle[aria-expanded="true"] .nav-sub-chevron { transform: rotate(180deg); }
.nav-menu-sub[hidden] { display: none; }
.nav-menu-sub { list-style: none; margin: 12px 0 0; padding: 0; }
.nav-menu-sub li + li { margin-top: 2px; }
.nav-menu-sub a {
	display: block; padding: 9px 0; font-size: 14px; line-height: 1.4;
	color: var(--body-muted); text-decoration: none;
}
.nav-menu-sub a:hover { color: var(--accent); }
/* Government's agencies: the same label-plus-chevron row and disclosed list
   as the level above, one size down and indented, so the pattern reads as
   "one more of these" rather than a different control. */
.nav-menu-sub-link { font-size: 14px; color: var(--ink); text-decoration: none; }
.nav-menu-sub-link:hover { color: var(--accent); }
.nav-menu-sub-toggle--nested { width: 36px; height: 36px; }
/* Government itself sits flush with its siblings (Office, Retail...); only
   its own children step in from there, so the indent reads as Government's
   agencies rather than as Government being one level deeper than the rest
   of the sector list. */
.nav-menu-sub--nested { padding-left: 14px; }
.nav-menu-sub--nested a { font-size: 13px; }
.nav-menu-ftp {
	display: inline-flex; align-items: center; gap: 6px;
	margin-top: 34px; padding-top: 22px;
	font-size: 12px; letter-spacing: 1.6px; text-transform: uppercase;
	color: var(--faint); text-decoration: none;
}
.nav-menu-ftp:hover { color: var(--ink); }

/* No `scroll-margin-top` here. `html` already carries
   `scroll-padding-top: calc(var(--header-h) + 16px)`, which applies to every
   anchor on the site including ones added later -- and the two stack. With both
   in force `#past-performance` landed 212px down the viewport with the tail of
   the previous section above it, so an evaluator arriving from a submission
   read the end of Sectors before the heading they came for. One offset, on the
   scroll container, is the one that scales. */

@media (prefers-reduced-motion: reduce) {
	.nav-sub-chevron, .nav-sub-toggle, .nav-menu-toggle, .nav-menu-close { transition: none; }
}

.site-footer { border-top: var(--hairline) solid 1px; margin-top: 24px; }
.footer-grid {
	max-width: 1280px; margin: 0 auto; padding: 56px 32px;
	display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 40px;
}
.footer-title { margin: 0; font-family: "Newsreader", Georgia, serif; font-size: 22px; }
.footer-blurb { margin: 14px 0 0; font-size: 14px; line-height: 1.6; color: var(--muted); max-width: 38ch; }
.footer-col .eyebrow { font-size: 11px; letter-spacing: 2px; margin-bottom: 14px; }
.footer-address { margin: 0; font-size: 14px; line-height: 1.7; color: var(--muted); }
.footer-links { display: flex; flex-direction: column; gap: 9px; }
.footer-links a, .footer-ftp { text-decoration: none; font-size: 14px; color: var(--muted); display: flex; align-items: center; gap: 5px; }
.footer-copyright { margin: 16px 0 0; font-size: 12px; color: var(--fainter); }

/* ==================================== Buttons ============================ */
.btn { display: inline-block; border-radius: var(--r-form); padding: 13px 26px; font-size: 12px; letter-spacing: 1.6px; text-transform: uppercase; text-decoration: none; cursor: pointer; border: none; transition: background .3s ease, color .3s ease, border-color .3s ease; }
.btn-outline { border: 1px solid rgba(0,0,0,0.18); background: none; color: var(--ink); }
.btn-outline:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.btn-solid { background: var(--ink); color: var(--paper); padding: 15px 30px; }
.btn-solid:hover { background: var(--accent); }

.cta { border: none; background: none; padding: 0; display: inline-flex; align-items: center; gap: 20px; cursor: pointer; text-decoration: none; }
.cta-dot { flex: none; width: 66px; height: 66px; border-radius: 999px; border: 1px solid rgba(0,0,0,0.25); display: flex; align-items: center; justify-content: center; color: var(--ink); transition: background .4s ease, border-color .4s ease, color .4s ease; }
.cta-arrow { font-size: 23px; line-height: 1; transition: transform .45s var(--ease), color .4s ease; }
.cta-label { font-size: 13px; letter-spacing: 2px; text-transform: uppercase; color: var(--ink); background-image: linear-gradient(currentColor, currentColor); background-repeat: no-repeat; background-position: 0 100%; background-size: 0% 1px; transition: background-size .5s var(--ease); }
.cta:hover .cta-dot { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.cta:hover .cta-arrow { transform: translateX(5px); color: var(--paper); }
.cta:hover .cta-label { background-size: 100% 1px; }
/* Secondary exits: quiet enough that the animated CTA still leads, findable
   enough to serve the visitor the CTA is not for. */
.cta-alt { margin: 22px 0 0; display: flex; align-items: baseline; gap: 8px 26px; flex-wrap: wrap; }
.cta-alt-tel { font-size: 14px; color: var(--muted); text-decoration: none; border-bottom: 1px solid var(--hairline); padding-bottom: 2px; position: relative; }
.cta-alt-tel:hover { color: var(--ink); border-bottom-color: var(--ink); }
/* 14px text needs help to clear the 24px floor at every pointer type. */
.cta-alt-tel::after { content: ""; position: absolute; inset: -5px 0; }
.footer-tel { color: inherit; text-decoration: none; position: relative; }
.footer-tel:hover { color: var(--ink); }
.footer-tel::after { content: ""; position: absolute; inset: -5px 0; }

.text-link-accent { border: none; background: none; cursor: pointer; padding: 0; font-size: 16px; color: var(--accent); border-bottom: 1px solid var(--accent); padding-bottom: 2px; transition: opacity .25s ease; text-decoration: none; display: inline-block; }
.text-link-accent:hover { opacity: 0.6; }
.text-link-accent.is-on-dark { color: var(--accent-on-dark); border-bottom-color: var(--accent-on-dark); }
.text-link-muted { border: none; background: none; cursor: pointer; font-size: 12px; letter-spacing: 1.4px; text-transform: uppercase; color: var(--faint); text-decoration: none; }

/* ============================= Project cards ============================= */
/* The card's four state changes answer focus as well as the pointer. The ring
   alone satisfies WCAG, but it is drawn by the browser around a 4:3 plate and
   says only “something here is focused” — not that the plate, its wiping rule,
   its sliding caption and its accented name are one clickable object. A mouse
   visitor is told that and a keyboard visitor was not.
   `:is()` rather than a `,` list: its selector parsing is forgiving, so a
   browser that does not know `:focus-visible` ignores that argument and keeps
   hover. In a comma list one unknown selector voids the whole rule, which
   would cost the pointer visitor the states to give the keyboard visitor
   nothing. Specificity is unchanged: both arguments are pseudo-classes. */
.proj { text-decoration: none; color: inherit; display: block; }
.proj-card { position: relative; border-radius: var(--r-card); overflow: hidden; aspect-ratio: 4/3; background: var(--img-placeholder); }
/* A real <img> rather than a background, so it can carry srcset/sizes and be
   lazy-loaded. The placeholder tone stays on .proj-card underneath. */
.proj-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform .85s var(--ease), filter .85s ease; }
.proj:is(:hover, :focus-visible) .proj-img { transform: scale(1.05); filter: brightness(1.06); }
/* ============================ Agency panel =============================== */
/* Stands in for a photograph on federal work the firm has no photos of yet:
   the agency's short form in the serif, its full name beneath, set bottom-left
   like a drawing's title block. A plate, not a missing image -- a step darker
   than the cards around it, with a hairline edge, and no pretence of a
   picture. Hover warms the abbreviation where a photograph would zoom. */
.agency-panel {
	position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: flex-end; gap: 8px;
	padding: 22px 24px; background: color-mix(in srgb, var(--card) 60%, var(--img-placeholder));
	box-shadow: inset 0 0 0 1px var(--hairline);
}
.agency-panel-abbr { font-family: "Newsreader", Georgia, serif; font-size: clamp(34px, 3.2vw, 46px); line-height: 1; letter-spacing: -0.3px; color: var(--ink); transition: color .3s ease; }
.agency-panel-name { max-width: 32ch; font-size: 11px; line-height: 1.5; letter-spacing: 1.4px; text-transform: uppercase; color: var(--muted); }
.proj:is(:hover, :focus-visible) .agency-panel-abbr { color: var(--accent); }
/* On a project page the panel is a band, not a 5:3 picture frame: a full
   hero's worth of empty plate would read as a photograph that failed. */
.detail-hero.is-stand-in { position: relative; aspect-ratio: 3 / 1; }
.agency-panel--hero { padding: clamp(24px, 4vw, 48px); }
.agency-panel--hero .agency-panel-abbr { font-size: clamp(56px, 8vw, 112px); }
.agency-panel--hero .agency-panel-name { font-size: 12px; max-width: none; }

.proj-status-pill { position: absolute; top: 14px; left: 14px; padding: 6px 12px; border-radius: 999px; background: rgb(var(--paper-rgb) / 0.92); backdrop-filter: blur(6px); font-size: 11px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--ink); }
/* An agency's own short form (DeCA) is content, not a badge word -- the
   uppercase treatment every other pill wants would print it as DECA. */
.proj-status-pill--agency { text-transform: none; letter-spacing: 0.4px; }
/* Diagonally opposite .proj-status-pill -- top-right still shared its row
   with a long status string ("In Design / Construction") on a narrow card,
   and two variable-width pills sharing a row will collide at some width
   no matter how much either one is trimmed. Ink background rather than
   paper -- this is a warning label, not a fact chip, and it says so on
   every size of the same image: card, hero, gallery tile and lightbox
   share this one class rather than four bespoke badges. */
.jg-rendering-tag {
	position: absolute; bottom: 14px; right: 14px; z-index: 1;
	padding: 6px 12px; border-radius: 999px;
	background: color-mix(in srgb, var(--ink) 86%, transparent); backdrop-filter: blur(6px);
	font-size: 11px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--paper);
}
.proj-rule { height: 2px; margin: 12px 0 0; background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform .5s var(--ease); }
.proj:is(:hover, :focus-visible) .proj-rule { transform: scaleX(1); }
/* The 12px right padding is the room the caption's own travel needs. Under a
   pointer the slide had no boundary to cross, so the three longest names in
   the portfolio — whose ink already ends within 4px of the plate — simply
   moved toward nothing. Under a focus ring they crossed it by up to 7px. An
   element that travels owns the room it travels into; widening the ring
   instead would put one offset on cards and another on everything else.
   Five names in the portfolio already wrap, so the three that now join them
   introduce nothing new. */
.proj-cap { padding: 6px 14px 0 2px; transition: transform .5s var(--ease); }
.proj:is(:hover, :focus-visible) .proj-cap { transform: translateX(12px); }
.proj-name { margin: 0; font-family: "Newsreader", Georgia, serif; font-size: 19px; line-height: 1.15; letter-spacing: -0.2px; color: var(--ink); transition: color .4s ease; }
.proj:is(:hover, :focus-visible) .proj-name { color: var(--accent); }
/* Sentence case, not uppercase. 29 of these run 31–61 characters — "NAVAL
   CONSTRUCTION BATTALION CENTER, GULFPORT, MISSISSIPPI" is 56 — and all-caps
   removes the word-shape cues a reader scans by, on the line carrying the
   installation name an evaluator is scanning FOR. The tracking drops with the
   caps (1.5px is a uppercase device) and the size rises a step to hold the
   same visual weight. This is not the settled `.eyebrow`/`.subhead` caps
   decision: ignore.md scopes that to three named classes and keeps the rest
   in play. */
.proj-sub { margin: 5px 0 0; font-size: 12px; letter-spacing: 0.2px; color: var(--accent); }
.proj-sub.is-muted { color: var(--faint); }

.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }

/* ================================== Hero ================================= */
/* The hero fills the screen below the header on arrival: standing line and
   headline at the top, the project line along the bottom, and the photograph
   taking whatever height is left. The photograph spans the text column plus
   a 24px bleed each side (16px where the window is too narrow for that), so
   its edges sit just outside the headline's rather than floating inside it.
   Its height is whatever the screen leaves, between 16:9 at the tallest and
   5:2 at the widest -- past 5:2 a short laptop window would crop the
   building to a strip, so the band grows and the page scrolls a little
   instead. `svh` so a phone's collapsing toolbar does not leave the bottom
   band under it; the `vh` line is the fallback for browsers without it. */
.hero-section { --hero-margin: clamp(20px, 4vw, 64px); padding-top: 44px; }
/* A grid, not a column flexbox: the middle row has to both fill the screen's
   height when there is room and hold its 5:2 floor when there is not, and a
   1fr track resolves against min-height where a flex item's grow did not. */
.hero-wrap {
	display: grid;
	grid-template-rows: auto minmax(max(280px, calc(min(100vw - 32px, 1264px) * 2 / 5)), 1fr) auto;
	min-height: max(620px, calc(100vh - var(--header-h) - 44px));
	min-height: max(620px, calc(100svh - var(--header-h) - 44px));
}
.hero-intro { padding-bottom: 26px; }
.hero-wrap > .container { width: 100%; }
.hero-eyebrow { margin: 0 0 14px; font-size: 12px; letter-spacing: 3px; text-transform: uppercase; color: var(--accent); }
/* One line at desktop widths. The italic accent on the last word is the
   same Newsreader, set as an editorial aside. The clamp only ever shrinks
   it; 76px -- DESIGN.md's display role -- is the ceiling on every screen. */
.hero-head { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(38px, 6vw, 76px); line-height: 1.02; letter-spacing: -1px; }
.hero-head em { font-style: italic; color: var(--accent); }
/* The stage is the space the photograph may use; as a size container it
   lets the frame be sized from both its width and its height at once.
   --hero-w is the text column (the 1216px .container content box, or the
   window less its 32px gutters) plus the bleed, which folds to
   min(100% - 32px, 1264px). The row's floor above (.hero-wrap) is the 5:2
   height of that width, in vw because a size container cannot measure
   itself -- vw counts the scrollbar, so it runs a few pixels generous. */
.hero-stage { min-height: 0; container-type: size; display: grid; place-items: center; }
.hero {
	--hero-w: min(calc(100cqw - 32px), 1264px);
	position: relative;
	width: var(--hero-w);
	height: clamp(calc(var(--hero-w) * 2 / 5), 100cqh, calc(var(--hero-w) * 9 / 16));
	border-radius: var(--r-hero); overflow: hidden; background: var(--img-placeholder);
}
.hero-slide { position: absolute; inset: 0; opacity: 0; transition: opacity 1.6s ease; }
.hero-slide-img { width: 100%; height: 100%; object-fit: cover; }
.hero-slide.is-active { opacity: 1; animation: jgKen 18s ease-in-out infinite alternate; }
/* Pressing pause used to stop the slide advancing while the photograph kept
   zooming 1.02 -> 1.10 forever, so the control reported "paused" about motion
   that was still running. `paused` rather than `none`: a pause should hold the
   frame the visitor is looking at, where `none` would snap the image back to
   its untransformed size. Reduced motion still uses `none`, because there the
   animation should never start at all. */
.hero.is-paused .hero-slide.is-active { animation-play-state: paused; }
/* The project line: the project in the serif italic, rising into place as
   its slide comes up, with the numbered index to its right. */
.hero-caption { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 40px; padding: 18px 0 22px; }
.hero-credits { position: relative; flex: 1 1 auto; min-width: 0; min-height: 82px; }
.hero-credit { display: none; flex-wrap: wrap; align-items: baseline; gap: 8px 18px; }
.hero-credit.is-active { display: flex; animation: jgHeroRise .9s cubic-bezier(.2,.7,.2,1) both; }
@keyframes jgHeroRise { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }
.hero-credit-name { flex-basis: 100%; font-family: "Newsreader", Georgia, serif; font-style: italic; font-size: clamp(24px, 2.4vw, 32px); line-height: 1.1; color: var(--ink); }
.hero-credit-loc { font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: var(--accent); }
.hero-credit-link { position: relative; font-size: 14px; color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--ink); padding-bottom: 2px; }
.hero-credit-link:hover { border-bottom-color: var(--accent); color: var(--accent); }
/* 14px text still needs help to clear the 24px target floor at every pointer type. */
.hero-credit-link::after { content: ""; position: absolute; inset: -6px 0; }

/* The index: 01-05 in the serif, each a full 44px target, with a hairline
   under the active one that fills over exactly one slide (--hero-speed is
   written by theme.js from the slideshow's own timing). It holds while the
   slideshow is paused, and on reduced motion simply shows as full. */
.hero-dots { flex: none; display: flex; align-items: center; gap: 4px; }
.hero-dot {
	position: relative; min-width: 44px; min-height: 44px; padding: 0 6px;
	border: none; background: none; cursor: pointer;
	display: inline-flex; align-items: center; justify-content: center;
	font-family: "Newsreader", Georgia, serif; font-size: 18px; color: var(--faint);
	transition: color .3s ease;
}
.hero-dot:hover, .hero-dot.is-active { color: var(--ink); }
.hero-dot-bar { position: absolute; left: 10px; right: 10px; bottom: 8px; height: 1px; background: var(--accent); transform: scaleX(0); transform-origin: left; }
.hero-dot.is-active .hero-dot-bar { animation: jgHeroFill var(--hero-speed, 6s) linear forwards; }
.hero-stage:has(.hero.is-paused) ~ .hero-caption .hero-dot.is-active .hero-dot-bar { animation-play-state: paused; }
@keyframes jgHeroFill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
	.hero-credit.is-active { animation: none; }
	.hero-dot.is-active .hero-dot-bar { animation: none; transform: scaleX(1); }
}

/* Pause/play for the auto-advancing hero (WCAG 2.2.2), at the end of the
   index, on the paper now rather than over the photograph. */
.hero-pause {
	flex: none; width: 32px; height: 32px; margin-left: 14px; padding: 0;
	border: 1px solid var(--hairline); border-radius: 999px; cursor: pointer;
	background: transparent; color: var(--ink);
	display: flex; align-items: center; justify-content: center;
	transition: background .3s ease;
}
.hero-pause:hover { background: var(--card); }

/* ============================ Federal proof strip ======================== */
/* One ruled row under the hero: a label, the agencies in the serif, and the
   SAM.gov registration. Hairlines rather than a panel -- still no fill, no
   icon -- but the agencies now stand at the same size as .intro-statement
   above them: the dual-practice claim names federal work in its first
   sentence, and the evidence for it earns the story's own weight rather than
   reading as a footnote under it. Only the registration line stays small: a
   UEI/CAGE/NAICS string is copied, not read, and the identifiers are ink on
   muted and select whole on a tap (`user-select: all`) because they are what
   an evaluator copies. */
/* Under the welcome, whose own bottom padding spaces the rule above; the
   56px below is the same rhythm, clearing Selected Work's heading. */
.proof-strip { padding: 0 0 56px; }
.proof-row {
	display: flex; flex-direction: column; align-items: flex-start; gap: 20px;
	padding: 32px 0; border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline);
}
/* The section's heading, in the site's eyebrow voice like the welcome above
   it -- the quiet lead-in, not the evidence itself. */
.proof-label { margin: 0; }
/* Same weight as .intro-statement above it: the agencies are the evidence
   for that statement's federal claim, not a footnote under it. */
.proof-agencies {
	margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 10px 36px;
	font-family: "Newsreader", Georgia, serif; font-size: clamp(24px, 3vw, 38px); line-height: 1.32; letter-spacing: -0.2px;
}
.proof-agencies a { display: inline-block; padding: 2px 0; color: var(--ink); text-decoration: none; transition: color .2s ease; }
.proof-agencies a:hover { color: var(--accent); }
.proof-agencies abbr { text-decoration: none; }
.proof-reg { margin: 0; font-size: 12px; letter-spacing: 0.3px; color: var(--muted); font-variant-numeric: tabular-nums; }
.reg-code { color: var(--ink); user-select: all; -webkit-user-select: all; }
.reg-sep { margin: 0 6px; color: var(--faint); }
/* The same line in the footer, a step quieter. */
.footer-reg { margin: 12px 0 0; max-width: 38ch; font-size: 12px; line-height: 1.6; color: var(--fainter); font-variant-numeric: tabular-nums; }
.footer-reg .reg-code { color: var(--muted); }

/* Icon-swap shared by both pause controls. */
.jg-icon-play { display: none; }
[data-playing="false"] .jg-icon-pause { display: none; }
[data-playing="false"] .jg-icon-play { display: block; }

/* Narrow text measures (.intro, .work-intro-inner, .about-intro-inner) MUST sit
   on an inner wrapper, never on the .container element itself. .container is what
   centers each section box (margin: 0 auto), so a narrower max-width on that same
   element centers the text instead of leaving it flush with the page gutter —
   visible only above 1280px, where the auto margins stop computing to ~0. */
.intro { max-width: 920px; }
.intro-statement { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(24px, 3vw, 38px); line-height: 1.32; letter-spacing: -0.2px; }
.intro .btn { margin-top: 38px; }

.split-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 26px; }
.split-head h2 { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: 30px; letter-spacing: -0.3px; }

.testimonial-band { border-radius: var(--r-hero); background: var(--ink); color: var(--paper); padding: clamp(40px, 6vw, 80px); text-align: center; }
/* A heading, not a decorative label: this band is one of only three sections
   on the page and it had no entry in the document outline. */
.testimonial-eyebrow { margin: 0 0 46px; font-size: 11px; font-weight: 400; letter-spacing: 3px; text-transform: uppercase; color: var(--accent-on-dark); }

/* Two columns rather than one rotating slot. Left-aligned inside the cells
   because a centred multi-line quote is hard to read, while the heading and
   the exit link stay centred to the band. No fills and no borders per quote:
   four cards on a dark band would be the lazy container, and whitespace plus
   the italic serif already says "quote". */
.testimonial-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 54px 56px; text-align: left; }
.testimonial-item { margin: 0; }
.testimonial-quote { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-style: italic; font-size: 19px; line-height: 1.5; letter-spacing: -0.2px; }
.testimonial-credit { margin: 18px 0 0; }
.testimonial-name { display: block; font-size: 13px; letter-spacing: 0.4px; }
/* Was var(--faint) at 2.75:1 on ink -- the client's own name was the least
   legible text on the page. Bronze-on-dark measures 6.45:1 and is a tint of
   the accent rather than a gray, so the attribution reads as the second
   voice on the band instead of the quietest. */
/* 1.6px, matching .hero-credit-link: the same job at the same size -- a small
   uppercase attribution on a dark surface -- was tracked wider elsewhere on the
   page. This sat at 1px, under the site's own 1.2px floor for uppercase type. */
.testimonial-org { display: block; margin: 4px 0 0; font-size: 12px; letter-spacing: 1.6px; text-transform: uppercase; color: var(--accent-on-dark); }
.testimonial-band .text-link-accent { margin-top: 52px; }

.contact-cta { padding: 122px 0 130px; border-top: 1px solid var(--hairline-soft); margin-top: 8px; }
.contact-cta .split-head { align-items: center; gap: 48px; margin-bottom: 0; }
.contact-cta h2 { flex: none; margin: 0; max-width: 26ch; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-style: italic; font-size: clamp(34px, 4.6vw, 58px); line-height: 1.05; letter-spacing: -0.5px; }
/* A column, which is what the markup comment beside it always claimed: "the
   other two are quiet lines beneath it". It shipped as a row flex with no
   gap, so the animated CTA and the two quiet links queued on one baseline
   with the label's ink ending at the exact pixel the next link's ink began
   -- measured 0px between them at 1440, 720 and 375, and 0px between their
   hit areas, which on touch makes a mis-tap the expected outcome rather
   than the edge case. `.cta-alt`'s 22px top margin was dead the whole time,
   because a top margin separates nothing in a row.
   `flex-start` rather than the old `center`: the doors share a left axis
   and read as one hierarchy, which is the alignment the rest of the page
   already uses. The old centering was only ever meaningful as a horizontal
   instruction in the broken row.
   Only the arrow lives in this column now. The two quiet links moved out
   of it to their own line under the headline, starting at the band's left
   edge in line with the eyebrow's dot -- on the home page and on the
   contact page's careers band alike. */
.contact-cta-wrap { flex: 1; display: flex; flex-direction: column; align-items: flex-start; }
/* The careers band on the contact page. Same component as the home page's
   closing CTA, one size down: there it ends the page and can match the h1; here
   it sits mid-page between the inquiry form and the map, and at 58px it would
   have read as a second title competing with "Let's talk about your project"
   rather than as a door beside it. Padding comes down for the same reason --
   the home version has the whole bottom of the page to itself. */
.contact-cta.is-careers { padding: 72px 0 84px; margin-top: 0; }
.contact-cta.is-careers h2 { font-size: clamp(28px, 3.4vw, 44px); }

/* Left, not centred. Every other section on both pages that use this band
   starts its first text at x=104 above 1280px; this one sat at x=468, so the
   page's terminal moment was the one place the spine broke. */
.compact-cta { padding: 46px 0 56px; border-top: 1px solid var(--hairline); display: flex; align-items: baseline; justify-content: flex-start; flex-wrap: wrap; gap: 6px 16px; text-align: left; }
.compact-cta-line { font-family: "Newsreader", Georgia, serif; font-weight: 400; font-style: italic; font-size: clamp(19px, 2vw, 24px); line-height: 1.3; letter-spacing: -0.2px; }

/* =================================== Work ================================ */
.work-intro { padding: 64px 0 24px; }
.work-intro-inner { max-width: 760px; }
.work-intro h1 { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(34px, 5vw, 58px); line-height: 1.05; letter-spacing: -0.5px; }
/* `:not(.eyebrow)` because `.work-intro p` (0,1,1) outranked `.eyebrow` (0,1,0)
   and quietly rewrote it: the portfolio page's first word rendered 16px
   #615d55 with a 0px gap to the H1, against 12px #8a5c37 and an 18px gap on
   every other page. It also broke the Uppercase Ceiling Rule (13px max) and
   made the eyebrow identical to the intro paragraph on the far side of the
   headline — three tiers collapsed to two. Excluding the component is the fix;
   raising specificity would only move the fight. */
/* ── Sector index ─────────────────────────────────────────────────
   Six in-page jumps and the portfolio's own size, in one band. The anchors it
   uses already existed and were already linked from About; /work/ simply never
   offered them, so reaching Government meant ~2,600px of scrolling. */
.work-index { padding: 0 0 8px; }
/* The phone-width contents list; shown only below 560px, so the row above
   560px is exactly the names it always was. */
.work-index-menu { display: none; }
@keyframes jg-index-in { from { opacity: 0; transform: translateY(-6px); } }
.work-index-list { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 0; padding: 0; list-style: none; }
.work-index-link { display: inline-block; font-size: 14px; color: var(--ink); text-decoration: none; border-bottom: 1px solid transparent; padding-bottom: 2px; transition: color .25s ease, border-color .25s ease; }
.work-index-link:hover { color: var(--accent); border-color: var(--accent); }

/* ── Sibling sectors on the archive ───────────────────────────────── */
.sector-siblings { padding: 8px 0 44px; }
.sector-siblings-label { margin: 0 0 12px; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; color: var(--faint); }
.sector-siblings-list { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 0; padding: 0; list-style: none; }

.work-intro p:not(.eyebrow) { margin: 24px 0 0; font-size: 16px; line-height: 1.6; color: var(--muted); }

.sector-row { padding: 56px 0; border-top: 1px solid var(--hairline); display: grid; grid-template-columns: 300px 1fr; gap: 48px; align-items: start; }
.sector-sticky { position: sticky; top: calc(var(--header-h) + 20px); }
.sector-index { margin: 0; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; color: var(--accent); }
.sector-name { margin: 12px 0 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: 32px; letter-spacing: -0.3px; }
.sector-blurb { margin: 16px 0 0; font-size: 14px; line-height: 1.6; color: var(--muted); }
/* The count is the point. This page held 34 projects and stated no number
   anywhere, while Government showed 2.18 of its 15 cards at a time behind a
   suppressed scrollbar — nothing told a visitor the other 13 existed. The
   sticky column had ~390px of empty paper under the blurb; this is what it
   was for. */
.sector-viewall { display: inline-block; margin: 20px 0 0; font-size: 13px; letter-spacing: 0.2px; color: var(--ink); text-decoration: none; border-bottom: 1px solid rgba(0,0,0,0.18); padding-bottom: 2px; }
.sector-viewall:hover { color: var(--accent); border-color: var(--accent); }

/* ΓöÇΓöÇ Sector archive ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
   Mirrors the /work/ intro so a visitor arriving from a "View all" link
   lands somewhere that reads as the same page family, not a WordPress
   default archive. */
.sector-archive-intro { padding: 36px 0 20px; }
.sector-archive-inner { max-width: 760px; }
.sector-archive-name { margin: 12px 0 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(34px, 5vw, 58px); line-height: 1.05; letter-spacing: -0.5px; }
.sector-archive-blurb { margin: 20px 0 0; font-size: 16px; line-height: 1.6; color: var(--muted); }
/* Tabular figures: this is the number the page exists to state. */
.sector-archive-count { margin: 18px 0 0; font-size: 13px; letter-spacing: 0.3px; color: var(--faint); font-variant-numeric: tabular-nums; }
.sector-archive-grid-wrap { padding-bottom: 56px; }
.sector-archive-empty { margin: 0; font-size: 16px; line-height: 1.6; color: var(--muted); }

/* ====================== Government archive: federal front door =========== */
/* taxonomy-sector-government.php's two extensions to the archive above: a
   qualifications strip reusing About's own list styling (.evidence-list) and
   the registration line's own styling (.proof-reg), and each agency's own
   heading + count row on .split-head, the same component "Selected Work" and
   the /work/ sector rows use. Nothing here is a new visual idiom. */
.gov-quals { padding: 4px 0 44px; border-top: 1px solid var(--hairline); }
.gov-quals-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px 40px; }
.gov-quals-list { flex: 1 1 320px; margin: 0; }
/* .proof-reg's own margin-left: auto already pushes this to the row's far
   edge; only the mobile stack (below) needs to cancel it. */
.gov-quals-reg { flex: 0 0 auto; }

.gov-agency { padding: 40px 0 0; }
.gov-agency + .gov-agency { border-top: 1px solid var(--hairline); }
.gov-agency:last-of-type { padding-bottom: 56px; }
.gov-agency-head { margin-bottom: 22px; }
/* Sans, muted, sentence case, beside the serif abbreviation inside the same
   h2 — the abbreviation is the heading; the full name is a gloss on it, the
   way jg_agency_panel() pairs "DeCA" with "Defense Commissary Agency" for a
   project with no photograph. */
.gov-agency-fullname { margin-left: 10px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; color: var(--muted); }
.gov-agency-count { margin: 0; font-size: 13px; letter-spacing: 0.3px; color: var(--faint); font-variant-numeric: tabular-nums; white-space: nowrap; }

.hscroll-wrap { position: relative; min-width: 0; }
/* The caption an In The Works row renders for itself, below the card rather
   than inside it. Settled here by context so the template does not have to
   carry a `style="margin-top:5px"` to say the same thing. */
.hscroll-item > .proj-sub { margin-top: 5px; }
/* 6px, not 2px, on the three clipping edges. The global focus ring is 2px at
   3px offset, so it needs 5px of room outside the card — the cards sat flush
   with this padding box (gap 0.00px) and `overflow: auto` cut the ring's top
   on all 34, and its outer edge on the first and last of every row. The file
   already solves this shape for `.jg-zoom` by pulling the ring inside; here
   there is room to keep it outset, which reads better over photography.
   `scroll-padding-left` tracks the padding so snap still lands true. */
.hscroll { display: flex; gap: 18px; overflow-x: auto; padding: 6px 6px 12px; scroll-snap-type: x proximity; scroll-padding-left: 6px; scrollbar-width: none; -ms-overflow-style: none; }
.hscroll::-webkit-scrollbar { display: none; }
.hscroll-item { flex: none; width: 380px; max-width: 72%; scroll-snap-align: start; }
/* `z-index: 2` is load-bearing. The buttons sit BEFORE `.hscroll` in the markup
   so a keyboard visitor reaches the control before the fifteen cards it
   scrolls — but a positioned element with `z-index: auto` paints in DOM order,
   so that reorder put the arrows underneath the cards. `opacity: 1`,
   `visibility: visible`, in the viewport, and completely invisible:
   `elementFromPoint` at the arrow's centre returned `img.proj-img`.
   Paint order and source order are separate problems and this is the property
   that separates them. */
.hs-arrow { position: absolute; z-index: 2; top: 0; height: calc(100% - 52px); width: 72px; border: none; padding: 0; cursor: pointer; transition: opacity .25s ease; display: flex; align-items: center; }
/* Availability, in one place. An arrow that is not usable is `visibility:
   hidden`, not merely transparent, so it leaves the tab order with the same
   gesture that takes it off the screen — opacity alone hides the focus ring
   while keeping the stop, which is a focus stop that cannot be seen.
   `pointer-events` rides along so the gradient stops intercepting taps meant
   for the card beneath it.

   No transition on `visibility`, deliberately. Delaying it by the length of
   the fade would look marginally smoother on the way out, and it would make
   leaving the tab order depend on a transition actually running — which is
   not guaranteed under `prefers-reduced-motion`, in a background tab, or
   wherever a UA declines to animate. That is the original bug with a timer on
   it. The arrow is a gradient overlay appearing at a scroll extreme; it can
   afford to go at once, and correctness cannot afford to wait. */
.hs-arrow.is-off { opacity: 0; visibility: hidden; pointer-events: none; }
/* The button is 72px wide because the gradient needs that far to fade; the
   part a visitor aims at is the 36px circle. Leaving the whole 72px
   clickable meant the arrow sat on top of the next card — 70 of the 77
   visible pixels of the peeking card at 375px, and the entire visible sliver
   of the third card at 1440px — so the tap that should open a project
   scrolled the row instead. `pointer-events` splits the two: the fade is a
   cue, the circle is the target. Focus is unaffected, and `is-off`'s
   `visibility: hidden` still takes the circle with it, because visibility
   inherits. */
.hs-arrow { pointer-events: none; }
.hs-arrow-circle { pointer-events: auto; }
/* All three stops now come from one place. The note that used to sit here
   claimed an alpha could not be derived from a custom property, which is
   only true while the property holds a hex — `--paper-rgb` is the fix, not
   the workaround. */
.hs-left { left: -2px; justify-content: flex-start; background: linear-gradient(270deg, rgb(var(--paper-rgb) / 0) 0%, rgb(var(--paper-rgb) / 0.85) 70%, var(--paper) 100%); }
.hs-right { right: -2px; justify-content: flex-end; background: linear-gradient(90deg, rgb(var(--paper-rgb) / 0) 0%, rgb(var(--paper-rgb) / 0.85) 70%, var(--paper) 100%); }
/* The container is a focus stop only where its cards are not links, so the
   ring has to sit inside: the row is an overflow container and an outset ring
   would be clipped along the scroll axis. */
.hscroll:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--r-form); }
.hs-arrow-circle { width: 36px; height: 36px; border-radius: 999px; background: var(--ink); color: var(--paper); display: flex; align-items: center; justify-content: center; transition: background-color .18s ease; }
/* The only thing that acknowledged a hover here was the row starting to
   move, which left the control with no answer at all for a visitor who
   asked for less motion — a button with `cursor: pointer` and no reply.
   Colour is the answer, and accent is already what this site means by
   interactive. The paper glyph measures 14.7:1 on ink and 5.12:1 on the
   default accent, so the state change is legible and the glyph never
   drops below the 3:1 a graphic needs.

   `:hover` sits on the circle, not the button: the button is
   `pointer-events: none` so the gradient cannot steal a card's clicks,
   which also means it is not hit-tested and cannot match `:hover`. Focus
   still belongs to the button, which is the element that takes it. */
.hs-arrow-circle:hover { background: var(--accent); }
.hs-arrow:focus-visible .hs-arrow-circle { background: var(--accent); }
.hs-left .hs-arrow-circle { margin-left: 2px; }
.hs-right .hs-arrow-circle { margin-right: 2px; }

/* The label column is 300px of a 754px content width at 881px — 40% of the row
   spent on a heading, leaving 1.40 cards where one pixel narrower shows 2.01.
   The stacked layout already handles this; it just started 120px too late.
   Scoped to `.sector-row` so the other grids keep their own breakpoint. */
@media (max-width: 1000px) {
	.sector-row { grid-template-columns: 1fr; gap: 28px; }
	.sector-sticky { position: static; top: auto; }
}

/* Full width, starting where the heading starts.

   This row used to be inset 348px so its cards lined up with the cards in the
   sector rows above -- 300px label column plus a 48px gap. That alignment was
   the problem rather than the point: it made a seventh row of the same shape,
   and this section is not a seventh sector. It is the work in hand, and the
   record above is work delivered.

   Running the full container breaks the grid the six rows are built on, which
   is what makes the change of subject legible before a word is read. It also
   closes the ~348px of empty paper that sat under the heading, which had no
   label column to justify it here. Deliberate, and a reversal -- do not
   re-indent it to match the rows above. */
.in-the-works .hscroll-wrap { max-width: none; margin-left: 0; }
/* Immediately after the rule it overrides, not in the earlier 1000px block.
   A media query adds no specificity, so at equal specificity this is decided
   by source order — the override sat above the base rule and lost, leaving the
   indent in force below 1000px where the label column no longer exists. That
   put the row 348px inset in an 821px column and pushed 63px off-screen at
   phone width. */
@media (max-width: 1000px) {

	/* The corner cannot hold: seven of twelve columns at 1000px leaves ~250px a
	   credentials column, and "NAVFAC — Naval Facilities Engineering Systems
	   Command" wraps to four lines in it. The placement is dropped and the
	   zones take their DOM order -- intro, globe, credentials, geography --
	   which is the same reading order the corner layout gives. */
	.evidence-band { grid-template-columns: 1fr; gap: 44px; }
	.evidence-intro, .evidence-plate, .evidence-credentials, .evidence-geo { grid-column: 1 / -1; grid-row: auto; }
	.evidence-geo { grid-template-columns: 1fr 1fr; }
	/* `justify-self`, not `margin: 0 auto` -- auto margins make a grid item
	   fit-content, which sized the plate off the iframe's max-content (305px)
	   instead of the cap below. */
	.evidence-plate { width: 100%; max-width: 440px; justify-self: center; }
}

.in-the-works { padding: 72px 0 96px; border-top: 1px solid var(--hairline); }
.in-the-works .split-head { align-items: end; gap: 32px; margin-bottom: 34px; }
/* Capped at the sector heading's 32px. At 1440 this rendered 44px against six
   sector h2s at 32 — a 37% inversion inside one heading level, giving the page's
   loudest voice to seven projects that are duplicates of cards above.

   The original of this comment claimed “375px was already correct at 28px”.
   That was asserted, not measured: at 375 this renders 28px against the sector
   names' flat 32px, so the ladder is still inverted there, just the other way
   up. Left as-is pending a decision on whether the sector names should clamp
   down at mobile rather than this heading clamping up. */
.in-the-works h2 { font-size: clamp(28px, 3.4vw, 32px); line-height: 1.05; letter-spacing: -0.4px; max-width: 18ch; }
.in-the-works .split-head p { margin: 0; font-size: 16px; line-height: 1.6; color: var(--muted); max-width: 36ch; }

/* =============================== Project detail =========================== */
.detail-back { border: none; background: none; cursor: pointer; padding: 0 0 24px; font-size: 12px; letter-spacing: 1.6px; text-transform: uppercase; color: var(--faint); text-decoration: none; display: inline-block; }
/* Every enlargeable photograph is a real button, so the gallery is operable by
   keyboard and announced as a control rather than being a click-handler on an
   <img> (WCAG 2.1.1). Transparent by design — the photo is the affordance. */
.jg-zoom {
	position: relative; display: block; width: 100%; height: 100%; padding: 0;
	border: none; background: none; cursor: zoom-in;
}

/* Same fixed-height fault as the home hero: at 375px this well was 335x633
   (0.53) against landscape photography. The ratio is chosen so the max-height
   still binds at desktop -- 1216/1.667 = 730 > 720 -- leaving desktop
   unchanged while every narrower width stays landscape. */
.detail-hero { width: 100%; border-radius: var(--r-hero); overflow: hidden; background: var(--img-placeholder); height: auto; aspect-ratio: 5 / 3; max-height: min(78vh, 720px); }
.detail-hero img { width: 100%; height: 100%; object-fit: cover; }

.detail-grid { padding: 56px 0 24px; display: grid; grid-template-columns: 1fr 340px; gap: 64px; align-items: start; }
.detail-sector { margin: 0 0 16px; font-size: 12px; letter-spacing: 3px; text-transform: uppercase; color: var(--accent); }
.detail-title { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(36px, 5.2vw, 64px); line-height: 1.04; letter-spacing: -0.6px; }
.detail-body { margin: 20px 0 0; font-size: 18px; line-height: 1.7; color: var(--body-muted); max-width: 60ch; }
.detail-body:first-of-type { margin-top: 28px; }

/* The single-plate column: a photograph and the fact card as one sticky
   unit, in that order, in a project with exactly one real photograph. The
   card keeps its own look (.meta-card, unchanged from the multi-photo
   pages); the column around it is what now carries the sticky position, so
   the photograph rides along with the facts instead of scrolling away
   before them. */
.meta-col { position: sticky; top: calc(var(--header-h) + 20px); display: flex; flex-direction: column; gap: 20px; }
.detail-plate { width: 100%; aspect-ratio: 4 / 3; border-radius: var(--r-card); overflow: hidden; background: var(--img-placeholder); }
.detail-plate img { width: 100%; height: 100%; object-fit: cover; }
.meta-card { background: var(--card); border: 1px solid var(--hairline-soft); border-radius: var(--r-card); padding: 28px; }
.meta-row { padding: 13px 0; border-bottom: 1px solid var(--hairline); }
.meta-row:last-child { border-bottom: none; }
.meta-label { margin: 0; font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--faint); }
.meta-value { margin: 5px 0 0; font-size: 14px; color: var(--ink); }

.detail-wide-img { width: 100%; border-radius: var(--r-card); overflow: hidden; background: var(--img-placeholder); height: auto; aspect-ratio: 16 / 9; max-height: min(72vh, 640px); }
.detail-wide-img img { width: 100%; height: 100%; object-fit: cover; }
.detail-2up { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
/* Each pair's shape comes from single-project.php's $pair_ratio() -- the
   two photographs' own average, landscape-floored -- via --tile-ratio on the
   section; 4:3 only stands in if that custom property is somehow absent. */
.detail-2up-item { border-radius: var(--r-card); overflow: hidden; background: var(--img-placeholder); aspect-ratio: var(--tile-ratio, 4/3); }
.detail-2up-item img { width: 100%; height: 100%; object-fit: cover; }

/* Upright, not italic, and a <p> rather than a <blockquote>: every one of
   these lines is the firm's own editorial read on the building type ("A
   hangar is a very large box with very specific opinions about what goes
   in it"), never a person's spoken words, and italic serif at quote size is
   this site's own signal for the opposite -- a named client's testimonial,
   set exactly this size on the home page. Same family and measure as that
   quote role on purpose; only the slant and the tag change, so the two
   still read as the same voice at the same weight, not as two unrelated
   styles that happen to share a page. */
.pull-quote { margin: 0 auto; max-width: 760px; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(22px, 2.8vw, 32px); line-height: 1.42; letter-spacing: -0.2px; }
.pull-quote-inline { margin: 42px 0 0; padding: 6px 0 6px 26px; border-left: 2px solid var(--accent); font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(20px, 2.4vw, 28px); line-height: 1.42; letter-spacing: -0.2px; }

.gallery-head { display: flex; align-items: baseline; justify-content: space-between; gap: 18px; padding-bottom: 22px; margin-bottom: 30px; border-bottom: 1px solid var(--hairline); }
.gallery-count { font-size: 12px; letter-spacing: 2px; color: var(--fainter); font-variant-numeric: tabular-nums; }
.gallery-mosaic { display: grid; grid-template-columns: repeat(6, 1fr); grid-auto-rows: clamp(62px, 7.6vw, 112px); gap: 14px; }
.gallery-tile { grid-row: span 3; border-radius: var(--r-card); overflow: hidden; background: var(--img-placeholder); transition: filter .6s ease; }
.gallery-tile:hover { filter: brightness(1.05); }
.gallery-tile img { width: 100%; height: 100%; object-fit: cover; }
.gallery-tile.col-span-4 { grid-column: span 4; }
.gallery-tile.col-span-2 { grid-column: span 2; }
.gallery-tile.col-span-6 { grid-column: 1 / -1; }

.more-work { padding: 8px 0 96px; border-top: 1px solid var(--hairline); }
.more-work-head { display: flex; align-items: baseline; justify-content: space-between; padding-top: 48px; margin-bottom: 26px; }
.more-work-head h2 { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: 28px; letter-spacing: -0.3px; }

/* =================================== About ================================ */
.about-intro { padding: 64px 0 0; }
.about-intro-inner { max-width: 880px; }
.about-intro h1 { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(34px, 5vw, 58px); line-height: 1.06; letter-spacing: -0.5px; }
.about-wide-img { width: 100%; border-radius: var(--r-hero); overflow: hidden; background: var(--img-placeholder); height: auto; aspect-ratio: 16 / 9; max-height: min(56vh, 460px); }
.about-wide-img img { width: 100%; height: 100%; object-fit: cover; }

/* About's own section rhythm and its headline accent, which were four inline
   `style=` attributes on the template. The accent half-headline is the best
   idea on that page; it belongs in the system, not in the markup. */
.headline-accent { color: var(--accent); }
/* 44 below the photo, not 88. The photo's own bottom padding, the team
   section's top margin and its top padding were stacking to 228px of void
   between the image and "Our team", with the dividing rule marooned in the
   middle of it. */
.about-photo { padding: 48px 0 44px; }
/* Its 8px top padding and no rule were correct when it trailed `.whatwedo` as
   one continuous block. It now opens its own section, so it takes the same
   rule and rhythm as every other band on the page. */
.story-row { padding: 74px 0 88px; border-top: 1px solid var(--hairline); margin-top: 8px; display: grid; grid-template-columns: 300px 1fr; gap: 64px; align-items: start; }
.story-row h2 { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(26px, 2.8vw, 36px); line-height: 1.14; letter-spacing: -0.3px; max-width: 13ch; }
/* One tier and one measure for the whole story. It used to run an 18px lead at
   85ch — 42% past the 60ch cap — above six paragraphs demoted to 16px
   `body-small` in two columns, so the firm's own account of itself was
   typeset as supporting copy beneath a line too wide to read comfortably.
   The measure cap came off at the firm's request: the column now runs to the
   container's right edge, squaring with the team cards directly above it. */
.story-body { min-width: 0; }
/* 1.6em, not a flat px value: at 18px this is 28.8px, just under the 30.96px
   a line of its own 1.72 leading takes up, so the gap between paragraphs
   reads as a full line-break rather than the 18px it shipped at — which was
   42% tighter than the leading inside each paragraph, fusing them together. */
.story-body p { margin: 0 0 1.6em; font-size: 18px; line-height: 1.72; color: var(--body-muted); }
.story-body p:last-child { margin-bottom: 0; }
/* Disclosure animated on grid-template-rows rather than max-height. Animating
   max-height thrashes layout every frame and needs a magic ceiling — the old
   3200px cap would have clipped this copy outright in a single column, where it
   runs taller. 0fr→1fr measures the content itself, so nothing can be cut off.
   Where the interpolation isn't supported the panel still opens, just instantly. */
/* min-height:0 is load-bearing: a grid item defaults to min-height:auto, which
   floors the track at the item's min-content height and leaves the panel open a
   crack. The clip wrapper also keeps the inner element's margin and border
   inside the clipped box, so they collapse with everything else. */

.team-section { padding: 56px 0 96px; border-top: 1px solid var(--hairline); margin-top: 0; }
.team-section h2 { margin: 0 0 14px; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(28px, 3.4vw, 44px); line-height: 1.05; letter-spacing: -0.4px; max-width: 20ch; }
.team-section .lede { margin: 0 0 44px; font-size: 16px; line-height: 1.6; color: var(--muted); max-width: 52ch; }
/* An <h3> now, not a <p>: "Principals" and "The Team" label the two groups a
   reader navigates by, and a seven-screen page offered nothing below its three
   h2s. `font-weight` is restated because a heading arrives bold. */
.subhead { margin: 0 0 22px; font-size: 12px; font-weight: 400; letter-spacing: 2px; text-transform: uppercase; color: var(--faint); }

/* `align-items: start` so each card sizes to its own content. Grid's default
   `stretch` made all three match the tallest, so opening one principal's
   disclosure grew the whole row and the other two cards moved with it. */
.principals-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-bottom: 64px; align-items: start; }
.principal-card { background: var(--card); border: 1px solid var(--hairline-soft); border-radius: var(--r-card); padding: 26px; }
/* 168px, not 86px. Principal-led delivery is the firm's stated structural
   differentiator, and it was being illustrated at the smallest size a face can
   be and still be a face. The team rows keep 58px — they are a list. */
/* 58px, not 54: it is a documented ramp step, and 58/168 is the same monogram
   proportion the 86px avatar had at 30px. The ramp and the arithmetic agreed. */
.principal-card .avatar { width: 168px; height: 168px; font-size: 58px; }
.avatar { position: relative; width: 86px; height: 86px; border-radius: var(--r-card); overflow: hidden; background: var(--ink); color: var(--paper); display: flex; align-items: center; justify-content: center; font-family: "Newsreader", Georgia, serif; font-size: 30px; }
/* The photograph fills the monogram's box and covers it. `top` rather than
   `center`: a portrait cropped square loses the chin before it loses the
   forehead, so the head stays in frame.
   This only worked for an attachment with a WebP sibling, where jg_picture()
   wraps the img in a <picture> element -- the wrapper is what this rule
   targeted. Without one, jg_picture() returns a bare <img>, which stayed a
   flex child beside the monogram text instead of covering it, splitting the
   box into "letters | photo" exactly where no WebP had been generated yet.
   Targeting the img directly covers both cases the same way. */
.avatar picture, .avatar img { position: absolute; inset: 0; display: block; }
.avatar img { width: 100%; height: 100%; object-fit: cover; object-position: center top; }
.principal-name { margin: 20px 0 0; font-size: 18px; }
.principal-role { margin: 4px 0 0; font-size: 13px; line-height: 1.5; color: var(--accent); }
.principal-cred { margin: 0; font-size: 14px; line-height: 1.6; color: var(--muted); }
/* The biography reads as prose, so it takes the body measure and leading rather
   than the blurb's tighter label setting. Last paragraph loses its bottom
   margin so the contact rule below sits on the same rhythm as it does after a
   one-line blurb. */
.principal-bio p { margin: 0 0 1em; font-size: 14px; line-height: 1.68; color: var(--body-muted); }
.principal-bio p:last-child { margin-bottom: 0; }
.principal-contact { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--hairline); }
.principal-contact p { margin: 0; font-size: 13px; color: var(--faint); }
.principal-phone { font-variant-numeric: tabular-nums; }
.principal-contact a { color: var(--ink); text-decoration: none; border-bottom: 1px solid rgba(0,0,0,0.18); }
.principal-contact a:hover { color: var(--accent); border-color: var(--accent); }

/* Credentials and contact, collapsed behind a disclosure: the card led with
   name/role/photo and buried the two most name-recognizable facts (a phone
   number, a license) under the same weight as everything else. `margin-top`
   here rather than `border-top` on the button itself, so the rule sits flush
   against whichever state (open or closed) the panel is in. */
.principal-toggle {
	display: flex; align-items: center; justify-content: space-between; gap: 12px;
	width: calc(100% + 52px); margin: 18px -26px 0; padding: 14px 26px;
	background: none; border: none; border-top: 1px solid var(--hairline);
	font-family: inherit; font-size: 13px; letter-spacing: 0.3px; color: var(--ink);
	cursor: pointer; text-align: left;
}
.principal-toggle:hover { color: var(--accent); }
.principal-toggle-icon { display: flex; flex: none; color: var(--faint); transform: rotate(90deg); transition: transform .3s var(--ease); }
.principal-toggle[aria-expanded="true"] .principal-toggle-icon { transform: rotate(-90deg); }
.principal-toggle:hover .principal-toggle-icon { color: var(--accent); }

/* The row track animates between 0fr and 1fr rather than animating
   `max-height`/height directly, which forces layout recalculation on every
   frame; `grid-template-rows` composites instead, and the transition still
   runs off the content's real height with no JS measurement step.
   `.principal-detail-inner`'s `min-height: 0` is load-bearing -- a grid
   row's intrinsic minimum is its content size regardless of the 0fr track,
   which would skip the animation entirely without it. */
.principal-detail { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .35s var(--ease); }
/* `[hidden]` is a UA rule of the lowest possible specificity, so the `display:
   grid` above beats it and the attribute does nothing on its own. Without this
   line the panel stays in the tab order and the accessibility tree while the
   button insists it is collapsed. */
.principal-detail[hidden] { display: none; }
.principal-detail.is-open { grid-template-rows: 1fr; }
.principal-detail-inner { overflow: hidden; min-height: 0; }
.principal-detail .principal-cred,
.principal-detail .principal-bio { padding-top: 18px; }
.principal-detail .principal-contact { margin-top: 14px; }

/* Three, deliberately, after measuring the alternative. Five people leave one
   empty cell here; `auto-fill` at minmax(280px) resolved to four columns at
   1440px and left THREE empty cells, which is the same complaint made worse.
   A ragged final row is ordinary grid behaviour for a count that is not a
   multiple of the columns — the fix for it is a sixth colleague, not CSS. */
/* 18px, matching `.principals-grid`. Two people-grids stacked in one section
   with different gutters read as two components that happen to be adjacent. */
.team-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
/* Same ground, border and radius as `.principal-card`, one density down. White
   cards sitting directly beneath card-coloured ones made the principals and
   their colleagues look like they came from different pages; they are one firm
   and should be one card, distinguished by size rather than by material. */
.team-row { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 16px; background: var(--card); border: 1px solid var(--hairline-soft); border-radius: var(--r-card); padding: 20px; }
/* Two columns -- photograph, person -- and two rows, the second holding the
   email. Above 880 the email sits in the text column under the phone number and
   the photograph spans both rows, which is what makes it grow: it takes its
   height from the whole text block rather than from the first line of it. */
/* `span 2`, not `1 / -1`. Both rows here are implicit -- the row track list is
   never declared -- and a negative line number counts back from the end of the
   EXPLICIT grid, which in that case is line 1. So `1 / -1` silently placed the
   photograph in row 1 alone and it stayed the height of the name/role/phone
   block. The column span below is written the same way and does work, because
   `grid-template-columns` is declared. */
.team-row .avatar { grid-column: 1; grid-row: 1 / span 2; }
.team-row-main { grid-column: 2; grid-row: 1; min-width: 0; }
.team-row-email { grid-column: 2; grid-row: 2; }
/* Neither a grid item nor a flex item shrinks below its own content unless told
   to, and the row is both: a grid item of `.team-grid`, and a flex container
   holding the text block. With `min-width: auto` on either, the email's
   `nowrap` + ellipsis could never take effect, so at 320px the row measured
   306px inside a 280px column and pushed 6px past the viewport. Both levels
   need it — fixing only the inner one changed nothing. */
.team-row { min-width: 0; }

/* Fixed width, stretched height: the photograph is 86px across and runs from
   the top of the name to the bottom of the address, which above 880 is all four
   lines.
   It used to derive BOTH axes from the text -- `align-self: stretch` for the
   height and `aspect-ratio: 1` to turn that back into a square -- and that was
   wrong twice over. It could not work here: with an `auto` column track the
   width is resolved first, from the text, and a definite width plus a ratio
   makes the height definite too, so `stretch` never applied and the square
   stayed at the height of the first row (69px) no matter how many rows it
   spanned. And it should not work: a photograph sized from its own caption
   means the colleague with the longest credentials gets the largest portrait,
   which is how one person's photo came to be visibly bigger than everyone
   else's. A constant width ends that class of bug -- and because grid items
   stretch, every card in a row is the same height, so every photograph in a row
   is identical. The box is 86x94 rather than exactly square; `object-fit:
   cover` crops to it, and a portrait two pixels taller than it is wide is the
   direction a face wants to go anyway.
   The monogram plate is left at the `.avatar` default -- ink behind paper, the
   same plate the principals use. It was overridden to `--card` on `--ink` only
   because the row's ground used to be white; on the card ground it now shares
   with the principals, a `--card` plate would have been invisible. */
.team-row .avatar {
	align-self: stretch;
	width: 86px; height: auto;
}
.team-row-name { margin: 0; font-size: 16px; }
.team-row-role { margin: 3px 0 0; font-size: 13px; line-height: 1.4; color: var(--accent); }
.team-row-phone { margin: 5px 0 0; font-size: 12px; letter-spacing: 0.4px; color: var(--fainter); font-variant-numeric: tabular-nums; }
/* A dialled number is a link, and reads as one only on hover: an underline on
   every phone number would put eleven rules through the people section. */
.team-row-phone a, .principal-phone a { color: inherit; text-decoration: none; }
.team-row-phone a:hover, .principal-phone a:hover { color: var(--accent); }
/* The principals' phone numbers inherit a hit-area overlay through
   `.principal-contact a`; this one never joined any hit-area rule, and
   measured 78.2x13.3px — under the WCAG 2.5.8 AA 24x24 floor. It sits between
   the role above and the email directly below, too tight for an `::after`
   overlay to clear 24px on both edges without risking either neighbour.
   Inline-block padding grows the line box in normal flow instead,
   the same technique already used below for the email it sits next to. */
.team-row-phone a { display: inline-block; padding: 5px 0; }
/* No truncation. `overflow: hidden` + `text-overflow: ellipsis` + `nowrap` sat
   on this block while the thing that overflows is the inline <a> inside it, so
   the ellipsis never rendered and the address was simply cut: at 320 that hid
   67-90px, ending addresses mid-domain -- "bschultz@thejenkin" -- with nothing
   to say anything was missing. An address a reader cannot finish is worse than
   one that takes two lines, so it wraps. `anywhere` because an email has no
   spaces to break at, and the row's photograph sizes itself from this text, so
   a second line is absorbed rather than overflowing. */
/* No top margin: the phone number and the address are one contact block, and
   should read as one. Both links carry 5px of inline-block padding for their
   hit areas, so the two sit 10px apart visually -- the same gap the phone keeps
   from the role above it -- while their 24px targets end up exactly adjacent
   rather than overlapping, which is the tightest this can go and still clear
   WCAG 2.5.8. The principals' contact block already sets its two lines flush
   this way; this is the team row agreeing with it. */
.team-row-email { margin: 0; font-size: 13px; overflow-wrap: anywhere; }
.team-row-email a { color: var(--faint); text-decoration: none; }
.team-row-email a:hover { color: var(--accent); }

/* Below this the address slides out of the text column onto its own full-width
   row beneath the photograph, and the photograph shrinks back to the height of
   the name/role/phone block.
   1160 is measured, not chosen. The longest address on the page sets at 190px,
   and inline it gets the card minus 40px of padding, the 86px photograph and
   the 16px gutter -- so the card has to reach ~350px before the address clears
   one line with any room to spare, and three of those plus gutters and page
   padding is ~1160px of viewport. Above it the addresses set on one line with
   20-70px to spare; below it, at 1000px, the text column drops to 153px and
   every address in the section breaks across two lines mid-domain, which is the
   thing the full-width row exists to prevent. Tying this to the 880 breakpoint
   where the grid drops to two columns would have been tidier and would have
   left that 280px band broken. */
@media (max-width: 1160px) {
	/* 72px, not 86: the photograph spans one row here rather than two, so a
	   square wants less width -- and at the narrow end of the range it would
	   otherwise take a third of a 231px card. */
	.team-row .avatar { grid-row: 1; width: 72px; }
	/* The flush-to-the-phone margin is for the inline layout; here the address
	   clears the photograph instead, and needs the gap back. */
	.team-row-email { grid-column: 1 / -1; margin-top: 4px; }
}

.whatwedo { padding: 74px 0 88px; border-top: 1px solid var(--hairline); margin-top: 8px; }
/* This rule did not exist. `.whatwedo h2` matched nothing but `* { box-sizing }`
   and rendered at the UA default — 24px, weight 700, Helvetica — so the section
   answering "do you do my building type?" was introduced by the only object on
   the site that looked unfinished, in the wrong typeface, breaking the two-voice,
   never-bold-the-serif and no-ninth-rung rules at once. Its two siblings had full
   rules twenty lines above; this one was simply missed. Matched to `.story-row h2`
   so the three section headings finally read as peers. */
.whatwedo h2 { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(26px, 2.8vw, 36px); line-height: 1.14; letter-spacing: -0.3px; max-width: 13ch; }
.whatwedo-row { display: grid; grid-template-columns: 300px 1fr; gap: 64px; align-items: start; }
.whatwedo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 56px; min-width: 0; }
.sector-item { padding: 24px 0; border-top: 1px solid var(--hairline); }
.sector-item-index { margin: 0 0 8px; font-size: 12px; letter-spacing: 1.5px; color: var(--accent); }
/* `inline-block`, not `inline-flex`. As a flex container the title made its own
   text an anonymous flex item and the arrow a second one, so a wrapping title
   left the arrow baseline-aligned to line ONE — stranded at the far right,
   detached from the words it belongs to. As an inline-block the arrow is
   ordinary inline content bound to the last word by a non-breaking space, and
   the box still takes the vertical margin and the target-size overlay, which
   a bare `inline` would have dropped. */
.sector-item-title { margin: 0 0 8px; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: 21px; line-height: 1.1; letter-spacing: -0.2px; color: var(--ink); cursor: pointer; display: inline-block; transition: color .25s ease; text-decoration: none; }
.sector-item-title:hover { color: var(--accent); }
.sector-item-blurb { margin: 0; font-size: 14px; line-height: 1.55; color: var(--muted); }


/* ================================= Contact ================================ */
.contact-header h1 { margin: 0 0 56px; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(34px, 5vw, 58px); line-height: 1.05; letter-spacing: -0.5px; max-width: 16ch; }
.contact-grid { display: grid; grid-template-columns: 340px 1fr; gap: 64px; align-items: start; }
/* --muted (5.86:1 on paper), not --faint (3.16:1) — these are the labels that
   name the studio's contact details and the form's fields, so they have to pass
   AA rather than sit at the decorative tier. */
.contact-info-label { margin: 0 0 6px; font-size: 12px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); }
.contact-info-value { margin: 0 0 28px; font-size: 16px; line-height: 1.6; color: var(--ink); }

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-label { display: block; margin: 0 0 7px; font-size: 12px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); }
.form-required { color: var(--accent); margin-left: 3px; }
.form-note { margin: 0 0 20px; font-size: 13px; line-height: 1.5; color: var(--muted); }
.field { grid-column: span 1; }
.field.span-2 { grid-column: span 2; }
/* The border was `rgba(0,0,0,0.12)`: 1.32:1 against the white fill, which is
   itself 1.12:1 against the paper ground. Nothing else marked the boundary, so
   the four fields were very nearly invisible -- against the 3:1 that WCAG
   1.4.11 asks of a control's boundary, and on this site's own stated AA
   standard, that is a bug rather than a preference.
   0.50 of ink over the white fill composites to rgb(144,143,141) and measures
   3.23:1. The alpha is the whole point of that sentence: a translucent border
   has to be composited against what is behind it before it is measured, and
   reading the ink's own 16.45:1 instead is how the first attempt at this fix
   shipped at 0.38 -- which looks like a reasonable number and lands at 2.32:1,
   still under the floor. */
.field input, .field textarea, .field select {
	width: 100%; border: 1px solid rgba(33,31,27,0.50); border-radius: var(--r-form);
	padding: 14px 16px; font-size: 16px; font-family: inherit; background: var(--white);
	color: var(--ink);
}
.field textarea { resize: vertical; }
/* A native select, deliberately. This control has to work on a locked-down
   government machine and under a screen reader, and nothing custom is worth
   that risk for four options. Only the arrow is ours: `appearance: none` drops
   the platform's, which is the one part of a select that never matches
   anything. Right padding clears the mark; `background` carries it as an
   inline SVG so it needs no request and inherits nothing. */
.field select {
	appearance: none; -webkit-appearance: none;
	padding-right: 42px; cursor: pointer; line-height: 1.3;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='11' viewBox='0 0 24 24' fill='none' stroke='%23211f1b' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 9l7 7 7-7'/%3E%3C/svg%3E");
	background-repeat: no-repeat;
	background-position: right 16px center;
}
/* Guidance that stays. This copy used to be the textarea's placeholder -- the
   most product-specific line on the page, and it deleted itself the moment
   anyone typed. Sits between the label and the control so it is read in that
   order, and is wired up with `aria-describedby` so it is heard there too. */
.field-hint { margin: -2px 0 7px; font-size: 13px; line-height: 1.5; color: var(--body-muted); }
/* Errors name the field they belong to and sit under it, not only in a summary
   at the top of the form. The warning mark before the text means the message
   does not rely on colour alone to read as an error, which matters here because
   the same sentence has to work for someone who cannot see that it is red. */
.field-error { margin: 7px 0 0; font-size: 13px; line-height: 1.5; color: var(--error-ink); }
.field-error::before { content: "\26A0\FE0E"; margin-right: 6px; }
.field [aria-invalid="true"] { border-color: var(--error-ink); }
.contact-form .btn-solid { margin-top: 18px; border-radius: var(--r-form); }
.contact-notice { margin: 0 0 20px; padding: 14px 16px; border-radius: var(--r-form); font-size: 14px; line-height: 1.55; }
.contact-notice p { margin: 0; }
.contact-notice p + p { margin-top: 8px; }
.contact-notice.is-success { background: #e7f3e8; color: #205c25; }
.contact-notice.is-error { background: #fbe7e7; color: #7a2020; }
.contact-notice a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
/* The notice takes focus on load so it is announced. `:focus-visible` does not
   match a programmatic focus, so the ring never drew and a sighted keyboard
   user saw the coloured bar with no sign that focus had moved to it -- the next
   Tab then arrived at the first field from nowhere. `:focus` is correct for the
   same reason it is correct on `.skip-link`. */
.contact-notice:focus, .contact-done:focus { outline: 2px solid var(--accent); outline-offset: 3px; }

/* A heading over each column. The page had exactly one heading in the whole
   document, so a screen reader's heading list -- the usual way of skipping to
   the thing you came for -- offered a single stop. */
.contact-col-head { margin: 0 0 22px; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: 22px; letter-spacing: -0.2px; }

/* The confirmation replaces the form rather than sitting above an empty one. */
.contact-done { background: var(--card); border: 1px solid var(--hairline-soft); border-radius: var(--r-card); padding: 30px 32px; }
.contact-done-head { margin: 0; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: 26px; line-height: 1.25; letter-spacing: -0.3px; }
.contact-done-body { margin: 12px 0 0; font-size: 16px; line-height: 1.65; color: var(--body-muted); }
.contact-done-next { margin: 20px 0 0; padding: 18px 0 0; list-style: none; border-top: 1px solid var(--hairline); }
.contact-done-next li { margin: 0 0 9px; font-size: 14px; line-height: 1.6; color: var(--body-muted); }
.contact-done-next li:last-child { margin-bottom: 0; }
.contact-done-next a { color: var(--ink); text-decoration: none; border-bottom: 1px solid rgba(0,0,0,0.18); }
.contact-done-next a:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* ---------------------------------------------------------------- 404 --- */
.error-404 { padding: 140px 0; text-align: center; }
.error-404 .eyebrow { text-align: center; }
.error-404 .detail-title { max-width: 22ch; margin: 0 auto; }
.error-404-body { margin: 20px auto 0; max-width: 52ch; }
.error-404-actions { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-top: 40px; }

/* Both of these are the only thing on their line, so inline-block padding grows
   the line box to clear the 24x24 of WCAG 2.5.8 without moving anything around
   them -- 18px and 17px tall as bare text, which is what they measured before.
   An `::after` overlay would have been the other option and is the wrong one
   here: these sit inside running text, where an outset box would reach into the
   line above. */
.contact-tel { display: inline-block; padding: 5px 0; color: inherit; text-decoration: none; border-bottom: 1px solid rgba(0,0,0,0.18); }
.contact-tel:hover { color: var(--accent); border-bottom-color: var(--accent); }
/* 6px, not 4. At 14px this line box is 14.7px tall, so 4px each side reached
   22.7 and stopped 1.3px short of the floor -- close enough to look right and
   still a failure. Measured, not estimated. */
.map-card-link a { display: inline-block; padding: 6px 0; }

/* Off-screen rather than `display: none`, because the bots worth stopping fill
   hidden inputs. Not `.visually-hidden`: that is for content a screen reader
   SHOULD read, and this is the one field nobody should ever meet. */
.jg-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }


/* Two registers, one form. The project wording is what renders by default, so
   it is also what renders when the script never arrives -- at which point the
   portfolio field is simply visible on every route, which is a stray optional
   field rather than a broken form. Everything here is scoped to `.is-scripted`
   for that reason: with no JavaScript, nothing hides.
   `data-mode` is written server-side, so the right fields are in the first
   paint and a bounced careers submission does not visibly rearrange itself. */
.mode-careers { display: none; }
.is-scripted form[data-mode="careers"] .mode-careers { display: inline; }
.is-scripted form[data-mode="careers"] .mode-project { display: none; }
.is-scripted form[data-mode="project"] .field-portfolio { display: none; }

.map-card { border-radius: var(--r-hero); overflow: hidden; background: var(--card); border: 1px solid var(--hairline-soft); }
.map-card-cap { padding: 40px 44px 30px; }
/* Was an inline style on a <p>. It is the heading of this band in everything
   but markup, and the page could not afford to keep pretending otherwise: there
   was exactly one heading in the whole document. */
.map-card-line { margin: 0; max-width: 52ch; font-family: "Newsreader", Georgia, serif; font-weight: 400; font-size: clamp(20px, 2.4vw, 28px); line-height: 1.35; letter-spacing: -0.2px; }
.map-card-link { margin: 16px 0 0; font-size: 14px; }
.map-image-wrap { position: relative; }
/* The live map sits on top of the static one and is invisible until it has
   drawn. Both occupy the same box, so nothing reflows when the swap happens and
   nothing is missing if it never does. `.has-live-map` is set by map.js on
   MapLibre's `load` event -- after the first frame, not on script start. */
/* Scoped to the wrapper for specificity, not for tidiness. MapLibre puts
   `.maplibregl-map { position: relative }` on the container it is given, and its
   stylesheet is enqueued after this one -- so at equal specificity it won.
   `position: relative` then makes `inset: 0` a set of offsets rather than a
   size, the container collapsed to 0px tall, and the map initialised into
   nothing while reporting no error at all. One extra class outranks it. */
/* Hidden with `visibility`, not just opacity. MapLibre's canvas and zoom
   buttons are focusable, and at `opacity: 0` they stayed in the tab order as
   invisible stops over a picture. `visibility: hidden` takes them out of both
   the tab order and the accessibility tree while keeping the box laid out, so
   WebGL still has a real size to draw into before it is shown. */
.map-image-wrap .map-live {
	position: absolute; inset: 0; width: 100%; height: 100%;
	visibility: hidden; opacity: 0;
	transition: opacity .5s var(--ease), visibility 0s linear .5s;
}
/* One crossfade: the live map fades up over the picture, and the picture is
   hidden only once the fade has finished, so nothing flashes between them. */
.map-card.has-live-map .map-live { visibility: visible; opacity: 1; transition: opacity .5s var(--ease); }
.map-card.has-live-map .map-static { visibility: hidden; transition: visibility 0s linear .5s; }
@media (prefers-reduced-motion: reduce) {
	.map-image-wrap .map-live,
	.map-card.has-live-map .map-live,
	.map-card.has-live-map .map-static { transition: none; }
}
/* MapLibre draws its own chrome. Left alone it arrives in default blue-grey
   with 4px radii, which is another design system's, so the parts that show are
   brought onto this one: the site radius, the paper ground, ink strokes. */
.map-live .maplibregl-ctrl-group {
	background: var(--paper);
	border: 1px solid var(--hairline);
	border-radius: var(--r-form);
	box-shadow: 0 2px 10px rgba(33,31,27,0.10);
}
.map-live .maplibregl-ctrl-group button + button { border-top: 1px solid var(--hairline); }
.map-live .maplibregl-ctrl-group button { width: 32px; height: 32px; }
.map-live .maplibregl-ctrl-group button:hover { background: var(--card); }
.map-live .maplibregl-ctrl-group button span { filter: saturate(0) contrast(1.1); }
.map-live .maplibregl-ctrl-attrib {
	background: rgb(var(--paper-rgb) / 0.82);
	font-size: 11px;
	border-radius: var(--r-form) 0 0 0;
}
.map-live .maplibregl-ctrl-attrib a { color: var(--muted); }
/* The cooperative-gesture overlay ("use ctrl + scroll"): same treatment, so the
   one moment the map interrupts the page still looks like the page. */
.map-live .maplibregl-cooperative-gesture-screen {
	background: rgba(33,31,27,0.42);
	font-family: inherit; font-size: 14px; letter-spacing: 0.2px;
}
/* The studio marker: the firm's mark in a framed tile, with a tail that points.
   It is the same pin the static map has always carried -- `.map-pin-photo` and
   `.map-pin-tail` below -- so the picture and the live map mark the studio the
   same way, and the swap between them does not change what the visitor is
   looking for. A DOM marker rather than a sprite, so it needs no sprite sheet.
   The tail's border box ends at its point, which is what MapLibre anchors to
   with `anchor: 'bottom'`. */
.jg-map-pin { display: flex; flex-direction: column; align-items: center; pointer-events: none; }
.map-image { display: block; width: 100%; height: clamp(320px, 38vw, 440px); object-fit: cover; background: var(--card); }
.map-tint { position: absolute; inset: 0; pointer-events: none; background: var(--accent); opacity: 0.07; }
.map-pin { position: absolute; left: 49%; top: 40%; transform: translate(calc(-50% - 30px), calc(-100% - 7px)); pointer-events: none; display: flex; flex-direction: column; align-items: center; }
.map-pin-photo { width: 50px; height: 50px; border-radius: var(--r-form); overflow: hidden; border: 3px solid #faf9f6; box-shadow: 0 10px 22px rgba(33,31,27,0.38); }
.map-pin-photo img { width: 100%; height: 100%; object-fit: cover; }
.map-pin-tail { width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 11px solid #faf9f6; margin-top: -1px; filter: drop-shadow(0 5px 3px rgba(33,31,27,0.28)); }

/* ================================= Lightbox ================================ */
.jg-lightbox { position: fixed; inset: 0; z-index: 200; background: rgba(18,16,13,0.95); backdrop-filter: blur(10px); display: flex; flex-direction: column; animation: jgFade .28s ease; }
.jg-lightbox[hidden] { display: none; }
.jg-lightbox-top { flex: none; display: flex; align-items: center; justify-content: space-between; padding: 22px 30px; }
.jg-lightbox-counter { font-size: 12px; letter-spacing: 2.5px; color: rgb(var(--paper-rgb) / 0.65); font-variant-numeric: tabular-nums; }
.jg-lightbox-close, .jg-lightbox-nav { border: none; background: rgb(var(--paper-rgb) / 0.1); color: var(--paper); border-radius: 999px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background .2s ease; }
.jg-lightbox-close:hover, .jg-lightbox-nav:hover { background: rgb(var(--paper-rgb) / 0.22); }
.jg-lightbox-close { width: 44px; height: 44px; font-size: 17px; }
.jg-lightbox-nav { flex: none; width: 54px; height: 54px; font-size: 22px; }
.jg-lightbox-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; gap: 18px; padding: 0 24px; }
.jg-lightbox-img { max-width: calc(100% - 160px); max-height: 100%; object-fit: contain; border-radius: var(--r-hero); box-shadow: 0 30px 80px rgba(0,0,0,0.5); }
.jg-lightbox-thumbs { flex: none; display: flex; gap: 10px; overflow-x: auto; padding: 18px 30px 28px; justify-content: safe center; scrollbar-width: none; }
.jg-lightbox-thumbs::-webkit-scrollbar { display: none; }
.jg-lightbox-thumb { flex: none; width: 90px; height: 64px; border-radius: var(--r-form); cursor: pointer; background: #3a352e center/cover no-repeat; border: 2px solid transparent; opacity: 0.5; transition: opacity .25s ease; padding: 0; }
.jg-lightbox-thumb.is-active { border-color: var(--accent); opacity: 1; }

/* ---------------------------------------------- Lightbox, phone and tablet */
/* The photographs take the whole screen: edge to edge on near-black, one per
   swipe, snapping into place under native momentum. The counter, close button
   and caption float over the picture and fade after a moment (a tap brings
   them back), so what is left is the photograph. Turning the phone sideways
   lets a landscape shot fill the screen; nothing is forced, the strip reflows.
   Hidden on desktop, where the single image, arrows and thumbnails remain. */
.jg-lightbox-track, .jg-lightbox-caption, .jg-lightbox-hint { display: none; }
/* The name and place a card or the meta panel already gave the photograph,
   repeated here because the lightbox is the one view with nothing else on
   screen to carry them. A plain caption line above the thumbnails; the
   touch rule below replaces this with a floating scrim, the one place a
   caption has to sit over the picture instead of beside it. */
.jg-lightbox:not(.is-touch) .jg-lightbox-caption {
	display: block; margin: 0; padding: 4px 30px 24px; text-align: center;
	font-size: 14px; line-height: 1.5; color: rgb(var(--paper-rgb) / 0.75);
}
/* Placeholder stock has no alt text to show; an empty scrim or an empty
   band of padding above the thumbnails would both read as a rendering bug
   rather than a photograph with nothing to caption. The #id beats both the
   touch and desktop rules' three classes regardless of source order, since
   this one has to win in both contexts. */
#jg-lightbox .jg-lightbox-caption:empty { display: none; }
.jg-lightbox.is-touch { background: rgb(9 8 7 / var(--lb-dim, 1)); backdrop-filter: none; }
.jg-lightbox.is-touch .jg-lightbox-img,
.jg-lightbox.is-touch .jg-lightbox-nav,
.jg-lightbox.is-touch .jg-lightbox-thumbs { display: none; }
.jg-lightbox.is-touch .jg-lightbox-stage { position: relative; padding: 0; gap: 0; }
.jg-lightbox.is-touch .jg-lightbox-track {
	display: flex; gap: 12px; width: 100%; height: 100%;
	overflow-x: auto; overflow-y: hidden; overscroll-behavior: contain;
	scroll-snap-type: x mandatory; scrollbar-width: none;
	touch-action: pan-x pinch-zoom;
	animation: jgLightboxIn .42s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.jg-lightbox.is-touch .jg-lightbox-track::-webkit-scrollbar { display: none; }
.jg-lightbox.is-touch:not(.is-dragging) .jg-lightbox-track { transition: transform .3s cubic-bezier(0.2, 0.7, 0.2, 1); }
.jg-lightbox-slide { flex: 0 0 100%; height: 100%; display: flex; align-items: center; justify-content: center; scroll-snap-align: center; scroll-snap-stop: always; }
.jg-lightbox-slide img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
@keyframes jgLightboxIn { from { opacity: 0; transform: scale(0.82); } to { opacity: 1; transform: none; } }
/* While swiping, the neighbours ease back and dim and the arriving photograph
   comes forward: depth from the strip's own scroll position, where supported. */
@supports (animation-timeline: view()) {
	.jg-lightbox.is-touch .jg-lightbox-slide img { animation: jgSlideDepth linear both; animation-timeline: view(inline); }
	@keyframes jgSlideDepth { 0% { transform: scale(0.9); opacity: 0.45; } 50% { transform: none; opacity: 1; } 100% { transform: scale(0.9); opacity: 0.45; } }
}
.jg-lightbox.is-touch .jg-lightbox-top {
	position: absolute; top: 0; left: 0; right: 0; z-index: 2;
	padding: max(14px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) 28px max(20px, env(safe-area-inset-left));
	background: linear-gradient(rgb(0 0 0 / 0.5), transparent);
	transition: opacity .35s ease;
}
.jg-lightbox.is-touch .jg-lightbox-caption {
	display: block; position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; margin: 0;
	padding: 36px max(20px, env(safe-area-inset-right)) max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
	background: linear-gradient(transparent, rgb(0 0 0 / 0.62));
	font-size: 13px; line-height: 1.5; color: rgb(var(--paper-rgb) / 0.88);
	pointer-events: none; transition: opacity .35s ease;
}
.jg-lightbox.is-touch:not(.chrome-on) .jg-lightbox-top,
.jg-lightbox.is-touch:not(.chrome-on) .jg-lightbox-caption,
.jg-lightbox.is-touch.is-dragging .jg-lightbox-top,
.jg-lightbox.is-touch.is-dragging .jg-lightbox-caption { opacity: 0; pointer-events: none; }
/* A keyboard user on a tablet never loses the close button. */
.jg-lightbox.is-touch .jg-lightbox-top:focus-within { opacity: 1; pointer-events: auto; }
.jg-lightbox.is-touch .jg-lightbox-close { width: 44px; height: 44px; background: rgb(0 0 0 / 0.35); backdrop-filter: blur(8px); }
.jg-lightbox.is-touch .jg-lightbox-hint.is-on {
	display: flex; position: absolute; left: 50%; bottom: max(96px, calc(env(safe-area-inset-bottom) + 80px)); z-index: 3;
	align-items: center; gap: 12px; padding: 11px 18px 11px 14px; border-radius: 999px;
	background: rgb(0 0 0 / 0.58); backdrop-filter: blur(10px); color: var(--paper);
	font-size: 13px; white-space: nowrap; pointer-events: none;
	animation: jgHint 3.4s ease forwards;
}
.jg-lightbox-hint .jg-rotate-phone { transform-box: fill-box; transform-origin: center; animation: jgRotatePhone 1.6s cubic-bezier(0.6, 0, 0.3, 1) .3s 2 both; }
@keyframes jgHint { 0% { opacity: 0; transform: translate(-50%, 10px); } 10%, 82% { opacity: 1; transform: translate(-50%, 0); } 100% { opacity: 0; transform: translate(-50%, 0); } }
@keyframes jgRotatePhone { 0%, 15% { transform: rotate(0deg); } 55%, 100% { transform: rotate(-90deg); } }
@media (orientation: landscape) { .jg-lightbox.is-touch .jg-lightbox-hint.is-on { display: none; } }

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

/* ========================== Reduced motion =================================
   Not a kill switch. Movement through space goes; opacity, color and state
   stay, because a visitor who asked for less motion still needs to see that
   clicking a dot did something. Autoplay is handled in theme.js, which starts
   both carousels paused under this same query.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {

	/* The Ken Burns is the site's one infinite animation, and pausing the
	   slideshow does not stop it — the zoom lives on .is-active, so it would
	   keep breathing on a paused slide forever. This is the whole reason this
	   block exists. The photograph sits still, and sharper for it. */
	.hero-slide.is-active { animation: none; }

	/* Kept as a fade, not cut to 0s: at 1.6s the crossfade is a dissolve, but
	   removing it entirely makes a dot click look like a broken reload. 250ms
	   is short enough to feel like a swap and long enough to read as caused. */
	.hero-slide { transition-duration: .25s; }

	.jg-lightbox { animation-name: jgFadeFlat; }
	/* The phone strip still snaps, but nothing zooms, scales or rotates. */
	.jg-lightbox.is-touch .jg-lightbox-track { animation: jgFadeFlat .2s ease; }
	.jg-lightbox.is-touch .jg-lightbox-slide img { animation: none !important; }
	.jg-lightbox-hint .jg-rotate-phone { animation: none; }

	/* Deep links to #sector-0X would otherwise animate the whole page. */
	html { scroll-behavior: auto; }

	/* A disclosure's job is the state change, not the sweep. */

	/* Card hover and focus keep every signal except travel: the name still goes
	   accent, the image still brightens, the accent rule still appears — by
	   fading in rather than wiping across, so the affordance survives without
	   the sweep. Both triggers are overridden together: a keyboard visitor who
	   asked for less motion would otherwise get the 12px caption slide that the
	   pointer visitor beside them is spared. */
	.proj:is(:hover, :focus-visible) .proj-img { transform: none; }
	.proj:is(:hover, :focus-visible) .proj-cap { transform: none; }
	.cta:hover .cta-arrow { transform: none; }
	.proj-rule { transform: scaleX(1); opacity: 0; transition: opacity .2s ease; }

	/* The scroller arrows lose the hover-glide in the script, not here. What
	   stays is the circle going accent under the pointer — the whole reason
	   that state exists is so this visitor still gets an answer. The 180ms
	   colour fade is not travel and is left alone. */
	.proj:is(:hover, :focus-visible) .proj-rule { opacity: 1; }

	/* The dots' 1.25x active scale stays: it is a state indicator, not motion,
	   and dropping it would leave color as the only cue. */
}

/* ========================= Target size (all pointers) ======================
   WCAG 2.5.8's 24x24 floor applies at every pointer type, not only touch --
   the earlier work put these accommodations behind `pointer: coarse`, which
   left twelve targets under the floor with a mouse (footer links at 284x16,
   text links at 116x14, the 9px dots). Grown with transparent overlays so
   nothing moves visually; the Touch block below escalates to 44px.
   ========================================================================== */
/* The dot's name, shown on keyboard focus only. The accessible name already
   carries it; this is the sighted keyboard user's half. No hover tooltip --
   that would be furniture for everyone to serve a few. */
.hero-dot[data-label]:focus-visible::before {
	content: attr(data-label);
	/* Above the index, over the photograph's lower edge. */
	position: absolute; bottom: calc(100% + 8px); right: 0;
	white-space: nowrap; padding: 6px 10px; border-radius: var(--r-form);
	background: rgba(20,18,15,0.92); color: var(--ink-on-dark);
	font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
}

/* `.sector-viewall` joined this list late and was missed: it shipped at 18px
   with no expansion at any width, against 44.67px for `.text-link-accent`
   three hundred pixels below it — and it is the only control that escapes a row
   showing 14.6% of its contents. Any new link class belongs here on the same
   commit that creates it; this is the second time the omission has shipped. */
:where(.text-link-muted, .text-link-accent, .detail-back, .sector-item-title, .sector-viewall, .work-index-link, .principal-contact a) { position: relative; }
:where(.text-link-muted, .text-link-accent, .detail-back, .sector-item-title, .sector-viewall, .work-index-link, .principal-contact a)::after {
	content: ""; position: absolute; inset: -5px 0;
}

/* 16px links on a 25px pitch: -4px reaches the floor without the overlays of
   two stacked links meeting. `.evidence-list a` joins this rule rather than
   the wider -5px one above: its 14px text renders a 16px box on a 25px pitch
   (16px line + 9px margin), so -4px lands exactly at the 24px floor with 1px
   to spare between hit areas — the wider inset would overlap the next item. */
:where(.footer-links a, .footer-ftp, .evidence-list a) { position: relative; }
:where(.footer-links a, .footer-ftp, .evidence-list a)::after { content: ""; position: absolute; inset: -4px 0; }

/* The team row's email sits in a clipped, ellipsised line, so an absolutely
   positioned overlay would be cut off by the clip. Inline-block padding grows
   the line box instead: 15px + 10 = 25px. */
.team-row-email a { display: inline-block; padding: 5px 0; }

/* ================================= Touch ===================================
   Keyed to pointer type rather than viewport width: a touch laptop at 1440px
   needs the same targets as a phone, and a width breakpoint would miss it.
   Hit areas grow; nothing gets visually heavier — the dots stay 9px, the
   underlines stay put. WCAG 2.5.8 asks for 24×24; primary controls go to 44
   where the layout has room for it.
   ========================================================================== */
@media (pointer: coarse) {

	/* 44px tall and still one row at 375px: the five items measure 245px of
	   labels plus 72px of gaps against 335px of content width. */
	.nav-link { padding: 15px 0; }

	.hero-pause { width: 44px; height: 44px; }

	/* Arrow removal moved out of this block — see the `hover: none` query below.
	   `pointer: coarse` matches a touchscreen laptop being driven with a mouse,
	   so hiding them here took the arrows off desktops that have no other way
	   to page a row. Everything else in this block only grows tap targets,
	   which is harmless when it over-applies; hiding a control is not. */

	/* Block-level controls: padding is free to grow the box. */
	.text-link-muted,
	.detail-back { padding-top: 12px; }
	.sector-item-title { padding: 5px 0; }

	/* .btn's 13px padding leaves the outline variant at 41px; both variants get
	   the same floor so the two buttons stay the same height side by side. */
	.btn { padding-top: 15px; padding-bottom: 15px; }

	/* Stacked footer links: padding plus a tighter gap, so 36px targets sit on
	   a 40px pitch instead of turning the column into a ladder. */
	.footer-links { gap: 4px; }
	.footer-links a,
	.footer-ftp { padding: 10px 0; }

	/* The studio number was the only footer control left out of this list, so
	   it sat at 25.33px against 44px for everything around it — on a phone, on
	   the control a facilities director is most likely to want. It carries its
	   target as an ::after overlay rather than padding, because padding would
	   push the number off the footer's baseline grid. */
	.footer-tel::after { inset: -15px 0; }

	/* These carry their underline as a border, so padding would detach it from
	   the text. An overlay expands the target without touching the type. */
	.text-link-accent,
	.sector-viewall,
	.work-index-link,
	.principal-contact a { position: relative; }
	.text-link-accent::after,
	.sector-viewall::after,
	.work-index-link::after,
	.principal-contact a::after { content: ""; position: absolute; inset: -14px 0; }

	/* The team row's email is the last thing in its grid row, so an overlay
	   expanding past it would reach into the card's padding and overlap the
	   phone number's target above — inline-block padding grows the line box
	   instead, which the row's own height then accounts for. */
	.team-row-email a { display: inline-block; padding: 8px 0; }
	.team-row-phone a { padding: 8px 0; }
}

/* =============================== Responsive ================================ */
/* The arrows ship on every device, deliberately.

   They were hidden under `pointer: coarse`, which is true of a laptop with a
   touchscreen being driven with a mouse — so they vanished for an owner who had
   no swipe to fall back on. Narrowing that to `(hover: none) and (pointer:
   coarse)` still hid them on his machine, so the query is gone rather than
   narrowed again: a control the owner asks to see is not a control to guess at.

   Safe to show on touch because the band already yields: `.hs-arrow` is
   `pointer-events: none` and only the 36px circle is `auto`, so a tap aimed at
   the card beneath passes straight through the gradient. A finger keeps the
   swipe; the arrows are simply also there. */

@media (max-width: 880px) {
	/* 110px is this band's tallest measured header (at ~790px, where the nav
	   wraps below the wordmark), not its shortest. Overshooting lands an
	   anchor a little low; undershooting puts it behind the header, and only
	   one of those is a bug. `initHeaderHeight` replaces this with the real
	   number as soon as it runs. */
	:root { --header-h: 110px; }

	/* One column of groups: two of them at 335px leaves ~140px a column, and
	   these runs are ten place names long. */
	.evidence-band { gap: 36px; }
	.evidence-credentials, .evidence-geo { grid-template-columns: 1fr; gap: 28px; }
	/* Twelve client names two-up at 335px gives ~14ch a column. */
	.evidence-list.is-2up { grid-template-columns: 1fr; }
	/* The nav is the incompressible half of the lockup. Tighter gaps keep the
	   row intact where it fits, and `flex-wrap` lets it drop to a second line
	   where it does not -- at 580px the two halves used to force horizontal
	   overflow, which is worse than wrapping. No new breakpoint needed. */
	/* The header measured 135px at 720px against 120px at 560px: a visitor on a
	   small tablet got a taller header than one on a phone, because this band
	   let the nav wrap to a second row while keeping the full 18px of desktop
	   padding, and only the 560px block below tightened it. The tightening
	   belongs here, where the extra row appears. `flex-wrap` stays as the
	   safety net for a longer nav; the clamped gap is what keeps it unused. */
	.nav-row { flex-wrap: wrap; row-gap: 10px; padding-top: 13px; padding-bottom: 13px; }
	.wordmark { min-width: 0; }
	.nav-links { flex-wrap: wrap; row-gap: 8px; }
	/* 880 is where the links already wrapped to a second row, so the panel menu
	   takes over at the width the inline nav had started to fail rather than at
	   a third breakpoint invented for it. The swap is all-or-nothing: the inline
	   nav goes, the button arrives, and there is never a moment where both are
	   navigation. `.is-scripted` is set by theme.js, so a browser without it
	   keeps the wrapped links instead of losing the nav to a dead button. */
	.is-scripted .nav-links { display: none; }
	.is-scripted .nav-menu-toggle { display: grid; }
	/* Pinned, not a flex sibling. In flow the button competed with the wordmark
	   for a 375px row -- the kicker alone runs ~300px of the 281px left beside a
	   42px control -- so it wrapped to a second line and landed bottom left,
	   the corner furthest from a thumb. Taken out of flow it sits top right at
	   every width and the wordmark keeps the full row for its two stacked lines. */
	.nav-row { position: relative; }
	.is-scripted .nav-menu-toggle { position: absolute; top: 13px; right: 32px; }
	.is-scripted .wordmark { padding-right: 48px; }
	/* The kicker is 36 characters and its 3px tracking alone runs ~300px, so on a
	   375px row beside a 42px button it broke to two lines. Tracking scales with
	   the viewport instead: it keeps the letterspaced register the wordmark is
	   built on.
	   The coefficient dropped from 0.5vw to 0.35vw when the kicker's own font-size
	   went from 9.5px to 11px to clear the site's functional-text floor -- the
	   wider glyphs meant 0.5vw's tracking (1.8px at 360px, 1.875px at 375px) no
	   longer left room for the extra width, verified by measuring the rendered
	   line at each width rather than by the formula alone. 0.35vw keeps one line
	   through 360-880px; below ~360px no letter-spacing legal under the 1.2px
	   floor recovers a single line at 11px, and `text-wrap: balance` on the base
	   rule is what carries that width. */
	.wordmark-kicker { letter-spacing: clamp(1.2px, 0.35vw, 3px); }
	/* One column: at 880px a two-up quote falls under ~34ch and breaks up. */
	.testimonial-grid { grid-template-columns: 1fr; gap: 44px; }
	.detail-grid, .contact-grid, .sector-row, .story-row, .whatwedo-row { grid-template-columns: 1fr !important; gap: 36px !important; }
	/* The globe stays square -- it draws off the smaller of its box's two
	   dimensions, so a fixed height only letterboxes it. Capping the plate caps
	   the height with it: a full-bleed 335px square would take 320px of an
	   812px screen for what is, stacked, an illustration. */
	.evidence-plate { max-width: 320px; }
	.meta-col, .sector-sticky { position: static !important; top: auto !important; }
	/* Stacked under the body text, the plate keeps its own compact scale
	   rather than growing to the full column -- the same reasoning as
	   .evidence-plate just above: a photograph beside a short list of facts
	   should stay a plate, not become a second hero once the column is the
	   only one left. The fact card below it is unaffected and stays full
	   width, matching every other stacked meta-card on the site. */
	.detail-plate { max-width: 340px; }
	/* .meta-col's own DOM position -- after the body text, which is where it
	   sits beside it on desktop -- would otherwise bury a single-plate
	   project's only photograph below a full column of body copy once the
	   grid stacks to one column, the opposite of every other project page on
	   the site, which opens on its photograph. display:contents dissolves
	   the wrapper so the plate and the fact card rejoin .detail-grid's own
	   single-column flow as independent items, and order moves the
	   photograph back to the top: photograph, then story, then facts. */
	.meta-col { display: contents; }
	.detail-plate { order: -1; }
	.grid-3, .principals-grid { grid-template-columns: repeat(2, 1fr) !important; }
	.team-grid { grid-template-columns: repeat(2, 1fr) !important; }
	/* The email slides out to its own full-width row beneath the photograph, and
	   the photograph goes back to sizing from the name/role/phone block alone.
	   Inline it needs about 185px, and the narrow end of the two-column band --
	   a 700px screen -- leaves it 150: it wraps, the text block grows, the
	   square grows with it, and the text narrows again. At 320 that loop used to
	   settle at a 143px portrait taking 51% of the card with 79px left for the
	   address. The full-width row breaks it, and gives the address the single
	   line it is meant to be read on. */
	.footer-grid { grid-template-columns: 1fr 1fr !important; }
	.gallery-mosaic { grid-template-columns: 1fr 1fr !important; grid-auto-rows: auto !important; }
	.gallery-mosaic > * { grid-column: 1 / -1 !important; grid-row: auto !important; aspect-ratio: 4/3 !important; }
}
@media (max-width: 560px) {
	/* The artifact has to lead on the page that IS the work. At 375 the first
	   photograph's top edge sat at 751px of an 812px viewport — a 239×61px strip
	   of one building, 92% of the first screen given to chrome and framing
	   text. This is DESIGN.md's own Ratio-Not-Height Rule recurring on /work/.
	   Two levers: less air above, and a wider plate. */
	/* Reclaiming height ahead of the first photograph. The index below costs
	   ~70px here, closed, so it has to come from somewhere: this trims padding and the
	   sticky block's internal margins rather than the copy, which is the
	   biggest remaining lever and not mine to cut. */
	.work-intro { padding: 20px 0 10px; }
	.work-intro p:not(.eyebrow) { margin-top: 16px; }
	.sector-row { padding: 24px 0 36px; }
	/* Below 560px the row of names gives way to the <details> contents list.
	   The row, as one sideways-scrolling line, put three of the six sectors
	   327px off-screen behind a swipe; it is simply not shown here now. */
	.work-index { padding: 0; }
	.work-index-list { display: none; }
	/* Set as this page's table of contents, not as a form control. A first
	   version borrowed the contact form's select -- a white bordered box opening
	   a white card -- and read as software dropped onto an editorial page, where
	   nothing else is a white box. This uses only what the page already says:
	   hairline rules, tracked caps, and the bronze "01" and serif name each
	   sector below is introduced with. */
	.work-index-menu { display: block; margin-top: 18px; }
	/* No box. A rule above, the label in tracked caps, a bronze chevron; the
	   first sector's own top rule closes the band beneath it. */
	.work-index-summary {
		display: flex; align-items: center; justify-content: space-between; gap: 12px;
		min-height: 52px; border-top: 1px solid var(--hairline);
		font-size: 12px; letter-spacing: 2px; text-transform: uppercase; color: var(--ink);
		list-style: none; cursor: pointer;
		-webkit-tap-highlight-color: transparent;
	}
	.work-index-summary::-webkit-details-marker { display: none; }
	.work-index-summary svg { flex: none; color: var(--accent); transition: transform .35s var(--ease); }
	.work-index-menu[open] .work-index-summary svg { transform: rotate(180deg); }
	.work-index-menu-list { margin: 0; padding: 0; list-style: none; }
	/* The one authored moment: the contents settle in, a short fade and a
	   6px drop, rather than popping. A keyframe rather than a transition,
	   because <details> shows its content by display and a transition has
	   nothing to run from; an animation plays on the frame it appears. */
	.work-index-menu[open] .work-index-menu-list { animation: jg-index-in .32s var(--ease) both; }
	.work-index-menu-list > li { border-top: 1px solid var(--hairline-soft); }
	/* Each entry the way its sector is introduced below: the bronze index, then
	   the name in Newsreader. The whole 52px row is the link. */
	.work-index-menu-link {
		display: flex; align-items: center; gap: 16px;
		min-height: 52px; color: var(--ink); text-decoration: none;
		-webkit-tap-highlight-color: transparent;
	}
	.work-index-num {
		display: block; flex: none; width: 24px;
		font-size: 12px; letter-spacing: 2px; color: var(--accent); font-variant-numeric: tabular-nums;
	}
	.work-index-name {
		font-family: "Newsreader", Georgia, serif; font-weight: 400;
		font-size: 21px; line-height: 1.2; letter-spacing: -0.2px;
		transition: color .25s ease;
	}
	.work-index-menu-link:hover .work-index-name,
	.work-index-menu-link:active .work-index-name { color: var(--accent); }
	@media (prefers-reduced-motion: reduce) {
		.work-index-summary svg, .work-index-name { transition: none; }
		.work-index-menu[open] .work-index-menu-list { animation: none; }
	}
	.sector-name { margin-top: 8px; }
	.sector-blurb { margin-top: 10px; }
	.sector-viewall { margin-top: 14px; }
	.sector-row { padding: 36px 0; }
	.sector-archive-intro { padding: 24px 0 16px; }

	/* 88%, not 72%. The wider card is 1.5× the photographic area and still peeks
	   the next one, so the swipe affordance survives — measured at 375 that peek is
	   24px, not the ~40px an earlier version of this comment claimed. The peek is
	   also no longer the only cue that more exist, now that each sector states its
	   count in a "View all" link. */
	.hscroll-item { max-width: 88%; }

	/* 164px measured at 375px with a coarse pointer, where the nav takes two
	   rows at touch-sized padding — the tallest the header ever gets. */
	:root { --header-h: 164px; }

	.evidence { padding: 52px 0 56px; }
	/* The country label above its places, rather than a 116px column beside them
	   that leaves ~200px for five installation names. */
	/* Except where the items are short. Client names run to "Houghton Mifflin
	   Harcourt" and need the full width; state names do not, and twelve of them
	   in a single column costs about 130px on the tallest page on the site. */
	.evidence-list.is-compact { grid-template-columns: 1fr 1fr; }
	/* On a phone the hero fills the screen again, and the photograph takes all
	   the height between the headline and the project line rather than holding
	   16:9 -- at this width a 16:9 frame is a short strip. The numbered index
	   is hidden (a swipe on the photograph moves between projects; see
	   initHero), and the pause button moves into the photograph's bottom-right
	   corner. A grid does the placing: `display: contents` lifts the caption
	   band's two parts into it, so the pause control can share the
	   photograph's cell while the credit keeps its own row. */
	.hero-wrap {
		display: grid; grid-template-rows: auto minmax(240px, 1fr) auto;
		grid-template-areas: "intro" "stage" "credit";
		height: calc(100vh - var(--header-h) - 44px);
		height: calc(100svh - var(--header-h) - 44px);
		min-height: 520px;
	}
	.hero-intro { grid-area: intro; padding-bottom: 18px; }
	.hero-stage { grid-area: stage; display: flex; place-items: stretch; min-height: 0; container-type: normal; }
	.hero { flex: 1 1 auto; width: auto; height: auto; margin: 0 var(--hero-margin); }
	.hero-caption { display: contents; }
	.hero-credits { grid-area: credit; min-height: 76px; padding: 18px 20px 22px; }
	.hero-dot { display: none; }
	.hero-dots { grid-area: stage; align-self: end; justify-self: end; z-index: 3; margin: 0 calc(var(--hero-margin) + 12px) 12px 0; }
	.hero-pause {
		margin-left: 0; border: none;
		background: rgb(var(--paper-rgb) / 0.92);
		backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
	}

	/* Header: 145px was 17.8% of an 812px screen and clipped content passing
	   under it. The clamped wordmark and tighter rows take it down. The
	   vertical padding now arrives from the 880px block, which tightens at the
	   point the nav starts needing a second row rather than only down here. */
	.wordmark-kicker { margin-top: 4px; }
	/* The band's own 40px padding costs 80px of a 335px column, dropping the
	   quote measure to ~27ch. Tightening it to 26px buys back ~30ch. The band
	   is tall on a phone because it now shows all four quotes -- that is the
	   point, and a timer that hid three of them was the defect. */
	.testimonial-band { padding: 34px 26px; }
	.testimonial-grid { gap: 40px; }
	.testimonial-band .text-link-accent { margin-top: 40px; }
	/* One rule, not two. A second `.nav-row` block above used to set `gap: 9px`
	   and the vertical padding, both of which this one already overrode — the
	   kind of pair that makes a header look like it has two minds. */
	/* The row no longer stacks. Column direction existed for the wrapped link
	   list, which below 880 is now a single button instead -- stacking left the
	   menu control marooned under the wordmark at the bottom left, the one
	   corner nobody reaches for. Wordmark left with its kicker beneath it,
	   button top right, the way the header reads at every other width. */
	.nav-row { align-items: flex-start !important; gap: 12px !important; padding-left: 20px !important; padding-right: 20px !important; }
	/* Follows the row's own padding down from 32. */
	.is-scripted .nav-menu-toggle { right: 20px; }
	/* `column-gap`/`row-gap`, not `gap` -- the previous `gap: 18px !important`
	   set both axes and then beat the `row-gap: 10px` sitting in its own
	   declaration block, because an important shorthand outranks a
	   non-important longhand regardless of source order. Two separate
	   attempts to close this gap were defeated that way, which is why the
	   nav read as broken rather than wrapped: 18px between items across a
	   row against 48px between the rows themselves. At row-gap 0 the
	   `.nav-link` padding is the row rhythm, as it was meant to be. */
	.nav-links { width: 100%; column-gap: 18px !important; row-gap: 0 !important; flex-wrap: wrap; }
	.container, .footer-grid { padding-left: 20px !important; padding-right: 20px !important; }
	.grid-3, .team-grid, .footer-grid, .principals-grid { grid-template-columns: 1fr !important; }
	/* A 3:1 band is 120px tall on a phone; 2:1 leaves the name room to breathe. */
	.detail-hero.is-stand-in { aspect-ratio: 2 / 1; }
	/* The proof strip already stacks at every width; a phone just needs the
	   rows closer together than the 20px desktop uses. */
	.proof-row { gap: 8px; }
	/* All four agencies on one line at every phone width: the 24px clamp
	   floor plus the 36px desktop gap between four names left only USACE
	   room to wrap to its own line at 375-430px. 21px keeps a comfortable
	   margin at the narrowest phones tested (335px available at 375px
	   viewport, ~34px to spare) without dropping far below the 22px this
	   line held before it took .intro-statement's weight; every wider
	   phone keeps the clamp's room to grow past it. */
	.proof-agencies { gap: 8px 12px; font-size: 21px; }
	.gov-quals-reg { margin-left: 0; }
	.gov-quals-row { flex-direction: column; align-items: flex-start; gap: 16px; }
	/* The full name wraps to its own line under the abbreviation rather than
	   fighting it for width on a phone -- matching .split-head's own column
	   stack immediately below in this file. */
	.gov-agency-fullname { display: block; margin-left: 0; margin-top: 4px; }
	/* A narrower edge fade on a phone: 72px was nearly all of the next card's
	   visible sliver. 52px still clears the 36px arrow with room to fade. */
	.hs-arrow { width: 52px; }
	/* About opens like the home hero on a phone: heading and photograph fill
	   the screen below the header, the photograph taking the height the
	   heading leaves rather than holding 16:9. Same top spacing as the home
	   hero (44px), and a floor so a short screen keeps a usable photo. */
	.about-hero { display: flex; flex-direction: column; height: calc(100vh - var(--header-h)); height: calc(100svh - var(--header-h)); min-height: 520px; }
	.about-intro { flex: none; padding-top: 44px; }
	.about-photo { flex: 1 1 auto; min-height: 0; display: flex; padding: 22px 0 22px; }
	.about-wide-img { flex: 1 1 auto; min-height: 240px; aspect-ratio: auto; max-height: none; }
	/* One card per row again, full width, so the team card goes back to its
	   wide layout -- the email in the text column under the phone number --
	   with a square photograph whose top meets the top of the name and whose
	   bottom meets the bottom of the email. The narrow layout above
	   (max-width: 1160px) is for the two- and three-up grids.

	   The square is sized from the text, so the text's height has to be fixed
	   by the layout rather than by each device's font metrics: every line gets
	   an explicit line-height, and the phone and email links drop the padding
	   that made them tall enough to tap and take that height from a
	   transparent ::after instead, so the tap targets stay at 24px+ without
	   moving anything. The four line boxes then sum to a known height:
	     name 20 + (3 + role 18) + (6 + phone 16) + (8 + email 18) = 89px.
	   The 6 and 8 are what keep the two 24px targets from overlapping. */
	.team-row .avatar { grid-row: 1 / span 2; align-self: start; width: 89px; height: 89px; }
	.team-row:not(:has(.team-row-role)) .avatar { width: 68px; height: 68px; }
	.team-row-name { line-height: 20px; }
	.team-row-role { margin-top: 3px; line-height: 18px; }
	.team-row-phone { margin-top: 6px; line-height: 16px; }
	.team-row-email { grid-column: 2; margin-top: 8px; line-height: 18px; }
	/* inline-block, so each link is exactly its line box (16 and 18px) and the
	   4px overlay makes 24 and 26px targets that meet, never overlap, across
	   the 8px between the lines. */
	.team-row-phone a, .team-row-email a { display: inline-block; position: relative; padding: 0; }
	.team-row-phone a::after, .team-row-email a::after { content: ""; position: absolute; left: 0; right: 0; top: -4px; bottom: -4px; }
	/* The longest address sets at ~190px against 192px of room on a 375px
	   phone -- close enough that it wrapped and pushed the text below the
	   photograph. Half a pixel of type size buys 7px of clearance. */
	@media (max-width: 390px) { .team-row-email { font-size: 12.5px; } }
	/* A common Android width, 360px, needs another half pixel of type and
	   4px less card padding a side before the longest address clears. */
	@media (max-width: 370px) { .team-row { padding: 16px; } .team-row-email { font-size: 12px; } }
	/* The 880px block collapses `.whatwedo-row`, but the sector grid nested
	   inside it kept its two columns all the way down: measured 135.6px and
	   144.6px at 375px with a 56px gutter between them, so 17% of the row was
	   gap and every sector title broke mid-phrase. */
	.whatwedo-grid { grid-template-columns: 1fr; }
	/* One column -- and `.field.span-2` has to come with it. Collapsing the track
	   list alone did nothing: `grid-column: span 2` on the email and message
	   fields made the grid create the second track implicitly, so the "single
	   column" layout stayed two. Measured at 375 the computed tracks were
	   91.33px and 228px, which put the REQUIRED Name field at a quarter the
	   width of the optional Company field beside it, on the primary conversion
	   path, at the width most inbound visitors arrive on. It had been shipping
	   that way. */
	.form-grid { grid-template-columns: 1fr !important; }
	.field.span-2 { grid-column: auto; }
	.split-head, .contact-cta .split-head { flex-direction: column !important; align-items: flex-start !important; gap: 14px !important; }
	.section-tight { padding-top: 60px !important; padding-bottom: 60px !important; }
}
