/* Service pages (template-service.php) — Strategy, Content & Creative,
   Activation. See the template's own top comment for what's deliberately
   not built yet (hero scroll-lock, card hover-reveal tags). */

/* --- Hero (locked-scroll sequence) ---
   Ported from a working reference demo (front-end only) the user built —
   the key structural insight it confirmed: the photo lives on ONE shared
   layer behind BOTH the natural-scroll hero AND the pinned section below
   it, not as two separate section backgrounds. That's exactly why the
   very start of the page still scrolls completely normally — .service-hero
   itself is a plain 100vh block in normal flow; only .service-pinned
   (below) is tall + sticky, which is what makes IT lock.

   One deliberate change from the reference: it applied the photo via
   background-attachment:fixed on <body>. This theme hit real, confirmed
   bugs with that exact property elsewhere (assets/css/blog.css has the
   fuller writeup) — it sizes background-size:cover against the viewport
   rather than the element, and iOS Safari doesn't support it at all.
   .service-locked__bg here is a real position:fixed layer instead, sized
   explicitly to the viewport, which sidesteps both problems.

   Figma exports this as ONE layer — a gradient stacked directly on top of
   the photo in a single `background` value, not a separate overlay
   element — specifically so the two can never drift apart. Built the same
   way here: --hero-image (set inline per-page in template-service.php)
   and the gradient are two background-image layers on this one element,
   both perfectly pinned together since they're the same fixed box.

   background-position is bottom-anchored, not centered — the Figma crop
   keeps the source photo's sky entirely out of frame regardless of
   viewport height, so cover needs to crop from the top, not both edges.

   Figma's own crop is also zoomed in noticeably further than a plain
   cover fit — its declared background-size is ~140% width / ~171% height
   of its (fixed 1440×787) frame, and cover alone for that same frame only
   needs ~100%/~122%: both ratios land on almost exactly 1.4×, i.e. Figma
   scaled the image up 1.4× beyond cover, uniformly. transform:scale(1.4)
   reproduces that same relative zoom at any viewport size/aspect ratio,
   which a fixed background-size percentage (aspect-ratio-dependent)
   couldn't do responsively. transform-origin is pinned to the bottom so
   the extra zoom eats into the top (more sky cropped away, as intended)
   rather than shifting the bottom-anchor established above.

   Because this is position:fixed, it stays rendered behind the ENTIRE
   page for the rest of the scroll, permanently — a fixed element doesn't
   get removed or clipped just because its trigger section scrolled out
   of view, and covering it once does NOT mean it's "done": at z-index:-1
   it's behind normal in-flow content everywhere by definition, which
   means EVERY section from here to the footer needs its own genuinely
   opaque background, not only the first one after .service-locked. This
   was under-scoped once already — only .service-cards got an explicit
   background, on the wrong assumption that later sections would just
   inherit plain white once "past" this layer; they don't, because this
   layer is still there behind them too, so .service-hwt and
   .service-closing carry the same fix (see their own comments). This is
   exactly how the reference demo's own background-attachment:fixed on
   <body> works too — it never "turns off," every section of that page
   is simply opaque enough to cover it throughout. */

.service-locked {
	position: relative;
}

.service-locked__bg {
	position: fixed;
	inset: 0;
	z-index: -1;
	background-image:
		linear-gradient(180deg, rgba(27, 27, 27, 0) var(--scrim-stop, 44%), rgba(27, 27, 27, var(--scrim-end-opacity, 1)) 100%),
		var(--hero-image);
	background-size: cover;
	background-position: center bottom;
	background-repeat: no-repeat;
	background-color: var(--color-ink);
	/* transform: scale(1.4); */
	transform-origin: center bottom;
}

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

.service-hero__inner {
	position: relative;
	z-index: 1;
	display: flex;
	flex-wrap: wrap;
	align-items: flex-end;
	justify-content: space-between;
	gap: 40px;
	padding-block: clamp(48px, 6vw, 90px);
}

/* Fades/slides in on load (service.js), the right column slightly after
   the left — matches the reference demo's own stagger. */
.service-hero__left,
.service-hero__right {
	opacity: 0;
	transform: translateY(30px);
	transition: opacity 1s ease, transform 1s ease;
}

.service-hero__right {
	transition-delay: 0.4s;
}

.service-hero__inner.is-visible .service-hero__left,
.service-hero__inner.is-visible .service-hero__right {
	opacity: 1;
	transform: translateY(0);
}

.service-hero__left {
	flex: 1 1 20rem;
}

.service-hero__right {
	flex: 0 1 31rem;
}

.service-hero__heading {
	margin: 0 0 12px;
	font-family: var(--font-display);
	font-weight: 800;
	font-size: clamp(2.25rem, 5vw, 3.75rem);
	line-height: 1.07;
	color: var(--color-white);
}

/* Global mobile h1 baseline (3.1rem) — see base.css. */
@media (max-width: 900px) {
	.service-hero__heading {
		font-size: clamp(2.25rem, 13vw, 3.1rem);
	}
}

.service-hero__subheading {
	margin: 0;
	font-family: var(--font-body);
	font-weight: 300;
	font-size: 1.625rem;
	color: var(--color-white);
}

.service-hero__intro {
	margin: 0;
	font-family: var(--font-body);
	font-weight: 300;
	font-size: 1.375rem;
	line-height: 1.45;
	text-align: right;
	color: var(--color-white);
}

/* --- Pinned (the actual scroll-jacked part) ---
   .service-pinned reserves a tall scroll distance (300vh); .service-pinned
   __sticky pins to the viewport for the duration of it via plain
   position:sticky — a real, natively-supported technique, no JS needed to
   fake the "stay in place" part. What service.js actually drives is the
   two panels' opacity/transform and .service-locked's --scrim-stop /
   --scrim-end-opacity, based on how far scrolled through that 300vh the
   user currently is (see its own top comment for the exact phase
   breakdown, ported from the reference demo's proven timing). */

.service-pinned {
	position: relative;
	height: 300vh;
}

/* Content & Creative only has one locked stat, not two — service.js
   already treats scroll progress as continuous darkening rather than a
   fade-in/swap/settle sequence for that case (see its own comment), but
   the reserved scroll distance itself was still the full 300vh built for
   the two-panel version, so the single stat just sat there doing nothing
   for most of it. The phase fractions in service.js (FADE_END etc.) are
   all relative to this height, not absolute, so shrinking it here alone
   proportionally shortens every phase together — no JS change needed to
   keep the timing shared between both versions. */
.service-pinned--single {
	height: 150vh;
}

.service-pinned__sticky {
	position: sticky;
	top: 0;
	height: 100vh;
	overflow: hidden;
}

/* Row, not a stack — the citation pill (panel B only) sits to the right
   of the stat text, bottom-aligned with it, per Figma. It was previously
   just a bare <span> straight after the <p> in the markup with no layout
   rule targeting it at all, so as an inline element after a block-level
   paragraph it fell to a new line below the text, left-aligned by
   default — not a mis-tuned attempt, just never actually built. Panel A
   has no citation, so this is a no-op there (a single flex item just
   sits at its own start edge). */
.service-pinned__panel {
	position: absolute;
	z-index: 1;
	left: 0;
	right: 0;
	bottom: clamp(48px, 6vw, 90px);
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: 24px;
}

.service-pinned__stat {
	max-width: 51.6rem;
	margin: 0;
	min-width: 0;
	font-family: var(--font-body);
	font-weight: 500;
	font-size: clamp(1.75rem, 3.4vw, 3.4375rem);
	line-height: 1.27;
	color: var(--color-white);
}

/* Frosted pill — semi-transparent fill + backdrop-filter, not Figma's
   literal opaque-white + mix-blend-mode:soft-light (that combination has
   reliably rendered as a flat, unblurred fill everywhere else it's been
   tried on this site — see .insights-badge in blog.css for the fuller
   explanation). Same look, without the fragility. */
.service-hero__citation {
	display: inline-flex;
	align-items: center;
	flex-shrink: 0;
	padding: 10px 20px;
	border-radius: 40px;
	font-family: var(--font-body);
	font-weight: 500;
	font-size: 1.125rem;
	color: var(--color-white);
	background: rgba(255, 255, 255, 0.18);
	-webkit-backdrop-filter: blur(20px);
	backdrop-filter: blur(20px);
	transition: background-color 0.2s ease;
}

/* Only ever true when this is a real link (an optional citation URL) —
   the plain-text <span> version never gets a hover-capable pointer, so
   this rule has nothing to affect there. */
a.service-hero__citation:hover,
a.service-hero__citation:focus-visible {
	background: rgba(255, 255, 255, 0.3);
}

/* Non-hover devices — checked against Figma's own mobile frame (node
   4002:22189) directly, which turns out to be a bigger structural change
   than just "drop the scroll-jack": the stat copy isn't part of the hero
   photo at all there. The photo is bounded to just the heading/subheading/
   intro paragraph and ends there; "When marketing isn't aligned…" sits
   AFTER it, as plain dark text on a plain white background, both stat
   fields reading as one continuous paragraph with no citation badge — not
   two white-on-photo panels with a McKinsey pill, which is what simply
   dropping the scroll-jack and keeping the existing markup/photo treatment
   would have produced (and which is what was here before this pass).

   .service-locked__bg (the fixed, permanently-behind-everything photo
   layer used to support the scroll-jack revealing over a persistent
   background — see its own comment) is dropped entirely here in favour of
   a normal, bounded background-image on .service-hero itself, so the
   photo naturally ends where the hero's own content does instead of
   bleeding behind every section after it. */
@media (hover: none) {
	.service-locked__bg {
		display: none;
	}

	.service-hero {
		/* A full 100vh (previously needed so the hero didn't feel
		   truncated once .service-pinned below it was dropped) read as
		   oversized on its own — 75vh keeps it a full "moment" without
		   forcing a scroll before any of the next section peeks into
		   view. align-items:center (overriding the desktop rule's own
		   flex-end) is what actually moves the heading up from the very
		   bottom edge — see .service-hero__inner's own comment. */
		min-height: 75vh;
		align-items: center;
		/* Content is centred vertically now, not pinned to the bottom, so
		   a gradient that only starts darkening at 44% left the heading
		   sitting on almost-undarkened photo (confirmed live: white text
		   over open sky on Strategy's own shot). Dark from top to bottom
		   throughout instead — still graduated (lighter up top, solid by
		   the final section) rather than a flat tint, but legible
		   wherever the centred content actually lands. */
		background-image:
			linear-gradient(180deg, rgba(27, 27, 27, 0.4) 0%, rgba(27, 27, 27, 0.6) 50%, rgba(27, 27, 27, 1) 100%),
			var(--hero-image);
		/* This box is much taller/narrower relative to these landscape
		   photos than the old bottom-pinned full-height version was, so
		   "cover" alone scales to match the box's HEIGHT exactly — with
		   zero vertical overflow left to crop, background-position's Y
		   value has nothing to act on, and the photo's own sky/horizon
		   shows in full regardless of "top"/"bottom"/"center" (confirmed
		   live on Strategy's river/forest shot). Same fix as the desktop
		   locked hero's own __bg layer uses for the same reason (its own
		   comment above) — over-zooming via a fixed vertical size forces
		   real vertical overflow to exist, so "bottom" then actually has
		   something to crop away (the sky) instead of showing the photo's
		   full, uncropped vertical extent. */
		background-size: auto 140%;
		background-position: center bottom;
		background-repeat: no-repeat;
		background-color: var(--color-ink);
	}

	/* No top padding here (unlike the previous bottom-pinned version of
	   this layout, which needed one to clear the fixed header) — content
	   is vertically centred within the hero now (.service-hero's own
	   align-items:center), and adding one-sided padding back would just
	   push it off-centre again, the opposite of what centring is for. */
	.service-hero__inner {
		flex-direction: column;
		align-items: center;
		text-align: center;
	}

	/* Both columns stack full-width and centre their own text — Figma's
	   mobile frame centres the heading/subheading/intro paragraph as one
	   block, not left-aligned heading beside a right-aligned paragraph
	   the way the desktop two-column layout reads. */
	.service-hero__left,
	.service-hero__right {
		flex: none;
		width: 100%;
	}

	.service-hero__intro {
		text-align: center;
	}

	/* Dropped entirely on mobile — no locked-scroll sequence there anyway
	   (see .service-pinned__sticky's own base rule), so this was just the
	   run-on stat text sitting between the hero and whatever centred
	   intro section actually follows (cards / pill-reveal / plain intro,
	   depending on the page). Hiding it lets the hero flow straight into
	   that instead. */
	.service-pinned,
	.service-pinned--single {
		display: none;
	}
}

/* --- 3-card section --- */

/* This is the section that actually ends the fixed photo/gradient behind
   it (see .service-locked__bg's own comment) — its white background has
   to be genuinely full-bleed, edge to edge, not just visually centred.
   .container (used everywhere else on this site, including inside this
   very section for .service-cards__inner) caps at a max-width with
   auto margins, so putting the background on a "container service-cards"
   element the way .service-hero__inner does would leave the fixed layer
   showing through in the gutters on either side at any viewport wider
   than that max-width. The background lives on this outer, unconstrained
   section instead; .service-cards__inner carries the max-width/padding
   for the actual content. */
.service-cards {
	position: relative;
	z-index: 1;
	background: var(--color-white);
}

.service-cards__inner {
	/* Narrower than the site's regular .container (1440px) on purpose —
	   with 3 fixed-height columns, the regular width made each card come
	   out close to square; this overrides it back down (wins on cascade
	   order over the shared .container rule, both being one class
	   selector, since service.css loads after base.css) so each column
	   is narrower relative to its height, closer to the reference demo's
	   own card proportions (340×420, a noticeably portrait rectangle). */
	max-width: 1180px;
	padding-block: clamp(56px, 8vw, 96px);
}

.service-cards__intro {
	max-width: 51rem;
	margin: 0 auto clamp(40px, 5vw, 64px);
	text-align: center;
}

.service-cards__heading {
	margin: 0 0 15px;
	font-family: var(--font-body);
	font-weight: 500;
	font-size: 2.5rem;
	line-height: 1.25;
	color: var(--color-ink);
}

/* Global mobile h2 baseline (2.3rem) — see base.css. */
@media (max-width: 900px) {
	.service-cards__heading {
		font-size: 2.3rem;
	}
}

.service-cards__lead {
	margin: 0;
	font-family: var(--font-body);
	font-weight: 300;
	font-size: 1.375rem;
	line-height: 1.55;
	color: var(--color-ink);
}

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

/* min-height lives on the OUTER card, not -media — -media is
   position:absolute (below) so it can't establish its parent's height
   itself; the outer card needs its own height for -media to then fill via
   inset:0, matching the old single-element card's footprint exactly. */
/* overflow:hidden clips .service-cards__card-tags' resting position (translateY
   pushes it below the card, ready to slide up on hover) — before the
   accordion restructure that clipping lived on this same element implicitly
   (tags was a child of .service-cards__card-media, which has its own
   overflow:hidden); now that tags is a sibling of -media instead (see
   template-service.php's own comment on why), it needs its own clip
   boundary here or it visibly pokes out below the card at rest. */
.service-cards__card {
	position: relative;
	overflow: hidden;
	min-height: 26.25rem;
}

/* .service-cards__card-media (not the outer .service-cards__card) carries
   the photo, the title, and (non-hover only) the expand toggle — see
   template-service.php's own comment on why: on a hover-capable device
   this inner element fills the whole card exactly as the single-element
   version used to (inset:0 below), but on a non-hover device it needs to
   be a short, bounded bar instead, with the description/tags panel below
   it sitting on a genuine white background, not this same photo stretched
   down behind them too (see the (hover: none) rules further down). */
.service-cards__card-media {
	position: absolute;
	inset: 0;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	justify-content: flex-end;
	background-size: cover;
	background-position: center;
	background-color: var(--color-ink);
}

.service-cards__card-media::before {
	content: '';
	position: absolute;
	inset: 0;
	background: linear-gradient(210deg, rgba(0, 0, 0, 0) 13%, rgba(0, 0, 0, 0.8) 84%);
	pointer-events: none;
}

/* Hover mechanic ported from the reference demo (its own .sc-h3/.sc-bottom/
   .sc-copy/.sc-buttons) — default state: heading sits low, resting just
   above the description (service.js measures the exact gap per card, same
   technique as the How We Think reveal, since a fixed guessed offset can't
   account for cards with differing description lengths); tags sit fully
   below the card's visible bottom edge, clipped by overflow:hidden above.
   On hover: heading slides up to the top, the description fades out, and
   the tags slide up into the space it left, with a short extra delay so
   they trail slightly behind the heading/description movement instead of
   all arriving at once. */
.service-cards__card-title {
	position: absolute;
	top: 30px;
	left: 30px;
	right: 30px;
	z-index: 2;
	margin: 0;
	color: var(--color-white);
	font-family: var(--font-body);
	font-weight: 500;
	font-size: 1.75rem;
	line-height: 1.25;
	transform: translateY(var(--card-title-drop, 200px));
	transition: transform 1.1s cubic-bezier(0.25, 1, 0.5, 1);
}

/* Triggered off the OUTER card, not .service-cards__card-media itself —
   .service-cards__card-bottom/-tags are siblings of -media now (see
   template-service.php's own comment on why), not descendants of it, so a
   hover selector scoped to -media alone couldn't reach them. The outer
   card and -media share the exact same visual bounds on a hover-capable
   device (see -media's own default position:absolute;inset:0 rule), so
   hovering either one is the same gesture in practice. */
.service-cards__card:hover .service-cards__card-title {
	transform: translateY(0);
}

/* Pinned to the card's bottom edge via absolute positioning (mirroring
   .service-cards__card-tags below it), not normal flow — before the
   accordion restructure this sat inside .service-cards__card-media's own
   flex column (justify-content:flex-end), which put it at the bottom for
   free; now that it's a sibling of -media instead of a child (see
   template-service.php's own comment on why), it's no longer inside that
   flex context, and -media itself is out of flow (position:absolute), so
   a plain position:relative here rendered right at the TOP of the card —
   overlapping the title instead of sitting under it. */
.service-cards__card-bottom {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 1;
	padding: 30px;
}

.service-cards__card-description {
	margin: 0;
	color: var(--color-white);
	font-family: var(--font-body);
	font-weight: 300;
	font-size: 1.375rem;
	line-height: 1.36;
	transition: opacity 0.25s ease;
}

.service-cards__card:hover .service-cards__card-description {
	opacity: 0;
}

.service-cards__card-tags {
	position: absolute;
	left: 30px;
	right: 30px;
	bottom: 30px;
	z-index: 1;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 8px;
	transform: translateY(calc(100% + 30px));
	transition: transform 1.1s cubic-bezier(0.25, 1, 0.5, 1);
}

.service-cards__card:hover .service-cards__card-tags {
	transform: translateY(0);
	transition: transform 1.1s cubic-bezier(0.25, 1, 0.5, 1) 0.14s;
}

.service-cards__card-tag {
	display: inline-block;
	margin: 0;
	max-width: 100%;
	padding: 11px 20px;
	border: 1px solid rgba(255, 255, 255, 0.5);
	border-radius: 999px;
	background: transparent;
	-webkit-backdrop-filter: blur(40px);
	backdrop-filter: blur(40px);
	color: var(--color-white);
	font-family: var(--font-body);
	font-weight: 600;
	font-size: 1.125rem;
}

/* Click-to-expand trigger, non-hover devices only — see the (hover: none)
   block below and template-service.php's own comment on this element. */
.service-cards__card-toggle {
	display: none;
}

@media (prefers-reduced-motion: reduce) {
	.service-cards__card-title,
	.service-cards__card-tags {
		transition: none;
	}
}

@media (max-width: 900px) {
	.service-cards__grid {
		grid-template-columns: 1fr;
	}
}

/* Click-to-expand accordion, non-hover devices — a touch/no-hover input has
   no gesture equivalent to "hover the card", so the same tag-reveal content
   becomes a real interaction instead (service.js's own click handler toggles
   .is-expanded), on a plain white background below a short image bar rather
   than an overlay on the photo — matches the design's own mobile treatment,
   and is what actually makes the tags reachable at all here (the hover
   version has no touch/keyboard equivalent otherwise). Sized/laid out from
   scratch rather than reusing the hover version's absolute-overlay
   positioning, which assumes a tall fixed-height card that doesn't apply
   once the expanding content needs to push real layout height. */
@media (hover: none) {
	/* Accordion rows sit flush against each other, divided by a plain rule
	   (matching .service-hwt/.service-capabilities' own list-divider
	   convention elsewhere in this file) rather than the grid's desktop
	   card gap, which read as separate floating cards rather than one
	   continuous accordion list. */
	.service-cards__grid {
		gap: 0;
	}

	.service-cards__card {
		min-height: 0;
		/* Chrome's scroll anchoring otherwise re-targets the scroll
		   position to whichever nearby element it thinks moved — since
		   expanding a card changes ITS OWN height by hundreds of pixels,
		   the browser was compensating by scrolling further down after
		   every tap, landing on the newly-revealed tags instead of the
		   top of the card that was just opened. */
		overflow-anchor: none;
		border-bottom: 1px solid var(--color-ink);
	}

	.service-cards__card:last-child {
		border-bottom: none;
	}

	.service-cards__card-media {
		position: relative;
		inset: auto;
		min-height: 0;
		height: 9rem;
		flex-direction: row;
		align-items: center;
		justify-content: space-between;
		gap: 16px;
		padding-inline: 20px;
	}

	.service-cards__card-title {
		position: static;
		flex: 1 1 auto;
		min-width: 0;
		transform: none;
		font-size: 1.375rem;
	}

	.service-cards__card-toggle {
		display: flex;
		align-items: center;
		justify-content: center;
		position: static;
		flex-shrink: 0;
		width: 2.75rem;
		height: 2.75rem;
		/* base.css only zeroes out <button>'s BORDER, not its UA-default
		   padding (measured live at 1px 6px in Chrome) — with box-sizing:
		   border-box that ate into this button's declared 44×44 box
		   asymmetrically (12px off the width from left+right padding, only
		   2px off the height), leaving a 32×42 content box for the img
		   below to fill instead of a proper 44×44 square — squashing the
		   circle into an oval. .closing-statement__arrow (home.css) is the
		   same arrow-circle-as-a-button pattern elsewhere in this codebase
		   and already zeroes this out; missed here originally. */
		padding: 0;
		/* Source icon points down — rotated to point right at rest (an
		   "expand this way" cue), rotating back to its natural down
		   orientation once opened. */
		transform: rotate(-90deg);
		transition: transform 0.3s ease;
	}

	.service-cards__card-toggle img {
		width: 100%;
		height: 100%;
	}

	/* Same tap-triggered-:hover problem as .service-cards__card-description
	   above, just via .arrow-circle's own :hover/:focus-visible crossfade
	   (base.css) — a tap leaves the toggle showing its desktop filled-in
	   hover state permanently instead of the plain outline icon, since
	   there's no mouseleave on a touch device to ever clear it. Higher
	   specificity (3 classes + pseudo vs base.css's 1 class + pseudo) wins
	   this outright rather than needing source-order help. */
	.service-cards__card-toggle.arrow-circle:hover::after,
	.service-cards__card-toggle.arrow-circle:focus-visible::after {
		opacity: 0;
	}

	.service-cards__card.is-expanded .service-cards__card-toggle {
		transform: rotate(0deg);
	}

	/* Collapsed by default (max-height:0 + hidden overflow) — the OPEN
	   height is deliberately not set here at all. service.js measures
	   each panel's real scrollHeight and transitions to that exact pixel
	   value inline instead of a fixed CSS guess: a guess big enough to fit
	   any card's content is much taller than most cards actually need,
	   and a max-height transition only moves visibly once the value
	   passes below the content's real height — so the box would sit open
	   for most of the animation then "snap" through the real distance
	   right at the end (and the reverse shape closing). Measuring removes
	   the guess entirely. Vertical padding only applies once expanded
	   (horizontal stays always, see each element's own rule below) —
	   max-height can only shrink the CONTENT area, never the padding
	   itself, so a fixed top/bottom padding here would leave that much of
	   the panel visibly peeking out between cards even at max-height:0
	   (confirmed live: a border-box max-height:0 with 16px+24px of
	   padding still rendered 40px tall, not 0). */
	.service-cards__card-bottom,
	.service-cards__card-tags {
		box-sizing: border-box;
		max-height: 0;
		padding-top: 0;
		padding-bottom: 0;
		overflow: hidden;
		/* padding-top/padding-bottom transition ALONGSIDE max-height,
		   deliberately — a max-height:0 box can't actually render at 0
		   while its own padding is still applied (confirmed live just
		   above: a border-box max-height:0 with 16+24px of padding still
		   rendered 40px tall), so if padding snapped to its full value
		   instantly while max-height was still easing down, the box would
		   visibly get "stuck" at exactly its own padding height for most
		   of the close animation, only reaching 0 at the very last instant
		   once padding also let go — exactly what this looked like before
		   (confirmed live). Both animating together is what lets the box
		   actually reach 0 smoothly. service.js's own expand()/collapse()
		   measure the OPEN target height before this class (and its
		   padding) is ever applied, specifically so scrollHeight is never
		   read while padding is mid-transition — see its own comment for
		   why that read would otherwise be unreliable. */
		transition: max-height 0.35s ease, padding-top 0.35s ease, padding-bottom 0.35s ease;
	}

	.service-cards__card.is-expanded .service-cards__card-bottom {
		padding-top: 20px;
	}

	.service-cards__card.is-expanded .service-cards__card-tags {
		padding-top: 16px;
		padding-bottom: 24px;
	}

	.service-cards__card-bottom {
		position: static;
		z-index: auto;
		padding-inline: 20px;
		background: var(--color-white);
	}

	.service-cards__card-description {
		color: var(--color-ink);
	}

	/* Touch browsers commonly apply a "sticky :hover" to the tapped element
	   (and its ancestors) after a tap, with no mouseleave to ever clear it
	   — .service-cards__card:hover .service-cards__card-description
	   {opacity:0} (the hover version's fade-out, unconditional, above) was
	   winning that specificity fight (0,0,3,0 beats the 0,0,1,0 override
	   right above) as soon as a card's toggle was tapped, leaving the
	   description invisible even though it's really still there above the
	   tags — this is what actually reads as "the content isn't showing
	   when I open it". Re-declaring the exact same selector here, after it
	   in source order, is the only way to win that specificity tie back. */
	.service-cards__card:hover .service-cards__card-description {
		opacity: 1;
	}

	.service-cards__card-tags {
		position: static;
		left: auto;
		right: auto;
		bottom: auto;
		z-index: auto;
		flex-direction: column;
		transform: none;
		padding-inline: 20px;
		background: var(--color-white);
	}

	/* Plain bullet points here instead of the desktop pill shape — the
	   pills' own padding/gap took up considerably more vertical room per
	   tag once wrapped into a column list like this, for real content that
	   doesn't need the extra visual weight a standalone pill implies.
	   list-style-position:inside keeps the bullet flush with the text
	   instead of needing extra reserved space to its left the way the
	   default "outside" position does. */
	.service-cards__card-tag {
		display: list-item;
		list-style-type: disc;
		list-style-position: inside;
		padding: 0;
		border: none;
		border-radius: 0;
		background: none;
		color: var(--color-ink);
		-webkit-backdrop-filter: none;
		backdrop-filter: none;
	}

	@media (prefers-reduced-motion: reduce) {
		.service-cards__card-toggle,
		.service-cards__card-bottom,
		.service-cards__card-tags {
			transition: none;
		}
	}
}

/* --- How we think --- */
/* Same hover-driven text slot as the reference demo's own "How We Think"
   section (demo-main.js) — heights are computed and set inline by
   service.js from each item's actual rendered content (see its own
   comment), not guessed here, since a fixed CSS height can't account for
   items of differing copy length without clipping the longer ones or
   leaving dead space under the shorter ones.

   Same outer/inner split as .service-cards, for the same reason: this
   section still needs to fully cover .service-locked__bg's fixed photo
   layer, which stays behind the whole rest of the page, not just the
   section right after it (see .service-locked__bg's own comment) — a
   background on a "container service-hwt" element the way this used to
   be written would only paint the centred content box, leaving the fixed
   layer visible in the gutters at any viewport wider than the max-width. */

.service-hwt {
	background: var(--color-white);
}

.service-hwt__inner {
	padding-block: clamp(56px, 8vw, 96px);
	display: grid;
	grid-template-columns: 1fr;
	gap: 64px;
}

@media (min-width: 900px) {
	.service-hwt__inner {
		/* Gap comes out of the fr tracks' shared space, so a bigger gap
		   alone would shrink both columns proportionally, right included.
		   Left's share is nudged down (2fr → 1.75fr) to absorb the extra
		   gap on its own, so the right column ends up a bit wider than
		   before, with enough margin that its longest link heading
		   ("Alignment before activation.") doesn't wrap to 2 lines — a
		   bigger gap value tried earlier trimmed the right column too far
		   at some widths and caused exactly that. */
		grid-template-columns: 1.75fr 3fr;
		gap: 110px;
	}
}

/* Heading stays pinned to the top; the reveal box below it is pushed down
   to the bottom instead (margin-top:auto eats all the leftover space in
   the column) so ITS bottom edge lines up with the nav's last link,
   without dragging the heading down with it. gap provides a guaranteed
   minimum heading-to-reveal spacing that the auto margin adds on top of,
   rather than the fixed spacing living on the reveal box's own margin,
   which would double up with the auto margin instead of coexisting with
   it. .service-hwt__inner keeps the grid default (stretch), so this
   column's box is exactly as tall as the nav column on desktop — on
   mobile's single-column layout the two sit in separate rows with no
   shared height to push into, so the auto margin naturally does nothing
   there and gap alone provides the spacing. */
.service-hwt__intro {
	display: flex;
	flex-direction: column;
	height: 100%;
	gap: 24px;
}

.service-hwt__heading {
	margin: 0;
	font-family: var(--font-body);
	font-weight: 700;
	font-size: clamp(2.75rem, 5.6vw, 5.5125rem);
	line-height: 1.07;
	color: var(--color-ink);
}

.service-hwt__reveal {
	overflow: hidden;
}

.service-hwt__reveal-track {
	transition: transform 0.4s ease;
}

.service-hwt__reveal-text {
	overflow: hidden;
}

.service-hwt__reveal-lead {
	margin: 0 0 12px;
	font-family: var(--font-body);
	font-weight: 600;
	font-size: 1.25rem;
	line-height: 1.6;
	color: var(--color-ink);
}

.service-hwt__reveal-body {
	margin: 0;
	font-family: var(--font-body);
	font-weight: 400;
	font-size: 1.25rem;
	line-height: 1.6;
	color: var(--color-ink);
}

@media (prefers-reduced-motion: reduce) {
	.service-hwt__reveal-track {
		transition: none;
	}
}

.service-hwt__nav {
	display: flex;
	flex-direction: column;
}

.service-hwt__link {
	display: block;
	width: 100%;
	padding-block: 2.5rem;
	padding-left: 0;
	border: none;
	background: none;
	text-align: left;
	cursor: pointer;
	font-family: var(--font-body);
	font-weight: 600;
	font-size: 3.1rem;
	line-height: 1;
	color: var(--color-ink);
	transition: color 0.35s ease, padding-left 0.35s ease;
}

/* Bleeds to the right edge of the viewport regardless of nesting depth —
   .service-hwt__nav's own right edge sits flush with .container's inner
   right edge (it's a grid item stretched to fill its column), so the
   distance from there to the true viewport edge is exactly the gutter
   plus however much outer margin .container currently has centering
   itself (zero once the viewport is narrower than --container-width,
   hence the max(0px, ...) — without it this would flip sign and shrink
   the divider instead of extending it on smaller screens). */
.service-hwt__divider {
	height: 1px;
	background: var(--color-ink);
	margin-right: calc(-1 * var(--gutter) - max(0px, (100vw - var(--container-width)) / 2));
}

.service-hwt[data-active] .service-hwt__link {
	color: rgba(27, 27, 27, 0.3);
}

/* Matches the reference demo's hwt-item hover exactly: hovered link goes
   orange with a small rightward shift (padding-left, not transform —
   same technique the demo uses), everything else greys out via the rule
   above. This has to out-specify that rule (0,0,3,0: .service-hwt
   [data-active] .service-hwt__link) or the active link stays grey too —
   an earlier version used a plain `.service-hwt__link.is-active` selector
   (0,0,2,0), which lost that specificity fight silently: the class was
   being toggled correctly by service.js the whole time, but this rule
   never actually won, so nothing ever visibly changed on hover. */
.service-hwt[data-active] .service-hwt__link.is-active {
	color: var(--color-accent);
	padding-left: 24px;
}

/* Non-hover devices — there's no hover gesture to drive the single shared
   reveal panel above, so instead every heading gets its own text sitting
   directly beneath it, all visible at once (service.js's own comment on
   why it skips its whole measure/slide mechanism here). Forced regardless
   of viewport width, not just under the 900px breakpoint — a large
   touchscreen has no hover either, and the two-column hover layout makes
   no more sense there than it does on a phone.

   .service-hwt__intro/__nav and __reveal/__reveal-track collapse via
   display:contents so their children — the heading, each nav link/divider,
   and each reveal-text — all become direct items of the single-column
   .service-hwt__inner grid, letting `order` interleave link/text/divider
   triples across what used to be two separate DOM branches, without
   touching markup or duplicating content (same technique as the homepage
   specialties CTA reorder). nth-of-type still resolves against each
   element's real DOM parent even once contents-flattened, so this keeps
   working. Links lose their click/hover behaviour entirely (cursor,
   pointer-events, service.js's own tabindex="-1") since there's nothing
   left for them to trigger — they're just headings now. Bounded to 8
   items, comfortably more than this section has ever held. */
@media (hover: none) {
	.service-hwt__inner {
		grid-template-columns: 1fr;
		gap: 0;
	}

	.service-hwt__intro,
	.service-hwt__nav,
	.service-hwt__reveal,
	.service-hwt__reveal-track {
		display: contents;
	}

	.service-hwt__heading {
		order: 0;
		margin-bottom: 16px;
		/* The shared clamp() (min 2.75rem) was sized for desktop's much
		   wider two-column layout — at a phone-width single column
		   "How we think." wrapped to two lines; this shrinks it to fit
		   in one (now matching the site's global mobile h2 baseline,
		   2.3rem — see base.css). That alone wasn't enough, though: the
		   line break in "How we / think." isn't wrapping at all, it's a
		   hardcoded <br> baked into the ACF field content itself (nl2br()
		   in template-service.php keeps the manual break the same on
		   every layout) — sensible on desktop's wide two-line treatment,
		   wrong here, so it's hidden below rather than fought with
		   font-size alone. */
		font-size: 2.3rem;
	}

	.service-hwt__heading br {
		display: none;
	}

	.service-hwt__link {
		width: auto;
		padding-block: 1.75rem 0;
		margin-bottom: 12px;
		font-size: 1.75rem;
		line-height: 1.15;
		cursor: default;
		pointer-events: none;
		transition: none;
	}

	.service-hwt__reveal-text {
		overflow: visible;
	}

	.service-hwt__reveal-lead,
	.service-hwt__reveal-body {
		font-size: 1.0625rem;
	}

	.service-hwt__divider {
		margin-top: 1.75rem;
		margin-right: 0;
	}

	.service-hwt__link:nth-of-type(1) { order: 1; }
	.service-hwt__reveal-text:nth-of-type(1) { order: 2; }
	.service-hwt__divider:nth-of-type(1) { order: 3; }

	.service-hwt__link:nth-of-type(2) { order: 4; }
	.service-hwt__reveal-text:nth-of-type(2) { order: 5; }
	.service-hwt__divider:nth-of-type(2) { order: 6; }

	.service-hwt__link:nth-of-type(3) { order: 7; }
	.service-hwt__reveal-text:nth-of-type(3) { order: 8; }
	.service-hwt__divider:nth-of-type(3) { order: 9; }

	.service-hwt__link:nth-of-type(4) { order: 10; }
	.service-hwt__reveal-text:nth-of-type(4) { order: 11; }
	.service-hwt__divider:nth-of-type(4) { order: 12; }

	.service-hwt__link:nth-of-type(5) { order: 13; }
	.service-hwt__reveal-text:nth-of-type(5) { order: 14; }
	.service-hwt__divider:nth-of-type(5) { order: 15; }

	.service-hwt__link:nth-of-type(6) { order: 16; }
	.service-hwt__reveal-text:nth-of-type(6) { order: 17; }
	.service-hwt__divider:nth-of-type(6) { order: 18; }

	.service-hwt__link:nth-of-type(7) { order: 19; }
	.service-hwt__reveal-text:nth-of-type(7) { order: 20; }
	.service-hwt__divider:nth-of-type(7) { order: 21; }

	.service-hwt__link:nth-of-type(8) { order: 22; }
	.service-hwt__reveal-text:nth-of-type(8) { order: 23; }
	.service-hwt__divider:nth-of-type(8) { order: 24; }
}

/* --- Intro (Activation only) ---
   Plain heading + paragraph, no scroll behaviour — "Not unique, not
   different." sits before the circle-reveal lock as ordinary content,
   not part of that locked sequence. */

.service-intro {
	background: var(--color-white);
	padding-block: clamp(56px, 8vw, 96px);
}

.service-intro__inner {
	max-width: 66.75rem;
}

.service-intro__eyebrow {
	margin: 0 0 20px;
	font-family: var(--font-body);
	font-weight: 400;
	font-size: 1.6875rem;
	line-height: 1.3;
	color: var(--color-ink);
}

.service-intro__text {
	font-family: var(--font-body);
	font-weight: 500;
	font-size: 2.5rem;
	line-height: 1.375;
	letter-spacing: -0.02em;
	color: var(--color-ink);
}

/* Smaller than the site's usual mobile h2 baseline (2.3rem, see base.css)
   — requested specifically for this paragraph, matching the size other
   similarly long/large paragraphs on this template use elsewhere (e.g.
   .service-pinned__stat's own clamp() bottoms out at the same 1.75rem). */
@media (max-width: 900px) {
	.service-intro__text {
		font-size: 1.75rem;
	}
}

/* --- Circle Reveal (Activation only) ---
   Scroll-lock, found via a separate Figma component (3296:16853) showing
   its 3 states after the single "Expanded" frame turned out not to
   contain everything — the black/orange circles sliding together was
   right, but the "two bits of content" the user described turned out to
   be two flanking text blocks (heading + body each), not the big heading
   above, which is genuinely static.
   Phase 1: the black and orange circles slide in from opposite edges of
   the viewport and settle into a slight, deliberate overlap (not fully
   coincident) — the black circle carries both mix-blend-mode:hard-light
   (matching Figma's own fill) AND explicit opacity, so the overlap
   region reads as visibly distinct from either circle's own solid area,
   like a Venn diagram (confirmed against Figma's own final-state
   screenshot, not guessed).
   Phase 2: the two text blocks — one flanking each circle — fade and
   slide in from further out on their own side, finishing flush next to
   the circles.
   Same position:sticky + tall-reserve technique as the hero and Pill
   Reveal; values set directly via inline custom properties every scroll
   tick, no CSS transition, for the same 1:1 scroll-coupling reason
   documented on Pill Reveal. Heading/stage sit in a centred flex column
   rather than pinned to Figma's literal pixel coordinates, since those
   wouldn't hold up across different viewport heights. */

.service-circlereveal {
	position: relative;
	height: 250vh;
	background: var(--color-white);
}

.service-circlereveal__sticky {
	position: sticky;
	top: 0;
	height: 100vh;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: clamp(40px, 6vw, 90px);
}

.service-circlereveal__heading {
	margin: 0;
	text-align: center;
	font-family: var(--font-body);
	font-weight: 500;
	font-size: clamp(1.75rem, 4vw, 2.8125rem);
	line-height: 1.2;
	color: var(--color-ink);
}

/* Purely a visual cue for the "gap" this section's own 250vh scroll-jack
   track leaves once the sticky content above releases and natural scroll
   resumes — not a link to any specific next section (that varies by
   page), so it's aria-hidden and unclickable. --gap-arrow-progress is set
   by service.js's existing onScroll (same 0-1 "p" it already computes),
   ramping opacity up only in the last stretch of the scroll-jack, right
   before release — see GAP_ARROW_START there. */
.service-circlereveal__gap-arrow {
	position: absolute;
	bottom: clamp(32px, 6vw, 64px);
	left: 50%;
	transform: translateX(-50%);
	width: 3.6875rem;
	height: 3.6875rem;
	opacity: var(--gap-arrow-progress, 0);
	pointer-events: none;
}

.service-circlereveal__gap-arrow img {
	display: block;
	width: 100%;
	height: 100%;
}

/* Matches .service-intro__text's own mobile size (1.75rem) — the two sit
   back to back as this page scrolls, so they're sized to match rather
   than the site's usual mobile h2 baseline (2.3rem, see base.css). */
@media (max-width: 900px) {
	.service-circlereveal__heading {
		font-size: 1.75rem;
	}
}

.service-circlereveal__stage {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: clamp(56px, 9vw, 150px);
	width: 100%;
}

/* Fluid (17vw reaches the 15.25rem max right around 1440px, the width
   these were originally fixed at) — below that, the circles/flanking
   text (.service-circlereveal__text, same reasoning) were a fixed
   combined width that stopped fitting the viewport on a smaller desktop
   screen, overflowing off one or both sides instead of shrinking with
   everything else in the stage. */
.service-circlereveal__circles {
	position: relative;
	width: clamp(8rem, 17vw, 15.25rem);
	height: clamp(8rem, 17vw, 15.25rem);
	flex-shrink: 0;
}

.service-circlereveal__circle {
	position: absolute;
	top: 0;
	left: 50%;
	width: clamp(8rem, 17vw, 15.25rem);
	height: clamp(8rem, 17vw, 15.25rem);
	border-radius: 50%;
}

/* z-index matters here, not just decorative — black needs to paint ON
   TOP of orange for its opacity to have any visible effect. With orange
   on top instead (the default without an explicit z-index, since it
   comes second in the markup), orange is fully opaque and would just
   completely hide black underneath at the overlap, making the whole
   point of giving black an opacity invisible. */
.service-circlereveal__circle--black {
	z-index: 2;
	background: var(--color-ink);
	opacity: 0.85;
	mix-blend-mode: hard-light;
	/* The 50vw term alone only gets the circle's CENTER to the viewport
	   edge, not the circle itself — with a 7.625rem radius, that left
	   ~2.6rem of it still visibly peeking in at rest. +9rem covers the
	   radius plus a margin, without touching the final resting offset
	   (still exactly 5rem once circle-progress reaches 1). */
	transform: translateX(calc(-50% - 5rem - (50vw + 9rem) * (1 - var(--circle-progress, 0))));
}

.service-circlereveal__circle--orange {
	z-index: 1;
	background: var(--color-accent);
	transform: translateX(calc(-50% + 5rem + (50vw + 9rem) * (1 - var(--circle-progress, 0))));
}

.service-circlereveal__text {
	/* Narrower, not repositioned — each block keeps its own text-align
	   (left block stays left-aligned, right stays right-aligned), so
	   shrinking max-width pulls the inner edge away from the circles
	   while the outer edge holds its place, opening up more of the gap
	   the stage's own flex gap alone wasn't providing.
	   Fluid for the same reason as .service-circlereveal__circles just
	   above (27vw reaches this same 24.4rem max around 1440px) — a fixed
	   max-width here was part of the same smaller-desktop overflow. */
	max-width: clamp(12rem, 27vw, 24.4rem);
	opacity: var(--text-progress, 0);
}

.service-circlereveal__text--left {
	text-align: left;
	transform: translateX(calc(-1 * (1 - var(--text-progress, 0)) * 60px));
}

.service-circlereveal__text--right {
	text-align: right;
	transform: translateX(calc((1 - var(--text-progress, 0)) * 60px));
}

.service-circlereveal__text-heading {
	margin: 0 0 15px;
	font-family: var(--font-body);
	font-weight: 600;
	font-size: 1.5625rem;
	line-height: 1.6;
	color: var(--color-ink);
}

.service-circlereveal__text-body {
	margin: 0;
	font-family: var(--font-body);
	font-size: 1.375rem;
	line-height: 1.636;
	color: var(--color-ink);
}

@media (prefers-reduced-motion: reduce) {
	.service-circlereveal {
		height: auto;
	}

	.service-circlereveal__sticky {
		position: static;
		/* Without these the circles/text are stuck at their CSS defaults
		   (var(--circle-progress, 0) etc) — i.e. off-screen and invisible
		   — since service.js's own scroll handler (the only thing that
		   ever sets them) still isn't running here either. */
		--circle-progress: 1;
		--text-progress: 1;
	}

	.service-circlereveal__gap-arrow {
		display: none;
	}
}

/* Non-hover devices — no scroll-jack gesture to offer (same reasoning as
   .service-pinned above; see service.js's own comment on why its scroll
   handler doesn't run here). The circle Venn-diagram animation this
   section is built around doesn't translate to a static screen, and
   rather than restructure it into a static fallback, the whole section
   is dropped on mobile — explicitly requested, not the site's usual
   "keep the content, lose the animation" treatment for these scroll-lock
   sections (compare .service-pinned/.service-pillreveal, which do keep
   theirs). */
@media (hover: none) {
	.service-circlereveal {
		display: none;
	}
}

/* --- Showcase (Activation only) ---
   "Activating with Invanity" — centred heading/intro, then a row of
   photo cards (Figma node 3296:16573). Redesigned from the original
   divided-list treatment (see git history) to match this Figma frame:
   each card's photo is permanent (not hover-revealed, unlike the
   homepage's Making It Happen v2 cards), and a card only crossfades to
   a video on hover/focus if one is set (inc/service.php's own field
   instructions). Mobile drops the photo/video entirely in favour of a
   plain divided list of labels — same fallback, same reasoning
   (:hover has no touch equivalent), as making-it-happen-v2.php/home.css. */

.service-showcase {
	background: var(--color-white);
	padding-block: clamp(56px, 8vw, 96px);
}

.service-showcase__intro {
	max-width: 44.875rem;
	margin-inline: auto;
	text-align: center;
}

.service-showcase__heading {
	margin: 0 0 15px;
	font-family: var(--font-body);
	font-weight: 500;
	font-size: 2.5rem;
	line-height: 1.25;
	color: var(--color-ink);
}

/* Global mobile h2 baseline (2.3rem) — see base.css. */
@media (max-width: 900px) {
	.service-showcase__heading {
		font-size: 2.3rem;
	}
}

.service-showcase__lead {
	margin: 0;
	font-family: var(--font-body);
	font-weight: 300;
	font-size: 1.375rem;
	line-height: 1.55;
	color: var(--color-ink);
}

.service-showcase__grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 24px;
	max-width: 66.625rem;
	margin: clamp(40px, 5vw, 64px) auto 0;
}

.service-showcase__card {
	position: relative;
	display: flex;
	align-items: flex-end;
	aspect-ratio: 340 / 420;
	padding: 30px;
	overflow: hidden;
	background: var(--color-ink);
}

.service-showcase__card-photo,
.service-showcase__card-video {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Permanent on the photo — always shown, unlike Making It Happen v2's
   hover-revealed one. Opacity 0 by default on the video, which sits on
   top of the photo, so hover just needs to fade the video in; nothing
   needs to fade out underneath it. */
.service-showcase__card-video {
	opacity: 0;
	transition: opacity 1s ease-in-out;
}

.service-showcase__card:hover .service-showcase__card-video,
.service-showcase__card:focus-visible .service-showcase__card-video {
	opacity: 1;
}

/* Permanent scrim, not hover-triggered — Figma's photo already reads as
   darkened at rest (unlike Making It Happen v2, where the card starts
   plain off-white and the photo/gradient only fade in on hover), so this
   needs to sit over the photo AND the video both, all the time, for the
   label to stay legible against whichever of the two is showing. */
.service-showcase__card-scrim {
	position: absolute;
	inset: 0;
	background: linear-gradient(180deg, rgba(27, 27, 27, 0) 43%, rgba(27, 27, 27, 0.8) 100%);
}

.service-showcase__card-label {
	position: relative;
	z-index: 1;
	margin: 0;
	font-family: var(--font-body);
	font-weight: 500;
	font-size: 1.75rem;
	line-height: 1.25;
	color: var(--color-white);
}

@media (max-width: 900px) {
	.service-showcase__intro {
		max-width: none;
		text-align: left;
	}

	/* Photos stay, stacked in one column instead of 3 across — just a
	   shorter box per card (16:9 rather than the desktop portrait crop)
	   so three of them in a row don't add up to too much scrolling. */
	.service-showcase__grid {
		grid-template-columns: 1fr;
		gap: 16px;
	}

	.service-showcase__card {
		aspect-ratio: 16 / 9;
	}

	/* No touch equivalent for :hover to reveal a video from — dropped on
	   mobile entirely rather than left as a dead hover target, same
	   reasoning as the desktop rule this overrides. */
	.service-showcase__card-video {
		display: none;
	}
}

/* --- Pill Reveal (Content & Creative only) ---
   A second scroll-lock after the hero's, with two stages in sequence
   inside the SAME locked scroll: heading 1/subheading 1 sit still
   throughout, a connector line draws downward, a dark pill bar fades in
   — scroll stays locked — a second connector line draws down, then
   heading 2/subheading 2 fade in, and only then does scroll finally
   release. Same position:sticky + tall-reserve technique as
   .service-pinned. Values are set directly via inline custom properties
   every scroll tick, no CSS transition, matching the hero's own 1:1
   scroll-coupling — an eased transition would fight the scroll position
   instead of tracking it. */

.service-pillreveal {
	position: relative;
	height: 300vh;
	background: var(--color-white);
}

.service-pillreveal__sticky {
	position: sticky;
	top: 0;
	height: 100vh;
	overflow: hidden;
	display: flex;
	align-items: center;
}

.service-pillreveal__inner {
	text-align: center;
}

/* Same "fill the scroll-jack gap" cue as .service-circlereveal__gap-arrow
   above — see its own comment for the full reasoning. --gap-arrow-progress
   is set by service.js's existing Pill Reveal onScroll. */
.service-pillreveal__gap-arrow {
	position: absolute;
	bottom: clamp(32px, 6vw, 64px);
	left: 50%;
	transform: translateX(-50%);
	width: 3.6875rem;
	height: 3.6875rem;
	opacity: var(--gap-arrow-progress, 0);
	pointer-events: none;
}

.service-pillreveal__gap-arrow img {
	display: block;
	width: 100%;
	height: 100%;
}

.service-pillreveal__heading {
	margin: 0;
	font-family: var(--font-body);
	font-weight: 500;
	font-size: 2.5rem;
	line-height: 1.25;
	color: var(--color-ink);
}

/* Exception to the site's global mobile h2 baseline (2.3rem, see
   base.css) — requested smaller specifically for this section: two of
   these headings stack in sequence on the same screen (this one, plus
   .service-pillreveal__stage2's own), so the standard size read as too
   heavy repeated back to back. */
@media (max-width: 900px) {
	.service-pillreveal__heading {
		font-size: 1.9rem;
	}
}

.service-pillreveal__subheading {
	margin: 12px 0 0;
	font-family: var(--font-body);
	font-weight: 400;
	font-size: 1.125rem;
	color: var(--color-ink);
}

.service-pillreveal__connector {
	display: flex;
	flex-direction: column;
	align-items: center;
	margin: 24px auto 0;
}

.service-pillreveal__connector-line {
	width: 1px;
	background: var(--color-ink);
	height: var(--connector-height, 0px);
}

.service-pillreveal__connector-dot {
	width: 8px;
	height: 8px;
	margin-top: -1px;
	border-radius: 50%;
	background: var(--color-ink);
	opacity: var(--connector-dot-opacity, 0);
}

/* Fixed pill width/padding, not content-driven — Figma actually fixes
   every pill at 220px regardless of label length ("Demand Capture" has
   noticeably less padding than "Education" there). That works for this
   exact 4-item set but not for an arbitrary repeater, where a long label
   would overflow a hardcoded width — padding-based sizing here instead,
   generous enough to read as substantial even for a short label. */
.service-pillreveal__bar {
	display: inline-flex;
	align-items: center;
	gap: 15px;
	margin-top: 24px;
	padding: 16px 14px;
	border-radius: 55px;
	background: var(--color-ink);
	opacity: var(--bar-opacity, 0);
	transform: translateY(var(--bar-shift, 20px));
}

.service-pillreveal__pill {
	padding: 18px 34px;
	border-radius: 36px;
	background: var(--color-white);
	color: var(--color-ink);
	font-family: var(--font-body);
	font-weight: 500;
	font-size: 1.25rem;
	white-space: nowrap;
}

/* Stage 2 — second connector (#service-pillreveal-connector-2 in the
   template) reuses .service-pillreveal__connector/-line/-dot as-is, each
   instance driven independently since service.js sets the custom
   properties on that specific element, not the shared class. This
   wrapper just fades/settles heading 2 + subheading 2 in the same way
   .service-pillreveal__bar does for the pills. */
.service-pillreveal__stage2 {
	margin-top: 24px;
	opacity: var(--stage2-opacity, 0);
	transform: translateY(var(--stage2-shift, 20px));
}

/* The pill bar's fixed per-pill padding (nowrap labels, no max-width) is
   wider than the viewport at this size and was overflowing off the right
   edge — width-gated, not hover-gated, since it happens on any narrow
   screen regardless of input type (this section's own hover-capable
   scroll-jack fades the same fixed-width bar in unchanged, so a narrow
   hover-capable viewport hits this too). max-width:100% + overflow-x:auto
   on the bar itself (not a wrapper) keeps its pill/rounded-ends styling
   intact while letting it scroll horizontally instead of clipping. */
@media (max-width: 900px) {
	/* A 2×2 grid instead of one scrolling row — 4 fixed labels ("Demand
	   Capture" included) never fit edge to edge at a legible size on a
	   phone, and a horizontally-scrolling bar with a label cut off
	   mid-word at rest read as broken rather than "swipe for more". Grid
	   (not flex-wrap) so it wraps to exactly 2 per row regardless of each
	   label's own width, rather than depending on them happening to add
	   up right — width:fit-content keeps the whole thing sized to its own
	   content instead of stretching the bar edge-to-edge, so it still
	   reads as one compact pill-shaped group, just 2 rows tall now. */
	.service-pillreveal__bar {
		display: grid;
		grid-template-columns: repeat(2, auto);
		gap: 8px;
		width: fit-content;
		max-width: 100%;
		margin-inline: auto;
		padding: 10px;
		/* Desktop's 55px suited one long, fully-rounded horizontal pill —
		   on a roughly square 2×2 block that read as over-rounded. */
		border-radius: 25px;
	}

	/* Smaller too — Figma's fixed desktop padding/size for these 4 labels
	   was noticeably wider than the screen. */
	.service-pillreveal__pill {
		padding: 8px 14px;
		font-size: 0.8125rem;
		text-align: center;
	}
}

@media (prefers-reduced-motion: reduce) {
	.service-pillreveal {
		height: auto;
	}

	.service-pillreveal__sticky {
		position: static;
		/* Without these every connector/bar/stage2 element is stuck at its
		   CSS default (var(--bar-opacity, 0) etc) — i.e. invisible — since
		   service.js's own scroll handler never runs to set them here
		   either. 44px matches LINE_HEIGHT in service.js — each
		   connector's fully-drawn length. */
		--connector-height: 44px;
		--connector-dot-opacity: 1;
		--bar-opacity: 1;
		--bar-shift: 0px;
		--stage2-opacity: 1;
		--stage2-shift: 0px;
	}

	.service-pillreveal__gap-arrow {
		display: none;
	}
}

/* Non-hover devices — no scroll-jack gesture to offer (same reasoning as
   .service-pinned/.service-circlereveal above; see service.js's own
   comment on why its scroll handler doesn't run here), so both connectors,
   the pill bar and stage 2 just render directly at their settled end
   state, the same values the reduced-motion fallback above uses. Forced
   regardless of viewport width, same as the other locked sections. No
   mobile mockup exists for this Content & Creative-only section
   specifically, so this is a reasonable "show the finished state plainly"
   fallback rather than a verified design value. */
@media (hover: none) {
	.service-pillreveal {
		height: auto;
	}

	.service-pillreveal__sticky {
		position: static;
		height: auto;
		overflow: visible;
		padding-top: clamp(56px, 10vw, 90px);
		/* Not the same clamp() as the top — .service-capabilities right
		   after this has its own top padding, and its first row has a
		   further padding-block of its own on top of that (see their own
		   rules below), so keeping this side symmetrical with the top
		   stacked 3 separate paddings into one gap that read as much
		   bigger than any single one of them looks intentionally. */
		padding-bottom: 16px;
		--connector-height: 44px;
		--connector-dot-opacity: 1;
		--bar-opacity: 1;
		--bar-shift: 0px;
		--stage2-opacity: 1;
		--stage2-shift: 0px;
	}

	.service-pillreveal__gap-arrow {
		display: none;
	}
}

/* --- Capabilities (Content & Creative only) ---
   Just the divided list of rows now — the heading/subheading that used
   to open this section (with a static decorative connector) actually
   belongs to Pill Reveal's own locked sequence as its second stage (see
   that section's own comment and inc/service.php's field instructions),
   not a separate normal-scroll section of its own; moving here was a
   misread of the design the first time round. Row layout mirrors
   .service-closing's final, working pattern (flex-wrap:nowrap +
   min-width:0 on the text side) rather than the flex-wrap+breakpoint
   approach tried and reverted there — that version let the button land
   on its own wrapped line, misaligned. */

.service-capabilities {
	background: var(--color-white);
	padding-block: clamp(56px, 8vw, 96px);
}

.service-capabilities__row {
	display: flex;
	flex-wrap: nowrap;
	align-items: center;
	justify-content: space-between;
	gap: 48px;
	padding-block: 38px;
}

.service-capabilities__row-text {
	display: flex;
	align-items: center;
	gap: clamp(24px, 5vw, 80px);
	flex: 1 1 auto;
	min-width: 0;
}

/* Fixed, not just flex-shrink:0 — with only flex-shrink:0, each title
   claims exactly its own text width, so the description column starts at
   a different point on every row depending on how long that row's title
   happens to be. A fixed width (same approach as .what-we-do-list__title)
   lines every description up at the same spot; longer titles wrapping to
   2 lines is expected and fine. */
.service-capabilities__row-title {
	margin: 0;
	width: 21rem;
	flex-shrink: 0;
	font-family: var(--font-body);
	font-weight: 600;
	font-size: 2.5rem;
	line-height: 1.25;
	color: var(--color-ink);
}

.service-capabilities__row-description {
	margin: 0;
	max-width: 37rem;
	font-family: var(--font-body);
	font-weight: 400;
	font-size: 1.25rem;
	line-height: 1.6;
	color: var(--color-ink);
}

.service-capabilities__row-cta {
	flex-shrink: 0;
}

.service-capabilities__divider {
	height: 1px;
	background: var(--color-ink);
}

@media (max-width: 900px) {
	.service-capabilities__row {
		flex-direction: column;
		align-items: flex-start;
		gap: 20px;
	}

	.service-capabilities__row-text {
		flex-direction: column;
		align-items: flex-start;
		gap: 12px;
	}

	/* Global mobile h3 baseline (1.7rem) — see base.css. */
	.service-capabilities__row-title {
		width: auto;
		font-size: 1.7rem;
	}

	/* One "Let's talk" per row read as excessive repeated down the whole
	   list on mobile — .service-closing__cta at the very end of the page
	   is already the same button, so that's the only one left. */
	.service-capabilities__row-cta {
		display: none;
	}
}

/* --- Our Work (Content & Creative only) ---
   Hand-placed to match Figma's exact bento layout for this specific
   project set, not a generic reusable grid — each project's own
   col_start/col_span/row_start/row_span (set in ACF, 1-12 columns / row
   unit 120px) is written as explicit inline grid-column/grid-row values
   per item in the template. An earlier version used spans only, with
   grid-auto-flow:dense placing each item automatically — that greedily
   fills whatever gap it finds first in scan order, not necessarily the
   gap actually intended, and one wrong auto-placement cascades into
   every later item shifting too. Explicit start positions remove that
   ambiguity: every tile's position is exactly what's stored, full stop. */

.service-work {
	background: var(--color-white);
	padding-block: clamp(56px, 8vw, 96px);
}

.service-work__heading {
	margin: 0 0 clamp(32px, 4vw, 56px);
	font-family: var(--font-body);
	font-weight: 400;
	font-size: 2.6rem;
	color: var(--color-ink);
}

/* Global mobile h2 baseline (2.3rem) — see base.css. */
@media (max-width: 900px) {
	.service-work__heading {
		font-size: 2.3rem;
	}

	/* .service-capabilities right above this already has its own bottom
	   padding (same clamp()), so the two stacked into one gap well over
	   100px — much bigger than either looked like it was trying to be on
	   its own. */
	.service-work {
		padding-top: 16px;
	}

	.service-capabilities {
		padding-bottom: 16px;
	}
}

.service-work__grid {
	display: grid;
	grid-template-columns: repeat(12, 1fr);
	grid-auto-rows: 120px;
	gap: 24px;
}

.service-work__item {
	position: relative;
	overflow: hidden;
	background-size: cover;
	background-position: center;
	background-color: var(--color-ink);
}

.service-work__video {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Inline per-item grid-column/grid-row (set from ACF, different per
   project) can't be overridden by a class rule at a lower specificity —
   !important is the legitimate way to override an inline style, which is
   exactly the situation here, not a specificity shortcut being reached
   for out of laziness. Mobile just stacks everything full-width instead
   of trying to preserve Figma's bento proportions at a tiny viewport. */
@media (max-width: 700px) {
	.service-work__grid {
		grid-template-columns: 1fr;
	}

	.service-work__item {
		grid-column: span 1 !important;
		grid-row: span 3 !important;
	}
}

/* --- Closing --- */
/* Same orange scroll-reveal as .what-we-do-closing (what-we-do.css) —
   ported directly rather than rebuilt, see that file's own comment for
   the full mechanic. This template's closing also has a button beside
   the quote (What We Do's doesn't).

   Always one row, never wrap — a first attempt used flex-wrap with a
   breakpoint to stack on narrow screens, but that's exactly what caused
   the bug: as soon as the row wrapped, the button landed on its own line
   where justify-content:space-between left-aligns a lone item instead of
   pinning it bottom-right, i.e. underneath the quote, left-aligned —
   backwards from Figma. Unconditional nowrap plus min-width:0 on the copy
   (so the flex item can actually shrink instead of forcing an overflow)
   keeps the button pinned bottom-right at every width; the quote just
   wraps its own text more on narrow screens instead of the whole row
   breaking.

   Same outer/inner split as .service-cards and .service-hwt: this is the
   last section that needs to cover .service-locked__bg's fixed photo
   layer (see its own comment), so the background can't live on a
   "container service-closing" element — that would only paint the
   centred content box, leaving the fixed layer visible in the gutters. */

.service-closing {
	background: var(--color-white);
}

.service-closing__inner {
	display: flex;
	flex-wrap: nowrap;
	align-items: flex-end;
	justify-content: space-between;
	gap: 24px;
	padding-block: clamp(32px, 4vw, 60px) clamp(48px, 6vw, 90px);
}

.service-closing__copy-wrap {
	position: relative;
	overflow: hidden;
	flex: 1 1 40rem;
	min-width: 0;
}

.service-closing__copy {
	/* Now an <a> (links through to Contact), not a <p> — block, same as a
	   paragraph defaults to, since an inline anchor wouldn't respect
	   max-width/line wrapping the same way. */
	display: block;
	position: relative;
	margin: 0;
	max-width: 56.5rem;
	font-family: var(--font-body);
	font-weight: 300;
	font-size: clamp(1.5rem, 3vw, 2.5rem);
	line-height: 1.4;
	color: var(--color-ink);
}

.service-closing__copy:hover,
.service-closing__copy:focus-visible {
	text-decoration: underline;
}

/* Matches .what-we-do-closing__copy's own mobile size (what-we-do.css) —
   the same "big closing statement + link to /contact/" pattern on that
   template. */
@media (max-width: 900px) {
	.service-closing__copy {
		font-size: 1.8rem;
	}
}

.service-closing__accent {
	position: absolute;
	left: 0;
	right: 0;
	top: var(--accent-top, 0px);
	height: 100vh;
	background: var(--color-accent);
	mix-blend-mode: screen;
	pointer-events: none;
}

.service-closing__cta {
	flex-shrink: 0;
}

/* flex-wrap:nowrap (above) exists specifically to keep the button pinned
   bottom-right of the quote at desktop widths (see this section's own
   top comment) — at phone widths there's no room left for the quote once
   the button claims its own natural width in that same row, so
   min-width:0 let it get squeezed down to a narrow, heavily-wrapped
   column instead of reading at a normal paragraph width. Stacking
   restores a normal full-width paragraph with the button sitting below
   it, same pattern as this page's other CTAs. */
@media (max-width: 700px) {
	.service-closing__inner {
		flex-direction: column;
		align-items: flex-start;
	}

	.service-closing__copy-wrap {
		flex-basis: auto;
		width: 100%;
	}
}
