/* Expanding Full Width Image — About §5 (Figma 11016:4384, node 11016:4418)
   and Services §3 (10534:3876, node 10534:3970).

   The media never resizes. The window over it opens outward, which is why this
   section causes no reflow and cannot disturb §4's pin measurements.

   The rest geometry is AUTHORABLE as of v1.2.0. Both values below are the
   fallbacks, and they are About §5's; frontend.php emits --tbl-ei-inset and
   --tbl-ei-aspect on the section root only where a placement differs, so About
   keeps taking these literal values and the v1.2.0 change is inert there by
   construction. Do not edit either number without editing frontend.php's
   $ei_default_rest / $ei_default_frame_h to match — they are one pair.

   The frame is 1600 x FRAME_H (capped, then proportional below that). The
   resting box is the frame's own width less a symmetric horizontal clip
   derived from the rest width: REST/1600 visible => (1600 - REST) / 2 / 1600
   per side, which is 22.78% for About's 871 and 11.6875% for Services' 1226.
   The inset is HORIZONTAL ONLY — the frame's two endpoints, REST x FRAME_H and
   1600 x FRAME_H, differ on one axis, and a lone percentage in inset() resolves
   against height for top/bottom, which would need a 1440px-tall wrapper in a
   784px slot.

   CSS default is the END state, inset(0). Firefox has no animation-timeline, so
   it takes the @supports fallback and lands on the finished full-width picture
   rather than a permanently boxed-in one — the same trade footer-reveal.css
   already makes sitewide.

   BB owns section spacing (bb-owns-section-spacing) — no padding or min-height
   here, and no CSS full-bleed breakout: the row is set to Full Width, the
   tbl-hero route rather than the tbl-case-studies-index one, which is what keeps
   this module JS-free. */

.tbl-expanding-image {
	/* Parallax travel per direction, as a percentage of the MEDIA's own height —
	   because that is what a percentage translateY resolves against, not the frame.
	   Total drift is twice this. The overscan below gives 8% of FRAME height of
	   slack on each side, which is 6.9% of media height, so anything up to ~6.8%
	   here is safe; past that the media's edge slides into the window.

	   Both terms are percentages of the same box, so this ceiling is
	   aspect-independent — it holds at 1600/784, 1600/652 and 1600/800 alike. */
	--tbl-expanding-image-parallax: 5%;

	display: block;
	/* Authorable at Pastel #112. The frame's max-width is measured against the
	   1600 frame, so the gutter has to live on the section OUTSIDE it — padding
	   here shrinks the available width and the frame still centres in what is
	   left, which is what keeps `max-width: 1600px` meaning the same thing.

	   BB owns section spacing as a rule (house convention), and this is the
	   documented exception: the row's own padding is still there and still wins
	   for vertical rhythm — this only adds a gutter the module itself can carry
	   when the row is set to 0. */
	padding: 0 var(--tbl-ei-pad-x, 20px);
	margin: 0;
}

.tbl-expanding-image__frame {
	position: relative;
	width: 100%;
	/* Render cap only — authorable at Pastel #114. Every geometry number above is
	   measured against the 1600 Figma frame and carried as a percentage, so a
	   larger cap scales the composition rather than changing it. */
	max-width: var(--tbl-ei-frame-max, 1600px);
	margin-inline: auto;
	aspect-ratio: var(--tbl-ei-aspect, 1600 / 784);
	/* End state, and therefore the fallback everywhere the scrub does not run.
	   Also the parallax's clip: the media is taller than this box and slides, and
	   clip-path is always applied, so no separate overflow:hidden is needed. */
	clip-path: inset(0% 0% 0% 0%);
	/* Named so the media can hang its parallax off THIS box. The media is taller
	   than the frame and vertically offset, so a bare view() declared on the media
	   would measure a different rect and drift out of sync with the window opening.
	   Timeline lookup walks self-then-ancestors, so the frame's own scrub can keep
	   using view(block 0) — same box, same inset, same numbers. */
	view-timeline-name: --tbl-expanding-image;
	view-timeline-inset: 0;
}

.tbl-expanding-image__media {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	/* No corner radius at any point — rejouice has none and neither does the frame. */
	border-radius: 0;
}

/* Below 768 the scrub and its parallax are off entirely (Pastel #75) — the CSS
   default IS the end state, so mobile lands on the finished full-width picture,
   the same trade the reduced-motion and Firefox paths already take. */
@media (prefers-reduced-motion: no-preference) and (min-width: 768px) {
	@supports (animation-timeline: view()) {
		.tbl-expanding-image__frame {
			/* linear is mandatory. The animation shorthand's timing function
			   defaults to ease, which would accelerate the opening against a
			   linear scroll input (scroll-scrub-pin-needs-linear). */
			animation: tbl-expanding-image-open linear both;
			/* The 0 inset is load-bearing, NOT a default being restated. `view()`
			   defaults to `view-timeline-inset: auto`, which adopts the
			   scrollport's scroll-padding — and this site sets
			   `scroll-padding-top: 80px` for the sticky header. Under a bare
			   `view()` the timeline's start edge is the header's bottom edge, so
			   the window finished opening 80px early and the scrub ran across
			   820px rather than a viewport height. Measured, not assumed. */
			animation-timeline: view(block 0);
			animation-duration: 1ms;
			/* Matches the Studios Callout box-grow EXACTLY (Ben, About §5 edit).
			   tbl-studios-callout.js drives its clip from
			     p  = clamp( (vh - rect.top) / (vh + rect.height), 0, 1 )
			     gp = clamp( 2 * p, 0, 1 )
			   That p IS the cover range — p=0 at the frame's top edge on the viewport
			   bottom, p=1 at its bottom edge on the viewport top — and the 2x clamp
			   lands full-open at cover 50%, i.e. the frame vertically centered. So
			   `cover 0% cover 50%` is the same curve, not an approximation of it.

			   The old `entry 0% exit 0%` finished at contain end (the frame's top edge
			   at the viewport TOP), roughly half a viewport later than Studios — that
			   lateness was the whole mismatch.

			   Reversal on scroll-up is free: a view-timeline is a pure function of
			   scroll position, so there is no toggleActions equivalent to declare. */
			animation-range: cover 0% cover 50%;
		}

		/* Pastel #128 (Chris) — Services runs the same window in reverse:
		   full width at rest, contracting slightly on the way down. `both`
		   fill still holds an endpoint outside the range, so the pre-range
		   hold is now the `to` keyframe (inset 0) and the post-range hold is
		   the `from` inset — which is exactly the motion Chris described.
		   The parallax is deliberately NOT reversed; the media should still
		   trail the page. */
		.tbl-expanding-image--contract .tbl-expanding-image__frame {
			animation-direction: reverse;
		}

		/* Parallax. Only inside these guards — Firefox and reduced-motion keep the
		   plain 100%-height, untransformed media above, so the fallback is exactly
		   the pre-parallax rendering rather than an image frozen off-centre.

		   The media box is overscanned symmetrically (8% of frame height proud at the
		   top and the bottom) and then slid across that slack. Symmetric means the
		   image sits dead-centre at mid-scroll, so the composition Chris framed is
		   what you see when the section is centred — the drift is around it, not
		   away from it.

		   It slides DOWN as the page scrolls down. That is the direction that reads
		   as depth (the media trailing the page); inverting it reads as too-fast.

		   Runs the FULL cover range, not the window's cover 0%-50%. The drift should
		   carry on after the window has finished opening — stopping both at centre
		   would land as one abrupt halt. */
		.tbl-expanding-image__media {
			position: absolute;
			top: -8%;
			left: 0;
			width: 100%;
			height: 116%;
			/* linear, same reason as the window (scroll-scrub-pin-needs-linear). */
			animation: tbl-expanding-image-parallax linear both;
			animation-timeline: --tbl-expanding-image;
			animation-duration: 1ms;
			animation-range: cover 0% cover 100%;
		}
	}
}

/* var() substitutes at computed-value time, so both endpoints are concrete
   inset() values by the time the timeline interpolates them — the same route
   the parallax keyframes below already take. The properties are deliberately
   NOT registered with @property: nothing reads them back in JS, and an
   unregistered property that something later tries to parseFloat returns NaN,
   which the near-universal `|| 0` guard turns into a silent zero. Verify the
   RENDERED clip, never the property. */
@keyframes tbl-expanding-image-open {
	from { clip-path: inset(0% var(--tbl-ei-inset, 22.78%) 0% var(--tbl-ei-inset, 22.78%)); }
	to   { clip-path: inset(0% 0% 0% 0%); }
}

@keyframes tbl-expanding-image-parallax {
	from { transform: translateY(calc(-1 * var(--tbl-expanding-image-parallax))); }
	to   { transform: translateY(var(--tbl-expanding-image-parallax)); }
}

/* ------------------------------------------------------------------ overlay ---
   Services Studios call-out (Figma 10223:3495, content frame 11278:735). Optional
   — About §5 authors none of the fields, renders no overlay, and is untouched.

   Figma geometry, read off the 1600 x 800 expanded frame (10223:3493): the content
   block is 713 wide (44.5625%), the heading is the system's Heading 2, the subline
   sits directly under it (0 gap, 20/30, 0.4 tracking) and the button sits 44 below.
   The block is centred horizontally and sits 40 below centre vertically, carried as
   a 55% offset so it survives a different authored frame aspect.

   The blur-in runs the SAME range as the window opening — cover 0% to cover 50%,
   i.e. finished when the section reaches the middle of the viewport, which is what
   the ticket asks for. As everywhere else in this module the CSS default is the
   END state, so Firefox, reduced motion and <768 all get plain legible content
   rather than something permanently blurred out. */

/* The scrim IS in the frame, and it is a flat 40% black covering the whole image —
   the instance carries two fills, the photo and a SOLID #000 at opacity 0.4 above
   it. It reads as "the photo, dimmed", not as a vignette, which is why an earlier
   radial gradient here looked wrong against the render. Only the button is
   interactive; the rest must not sit over the media. */
.tbl-expanding-image__overlay {
	position: absolute;
	inset: 0;
	background: rgba(0, 0, 0, 0.4);
	pointer-events: none;
}

/* The frame pads 308 above the block and 228 below it, so the block's own centre
   sits at (308 + 264/2) / 800 = 55% of the frame height, not 50%. Expressed as a
   percentage rather than the frame's literal 40px offset, because the aspect is
   authorable per placement and a fixed pixel nudge would not carry to a different
   frame height. */
.tbl-expanding-image__content {
	position: absolute;
	top: 55%;
	left: 50%;
	translate: -50% -50%;
	width: 44.5625%;
	max-width: var(--tbl-ei-content-max, 713px);
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	color: var(--Neutral-0, #fff);
}

/* (0,2,0): `color: inherit` at (0,1,0) tied with the theme's own .h2 colour and
   lost on source order, so the frame's white heading rendered near-black. */
.tbl-expanding-image .tbl-expanding-image__heading {
	margin: 0;
	color: var(--Neutral-0, #fff);
	text-wrap: balance;
}

.tbl-expanding-image__subline {
	/* 0 gap in the frame — the subline's own line box is the separation. */
	margin: 0;
	font-family: var(--body-font);
	font-size: 20px;
	line-height: 1.5;   /* 30px, the frame's own line box */
	letter-spacing: 0.4px;
	color: inherit;
}

/* The markup carries `btn light arrow`, so the easing, the pill radius, the
   focus-visible ring, the pressed state and the arrow all come from the design
   system. Everything below is a TOKEN override on this one element, not a
   restatement of the button — which is what keeps hover a system behaviour.

   `.light` remaps --Light-button-Filled-* onto the on-dark set, and its winning
   selector `.btn.light` is (0,2,0) — so a (0,2,0) override here TIES and loses to
   whichever stylesheet the page happens to load last (it did: the pill came up
   lavender). Hence `.btn` in the selector below: (0,3,0) settles it on specificity
   rather than on enqueue order. Hover / Focused / Pressed are deliberately left
   alone and fall through to the system's own on-dark values. Chris drew the resting pill white where the system's
   on-dark default is lavender; hovering therefore lifts white -> --Primary-Alt-30
   with the label staying black, which is the system's relationship, not a
   value invented here.

   No min-height: the frame's 54 is 20 + 13.8 + 20, so the padding alone lands it
   once the 1.5px system border is zeroed (the border is invisible anyway — the
   system paints it the same colour as the fill). */
.tbl-expanding-image .tbl-expanding-image__cta.btn {
	--button-margin: 44px 0 0;
	--button-padding: 20px 15px;   /* frame: 20 top/bottom, 20 left, 15 right + the arrow's own margin */
	--button-font-size: 13.8px;
	/* SequelSans-MediumDisp in the frame, which is the 500 face here — the button
	   default is 400 and rendered the label ~11px narrow. */
	--button-font-weight: 500;
	--button-letter-spacing: 0;
	--button-line-height: 1;
	--button-border-width: 0;

	--Light-button-Filled-Color: var(--Neutral-100, #0f0f0f);
	--Light-button-Filled-Fill: var(--Neutral-0, #fff);
	--Light-button-Filled-Outline: var(--Neutral-0, #fff);

	pointer-events: auto;
	flex-wrap: nowrap;
}

/* The frame spaces the label and the arrow 9.51 apart; the system's arrow ships
   at 0.4em, which is 5.5px at this font size. (0,3,1) to clear `.btn.arrow:after`
   without relying on which stylesheet loads last. */
.tbl-expanding-image .tbl-expanding-image__cta.arrow::after {
	margin-left: 9.51px;
}

@media (prefers-reduced-motion: no-preference) {
	@supports (animation-timeline: view()) {
		@media (min-width: 768px) {
			/* The whole overlay, not just the text: the scrim has to arrive with the
			   content, or the resting boxed image would sit darkened under nothing. */
			.tbl-expanding-image__overlay {
				animation: tbl-expanding-image-blur-in linear both;
				animation-timeline: --tbl-expanding-image;
				animation-duration: 1ms;
				animation-range: cover 0% cover 50%;
			}
		}
	}
}

/* Mobile. The frame's authored aspect is a wide desktop band — 172px tall at 390 —
   and the overlay's own content is 195px, so the button was being clipped off the
   bottom (worse, not better, once the block moved to its 55% position). Only where
   there IS an overlay: the frame becomes a one-cell grid with the media and the
   overlay stacked in it, so the row grows to whatever the copy needs and the media
   stretches to match. aspect-ratio stays as the preferred height and simply yields
   when the content is taller, which is why no min-height has to be guessed here.

   Scoped with :has() so About §5 — no overlay — keeps the plain aspect-ratio box it
   has always had.

   Chris has not drawn this section at mobile; this is a legibility floor, not a
   design. */
@media (max-width: 767px) {
	/* Pastel #117/#118/#119. Cancelling this module's own Side Padding lands 40px
	   short of the screen, because the BB row contributes a gutter of its own that
	   the module cannot see (bb-adds-40px-inset-per-side). The classic
	   `margin-inline: calc(50% - 50vw)` breakout is measured against the viewport
	   instead, so it clears every ancestor gutter at once and needs no knowledge of
	   what any of them are set to. Scrollbar width is not a factor at this tier —
	   mobile scrollbars are overlaid. */
	.tbl-expanding-image--bleed-m {
		padding-inline: 0;
		width: 100vw;
		max-width: 100vw;
		margin-inline: calc(50% - 50vw);
	}

	/* Otherwise the frame still caps at Max Frame Width and the breakout buys
	   nothing on a large phone. */
	.tbl-expanding-image--bleed-m .tbl-expanding-image__frame {
		max-width: none;
	}

	/* Authorable per placement as of v1.7.0 (Pastel #110) — measured in a 390-wide
	   mobile frame, so the ratio is 390 / H. Falls through to the desktop aspect
	   when the placement authors nothing, which is exactly what About §5 does. */
	.tbl-expanding-image__frame {
		aspect-ratio: var(--tbl-ei-aspect-m, var(--tbl-ei-aspect, 1600 / 784));
	}

	.tbl-expanding-image__frame:has(.tbl-expanding-image__overlay) {
		display: grid;
	}

	.tbl-expanding-image__frame:has(.tbl-expanding-image__overlay) > * {
		grid-area: 1 / 1;
	}

	.tbl-expanding-image__frame:has(.tbl-expanding-image__overlay) .tbl-expanding-image__media {
		align-self: stretch;
		height: 100%;
		min-height: 0;
	}

	.tbl-expanding-image__overlay {
		position: relative;
		display: flex;
		align-items: center;
		justify-content: center;
		padding-block: var(--tbl-ei-overlay-pad-m, 28px);
	}

	.tbl-expanding-image__content {
		position: static;
		translate: none;
		width: min(var(--tbl-ei-overlay-width-m, 88%), var(--tbl-ei-content-max, 713px));
	}

	.tbl-expanding-image__subline {
		font-size: 16px;
	}

	.tbl-expanding-image .tbl-expanding-image__cta.btn {
		--button-margin: 28px 0 0;
	}
}

@keyframes tbl-expanding-image-blur-in {
	from {
		opacity: 0;
		filter: blur(14px);
	}
	to {
		opacity: 1;
		filter: blur(0);
	}
}
