/* ==========================================================================
   SIEGEL & STRAIN — SITE STYLESHEET
   ==========================================================================
   Loads after assets/css/reset.css (see inc/enqueue.php). Design tokens are
   in section 1, directly below.
   ========================================================================== */


/* ==========================================================================
   1. DESIGN TOKENS
   ==========================================================================
   Every colour, size and duration in this file resolves to one of these.
   ========================================================================== */

:root {

	/* --- Colour ------------------------------------------------------- */

	--color-bg: #ffffff;
	--color-bg-alt: #f9f9f9;
	--color-bg-sunk: #f4f4f4;

	--color-text: #000000;               /* body, h1, h2, p */
	--color-text-muted: #555555;
	/* #767676, not #777: #777 fails WCAG 2.1 AA contrast (4.5:1) by a small
	   margin; #767676 is the lightest value that passes. Do not change this
	   back to #777. */
	--color-text-soft: #767676;          /* h5, default link colour */

	--color-accent: #c01a29;             /* ::selection, nav hover, close hover */
	--color-highlight: #c5ab91;          /* span.highlight — tan */

	--color-rule: #333333;               /* .separator */
	--color-border: #e6e6e6;
	--color-border-light: #eeeeee;

	--color-overlay: rgba(0, 0, 0, 0.55);
	--color-scrim: rgba(0, 0, 0, 0.5);   /* hero tagline background */

	/* --- Type --------------------------------------------------------- */

	--font-body: "PT Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
	--font-heading: var(--font-body);

	--fw-light: 300;
	--fw-normal: 400;
	--fw-bold: 700;

	--fs-base: 1rem;             /* 16px */
	--fs-sm: 0.875rem;           /* 14px — captions, meta, nav */
	--fs-xs: 0.75rem;            /* 12px — labels, detail list */

	/* Headings use clamp() so the four breakpoints don't each need a type
	   override. */
	--fs-h1: clamp(1.75rem, 1.25rem + 2vw, 2.5rem);      /* 28 → 40px */
	--fs-h2: clamp(1.25rem, 1rem + 1vw, 1.625rem);       /* 20 → 26px */
	--fs-h3: clamp(1.0625rem, 1rem + 0.4vw, 1.25rem);    /* 17 → 20px */
	--fs-h4: 1.125rem;
	--fs-h5: 1rem;
	--fs-h6: 0.8125rem;

	/* Endpoints are 43.4px and 15.4px, not the 49.6px a naive `3.1rem`
	   conversion would give; do not simplify this to a round rem value. */
	--fs-hero-tagline: clamp(0.9625rem, -1.954rem + 7.29vw, 2.7125rem);

	--lh-tight: 1.15;
	--lh-heading: 1.3;
	--lh-body: 1.7;

	--ls-wide: 0.1em;            /* uppercase labels */
	--ls-nav: 0.06em;

	/* --- Space -------------------------------------------------------- */

	--space-3xs: 0.25rem;
	--space-2xs: 0.5rem;
	--space-xs: 0.75rem;
	--space-sm: 1rem;
	--space-md: 1.5rem;
	--space-lg: 2.5rem;
	--space-xl: 4rem;

	/* Vertical rhythm between major page sections; grows with the viewport. */
	--space-section: clamp(2.5rem, 1.5rem + 4vw, 5rem);

	/* --- Layout ------------------------------------------------------- */

	--container: 1100px;
	--container-wide: 1400px;
	--gutter: 1.25rem;

	/* Deliberately smaller than production below desktop; do not correct
	   this back up. Read by `.site-header__inner`'s min-height, the desktop
	   nav link's line-height, `scroll-padding-top`, and
	   `--carousel-fit-height` below — a change here moves the header, nav,
	   and both carousels together, by design. */
	--header-height: 48px;

	/* Fits a full-bleed slider to the viewport, less the header and the
	   controls strip beneath it. Shared by the homepage hero and, at
	   desktop, the project gallery.

	   The 48px offset is deliberately smaller than production's reserved
	   64px; do not correct it back. */
	--carousel-fit-height: calc(100svh - var(--header-height) - 48px);

	/* --- Motion ------------------------------------------------------- */

	--ease: cubic-bezier(0.22, 1, 0.36, 1);
	--dur-fast: 150ms;
	--dur: 300ms;
	--dur-slow: 600ms;
	/* main.js's close-animation timeout must stay longer than this. */
	--dur-dialog: 500ms;
	/* The loader overlay's fade-out, deliberately quicker than `--dur-slow`.
	   It fades white-on-white — the page beneath is the same colour — so a
	   long fade buys no smoothness and only delays the homepage intro, which
	   cannot start until the overlay is clear (see the loader styles and
	   main.js's `LOADER_FADE`, which must stay in step with this value). */
	--dur-loader-out: 250ms;

	/* --- Depth -------------------------------------------------------- */

	--shadow-card: 0 5px 25px rgba(0, 0, 0, 0.12);
	--shadow-dialog: 0 0 40px rgba(0, 0, 0, 0.8);

	--z-nav: 100;
	--z-header: 200;
	/* Nothing in the theme uses --z-consent since the theme's own banner was
	   removed (2026-08-15); Complianz sets its own. Kept to document where
	   the cookie banner sits in the stack — start here if it ever fights the
	   header. */
	--z-consent: 900;
	--z-loader: 9999;
}

@media (min-width: 1025px) {

	:root {
		--gutter: 2rem;
		--header-height: 78px;
	}
}

@media (min-width: 1441px) {

	:root {
		--header-height: 100px;
	}
}


/* ==========================================================================
   2. FONTS
   ==========================================================================
   Self-hosted: no request to fonts.googleapis.com, keeping one third party
   off the consent banner and one less DNS + TLS round trip before first
   paint.

   Regular and Bold are the only weights in use — PT Sans has no real 300 or
   800 cut, so `var(--fw-light)` deliberately falls back to this regular
   face. Regular italic gets its own face so the browser doesn't fake-slant
   it; bold italic is unused and skipped.
   ========================================================================== */

@font-face {
	font-family: "PT Sans";
	src: url("../fonts/pt-sans-400.woff2") format("woff2");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "PT Sans";
	src: url("../fonts/pt-sans-700.woff2") format("woff2");
	font-weight: 700;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "PT Sans";
	src: url("../fonts/pt-sans-400italic.woff2") format("woff2");
	font-weight: 400;
	font-style: italic;
	font-display: swap;
}


/* ==========================================================================
   3. BASE DOCUMENT
   ========================================================================== */

html {
	scroll-behavior: smooth;
	/* Anchor targets clear the sticky header rather than hiding behind it. */
	scroll-padding-top: calc(var(--header-height) + 1rem);
}

body {
	background-color: var(--color-bg);
	color: var(--color-text);
	font-family: var(--font-body);
	font-size: var(--fs-base);
	font-weight: var(--fw-normal);
	line-height: var(--lh-body);
	display: flex;
	flex-direction: column;
	min-height: 100vh;
}

::selection {
	background-color: var(--color-accent);
	color: #ffffff;
}

/* Stop the page scrolling behind an open menu or dialog. */
body.nav-open,
body.dialog-open {
	overflow: hidden;
}

.site {
	display: flex;
	flex-direction: column;
	flex: 1;
	width: 100%;
}

.site-main {
	flex: 1;
}

:focus-visible {
	outline: 2px solid var(--color-accent);
	outline-offset: 3px;
}

:focus:not(:focus-visible) {
	outline: none;
}


/* ==========================================================================
   4. TYPOGRAPHY
   ==========================================================================
   Headings carry their case in CSS, never in the stored content: a title
   stored in Title Case reads correctly in menus, breadcrumbs, <title> tags
   and search results, and appears uppercase here via `text-transform`.
   ========================================================================== */

/* Every heading names `--font-heading` explicitly rather than inheriting the
   body family. It resolves to the same face today, but when the branding
   package lands and headings move to a display font, that is a one-token
   change with nothing to hunt down. */

h1,
.h1 {
	font-family: var(--font-heading);
	font-size: var(--fs-h1);
	font-weight: var(--fw-bold);
	line-height: var(--lh-tight);
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var(--color-text);
}

h2,
.h2 {
	font-family: var(--font-heading);
	font-size: var(--fs-h2);
	font-weight: var(--fw-bold);
	line-height: var(--lh-heading);
	letter-spacing: 0.01em;
	color: var(--color-text);
}

h3,
.h3 {
	font-family: var(--font-heading);
	font-size: var(--fs-h3);
	font-weight: var(--fw-bold);
	line-height: var(--lh-heading);
	text-transform: none;
	color: var(--color-text);
}

h4,
.h4 {
	font-family: var(--font-heading);
	font-size: var(--fs-h4);
	font-weight: var(--fw-normal);
	line-height: var(--lh-heading);
	text-transform: uppercase;
	letter-spacing: var(--ls-nav);
	color: var(--color-text-muted);
}

h5,
.h5 {
	font-family: var(--font-heading);
	font-size: var(--fs-h5);
	font-weight: var(--fw-bold);
	line-height: var(--lh-heading);
	/* h5 only ever appears in editor content, never in a template, so
	   nothing else depends on this colour. */
	color: var(--color-text-soft);
}

h6,
.h6 {
	font-family: var(--font-heading);
	font-size: var(--fs-h6);
	font-weight: var(--fw-normal);
	line-height: var(--lh-heading);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	color: var(--color-text-muted);
}

/* --- Links ----------------------------------------------------------------- */

a {
	color: var(--color-text);
	text-decoration: none;
	transition: color .3s ease-in, opacity .3s ease-in;
}

a:hover,
a:focus-visible {
	color: var(--color-accent);
}

/* --- Prose ----------------------------------------------------------------
   `.prose` wraps every block of editor-controlled HTML: page bodies, project
   descriptions, staff bios. It is the only place where bare element selectors
   are allowed to style content, because it is the only place where the markup
   is not written by a template.
   -------------------------------------------------------------------------- */

.prose {
	max-width: 70ch;
}

.prose > * + * {
	margin-top: var(--space-sm);
}

.prose h2,
.prose h3,
.prose h4 {
	margin-top: var(--space-lg);
}

/* `.prose h5`: 15px margin-top (not the 40px `--space-lg` other levels
   use) and 25.14px line-height (not `--lh-heading`'s 1.3) — must stay in
   sync with `.prose h2.section-heading`, which mirrors both values.
   Global: `.prose` wraps every editor-controlled block sitewide. */
.prose h5 {
	margin-top: 15px;
	line-height: 25.14px;
}

/*
 * `.prose h2.section-heading` makes a semantic <h2> look like `.prose h5`'s
 * small bold grey label — used where a page needed a promoted heading level
 * without changing its appearance.
 *
 * Only five properties are declared (margin-top, font-size, line-height,
 * letter-spacing, color); weight, family, margin-bottom, transform, style
 * and display already match `.prose h5` and are deliberately not restated.
 * `letter-spacing: normal` is the one that would be missed by reading the
 * stylesheet alone — the global h2 rule adds 0.26px that `.prose h5`
 * computes as `normal`.
 *
 * A separate rule, not a wider `.prose h5` selector: broadening a shared
 * rule promotes every declaration in it, and `.prose h5` must stay live for
 * any h5 an editor types.
 *
 * Placed after `.prose h2:first-child` deliberately — both are (0,2,1), so
 * source order decides, and landing later keeps a first-child section
 * heading's 15px margin, matching `.prose h5`'s own lack of a first-child
 * exception.
 */
.prose h2:first-child,
.prose h3:first-child {
	margin-top: 0;
}

.prose h2.section-heading {
	margin-top: 15px;
	font-size: var(--fs-h5);
	line-height: 25.14px;
	letter-spacing: normal;
	color: var(--color-text-soft);
}

.prose p {
	line-height: var(--lh-body);
}

.prose a {
	text-decoration: underline;
	text-underline-offset: 0.15em;
	text-decoration-thickness: 1px;
}

/* Scoped to `.prose` — a bare `ul` selector here would also hit every
   navigation list. */
.prose ul {
	padding-left: 1.4em;
	list-style: square outside;
}

.prose ol {
	padding-left: 1.4em;
	list-style: decimal outside;
}

.prose li + li {
	margin-top: var(--space-3xs);
}

.prose blockquote {
	border-left: 2px solid var(--color-rule);
	padding-left: var(--space-md);
	font-style: italic;
	color: var(--color-text-muted);
}

.prose img {
	margin-block: var(--space-md);
}

.prose figcaption {
	font-size: var(--fs-sm);
	color: var(--color-text-soft);
	margin-top: var(--space-2xs);
}

/* `ul.dash-list`: a hanging-dash list. Uses a `::marker` content string
   rather than `position: absolute`, which needs a positioned ancestor it
   doesn't always have. */
.prose ul.dash-list {
	list-style: none;
	padding-left: 1em;
}

.prose ul.dash-list > li::marker {
	content: "– ";
}

.highlight {
	background-color: var(--color-highlight);
	padding: 0 0.2em;
}

/* --- Shared type utilities ------------------------------------------------ */

.eyebrow,
.label-caps {
	font-size: var(--fs-xs);
	font-weight: var(--fw-bold);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	color: var(--color-text-soft);
	line-height: var(--lh-tight);
}

.separator {
	width: 33px;
	height: 1px;
	border: 0;
	background-color: var(--color-rule);
	margin: var(--space-sm) 0;
}


/* ==========================================================================
   5. SITE LOADER
   ==========================================================================
   A white overlay with a row of bars that fill left to right, removed once
   the page has painted (assets/js/main.js, module 1). The positioning half
   lives inline in <head> (inc/enqueue.php) so it paints on the first frame;
   this is everything else.

   Scoped to `html.js` throughout, so with JS off the overlay never renders
   and nobody is trapped behind a spinner that will not resolve. Pure CSS —
   no image request, no third party, nothing for the consent banner to gate.

   Bar proportions (width, height, gap) are sized for the twelve bars
   site-loader.php renders.
   ========================================================================== */

.js #site-loader {
	transition: opacity var(--dur-loader-out) var(--ease), visibility var(--dur-loader-out);
}

.site-loader__bars {
	/* Both tokens are read by the bars and by the keyframes below, so they
	   are declared on the shared parent rather than on each bar. */
	--loader-bar-idle: 0.28;
	--loader-bar-cycle: 1600ms;

	display: flex;
	gap: 7px;
}

.site-loader__bar {
	width: 9px;
	height: 30px;
	background-color: var(--color-accent);

	/* The resting state. It matters that this is declared outside the
	   animation as well as inside it: each bar's `animation-delay` means bar
	   12 has not started animating until 880ms in, and until then it takes
	   its opacity from here. Drop this and the row paints at full strength
	   for the first frame, then the sweep starts — reading as a flash. */
	opacity: var(--loader-bar-idle);

	animation: ss-loader-fill var(--loader-bar-cycle) linear infinite;

	/* `--i` comes from the template's loop. Dividing the cycle by 20 rather
	   than by the bar count keeps the stagger proportional to the timing rather
	   than to how many bars there happen to be, so the sweep speed does not
	   change if the count does — and it leaves the last bar starting at 55% of
	   the cycle, comfortably before the reset. */
	animation-delay: calc(var(--i) * (var(--loader-bar-cycle) / 20));
}

/* Fill fast, hold, then release — not a symmetrical pulse. The long
   plateau keeps the row mostly solid, with only one bar mid-transition
   at a time. */
@keyframes ss-loader-fill {

	0%,
	4% {
		opacity: var(--loader-bar-idle);
	}

	12%,
	92% {
		opacity: 1;
	}

	100% {
		opacity: var(--loader-bar-idle);
	}
}

/* Reduced motion: the overlay still fades, the bars just do not sweep. They
   rest at full strength rather than at `--loader-bar-idle`, because a row of
   pale bars that never resolves reads as broken rather than as still. */
@media (prefers-reduced-motion: reduce) {

	.site-loader__bar {
		animation: none;
		opacity: 1;
	}
}


/* ==========================================================================
   6. HEADER AND SITE MENUS
   ==========================================================================
   One menu list drives both the desktop and mobile presentation; CSS
   switches which one is visible at the laptop breakpoint.
   ========================================================================== */

.site-header {
	position: sticky;
	top: 0;
	z-index: var(--z-header);
	background-color: var(--color-bg);
	border-bottom: 1px solid transparent;
	transition: border-color var(--dur) var(--ease);
}

.site-header.is-stuck {
	border-bottom-color: var(--color-border-light);
}

.site-header__inner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-sm);
	min-height: var(--header-height);
}

/* --- Logo ----------------------------------------------------------------
   A real <img>, not a CSS background — it needs usable alt text.
   -------------------------------------------------------------------------- */

.site-branding {
	flex: 0 1 auto;
	min-width: 0;
}

.site-branding__link {
	display: block;
	transition: opacity var(--dur) var(--ease);
}

a.site-branding__link:hover,
a.site-branding__link:focus-visible {
	opacity: 0.7;
}

.site-logo {
	width: auto;
	height: 30px;
	max-width: 220px;
	object-fit: contain;
	object-position: left center;
}

@media (min-width: 641px) {

	.site-logo {
		height: 38px;
		max-width: 280px;
	}
}

@media (min-width: 1025px) {

	.site-logo {
		height: 45px;
		max-width: 320px;
	}

	/* The header runs full-bleed rather than capped at 1100px like the rest of
	   the site — `.container` here overrides `.site-header__inner`'s own
	   `.container` cap. The qualifier is required: `.container` is declared in
	   section 7, after this rule, so at equal specificity it would win on
	   source order. Do not simplify this to `.site-header__inner`. */
	.site-header__inner.container {
		max-width: none;
		padding-inline: 43px;
	}
}

.site-title {
	font-size: var(--fs-h3);
	font-weight: var(--fw-bold);
	text-transform: uppercase;
	letter-spacing: var(--ls-nav);
}

/* --- Mobile toggle -------------------------------------------------------- */

.nav-toggle {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 44px;
	height: 44px;
	margin-left: auto;
	order: 3;
}

/* Three bars, three complete rules — no shared group, so each selector is
   declared exactly once and there is one place to change any of them.
   `position: relative` on the middle bar is load-bearing: without it the
   absolutely positioned pseudo-elements resolve against the nearest positioned
   ancestor, which is the sticky header, and the icon falls apart. */
.nav-toggle__bars {
	position: relative;
	display: block;
	width: 22px;
	height: 2px;
	background-color: var(--color-text);
	transition: background-color var(--dur-fast) var(--ease);
}

.nav-toggle__bars::before {
	content: "";
	position: absolute;
	display: block;
	width: 22px;
	height: 2px;
	background-color: var(--color-text);
	transform: translateY(-7px);
	transition: transform var(--dur) var(--ease);
}

.nav-toggle__bars::after {
	content: "";
	position: absolute;
	display: block;
	width: 22px;
	height: 2px;
	background-color: var(--color-text);
	transform: translateY(7px);
	transition: transform var(--dur) var(--ease);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bars {
	background-color: transparent;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before {
	transform: rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after {
	transform: rotate(-45deg);
}

@media (min-width: 1025px) {

	.nav-toggle {
		display: none;
	}
}

/* --- Primary navigation, mobile -------------------------------------------
   A full-height panel below the header. Collapsed with `grid-template-rows`
   rather than `max-height: 0`, so it animates to its real height without
   guessing a magic number that breaks when a menu item is added.
   -------------------------------------------------------------------------- */

.primary-nav {
	position: absolute;
	top: 100%;
	left: 0;
	right: 0;
	background-color: var(--color-bg);
	border-bottom: 1px solid var(--color-border-light);
	display: grid;
	grid-template-rows: 0fr;
	transition: grid-template-rows var(--dur) var(--ease);
	overflow: hidden;
}

body.nav-open .primary-nav {
	grid-template-rows: 1fr;
}

.primary-nav__list {
	min-height: 0;
	overflow: hidden;
}

.primary-nav .menu-item {
	position: relative;
	border-top: 1px solid var(--color-border-light);
}

.primary-nav .menu-link {
	display: block;
	padding: var(--space-sm) var(--gutter);
	font-size: var(--fs-sm);
	font-weight: var(--fw-normal);
	text-transform: uppercase;
	letter-spacing: var(--ls-nav);
}

.primary-nav .menu-item[class*="current"] > .menu-link,
.primary-nav .menu-link:hover,
.primary-nav .menu-link:focus-visible {
	color: var(--color-accent);
}

/* Submenu toggle — a real <button> from SSTheme_Nav_Walker, so a dropdown
   parent can be opened from the keyboard. */
.submenu-toggle {
	position: absolute;
	top: 0;
	right: 0;
	width: 48px;
	height: 48px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
}

.submenu-toggle__chevron {
	width: 8px;
	height: 8px;
	border-right: 1.5px solid currentColor;
	border-bottom: 1.5px solid currentColor;
	transform: rotate(45deg) translate(-2px, -2px);
	transition: transform var(--dur) var(--ease);
}

.submenu-toggle[aria-expanded="true"] .submenu-toggle__chevron {
	transform: rotate(225deg) translate(-2px, -2px);
}

.primary-nav .sub-menu {
	display: none;
	background-color: var(--color-bg-alt);
}

.primary-nav .menu-item.is-open > .sub-menu {
	display: block;
}

.primary-nav .sub-menu .menu-link {
	padding-left: calc(var(--gutter) + var(--space-sm));
	font-size: var(--fs-xs);
	color: var(--color-text-muted);
	transition: color var(--dur-fast) var(--ease);
}
.primary-nav .sub-menu .menu-link:hover,
.primary-nav .sub-menu .menu-link:focus {
	color: var(--color-accent);
}

/* --- Primary navigation, laptop and up ------------------------------------
   Horizontal, with hover-or-focus dropdowns.
   -------------------------------------------------------------------------- */

@media (min-width: 1025px) {

	.primary-nav {
		position: static;
		display: block;
		background: none;
		border: 0;
		overflow: visible;
		margin-left: auto;
	}

	.primary-nav__list {
		display: flex;
		align-items: center;
		gap: 0;
		overflow: visible;
	}

	.primary-nav .menu-item {
		border: 0;
	}

	/* 0.9rem (14.4px) with no tracking — `--ls-nav` does not apply to this
	   top-level menu. */
	.primary-nav .menu-link {
		padding: 0 1.5rem;
		font-size: 0.9rem;
		font-weight: var(--fw-light);
		line-height: var(--header-height);
		letter-spacing: normal;
	}

	.primary-nav > ul > .menu-item.has-children > .menu-link {
		/* Leave room for the chevron so it does not sit on the text. */
		padding-right: 2.25rem;
	}

	.submenu-toggle {
		top: 50%;
		right: 0.75rem;
		transform: translateY(-50%);
		width: 24px;
		height: 24px;
	}

	.primary-nav .sub-menu {
		display: block;
		position: absolute;
		top: 100%;
		left: 0;
		min-width: 220px;
		background-color: var(--color-bg);
		border: 1px solid var(--color-border-light);
		box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
		opacity: 0;
		visibility: hidden;
		transform: translateY(-6px);
		transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease), visibility var(--dur);
		z-index: var(--z-nav);
	}

	.primary-nav .menu-item:hover > .sub-menu,
	.primary-nav .menu-item:focus-within > .sub-menu,
	.primary-nav .menu-item.is-open > .sub-menu {
		opacity: 1;
		visibility: visible;
		transform: translateY(0);
	}

	/* Dropdown items intentionally match the top-level size and colour — not
	   a smaller, muted variant. */
	.primary-nav .sub-menu .menu-link {
		line-height: 16px;
		padding: var(--space-2xs) var(--space-sm);
		font-size: 0.9rem;
		letter-spacing: 1px;
		color: var(--color-text);
	}
}

@media (min-width: 1441px) {

	.primary-nav .menu-link {
		padding: 0 2.25rem;
	}
}

/* --- Header actions: social + search --------------------------------------- */

.site-header__actions {
	display: none;
	align-items: center;
	gap: 14px;
}

@media (min-width: 1025px) {

	.site-header__actions {
		display: flex;
	}
}

/* `align-items: center` is load-bearing, not tidying: the two badges are
   different heights (36px and 32px), so without it they stretch/start-align
   and their centres land visibly apart. */
.social-links {
	display: flex;
	align-items: center;
	gap: 6.5px;
}

/* Badge sizes are intentionally asymmetric: LinkedIn is a 36px circle,
   Instagram a 32px rounded square, both on #bbb — reproduced as-is rather
   than normalised. The border below is declared once on the base rule
   rather than per-modifier; box-sizing: border-box (reset.css) carves it
   out of each size automatically. */
.social-links__link {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	border-radius: 3px;
	/* box-sizing: border-box (reset.css, site-wide) means this 3px ring is
	   carved OUT of the 32px/36px box declared above, not added on top —
	   so the outer box stays exactly the measured 32px/36px and the content
	   box becomes 26px/30px for free. Do not compensate the width/height
	   values for the border; that would double-count it. */
	border: 3px solid #f0f0f0;
	background-color: #bbbbbb;
	color: #ffffff;
	font-size: var(--fs-xs);
	font-weight: var(--fw-bold);
	/* Deliberately uses `ease-out`, not the site's `--ease` curve, for this
	   hover transition. */
	transition: background-color var(--dur) ease-out;
}

.social-links__link--linkedin {
	width: 36px;
	height: 36px;
	border-radius: 50%;
	/* Border and content-box sizing inherit from .social-links__link above;
	   no override needed here. */
}

.social-links__icon {
	display: block;
	/* Intentionally different from the LinkedIn icon override below — do not
	   "tidy" these to match. */
	width: 15px;
	height: 15px;
	fill: currentColor;
}

.social-links__link--linkedin .social-links__icon {
	/* This Simple Icons path fills its full 24 viewBox, so the declared size
	   equals the painted size here — unlike the stroked icons below, no
	   scaling correction is needed. */
	width: 9px;
	height: 9px;
}

.social-links__link:hover,
.social-links__link:focus-visible {
	/* Hover state deliberately stays at #999999, not black — border colour
	   and icon colour (#fff) do not change on hover either. */
	background-color: #999999;
	color: #ffffff;
}

/* The 40x40 box is a hit target for WCAG 2.5.8 (24x24 minimum); the icon
   itself is smaller. Colour is deliberately #303030, not `--color-text` —
   do not swap in the token. */
.search-toggle {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	color: #303030;
}

/* A declared SVG size is only the painted size if the path fills its
   viewBox. This magnifier's stroked path covers about 81% of its 24-unit
   viewBox, so 16px here paints roughly 13px of ink. The badge icons above
   don't need this correction — their filled paths fill the full viewBox.
   Scoped to the toggle, not `.icon` itself, which other elements use at
   18px. */
.search-toggle .icon {
	width: 16px;
	height: 16px;
	stroke-width: 2.4;
}

/* No hover state on purpose — only `:focus-visible`. Keyboard focus stays
   visible regardless. */
.search-toggle:focus-visible {
	color: var(--color-accent);
}

.icon {
	width: 18px;
	height: 18px;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.6;
	stroke-linecap: round;
}

/* --- Header search bar -----------------------------------------------------
   A full-bleed white bar that slides down from the top of the header,
   pushing the logo/nav row down. One element in normal document flow at
   the top of <header> (see header.php), so the push-down falls out of
   layout with nothing to keep in sync.
   -------------------------------------------------------------------------- */

/* `visibility` keeps the closed field out of the tab order, and must be
   transitioned rather than toggled — `visibility: hidden` applied
   immediately would cut the height animation off mid-collapse. `0s linear
   150ms` holds visibility until the height finishes animating out; the
   open rule resets the delay to 0s so it appears instantly. */
.site-search {
	height: 0;
	overflow: hidden;
	visibility: hidden;
	background-color: #ffffff;
	transition: height 150ms ease-in-out, visibility 0s linear 150ms;
}

.site-search.is-open {
	height: 50px;
	visibility: visible;
	transition: height 150ms ease-in-out, visibility 0s linear 0s;
}

.site-search__form {
	display: flex;
	align-items: center;
	height: 50px;
}

/* 15px declared paints ~12px of ink (this path fills ~81% of its viewBox —
   see the note on `.search-toggle .icon` above). The heavier 2.4 stroke
   approximates a filled glyph; a hairline at this size reads too light. */
.site-search__icon {
	flex: 0 0 auto;
	width: 15px;
	height: 15px;
	/* 43px, not a rounder number, for the same viewBox-coverage reason as
	   `.search-toggle .icon` above — this icon's ink starts ~1.75px inside
	   its own box. */
	margin-left: 43px;
	fill: none;
	stroke: #000000;
	stroke-width: 2.4;
	stroke-linecap: round;
}

.site-search__field {
	flex: 1;
	min-width: 0;
	height: 20px;
	margin-left: 10px;
	padding: 0 20px 0 0;
	border: 0;
	background-color: transparent;
	font-family: inherit;
	font-size: 15px;
	font-weight: var(--fw-normal);
	line-height: 20px;
	color: #000000;
	text-transform: capitalize;
}

/* `outline: none` is acceptable here only because the bar auto-focuses
   this field the moment it opens and holds a visible caret — there is no
   state where it is focused without the user having just put it there. */
.site-search__field:focus {
	outline: none;
}

.site-search__field::placeholder {
	color: #000000;
	opacity: 1;
}

.site-search__close {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	margin-right: 45px;
	color: #000000;
	/* Anchor for the hit-area expander below. */
	position: relative;
}

/* The ink and the target are two different sizes on purpose — do not size
   the button to the icon. The X is 13px, under WCAG 2.5.8's 24x24 minimum
   target; this pseudo-element expands the clickable area to 24x24 while
   the painted box stays 13x13, so layout does not shift. `margin-right:
   45px` leaves room for the resulting bleed. */
.site-search__close::before {
	content: "";
	position: absolute;
	top: 50%;
	left: 50%;
	width: 24px;
	height: 24px;
	transform: translate(-50%, -50%);
}

/* 13px declared paints ~8.9px of ink — this X spans 3.8 to 20.2 of its 24
   viewBox, ~68%, an even bigger correction than the magnifier's above. */
.site-search__close svg {
	width: 13px;
	height: 13px;
	fill: none;
	stroke: currentColor;
	stroke-width: 2.4;
	stroke-linecap: round;
}

@media (prefers-reduced-motion: reduce) {

	/* Drop the slide, keep the reveal — the bar still opens and closes, it
	   just snaps. `visibility` must lose its delay too or closing would leave
	   the field tabbable for 150ms after it disappears. */
	.site-search,
	.site-search.is-open {
		transition: none;
	}
}


/* ==========================================================================
   7. BODY AND PAGE LAYOUT
   ==========================================================================
   `.container` is a 1100px column with gutters; everything that is not
   full-bleed sits inside one.
   ========================================================================== */

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

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

.container--flush {
	padding-inline: 0;
}

/* Sections own their own spacing; there is no blanket paragraph margin. .page-content's own
   padding-block is global — shared by page.php, 404.php, search.php, index.php and Who We Are. */
.page-content {
	padding-block: 30px;
}

.section {
	padding-block: var(--space-section);
}

.section--sunk {
	background-color: var(--color-bg-sunk);
}

/* --- Page banner ----------------------------------------------------------
   The image is a positioned <img> with object-fit, not a CSS background, so it carries alt text
   and SEO value that a crawler can see.
   -------------------------------------------------------------------------- */

.page-banner {
	position: relative;
	display: flex;
	align-items: flex-end;
	background-color: var(--color-bg);
}

/* `overflow: hidden` lives here, not on `.page-banner`, because on `--plain` it would clip the
   title, which deliberately hangs below the banner box. No plain banner has anything to clip. */
.page-banner--image {
	min-height: 220px;
	color: #ffffff;
	overflow: hidden;
}

@media (min-width: 641px) {

	.page-banner--image {
		min-height: 300px;
	}
}

@media (min-width: 1025px) {

	.page-banner--image {
		min-height: 380px;
	}
}

.page-banner__media {
	position: absolute;
	inset: 0;
	z-index: 0;
}

/*
 * reset.css's `img[width][height] { height: auto }` is (0,2,1); a plain class selector only
 * reaches (0,1,1) and loses. The attribute-qualified rule below, at (0,3,1), is what wins.
 *
 * Without it, `.page-banner__media`'s `inset: 0` box and the uncropped hero image never share a
 * height, so `object-fit: cover` becomes a no-op and the crop silently anchors to the top.
 */
img.page-banner__img {
	width: 100%;
	object-fit: cover;
	object-position: center;
}

img[width][height].page-banner__img {
	height: 100%;
}

/* A scrim, so white type stays legible over any photograph. Without it the
   title is unreadable on roughly a third of the firm's images. */
.page-banner--image .page-banner__media::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(to top, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0) 60%);
}

.page-banner__inner {
	position: relative;
	z-index: 1;
	padding-block: var(--space-md);
}

/* A negative margin-bottom on the flex item drops the title 42.5px below the banner's content
   bottom without changing the banner's own height: because the item is `align-items: flex-end`,
   its margin box stays pinned to the content bottom, so only its border box (and the title with
   it) moves. The item's outer height falls well under `min-height: 120px`, so the banner height
   and `.page-content`'s start position are unaffected.

   Not a fixed-height holder: padding-top plus a fixed height collapses rather than overflows
   under a border-box reset, unlike content-box. And not a `transform`: a negative margin is plain
   layout, creates no stacking context, and needs no `will-change`.

   Shared: `.page-banner--plain` is emitted by template-parts/page-banner.php (every standard page
   and Who We Are), 404.php, search.php and index.php — the h1 moves on all of them.
   archive-project.php has no banner and is unaffected.

   Known limit: a title that wraps to two lines overflows by 42px instead of 18.5px and sits over
   the first line of body text. The longest title in the corpus stays on one line down to roughly
   340px. */
.page-banner--plain .page-banner__inner {
	margin-bottom: -42.5px;
}

/* The negative margin above extends this box past the banner, and the box still intercepts
   clicks there — the title is only part of that overhang, so the rest is an invisible strip lying
   over the top of the first body element. `.page-banner__inner` is `position: relative; z-index:
   1` (it sits above the banner image overlay) and `.page-content` is unpositioned, so the banner
   wins hit-testing even though it paints nothing.

   Fixed here rather than by giving `.page-content` a stacking context, since that rule is global
   to page.php / 404.php / search.php / index.php and this defect belongs only to the plain banner
   that caused it. The title re-enables pointer events so it stays selectable, rather than leaving
   the whole box non-interactive. */
.page-banner--plain .page-banner__inner {
	pointer-events: none;
}

.page-banner--plain .page-banner__title {
	pointer-events: auto;
}

.page-banner__title {
	font-size: var(--fs-h1);
}

.page-banner--plain .page-banner__title {
	font-size: 1.125rem;
	font-weight: var(--fw-light);
	color: var(--color-text);
	/* Letter-spacing and line-height are measured, not tokens — `var(--ls-wide)` and the inherited
	   heading line-height are both wrong for this specific element. */
	letter-spacing: 1px;
	line-height: 23.478px;
}

.page-banner--image .page-banner__title {
	color: #ffffff;
	text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}


/* ==========================================================================
   8. FOOTER
   ========================================================================== */

.site-footer {
	background-color: var(--color-bg);
	margin-top: var(--space-section);
}

.site-footer__top {
	padding-block: var(--space-lg);
}

.site-footer__grid {
	display: grid;
	gap: var(--space-lg);
	grid-template-columns: 1fr;
}

@media (min-width: 641px) {

	.site-footer__grid {
		grid-template-columns: repeat(2, 1fr);
	}
}

@media (min-width: 1025px) {

	.site-footer__grid {
		grid-template-columns: repeat(4, 1fr);
		gap: var(--space-md);
	}
}

.site-footer__heading {
	font-size: var(--fs-xs);
	font-weight: var(--fw-bold);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	color: var(--color-text-soft);
	margin-bottom: var(--space-2xs);
}

.site-footer__block address,
.site-footer__block p {
	font-size: var(--fs-sm);
	line-height: 1.6;
}

.footer-nav__list li + li,
.footer-social li + li {
	margin-top: var(--space-3xs);
}

.footer-nav__list a,
.footer-social a {
	font-size: var(--fs-sm);
}

.site-footer__bottom {
	display: flex;
	align-items: center;
	min-height: 53px;
}

/* Deliberate deviation from production, for accessibility. #767676 is the lightest value that
   passes WCAG AA contrast (4.54:1) against white at this size; #777, the theme's body grey, is
   4.48:1 and fails — do not "tidy" this to match it. */
.site-footer__copyright {
	font-size: 12px;
	color: #767676;
	text-align: center;
	line-height: 22px;
}

/* Copyright and the cookie control sit together, centred as one group. Wraps
   to two lines on narrow screens rather than letting either shrink. */
.site-footer__bottom > .container {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: center;
	gap: var(--space-2xs);
}

/* A <button>, not an <a>, because it opens a dialog rather than navigating —
   styled as a link so it reads as part of the copyright line. Colour and size
   match .site-footer__copyright above, including the #767676 contrast note. */
.site-footer__consent {
	appearance: none;
	background: none;
	border: 0;
	padding: 0;
	font-family: inherit;
	font-size: 12px;
	line-height: 22px;
	color: #767676;
	text-decoration: underline;
	text-underline-offset: 0.15em;
	cursor: pointer;
	transition: color var(--dur) var(--ease);
}

.site-footer__consent:hover,
.site-footer__consent:focus-visible {
	color: var(--color-text);
}

/* The WYSIWYG `footer_text` arrives wrapped in its own <p>; it must not
   reintroduce body margins or type inside the 53px strip. */
.site-footer__copyright p {
	margin: 0;
	font-size: inherit;
	line-height: inherit;
	color: inherit;
}



/* ==========================================================================
   9. SHARED COMPONENTS
   ========================================================================== */

/* --- Utilities ------------------------------------------------------------ */

.screen-reader-text {
	position: absolute !important;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* The skip link is the first focusable thing on the page and must become
   visible when focused — otherwise a keyboard user tabs into an invisible
   control that silently jumps them somewhere. */
.skip-link {
	position: absolute;
	top: -100px;
	left: var(--gutter);
	z-index: calc(var(--z-loader) + 1);
	background-color: var(--color-text);
	color: #ffffff;
	padding: var(--space-2xs) var(--space-sm);
	font-size: var(--fs-sm);
	transition: top var(--dur-fast) var(--ease);
}

.skip-link:focus {
	top: var(--space-2xs);
	color: #ffffff;
}

/* --- Buttons --------------------------------------------------------------
   -------------------------------------------------------------------------- */

.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--space-2xs);
	padding: 0.7em 1.4em;
	border: 1px solid var(--color-text);
	background-color: transparent;
	color: var(--color-text);
	font-size: var(--fs-xs);
	font-weight: var(--fw-bold);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	line-height: 1;
	transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.btn:hover,
.btn:focus-visible {
	background-color: var(--color-text);
	color: #ffffff;
}

/* Grey variant, used by the consent banner. */
.btn--grey {
	background-color: #e4e4e4;
	border-color: #e4e4e4;
	color: var(--color-text);
}

.btn--grey:hover,
.btn--grey:focus-visible {
	background-color: #cfcfcf;
	border-color: #cfcfcf;
	color: var(--color-text);
}

/* --- Search form ----------------------------------------------------------- */

.search-form {
	display: flex;
	align-items: stretch;
	gap: var(--space-2xs);
	max-width: 480px;
}

.search-form__field {
	flex: 1;
	min-width: 0;
	padding: 0.6em 0.8em;
	border: 1px solid var(--color-border);
	background-color: var(--color-bg);
	font-size: var(--fs-sm);
}

.search-form__field:focus {
	border-color: var(--color-text);
	outline: none;
}

.search-form__submit {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 44px;
	border: 1px solid var(--color-text);
	background-color: var(--color-text);
	color: #ffffff;
	transition: opacity var(--dur) var(--ease);
}

.search-form__submit:hover,
.search-form__submit:focus-visible {
	opacity: 0.8;
}

/* --- Pagination ------------------------------------------------------------ */

/* wp_link_pages() output, for a long page split across several screens. */
.page-links {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-3xs);
	margin-top: var(--space-lg);
	font-size: var(--fs-sm);
}

.page-links a,
.page-links > span:not(:first-child) {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 36px;
	height: 36px;
	border: 1px solid var(--color-border);
}

.pagination {
	margin-top: var(--space-lg);
}

.pagination .nav-links {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3xs);
	justify-content: center;
}

.pagination .page-numbers {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 40px;
	height: 40px;
	padding-inline: var(--space-2xs);
	border: 1px solid var(--color-border);
	font-size: var(--fs-sm);
}

.pagination .page-numbers.current,
.pagination .page-numbers:hover {
	background-color: var(--color-text);
	border-color: var(--color-text);
	color: #ffffff;
}

/* --- Back to top -----------------------------------------------------------
   -------------------------------------------------------------------------- */

.back-to-top {
	position: fixed;
	right: var(--gutter);
	bottom: var(--gutter);
	z-index: var(--z-nav);
	width: 46px;
	height: 46px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border: 1px solid var(--color-text);
	background-color: rgba(255, 255, 255, 0.9);
	color: var(--color-text);
	transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.back-to-top:hover,
.back-to-top:focus-visible {
	background-color: var(--color-text);
	color: #ffffff;
}

.back-to-top svg {
	width: 20px;
	height: 20px;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.6;
	stroke-linecap: round;
	stroke-linejoin: round;
}

/* Keep it clear of the cookie banner while that is still on screen. The
   banner is Complianz's, injected late and after this button in the DOM, so
   `:has()` rather than a sibling combinator — `~` would never match.
   Selector targets the plugin's own classes: verify against the live markup
   if Complianz ever changes them. */
body:has(.cmplz-cookiebanner:not(.cmplz-dismissed)) .back-to-top {
	bottom: calc(var(--gutter) + 5rem);
}


/* ==========================================================================
   10. CONTENT COMPONENTS
   ========================================================================== */

/* --- 10.1 Carousel --------------------------------------------------------
   Before JS runs, the track is a horizontal scroller: with JS off, every
   slide is still reachable by scrolling. `.is-enhanced` gates the slider
   layout, not the other way round.
   -------------------------------------------------------------------------- */

.carousel {
	position: relative;
}

.carousel__viewport {
	position: relative;
	overflow: hidden;
}

/* The track MUST carry a definite height. Every `height: 100%` below it — the
   slide, and the image inside the slide — resolves against this. Without it the
   percentages are indefinite, each slide falls back to its image's intrinsic
   height, and `object-fit: cover` becomes a no-op because every box already
   matches its own image's aspect ratio. The visible symptom is a hero of
   ragged-height slides clipped by the viewport. The height comes from
   `.carousel__viewport`, which each template sizes in §13. */
.carousel__track {
	display: flex;
	height: 100%;
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	scrollbar-width: none;
}

.carousel__track::-webkit-scrollbar {
	display: none;
}

/* `min-height: 0` is load-bearing, not tidying. A grid/flex item defaults to
   `min-height: auto`, which floors it at its content height. The content is the
   slide image, whose own `height: 100%` cannot resolve while the slide's height
   is still content-dependent — so the image falls back to its intrinsic height
   and drags the slide up to match, defeating the definite height set in §13.
   Zeroing the floor breaks the cycle: the slide takes its 100%, and the image
   then has a definite box to fill and crop into. */
.carousel__slide {
	flex: 0 0 100%;
	scroll-snap-align: start;
	position: relative;
	min-height: 0;
}

.carousel.is-enhanced .carousel__track {
	display: grid;
	overflow: visible;
}

/* Hold-then-cut, not a symmetric cross-fade: fading the outgoing slide out
   while the incoming fades in leaves both partly transparent over the page
   during the transition, and the hero washes out. The outgoing slide's
   opacity change is instant but delayed by the full transition, so it
   stays solid beneath until the incoming slide finishes arriving, then
   cuts — nothing is ever semi-transparent over the background. */
.carousel.is-enhanced .carousel__slide {
	grid-area: 1 / 1;
	opacity: 0;
	visibility: hidden;
	transition:
		opacity 0s linear var(--carousel-speed, 600ms),
		visibility 0s linear var(--carousel-speed, 600ms);
}

.carousel.is-enhanced .carousel__slide.is-active {
	z-index: 1;
	opacity: 1;
	visibility: visible;
	transition: opacity var(--carousel-speed, 600ms) var(--ease), visibility 0s;
}

/* Slide mode — `data-carousel-effect="slide"` set on `.carousel` in
   front-page.php and single-project.php. It never touches opacity, so it
   is its own block rather than an override of the rules above.

   The attribute selector gives every rule here one more class-level
   selector than its counterpart above (4 vs. 3 for the track and plain
   slide, 5 vs. 4 for `.is-active`), so these always win regardless of
   source order. */
.carousel.is-enhanced[data-carousel-effect="slide"] .carousel__track {
	display: flex;
	gap: 4px;
	/* `ease-in-out`, not `var(--ease)`: `--ease` is an overshoot curve used
	   for UI chrome elsewhere in this file and would look wrong here. */
	transition: transform var(--carousel-speed, 600ms) ease-in-out;
}

/* No opacity animation in slide mode, ever — the track's translateX (set by
   assets/js/main.js on every render()) is the only thing that moves. The
   fade path's `grid-area: 1 / 1` above is simply unused once the track is a
   flex row instead of a grid, so it does not need overriding here. */
.carousel.is-enhanced[data-carousel-effect="slide"] .carousel__slide,
.carousel.is-enhanced[data-carousel-effect="slide"] .carousel__slide.is-active {
	opacity: 1;
	visibility: visible;
	transition: none;
}

/* Reduced motion: jump instead of sliding. `transition: none` makes the
   JS-set transform apply on the next frame with no animation — no JS
   branch needed. */
@media (prefers-reduced-motion: reduce) {

	.carousel.is-enhanced[data-carousel-effect="slide"] .carousel__track {
		transition: none;
	}
}

/*
 * This `height: 100%` never applies on its own: reset.css's
 * `img[width][height] { height: auto }` is (0,2,1) and beats this rule's
 * (0,1,1). Both real usages — `.carousel--hero img.carousel__img` and
 * `.carousel--gallery img.carousel__img`, both (0,2,1) — override it at a
 * specificity that ties the reset and wins on load order, so the height is
 * correct in practice; this declaration is kept only so the base component
 * reads as complete. `width` and `object-fit` below apply normally.
 *
 * If a carousel is ever used without `--hero` or `--gallery`, this height
 * will silently not apply — give that modifier its own height rule, or
 * promote this one to `img[width][height].carousel__img`.
 */
img.carousel__img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center;
}

.carousel__link {
	display: block;
	height: 100%;
}

/* --- The title line --------------------------------------------------------
   One rule for the homepage's slide caption and the project page's `<h1>`.
   The homepage element is a `<p data-carousel-caption>` that JS refills
   from the active slide; the project page's is a static `<h1>`. Same
   element, same slot, same type.

   The case comes from CSS, not the stored values: ACF captions and post
   titles stay Title Case in the database — this file's convention is that
   headings carry their case in CSS, never in the content. Do not "fix" the
   ACF values to match.
   -------------------------------------------------------------------------- */
.carousel__title {
	margin: 0;
	font-size: 18px;
	font-weight: var(--fw-normal);
	line-height: var(--lh-tight);
	letter-spacing: normal;
	text-transform: uppercase;
	text-align: left;
	color: #333333;
	/* Wraps rather than forcing the dots off the right edge; see the mobile
	   arm on `.carousel__controls` below, where it takes its own line. */
	min-width: 0;
}

/* --- Carousel controls -----------------------------------------------------
   Shared chrome for both the homepage hero and the project galleries.

   `gap: 0` and `padding: 0` are load-bearing, not tidying: every child
   below (`.carousel__dots`, `.carousel__btn--pause`) carries its own
   margins, which are the spacing between elements — adding the container's
   own gap/padding on top would double the offset from the right edge.
   `justify-content: flex-end` is what makes the front page's DOM order
   (prev, next, caption, dots, pause) read as caption→dots→pause
   left-to-right with pause flush at the right edge — see front-page.php.

   `align-items: center` centres each flex child's margin box, not its
   content — with the offset on a single child alone, differing margin-box
   heights land children on different centre lines. Putting `margin-top:
   6px` on the container instead gives every child one shared centre line.

   `padding: 0` has one exception: the row carries the title as its first
   child, and the title's left edge has to land on the content column's
   edge while the dots stay on their own right edge. The row is full-bleed,
   so there is no flat padding that does this: the content edge is
   `(width - 1100px) / 2 + gutter` at wide viewports and just `gutter` at
   narrow ones — hence the `max()` below.

   A percentage padding resolves against the element's own containing
   block, so `50%` here is half the carousel's width, not `50vw` (which
   includes the scrollbar and would drift the title right of every other
   page element) — no media queries are needed since `--container` and
   `--gutter` are already correct at every breakpoint. `padding-inline-end`
   stays 0 so the dots and pause do not move.
   -------------------------------------------------------------------------- */
.carousel__controls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: flex-end;
	gap: 0;
	/* Reserve the dots' own 32px whether or not there are dots: a
	   single-image project renders this row with only the title, and without
	   a minimum height it collapses to the title's line box and eats into
	   the title→location gap below. */
	min-height: 32px;
	padding: 0;
	margin-top: 6px;
	background-color: transparent;
}

/* Mobile: the title and the dots/pause row can't share one line at
   typical mobile widths, so the title takes its own row below the dots.
   `order` does the reordering, not the DOM, so source order stays
   title-first for screen readers and tab order. */
.carousel__title {
	order: 2;
	flex: 0 0 100%;
	margin-top: var(--space-lg);
	padding-inline: var(--gutter);
}

.carousel__dots,
.carousel__btn--pause {
	order: 1;
}

@media (min-width: 1025px) {

	/* `nowrap` here, unlike the mobile arm's wrap: at this width, wrapping
	   drops the dots to a second line instead of just wrapping the title.
	   The title wraps inside its own box instead (`flex: 0 1 auto` plus
	   `min-width: 0` above), which keeps the dots in place and truncates
	   nothing. */
	.carousel__controls {
		flex-wrap: nowrap;
		padding-inline-start: max(var(--gutter), calc(50% - var(--container) / 2 + var(--gutter)));
	}

	/* Back onto the controls line, first, with `margin-inline-end: auto`
	   pushing the dots and pause to the right edge. `align-items: center` on
	   the row is what centres the title's line box on the pagination
	   squares. */
	.carousel__title {
		order: 0;
		flex: 0 1 auto;
		margin-top: 0;
		margin-inline-end: auto;
		padding-inline: 0;
	}
}

.carousel__btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 36px;
	height: 36px;
	color: var(--color-text);
	transition: opacity var(--dur) var(--ease);
}

.carousel__btn:disabled {
	opacity: 0.3;
}

.carousel__btn svg {
	width: 20px;
	height: 20px;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.6;
	stroke-linecap: round;
	stroke-linejoin: round;
}

/* Prev/next live inside `.carousel__viewport` (see front-page.php and
   single-project.php), not `.carousel__controls` — the only way to make
   flush left/right, full viewport height true at every breakpoint without
   hard-coding a height that duplicates the viewport's own height/clamp()
   rule. `.carousel__viewport` is already `position: relative` (above), so
   `top: 0; bottom: 0` stretches these to its exact rendered height with no
   magic number. */
.carousel__btn--prev,
.carousel__btn--next {
	position: absolute;
	top: 0;
	bottom: 0;
	/* `.carousel__btn`'s own `height: 36px` above wins over `top`/`bottom`'s
	   stretch unless `height` is reset to `auto` here: a positioned box with
	   `top: 0; bottom: 0` only stretches to fill when `height` is otherwise
	   `auto`. */
	height: auto;
	width: 44px;
	z-index: 2;
	background-color: transparent;
	border-radius: 2px;
	opacity: 0;
	transition: opacity 0.3s linear;
}

.carousel__btn--prev {
	left: 0;
}

.carousel__btn--next {
	right: 0;
}

/* Hover-revealed on the carousel, not just the viewport. `:focus-within`
   is what makes tabbing to a hidden arrow reveal it; without it a keyboard
   user focuses a control they can't see. `opacity`, never
   `visibility`/`display`, so the buttons stay in the accessibility tree and
   stay keyboard-reachable at rest. */
.carousel:hover .carousel__btn--prev,
.carousel:hover .carousel__btn--next,
.carousel:focus-within .carousel__btn--prev,
.carousel:focus-within .carousel__btn--next {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {

	/* Drop the transition, not the reveal — the arrows still show on
	   hover/focus, they just snap instead of fading. */
	.carousel__btn--prev,
	.carousel__btn--next {
		transition: none;
	}
}

.carousel__btn--pause svg {
	fill: currentColor;
	stroke: none;
}

/* Sized to match the bullet buttons so it reads as part of the same quiet
   row rather than calling attention to itself — see the WCAG 2.2.2 note in
   front-page.php/single-project.php. `margin-right: 20px` is the
   pause-to-edge offset — see the row-spacing comment above `.carousel__dots`
   below. */
.carousel__btn--pause {
	width: 28px;
	height: 30px;
	margin-right: 20px;
}

/* The pause button is only meaningful while something is playing. */
.carousel:not([data-carousel-autoplay]) .carousel__btn--pause {
	display: none;
}

/* Paused: show the play glyph instead, so the control's state is visible and
   not only announced through its aria-label. */
.carousel.is-paused .carousel__btn--pause svg {
	display: none;
}

.carousel.is-paused .carousel__btn--pause::before {
	content: "";
	display: block;
	width: 0;
	height: 0;
	border-left: 11px solid currentColor;
	border-top: 7px solid transparent;
	border-bottom: 7px solid transparent;
}

/* The pause button sits after this in DOM order, so this isn't the last
   in-flow control — the row's spacing is three parts: `margin-left: 10px`
   here is caption→dots, `margin-right: 10px` here is dots→pause, and
   `.carousel__btn--pause`'s own `margin-right: 10px` is pause→edge.
   `.carousel__controls`'s `gap: 0` / `padding: 0` are still load-bearing —
   every child carries its own margin instead of relying on the
   container. */
.carousel__dots {
	display: flex;
	align-items: center;
	height: 32px;
	margin-left: 10px;
	margin-right: 10px;
	gap: 0;
}

/* Plain 28x32 hit target with no visible chrome of its own — the square
   mark shown is the `::before` below. `border: none` here is explicit
   redundancy against reset.css's `border: 0`, not dead weight. `height:
   32px`: `::before`'s `margin: 12px 10px` on an 8x6 content box makes a
   32px mark-box, so this height makes the dot box equal the mark box and
   centres the square exactly on the pause glyph. */
.carousel__dot {
	display: inline-block;
	width: 28px;
	height: 32px;
	background-color: transparent;
	border: none;
	border-radius: 0;
}

/* The visible mark is an 8x6 content box, rendering 10x8 with its border.
   Needs `box-sizing: content-box` to opt out of the site-wide `border-box`
   reset — under `border-box` the same `width: 8px` would include the
   border and render smaller. */
.carousel__dot::before {
	content: "";
	display: block;
	box-sizing: content-box;
	width: 8px;
	height: 6px;
	margin: 12px 10px;
	background-color: #ffffff;
	border: 1px solid #bbbbbb;
	border-radius: 0;
}

/* Selected: only the border colour changes, to the S&S red. */
.carousel__dot.is-active::before {
	border-color: rgb(192, 26, 41);
}

/* --- 10.2 Project card ----------------------------------------------------- */

.project-card {
	position: relative;
	overflow: hidden;
	background-color: var(--color-bg);
}

.project-card__link {
	display: block;
	height: 100%;
}

.project-card__media {
	position: relative;
	aspect-ratio: 1;
	overflow: hidden;
	background-color: var(--color-bg-sunk);
}

/*
 * `img.` at (0,1,1) loses to `img[width][height]` at (0,2,1), so this
 * `height: 100%` must stay qualified to actually apply. Needed for a
 * sub-600px non-square upload — without it, `object-fit: cover` has nothing
 * to fill and the image letterboxes against `--color-bg-sunk`.
 *
 * The height is split into its own rule on purpose: promoting the whole
 * block to (0,3,1) would also promote `transition`, which the
 * reduced-motion and hover-zoom rules below deliberately override at
 * (0,1,1). Raise only the one declaration that must out-specify the reset.
 */
img.project-card__img {
	width: 100%;
	object-fit: cover;
	transition: transform var(--dur-slow) var(--ease);
}

img[width][height].project-card__img {
	height: 100%;
}

.project-card__link:hover .project-card__img,
.project-card__link:focus-visible .project-card__img {
	transform: scale(1.04);
}

.project-card__placeholder {
	width: 100%;
	height: 100%;
	background-color: var(--color-bg-sunk);
}

/* Caption bar stays visible on touch, unlike the old site's touch handling —
   deliberate: unlabelled cards is worse than a permanent bar. This is the
   `(hover: none)` fallback and the base state everywhere. */
.project-card__body {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	padding: var(--space-2xs) var(--space-xs);
	background-color: rgba(255, 255, 255, 0.88);
	transition: background-color var(--dur) var(--ease);
}

.project-card__title {
	font-size: var(--fs-sm);
	font-weight: var(--fw-bold);
	line-height: var(--lh-tight);
	text-transform: none;
}

.project-card__type {
	font-size: var(--fs-xs);
	font-weight: var(--fw-normal);
	color: var(--color-text-soft);
	line-height: var(--lh-tight);
	margin-top: 2px;
}

/* On pointer devices, `.project-card__media::after` fades in as a dark
   shader with the caption centred over it in white while the image zooms
   further. `.project-card__body` is repurposed as the caption box here —
   same element as the touch bar above, different rules — so it still
   degrades correctly under `(hover: none)`. */
@media (hover: hover) and (pointer: fine) {

	.project-card__media::after {
		content: '';
		position: absolute;
		inset: 0;
		z-index: 3;
		background-color: rgba(21, 21, 21, 0.78);
		opacity: 0;
		transition: opacity 5s cubic-bezier(0.21, 1, 0.12, 1);
	}

	.project-card__link:hover .project-card__media::after,
	.project-card__link:focus-visible .project-card__media::after {
		opacity: 1;
	}

	/* `z-index: 5` is load-bearing: the shader above sits at 3, and this box is
	   `position: absolute` at `z-index: auto`, so without its own stacking
	   order the shader paints over the caption and white text reads through
	   the dark overlay.

	   Flex column, not `grid` + `place-items: center`: auto grid rows stretch
	   to fill, which centres the title and category independently instead of
	   as one tight block. */
	.project-card__body {
		inset: 0;
		padding: 10%;
		background-color: transparent;
		display: flex;
		flex-direction: column;
		justify-content: center;
		text-align: center;
		z-index: 5;
		opacity: 0;
		transition: opacity 0.6s;
	}

	.project-card__link:hover .project-card__body,
	.project-card__link:focus-visible .project-card__body {
		opacity: 1;
	}

	.project-card__title {
		font-size: 16px;
		font-weight: var(--fw-bold);
		line-height: 20px;
		color: #fff;
		text-transform: capitalize;
		margin: 0;
	}

	.project-card__title::after {
		content: '';
		display: block;
		width: 33px;
		height: 1px;
		background-color: #fff;
		margin: 15px auto 20px;
	}

	.project-card__type {
		font-size: 12px;
		font-weight: var(--fw-normal);
		line-height: 14px;
		color: #fff;
		margin: 0;
	}

	/* Keep the `img.` qualifier here too — same reset.css specificity fight
	   as above. */
	img.project-card__img {
		transform: scale(1.01);
		transition: transform 2s cubic-bezier(0.23, 1, 0.12, 1);
	}

	.project-card__link:hover .project-card__img,
	.project-card__link:focus-visible .project-card__img {
		transform: scale(1.1);
		transition: transform 5s cubic-bezier(0.21, 1, 0.12, 1);
	}
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) {

	.project-card__media::after,
	.project-card__body,
	img.project-card__img {
		transition: none;
	}
}

/* --- 10.3 Project grid ----------------------------------------------------- */

.project-grid {
	/* `position: relative` makes this the offsetParent for every
	   `.project-card` it contains — a card's own `position: relative`
	   (above) changes its own offsetParent, not this element's. main.js
	   module 7 reads a leaving card's offsetTop/Left/Width/Height and
	   pins it with `position: absolute` at those same values so it can
	   leave flow without jumping; those numbers are only correct
	   measured against this grid. Nothing else here depends on the grid
	   staying in normal flow, so this is safe to add. */
	position: relative;
	display: grid;
	column-gap: 2%;
	row-gap: 13px;
	grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 641px) {

	.project-grid {
		row-gap: 21px;
		grid-template-columns: repeat(3, 1fr);
	}
}

@media (min-width: 1025px) {

	.project-grid {
		row-gap: 24px;
		grid-template-columns: repeat(4, 1fr);
	}
}

.project-grid__empty {
	padding-block: var(--space-lg);
	text-align: center;
	color: var(--color-text-soft);
}

/* --- 10.4 Project filter --------------------------------------------------
   Hidden via the pre-paint `html:not(.js)` guard, not a JS-removed `hidden`
   attribute — revealing the bar after first paint shifts the grid down.
   main.js hides it explicitly when there are no cards to filter.
   -------------------------------------------------------------------------- */

html:not(.js) .project-filter {
	display: none;
}

.project-filter {
	margin-bottom: var(--space-md);
}

@media (min-width: 1025px) {

	.project-filter {
		margin-bottom: 65px;
	}
}

/* Button side padding supplies the horizontal spacing; the row gap here
   only needs to cover wrapped lines. */
.project-filter__list {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 4px 0;
}

.project-filter__btn {
	padding: 0 22px;
	font-size: 12px;
	letter-spacing: 1px;
	line-height: 27.2px;
	color: var(--color-text);
	border-bottom: none;
	transition: color var(--dur) var(--ease);
}

.project-filter__btn:hover,
.project-filter__btn:focus-visible {
	color: var(--color-text-muted);
}

/* Colour is the only visual active-state marker; `aria-pressed` carries the
   state programmatically, so this is not a WCAG 1.4.1 failure. */
.project-filter__btn.is-active {
	color: var(--color-text-muted);
	font-weight: var(--fw-normal);
}

/* --- 10.4b Project filter transition ---------------------------------
   Separate from 10.4 (button styling): covers the card fade/FLIP driven by
   main.js's `applyAnimated()`. `.is-leaving`/`.is-entering` only declare the
   resting opacity — main.js sets the transition and duration inline per
   card. Fades are opacity-only; FLIP's translate is the only transform this
   feature applies, so do not add scale or translate to these states.
   `.project-grid` needs `position: relative` (above) for a leaver's pinned
   offsets to resolve against the grid.
   ------------------------------------------------------------------------- */

.project-card.is-leaving,
.project-card.is-entering {
	opacity: 0;
}

/* main.js checks prefersReducedMotion() before adding these classes; this is
   a defensive fallback so a future path that forgets the check doesn't leave
   a card stuck invisible instead of merely un-animated. */
@media (prefers-reduced-motion: reduce) {

	.project-card.is-leaving,
	.project-card.is-entering {
		opacity: 1;
		transition: none;
	}
}

/* --- 10.5 Staff grid ------------------------------------------------------ */

.staff-grid {
	display: grid;
	/* Gap values match the old site's measured values, not an approximation. */
	column-gap: var(--space-3xs);
	row-gap: 19px;
	grid-template-columns: repeat(2, 1fr);
	margin-top: var(--space-md);
}

@media (min-width: 641px) {

	.staff-grid {
		grid-template-columns: repeat(3, 1fr);
	}
}

@media (min-width: 1025px) {

	.staff-grid {
		grid-template-columns: repeat(4, 1fr);
	}
}
.leadership-list {
	margin-top: 5px;
}
/*
 * These names open a staff bio dialog. They are `<button>` elements with a
 * `data-staff-open` hook, not anchors — a `<button>` is focusable, and
 * reset.css's `a:not([href]):not([class])` placeholder rule cannot match it.
 *
 * `display: inline` is not honoured on a `<button>`: browsers coerce it to
 * `inline-block` regardless, so that declaration is left out rather than
 * kept as decoration that reads like a working guarantee.
 *
 * `text-align` is insurance, not decoration: these boxes shrink-to-fit
 * single-line text, but a `<button>` centres by default, and a name long
 * enough to wrap would centre its second line instead of left-aligning it.
 *
 * Not restated here because reset.css already supplies them to every
 * button: transparent background, zero border/padding/margin, inherited
 * font family/size/line-height, and `cursor: pointer`.
 */
.leadership-list__link {
	color: var(--color-text);
	font-weight: 700;
	text-align: left;
}

/*
 * `:focus-visible` is paired with `:hover` per the standing rule that a
 * page-scoped rule overriding a shared state property must ship its own
 * state pair.
 */
.leadership-list__link:hover,
.leadership-list__link:focus-visible {
	color: var(--color-accent);
}
.staff-card {
	position: relative;
	display: block;
	width: 100%;
	text-align: left;
	overflow: hidden;
	transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.staff-card--has-bio {
	cursor: pointer;
}

.staff-card--has-bio:hover,
.staff-card--has-bio:focus-visible {
	transform: translateY(-2px);
	box-shadow: var(--shadow-card);
}

.staff-card__media {
	position: relative;
	aspect-ratio: 1;
	background-color: var(--color-bg-sunk);
}

/*
 * Same reset.css specificity fight as the project card: `img.` at (0,1,1)
 * loses to `img[width][height]` at (0,2,1), so this `height: 100%` must stay
 * qualified to apply.
 *
 * Kept rather than deleted for the same reason as the project card: the
 * slot is `aspect-ratio: 1`, and a non-square portrait under 600px would
 * collapse to its intrinsic height with `object-fit: cover` inert.
 */
img.staff-card__img {
	width: 100%;
	object-fit: cover;
	object-position: center top;
}

img[width][height].staff-card__img {
	height: 100%;
}

.staff-card__placeholder {
	width: 100%;
	height: 100%;
	background-color: var(--color-bg-sunk);
}

/* Decorative affordance signalling a tile opens something; aria-hidden in
   the template. Pinned to `--fw-bold` (700), not the old 800: PT Sans has
   no 800 cut, so 800 here would trigger a fallback weight. */
.staff-card__more {
	position: absolute;
	top: 7px;
	right: 10px;
	width: 26px;
	height: 26px;
	font-size: 1.25rem;
	font-weight: var(--fw-bold);
	line-height: 26px;
	text-align: right;
	color: #ffffff;
	text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
	opacity: 0.8;
}

.staff-card__caption {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	padding: 0 6px 3px;
	background-color: rgba(255, 255, 255, 0.7);
	transition: background-color var(--dur) var(--ease);
}

.staff-card--has-bio:hover .staff-card__caption,
.staff-card--has-bio:focus-visible .staff-card__caption {
	background-color: rgba(255, 255, 255, 0.85);
}

/* Line-height is measured on the spans that paint the text, not the
   caption box. */
.staff-card__name {
	display: block;
	font-size: var(--fs-sm);
	font-weight: var(--fw-bold);
	line-height: 23.8px;
}

.staff-card__role {
	display: block;
	font-size: var(--fs-sm);
	/* PT Sans only ships 400/700; pinning 400 explicitly avoids an ambiguous
	   fallback that could resolve to bold. */
	font-weight: var(--fw-normal);
	color: var(--color-text);
	line-height: 23.8px;
}

/* --- 10.6 Staff bio dialog ------------------------------------------------
   Native <dialog>. Focus trapping, Esc-to-close and the backdrop come from
   the element itself.
   -------------------------------------------------------------------------- */

.staff-dialog {
	/*
	 * Overrides the UA's modal centring (`inset: 0` + `margin: auto`) with a
	 * fixed bottom-right panel at 90% width.
	 */
	position: fixed;
	inset: auto 0 0 auto;
	margin: 0;
	width: 90%;
	max-width: 90%;
	height: auto;
	max-height: none;
	background-color: rgba(255, 255, 255, 0.9);
	box-shadow: var(--shadow-dialog);
	border-radius: 12px 0 0 0;
	/*
	 * `visible`, not `auto` — the close button deliberately overhangs the
	 * top-left corner by 13px and would otherwise be clipped. Scrolling is
	 * `.staff-dialog__body`'s job instead.
	 */
	overflow: visible;
	opacity: 0;
	transform: translate(100px, 0);
	transition:
		transform var(--dur-dialog) var(--ease),
		opacity var(--dur-dialog) var(--ease);
}

/*
 * The panel takes the dialog's initial focus (see staff-dialog.php), so it
 * needs no focus ring of its own — the close button still gets the global
 * `:focus-visible` ring when tabbed to.
 *
 * Kept as its own rule rather than added to `.staff-dialog`: raising that
 * block's specificity would promote every declaration in it, including the
 * open/close transition.
 */
.staff-dialog:focus,
.staff-dialog:focus-visible {
	outline: none;
}

/*
 * Driven by main.js, not by the `[open]` attribute: a modal <dialog> leaves the
 * top layer the instant `close()` runs, which would cut the exit transition
 * off. See module 8 in main.js.
 */
.staff-dialog--open {
	opacity: 1;
	transform: none;
}

.staff-dialog::backdrop {
	background-color: rgba(0, 0, 0, 0.7);
	opacity: 0;
	transition: opacity var(--dur-dialog) var(--ease);
}

.staff-dialog--open::backdrop {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {

	/*
	 * `transform: none` is the load-bearing half — it kills the slide, which
	 * reset.css's reduced-motion block does not touch.
	 *
	 * `transition: none` is belt-and-braces: reset.css already forces near-zero
	 * transition duration via `!important`, so transitions are already instant.
	 * It is not what keeps the JS safe — main.js takes its own reduced-motion
	 * branch and closes synchronously rather than waiting on `transitionend`.
	 */
	.staff-dialog,
	.staff-dialog::backdrop {
		transform: none;
		transition: none;
	}
}

.staff-dialog__close {
	/*
	 * Overhangs the panel's top-left corner. Sized as 40x40 total under this
	 * theme's global border-box reset — do not add padding here, or the
	 * button grows past 40px.
	 */
	position: absolute;
	top: -13px;
	left: -13px;
	z-index: 2;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	border-radius: 50%;
	font-size: 2.3rem;
	line-height: 1;
	color: var(--color-text);
	background-color: #fff;
	transition: color var(--dur) var(--ease);
}

.staff-dialog__close:hover,
.staff-dialog__close:focus-visible {
	color: var(--color-accent);
}

.staff-dialog__body {
	/*
	 * `content-box` is load-bearing and deliberately overrides this theme's
	 * global border-box reset: under border-box, the padding would fold inside
	 * the 76vh cap and shrink the visible content noticeably.
	 *
	 * `auto`, not `scroll`, so there is no permanent gutter when content fits.
	 */
	box-sizing: content-box;
	max-height: 76vh;
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: 4%;
}

.staff-dialog__banner {
	margin-bottom: 32px;
}

/*
 * Keep the `img.` qualifier, but it does not win a `height` fight: reset.css's
 * `img[width][height] { height: auto }` is (0,2,1), `img.thing` is only
 * (0,1,1), so the reset still wins. The qualifier narrows the gap, it does
 * not close it.
 *
 * These two are safe regardless: this one declares the same `height: auto`
 * the reset would force, and `.staff-dialog__portrait` below declares no
 * height at all. A real fixed height here must out-specify the attribute
 * selector, e.g. `img[width][height].staff-dialog__banner-img`.
 */
img.staff-dialog__banner-img {
	width: 100%;
	height: auto;
}

.staff-dialog__columns {
	display: grid;
	grid-template-columns: 1fr;
	/*
	 * Zero, deliberately: mobile spacing comes from the elements' own margins.
	 */
	row-gap: 0;
}

/*
 * 1024, not 1025: 1024 is a reference viewport, and a `min-width: 1025px`
 * step would drop it into the stacked band below.
 */
@media (min-width: 1024px) {

	.staff-dialog__columns {
		grid-template-columns: 29% 65%;
		column-gap: 6%;
		align-items: start;
	}

	/*
	 * Column one stacks aside-then-projects; column two is a single spanning
	 * item.
	 *
	 * This escapes the grid row-height trap (section 13.5) only because column
	 * two holds one spanning item — do not add a second child without
	 * re-checking.
	 */
	.staff-dialog__aside {
		grid-column: 1;
		grid-row: 1;
	}

	.staff-dialog__projects {
		grid-column: 1;
		grid-row: 2;
	}

	.staff-dialog__main {
		grid-column: 2;
		grid-row: 1 / span 2;
	}
}

@media (max-width: 1023px) {

	/* Pairs with the `min-width: 1024px` step above — same 1024/1025 boundary. */
	.staff-dialog__banner {
		display: none;
	}
}

/* Safe for the reason given above: no `height` declared, so the reset's
   `height: auto` is exactly what `aspect-ratio: 1` wants anyway. */
img.staff-dialog__portrait {
	width: 100%;
	aspect-ratio: 1;
	object-fit: cover;
}

/*
 * Values below are literal px, not the `--fs-*` scale: this panel is
 * 16px/1.7 throughout (projects heading is 1.1em), and the scale doesn't
 * land on these exact numbers.
 */

.staff-dialog__name {
	/* Margin and padding both 0 via reset.css's heading reset, not redeclared here. */
	font-size: 1rem;
	font-weight: var(--fw-bold);
	line-height: var(--lh-body);
	letter-spacing: 0.05em;
	color: var(--color-text);
}

/*
 * Deliberately inherits everything. The class exists only so the
 * credentials stay addressable — it must not restyle them.
 */
.staff-dialog__credentials {
	font: inherit;
	color: inherit;
}

.staff-dialog__role {
	/* Full-strength text colour here, deliberately not muted. */
	font-size: 1rem;
	font-weight: var(--fw-light);
	line-height: var(--lh-body);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	color: var(--color-text);
	/*
	 * The 10px removed from paragraph spacing site-wide (section 7) is
	 * expressed here as margin instead of restoring the padding.
	 */
	margin-bottom: 25px;
}

@media (max-width: 1023px) {

	/*
	 * Stacked-only and scoped to the breakpoint, not a base rule with a
	 * desktop reset: this block sits later in the file than the
	 * `min-width: 1024px` grid placement above, so an unscoped base value
	 * would win at desktop on source order and silently push the list down.
	 */
	.staff-dialog__projects {
		margin-top: 10px;
	}
}

.staff-dialog__projects-heading {
	/* `text-transform: uppercase` is not a deviation — it's what makes the
	   authored title-case string render like the design intends. */
	font-size: 1.1rem;
	font-weight: var(--fw-bold);
	line-height: var(--lh-body);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	color: var(--color-text);
	margin-bottom: 0;
}

.staff-dialog__projects ul {
	list-style: none;
	margin: 0;
	padding-left: 0;
}

.staff-dialog__projects li {
	font-size: 1rem;
	line-height: 1.4;
	/* Applies to every item including the last, not an `li + li` gap. */
	margin-bottom: 10px;
}

.staff-dialog__projects a {
	font-weight: var(--fw-bold);
	color: rgba(0, 0, 0, 0.9);
	text-decoration: none;
}

.staff-dialog__projects a:hover,
.staff-dialog__projects a:focus-visible {
	color: var(--color-accent);
}


/* ==========================================================================
   11. SCROLL REVEALS
   ==========================================================================
   The animation itself is AOS's — vendored, unmodified, at assets/vendor/aos/
   and enqueued before this file (inc/enqueue.php). Nothing here restyles it.
   This section exists only to hold the safety guards AOS does not ship with,
   which is why the selectors below are heavy-handed.

   The problem: aos.css unconditionally hides every [data-aos] element
   (opacity: 0) until AOS's JavaScript runs. Without a guard, a visitor with
   JS disabled — and any crawler that does not execute scripts — gets a page
   where the staff grid and every project body is invisible.

   Two layers guard against it:

     1. `html:not(.js)` below. The `js` class is added by an inline script in
        <head> (inc/setup.php) that runs before first paint, so this resolves
        on the first frame with no flash.
     2. A <noscript> block in header.php, in case the inline script is itself
        blocked by a CSP or an extension.

   `!important` is deliberate and load-bearing: AOS's selectors are doubled
   for specificity and some are further qualified by duration/delay
   attributes, so winning on specificity alone would mean guessing at the
   library's highest combination — a guess that silently breaks on upgrade.

   Reduced motion is not handled here. AOS's own `disable` option in main.js
   strips the `data-aos*` attributes so the library's selectors stop matching
   altogether; overriding opacity in CSS instead would leave the attributes
   in place and the transitions armed.
   ========================================================================== */

html:not(.js) [data-aos] {
	opacity: 1 !important;
	transform: none !important;
	transition: none !important;
	visibility: visible !important;
}

/* Same guard for the case where AOS's script fails to load or is stripped by a
   concatenation plugin. main.js sets this class when `window.AOS` is missing,
   rather than leaving the page blank. */
html.aos-unavailable [data-aos] {
	opacity: 1 !important;
	transform: none !important;
	transition: none !important;
}


/* ==========================================================================
   12. COOKIE CONSENT BANNER (Complianz)
   ==========================================================================
   The banner is the Complianz plugin's, not the theme's. These rules were
   rescued out of the plugin's custom-CSS field before the 2026-08-15 cutover
   so they live in version control rather than in a database field. The
   theme's own banner was removed on 2026-08-15 when Complianz went back on
   — see decisions.md.

   SPECIFICITY: every rule below repeats `.cmplz-cookiebanner` to sit one
   class above the plugin. This is load-bearing, not a typo. Complianz's
   generated stylesheet is enqueued AFTER the theme's, verified on production
   2026-08-15:

       cookieblocker.min.css
       → sstheme/assets/css/main.css
       → uploads/complianz/css/banner-1-optin.css

   so at EQUAL specificity the plugin wins on source order. An earlier version
   of this section matched Complianz's selectors class for class and asserted
   that was enough; it wasn't. The Accept button stayed Complianz's charcoal
   `#504f4f` on white text — the exact "dark primary" the approved design
   forbids — while the message stayed 12px and the plugin's downward shadow
   overrode the theme's upward one. All three were invisible in the source and
   only showed up in the computed styles on the live page. Do not "simplify"
   the doubled class away, and do not reach for `!important` instead.

   (`lessons.md` warns that raising a rule's specificity promotes every
   declaration in it. Here that is the intent — the whole section has to
   outrank the plugin — so the usual "split the one declaration" advice does
   not apply.)
   ========================================================================== */

/* --- Bug fix: banner doesn't hide after Accept/Deny/Save --- */
.cmplz-cookiebanner.cmplz-dismissed {
  display: none !important;
}

/* (The rule that used to sit here zeroed the negative margins on the two empty
   divider spacers. It is gone twice over: the dividers are now hidden outright
   further down, and half of it never matched anyway — Complianz emits
   `cmplz-divider cmplz-footer` on the second one, not `cmplz-divider-footer`.) */

/* --- Approved appearance --------------------------------------------------
   The design was signed off July 2026 and is fixed (decisions.md): a discreet
   bar across the bottom, white at 0.97 opacity, black text, LIGHT GREY buttons
   — never a dark or coloured primary.

   The target is the OLD site's banner, reconstructed from two sources in
   `Old website capture/`: the plugin's own generated token values in
   `…/Projects Main/…_files/banner-1-optin.css`, and the old theme's hand-
   written "Cookie Compliance" override block, which survives only inside the
   captured `…/Projects Main/Siegel & Strain Architects _ Projects.html`.
   Both were verified pixel-for-pixel against `Screenshots/contact-location-*`
   at 390 / 768 / 1024 / 1440. Measured at 1440: bar full-bleed, ~70px tall,
   message starting at x=102 with the body capped at 950px and the whole row
   centred, buttons auto-width (62 / 52 / 122) with 10px gaps ending at x≈1327.

   TWO THINGS ARE DELIBERATELY NOT COPIED FROM THE OLD SITE:
     - Opacity stays 0.97. The captured old CSS says 0.9; [K] already pushed
       back once on lowering it for readability and it must not go lower.
     - The typeface is PT Sans. The old bar rendered in the Bridge theme's
       face; that changed sitewide in the rebuild and is out of scope here.

   AND TWO ARE DELIBERATE ADDITIONS, marked below: the top border and the
   upward shadow. The old bar had neither visible.
   -------------------------------------------------------------------------- */

/* Complianz's own layout for a bottom bar is fought on three fronts here, so
   it is one rule rather than several:

   1. FULL-BLEED. Complianz insets the bar (`margin: 10px;
      width: calc(100% - 20px)`) at ≥1024px and rounds it by
      `--cmplz_banner_border_radius`. The approved bar runs edge to edge.
   2. THE 769–1023px COLLAPSE. Complianz sets the width at two breakpoints and
      leaves a gap between them: `width: 100%` at max-width 768px,
      `calc(100% - 20px)` at min-width 1024px, and NOTHING from 769px to
      1023px. In that band the bar falls back to shrink-to-fit, and because its
      own children are `width: 100%` of a parent being sized by those children,
      it degenerates — 450px wide at a 900px viewport, a 24px sliver at 800px,
      which reads as "the cookie banner has stopped appearing". The band covers
      landscape tablets and any non-maximised desktop window. The unconditional
      `width: 100%` below fixes it; the media query that used to do this job
      separately is gone. Measured on the live site 2026-08-15.
   3. THE GRID. At ≥1024px Complianz lays the banner out as a three-column
      grid, which is what strands the document links in a row of their own and
      stretches the buttons to 250px each. The old bar was a centred flex row
      at every width. `display: flex` here replaces the grid outright, which is
      why the responsive behaviour below needs almost no further work.

   `transform: none` is not optional and must stay paired with `left: 0`.
   Below 1024px Complianz centres the bar with `left: 50%` plus
   `transform: translateX(-50%)`; at ≥1024px it resets both. Overriding `left`
   to 0 without also killing the transform leaves the -50% shift applied to an
   offset that is no longer there, and the bar slides half its own width off
   the left edge — measured at 768px, a full-width bar rendered from x=-384 to
   x=384. It looks exactly like the shrink-to-fit collapse and is not. */
.cmplz-cookiebanner.cmplz-cookiebanner.cmplz-bottom {
	margin: 0;
	width: 100%;
	max-width: 100vw;
	left: 0;
	right: 0;
	transform: none;
	border-radius: 0;

	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: 10px;
	overflow: hidden;

	padding: 15px 20px;
	padding-bottom: max(15px, env(safe-area-inset-bottom));

	background-color: rgba(255, 255, 255, 0.97);
	/* ADDITION, not a copy. The old bar carried Complianz's default
	   `0 10px 20px rgba(0,0,0,.19), 0 6px 6px rgba(0,0,0,.23)` — a downward
	   shadow, which casts off-screen on a bar anchored to the bottom of the
	   viewport, so in a real viewport it showed nothing. This throws upward and
	   softly, and gives the bar an edge against content showing through at 0.97. */
	box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
	/* ADDITION, not a copy. The old bar had `border-width: 0`. */
	border-top: 1px solid var(--color-border);
}

/* The approved design shows no title and no close control.

   The header is taken out of flow rather than hidden with `display: none`,
   because the dialog is labelled by the title inside it via aria-labelledby
   and removing that from the accessibility tree would leave the dialog
   unnamed. `display: none` is doubly wrong now the bar is a flex container:
   left in flow the header would be a flex item and take space. Absolute
   positioning solves both. (This replaces the old visually-hidden rule on
   `.cmplz-title`, which no longer needs one of its own.) */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-header {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* …and the title itself has to be forced back into the box tree. Complianz's
   "hide the banner title" setting emits `.cmplz-cookiebanner .cmplz-title
   { display: none }`, and a `display: none` element is not an accessible name
   source — the dialog is `aria-labelledby` this exact node, so leaving the
   plugin's rule in place leaves the dialog unnamed for screen readers. The
   clipped header above is what hides it visually; this only restores the box.

   FOUR classes, not three: Complianz hides the title twice, once at
   `.cmplz-cookiebanner .cmplz-title` (two classes) and again inside
   `@media (max-width: 425px)` at `.cmplz-cookiebanner .cmplz-header
   .cmplz-title` (three). A three-class rule here wins everywhere except
   mobile, which is the one place nobody would think to check. Verified in the
   harness at 390px: at three classes the computed display is still `none`. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-header .cmplz-title {
	display: block;
}

/* Close is removed outright rather than hidden accessibly: it dismisses
   without recording a choice, which leaves the visitor in the denied default
   while looking like a decision. Deny does that job explicitly. It needs its
   own rule — clipping the header alone would leave close keyboard-reachable. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-close {
	display: none;
}

/* Empty decorative spacers. Harmless in Complianz's grid, but as flex items
   they eat two 10px gaps. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-divider {
	display: none;
}

/* `flex: 1 1 0` + a 950px cap is what centres the message against the buttons
   and reproduces the old x=102 start at 1440 without hard-coding a position.
   Below ~1000px the cap stops mattering and the body simply fills the row. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-body {
	flex: 1 1 0;
	width: auto;
	max-width: 950px;
	align-items: center;
	overflow: visible;
}

/* 13px is the old bar's rendered size. Note it is an override, not a match:
   `--cmplz_text_font_size` is 12px in the plugin settings on both the old and
   the current install, and the old theme forced 13px the same way.

   Colour is deliberately NOT set. `--cmplz_text_color` is #222222 on both
   installs, which is already the value the approved bar rendered — the rule
   that used to set `var(--color-text)` here lost to the plugin and was
   therefore doing nothing. If someone changes the text colour in Complianz's
   settings, this is the line that has to come back. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-message {
	margin-bottom: 0;
	font-size: 13px;
	line-height: 1.5;
}

/* The two policy links sit INLINE in the sentence, bold and un-underlined,
   exactly as on the old site. They are part of the banner message itself —
   Complianz's message field carries the `<a>` tags — which is why the plugin's
   own `.cmplz-documents` row is hidden below rather than restyled. CSS cannot
   flow a sibling element into another element's last text line, so the two
   have to travel together: if the links ever vanish from the bar, check the
   Complianz message field before touching this. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-message a {
	color: inherit;
	font-weight: var(--fw-bold);
	text-decoration: none;
}

/* ADDITION. The old bar had no hover state at all; every other link on the
   site goes to accent, and the banner was the only place that didn't. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-message a:hover,
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-message a:focus-visible {
	color: var(--color-accent);
}

/* Note the compound `.cmplz-links.cmplz-documents` — both classes are on the
   same element. Complianz targets it that way inside its ≥1024px block
   (`display: flex; grid-row-start: 5`), so a rule written as
   `… .cmplz-documents` alone is only three classes, ties, and loses. It then
   stays a flex item, which pushes the message off-centre as well as leaving a
   duplicate link row on screen. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-links.cmplz-documents {
	display: none;
}

/* `margin: auto 10px` is the old override's vertical centring, kept verbatim.
   `flex: 0 0 auto` is what stops Complianz stretching the block across a grid
   column. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons {
	flex: 0 0 auto;
	width: auto;
	margin: auto 10px;
	gap: 10px;
}

/* Shrink-to-fit buttons, ~41px tall. Complianz ships `min-height: 45px;
   width: 100%` and, at ≥1024px, `min-width: 250px` — three separate reasons
   the current bar's buttons are oversized slabs.

   `font-weight` is deliberately absent: Complianz sets 500, and with only the
   400 and 700 PT Sans faces loaded that resolves to 400, which is what the old
   bar rendered. Setting it explicitly would invite someone to "fix" it to a
   weight the theme has no font file for. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn {
	width: auto;
	min-width: 50px;
	min-height: 20px;
	padding: 10px;
	font-size: 13px;
	line-height: 20px;
	text-transform: none;
	letter-spacing: normal;
	border-radius: 6px;
	white-space: nowrap;
	transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

/* EACH BUTTON IS NAMED DELIBERATELY, and each selector carries FIVE classes.
   Complianz styles these at `.cmplz-cookiebanner .cmplz-buttons
   .cmplz-btn.cmplz-accept` — four classes — and its stylesheet loads after
   this one, so four classes here would tie and lose. See the specificity note
   at the top of the section. (Overriding Complianz's `--cmplz_button_*` custom
   properties instead was tried and only half works: background and border
   take, text colour does not.)

   The three greys are the old install's plugin settings, restated here in CSS
   so the approved design lives in version control instead of a settings screen.
   Accept is a shade darker than the other two — that is the whole of the
   visual hierarchy, and it is the approved amount of it. Nothing here is dark
   or coloured; the current install's settings render Accept #504f4f on white
   text, which is what these rules exist to suppress. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-accept {
	background-color: #d6d6d6;
	border: 1px solid #adadad;
	color: #000000;
}

.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-deny {
	background-color: #f9f9f9;
	border: 1px solid #f2f2f2;
	color: #222222;
}

.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-view-preferences,
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-save-preferences {
	background-color: #f9f9f9;
	border: 1px solid #f2f2f2;
	color: #333333;
}

/* ADDITION, like the message-link hover: the old buttons had no hover state.
   Each keeps its own resting shade so the Accept/rest distinction survives. */
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-accept:hover,
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-accept:focus-visible {
	background-color: #c9c9c9;
	border-color: #9d9d9d;
}

.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-deny:hover,
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-deny:focus-visible,
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-view-preferences:hover,
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-view-preferences:focus-visible,
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-save-preferences:hover,
.cmplz-cookiebanner.cmplz-cookiebanner .cmplz-buttons .cmplz-btn.cmplz-save-preferences:focus-visible {
	background-color: #f0f0f0;
	border-color: #e4e4e4;
}

/* The expanded state — "View preferences" reveals four category rows. The bar
   stops being a single line, so the flex row has to become a column and the
   950px body cap has to come off, or the categories are squeezed against the
   buttons. `overflow: hidden` on the bar is why `.cmplz-body` keeps its own
   scroll here: on a short viewport the category list scrolls inside the bar
   rather than overflowing it. */
.cmplz-cookiebanner.cmplz-cookiebanner.cmplz-categories-visible {
	flex-direction: column;
	align-items: stretch;
}

/* `flex: 1 1 0` on `.cmplz-body` above is a row-axis assumption: it sizes the
   body's WIDTH while the bar is a single line. The moment the bar becomes a
   column here, that same `flex-basis: 0` starts sizing the body's HEIGHT
   instead and collapses it to 0 — the category list then renders outside the
   bar entirely. Resetting to `auto` lets the body take its content height. */
.cmplz-cookiebanner.cmplz-cookiebanner.cmplz-categories-visible .cmplz-body {
	flex: 0 1 auto;
	max-width: none;
	overflow-y: auto;
}

/* Five classes, because Complianz stacks these buttons full-width at ≥1024px
   from `.cmplz-cookiebanner.cmplz-categories-type-view-preferences
   .cmplz-categories-visible .cmplz-buttons` — four classes. Keeping them a
   right-aligned auto-width row means the expanded panel reads as the same bar
   grown taller rather than a second, differently-styled component. */
.cmplz-cookiebanner.cmplz-cookiebanner.cmplz-bottom.cmplz-categories-visible .cmplz-buttons {
	margin: 0;
	flex-direction: row;
	flex-wrap: wrap;
	justify-content: flex-end;
}


/* ==========================================================================
   13. PAGE AND TEMPLATE OVERRIDES
   ==========================================================================
   Everything above is general. Everything below applies to exactly one
   template. If a rule here would be useful on a second page, move it up into
   the relevant component section instead of duplicating it.
   ========================================================================== */

/* --- 13.1 Front page ------------------------------------------------------ */

.hero {
	position: relative;
}

/* Keyed to the `carousel--hero` modifier rather than to `.hero` as an ancestor,
   so the sizing travels with the component if the hero is ever reused. */

/* Not `row-reverse`: the row is full width now that it carries the
   title, and DOM order must match reading order for tab and
   screen-reader navigation. Use `order` on one item instead if visual
   reordering is ever needed. */

/* Height is a viewport-height calc via `--carousel-fit-height`, not a
   fixed aspect ratio.

   `100svh`, not `100vh`, so mobile browser chrome showing or hiding
   cannot push the controls off-screen mid-scroll.

   No `min-height` floor is set, deliberately.

   `--carousel-fit-height` is shared with the project gallery so the two
   carousels cannot drift apart — change it there, not here. */
.carousel--hero .carousel__viewport {
	width: 100%;
	flex-basis: 100%;
	height: var(--carousel-fit-height);
}

/* Desktop only: the title sits flush with the gutter, diverging from
   the project gallery's content-column inset — the homepage has no
   content column to align to. Mobile/tablet keep the separate gutter
   offset below (unrelated, fold clearance) — leave that alone. */
@media (min-width: 1025px) {

	.carousel--hero .carousel__controls {
		padding-inline-start: var(--gutter);
	}
}

.carousel--hero .carousel__slide,
.carousel--hero img.carousel__img {
	height: 100%;
}

.hero__tagline {
	position: absolute;
	top: 25px;
	right: 0;
	z-index: 2;
	padding: 0.8em 1em;
	background-color: var(--color-scrim);
	color: rgba(255, 255, 255, 0.85);
	font-size: var(--fs-hero-tagline);
	font-weight: var(--fw-normal);
	line-height: 26px;
}

@media (min-width: 641px) {

	.hero__tagline {
		top: 100px;
	}
}

/* Controls sit below the image, not overlaid — no absolute positioning
   or translucent panel here. Title, dots and pause are shared chrome
   defined elsewhere; this page adds nothing hero-specific. */

/* Mobile/tablet only: the title stays on the controls line instead of
   wrapping below the dots, unlike the project gallery — deliberate,
   since the homepage has no page below the fold for it to wrap into.

   Dot buttons are 24px wide for the WCAG 2.5.8 AA 24×24 touch-target
   minimum; only the hit area grew, the painted dot did not. */
@media (max-width: 1024px) {

	.carousel--hero .carousel__controls {
		flex-wrap: nowrap;
		padding-inline-start: var(--gutter);
	}

	.carousel--hero .carousel__title {
		order: 0;
		flex: 0 1 auto;
		margin-top: 0;
		margin-inline-end: auto;
		padding-inline: 0;
		font-size: 12px;
	}

	.carousel--hero .carousel__dot {
		width: 24px;
	}

	.carousel--hero .carousel__dot::before {
		margin: 12px 7px;
	}

	.carousel--hero .carousel__dots {
		margin-inline: 6px;
	}

	.carousel--hero .carousel__btn--pause {
		margin-right: 8px;
	}
}

/* --- 13.2 Projects archive ------------------------------------------------ */

.projects-archive {
	padding-block: var(--space-md) var(--space-section);
}

/* This page's content column is deliberately wider than the sitewide
   default. `max-width` is widened rather than zeroing `padding-inline`
   so the gutter still protects the edges, with no extra breakpoint
   needed. The qualifier is required since the shared `.container` rule
   is declared earlier in the file. */
.projects-archive .container {
	max-width: calc(var(--container) + var(--gutter) * 2);
}

/* --- 13.3 Single project -------------------------------------------------- */

/* Mobile and tablet keep a width-driven box: the height-driven fit
   below is desktop-only, since a short landscape phone would squash
   the slider almost flat, and unlike the hero this page has a title
   competing for the same space. */
.carousel--gallery .carousel__viewport {
	height: clamp(240px, 56vw, 720px);
}

.carousel--gallery .carousel__slide,
.carousel--gallery img.carousel__img {
	height: 100%;
}

/* Drawings and diagrams must not be cropped: tag those attachments and
   the slide switches to `contain`. A class on the image is all it needs. */
.carousel__img.is-contain,
.carousel__slide[data-fit="contain"] .carousel__img {
	object-fit: contain;
	background-color: var(--color-bg-sunk);
}

.project-single__body {
	padding-block: var(--space-lg);
}

/* With a gallery, the title sits in the controls row, so the body
   starts at the location line and needs 15px here, not the usual
   rhythm. Scoped to the adjacent-gallery selector so the image-less
   fallback (its own `.project-single__header`) keeps the 40px. */
.project-gallery + .project-single__body {
	padding-block-start: 15px;
}

/* 15px is a literal value, not on the spacing scale (--space-xs is 12,
   --space-sm is 16). Only reachable on the image-less fallback (see
   single-project.php); with a gallery this 15px is instead expressed
   by `.project-single__body`'s own top padding. */
.project-single__header {
	margin-bottom: 15px;
}

/* Height matches the hero's carousel exactly — do not subtract here. */
@media (min-width: 1025px) {

	.carousel--gallery .carousel__viewport {
		height: var(--carousel-fit-height);
	}

	/* The title has left the body, so it now starts at the location line.
	   Title-bottom to location-top is 15px; the title overlaps the row's
	   lower half, so this padding is 15px less the 5.3px of row beneath
	   the h1's line box. Measured, not derived. */
	.project-gallery + .project-single__body {
		padding-block-start: 9.7px;
	}
}

/* `.project-single__title` is a hook only — it carries no
   declarations. Tag stays `<h1>`, matching production's *appearance*
   (18px/400/uppercase) but not its heading level, which would be an
   accessibility regression. Title type lives in `.carousel__title`,
   shared with the homepage hero; do not add rules here, or the two
   pages can drift apart again. */


/* Tag stays `<p>`; type approximates production (16px/capitalize).
   `var(--fw-bold)` (700) stands in for production's unavailable 800
   cut — this theme only ships PT Sans 400 and 700. */
.project-single__location {
	font-size: 16px;
	font-weight: var(--fw-bold);
	text-transform: capitalize;
	letter-spacing: normal;
	color: var(--color-text-soft);
	/* No margin-bottom: the gap below this line is the grid's row-gap, so one
	   property owns that distance. See `.project-single__columns` below. */
	margin-top: 0;
	margin-bottom: 0;
}

.project-single__columns {
	display: grid;
	gap: var(--space-lg);
	grid-template-columns: 1fr;
}

@media (min-width: 1025px) {

	.project-single__columns {
		grid-template-columns: 3fr 1fr;
		column-gap: var(--space-xl);
		/* 37px row-gap is deliberately separate from the 64px column-gap:
		   the two distances aren't meant to move together, so they're set
		   here rather than by one `gap` shorthand. */
		row-gap: 37px;
	}

	/* The sidebar aligns with the first description paragraph, not the
	   location line — the location is a full-width row above both
	   columns (see single-project.php). All three are placed explicitly,
	   since a spanning item can't auto-place beside siblings that don't
	   span. */
	.project-single__location {
		grid-column: 1 / -1;
		grid-row: 1;
	}

	.project-single__main {
		grid-column: 1;
		grid-row: 2;
	}

	/* Description measure is 680px, scoped to `.project-single__main`
	   `.prose` — the global `.prose` 70ch cap stays untouched; other
	   pages depend on it. Don't derive char counts from `ch`; the unit
	   under-reports for PT Sans — measure in-browser instead. */
	.project-single__main .prose {
		max-width: 680px;
	}

	.project-single__aside {
		grid-column: 2;
		grid-row: 2;
	}

	/* The location is optional in the template; without it, pinning the
	   columns to row 2 would leave a phantom empty row 1 with a row-gap
	   above them. Latent today (every imported project has one), but a
	   project created by hand in wp-admin may not. */
	.project-single__main:first-child {
		grid-row: 1;
	}

	.project-single__main:first-child + .project-single__aside {
		grid-row: 1;
	}
}

/* No `.project-sustainability` rules: it isn't a section here. Values
   flow through the `additional_details` repeater and are styled by the
   same rules as SIZE and CLIENT below. */

/* A definition list, not a stack of "SIZE: 9,700 sf" strings — so the
   label can be styled and translated separately. Uppercase is applied
   here in CSS, never stored in the value. */
.project-details__row + .project-details__row {
	margin-top: var(--space-2xs);
}

.project-details__label {
	font-size: var(--fs-xs);
	font-weight: var(--fw-bold);
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--color-text-soft);
	line-height: var(--lh-tight);
}

.project-details__value {
	font-size: var(--fs-sm);
	line-height: 1.5;
	margin-left: 0;
}

/* --- Project prev / next --------------------------------------------------

   Rendered as visible thumbnail cards, a deliberate deviation from
   production, which hides its prev/next arrows. Modelled on the
   search-result card — house style has no boxed or elevated cards
   anywhere on the site, only hairline rules. */

.project-nav {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--space-sm);
	margin-top: var(--space-xl);
	padding-top: var(--space-md);
	border-top: 1px solid var(--color-border-light);
}

/* Placement is explicit below 641px, not left to auto-flow.
   `.project-nav__all` spans both columns, so auto-placement can't fit
   it beside the prev card and pushes it to row 2, collapsing the nav
   to one column. Pinning all three cells keeps the cards side by side
   with the link beneath. The subgrid block below splits each card
   across two rows (body, thumb), so the link's row moves to 3 there —
   both placements are explicit for the same reason. */
.project-nav__side--prev {
	grid-area: 1 / 1;
}

.project-nav__side--next {
	grid-area: 1 / 2;
}

.project-nav__all {
	grid-area: 2 / 1 / 3 / -1;
	justify-self: center;
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	gap: var(--space-3xs);
}

.project-nav__all:hover,
.project-nav__all:focus-visible {
	color: var(--color-accent);
}

.project-nav__link {
	display: flex;
	flex-direction: column;
	gap: var(--space-2xs);
}

/* Text sits above the thumb on a phone; `order` is used rather than a
   DOM change, since prev/next carry opposite source orders on purpose
   (thumb first on prev, last on next) so visual order matches source
   order at ≥641px without `row-reverse`, which fakes order and breaks
   tab/reader order. The subgrid block below does the same job via an
   explicit `grid-row`. */
.project-nav__thumb {
	order: 2;
}

/* Flex items default to `min-width: auto`, so a long unbroken project title
   would push the thumb out of the row instead of wrapping. */
.project-nav__body {
	min-width: 0;
}

/* No `height` is declared here, so there's nothing for reset.css's
   `img[width][height]` (0,2,1) to beat — `img.project-nav__img` alone
   is only (0,1,1) and would lose that fight. `aspect-ratio: 1` plus
   the reset's own `height: auto` gives the sizing with no fight at
   all. If a height is ever needed it must be a separate
   `img[width][height].project-nav__img { height: 100% }` rule, never
   added to the block below. */
img.project-nav__img {
	width: 100%;
	aspect-ratio: 1;
	object-fit: cover;
}

/* Right-aligned at every width, not just desktop: this mirrors the
   prev card across the gutter, so label and title both hug the nav's
   outer edge. */
.project-nav__side--next .project-nav__body {
	text-align: right;
}

.project-nav__label {
	display: block;
	font-size: var(--fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	color: var(--color-text-soft);
}

/* Titles show at every width. `display: block` is load-bearing, not
   tidying: this is a <span>, and an inline box's own line-height is a
   floor, not a setting — each line box takes the taller of the inline
   content's line-height and the parent's strut, and
   `.project-nav__body` inherits the 1.7 body value. Blockifying is
   what makes `--lh-tight` actually govern. */
.project-nav__title {
	display: block;
	font-size: var(--fs-sm);
	font-weight: var(--fw-bold);
	line-height: var(--lh-tight);
	transition: color var(--dur-fast) var(--ease);
}

/* A page-scoped colour rule must ship its own hover/focus pair, or it
   sits silently inert under the global `a:hover`. */
.project-nav__link:hover .project-nav__title,
.project-nav__link:focus-visible .project-nav__title {
	color: var(--color-accent);
}

.project-nav__grid-icon {
	width: 19px;
	height: 19px;
	fill: currentColor;
	stroke: none;
	flex-shrink: 0;
}

/* Moved off .project-nav__all so the underline runs under the text only, not
   the icon above it. */
.project-nav__all-text {
	font-size: var(--fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	border-bottom: 1px solid var(--color-border);
	padding-bottom: 2px;
}

/* Below 641px the two cards are independent flex columns, so a title
   that wraps to two lines pushes its own thumb down while the
   single-line neighbour's stays put. Subgrid fixes this at the layout
   level: `.project-nav` gets a dedicated thumb row, `.project-nav__side`
   goes `display: contents` so each link becomes the grid item and
   adopts the parent's rows.

   Guarded by `@supports` so a browser without subgrid keeps the
   flex-column layout above, misaligned but never broken. The base
   rules stay unconditional since everything here only overrides them. */
@supports (grid-template-rows: subgrid) {

	.project-nav {
		grid-template-rows: auto auto auto;
	}

	/* The side <div> carries no styling of its own, so dissolving its box costs
	   nothing and is what lets the link reach the parent grid's row tracks. */
	.project-nav__side {
		display: contents;
	}

	.project-nav__link {
		display: grid;
		grid-template-rows: subgrid;
		grid-row: 1 / 3;
	}

	/* The column pinning moves off the side and onto the link, since the side
	   no longer generates a box to place. */
	.project-nav__side--prev .project-nav__link {
		grid-column: 1;
	}

	.project-nav__side--next .project-nav__link {
		grid-column: 2;
	}

	.project-nav__body {
		grid-row: 1;
	}

	.project-nav__thumb {
		grid-row: 2;
	}

	.project-nav__all {
		grid-area: 3 / 1 / 4 / -1;
	}
}

@media (min-width: 641px) {

	.project-nav {
		grid-template-columns: 1fr auto 1fr;
		grid-template-rows: auto;
		align-items: center;
	}

	/* The three below undo the subgrid block: the side gets its box back, the
	   link goes back to being a flex row, and every explicit placement is
	   released to auto-flow. Written unconditionally rather than inside a
	   second @supports — they are no-ops where the subgrid block never ran. */
	.project-nav__side {
		display: block;
	}

	.project-nav__side--prev,
	.project-nav__side--next,
	.project-nav__side--prev .project-nav__link,
	.project-nav__side--next .project-nav__link,
	.project-nav__all {
		grid-area: auto;
	}

	.project-nav__link {
		display: flex;
		flex-direction: row;
		align-items: center;
		gap: var(--space-sm);
	}

	.project-nav__body {
		grid-row: auto;
	}

	.project-nav__thumb {
		grid-row: auto;
		order: 0;
		flex: 0 0 96px;
	}

	.project-nav__side--next .project-nav__link {
		justify-content: flex-end;
	}
}

/* --- 13.4 Who We Are ------------------------------------------------------
   Two-column layout is rebuilt in CSS only, not with core/columns — page-
   specific, not a shared content pattern.
   -------------------------------------------------------------------------- */

.who-we-are .page-content {
	padding-bottom: var(--space-md);
}

.who-we-are .container {	
	max-width: 1198px;
}

/* Below 641px the fixed 320px column plus the photo has no room; this
   reverts to plain block flow and the two-column layout applies at 641px+. */
@media (min-width: 641px) {

	.who-we-are .page-content .prose {
		/* `.prose` defaults to `max-width: 70ch` (≈630px here); this layout
		   needs the full 1036px content box (1100px .container minus the 32px
		   gutter each side). */
		max-width: none;
		/* Containing block for the photo, which is positioned out of flow. */
		position: relative;
	}

	/*
	 * Every text child stacks in a 320px column. This is plain block flow,
	 * not a grid: a grid distributes a spanning item's height across every
	 * row it spans, which breaks the stack. Do not restore the grid.
	 */
	.who-we-are .page-content .prose > * {
		max-width: 330px;
	}
}

/* Page-specific spacing exception; not part of the shared prose rhythm. */
.who-we-are .page-content .prose > p:first-child {
	padding-bottom: 10px;
}

/*
 * Renders the photo at its own aspect ratio rather than forcing a fixed
 * crop box: no cropped version of this photo exists as a separate
 * attachment, and one is not imported without confirming it's needed.
 */
@media (min-width: 641px) {

	.who-we-are .page-content .prose figure.wp-block-image {
		/*
		 * Out of flow so it doesn't affect the text column's height, and centred
		 * against the whole text stack. The text column is a fixed 320px so its
		 * height is stable, while the photo's width — and so its height — shrinks
		 * with the container, so the photo can never outgrow the stack.
		 */
		position: absolute;
		top: 50%;
		left: calc(320px + var(--space-lg));
		right: 0;
		transform: translateY(-50%);
		max-width: none;
		margin: 0;
	}
}

/*
 * Below 641px the photo is back in normal flow, so its DOM position
 * matters again: LEADERSHIP is generated and appended after the_content()
 * (see templates/page-who-we-are.php), which puts the photo ahead of it.
 * The rule above takes the photo out of flow entirely at 641px+, so this
 * only applies below that breakpoint.
 *
 * `order` needs a flex or grid context. Flex is safe here because
 * `.prose`'s rhythm is margin-top only — flex's summing produces the same
 * result as block flow's collapsing — and default `align-items: stretch`
 * keeps every child full-width as before.
 */
@media (max-width: 640px) {

	.who-we-are .page-content .prose {
		display: flex;
		flex-direction: column;
	}

	.who-we-are .page-content .prose figure.wp-block-image {
		/* Send the photo back to the end, where the authored markup used to
		   put it. Everything else keeps its default order of 0. */
		order: 1;
	}
}

.who-we-are .staff-grid {
	margin-bottom: var(--space-section);
}

/* --- 13.5 Firm Profile ------------------------------------------------------
   Rebuilt in CSS only; scoped to `.page--profile` via the slug hook in
   inc/setup.php. Contained float plus offset, not a grid — see the
   column-assignment comment below.
   -------------------------------------------------------------------------- */

/* Below 641px the fixed-ratio columns have no room; the breakpoint
   matches Who We Are's for consistency between the two rebuilt rows. */
@media (min-width: 641px) {

	/* `flow-root`, not `block`: gives `.prose` its own formatting context so
	   the floated column 1 is contained — without it, whenever column 1 is
	   taller than column 2, the float escapes the bottom of `.prose` and
	   following content sits at column 2's height. `overflow: hidden` also
	   contains but was rejected: it clips rather than contains, so
	   unexpectedly tall content would be cut off instead of pushing the
	   page down. */
	.page--profile .page-content .prose {
		max-width: none;
		display: flow-root;
	}

	/*
	 * Column assignment is keyed to top-level block position (the leading
	 * figure plus the two staff-affiliation lists are column 1; everything
	 * from the lead paragraph on is column 2), not list length, so it
	 * survives either list gaining or losing items.
	 *
	 * Only column 1 floats; column 2 gets a plain margin-left that clears
	 * it, rather than floating both sides or using grid — grid couples row
	 * heights across columns, and floats cannot rise above an earlier float
	 * in document order, so neither can build two independently-flowing
	 * columns here.
	 *
	 * Column 2's `<ul>` needs the margin: without it, `.prose ul`'s
	 * padding-left would put its list markers inside column 1's box.
	 */
	.page--profile .page-content .prose > *:nth-child(-n+5) {
		float: left;
		clear: left;
		width: calc((100% - 30px) / 3);
	}

	.page--profile .page-content .prose > *:nth-child(n+6) {
		margin-left: calc((100% - 30px) / 3 + 30px);
	}

	/* The figure's inline `width: 400px` is already capped by reset.css's
	   `img { max-width: 100% }`, so it sizes itself to column 1's width for
	   free. */
}

/* Bottom margin on the content itself, applied at all widths — not part
   of the column grid above. */
.page--profile .page-content .prose {
	margin-bottom: 70px;
}

/* Scoped to `.page--profile`, not merged into the shared `.prose a`
   rule: that rule is a deliberate sitewide decision, and changing it
   here would move every other page's prose links too. The underline is
   overridden back off to match old's un-underlined staff-affiliation
   links. */
.page--profile .prose ul a {
	font-weight: var(--fw-bold);
	color: rgba(0, 0, 0, 0.9);
	text-decoration: none;
}

/* Load-bearing: the rule above is (0,2,2) and the global `a:hover` is
   (0,1,1); without this the links never change colour on hover. */
.page--profile .prose ul a:hover,
.page--profile .prose ul a:focus-visible {
	color: var(--color-accent);
}

/* --- 13.6 Search results -------------------------------------------------- */

.search-results-page__form {
	margin-bottom: var(--space-md);
}

.search-results-page__count {
	font-size: var(--fs-sm);
	color: var(--color-text-soft);
	margin-bottom: var(--space-sm);
}

.search-results {
	list-style: none;
	border-top: 1px solid var(--color-border-light);
}

.search-result {
	border-bottom: 1px solid var(--color-border-light);
	padding-block: var(--space-sm);
}

.search-result article {
	display: flex;
	gap: var(--space-sm);
	align-items: flex-start;
}

.search-result__media {
	flex: 0 0 80px;
}

@media (min-width: 641px) {

	.search-result__media {
		flex-basis: 160px;
	}
}

/* No `height` is declared, so there is nothing for reset.css's
   `img[width][height]` to beat; `aspect-ratio: 1` plus the reset's
   `height: auto` gives the sizing. The `img.` qualifier is cosmetic — it
   would not win a height fight if one were added; use
   `img[width][height].` instead. */
img.search-result__img {
	width: 100%;
	aspect-ratio: 1;
	object-fit: cover;
}

.search-result__type {
	font-size: var(--fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	color: var(--color-text-soft);
}

.search-result__title {
	font-size: var(--fs-h3);
	margin-top: 2px;
}

.search-result__excerpt {
	font-size: var(--fs-sm);
	color: var(--color-text-muted);
	margin-top: var(--space-3xs);
}

.search-term {
	font-style: italic;
}

/* --- 13.7 404 ---------------------------------------------------------- */

.error-404__search {
	margin-block: var(--space-md);
}

.error-404__links {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-sm) var(--space-md);
	margin-bottom: var(--space-xl);
}

.error-404__links a {
	font-size: var(--fs-sm);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	border-bottom: 1px solid var(--color-border);
	padding-bottom: 2px;
}

.error-404__projects h2 {
	font-size: var(--fs-h3);
	text-transform: uppercase;
	letter-spacing: var(--ls-wide);
	margin-bottom: var(--space-sm);
}

/* --- 13.8 Generic index fallback ------------------------------------------ */

.post-list__item + .post-list__item {
	margin-top: var(--space-lg);
	padding-top: var(--space-lg);
	border-top: 1px solid var(--color-border-light);
}

.post-list__title {
	font-size: var(--fs-h3);
	margin-bottom: var(--space-2xs);
}

/* --- 13.9 Location ----------------------------------------------------------
   Two columns out of a flat block flow: the map figure floats, and everything
   else is a plain offset beside it. `.page--location` comes from the slug
   body-class hook in inc/setup.php.

   Widths derive from a 12-column unit

       u = (100% + 30px) / 12

   where the 30px is the gutter: `u * n - 30px` is a column, `u * n` is an
   offset. It is proportional and re-derives itself at any container width —
   do not replace it with fixed pixel values. Sections 13.10 to 13.13 reuse
   this same unit.
   -------------------------------------------------------------------------- */

/* The map slot. `.location-map` is authored on the figure in the page body,
   not emitted by a template — keyed on the wrapper rather than the `<img>` so
   it survives the editor, and on a class rather than `:first-child`, which is
   the address `<h5>` whenever the image is absent.

   `min()` lets reset.css's `max-width: 100%` take over below 505px. The top
   margin sits on the wrapper so the floated box carries it. */
.page--location .page-content .prose > .location-map {
	margin-block: 10px 0;
}

.page--location .page-content .prose > .location-map img {
	width: min(505px, 100%);
	margin-inline: auto;
	margin-top: 10px;
}

/* Two columns from 641px, the same step the other multi-column page rows use.
   Do not raise it to 1025: 1024 is a reference viewport, so a `min-width`
   step meant to include it must be <= 1024. */
@media (min-width: 641px) {

	/* `flow-root`, not `overflow: hidden`: both contain the float, but `hidden`
	   clips where `flow-root` contains, so a taller map would be cut off
	   instead of pushing the page down.

	   The three custom properties are read only by children of `.prose`, so
	   their `100%` resolves against `.prose`'s own content width. Do not move
	   them onto a child. */
	.page--location .page-content .prose {
		--loc-col-unit: calc((100% + 30px) / 12);
		--loc-col-map: calc(var(--loc-col-unit) * 4 - 30px);
		--loc-col-offset: calc(var(--loc-col-unit) * 5);

		max-width: none;
		display: flow-root;
	}

	/* Column 2 is every other top-level block: a plain margin, never a float
	   and never a grid track, so it keeps its own rhythm beside a taller
	   column 1. The margin is load-bearing, not tidiness — a full-width box
	   beside a float keeps its full width, so a `.prose ul` here would paint
	   its markers on top of the map. */
	.page--location .page-content .prose > * {
		margin-left: var(--loc-col-offset);
	}

	/* Column 1. Higher specificity than the rule above, so source order is not
	   load-bearing. `clear: left` guards a second float ever being added. */
	.page--location .page-content .prose > .location-map {
		float: left;
		clear: left;
		width: var(--loc-col-map);
		margin-left: 0;
	}

	.page--location .page-content .prose > .location-map img {
		width: 100%;
		margin-inline: 0;
	}
}

/* A wider gap before one paragraph, keyed on a class authored in the page
   body rather than on `:nth-of-type`. A positional selector moves the gap to
   the wrong paragraph the moment one is inserted above it, which reads as a
   defect nobody would trace back to a stylesheet. */
.page--location .page-content .prose > p.prose-gap-before {
	margin-top: 47.2px;
}

/* Margins for the inline logo figure. `display: block` is required: under
   wp-block-library's `is-resized` rule the figure can become a `display:
   table` box, which does not margin-collapse with its siblings and changes
   both gaps. `:not(.location-map)` keeps this off the map slot, which sets
   its own margins. Page-scoped rather than changing the global `.prose img`,
   which every other page depends on. */
.page--location .page-content .prose > figure.wp-block-image:not(.location-map) {
	display: block;
	margin-block: 27.2px 17.7px;
}

.page--location .page-content .prose > figure.wp-block-image:not(.location-map) img {
	margin-block: 0;
}

/* Headings on this page are capitalised in presentation only — the stored
   content stays lower-case so it reads correctly in search results. Scoped
   to this page: `.prose h5` is shared with others. */
.page--location .prose h5,
.page--location .prose h2.section-heading {
	text-transform: capitalize;
}

/* The email link: bold and not underlined, page-scoped rather than changed
   on the shared `.prose a` rule. Scoped by `[href^="mailto:"]` so the anchor
   wrapping the logo is untouched.

   The hover/focus rule below is required, not tidiness: this selector is
   (0,3,1) and the global `a:hover` is (0,1,1), so declaring `color` here
   would otherwise beat the hover state and the link would never change. */
.page--location .prose a[href^="mailto:"] {
	font-weight: var(--fw-bold);
	color: rgba(0, 0, 0, 0.9);
	text-decoration: none;
}

.page--location .prose a[href^="mailto:"]:hover,
.page--location .prose a[href^="mailto:"]:focus-visible {
	color: var(--color-accent);
}

/* --- 13.10 Services ---------------------------------------------------------
   Three content columns beside a photo, all floated, using the column unit
   defined in section 13.9: a 4 / 1 / 4 / 3 split, with the 1 expressed as an
   offset rather than as an empty element.

   The two wrapper divs in the page body are what group each column's two
   heading-plus-list pairs; nothing else in the markup delimits them, and
   floating each heading and list individually packs by available width, so
   adding one list item migrates a heading into the other column. CSS grid is
   not an option: it sizes every row to the tallest cell across all columns
   and so couples the columns' rhythm together.

   Columns are top-aligned, which floats give by default.
   -------------------------------------------------------------------------- */

.page--services .page-content .prose {
	--svc-unit: calc((100% + 30px) / 12);

	max-width: none;
}

/* From 641px, the same step the other multi-column page rows use. Do not
   raise it to 1025: 1024 is a reference viewport, so a `min-width` step
   meant to include it must be <= 1024. */
@media (min-width: 641px) {

	/* `flow-root`, not `overflow: hidden`: both contain the float, but
	   `hidden` clips where `flow-root` contains. */
	.page--services .page-content .prose {
		display: flow-root;
	}

	.page--services .page-content .prose > .services-image,
	.page--services .page-content .prose > .services-col {
		float: left;
		/* `.prose > * + *`'s 16px would otherwise push column D and column C
		   down out of alignment with the photo. Columns are top-aligned. */
		margin-top: 0;
	}

	.page--services .page-content .prose > .services-image {
		width: calc(var(--svc-unit) * 4 - 30px);
	}

	/* The 1u spacer column is expressed as an offset: there is no empty
	   element, because an element whose only job is to create space belongs
	   in CSS, not in the page body. */
	.page--services .page-content .prose > .services-col--c {
		width: calc(var(--svc-unit) * 4 - 30px);
		margin-left: calc(var(--svc-unit) * 1 + 30px);
	}

	.page--services .page-content .prose > .services-col--d {
		width: calc(var(--svc-unit) * 3 - 30px);
		margin-left: 30px;
	}
}

/* The photo is this page's first body element, so the global
   `.prose img { margin-block: 24px }` lands above it and reads as a gap under
   the page title. Zeroed page-scoped rather than by changing that global
   rule, which every other page depends on. */
.page--services .page-content .prose > .services-image img {
	margin-block: 0;
}

/* List rhythm for this page only. `.prose ul`, `.prose li` and
   `.prose li + li` are shared with other pages — do not lift these values
   into them. */
.page--services .page-content .prose ul {
	margin-top: 0;
	padding-left: 0;
}

.page--services .page-content .prose li {
	margin-left: 14px;
}

.page--services .page-content .prose li + li {
	margin-top: 0;
}

/* Extra space before a section heading that follows the previous section's
   list. Keyed structurally rather than on a class or `:nth-of-type`: it is
   exactly what the rule means, it needs no content edit, and it stays
   correct if a third service group is added to either column. */
.page--services .page-content .prose ul + h5,
.page--services .page-content .prose ul + h2.section-heading {
	margin-top: 52.2px;
}

/* --- 13.11 Sustainability ---------------------------------------------------
   A 4 / 8 split using the column unit defined in section 13.9: one floated
   rail plus an offset body column, never three floats and never a grid. The
   body column is not floated, so it keeps its own uninterrupted rhythm
   beside a much taller rail.

   The two wrapper divs in the page body are what separate the rail from the
   body copy; nothing in the markup delimits them otherwise.
   -------------------------------------------------------------------------- */

.page--sustainability .page-content .prose {
	--sus-unit: calc((100% + 30px) / 12);

	max-width: none;
}

/* From 641px, the same step the other multi-column page rows use. Do not
   raise it to 1025: 1024 is a reference viewport, so a `min-width` step
   meant to include it must be <= 1024. */
@media (min-width: 641px) {

	/* `flow-root`, not `overflow: hidden`: both contain the float, but
	   `hidden` clips where `flow-root` contains. */
	.page--sustainability .page-content .prose {
		display: flow-root;
	}

	.page--sustainability .page-content .prose > .sustain-rail {
		float: left;
		clear: left;
		width: calc(var(--sus-unit) * 4 - 30px);
		margin-top: 0;
	}

	/* Never floated — a plain offset that clears the rail's box. The explicit
	   margin is load-bearing: a full-width box beside a float keeps its full
	   width, so the body's `ul` markers would otherwise paint on top of the
	   rail. */
	.page--sustainability .page-content .prose > .sustain-body {
		margin-left: calc(var(--sus-unit) * 4);
		margin-top: 0;
	}
}

/* A wrapper div drops its children out of the global
   `.prose > * + * { margin-top: var(--space-sm) }` rhythm, which matches
   direct children only. Headings and images keep their own margins, because
   `.prose h5` and `.prose img` select at any depth, but every
   paragraph-to-paragraph, paragraph-to-list and heading-to-paragraph gap
   collapses to 0. This restores it inside both wrappers; the explicit
   spacing rules below still win, on the specificity their type selectors
   add.

   Whether a wrapper wants this restore is decided per page — Recognition
   (section 13.12) deliberately does not have it. */
.page--sustainability .prose .sustain-rail > * + *,
.page--sustainability .prose .sustain-body > * + * {
	margin-top: var(--space-sm);
}

/* List rhythm for this page only. `.prose ul` and `.prose li + li` are
   shared with other pages — do not lift these values into them.

   Note the difference from Services (section 13.10), which also zeroes
   `ul { margin-top }`: there the list follows a heading, here it follows a
   paragraph, and zeroing the top margin here would over-correct. */
.page--sustainability .page-content .prose ul {
	padding-left: 0;
}

.page--sustainability .page-content .prose li {
	margin-left: 14px;
}

.page--sustainability .page-content .prose li + li {
	margin-top: 0;
}

/* The rail's vertical spacing is taken over wholesale rather than nudged:
   zero the `img` margins inside the rail so each figure box is its image
   box, then state each gap explicitly below. Margin collapsing between a
   `figure`, its `figcaption` and the next `figure` makes incremental
   adjustments unreliable. */
.page--sustainability .prose .sustain-rail figure img {
	margin-block: 0;
}

.page--sustainability .prose .sustain-rail > figure + figure {
	margin-top: 37.2px;
}

/* Keyed on `p + figure`: exactly one rail paragraph is immediately followed
   by a figure, so the selector is unambiguous without a class. */
.page--sustainability .prose .sustain-rail > p + figure {
	margin-top: 77.6px;
}

/* The body column's spacer, keyed structurally — the only `ul` on the page,
   and "the paragraph resuming after a list" is what the rule means. */
.page--sustainability .prose .sustain-body > ul + p {
	margin-top: 37.2px;
}

/* The one linked image on the page: its `<a>` is an inline-block, which
   stops the caption's margin collapsing and drops that caption lower than
   the other four. `display: block` restores the collapse. */
.page--sustainability .prose .sustain-rail figure a {
	display: block;
}

/* --- 13.12 Recognition ------------------------------------------------------
   A 4 / 8 split using the column unit defined in section 13.9: one floated
   rail plus an offset body column, never three floats and never a grid. The
   two wrapper divs in the page body separate the rail figure from the body
   copy; nothing in the markup delimits them otherwise.

   This page deliberately has no `> * + *` base-rhythm restore of the kind
   Sustainability (section 13.11) and Careers (section 13.13) carry. Do not
   add one — it is a regression here, not a missing feature. Walking the
   junctions inside `.recognition-body`: intro paragraph to `h5` is already
   supplied by `.prose h5`'s own margin-top, which applies at any nesting
   depth; `h5` to `ul` and `ul` to `ul` both want 0, which is exactly what
   not restoring the base rhythm gives; `ul` to `h5` is set explicitly below.
   Restoring the base would put 16px back at all 31 `h5`-to-`ul` junctions.
   `.recognition-rail` holds one child and has no `+ *` junction at all.
   -------------------------------------------------------------------------- */

.page--recognition .page-content .prose {
	--rec-unit: calc((100% + 30px) / 12);

	max-width: none;
}

/* From 641px, the same step the other multi-column page rows use. Do not
   raise it to 1025: 1024 is a reference viewport, so a `min-width` step
   meant to include it must be <= 1024. */
@media (min-width: 641px) {

	/* `flow-root`, not `overflow: hidden`: both contain the float, but
	   `hidden` clips where `flow-root` contains. */
	.page--recognition .page-content .prose {
		display: flow-root;
	}

	.page--recognition .page-content .prose > .recognition-rail {
		float: left;
		clear: left;
		width: calc(var(--rec-unit) * 4 - 30px);
		margin-top: 0;
	}

	/* Never floated — a plain offset that clears the rail's box. A full-width
	   box beside a float keeps its full width, so `.prose ul`'s padding would
	   otherwise paint bullets over the rail. */
	.page--recognition .page-content .prose > .recognition-body {
		margin-left: calc(var(--rec-unit) * 4);
		margin-top: 0;
	}
}

/* The figure box is the image box, so both column tops start at the same y. */
.page--recognition .prose .recognition-rail figure img {
	margin-block: 0;
}

/* List rhythm for this page only. `.prose ul`, `.prose li` and
   `.prose li + li` are shared with other pages — do not lift these values
   into them. */
.page--recognition .page-content .prose ul {
	padding-left: 0;
}

.page--recognition .page-content .prose li {
	margin-left: 14px;
}

.page--recognition .page-content .prose li + li {
	margin-top: 0;
}

/* Extra space before a year heading that follows the previous year's list,
   on top of `.prose h5`'s own margin-top. Keyed structurally rather than on
   a class or a positional selector: it needs no content edit, so a later
   content repair that renumbers blocks cannot break it. */
.page--recognition .prose .recognition-body ul + h5,
.page--recognition .prose .recognition-body ul + h2.section-heading {
	margin-top: 52.2px;
}

/* Award links are distinguished by weight rather than by underline, so the
   global `.prose a` underline is turned off here. Bold against 400 body text
   is the non-colour distinction WCAG 1.4.1 needs — do not drop the weight
   and leave colour to carry it. */
.page--recognition .prose .recognition-body li a {
	text-decoration: none;
	font-weight: var(--fw-bold);
	color: rgba(0, 0, 0, 0.9);
}

/* Required state pair: a page-scoped rule that declares a property a shared
   `:hover`/`:focus` rule also declares must ship its own, or it out-specifies
   the shared rule and the state goes dead on this page alone. */
.page--recognition .prose .recognition-body li a:hover,
.page--recognition .prose .recognition-body li a:focus-visible {
	color: #c01a29;
	text-decoration: underline;
}

/* --- 13.13 Careers -----------------------------------------------------------
   A 6 / 6 split using the column unit defined in section 13.9: one floated
   image column plus an offset text column, never a grid. `.careers-image`
   wraps the figure and `.careers-body` the paragraphs and headings; nothing
   in the markup delimits them otherwise.

   This page does need the `> * + *` base-rhythm restore, and Recognition
   (section 13.12) deliberately does not — both are correct. Walking
   `.careers-body`'s junctions: paragraph to paragraph wants the base gap;
   paragraph to heading wants the base gap plus the explicit override set
   below; heading to heading wants 0, also set explicitly. Without the
   restore the first two collapse to 0. Whether a wrapper wants the restore
   is a per-page call — walk its junctions every time one is used.
   -------------------------------------------------------------------------- */

.page--careers .page-content .prose {
	--car-unit: calc((100% + 30px) / 12);

	max-width: none;
}

/* From 641px, the same step the other multi-column page rows use. Do not
   raise it to 1025: 1024 is a reference viewport, so a `min-width` step
   meant to include it must be <= 1024. */
@media (min-width: 641px) {

	/* `flow-root`, not `overflow: hidden`: both contain the float, but
	   `hidden` clips where `flow-root` contains. */
	.page--careers .page-content .prose {
		display: flow-root;
	}

	.page--careers .page-content .prose > .careers-image {
		float: left;
		clear: left;
		width: calc(var(--car-unit) * 6 - 30px);
	}

	/* Never floated — a plain offset clearing the image column's box; a
	   float shortens line boxes, not blocks. */
	.page--careers .page-content .prose > .careers-body {
		margin-left: calc(var(--car-unit) * 6);
	}
}

/* A 32px offset on both columns, in CSS rather than as empty markup. It
   applies at every width, since both columns carry it regardless of
   layout. */
.page--careers .page-content .prose > .careers-image,
.page--careers .page-content .prose > .careers-body {
	margin-top: 32px;
}

/* The figure is this page's first body element, so the global
   `.prose img { margin-block: 24px }` collapses through it and out of the
   top of `.prose`, reading as a gap under the page title. The 32px rule
   above is then the only offset. This is a page-scoped image margin, not a
   fault in the global banner spacing — do not change that instead. */
.page--careers .prose .careers-image figure img {
	margin-block: 0;
}

/* Required here, and deliberately absent from Recognition (section 13.12).
   A wrapper drops its children out of `.prose > * + *`, which matches direct
   children only; three of the four junctions inside `.careers-body` want the
   base gap back. */
.page--careers .prose .careers-body > * + * {
	margin-top: var(--space-sm);
}

/* Both headings render at body size on this page, not the theme's heading
   step. Page-scoped: the shared `.prose h2, .prose h3, .prose h4` rule is
   used by other pages. */
.page--careers .prose .careers-body h3,
.page--careers .prose .careers-body h2 {
	font-size: 16px;
	line-height: 33px;
	color: #333;
	margin-top: 0;
	/* Without this the `<h2>` picks up the global `h2` letter-spacing; these
	   headings compute `normal`. */
	letter-spacing: normal;
}

/* Extra space before "Available Positions:", on top of the restored base
   gap. Keyed structurally, as on the other pages. This rule and the
   `margin-top: 0` heading rule above are both (0,3,1), so this one wins
   only by coming later — it must stay after it. Both beat the (0,3,0)
   restore rule. */
.page--careers .prose .careers-body > p + h3,
.page--careers .prose .careers-body > p + h2 {
	margin-top: 53.2px;
}

.page--careers .prose .careers-body h3 a,
.page--careers .prose .careers-body h2 a {
	text-decoration: none;
	color: #333;
}

/* Required state pair: a page-scoped rule that declares a property a shared
   `:hover`/`:focus` rule also declares must ship its own, or it out-specifies
   the shared rule and the state goes dead on this page alone. */
.page--careers .prose .careers-body h3 a:hover,
.page--careers .prose .careers-body h3 a:focus-visible,
.page--careers .prose .careers-body h2 a:hover,
.page--careers .prose .careers-body h2 a:focus-visible {
	color: #c01a29;
	text-decoration: underline;
}


/* ==========================================================================
   14. PLUGIN CSS
   ==========================================================================
   Reserved for CSS belonging to a plugin rather than to the theme, so it
   lives in version control instead of in a plugin's own custom-CSS field.

   The cookie banner is Complianz's, not the theme's — its CSS lives in
   section 12 above, rescued out of the plugin's custom-CSS field so it is
   version-controlled. Put further Complianz fixes in section 12, not here:
   one source of truth for the banner is worth more than a verbatim port.
   -------------------------------------------------------------------------- */


/* --------------------------------------------------------------------------
   Add a labelled sub-block per plugin as they arrive; do not let plugin CSS
   leak into the sections above.
   -------------------------------------------------------------------------- */
