/* ==========================================================================
   Filmscape — base tokens & reset
   ========================================================================== */

:root {
	--color-bg: #000000;
	--color-white: #ffffff;
	--color-blue: #2c53d0;
	--font-sans: 'Open Sans', system-ui, -apple-system, sans-serif;
	--container-max: 1680px;
	--container-pad: clamp(20px, 4vw, 42px);
	/* Contact page glow size — base clamp(180px, 45vmin, 520px) scaled +35%
	   for desktop; overridden to +80% under the mobile breakpoint below. */
	--contact-glow-size: clamp(243px, 60.75vmin, 702px);
}

@media (max-width: 760px) {
	:root {
		--contact-glow-size: clamp(324px, 81vmin, 936px);
	}
}

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

html {
	scroll-behavior: smooth;
}

body {
	margin: 0;
	background: var(--color-bg);
	color: var(--color-white);
	font-family: var(--font-sans);
	font-size: 16px;
	line-height: 1.5;
	-webkit-font-smoothing: antialiased;
}

img {
	max-width: 100%;
	display: block;
}

a {
	color: inherit;
	text-decoration: none;
}

.is-condensed {
	font-stretch: 75%;
	font-weight: 600;
}

.is-thin {
	font-weight: 300;
}

.is-bold {
	font-weight: 800;
}

.is-extrabold {
	font-weight: 800;
}

.screen-reader-text {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip: rect(0 0 0 0);
}

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

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border: none;
	border-radius: 999px;
	font-family: var(--font-sans);
	font-stretch: 75%;
	font-weight: 700;
	font-size: clamp(14px, 1.1vw, 20px);
	letter-spacing: 0.24em;
	padding: 0.85em 1.8em;
	white-space: nowrap;
	cursor: pointer;
	transition: transform 0.15s ease, opacity 0.15s ease;
}

.btn:hover {
	transform: translateY(-2px);
	opacity: 0.92;
}

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

.btn--outline {
	background: transparent;
	border: 1px solid var(--color-white);
	color: var(--color-white);
	font-stretch: 75%;
	font-weight: 600;
	letter-spacing: 0.05em;
	padding: 0.75em 1.6em;
}

.btn--wide {
	padding-inline: 2.4em;
}

/* ==========================================================================
   Header
   ========================================================================== */

.site-header {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	z-index: 100;
	padding: clamp(4px, 0.7vw, 11px) var(--container-pad);
	background: #000000;
}

.site-header__inner {
	max-width: var(--container-max);
	margin-inline: auto;
	display: grid;
	grid-template-columns: 1fr auto 1fr;
	align-items: center;
	gap: clamp(13px, 2.48vw, 33px);
}

.site-logo {
	justify-self: start;
}

.site-header__cta {
	justify-self: end;
}

.site-logo img {
	height: clamp(39px, 4.29vw, 68px);
	width: auto;
}

.nav-toggle {
	display: none;
	flex-direction: column;
	justify-content: center;
	gap: 4px;
	width: 33px;
	height: 33px;
	background: transparent;
	border: 1px solid var(--color-white);
	border-radius: 50%;
	cursor: pointer;
	flex: 0 0 auto;
}

.nav-toggle span {
	display: block;
	width: 13px;
	height: 2px;
	background: var(--color-white);
	margin-inline: auto;
}

.site-nav {
	flex: 1 1 auto;
	display: flex;
	justify-content: center;
	min-width: 0;
}

.site-nav__list {
	display: flex;
	align-items: center;
	list-style: none;
	margin: 0;
	padding: 0;
	border: 1.5px solid var(--color-white);
	border-radius: 999px;
	gap: 2px;
	/* Anchors .site-nav__pill's absolute positioning (desktop only — see
	   main.js) below. */
	position: relative;
}

/* Desktop's sliding equivalent of .site-nav__item.is-active a's own
   background below — a separate element (added by main.js) instead of
   that background itself, so it can transition its position/width on a
   nav click instead of the active state just hard-cutting from one link
   to the next. Positioned/sized via JS (a fixed CSS position can't know
   any link's real rendered width) the same "measure real elements"
   pattern used throughout main.js. Sits behind the link text (z-index 0
   vs. 1 below). */
.site-nav__pill {
	position: absolute;
	top: 0;
	left: 0;
	height: 100%;
	width: 0;
	border-radius: 999px;
	background: var(--color-white);
	transition: transform 0.3s ease, width 0.3s ease;
	pointer-events: none;
	z-index: 0;
}

.site-nav__item a {
	position: relative;
	z-index: 1;
	display: block;
	padding: 0.45em 1.16em;
	border-radius: 999px;
	font-stretch: 75%;
	font-weight: 600;
	font-size: clamp(11px, 0.83vw, 14px);
	letter-spacing: 0.05em;
	white-space: nowrap;
	transition: color 0.2s ease;
}

/* Mobile's own instant version (no sliding pill there — see main.js,
   which only creates/positions .site-nav__pill above 900px) still needs
   this background; desktop turns it off (see the min-width:901px
   override further down, after the rest of this file's mobile block) now
   that .site-nav__pill provides the same white background there instead. */
.site-nav__item.is-active a {
	background: var(--color-white);
	color: #000;
}

.site-header__cta {
	flex: 0 0 auto;
	font-size: clamp(11px, 0.83vw, 14px);
	padding: 0.45em 1.16em;
	/* Matches .site-nav__list's own 1.5px, kept as its own override here
	   rather than changing the shared .btn--outline base rule (used only
	   by this button today, but no reason to tie a header-specific
	   thickness to every future .btn--outline). */
	border-width: 1.5px;
}

.site-header__cta.is-active {
	background: var(--color-white);
	color: #000;
}

/* .site-header__cta is desktop-only (hidden below, same breakpoint as
   the mobile drawer) — this is the mobile-drawer-only counterpart, for
   whichever nav item doesn't get its own standalone header button (see
   filmscape_fallback_menu()'s mobile_only flag). Hidden here on desktop
   since that item already has its dedicated button there instead. */
.site-nav__item--mobile-only {
	display: none;
}

@media (max-width: 900px) {
	.nav-toggle {
		display: flex;
	}

	.site-header__inner {
		display: flex;
		flex-wrap: wrap;
		justify-content: space-between;
	}

	/* Hamburger button: selected (menu open) it goes inverted — white fill,
	   black bars — like the other selected pills. No 3D lip. */
	.nav-toggle {
		border-width: 1.5px;
		/* A pill, not a circle. */
		width: 50px;
		height: 30px;
		border-radius: 999px;
		transition: background-color 0.2s ease;
	}

	.nav-toggle span {
		transition: background-color 0.2s ease;
	}

	.nav-toggle[aria-expanded="true"] {
		background: var(--color-white);
	}

	.nav-toggle[aria-expanded="true"] span {
		background: #000;
	}

	/* The menu: no drawer any more. Each page is its own pill, stacked
	   right-aligned under the hamburger and cascading in one after another
	   over whatever page is open (solid black fill so they stay legible
	   over it). The container itself never takes clicks — only the pills. */
	.site-nav {
		position: fixed;
		/* Just under the header: logo height + the header's own top and
		   bottom padding (see .site-logo img / .site-header) + a small gap. */
		top: calc(clamp(39px, 4.29vw, 68px) + 2 * clamp(4px, 0.7vw, 11px) + 12px);
		right: var(--container-pad);
		width: auto;
		justify-content: flex-end;
		background: none;
		border: none;
		padding: 0;
		pointer-events: none;
		z-index: 90;
	}

	.site-nav__list {
		flex-direction: column;
		align-items: flex-end;
		width: auto;
		border: none;
		border-radius: 0;
		gap: clamp(8px, 2.4vw, 12px);
	}

	.site-nav__item {
		opacity: 0;
		visibility: hidden;
		transform: translateY(-14px);
		/* Closing: quick, all together, and hidden only once faded. */
		transition: opacity 0.15s ease, transform 0.15s ease, visibility 0s linear 0.15s;
	}

	.site-nav.is-open .site-nav__item {
		opacity: 1;
		visibility: visible;
		transform: none;
		pointer-events: auto;
		/* Opening: cascades down, each pill a beat after the one above. */
		transition: opacity 0.25s ease, transform 0.25s ease, visibility 0s;
	}

	.site-nav.is-open .site-nav__item:nth-of-type(2) { transition-delay: 0.05s; }
	.site-nav.is-open .site-nav__item:nth-of-type(3) { transition-delay: 0.1s; }
	.site-nav.is-open .site-nav__item:nth-of-type(4) { transition-delay: 0.15s; }
	.site-nav.is-open .site-nav__item:nth-of-type(5) { transition-delay: 0.2s; }
	.site-nav.is-open .site-nav__item:nth-of-type(6) { transition-delay: 0.25s; }

	/* Each page's own pill: outlined, black fill, no 3D lip. The current
	   page is inverted (white fill, black text). */
	.site-nav__item a {
		text-align: center;
		background: #000;
		border: 1.5px solid var(--color-white);
		font-size: clamp(13px, 3.6vw, 16px);
		letter-spacing: 0.1em;
		padding: 0.3em 1em;
		transition: color 0.2s ease, background-color 0.2s ease;
	}

	.site-header__cta {
		display: none;
	}

	.site-nav__item--mobile-only {
		display: block;
	}
}

/* Placed after the mobile block above so it actually wins the cascade
   (two rules of equal specificity resolve by source order regardless of
   the media query matching) — turns off .site-nav__item.is-active a's
   own instant background now that .site-nav__pill (main.js, desktop
   only) provides the same white background as a separately animated
   element instead. */
@media (min-width: 901px) {
	.site-nav__item.is-active a {
		background: none;
	}
}

.home-page {
	position: relative;
	display: flex;
	flex-direction: column;
	justify-content: center;
	min-height: 100vh;
	min-height: 100svh;
	--home-bts-height: clamp(50px, 8vh, 140px);

	/* Where the big white LED screen is in the current hero photo
	   (DSC00854-copy), as fractions of the photo — measured from its pixels.
	   main.js (fitHeroAboveIntro) uses these to keep the "Largest LED Volume
	   in Africa" title in the middle of that screen on every screen size,
	   desktop only. If the hero photo is ever swapped for one with the screen
	   somewhere else, update these three numbers:
	     --hero-screen-x / -y: the screen's centre, across / down the photo
	     --hero-screen-w:      the clear white width, as a fraction of the photo
	   --hero-title-fill is how wide the title is relative to that white width
	   (0.928 = the size that was set at 1440x900). */
	--hero-screen-x: 0.505;
	--hero-screen-y: 0.451;
	--hero-screen-w: 0.805;
	--hero-title-fill: 0.928;
}

@media (min-width: 901px) {
	/* Matches Studio V2's .facility-bts__item exactly. */
	.home-page {
		--home-bts-height: clamp(70px, 12vh, 220px);
	}
}

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
	position: relative;
	min-height: 100vh;
	min-height: 100svh;
	display: flex;
	align-items: center;
}

/* Clips the image here rather than on .hero itself, so .hero__content
   (mobile's version escapes above .hero's own box — see its comment
   below) isn't clipped along with it. */
.hero__media {
	position: absolute;
	inset: 0;
	overflow: hidden;
}

.hero__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
	/* The old scale(1.3) here was a workaround for the previous hero
	   image's own ~1.62 aspect ratio, sitting too close to some screens'
	   own ratio to leave any vertical crop room. It also scaled the
	   image horizontally, bleeding it past the left/right edges of the
	   screen. The current image is a clean 16:9 — object-fit:cover alone
	   already crops it correctly (vertically on wide screens, ideally
	   without any horizontal bleed) with no extra scale needed. */
	object-position: center 33%;
}

.hero__gradient {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 55%;
	background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 92%);
}

@media (min-width: 601px) {
	/* Mobile's .hero__image is already its own separate height:auto/
	   transform treatment above. 10% smaller (90% height, full width
	   still) instead of filling .hero__media's box edge to edge, offset
	   down from the top by 8% of that box (top/height percentages on an
	   absolutely positioned element both resolve against the
	   container's height, so these two stay independent of each other —
	   unlike margin's, which resolves against width). */
	.hero__image {
		position: absolute;
		top: 8%;
		left: 0;
		height: 90%;
	}

	/* Desktop only: a shorter fade than the shared base rule above —
	   .hero itself is now JS-shrunk to fit the page onto one screen (see
	   main.js), so the base 55% already covered more than half of it;
	   scoped down here so it only darkens the last stretch of the image,
	   right where it meets .intro's black background below, rather than
	   creeping up into the middle of the shot. */
	.hero__gradient {
		height: 25%;
	}
}

/* Desktop only: the image moved up by that 8% (the old top:8% above), so it
   now starts flush at the top of the hero instead of 8% down. Same size as
   before; its bottom edge is softened by a short fade so it doesn't end in
   a visible line above where the page fades to black. */
@media (min-width: 901px) {
	.hero__image {
		top: 0;
		-webkit-mask-image: linear-gradient(to bottom, #000 0, #000 88%, transparent 100%);
		mask-image: linear-gradient(to bottom, #000 0, #000 88%, transparent 100%);
	}
}

.hero__content {
	position: relative;
	z-index: 1;
	width: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	padding: 0 var(--container-pad);
	text-align: center;
}

.hero__title {
	margin: 0;
	font-size: clamp(2.2rem, 5.6vw, 6rem);
	line-height: 1.08;
	letter-spacing: 0.02em;
	text-transform: uppercase;
}

.hero__title span {
	display: block;
}

/* Home's title only — a single flowing line (wrapping naturally, not
   forced into stacked lines like Contact/Projects' own .hero__title
   usage), alternating thin/ultra-bold Open Sans weights with a small
   blue full stop, matching the updated design. Scoped via
   .hero__content (Home's own wrapper — Contact/Projects use their own
   differently-named wrappers) so those pages' stacked-line titles are
   untouched. */
.hero__content .hero__title span {
	display: inline;
}

.hero__title--home {
	/* Scales with the screen width all the way up (was capped at 4.86rem, ~1714px
	   wide, so it stopped growing and looked small on 4K). Identical to before
	   at 1440x900 and everywhere below the old cap. */
	font-size: clamp(1.944rem, 4.536vw, 14rem);
	color: #000;
}

/* 4K and up: keeps growing past 1920px wide, but slower than the 4.536vw
   above (which read slightly too big at 3840 — 174px). Picks up exactly where
   that leaves off at 1920 (87.1px) and reaches ~143px at 3840, i.e. ~18%
   smaller there. Nothing at 1920px and below changes. */
@media (min-width: 1921px) {
	.hero__title--home {
		font-size: min(14rem, calc(87.1px + (100vw - 1920px) * 0.0291));
		white-space: nowrap;
	}

	/* The title's box (.hero__content) stops growing at --container-max
	   (1680px) while this font keeps growing, so the line no longer fit and
	   wrapped before "AFRICA". Let the box follow the screen here instead
	   (it only holds the centered title), so it always stays on one line. */
	.hero__content {
		max-width: none;
	}
}

@media (max-width: 600px) {
	.home-page {
		/* No longer centering .hero via justify-content — that split the
		   leftover space evenly above/below it, pushing the image further
		   from the title than needed. A direct margin-top instead, just
		   enough to clear the fixed title (top:clamp(64px,16vw,96px)
		   above), puts the image directly under it. Shared via a custom
		   property so .hero__content (below) can reuse the same value. */
		justify-content: flex-start;
		--hero-margin-top: clamp(150px, 55vw, 200px);
	}

	.hero {
		/* Overriding the base 100vh/100svh floor down to 0 — combined with
		   .hero__media below now sizing naturally from the image
		   (height:auto, its own 16:9 ratio) instead of being an inset:0
		   overlay with no intrinsic size, .hero's own box ends up hugging
		   the image tightly with no leftover space inside it. The old
		   38vh floor left a gap of empty space between the image's own
		   bottom edge and .hero's box edge (align-items:center split it
		   above/below the image), which .intro's margin-top below wasn't
		   accounting for — it was measuring from .hero's box, not the
		   image actually visible inside it. */
		min-height: 0;
		margin-top: var(--hero-margin-top);
	}

	/* Normal flow (not the inset:0 overlay .hero__media is elsewhere) so
	   its content — the image — determines its own height, and in turn
	   .hero's height above. .hero__gradient's own inset:0 below still
	   anchors correctly to this box either way. */
	.hero__media {
		position: relative;
		inset: auto;
	}

	/* Width-matched to the screen instead of object-fit:cover cropping
	   to fill the container in both dimensions — height:auto lets the
	   image's own 16:9 ratio govern, so its full width is always
	   visible (object-position's Y offset above becomes moot, since
	   there's no vertical crop left to redistribute). */
	.hero__image {
		height: auto;
		/* Slightly bigger + shifted down — scale grows/shrinks around the
		   image's own center (transform-origin defaults to 50% 50%), so
		   translateY is what actually moves it down; .hero's own
		   overflow:hidden clips whatever this pushes past its edges. */
		transform: scale(1.03) translateY(2%);
	}

	/* Not sticky/fixed to the viewport (so it scrolls away with the page
	   like everything else) but lands in the exact same visual spot as
	   the old position:fixed version did: absolute is relative to .hero
	   itself (its nearest positioned ancestor), whose own top already
	   sits --hero-margin-top down the screen, so subtracting that back
	   out of the old viewport-relative top value re-derives the same
	   on-screen position. */
	.hero__content {
		position: absolute;
		top: calc(clamp(64px, 16vw, 96px) - var(--hero-margin-top));
		left: 0;
		right: 0;
		text-align: left;
	}

	.hero__title--home {
		font-size: clamp(2.4rem, 10vw, 3.75rem);
		/* Sits over plain black background (the image is centered further
		   down the screen, not behind it), not the bright hero image —
		   black text there is illegible, so white here only, overriding
		   the shared black used on desktop (where the title still sits
		   over the image). */
		color: var(--color-white);
	}
}

/* ==========================================================================
   Intro
   ========================================================================== */

/* Normal document flow directly below .hero (not overlaid on the image) —
   two sections: the full-screen hero, then this + .home-bts reachable by
   scrolling. z-index (a flex item of .home-page, so this doesn't need its
   own "position" for it to apply) keeps it above .home-bts below, which
   overlaps up into it by design — see .home-bts's own comment. */
.intro {
	z-index: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	width: 100%;
	text-align: center;
	max-width: 1180px;
	margin-inline: auto;
	padding: 0 var(--container-pad);
}

.intro__text {
	padding: 0 var(--container-pad);
	text-align: justify;
	font-size: clamp(15px, 1.35vw, 24px);
	line-height: 1.6;
	letter-spacing: 0.02em;
	margin: 0 0 clamp(24px, 3vw, 36px);
}

@media (min-width: 601px) {
	/* Sized so .intro__text ends up exactly centered between the bottom
	   of the hero image and the top of .home-bts, not just "below the
	   hero with some gap". The image sits inset 5% from .hero's own
	   bottom (see .hero__image above), so that's a flat 5vh gap already
	   there before .intro even starts; the gap on the OTHER side of the
	   text (to .home-bts's line) is .intro__text's own margin-bottom
	   plus half the button's height (the same half-height .home-bts's
	   negative margin-top below uses to center on it). Equalizing the
	   two gaps means solving padding-top + 5vh = margin-bottom +
	   half-button-height for padding-top — i.e. this formula. Floored at
	   0 for extreme tall/narrow aspect ratios where 5vh alone would
	   already exceed the other side. .intro has no bottom padding of its
	   own (the button is its last child, flush against its own bottom
	   edge) so .home-bts's negative margin-top lands exactly on the
	   button's center, not some extra padding beneath it. */
	.intro {
		padding-top: max(0px, calc(clamp(24px, 3vw, 36px) + clamp(22px, 1.76vw, 32px) - 5vh));
	}

	.intro__text {
		/* Tied to viewport HEIGHT rather than width — the hero above this
		   is JS-fit to exactly one screen height (see fitHeroAboveIntro in
		   main.js), so sizing this off the same dimension keeps it
		   proportionate to "how much screen is visible" instead of how
		   wide the window happens to be. 5% down from the old
		   clamp(13px, 1vw, 18px) at a typical ~900px-tall desktop window. */
		font-size: clamp(12.35px, 1.6vh, 17.1px);
	}
}

.intro__text strong {
	font-weight: 700;
	letter-spacing: 0.03em;
	color: var(--color-blue);
}

.intro__text p {
	margin: 0 0 1em;
}

.intro__text p:last-child {
	margin-bottom: 0;
}

/* Home's Contact Us button, 15% smaller than the shared .btn size
   (clamp(14px, 1.1vw, 20px)). Its padding is in em, so the whole button
   shrinks with the text. Scoped to .intro so the Contact form's SEND button
   and other .btn--primary buttons keep their size. */
.intro .btn--primary {
	font-size: clamp(11.9px, 0.935vw, 17px);
}

@media (max-width: 600px) {
	/* More than the shared var(--container-pad) alone — placed after the
	   base .intro__text rule above (not inside the earlier Hero-section
	   mobile media query) so it actually wins the cascade; two rules of
	   equal specificity resolve by source order regardless of the media
	   query matching. */
	.intro__text {
		padding: 0 clamp(28px, 8vw, 48px);
		/* Same fixed gap as .intro's own margin-top below, between text
		   and the button that follows it — a pixel value rather than a
		   fluid one so the two stay identical at every width, not just
		   proportionally similar. */
		margin-bottom: 20px;
	}

	/* Gap under the background image — desktop uses padding-top instead
	   (see the min-width:601px block above) since it needs a bigger,
	   fluid value; this one's deliberately a fixed pixel amount. */
	.intro {
		margin-top: 20px;
	}
}

/* ==========================================================================
   Home BTS bar

   Same idea as Studio V2's .facility-bts (a compiled strip of square BTS
   photos, edge-to-edge) — own classnames since this is a different page,
   but the same desktop tile size as that page's bar (--home-bts-height,
   defined on .home-page above). Normal flow, right after .intro — its top
   line still needs to land on the Contact Us button's vertical center
   though, so instead of a gap this pulls the bar up by half the button's
   own height (see the breakpoint-specific values below). */
.home-bts {
	display: flex;
	width: 100%;
	height: var(--home-bts-height);
	overflow: hidden;
	/* Photo strip disabled for now (see front-page.php) — plain black
	   fill instead, sized identically so nothing around it has to move. */
	background: var(--color-bg);
}

@media (min-width: 601px) {
	/* Half of .btn's own font-size clamp (14px/1.1vw/20px), scaled by the
	   ratio between its rendered height and font-size (measured ~3.2x),
	   since a fluid margin can't reference a sibling's box directly. */
	.home-bts {
		margin-top: calc(-1 * clamp(22px, 1.76vw, 32px));
	}
}

@media (max-width: 600px) {
	/* Mobile's button stays at its floor font-size across this entire
	   width range, so its rendered height (and half of it, here) is a
	   flat constant rather than needing the fluid clamp above. */
	.home-bts {
		margin-top: -22px;
	}
}

.home-bts__item {
	flex: 0 0 auto;
	height: 100%;
	width: auto;
	aspect-ratio: 1 / 1;
	object-fit: cover;
	display: block;
}

/* ==========================================================================
   Projects
   ========================================================================== */

.projects {
	position: relative;
	overflow: hidden;
	/* Matches the hero-title-to-intro gap: hero's bottom padding + intro's
	   top padding, minus what intro's own bottom padding already provides. */
	padding-top: calc(clamp(28px, 4vw, 60px) + clamp(40px, 6vw, 90px) - clamp(30px, 4vw, 50px));
	/* Same target gap, minus what the footer's own top padding already provides. */
	padding-bottom: calc(clamp(28px, 4vw, 60px) + clamp(40px, 6vw, 90px) - clamp(30px, 4.5vw, 54px));
}

.projects__bg {
	position: absolute;
	inset: 0;
	z-index: 0;
	background:
		radial-gradient(circle clamp(180px, 45vmin, 520px) at 100% 20%, rgba(44, 83, 208, 0.55), transparent 68%),
		radial-gradient(circle clamp(180px, 45vmin, 520px) at 0% 55%, rgba(44, 83, 208, 0.5), transparent 68%);
	pointer-events: none;
}

.projects__inner {
	position: relative;
	z-index: 1;
	max-width: var(--container-max);
	margin-inline: auto;
	padding: 0 var(--container-pad);
	text-align: center;
}

.projects__title {
	font-size: clamp(1.8rem, 3.7vw, 4rem);
	font-weight: 600;
	font-stretch: 75%;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	margin: 0 0 clamp(28px, 4vw, 48px);
}

.projects__grid {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: clamp(14px, 1.6vw, 26px);
	margin-bottom: clamp(30px, 4vw, 48px);
}

@media (min-width: 901px) {
	/* Projects page only (.projects-hero only precedes .projects there —
	   Home's own teaser grid, which reuses these same .projects__* card/
	   grid classes, sits after .intro instead, so this doesn't touch it).
	   10% smaller cards: width:90% shrinks the grid itself, and since
	   each card's height comes from its own width via aspect-ratio, that
	   scales every card down proportionally too. row-gap matches
	   assets/js/main.js's title-to-first-row gap (mirrored there by
	   hand — see that file's comment on why), for a consistent rhythm:
	   title→row1, row1→row2, and row1→bottom of the initial viewport
	   (set in that same script) all land on the same spacing. */
	.projects-hero + .projects .projects__grid {
		width: 90%;
		margin-inline: auto;
		row-gap: clamp(16px, 2vw, 28px);
	}
}

.projects__card {
	aspect-ratio: 282 / 418;
	border-radius: 28px;
	overflow: hidden;
	background: #111;
}

.projects__card img {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

@media (max-width: 760px) {
	.projects__grid {
		grid-template-columns: repeat(2, 1fr);
	}
}

@media (max-width: 600px) {
	.projects__grid {
		max-width: 75%;
		margin-inline: auto;
	}
}

/* ==========================================================================
   Projects page (photo banner + full poster grid)
   ========================================================================== */

.projects-hero {
	position: relative;
	overflow: hidden;
	/* Explicit, not just relying on the block default of filling its
	   container — combined with aspect-ratio below, a min-height taller
	   than "container width ÷ ratio" (true on mobile: 320px min-height
	   vs. the ~234px the ratio would derive from a 375px-wide screen)
	   otherwise makes the browser grow the WIDTH to maintain the ratio
	   against that taller height instead, well past the container's own
	   width — this was overflowing the entire page horizontally on
	   mobile (~512px wide at 375px viewports), not just clipping within
	   .projects-hero's own overflow:hidden. */
	width: 100%;
	aspect-ratio: 1726 / 1078;
	min-height: clamp(320px, 45vw, 620px);
	display: flex;
	align-items: flex-end;
}

@media (min-width: 901px) {
	/* Sizing this off the source image's own aspect ratio (1726:1078,
	   ~1.6:1) is why the flex-end title only ever looked right on
	   ~16:10 screens (MacBooks) — that ratio happens to be close to
	   1.6:1, so the aspect-ratio-derived height lines up with the real
	   viewport height almost by coincidence. On a 16:9 monitor the same
	   ratio makes this section taller than the actual viewport, pushing
	   the title down past where the visible screen ends. Sizing to the
	   real viewport height instead ties the title to the actual bottom
	   of the screen on every desktop aspect ratio, not just this image's
	   own. object-fit:cover on the image (below) still fills whatever
	   this resolves to, so the photo itself is unaffected — the hero's
	   own size/crop is untouched by the flex-start + pulled-up-grid
	   changes below, only where the title sits within it changes. */
	.projects-hero {
		aspect-ratio: unset;
		height: 100svh;
		/* This is just the pre-JS fallback position (title near the top).
		   assets/js/main.js measures the actual rendered title height and
		   first poster-row height on load/resize and repositions both
		   precisely (title's own transform, .projects' margin-top) so the
		   first row always ends up fully on-screen with the title sitting
		   right above it — those two are sized off responsive clamp()s
		   (card aspect-ratio, title font-size) whose combined height
		   can't be solved for in plain CSS across every viewport. Kept
		   flex-start (not flex-end) so that without JS this still reads
		   sensibly rather than the title landing at the very bottom. */
		align-items: flex-start;
	}
}

.projects-hero__media {
	position: absolute;
	inset: 0;
}

.projects-hero__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.projects-hero__gradient-top {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 34%;
	background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));
}

.projects-hero__gradient-bottom {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 74%;
	background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 92%);
}

.projects-hero__content {
	position: relative;
	z-index: 1;
	width: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	padding: 0 var(--container-pad) clamp(28px, 4vw, 60px);
}

@media (min-width: 901px) {
	/* Top padding (clearing the fixed header), not bottom — mirrors the
	   header-clearance value used on Plates/Studio/Contact. Placed here,
	   after the base (unscoped) rule above, on purpose — equal
	   specificity means source order decides, and a scoped override
	   placed BEFORE an unscoped rule targeting the same selector loses to
	   it regardless of the media query (hit this exact bug earlier on
	   this same page, with .projects-hero's own height/aspect-ratio). */
	.projects-hero__content {
		padding: clamp(90px, 9vw, 130px) var(--container-pad) 0;
	}

	/* Lines the title's left edge up with the first poster's — the grid
	   (.projects-hero + .projects .projects__grid, see below) is
	   width:90% + margin-inline:auto *inside* .projects__inner, which has
	   the same max-width/padding/margin-auto as .projects-hero__content
	   here. Giving the title that identical width:90%+auto-margin
	   treatment, nested the same way inside the same outer box, lands its
	   left edge on exactly the same pixel as the grid's, at any viewport
	   width, rather than a guessed offset. */
	.projects-hero__content .hero__title {
		width: 90%;
		margin-inline: auto;
	}
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-footer {
	border-top: 1px solid var(--color-white);
	/* 35% down from clamp(20px, 3vw, 36px) / clamp(16px, 2vw, 24px). */
	padding: clamp(13px, 1.95vw, 23.4px) var(--container-pad) clamp(10.4px, 1.3vw, 15.6px);
}

.site-footer__inner {
	max-width: var(--container-max);
	margin-inline: auto;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	/* This gap alone controls the space between the brand/address/social
	   row, the rule, and the copyright line below (flex gap applies
	   between wrapped lines, not just side-by-side items) — .site-footer
	   __rule used to ALSO carry its own top/bottom margin on top of this,
	   which stacked the two together into a much bigger gap than either
	   alone. */
	/* 10% down from clamp(12px, 1.6vw, 24px), then another 15%, 25%, 15%, and 35%. */
	gap: clamp(3.8px, 0.507vw, 7.61px);
}

.site-footer__brand {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	/* 35% down from clamp(4px, 0.55vw, 7px). */
	gap: clamp(2.6px, 0.3575vw, 4.55px);
}

/* All footer items below scaled down ~10% from their previous sizes,
   independently of the section-to-section spacing above (which stays
   as its own fixed clamp, not a proportion of these). */
.site-footer__logo-icon {
	/* 35% down from clamp(58px, 6.3vw, 106px), then 10% back up. */
	height: clamp(41.47px, 4.5045vw, 75.79px);
	width: auto;
}

.site-footer__logo-type {
	/* 35% down from clamp(23.8px, 2.48vw, 38.9px), then 10% back up. */
	height: clamp(17.02px, 1.773vw, 27.82px);
	width: auto;
}

.site-footer__address {
	font-style: normal;
	text-align: left;
	font-stretch: 75%;
	/* 35% down from clamp(13px, 1.2vw, 22px). */
	font-size: clamp(8.45px, 0.78vw, 14.3px);
	letter-spacing: 0.14em;
	line-height: 1.75;
}

.site-footer__social {
	/* Row, not column — now two icons side by side (YouTube left of
	   Instagram) rather than one icon under a "Follow us" label. */
	display: flex;
	flex-direction: row;
	align-items: center;
	/* 35% down from clamp(9px, 1.1vw, 14px). */
	gap: clamp(5.85px, 0.715vw, 9.1px);
	font-stretch: 75%;
	/* 35% down from clamp(13px, 1.2vw, 22px). */
	font-size: clamp(8.45px, 0.78vw, 14.3px);
	letter-spacing: 0.14em;
}

.site-footer__social a {
	display: inline-flex;
	align-items: center;
	justify-content: center;
}

.site-footer__social svg {
	/* 20% smaller than the icon's previous 22px base size, then another
	   35% down. */
	width: 11.44px;
	height: 11.44px;
}

/* The YouTube and Instagram artwork use the same 24x24 viewBox, but the
   visible glyph fills a different proportion of it (YouTube's rect is
   14/24 tall, Instagram's is 20/24 tall) — so equal box sizes above
   render as visibly different heights. Scaling Instagram's box down by
   14/20 makes the two RENDERED glyphs the same height. Selector needs
   the extra ".site-footer__social svg" specificity to actually beat
   the rule above instead of losing to it. */
.site-footer__social svg.site-footer__social-icon--instagram {
	width: 8.01px;
	height: 8.01px;
}

.site-footer__rule {
	/* Without an explicit width, this empty div has nothing to give it
	   size — .site-footer__inner's flex row (space-between) then treats
	   it as just another zero-width item alongside brand/address/social
	   instead of a full-width line wrapping onto its own row, which was
	   also squeezing .site-footer__social out of its intended
	   flush-right position and leaving .site-footer__copyright pinned to
	   the row's right edge instead of centered below the rule. */
	width: 100%;
	border-top: 1px solid rgba(255, 255, 255, 0.5);
	/* No margin here — .site-footer__inner's own gap (above) already
	   provides the space above and below this on its own; adding margin
	   here too just stacked on top of that gap. */
	margin: 0;
}

.site-footer__copyright {
	/* Same reasoning as .site-footer__rule above — needs its own full
	   width for text-align:center to center it across the whole footer
	   rather than within its own shrink-wrapped, space-between-positioned
	   box. */
	width: 100%;
	text-align: center;
	font-stretch: 75%;
	font-weight: 600;
	/* ~10% down from the Figma-matched 20px ceiling, same as the rest of
	   the footer's items, then another 35% down. */
	font-size: clamp(7.15px, 0.65vw, 11.7px);
	margin: 0;
}

/* Mobile keeps the exact same structure as desktop (brand/address/social
   in a row, divider, centered copyright) — .site-footer__inner's
   flex-wrap (from the base rule, untouched here) already puts
   .site-footer__rule/.site-footer__copyright on their own line below
   since they're width:100%. The only thing standing between mobile and
   that same single-row brand/address/social layout was sheer size — so
   this just shrinks brand/address/social's own content enough for all
   three to actually fit side by side on a phone width, same as the
   compact numbers this footer used before it was resized to match
   desktop. */
@media (max-width: 600px) {
	.site-footer {
		padding: clamp(12px, 4vw, 20px) var(--container-pad) clamp(10px, 3vw, 16px);
	}

	.site-footer__inner {
		gap: clamp(6px, 2vw, 12px);
	}

	.site-footer__brand {
		align-items: center;
		gap: 2px;
	}

	.site-footer__logo-icon {
		height: clamp(26px, 8vw, 36px);
	}

	.site-footer__logo-type {
		height: clamp(14px, 4.86vw, 19.4px);
	}

	.site-footer__address {
		font-size: clamp(8px, 2.3vw, 10px);
		letter-spacing: 0.08em;
		line-height: 1.4;
	}

	.site-footer__social {
		gap: 6px;
	}

	.site-footer__social svg {
		/* 50% up from the row-fitting 14.4px, then another 25% up. */
		width: 27px;
		height: 27px;
	}

	.site-footer__social svg.site-footer__social-icon--instagram {
		/* Same two bumps applied to 10.08px, still 0.7x the YouTube icon above. */
		width: 18.9px;
		height: 18.9px;
	}

	.site-footer__rule {
		margin: 0;
	}
}

/* ==========================================================================
   Studio page
   ========================================================================== */

.studio-page {
	position: relative;
	overflow: hidden;
	/* Vertically centers the slider within the full viewport height instead
	   of a fixed padding amount — on a small screen there's little slack so
	   this barely moves anything, but on a very tall/4K viewport the extra
	   space distributes evenly above and below automatically instead of
	   leaving the whole block stranded at the top. */
	min-height: 100vh;
	min-height: 100svh;
	display: flex;
	flex-direction: column;
	justify-content: center;
	padding: clamp(90px, 9vw, 130px) 0 clamp(30px, 4vw, 60px);
}

.studio-page__bg {
	position: absolute;
	inset: 0;
	z-index: 0;
	background:
		radial-gradient(circle var(--contact-glow-size) at 100% 15%, rgba(44, 83, 208, 0.5), transparent 68%),
		radial-gradient(circle var(--contact-glow-size) at 0% 90%, rgba(44, 83, 208, 0.45), transparent 68%);
	pointer-events: none;
}

.studio-slider {
	position: relative;
	z-index: 1;
	/* .studio-page is a flex container (for vertical centering), which
	   makes this a flex item — flex items shrink-to-fit their content by
	   default instead of filling available width like a normal block
	   would, so max-width alone isn't enough to reach it; width:100%
	   forces it to actually stretch out to that cap. */
	width: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	padding-inline: var(--container-pad);
}

.studio-slide {
	position: relative;
	display: none;
}

.studio-slide.is-active {
	display: block;
}

.studio-slide__info {
	position: absolute;
	left: clamp(24px, 4vw, 64px);
	top: 50%;
	transform: translateY(-50%);
	z-index: 2;
	width: clamp(220px, 26vw, 380px);
}

.studio-slide__title {
	font-stretch: 75%;
	/* 800 is the heaviest weight the loaded Open Sans variable range
	   (300..800) supports. */
	font-weight: 800;
	font-size: clamp(1.6rem, 2.6vw, 2.25rem);
	letter-spacing: 0.35em;
	text-transform: uppercase;
	margin: 0 0 clamp(20px, 3vw, 34px);
	color: var(--color-white);
}

.studio-slide__list {
	list-style: none;
	counter-reset: studio-item;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: clamp(12px, 2vw, 20px);
}

.studio-slide__list li {
	counter-increment: studio-item;
	/* 5% smaller than the original clamp(12px,1vw,16px) — titles are
	   unscaled, only body/list text. */
	font-size: clamp(11.4px, 0.95vw, 15.2px);
	letter-spacing: 0.3em;
	text-transform: uppercase;
	/* 300 is the lightest weight the loaded Open Sans variable range
	   (300..800) supports — contrasts against the bold ::before number. */
	font-weight: 300;
	transition: font-weight 0.2s ease;
}

.studio-slide__list li::before {
	content: counter(studio-item) ".";
	display: inline-block;
	width: 1.6em;
	letter-spacing: normal;
	/* 800 is the heaviest weight the loaded Open Sans variable range
	   (300..800) supports. */
	font-weight: 800;
}

.studio-slide__media {
	position: relative;
	z-index: 1;
	/* These images are alpha-transparent renders weighted to the right
	   half — the left half is dead space. The info block sits absolutely
	   positioned on top of that dead space (above) instead of sharing a
	   flex row with it. Sized by height (not width): a width-based image
	   can grow taller than the viewport depending on its aspect ratio,
	   which is what pushed the pagination below the fold. Height-based
	   sizing guarantees the whole slide fits, and align/justify keeps it
	   pushed to the right like the width-based version was.
	   display:grid (not flex) so the base image and an optional hotspot
	   overlay image (same selector below, see .studio-slide__hotspot)
	   stack in the same cell instead of flowing side by side — with
	   identical sizing rules applied to both via the shared "img"
	   selector, they land perfectly aligned with no extra positioning. */
	display: grid;
	align-items: center;
	justify-items: end;
}

.studio-slide__media img {
	width: auto;
	max-width: 100%;
	height: clamp(300px, 68vh, 620px);
	display: block;
	grid-area: 1 / 1;
}

/* Hover highlight — proof of concept for Facility's "The Volume" only.
   This is a real render from the same camera/composition as the base
   diagram (transparent except the highlighted shape), stacked directly
   on top via the shared "img" sizing rule + grid-area above — so it
   inherits the exact same size/position/crop as the base image at every
   breakpoint automatically. Only opacity is toggled here. */
.studio-slide__hotspot {
	opacity: 0;
	transition: opacity 0.25s ease;
	pointer-events: none;
}

.studio-slide__hotspot.is-active {
	opacity: 0.75;
}

.studio-slide__list li[data-hotspot] {
	cursor: pointer;
}

.studio-slide__list li[data-hotspot]:hover {
	font-weight: 800;
}

.studio-pagination {
	display: flex;
	justify-content: center;
	gap: 6px;
	margin-top: clamp(20px, 3vw, 36px);
}

.studio-pagination__dot {
	width: 46px;
	height: 17px;
	padding: 0;
	border: 1px solid var(--color-white);
	border-radius: 999px;
	background: transparent;
	cursor: pointer;
}

.studio-pagination__dot.is-active {
	background: #d9d9d9;
	border-color: #d9d9d9;
}

/* Large/4K displays only — 1440x900 and everything below is untouched. */
@media (min-width: 1921px) {
	.studio-slide__info {
		/* The slider's own inline padding (which the header's row doesn't
		   have) is the one remaining offset once it correctly stretches
		   to container-max — cancel it to land flush with the FILMSCAPE
		   logo's left edge. */
		left: calc(var(--container-pad) * -1);
		/* ~10% wider than the base clamp, to match the bigger text. */
		width: clamp(240px, 28.6vw, 420px);
	}

	.studio-slide__title {
		/* ~10% bigger than the base clamp (letter-spacing is in em, so it
		   scales along with this automatically). */
		font-size: clamp(1.76rem, 2.86vw, 2.5rem);
		margin-bottom: clamp(22px, 3.3vw, 38px);
	}

	.studio-slide__list {
		gap: clamp(13px, 2.2vw, 22px);
	}

	.studio-slide__list li {
		font-size: clamp(12.35px, 1.045vw, 17.1px);
	}

	.studio-slide__media {
		/* Same cancellation on the right, flush with the CONTACT US
		   button's right edge. */
		margin-right: calc(var(--container-pad) * -1);
	}

	.studio-slide__media img {
		/* ~37.5% bigger than the base clamp overall (the previous +25%,
		   plus another +10% on top). */
		height: clamp(440px, 93.5vh, 853px);
	}

	.studio-pagination {
		/* The slider block is vertically centered as a whole, so growing
		   this gap pushes the image/text up and the dots down together,
		   instead of shifting anything via top/margin hacks. */
		margin-top: clamp(100px, 10vh, 200px);
	}
}

@media (max-width: 900px) {
	/* The dead-space overlay trick only works when there's a wide image
	   with room to spare. On phone/tablet widths the image is much
	   shorter, so go back to a normal stacked flow instead of overlaying
	   text on it. */
	.studio-slide.is-active {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		text-align: center;
		gap: clamp(10px, 2vw, 18px);
	}

	/* Ground Floor & First Floor — much tighter gap between the list and
	   the image than the shared value above. */
	.studio-slide[data-slide="3"].is-active,
	.studio-slide[data-slide="4"].is-active {
		gap: 4px;
	}

	.studio-slide__info {
		position: static;
		/* translateY doesn't affect layout, so this shifts the text up
		   without disturbing the gap/image below it (or, transitively,
		   the pagination dots after it). */
		transform: translateY(-60px);
		width: 100%;
	}

	/* ~50% bigger than the mobile-clamped base sizes (title clamps to
	   1.6rem/25.6px, list items to 12px at this width), keeping the same
	   ratio between them. Only text size changes here — image and
	   pagination sizing/position rules are untouched. */
	.studio-slide__title {
		font-size: 2.4rem;
	}

	.studio-slide__list li {
		font-size: 17.1px;
	}

	/* Fixed height (instead of following each scaled image's own natural
	   height) so the pagination dots land in the same spot on every slide
	   — each image is vertically centered and cropped to fit instead of
	   pushing the dots up/down as you page through. */
	.studio-slide__media {
		display: grid;
		align-items: center;
		justify-items: start;
		width: 100%;
		height: clamp(280px, 100vw, 420px);
		margin-inline: 0;
		overflow: hidden;
		/* Centers the image (as a block) in the gap between the text above
		   it (which only moved visually, via its own transform — see
		   .studio-slide__info — so this row's actual flow position, and
		   the pagination after it, are both untouched by that) and the
		   pagination dots below. Measured directly against each slide's
		   own rendered text-bottom/pagination-top/image-height, since
		   those vary per slide (Facility/Studio 5/Volume share one value,
		   see below for Ground Floor/First Floor's, which differ because
		   their shortened media row is a different height). */
		transform: translateY(-25px);
	}

	.studio-slide[data-slide="3"] .studio-slide__media,
	.studio-slide[data-slide="4"] .studio-slide__media {
		transform: translateY(-22px);
	}


	/* The height-based sizing that keeps desktop slides on-screen produces
	   a very wide (overflowing) image on a narrow, tall phone viewport —
	   go back to width-based so it just fills the column instead. */
	.studio-slide__media img {
		width: 100%;
		max-width: none;
		height: auto;
	}

	/* Each diagram is an alpha-transparent render weighted to the right —
	   the left portion (varying per image) is dead space. width:100% above
	   just stretches that dead space along with the artwork, leaving the
	   actual visible diagram smaller than the screen. Scale each image up
	   and shift it left so its real (measured, per-image) visible bounding
	   box lands exactly edge-to-edge instead. */
	.studio-slide[data-slide="0"] .studio-slide__media img {
		width: 163%;
		transform: translateX(-35.1%);
	}

	.studio-slide[data-slide="1"] .studio-slide__media img {
		/* Scaled down from the exact-fill 310% — at full width this one
		   was tall enough to crop off the top and bottom of the diagram
		   against the fixed-height row below. At this smaller size it no
		   longer reaches both edges, so the shift is recalculated to
		   center the visible artwork's midpoint in the row instead of
		   left-aligning it like the other (edge-to-edge) slides. */
		width: 200%;
		transform: translateX(-42.35%);
	}

	.studio-slide[data-slide="2"] .studio-slide__media img {
		width: 156%;
		transform: translateX(-33.1%);
	}

	.studio-slide[data-slide="3"] .studio-slide__media img {
		/* Scaled down further (~78% of the earlier -10% size) so the full
		   diagram — not just its dead space — fits inside a shortened row
		   (see .studio-slide__media height override below) without any
		   of it being clipped by overflow:hidden. Re-measured (alpha-
		   channel bounding box) and recentered at this size.

		   The Y half of this transform is doing what align-items:center
		   above normally would — it doesn't, here: with an implicit grid
		   row and an item taller than the container's own explicit
		   height, the row auto-sizes to the ITEM's height rather than
		   the container's, so align-items has no extra space to center
		   within and the image renders flush against the top instead,
		   silently overflowing (and getting clipped) at the bottom.
		   -45.2px is half of that measured overflow (image height minus
		   container height), shifting it up to sit centered instead. */
		width: 178.19%;
		transform: translate(-41.32%, -38.4px);
	}

	.studio-slide[data-slide="4"] .studio-slide__media img {
		/* Same ~78% further scale-down as slide 3, recentered at this
		   size (-57.7px is that slide's own measured half-overflow — see
		   slide 3's comment above for why this is needed at all). */
		width: 191.42%;
		transform: translate(-42.87%, -44.5px);
	}

	/* Ground Floor & First Floor: shortening this row pulls the
	   pagination dots up to land exactly where Facility/Studio 5's do
	   (135.625px[extra-list-height] − 6px[tightened list gap] measured
	   empirically against the page's actual rendered pagination
	   position — .studio-page vertically centers .studio-slider, so a
	   given px of shortening here only moves the dots up by roughly half
	   as much). Both images above are sized specifically to still fit
	   fully inside this shorter height with no cropping — this height
	   and those widths were solved together, so if either changes the
	   other likely needs re-measuring too. */
	.studio-slide[data-slide="3"] .studio-slide__media,
	.studio-slide[data-slide="4"] .studio-slide__media {
		height: calc(clamp(280px, 100vw, 420px) - 129.625px);
	}

	.studio-slide__list {
		/* Stretch (not flex-start) — each li spans the full row width so
		   its own text-align:center (inherited from .studio-slide.is-
		   active) has room to actually center the line, instead of the
		   li's box just shrinking to fit its own content. */
		align-items: stretch;
	}

	.studio-slide__list li::before {
		width: auto;
		margin-right: 0.4em;
	}

	/* .plates-page centers its slider vertically (justify-content:center)
	   for desktop, where the row is short. On mobile it now follows the
	   same shape as Home's mobile layout instead: title, then the image
	   right below it, then the body text — normal document flow, not
	   locked to one screen, so the page scrolls down to the pagination
	   dots if a slide's content runs long. Each slide's own image sizing/
	   rotation (Plates Capture & Overview still rotate 90°) is untouched
	   below — only the stacking order and overall page flow change. */
	.plates-page {
		justify-content: flex-start;
	}

	.plates-slider {
		display: flex;
		flex-direction: column;
		/* Guaranteed minimum gap before the pagination/BTS bar, on top of
		   whatever .studio-pagination's own margin-top:auto adds when
		   there's leftover screen space — without this, a slide tall
		   enough to already fill the screen (collapsing that auto margin
		   toward 0) could end up with barely any breathing room here. */
		margin-bottom: 36px;
	}

	.plates-slide.is-active {
		display: flex;
		flex-direction: column;
		gap: clamp(16px, 4vw, 28px);
	}

	/* .plates-slide__info is display:contents so .plates-slide__title and
	   .plates-slide__body (its two children) become direct flex items of
	   .plates-slide here, letting each get its own "order" — same
	   technique as Studio V2's mobile layout. text-align still applies to
	   them via inheritance even though .plates-slide__info itself no
	   longer generates a box. */
	.plates-slide__info {
		display: contents;
		text-align: left;
	}

	.plates-slide__title {
		order: 1;
		width: 100%;
		/* Matches Home's mobile title size. */
		font-size: clamp(2.4rem, 10vw, 3.75rem);
	}

	.plates-slide__media {
		order: 2;
		width: 100%;
		justify-content: center;
		/* A concrete height (rather than growing/shrinking to fill
		   whatever screen space happened to be left, as it did when this
		   page was locked to one viewport) — .plates-slide__media's own
		   container-type:size below needs a definite height regardless,
		   for Capture/Overview's rotation math to have something to
		   measure. */
		height: clamp(280px, 50vh, 500px);
		overflow: hidden;
		/* Same reasoning as Studio's own .facility-content touch-action —
		   without this, a real phone can grab an in-progress swipe (see
		   main.js) for its own native scroll the moment it sees any
		   vertical drift, cancelling the gesture before main.js ever gets
		   a pointerup to act on. pan-y still leaves vertical scrolling
		   itself untouched. */
		touch-action: pan-y;
	}

	.plates-slide__body {
		order: 3;
		/* Same left/right padding as Home's mobile intro text. */
		padding: 0 clamp(28px, 8vw, 48px);
	}

	.plates-slide__media img {
		/* Shrink-to-fit both axes of the now height-constrained row,
		   preserving aspect ratio. */
		width: auto;
		height: auto;
		max-width: 100%;
		max-height: 100%;
	}

	/* Height Options (slide 1, the one slide that isn't rotated) — 200%
	   of its normal fit-to-box size, anchored to the right edge of the
	   box (flex-end) so it grows toward the left instead of staying
	   centered. overflow:visible (like Overview below) since at 200% it
	   deliberately spills past the box on every other edge too. */
	.plates-slide[data-slide="1"] .plates-slide__media {
		justify-content: flex-end;
		overflow: visible;
	}

	.plates-slide[data-slide="1"] .plates-slide__media img {
		/* A real width (not max-width) — this image's own natural/
		   intrinsic size is smaller than the box at most viewport
		   widths, so a max-width cap alone wouldn't force any growth
		   past that intrinsic size the way it would for an image
		   naturally bigger than its box. flex-shrink:0 since a flex
		   item's default (1) would otherwise let the flex algorithm
		   compress this back down to fit the container, canceling out
		   the 200% width entirely. */
		width: 200%;
		height: auto;
		max-width: none;
		max-height: none;
		flex-shrink: 0;
	}


	/* Plates Capture — rotated 90° clockwise on mobile only. A transform
	   doesn't affect layout, so sizing the img via plain max-width/
	   max-height (as above) would constrain the PRE-rotation box, not the
	   rotated result. Instead this sizes the img using container query
	   units off .plates-slide__media's own (now flex-resolved, definite)
	   box: min(100cqw, 100cqh × the image's natural height/width ratio)
	   is the largest pre-rotation HEIGHT that still keeps the
	   post-rotation visual width ≤ the box's width *and* the
	   post-rotation visual height ≤ the box's height — i.e. the rotated
	   image fills the box without overflowing either edge. */
	.plates-slide[data-slide="0"] .plates-slide__media {
		container-type: size;
	}

	.plates-slide[data-slide="0"] .plates-slide__media img {
		width: auto;
		max-width: none;
		max-height: none;
		flex-shrink: 0;
		transform: rotate(90deg);
	}

	/* Plates Capture's mobile-only image (plates-1-capture-mobile.svg) is
	   292×150 (natural, per its own width/height attributes — its
	   viewBox uses a different internal coordinate scale) — scaled to
	   85% (-15%) of its fit-to-box size. */
	.plates-slide[data-slide="0"] .plates-slide__media img {
		height: calc(min(100cqw, 100cqh * 150 / 292) * 0.85);
	}

	/* Overview — same rotate-and-fit technique as Capture above, its own
	   natural size (1844×1421, already cropped to its visible content —
	   see plates-3-overview.webp) standing in for Capture's 292×150. Its
	   own JS-measured vertical position (see main.js) then re-centers the
	   whole .plates-slide__media box between the title and the pagination
	   pills, rather than leaving it at its plain flex order:2 position. */
	.plates-slide[data-slide="2"] .plates-slide__media {
		container-type: size;
	}

	.plates-slide[data-slide="2"] .plates-slide__media img {
		width: auto;
		max-width: none;
		max-height: none;
		flex-shrink: 0;
		transform: rotate(90deg);
		height: min(100cqw, 100cqh * 1421 / 1844);
	}

	/* auto — pushes the pagination dots (and .plates-bts right after
	   them) down to consume all the leftover space in .plates-page's
	   min-height:100svh column, pinning the BTS bar to the bottom of the
	   available screen whenever the slide's own content (title/image/
	   text above) doesn't already fill it. On a slide tall enough to
	   need scrolling anyway, this just collapses toward 0 instead —
	   there's no leftover space left to push into. Replaces
	   .studio-pagination's own margin-top (would otherwise add on top of
	   this), scoped to Plates only. */
	.plates-page .studio-pagination {
		margin-top: auto;
		/* .plates-slider is position:relative with z-index:1, which pulls
		   it into its own stacking context above any later *static*
		   sibling regardless of DOM order — so Overview's image (now
		   deliberately overflowing its box, see overflow:visible above)
		   was painting over the dots and silently eating clicks, even
		   though it's visibly "behind" them. Same fix as the desktop
		   pagination rule further down: give the dots a higher explicit
		   stacking context of their own. */
		position: relative;
		z-index: 2;
	}

	/* Unselected dots get a black fill here (Studio V2's own pills are
	   already black at every width) — matches .is-active's own #d9d9d9
	   fill being an intentional, visible contrast rather than sitting on
	   a transparent/see-through pill. */
	.plates-page .studio-pagination__dot {
		background: #000;
	}

	/* The selected dot uses the shared #d9d9d9 (near-white) fill
	   elsewhere — pure white here, scoped to Plates only. */
	.plates-page .studio-pagination__dot.is-active {
		background: var(--color-white);
		border-color: var(--color-white);
	}

	/* Dots "stuck to" the BTS bar below — pulled up by half their own
	   fixed 17px height so the bar's top line crosses their vertical
	   center, same technique as Studio V2's .facility-pagination /
	   .facility-bts (mobile). */
	.plates-bts {
		display: flex;
		width: 100%;
		/* Same size as Home's mobile BTS bar. */
		height: clamp(50px, 8vh, 140px);
		margin-top: -8.5px;
		overflow: hidden;
		/* Photo strip disabled for now (see page-plates.php) — plain
		   black fill instead, sized identically so nothing around it has
		   to move. */
		background: var(--color-bg);
		/* Explicit stacking order: .plates-bts itself lowest (z-index:0)
		   — .plates-page__bg's blue glow above that (z-index:1, see the
		   mobile override below — its own base rule elsewhere is 0) so it
		   shows over this black bar instead of being hidden behind it —
		   the pill navigation on top of both (z-index:2, set above). */
		position: relative;
		z-index: 0;
	}

	.plates-bts__item {
		flex: 0 0 auto;
		height: 100%;
		width: auto;
		aspect-ratio: 1 / 1;
		object-fit: cover;
		display: block;
	}

	/* Bottom of the same stack as .plates-bts/.studio-pagination above —
	   scoped to Plates' own footer via the adjacent-sibling selector so
	   other pages' footers are untouched. */
	.plates-page + .site-footer {
		position: relative;
		z-index: 0;
	}
}

/* Hidden outside mobile — Plates' desktop layout has no BTS bar concept
   (its pagination dots are independently pinned via the min-width:901px
   rules further down, synced to Studio's own math instead). Explicitly
   scoped to min-width (rather than an unscoped rule relying on losing
   the cascade to the max-width:900px block above) so source order can't
   flip which one wins on mobile. */
@media (min-width: 901px) {
	.plates-bts {
		display: none;
	}
}

/* ==========================================================================
   Plates page
   ========================================================================== */

.plates-page {
	position: relative;
	overflow: hidden;
	min-height: 100vh;
	min-height: 100svh;
	display: flex;
	flex-direction: column;
	padding: clamp(90px, 9vw, 130px) 0 clamp(30px, 4vw, 60px);
}

@media (max-width: 900px) {
	/* 0, not the base rule's clamp above — .plates-bts (mobile's true
	   last element) needs to sit flush against the footer below it, same
	   as .home-bts does on Home mobile. A separate block placed after
	   the base rule (rather than added to the max-width:900px block
	   further up) so it actually wins the cascade — that earlier block
	   is written before this base rule in source order, so an override
	   placed there would lose to it despite the media query matching. */
	.plates-page {
		padding-bottom: 0;
	}
}

@media (min-width: 901px) {
	/* Desktop only — on mobile the info+image stack is much taller, so
	   centering pushes the heading well down the page instead of sitting
	   at the top under the header (see the flex-start override in the
	   max-width:900px block above). This used to be unscoped here and,
	   being later in source than that mobile override with equal
	   specificity, silently won on mobile too — same bug pattern as the
	   .plates-slide__media justify-content fix below. */
	.plates-page {
		justify-content: center;
	}
}

.plates-page__bg {
	position: absolute;
	inset: 0;
	z-index: 0;
	background:
		radial-gradient(circle var(--contact-glow-size) at 100% 15%, rgba(44, 83, 208, 0.5), transparent 68%),
		radial-gradient(circle var(--contact-glow-size) at 0% 90%, rgba(44, 83, 208, 0.45), transparent 68%);
	pointer-events: none;
}

@media (max-width: 900px) {
	/* Mobile only: lifted above .plates-bts (z-index:0, see its own rule
	   further up) so the glow shows over the black BTS bar instead of
	   being hidden behind it — still below the pill navigation's own
	   z-index:2. Placed after the base rule above (not folded into the
	   earlier max-width:900px block) so it actually wins the cascade —
	   same equal-specificity/source-order trap this file has hit before. */
	.plates-page__bg {
		z-index: 1;
	}
}

.plates-slider {
	position: relative;
	z-index: 1;
	width: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	padding-inline: var(--container-pad);
}

@media (min-width: 901px) {
	/* Absolutely positioned and layered over the image (overlap is fine),
	   same technique as Studio — this way the row's height is driven only
	   by the image, not by how much text a given slide has, so slides
	   with no body copy (Overview) don't pull the pagination up/down. */
	.plates-slide__info {
		position: absolute;
		left: 0;
		top: 50%;
		transform: translateY(-50%);
		z-index: 2;
		width: clamp(240px, 28vw, 420px);
	}

}

.plates-slide__title {
	font-stretch: 75%;
	/* 800 is the heaviest weight the loaded Open Sans variable range
	   (300..800) supports. */
	font-weight: 800;
	text-transform: uppercase;
	font-size: clamp(1.8rem, 3.7vw, 4rem);
	letter-spacing: 0.15em;
	margin: 0 0 clamp(16px, 2.5vw, 28px);
	color: var(--color-white);
}

.plates-slide__body {
	font-size: clamp(14px, 1.4vw, 24px);
	line-height: 2.2;
	letter-spacing: 0.1em;
}

.plates-slide__body p {
	margin: 0 0 1em;
}

.plates-slide__body p:last-child {
	margin-bottom: 0;
}

/* Overview's call to action: a white pill (this wrapper) with Home's blue
   "Contact Us" button (a .btn--primary) laid over its left end, and "For our
   plates library" in blue on the white to its right. The button is the same
   height as the pill, so it covers the pill's left cap exactly while the
   white shows around its rounded right end. */
.plates-slide__cta {
	display: flex;
	flex-direction: row;
	align-items: center;
	background: var(--color-white);
	border-radius: 999px;
	/* The whole component's size: the button's label and padding, the text
	   and the spacing all follow this (they're all in em). 30% smaller than
	   the old clamp(14px, 1.1vw, 20px) on desktop. */
	font-size: clamp(9.8px, 0.77vw, 14px);
}

/* The blue button sits flush in the white pill (no .btn:hover lift, which
   would show a white sliver under it) and can be dragged along the pill (see
   the Plates "slide to contact" code in main.js, which sets --drag-x) as well
   as clicked. Above the text so it covers it as it slides over. */
.plates-slide__cta .btn,
.plates-slide__cta .btn:hover {
	transform: translateX(var(--drag-x, 0px));
}

/* Each word of the text hides entirely as soon as the sliding button reaches
   it (main.js toggles .is-hidden), so no half-covered letters ever show
   around the button's rounded end — only white is left of the button. */
.plates-slide__cta-word {
	transition: opacity 0.1s ease;
}

.plates-slide__cta-word.is-hidden {
	opacity: 0;
}

.plates-slide__cta .btn {
	/* Follows the wrapper's size (a plain .btn has its own clamp). */
	font-size: 1em;
	position: relative;
	z-index: 1;
	cursor: grab;
	user-select: none;
	-webkit-user-select: none;
	-webkit-user-drag: none;
	/* Lets a phone keep scrolling the page vertically, while a horizontal
	   drag on the button comes through as its own pointer events instead. */
	touch-action: pan-y;
	transition: transform 0.25s ease, opacity 0.15s ease;
}

.plates-slide__cta .btn.is-dragging {
	cursor: grabbing;
	transition: opacity 0.15s ease;
}

.plates-slide__cta-note {
	padding: 0 1.2em 0 0.9em;
	font-weight: 600;
	letter-spacing: 0.15em;
	white-space: nowrap;
	color: var(--color-blue);
}

@media (min-width: 901px) {
	/* Hangs just under the heading (top:100% of .plates-slide__info, which
	   is itself positioned) rather than sitting in its flow, so the heading
	   keeps the exact spot it had before the button existed instead of the
	   block re-centering around it. Sized to its content (not the narrow
	   heading column), so the text stays on one line beside the button. */
	.plates-slide__cta {
		position: absolute;
		top: 100%;
		left: 0;
		width: max-content;
	}
}

@media (max-width: 900px) {
	/* Below the image (the title is order 1, the image 2), centered on the
	   page. Placed here, after the base rules above, so it wins the
	   cascade. */
	.plates-slide__cta {
		order: 3;
		align-self: center;
		/* One shared size for the button's label and the text beside it (the
		   button's em-based padding follows it too). 20% smaller than before
		   (min(11.2px, 2.95vw)): 9px on a 375px phone, scaling down with the
		   screen width on narrower ones. */
		font-size: min(8.96px, 2.36vw);
		max-width: 100%;
	}

	.plates-slide__cta .btn {
		flex: 0 0 auto;
		font-size: 1em;
	}

	.plates-slide__cta-note {
		font-size: 1em;
		letter-spacing: 0.12em;
		line-height: 1.4;
		min-width: 0;
	}
}

.plates-slide__media {
	position: relative;
	z-index: 1;
	display: flex;
	align-items: center;
}

/* Plates Capture's <picture> (mobile-only source swap, see
   page-plates.php) shouldn't introduce a box of its own — tested at
   desktop widths, an actual <picture> element (even one whose <source>
   doesn't match, falling through to the plain <img>) collapses to 0×0
   here, taking the img with it; some interaction between the flex
   layout and the img's max-height-driven auto sizing that a plain img
   never hit. display:contents removes picture from the box tree
   entirely so the img participates in the flex layout directly, exactly
   as if there were no wrapper — sidesteps it on every breakpoint. */
.plates-slide__media picture {
	display: contents;
}

@media (min-width: 901px) {
	/* Right-anchored on desktop, same as Studio. Scoped here (rather than
	   in the shared rule above) because it was previously unscoped and,
	   being equal specificity but later in source than the mobile
	   block's own justify-content:center, silently won on mobile too —
	   pinning every Plates image to the right edge there regardless of
	   the mobile rule's intent. Only noticeable once an image (Overview,
	   scaled to 180%) was wide enough for the mispositioning to be
	   visible. */
	.plates-slide__media {
		justify-content: flex-end;
	}
}

.plates-slide__media img {
	/* Belt-and-suspenders alongside the draggable="false" attribute in
	   page-plates.php — stops the browser's own native image-drag/text-
	   selection gestures from hijacking the swipe-between-slides pointer
	   sequence in main.js. */
	-webkit-user-drag: none;
	user-select: none;
	/* width/height both auto + max-width/max-height as the only caps —
	   the browser fits the image within both limits while preserving its
	   natural aspect ratio. A fixed height paired with max-width can
	   squish it (width gets capped, height doesn't follow); this can't. */
	width: auto;
	height: auto;
	max-width: 100%;
	/* 200% of the original clamp(220px, 40vh, 460px), then +25% on top. */
	max-height: clamp(550px, 100vh, 1150px);
	display: block;
}

@media (min-width: 901px) {
	/* "Plates Capture" (slide 0) is a tightly-cropped diagram with no
	   built-in dead space around it (unlike "Overview"'s composite) — at
	   the shared base size it's oversized and collides with the
	   paragraph text, so it's scaled down specifically. Scoped to
	   desktop only — mobile uses the same plain width:100% rule for
	   every slide, so they all scale identically there (see the
	   max-width:900px block above). */
	.plates-slide[data-slide="0"] .plates-slide__media img {
		/* -5% on top of the previous clamp(237px, 38.1vh, 440px). */
		max-height: clamp(225px, 36.2vh, 418px);
	}

	/* "Height Options" (slide 1) is also tightly cropped, but much wider
	   and shorter (roughly 3.3:1) than slide 0's diagram — height alone
	   isn't the binding constraint here, since a height that fits
	   comfortably still leaves the width wide enough to run into the
	   text column. Capped by width instead, sized to clear the info
	   column (clamp(240px, 28vw, 420px) wide) plus a gap. Desktop only,
	   same reasoning as slide 0 above. */
	.plates-slide[data-slide="1"] .plates-slide__media img {
		max-width: 58%;
		max-height: clamp(225px, 36.2vh, 418px);
	}

	/* "Overview" (slide 2): width is 50% of the distance between the
	   FILMSCAPE logo's left edge and the RESOURCES button's right edge in
	   the header — not just 50vw — so main.js measures both and sets an
	   exact px width inline; this is only the static no-JS fallback.
	   height left to auto so it scales proportionally from that.
	   .plates-slide__media's own justify-content:flex-end (above) is what
	   keeps the image's RIGHT edge anchored in place as this shrinks it:
	   a flex item sized smaller than its container, right-aligned, gives
	   up space from its left edge only, not its right. */
	.plates-slide[data-slide="2"] .plates-slide__media img {
		width: 50vw;
		max-width: 50vw;
		height: auto;
		max-height: none;
	}
}

@media (min-width: 901px) {
	/* .plates-page's own flex centering balances the image/text block
	   between ITS padding-top and padding-bottom — but the pagination
	   below is pinned to Studio's own (different) numbers, not Plates',
	   so that centering doesn't actually land in the middle of the real
	   gap between the header and the dots. This shifts the block up by
	   exactly the offset between those two reference frames — derived
	   from the same formula the pagination's own "top" uses below, so it
	   stays correct at any viewport size instead of a guessed pixel
	   value. Doesn't touch the pagination itself (it's positioned
	   independently, absolute relative to .plates-slider, not the slide). */
	.plates-slide {
		transform: translateY(calc(
			-1 * (100vh - clamp(90px, 9vw, 130px) - clamp(300px, 68vh, 620px) - clamp(20px, 3vw, 36px) - clamp(30px, 4vw, 60px) + 17px) / 4
		));
	}

	/* Pinned to exactly where Studio's pagination naturally lands —
	   reproducing Studio's own padding + image-height + centering math
	   here, entirely in terms of Studio's numbers, not Plates' (now much
	   taller) image. That keeps the dots in a fixed spot regardless of
	   how big the Plates image gets, instead of chasing it down the page. */
	.plates-page .studio-pagination {
		position: absolute;
		/* .plates-slider creates its own stacking context (z-index: 1) and
		   now contains a much taller image than before — without this,
		   that image paints above the dots and silently swallows clicks
		   even though they're still visibly on top of nothing. */
		z-index: 5;
		left: var(--container-pad);
		right: var(--container-pad);
		margin-top: 0;
		top: calc(
			clamp(90px, 9vw, 130px) + clamp(300px, 68vh, 620px) + clamp(20px, 3vw, 36px) +
			(100vh - clamp(90px, 9vw, 130px) - clamp(300px, 68vh, 620px) - clamp(20px, 3vw, 36px) - 17px - clamp(30px, 4vw, 60px)) / 2
		);
	}
}

/* ==========================================================================
   Contact page
   ========================================================================== */

.contact-hero {
	position: relative;
	width: 100%;
	overflow: hidden;
	aspect-ratio: 1728 / 1188;
	min-height: clamp(320px, 45vw, 620px);
	max-height: 100vh;
	max-height: 100svh;
	display: flex;
	align-items: flex-end;
}

.contact-hero__media {
	position: absolute;
	inset: 0;
}

.contact-hero__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.contact-hero__gradient-top {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 34%;
	background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));
}

.contact-hero__gradient-bottom {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 74%;
	background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 92%);
}

.contact-hero__content {
	position: relative;
	z-index: 1;
	width: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	padding: 0 var(--container-pad) clamp(28px, 4vw, 60px);
}

@media (min-width: 1920px) {
	.contact-hero__content {
		padding-bottom: 100px;
	}
}

@media (min-width: 901px) {
	/* Title's vertical position is fully taken over by assets/js/main.js
	   (measures the actual rendered title/form heights and pins the
	   title `gap`px above the form, with the form's own submit button
	   landing `gap`px above the bottom of the initial viewport — the
	   same reasoning as the Projects page's title/poster-row script: the
	   combined height of both is a mix of several responsive clamp()s
	   that plain CSS can't solve for across every viewport). flex-start
	   + top padding here is just that script's pre-JS fallback position,
	   same pattern as Projects. */
	.contact-hero {
		align-items: flex-start;
	}

	.contact-hero__content {
		max-width: none;
		padding: clamp(90px, 9vw, 130px) var(--container-pad) 0;
	}

	/* Lines the title's left edge up with the form's — .contact-form-
	   section__inner is max-width:450px + margin-inline:auto *inside*
	   .contact-form-section's own container-pad padding, with no extra
	   max-width cap on that outer section. Giving the title the exact
	   same max-width + auto-margin, nested inside this same container-pad
	   padding (uncapped above, unlike the default container-max), lands
	   its left edge on exactly the same pixel as the form's at any
	   viewport width — including wider ones where container-max would
	   otherwise have pulled the title's reference frame in from the
	   form's uncapped one. Kept in sync with .contact-form-section__inner
	   above — if that width changes, this needs to match it again. */
	.contact-hero__content .hero__title {
		max-width: 450px;
		margin-inline: auto;
	}
}

.contact-form-section {
	position: relative;
	overflow: hidden;
	padding: clamp(50px, 7vw, 100px) var(--container-pad);
}

.contact-form-section__inner {
	position: relative;
	z-index: 1;
	/* -50% off the original 900px. */
	max-width: 450px;
	margin-inline: auto;
}

.contact-form__status {
	padding: 0.9em 1.2em;
	margin: 0 0 clamp(20px, 2.5vw, 32px);
	border-left: 3px solid var(--color-blue);
	background: rgba(255, 255, 255, 0.05);
	font-size: clamp(14px, 1vw, 16px);
}

.contact-form__status--error {
	border-left-color: #d02c2c;
}

.contact-form__honeypot {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

.contact-form__field {
	border-bottom: 1px solid rgba(255, 255, 255, 0.4);
	padding-bottom: clamp(10px, 1.4vw, 16px);
	margin-bottom: clamp(20px, 2.6vw, 32px);
}

.contact-form__field label {
	display: block;
	font-family: 'Didact Gothic', sans-serif;
	font-size: clamp(12px, 0.9vw, 16px);
	letter-spacing: 0.23em;
	text-transform: uppercase;
	margin-bottom: clamp(8px, 1vw, 12px);
}

.contact-form__field input,
.contact-form__field textarea {
	display: block;
	width: 100%;
	background: transparent;
	border: none;
	color: var(--color-white);
	font-family: var(--font-sans);
	font-size: clamp(15px, 1.1vw, 18px);
	padding: 0;
	resize: vertical;
}

.contact-form__field input:focus,
.contact-form__field textarea:focus {
	outline: none;
}

.contact-form__field--message textarea {
	min-height: 4.5em;
}

.contact-form__submit {
	display: block;
	margin-inline: auto;
}

@media (max-width: 900px) {
	/* Pulls the fields up closer to the title (smaller gap below the
	   hero, smaller gap above the first field) without moving the submit
	   button's own position on the page — its margin-top below adds back
	   exactly what these two reductions remove (old values minus new
	   ones), so the extra room shows up as breathing room right above
	   the button instead of the button drifting upward with everything
	   else. */
	.contact-hero__content {
		padding-bottom: clamp(10px, 2vw, 16px);
	}

	.contact-form-section {
		padding-top: clamp(8px, 1.5vw, 14px);
	}

	.contact-form__submit {
		/* Decreased from the old position-compensating calc() (which
		   worked out to ~60px at common mobile widths) down to a plainer,
		   smaller gap — this is just the button's own breathing room now,
		   not counteracting the other mobile spacing reductions above. */
		margin-top: clamp(16px, 3vw, 28px);
	}
}

@media (min-width: 901px) {
	/* Compacted so title + gap + the whole form + gap has a realistic
	   chance of fitting above the fold on typical desktop heights (see
	   the layout script in main.js) — at the original spacing, even a
	   1200px-tall viewport wasn't enough once the title's header-
	   clearance floor was respected. Mobile keeps the original, more
	   comfortable spacing (not part of this fit constraint). */
	.contact-form-section {
		padding-top: clamp(14px, 1.5vw, 22px);
	}

	.contact-form__field {
		padding-bottom: clamp(3px, 0.4vw, 5px);
		margin-bottom: clamp(4px, 0.6vw, 7px);
	}

	.contact-form__field label {
		margin-bottom: clamp(2px, 0.3vw, 4px);
	}

	.contact-form__field--message textarea {
		min-height: 1.2em;
	}

	/* More breathing room above the button than the compressed field-to-
	   field gap above gives it on its own — the layout script in main.js
	   measures the button's own rendered position afterwards, so this is
	   already accounted for in the title+form centering, not fighting it. */
	.contact-form__submit {
		/* Increased further from clamp(14px, 2vw, 24px). */
		margin-top: clamp(24px, 3.2vw, 40px);
	}
}

.team-section {
	position: relative;
	padding: clamp(20px, 3vw, 40px) var(--container-pad) clamp(60px, 7vw, 100px);
	text-align: center;
	background: radial-gradient(circle var(--contact-glow-size) at 0% 50%, rgba(44, 83, 208, 0.4), transparent 68%);
}

.team-section__heading {
	font-stretch: 75%;
	font-weight: 600;
	font-size: clamp(2rem, 4.5vw, 4.5rem);
	letter-spacing: 0.23em;
	text-transform: uppercase;
	margin: 0 0 clamp(36px, 5vw, 64px);
}

.team-grid {
	/* Flex (not grid) so a short last row centers itself automatically —
	   grid's 1fr columns always stretch full-width even on a partial row,
	   which is what made centering it awkward. */
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: clamp(24px, 3vw, 48px) clamp(20px, 2.5vw, 40px);
	/* Capped well below container-max so cards stay a sensible size on
	   desktop instead of stretching to fill the full page width. */
	max-width: 1100px;
	margin-inline: auto;
}

.team-card {
	/* Exactly 1/4 of the row (accounting for 3 gaps) — deterministic 4
	   per row, rather than however many happen to fit a fixed width. */
	width: calc((100% - 3 * clamp(20px, 2.5vw, 40px)) / 4);
}

@media (max-width: 760px) {
	.team-card {
		width: calc((100% - clamp(20px, 2.5vw, 40px)) / 2);
	}
}

.team-card__portrait {
	aspect-ratio: 244 / 318;
	border-radius: 28px;
	overflow: hidden;
	background: #111;
	margin-bottom: clamp(14px, 1.6vw, 22px);
}

.team-card__portrait img {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.team-card__name {
	font-weight: 700;
	font-size: clamp(15px, 1.2vw, 20px);
	letter-spacing: 0.15em;
	margin: 0 0 0.4em;
}

.team-card__role {
	font-family: 'Didact Gothic', sans-serif;
	font-size: clamp(13px, 1vw, 17px);
	letter-spacing: 0.15em;
	color: rgba(255, 255, 255, 0.7);
	margin: 0;
}

.contact-map {
	width: 100%;
	aspect-ratio: 1728 / 440;
	min-height: 240px;
}

.contact-map__frame {
	width: 100%;
	height: 100%;
	border: 0;
	display: block;
	filter: invert(92%) hue-rotate(180deg) brightness(0.95) contrast(0.9);
}

/* ==========================================================================
   Resources page
   ========================================================================== */

.resources-hero {
	position: relative;
	min-height: 100vh;
	min-height: 100svh;
	display: flex;
	align-items: flex-end;
	overflow: hidden;
}

.resources-hero__media {
	position: absolute;
	inset: 0;
}

.resources-hero__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.resources-hero__gradient {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 65%;
	background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 92%);
}

.resources-hero__content {
	position: relative;
	z-index: 1;
	width: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	padding: 0 var(--container-pad) clamp(28px, 4vw, 60px);
}

.resources-hero__body {
	margin: clamp(20px, 3vw, 32px) 0 0;
	font-size: clamp(15px, 1.25vw, 24px);
	letter-spacing: 0.1em;
	line-height: 1.7;
	color: rgba(255, 255, 255, 0.9);
}

.resources-hero__body p {
	margin: 0;
}

.resources-hero__download {
	margin-top: clamp(24px, 3vw, 40px);
}

/* ==========================================================================
   Basic fallback page (index.php)
   ========================================================================== */

.site-main--basic {
	padding-top: clamp(140px, 14vw, 200px);
	padding-bottom: 80px;
}

/* ==========================================================================
   Studio V2 — Facility (draft revamp, page-studio-v2.php)

   Built bottom-up around available screen space: the page is exactly one
   viewport tall, laid out as a column pinned to the bottom — so the BTS
   photo bar is always flush with the bottom of the visible screen, and
   the pill pagination row (its own flex-column sibling, immediately
   before it in the markup) always lands directly above it, regardless of
   what ends up above the two of them. Own "facility-*" classnames
   throughout — never touches the live Studio page's "studio-*" rules.
   ========================================================================== */

.facility-page {
	position: relative;
	min-height: 100vh;
	min-height: 100svh;
	display: flex;
	flex-direction: column;
	justify-content: flex-end;
	overflow: hidden;
}

/* One .facility-content per slide (Facility, Studio 5, …) — same
   show/hide technique as the live Studio page's .studio-slide. Only the
   active one is flex:1 auto (i.e. actually takes up the row's space);
   the others are display:none so they don't stack up empty space. */
.facility-content {
	display: none;
	position: relative;
	min-height: 0;
	width: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	padding: 0 var(--container-pad);
}

.facility-content.is-active {
	display: block;
	flex: 1 1 auto;
}

/* Left-aligned title + numbered feature list. Vertically centered here
   as a CSS fallback (matches .facility-visual__diagram's own base
   position) — main.js overrides both to the same exact measured line
   (header bottom to BTS bar top) so they always share one center. */
.facility-text {
	position: absolute;
	left: 0;
	top: 50%;
	transform: translateY(-50%);
	z-index: 2;
}

.facility-title {
	margin: 0 0 clamp(24px, 5vw, 90px);
	/* 800 is the heaviest weight the loaded Open Sans variable range
	   (300..800) supports. */
	font-weight: 800;
	font-size: clamp(20px, 7vh, 80px);
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--color-white);
}

.facility-list {
	list-style: none;
	counter-reset: facility-item;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: clamp(2px, 0.6vw, 10px);
}

.facility-list li {
	counter-increment: facility-item;
	/* Grid instead of the plain inline flow this used to be — the "1."
	   counter and the label/desc content land in their own columns, so
	   .facility-list__desc lines up under the label exactly regardless of
	   font metrics/letter-spacing, instead of an inline-block counter
	   width plus a guessed matching margin on the desc (which drifted out
	   of sync — letter-spacing on the counter's own characters isn't
	   reliably "1.4em of width" in practice). */
	display: grid;
	grid-template-columns: 1.4em 1fr;
	/* 5% smaller than the original clamp(14px,1.85vw,32px) — the title
	   above is unscaled, only body/list text. */
	font-size: clamp(13.3px, 1.7575vw, 30.4px);
	letter-spacing: 0.12em;
	text-transform: uppercase;
	/* One step up from the previous 300 (Light) — still lighter than the
	   800 (Bold) hover/expanded weight below, just not as thin at rest. */
	font-weight: 400;
	color: var(--color-white);
}

.facility-list li::before {
	content: counter(facility-item) ".";
	grid-column: 1;
	grid-row: 1;
	letter-spacing: normal;
	font-weight: 800;
}

/* Hover highlight, same as the live Studio page (see
   .studio-slide__list li[data-hotspot] / .studio-slide__hotspot). */
.facility-list li[data-hotspot] {
	cursor: pointer;
	transition: font-weight 0.2s ease;
}

.facility-list li[data-hotspot]:hover,
.facility-list li[data-hotspot].is-expanded {
	font-weight: 800;
}

.facility-list__label {
	grid-column: 1;
	grid-row: 1;
}

/* Pill outline around the clickable titles on every Studio slide.
   justify-self:start so the pill hugs the text instead of stretching the
   whole grid column; the "1." counter stays outside it, vertically
   centered against the (now taller) pill. */
.facility-list li {
	align-items: center;
}

/* The number moves inside the pill: li::before's counter is hidden here
   and re-drawn as the label's own ::before, so it sits within the outline
   (and picks up the label's black-on-white inverted colors when open). The
   label and description then span the whole row instead of sitting in
   column 2, since column 1 no longer holds anything. */
.facility-list li::before {
	display: none;
}

.facility-list li {
	grid-template-columns: 1fr;
}

.facility-list__label,
.facility-list__desc {
	grid-column: 1;
}

/* The number re-drawn inside the pill, ahead of the title. It's a pseudo-
   element, so main.js's Range over the label's text (which lines the
   revealed text up with the title's WORDS) doesn't include it — the text
   still lines up with the words, not the number. */
.facility-list__label::before {
	content: counter(facility-item) ".";
	margin-right: 0.5em;
	letter-spacing: normal;
	font-weight: 800;
}

.facility-list__label {
	position: relative;
	justify-self: start;
	/* 20% smaller than the li's size; the desc text is sized off the li, not
	   this, so it's unaffected. Padding is in em so the pill shrinks with it. */
	font-size: 0.8em;
	padding: 0.1em 0.9em;
	border: 1.5px solid var(--color-white);
	border-radius: 999px;
	/* The raised-button 3D lip: a hard white shadow straight down. The pill
	   itself is transparent, so it only shows below the pill's edge. */
	box-shadow: 0 4px 0 var(--color-white);
	transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
}

/* Border doubles (1.5px -> ~3px) on hover, along with the text's own bold.
   Done as an extra ring in box-shadow rather than by changing border-width,
   so the pill's box (and everything around it) doesn't shift every time the
   mouse crosses it. Keeps the lip too, since this replaces the box-shadow. */
.facility-list li:hover .facility-list__label {
	box-shadow: 0 4px 0 var(--color-white), 0 0 0 0.75px var(--color-white);
}

/* Same inverted treatment as the header nav's active item (white fill,
   black text) while this title's description is open. Pressed in: the 3D
   lip is dropped. (No sink/translate — that would push the title's words
   off the left edge the revealed text is lined up to.) */
.facility-list li.is-expanded .facility-list__label {
	background: var(--color-white);
	color: #000;
	text-shadow: none;
	box-shadow: none;
}

.facility-list li.is-expanded:hover .facility-list__label {
	box-shadow: 0 0 0 0.75px var(--color-white);
}

/* While any title on a slide is open, every OTHER title's pill drops to 30%
   opacity, leaving only the selected one at full strength. */
.facility-list:has(li.is-expanded) li:not(.is-expanded) .facility-list__label {
	opacity: 0.3;
}

/* Clicking a list item (see main.js) toggles .is-expanded on it, revealing
   this description beneath the label and pushing the following list items
   down with it — a plain max-height transition rather than height:auto
   (which can't be animated), sized well past a few short paragraphs so
   real copy isn't clipped (see Appearance > Studio Text, which now
   accepts more than one paragraph). Sized relative to the label's own
   font-size (em) so it stays proportionate at both the desktop and
   mobile list sizes without its own clamp(). Placed in the label's own
   grid column (see .facility-list li above) so it lines up under the
   label text, not the "1." counter, without having to guess a matching
   margin. The saved text is run through wpautop() (see inc/studio-
   data.php), which wraps it in real <p> tags — reset to remove the
   browser's own default paragraph margin, replaced with a fixed gap
   between paragraphs instead. */
.facility-list__desc {
	grid-column: 1;
	grid-row: 2;
	max-height: 0;
	overflow: hidden;
	opacity: 0;
	font-size: 0.75em;
	font-weight: 300;
	letter-spacing: 0.02em;
	text-transform: none;
	color: var(--color-white);
	transition: max-height 0.4s ease, opacity 0.4s ease, margin-top 0.4s ease, margin-bottom 0.4s ease;
}

.facility-list__desc p {
	margin: 0 0 0.8em;
}

.facility-list__desc p:last-child {
	margin-bottom: 0;
}

.facility-list li.is-expanded .facility-list__desc {
	max-height: 16em;
	opacity: 1;
	/* Space above and below the revealed text: 200% more than the old
	   0.5em above (and none below). */
	margin-top: 1.5em;
	margin-bottom: 1.5em;
}

/* Right-flush panel the floor plan diagram will sit on (added in a
   later step) — sized/positioned to match the Figma revamp's own
   Facility content row: full height of the row, ~40% of its width. */
.facility-visual {
	position: absolute;
	top: 0;
	right: 0;
	height: 100%;
	width: clamp(220px, 33vw, 563px);
	/* Purely decorative (the diagram/hotspot images already have their
	   own pointer-events:none too) — without this, its bounding box
	   being position:absolute makes it paint (and capture clicks) above
	   static-positioned siblings like .facility-pagination regardless of
	   DOM order, which is what was blocking the pills whenever a slide's
	   diagram sat low enough to overlap them (e.g. Ground Floor mobile). */
	pointer-events: none;
}

.facility-visual__bg {
	position: absolute;
	inset: 0;
	background: linear-gradient(180deg, var(--color-blue) 0%, #000 100%);
}

/* Positioned as one unit (main.js sets its "top") so the base diagram
   and its hotspot overlays stay pixel-aligned — display:grid stacks
   them in the same cell (like .studio-slide__media on the live Studio
   page) instead of each needing its own position. */
.facility-visual__diagram-wrap {
	position: absolute;
	right: 0;
	top: 50%;
	transform: translateY(-50%);
	display: grid;
	align-items: center;
	justify-items: end;
}

/* The diagram is an alpha-transparent render weighted to its own right
   half (like the live Studio page's identical source images — see
   .studio-slide__media img) — sized by height and right-aligned so it
   naturally bleeds left past the blue panel's own left edge onto the
   black background, matching the Figma revamp, and its right edge
   automatically tracks the same RESOURCES-button-synced offset as the
   panel underneath it (main.js) since it's positioned right:0 within
   the same .facility-visual box. */
.facility-visual__diagram,
.facility-visual__hotspot {
	grid-area: 1 / 1;
	height: clamp(280px, 55vh, 580px);
	width: auto;
	max-width: none;
	display: block;
	pointer-events: none;
}

/* Ground Floor, desktop only: 3% of the screen width of extra room on the
   image's left, done as a shift right (main.js pins the wrap's "right"
   and "top" only, so the wrap's transform is free to carry this). */
@media (min-width: 901px) {
	.facility-content[data-slide="3"] .facility-visual__diagram-wrap {
		transform: translate(3vw, -50%);
	}
}

.facility-visual__hotspot {
	opacity: 0;
	transition: opacity 0.25s ease;
}

.facility-visual__hotspot.is-active {
	opacity: 0.75;
}

@media (max-width: 900px) {
	/* .facility-visual sits above this in paint order but is
	   pointer-events:none (see above), so .facility-content itself is
	   the real touch target for the swipe-between-slides gesture in
	   main.js — pan-y tells the browser vertical scrolling is still
	   fine to handle natively, but a horizontal/diagonal touch is never
	   grabbed away for its own gesture handling first, so the pointerup
	   main.js needs to complete the swipe always actually fires instead
	   of the browser cancelling it partway through. */
	.facility-content {
		touch-action: pan-y;
	}

	/* Mobile swaps the desktop's right-flush, full-height vertical panel
	   for a shorter horizontal bar bleeding off both edges, sitting
	   behind the vertical middle of where the diagram lands (Figma's
	   "STUDIO REVAMP: Facility Mobile" frame) — not just a scaled-down
	   copy of the desktop shape. */
	/* Layout, top to bottom: the slide's title (right up against the header),
	   the render, then the title pills, right-aligned, which main.js keeps
	   in the lower third of the screen, and finally the pill navigation
	   bar. So the slide is a column (.facility-text is display:contents
	   below, so its title and list are separate items in it) and the visual
	   is a normal in-flow, full-bleed box as tall as the diagram itself —
	   the blue bar is now a child centered on it, rather than the visual
	   being the bar. */
	.facility-content.is-active {
		display: flex;
		flex-direction: column;
	}

	.facility-visual {
		order: 2;
		position: relative;
		top: auto;
		right: auto;
		left: auto;
		transform: none;
		width: auto;
		height: auto;
		/* Bleeds past the slide's own side padding to the true screen edges. */
		margin: 0 calc(-1 * var(--container-pad));
	}

	.facility-visual__bg {
		inset: auto;
		left: -12vw;
		width: 123vw;
		top: 50%;
		height: clamp(100px, 38vw, 160px);
		transform: translateY(-50%);
	}

	/* The Volume's own image is a wider aspect ratio than the other
	   slides' (see studio-3-the-volume-V2.webp), so it renders shorter at
	   the same 100vw width — exposing much less of this bar behind it
	   than the shared height leaves room for. Taller here specifically so
	   the blue bar still reads clearly on this slide. */
	.facility-content[data-slide="2"] .facility-visual__bg {
		/* Scaled up 15% along with the render (see its 115vw width below) so
		   the bar keeps the same proportion to it, then 5% shorter twice. */
		height: clamp(124.57px, 47.17vw, 199.33px);
	}

	.facility-visual__bg {
		background: linear-gradient(90deg, var(--color-blue) 0%, #000 84.8%);
		transition: opacity 0.2s ease;
	}

	/* While a title's text is open, the blue bar dims to the same 30% the
	   other title pills drop to. */
	.facility-content:has(.facility-list li.is-expanded) .facility-visual__bg {
		opacity: 0.3;
	}

	/* Centered on the true viewport (not just .facility-visual's own box,
	   which left:-12vw + width:123vw above offsets from it) — still
	   vertically centered at the bar's own center point (top:50% of the
	   same .facility-visual parent). */
	.facility-visual__diagram-wrap {
		position: relative;
		right: auto;
		left: auto;
		top: auto;
		transform: none;
		width: 100%;
		justify-items: center;
	}

	/* One shared size for all five slides now that every diagram/hotspot
	   is pre-cropped to its own visible-content bounding box (no more
	   wildly differing amounts of transparent dead space per slide to
	   compensate for with separate per-slide multipliers) — full screen
	   width, centered. */
	.facility-visual__diagram,
	.facility-visual__hotspot {
		width: 100vw;
		height: auto;
		max-width: none;
	}

	/* The Volume's render 15% bigger than that shared full-screen width
	   (it bleeds equally off both sides, centered; hotspots must match). */
	.facility-content[data-slide="2"] .facility-visual__diagram,
	.facility-content[data-slide="2"] .facility-visual__hotspot {
		width: 115vw;
		/* Negative side margins (half the 15% each) so its margin box is
		   still exactly 100vw: the grid track stays screen-wide and the
		   extra width bleeds evenly off both edges instead of all to the
		   right. */
		margin-inline: -7.5vw;
	}

	/* Desktop centers the text on the same line as the diagram — on
	   mobile they instead stack in normal flow, text above the
	   diagram/bar (Figma's "STUDIO REVAMP: Facility Mobile" frame), so
	   back out of the absolute/centered desktop positioning entirely. */
	.facility-text {
		/* Its title and list become direct items of the column above, so
		   they can sit on either side of the render. */
		display: contents;
	}

	/* relative + z-index so the title, pills and revealed text always paint
	   above the absolutely-positioned bar behind the render if they overlap. */
	.facility-title,
	.facility-list {
		position: relative;
		z-index: 2;
	}

	/* Title pills right-aligned to the screen's right margin; the revealed
	   text is right-aligned too, lined up (see main.js) with where the
	   pill's words end, mirroring how desktop lines it up with where they
	   start. */
	.facility-list__label {
		justify-self: end;
	}

	.facility-list__desc {
		text-align: right;
	}

	.facility-title {
		/* Matches Home's mobile title size. */
		font-size: clamp(2.4rem, 10vw, 3.75rem);
		letter-spacing: 0.1em;
		order: 1;
		/* Same distance from the header as Home's mobile title (its
		   .hero__content top, clamp(64px, 16vw, 96px), and 1.08 line-height). */
		line-height: 1.08;
		margin-top: clamp(64px, 16vw, 96px);
		margin-bottom: 0;
	}

	/* "GROUND FLOOR" is wider than the screen at the size above and wraps to
	   two lines. Sized to the available width instead (the title measures
	   ~9.4em wide, so /9.6 leaves a little slack) and kept on one line. */
	.facility-content[data-slide="3"] .facility-title {
		font-size: min(3.75rem, calc((100vw - 2 * var(--container-pad)) / 9.6));
		white-space: nowrap;
	}

	/* More room between the stacked title pills (was clamp(2px, 0.8vw, 6px),
	   ~3px at 375px wide), which also gives the 3D lip room to breathe. */
	.facility-list {
		gap: clamp(6px, 2vw, 12px);
		order: 3;
		/* Its top margin (and the render's, on both sides) is set by main.js:
		   the first pill always starts in the lower third of the screen and
		   the render is centered vertically between the title and it. */
		margin-top: 0;
	}

	.facility-list li {
		font-size: clamp(11.4px, 3.515vw, 15.2px);
		letter-spacing: 0.1em;
	}

	/* Title pills 20% bigger than desktop's 0.8em (0.8 x 1.2). The pill's
	   padding is in em, so the whole pill grows with the text; the desc
	   below is sized off the li, so it's unaffected. */
	.facility-list__label {
		font-size: 0.96em;
		transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
		/* Lip 30% shallower than the previous mobile 3.8px (2.66px). */
		box-shadow: 0 2.66px 0 var(--color-white);
	}

	.facility-list li:hover .facility-list__label {
		box-shadow: 0 2.66px 0 var(--color-white), 0 0 0 0.75px var(--color-white);
	}

	/* Re-assert the pressed-in state (no lip) over the two rules above. */
	.facility-list li.is-expanded .facility-list__label {
		box-shadow: none;
		/* Pushed in: sinks by the lip's depth (visual only — the text below
		   it doesn't move, and vertical motion doesn't affect the horizontal
		   line-up of the revealed text). */
		transform: translateY(2.66px);
	}

	.facility-list li.is-expanded:hover .facility-list__label {
		box-shadow: 0 0 0 0.75px var(--color-white);
	}

	/* Fixed 12px on mobile (was 0.75em of the list's fluid size, ~9.9px at
	   375px wide) — desktop keeps the base 0.5em. */
	.facility-list__desc {
		font-size: 12px;
	}
}

.facility-pagination {
	display: flex;
	justify-content: center;
	align-items: center;
	gap: 5px;
	padding: 0 var(--container-pad);
}

/* Desktop: pinned to the same vertical spot as Plates' pagination. Plates
   positions its dots with the formula in .plates-page .studio-pagination
   (top: ...); Studio's row naturally lands at 100vh minus the BTS bar's
   height, its 6vh margin and its own 17px, so it's nudged (visual only,
   nothing else moves) by the difference between the two — which keeps them
   matched at any viewport size, not just one measured pixel value. */
@media (min-width: 901px) {
	.facility-pagination {
		position: relative;
		top: calc(
			(
				clamp(90px, 9vw, 130px) + clamp(300px, 68vh, 620px) + clamp(20px, 3vw, 36px) +
				(100vh - clamp(90px, 9vw, 130px) - clamp(300px, 68vh, 620px) - clamp(20px, 3vw, 36px) - 17px - clamp(30px, 4vw, 60px)) / 2
			) - (100vh - clamp(70px, 12vh, 220px) - clamp(35px, 6vh, 110px) - 17px)
		);
	}
}

.facility-pagination__pill {
	width: 46px;
	height: 17px;
	flex: 0 0 auto;
	border-radius: 999px;
	border: 1px solid rgba(255, 255, 255, 0.6);
	background: #000;
	padding: 0;
	margin: 0;
	cursor: pointer;
}

.facility-pagination__pill.is-active {
	background: var(--color-white);
	border-color: var(--color-white);
}

/* Unselected pills (Studio's .facility-pagination__pill and Plates'
   .studio-pagination__dot) get the same treatment as the unselected Studio
   title pills: 1.5px white outline and a hard white 3D lip straight down
   (no dimming, though; 3px, a touch shallower than the title pills' 4px).
   The selected one stays the flat white fill, sunk 3px (the lip's height) so its bottom lines up with the lip's bottom. */
.facility-pagination__pill:not(.is-active),
.studio-pagination__dot:not(.is-active) {
	border: 1.5px solid var(--color-white);
	box-shadow: 0 3px 0 var(--color-white);
}

.facility-pagination__pill.is-active,
.studio-pagination__dot.is-active {
	transform: translateY(3px);
}

.facility-bts {
	display: flex;
	width: 100%;
	/* Exactly half of .facility-bts__item's own height clamp below. */
	margin-top: clamp(35px, 6vh, 110px);
	overflow: hidden;
	/* Photo strip disabled for now (see page-studio.php) — plain black
	   fill instead, sized identically so nothing around it has to move.
	   Needs its own explicit height now: with no .facility-bts__item
	   children left to size it (that's what it was always relying on —
	   this container itself never had a height of its own), it collapsed
	   to 0 and threw off everything pinned to it (pagination pills on
	   mobile, the gap above it on desktop). Matches the item's own
	   height clamp exactly, so this is a no-op change visually. */
	height: clamp(70px, 12vh, 220px);
	background: var(--color-bg);
}

.facility-bts__item {
	flex: 0 0 auto;
	height: clamp(70px, 12vh, 220px);
	width: auto;
	aspect-ratio: 1 / 1;
	object-fit: cover;
	display: block;
}

@media (max-width: 900px) {
	/* Same size as Home's mobile BTS bar. */
	.facility-bts__item,
	.facility-bts {
		height: clamp(50px, 8vh, 140px);
	}

	/* Mobile only: pagination pills sit centered on .facility-bts's top
	   line instead of a gap above it — desktop keeps the plain gap
	   (margin-top: clamp(35px,6vh,110px)) above. Positioned + z-index so
	   the pills stay visible above the bar they now overlap. */
	.facility-pagination {
		position: relative;
		z-index: 1;
		/* Nudged down (visual only, nothing else moves) so the pills sit
		   lower inside the bar instead of straddling its top line. */
		top: clamp(16px, 2.5vh, 28px);
	}

	.facility-bts {
		/* Half of the pagination pills' fixed 17px height. */
		margin-top: -8.5px;
	}
}
