/* ==========================================================================
   Homepage — Slide Zero
   Remy is the first slide of his own reel. Geometry (card / caption / peek
   rects, wrapper shift) is measured from the live carousel slot by js/home.js.

   This file ships on every page and html.home-slide-zero is authored on every
   page, because the template's AJAX swap replaces <main> only — it loads no
   stylesheet and runs no script, so a flag applied on arrival would come too
   late (and one applied per-page would be missing entirely). What keeps the
   case-study pages untouched is the .sz-* half of every selector: those nodes
   exist on the homepage alone. The two rules that do reach template elements
   are inert without them — a transition on the pagination bullets, and a
   user-select rule under body.scale-drag-x, which is what a drag wants anyway.
   ========================================================================== */

/* ---------- identity layer (sits over the carousel, inside #main-content) */

.home-slide-zero .sz-layer {
	position: absolute;
	inset: 0;
	z-index: 30;
	pointer-events: none;
	overflow: visible;
}

/* ---------- the card: the active-slot rect, owned by us */

.home-slide-zero .sz-card {
	position: absolute;
	left: 0;
	top: 0;
	width: 611px;
	height: 342px;
	pointer-events: auto;
	cursor: grab;
	overflow: visible;
	touch-action: pinch-zoom;
	z-index: 3;
	opacity: 0;
	will-change: transform;
}

.home-slide-zero .sz-card.is-dragging {
	cursor: grabbing;
}

/* The reel's drag takes both axes on touch (onPointerMove in js/home.js), so the browser
   must not claim one of them before the gesture is read: with the card on `pan-y` every
   vertical swipe went to the scroller, and since this page does not scroll the flick died
   there with nothing to show for it. `pinch-zoom` rather than `none` — panning belongs to
   the reel in this state, zooming still belongs to the reader. Identity only; the work
   state hands the gestures back to the template's own slider.

   It also stops a side effect worth having: #main overflows horizontally by the width of
   the neighbouring slides, so a stray sideways swipe used to scroll the whole landing off
   its own design. Not while the gesture is ours. */
html.home-slide-zero[data-iteration-state="identity"] #main-content {
	touch-action: pinch-zoom;
}

/* A drag can start anywhere on the page, so it can start on the caption; without this the
   gesture selects text on its way across. `scale-drag-x` is the template's own marker for
   "a drag is running", set by both the reel's handler and ours, and user-select inherits. */
html.home-slide-zero body.scale-drag-x {
	-webkit-user-select: none;
	user-select: none;
}

.home-slide-zero .sz-portrait {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: 50% 42%;
	border-radius: 8px;
	background: #2b2b2b;
	user-select: none;
	-webkit-user-drag: none;
}

/* the same <img> once it has flipped into the pagination row */
.home-slide-zero .sz-portrait.is-bullet {
	width: 28px;
	height: 28px;
	border-radius: 14px;
	object-position: 80% 22%;
	position: relative;
	z-index: 2;
}

/* ---------- caption: the carousel's own caption type, static copies */

.home-slide-zero .sz-caption {
	position: absolute;
	left: 0;
	top: 0;
	width: 611px;
	pointer-events: auto;
	color: #fff;
}

/* Every part the entrance tweens in (playEntrance) starts hidden here too: the page
   paints as soon as the preloader curtain lifts, which is a few frames before the
   entrance timeline's first tick, and a settled caption in those frames reads as a
   flicker of the finished slide before it flies in. */
.home-slide-zero .sz-reveal {
	opacity: 0;
}

.home-slide-zero .sz-title {
	font-family: 'basis_grotesque_promedium', sans-serif;
	font-size: 5vw;
	line-height: 6vw;
	font-weight: 500;
	color: #fff;
	text-transform: uppercase;
	margin: 0 0 4px -0.2vw;
	padding: 0;
	overflow: hidden;
	letter-spacing: 0;
}

.home-slide-zero .sz-title span {
	display: block;
	position: relative;
}

.home-slide-zero .sz-subtitle {
	font-family: 'basis_grotesque_promedium', sans-serif;
	font-size: 14px;
	line-height: 22px;
	font-weight: 500;
	color: #fff;
	text-transform: uppercase;
	margin: 0;
	padding: 0;
	overflow: hidden;
}

.home-slide-zero .sz-subtitle > span {
	display: block;
	position: relative;
}

.home-slide-zero .sz-dot {
	display: inline-block;
	width: 7px;
	height: 7px;
	border-radius: 50%;
	/* Green, because the dot sits immediately before the availability half of the row and
	   is read as its status light rather than as a separator. The white ring stays: it is
	   what holds the 7px dot together against the portrait behind it. */
	background: #3ecf8e;
	box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.55);
	margin: 0 12px;
	vertical-align: 1px;
}

.home-slide-zero .sz-line {
	font-family: 'Poppins', sans-serif;
	font-weight: 400;
	font-size: 14px;
	line-height: 22px;
	color: #999999;
	margin: 6px 0 0;
	padding: 0;
	text-transform: none;
}

/* The line holds its content in a <span> rather than directly: captionParts() in js/home.js
   tweens the first `.sz-line` only, so a sibling paragraph would sit out the entrance and
   both transitions, while a child travels with the paragraph. */
.home-slide-zero .sz-line > span {
	display: block;
}

/* The line carries one tier, and `data-kind` is what names it: the stack is not prose, it is
   a keyword strip, and it is styled for what it *is* rather than for where it sits. It used
   to carry two more — a reach line and a closing argument — and one grey at one size read as
   a single paragraph, which is what made three facts hard to scan. Cutting them (2026-09-15)
   was the fix that stuck; keying off `data-kind` rather than `nth-child` is why it cost one
   deletion and no reshuffling. Anything put back here is prose in a 611px column — see the
   caption block in docs/HOMEPAGE-NOTES.md before writing it. */
.home-slide-zero .sz-line-tiers > span[data-kind="stack"] {
	color: #bdbdbd;
	font-size: 12px;
	letter-spacing: 0.07em;
	text-transform: uppercase;
}

/* The stack tier, as marks and names rather than a run of words: four pairs land in one
   glance where four uppercase names landed as a fourth paragraph of the caption. The marks
   are also the only colour on a screen whose reel is deliberately held back, which is the
   point of putting them here — this is the part to look at. So no filter of any kind: the
   about page greys its marks at rest because it shows five brand palettes at once inside a
   section built to be scanned, and this is four, once, under the name. Same vendor assets,
   provenance in images/about/stack-logos/SOURCES.md.

   Whitespace between the items is not rendered in a flex container, so the gaps are the
   only spacing there is and the row cannot drift with the markup's indentation. `wrap` is
   the safety valve: a fifth technology, a longer name or a zoomed font breaks the row onto
   a second line rather than out of the column. */
.home-slide-zero .sz-line-tiers > span[data-kind="stack"].sz-stack {
	--sz-stack-logo: 22px;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 10px 22px;
}

/* Each mark is bound to its name — 8px apart inside the pair against 22px between pairs,
   so the row reads as four things and not as eight. The name inherits the tier's own type
   (12px, uppercase, tracked) from the span above; all it adds is a refusal to break. */
.home-slide-zero .sz-stack-item {
	display: inline-flex;
	align-items: center;
	gap: 8px;
}

.home-slide-zero .sz-stack-name {
	white-space: nowrap;
}

/* Fitted into a square rather than given a height, so four drawings of four different
   aspects — a square, a wide atom, a disc, a narrow hexagon — carry the same optical
   weight. The about page's box, for the about page's reason. */
.home-slide-zero .sz-stack-logo {
	display: block;
	width: auto;
	height: auto;
	max-width: var(--sz-stack-logo);
	max-height: var(--sz-stack-logo);
	object-fit: contain;
}

.home-slide-zero .sz-actions {
	display: flex;
	align-items: center;
	flex-wrap: nowrap;
	gap: 26px;
	margin-top: 22px;
	pointer-events: auto;
}

/* The two contact pills are one unit: 16px apart from each other, 26px from
   "Selected work", so the row reads as [ contact ] [ work ] rather than three
   equal siblings. Keeping them in their own box also means that when the row
   wraps on a phone it breaks here, never between the pills. */
.home-slide-zero .sz-cta-pair {
	display: flex;
	align-items: center;
	flex-wrap: nowrap;
	gap: 16px;
}

/* ---------- hire pill: the footer PREV/NEXT pill anatomy, light-content colours */

/* The pill carries `.link` for the magic cursor, and shortcodes.css paints every
   `.light-content a.link` white (0,2,1). Outrank it so the label stays black on the
   white pill, and give the label span its own colour so nothing inherited can hurt it. */
html.home-slide-zero .light-content a.sz-pill.link,
.home-slide-zero a.sz-pill,
.home-slide-zero .sz-pill .sz-swap > span,
html.home-slide-zero .light-content a.sz-compact-mail.link,
html.home-slide-zero .light-content a.sz-compact-tail.link,
.home-slide-zero a.sz-compact-mail,
.home-slide-zero a.sz-compact-tail {
	color: #000;
}

/* the template's a.link hover underline has no business on a pill */
.home-slide-zero a.sz-pill::before,
.home-slide-zero a.sz-compact-mail::before,
.home-slide-zero a.sz-compact-tail::before {
	display: none;
}

.home-slide-zero .sz-pill {
	position: relative;
	display: inline-flex;
	align-items: center;
	height: 44px;
	border-radius: 44px;
	background: #fff;
	color: #000;
	text-decoration: none;
	padding: 0 22px 0 52px;
	font-family: 'basis_grotesque_promedium', sans-serif;
	font-size: 14px;
	font-weight: 500;
	line-height: 44px;
	text-transform: uppercase;
	white-space: nowrap;
	pointer-events: auto;
	cursor: pointer;
	transition: padding 0.3s ease-out;
}

.home-slide-zero .sz-pill:hover {
	padding: 0 22px 0 58px;
}

.home-slide-zero .sz-pill-icon-wrap {
	position: absolute;
	left: 4px;
	top: 4px;
	width: 36px;
	height: 36px;
	display: flex;
	align-items: center;
	justify-content: center;
}

.home-slide-zero .sz-pill-icon {
	width: 36px;
	height: 36px;
	border-radius: 36px;
	background: #000;
	color: #fff;
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 14px;
	line-height: 36px;
	transition: transform 0.3s ease-out;
}

.home-slide-zero .sz-pill-text {
	display: block;
	overflow: hidden;
}

/* ---------- LinkedIn pill: the same anatomy, inverted */

/* The white pill is the ask; this one is the alternative, so it gives up the
   fill and keeps the outline. `box-shadow` rather than `border`, so the 44px
   box and the icon's 4px inset are identical to the solid pill's. */
html.home-slide-zero .light-content a.sz-pill.sz-pill-ghost.link,
.home-slide-zero a.sz-pill.sz-pill-ghost,
.home-slide-zero .sz-pill.sz-pill-ghost .sz-swap > span {
	color: #fff;
}

.home-slide-zero .sz-pill.sz-pill-ghost {
	background: transparent;
	box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.34);
	transition: padding 0.3s ease-out, box-shadow 0.3s ease-out;
}

.home-slide-zero .sz-pill.sz-pill-ghost:hover {
	box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.75);
}

.home-slide-zero .sz-pill.sz-pill-ghost .sz-pill-icon {
	background: rgba(255, 255, 255, 0.1);
	box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.34);
	color: #fff;
}

/* ---------- text link with the nav's data-hover swap */

.home-slide-zero .sz-swap > span {
	position: relative;
	display: block;
	transition: transform 0.35s cubic-bezier(0.19, 1, 0.22, 1);
}

.home-slide-zero .sz-swap > span::before {
	content: attr(data-hover);
	position: absolute;
	top: 100%;
	left: 0;
	white-space: nowrap;
}

.home-slide-zero .sz-pill:hover .sz-swap > span,
.home-slide-zero .sz-textlink:hover .sz-swap > span {
	transform: translateY(-100%);
}

.home-slide-zero .sz-textlink {
	appearance: none;
	-webkit-appearance: none;
	background: transparent;
	border: 0;
	margin: 0;
	padding: 0 4px;
	min-height: 44px;
	display: inline-flex;
	align-items: center;
	gap: 12px;
	color: #fff;
	font-family: 'basis_grotesque_promedium', sans-serif;
	font-size: 14px;
	font-weight: 500;
	line-height: 22px;
	text-transform: uppercase;
	cursor: pointer;
	pointer-events: auto;
	white-space: nowrap;
}

.home-slide-zero .sz-textlink .sz-swap {
	display: block;
	overflow: hidden;
	position: relative;
}

.home-slide-zero .sz-textlink .sz-swap::after {
	content: "";
	position: absolute;
	left: 0;
	bottom: 1px;
	height: 1px;
	width: 100%;
	background: currentColor;
	transform: scaleX(0);
	transform-origin: right;
	transition: transform 0.4s cubic-bezier(0.19, 1, 0.22, 1);
}

.home-slide-zero .sz-textlink:hover .sz-swap::after {
	transform: scaleX(1);
	transform-origin: left;
}

.home-slide-zero .sz-textlink i {
	font-size: 13px;
	transition: transform 0.35s cubic-bezier(0.19, 1, 0.22, 1);
}

.home-slide-zero .sz-textlink:hover i {
	transform: translateX(6px);
}

/* ---------- the transparent hit area over the next-project peek */

.home-slide-zero .sz-peek {
	position: absolute;
	left: 0;
	top: 0;
	width: 300px;
	height: 300px;
	min-width: 44px;
	min-height: 44px;
	margin: 0;
	padding: 0;
	border: 0;
	background: transparent;
	pointer-events: auto;
	cursor: pointer;
	border-radius: 8px;
	z-index: 2;
}

.home-slide-zero .sz-peek[hidden] {
	display: none;
}

/* computeLayout() sets this when the reel row has been pushed so far down that none
   of it is on screen. The 44px minimums above would otherwise leave an invisible tap
   target sitting on the bottom edge, in front of nothing. */
.home-slide-zero .sz-peek.is-offscreen {
	display: none;
}

/* ---------- the reel's own height, capped against the room there is

   The template sizes a slide from its width alone — `padding-top: 56%` of it — and never
   looks at how tall the window is, so a short one gets a row taller than the composition
   can hold: 322px of reel inside 768px of window at 1366x768. rowLayout used to absorb
   that by shortening the card, which left the card 66px smaller than the slot it sits in,
   and a card that does not cover its own slide has nothing to hide that slide behind while
   the two move out together. No easing fixes it; only the two boxes being the same size.

   So the row is what gives. computeLayout measures the room between the header and the
   pagination, and where the natural row will not fit it publishes --reel-slot-h, which
   turns the aspect box into a plain one. Width is untouched, so the slides keep their place
   in the slider's own arithmetic and only the crop changes — wider and shorter, which on a
   short window is what it should have been. A variable on the carousel rather than a height
   per slide because the slider clones slides, and a clone made later would have missed it.

   This is the identity composition's arithmetic, but the cap is not lifted for the work
   state: a row that resized on the way in would be the same flicker by another name. */
html.home-slide-zero .showcase-carousel.is-capped .slide-inner {
	padding-top: 0;
	height: var(--reel-slot-h);
}

/* ---------- the reel itself: the slider box travels with #itemsWrapper, so its own
   overflow:hidden would clip everything left of the shifted wrapper (the France TV
   clone that should peek on the far left in identity). The viewport clips anyway. */

html.home-slide-zero body.slide-zero .clapat-slider {
	overflow: visible;
}

/* ---------- face bullet: slide 0 at the head of the real pagination row */

/* In identity the face bullet is the current position; the template's own active ring
   (the Burger King dot) would be a second highlight in the same row, so it is quiet. */
.home-slide-zero .clapat-pagination .clapat-pagination-bullet .path {
	transition: opacity 0.3s ease-out;
}

.home-slide-zero .clapat-pagination.sz-identity .clapat-pagination-bullet-active .path {
	opacity: 0;
}

/* 44px hit area, 32px face (28px portrait + ring) so it reads as the first dot; the
   -2px right margin keeps the template's 40px dot pitch from the face centre onwards. */
.home-slide-zero .sz-bullet {
	appearance: none;
	-webkit-appearance: none;
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 44px;
	height: 44px;
	margin: -2px -2px -2px 0;
	padding: 0;
	border: 0;
	background: transparent;
	cursor: pointer;
	pointer-events: auto;
	border-radius: 50%;
	overflow: visible;
	flex: 0 0 auto;
}

.home-slide-zero .sz-bullet-wrap {
	width: 44px;
	height: 44px;
	display: flex;
	align-items: center;
	justify-content: center;
	pointer-events: none;
}

.home-slide-zero .sz-bullet-face {
	position: relative;
	width: 32px;
	height: 32px;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: visible;
}

.home-slide-zero .sz-ring {
	position: absolute;
	left: 0;
	top: 0;
	width: 32px;
	height: 32px;
	transform-origin: 50% 50%;
	pointer-events: none;
}

.home-slide-zero .sz-bullet-label {
	position: absolute;
	top: 100%;
	left: 50%;
	transform: translate(-50%, 2px);
	font-family: 'basis_grotesque_promedium', sans-serif;
	font-size: 11px;
	line-height: 14px;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: #fff;
	opacity: 0;
	white-space: nowrap;
	transition: opacity 0.25s ease-out, transform 0.25s ease-out;
	pointer-events: none;
}

.home-slide-zero .sz-bullet:hover .sz-bullet-label,
.home-slide-zero .sz-bullet:focus-visible .sz-bullet-label {
	opacity: 1;
	transform: translate(-50%, 6px);
}

/* ---------- compact hire pill in the footer (work state) */

/* One capsule holding two destinations: a LinkedIn tail, a hairline, then the email half.
   `right` here is a fallback only: applyLayout() measures NEXT's rect and sets the inline
   `right` so the capsule's right edge is NEXT's own. That right edge is the anchor and the
   width is content-driven, so revealing the email label grows the capsule leftwards; NEXT
   is kept COMPACT_GAP to the left of whatever width it currently has
   ([`followCompactCta`](js/home.js)), which is what pushes NEXT out of the way. */
.home-slide-zero .sz-cta-compact {
	position: absolute;
	right: 60px;
	top: 18px;
	height: 44px;
	border-radius: 44px;
	background: #fff;
	color: #000;
	display: flex;
	align-items: stretch;
	justify-content: flex-end;
	white-space: nowrap;
	overflow: hidden;
	opacity: 0;
	z-index: 5;
}

/* it is invisible outside the work state but still in the layout, so nothing can hover
   or click a capsule that isn't on screen — the halves set their own `auto`, so they have
   to be named here too or they stay hittable through the invisible capsule */
html.home-slide-zero:not([data-iteration-state="work"]) .sz-cta-compact,
html.home-slide-zero:not([data-iteration-state="work"]) .sz-cta-compact a {
	pointer-events: none;
}

/* ---------- the LinkedIn tail, hairline-separated from the email half */

.home-slide-zero .sz-compact-tail {
	position: relative;
	flex: 0 0 auto;
	width: 44px;
	display: flex;
	align-items: center;
	justify-content: center;
	color: #000;
	font-size: 15px;
	text-decoration: none;
	pointer-events: auto;
	cursor: pointer;
	transition: background 0.25s ease-out;
}

.home-slide-zero .sz-compact-tail::after {
	content: "";
	position: absolute;
	right: 0;
	top: 10px;
	bottom: 10px;
	width: 1px;
	background: rgba(0, 0, 0, 0.16);
}

.home-slide-zero .sz-compact-tail:hover {
	background: #e4e4e4;
}

/* The tail's glyph rides the same 44px cell / 36px disc pair as the envelope, so it can
   be magnetised by `bindMagnet` too — but the "in" mark reads optically smaller than an
   envelope, so it keeps the 15px the tail already set rather than the disc's 14px. */
.home-slide-zero .sz-compact-tail .sz-compact-icon {
	font-size: 15px;
}

.home-slide-zero .sz-compact-tail i {
	transition: transform 0.3s ease-out;
}

.home-slide-zero .sz-compact-tail:hover i {
	transform: translateY(-2px);
}

/* ---------- the email half: label (revealed on hover) then the envelope */

.home-slide-zero .sz-compact-mail {
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	color: #000;
	text-decoration: none;
	pointer-events: auto;
	cursor: pointer;
}

.home-slide-zero .sz-compact-wrap {
	flex: 0 0 auto;
	width: 44px;
	height: 44px;
	display: flex;
	align-items: center;
	justify-content: center;
}

.home-slide-zero .sz-compact-icon {
	width: 36px;
	height: 36px;
	border-radius: 36px;
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 14px;
	color: #000;
}

/* The label sits in the email half's own flow, ahead of the envelope, so revealing it
   widens the capsule rather than painting over the footer. `max-width` rather than `width`
   because the capsule is sized by its content and only max-width can transition from
   nothing; the cap is a little over the label's natural width (55px for "Hire me") so the
   ease stays honest — retune it if the wording changes. */
.home-slide-zero .sz-compact-label {
	display: block;
	max-width: 0;
	overflow: hidden;
	font-family: 'basis_grotesque_promedium', sans-serif;
	font-size: 14px;
	font-weight: 500;
	line-height: 44px;
	text-transform: uppercase;
	color: #000;
	opacity: 0;
	transition: max-width 0.35s ease-out, padding 0.35s ease-out, opacity 0.2s ease-out;
	pointer-events: none;
}

/* `is-open` is set by the email half and cleared when the pointer leaves the whole
   capsule ([`bindInputs`](js/home.js)), rather than living on `.sz-compact-mail:hover`.
   The capsule grows from its right edge, so a plain hover rule collapsed it the moment
   the pointer walked left toward the tail — taking the tail with it, out of reach. */
.home-slide-zero .sz-cta-compact.is-open .sz-compact-label,
.home-slide-zero .sz-cta-compact:focus-within .sz-compact-label {
	max-width: 80px;
	padding: 0 2px 0 20px;
	opacity: 1;
}

/* ---------- footer PREV while nothing precedes slide zero */

.home-slide-zero .cp-button-prev[inert] {
	pointer-events: none;
}

/* ---------- keyboard focus, visible on every control we author */

.home-slide-zero .sz-pill:focus-visible,
.home-slide-zero .sz-textlink:focus-visible,
.home-slide-zero .sz-peek:focus-visible,
.home-slide-zero .sz-bullet:focus-visible {
	outline: 2px solid #fff;
	outline-offset: 4px;
}

/* the capsule clips its own children, so its halves take an inset ring instead of the
   offset white one — dark, because they sit on the white pill */
.home-slide-zero .sz-compact-mail:focus-visible,
.home-slide-zero .sz-compact-tail:focus-visible {
	outline: 2px solid #000;
	outline-offset: -4px;
}

.home-slide-zero .sz-card:focus-visible {
	outline: 2px solid #fff;
	outline-offset: 6px;
	border-radius: 8px;
}

/* ---------- breakpoints: mirror the template's footer paddings */

@media only screen and (max-width: 1024px) {
	.home-slide-zero .sz-cta-compact {
		right: 60px;
	}

	/* Expanding the pill pushes NEXT left, and from here down that is far enough to land
	   NEXT on the pagination dots (at 768 it overlaps the last dot by ~57px). No room, and
	   nothing with a pointer to reveal it either, so the pill stays the plain envelope. */
	.home-slide-zero .sz-compact-label {
		display: none;
	}
}

@media only screen and (max-width: 767px) {
	.home-slide-zero .sz-cta-compact {
		right: 30px;
	}

	.home-slide-zero .sz-portrait {
		object-position: 62% 30%;
	}

	/* The phone stacks every part of the caption that the desktop sets side by side or
	   on one line, so six blocks that read as four groups on a wide screen arrive as one
	   dense column. The rhythm below is what separates them: tight inside a group (the
	   name and the role it belongs to), open between groups (identity / stack / prose /
	   the ask). The room comes out of the reel, which pays in position — see ROW_GAP and
	   CARD_GAP in js/home.js, raised alongside these. */
	.home-slide-zero .sz-title {
		font-size: 9vw;
		line-height: 10vw;
		margin-left: -0.3vw;
		margin-bottom: 8px;
	}

	.home-slide-zero .sz-subtitle {
		font-size: 13px;
		line-height: 20px;
	}

	/* The role and the availability each take their own line here. Giving the role the full
	   width to force that break — rather than making the row a column — leaves the dot and
	   the availability together as one wrapped line, which is what the dot needs: it is the
	   status light for that half of the row and not a separator between two halves, so a
	   column stranded it on a line of its own and it had to be hidden to make sense. */
	.home-slide-zero .sz-subtitle > span {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
	}

	.home-slide-zero .sz-subtitle > span > [data-iteration-role] {
		flex: 0 0 100%;
	}

	/* Leading its line rather than sitting between two: the 12px on its left would have
	   indented the availability off the column edge every other line holds. */
	.home-slide-zero .sz-dot {
		margin: 0 8px 0 0;
	}

	.home-slide-zero .sz-line {
		font-size: 13px;
		line-height: 20px;
		margin-top: 16px;
	}

	/* Labelled, the four pairs measure about 370px against the 352px a 412px phone offers,
	   so the row would wrap to two lines and cost the portrait above it the difference.
	   The marks alone are the point of the tier at this width — they are recognisable at a
	   glance, which is why they replaced the words here in the first place. The name is
	   taken from the eye and not from the accessibility tree, so the reading order stays
	   "TypeScript React Next.js Node.js" at every width — which is also why the images
	   carry alt="": naming is the label's job, once. */
	.home-slide-zero .sz-line-tiers > span[data-kind="stack"].sz-stack {
		gap: 14px;
	}

	.home-slide-zero .sz-stack-name {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: -1px;
		padding: 0;
		overflow: hidden;
		clip-path: inset(50%);
	}

	/* Three controls do not fit on one phone line. The row wraps, and it can only
	   break between the contact pair and "Selected work" — the pair is its own
	   flex box. computeLayout() measures the caption's real height, so the card
	   above gives back exactly the height this second line takes. */
	.home-slide-zero .sz-actions {
		flex-wrap: wrap;
		gap: 12px 14px;
		margin-top: 28px;
	}

	/* wrap is the safety valve: if the pair ever cannot fit its line (a zoomed
	   font, a longer label), LinkedIn drops below the email pill rather than
	   running off the right edge */
	.home-slide-zero .sz-cta-pair {
		flex-wrap: wrap;
		gap: 10px;
	}

	/* ...and this is what keeps the valve shut at the widths phones actually have.
	   Labelled, the three controls measure 386px against the 352px a 412px phone
	   offers, so the row wrapped to two lines — and every one of those 48px came
	   out of the portrait above it, which is what put the caption on top of the
	   reel. The email pill is the ask and keeps its label; LinkedIn is the
	   alternative and is a mark that reads without one (the anchor carries
	   aria-label="LinkedIn", so the accessible name survives the label going).
	   309px now, so the line holds from 360px up and wraps below that. */
	.home-slide-zero .sz-cta-pair .sz-pill-ghost,
	.home-slide-zero .sz-cta-pair .sz-pill-ghost:hover {
		width: 44px;
		padding: 0;
	}

	.home-slide-zero .sz-cta-pair .sz-pill-ghost .sz-pill-text {
		display: none;
	}

	/* ---------- the reel, held back while slide zero has the screen

	   Stacked, the two sections are a gap apart and nothing else, so the identity block
	   takes the colour and the reel sits behind it. Recessed, not disabled: it is still
	   the tap target and still says "there is work below" — hence a partial desaturation
	   rather than the flat grey and half-opacity that would read as "unavailable".

	   `filter`, never `opacity`: opacity is the entrance's own channel and both
	   transitions tween it on these very elements, so anything set here would be fighting
	   GSAP for the same property. One filter on the viewport rather than one per slide,
	   so the phone composites a single layer.

	   `sz-reel-idle` goes on at the top of toIdentity and off at the top of toWork, so
	   the colour arrives with the row rather than a second behind it. */
	html.home-slide-zero .showcase-carousel .clapat-slider-viewport {
		transition: filter 0.7s ease-out;
	}

	html.home-slide-zero body.sz-reel-idle .showcase-carousel .clapat-slider-viewport {
		filter: grayscale(0.92) brightness(0.55);
	}

	.home-slide-zero .sz-bullet {
		margin: -12px -2px -12px 0;
	}

	.home-slide-zero .sz-bullet-label {
		display: none;
	}

	/* Phones collapse PREV/NEXT to bare arrows and centre the pagination row on its own,
	   so it cannot reflow: the tail's extra 44px walks NEXT's arrow onto the last dot.
	   The footer keeps the envelope alone here — LinkedIn is still offered in the caption. */
	.home-slide-zero .sz-compact-tail {
		display: none;
	}
}

@media only screen and (max-width: 479px) {
	.home-slide-zero .sz-cta-compact {
		right: 20px;
	}

	.home-slide-zero .sz-pill {
		font-size: 12px;
		padding: 0 18px 0 50px;
	}

	.home-slide-zero .sz-pill:hover {
		padding: 0 18px 0 54px;
	}

	.home-slide-zero .sz-textlink {
		font-size: 12px;
		gap: 8px;
	}
}

/* ---------- short viewports: a phone in landscape, a half-height desktop window

   computeLayout() can shrink the card and push the reel below the fold, but no
   arithmetic fits a display-sized name and a portrait into the ~200px a 360px-tall
   viewport leaves between the header and the footer controls. So the caption gives
   up its display sizing here and keeps the irreducible identity — name, role,
   availability, stack, the two CTAs. With the block that size the allocator has
   something it can actually place, and the card drops out on its own (below
   CARD_FLOOR) rather than painting a stripe.

   It used to drop two prose lines here as well; the caption no longer has any
   (2026-09-15), which is most of why this block is now three declarations.

   This block sits last in the file on purpose: it shares specificity with the
   width breakpoints above and has to win against them at any width. */
@media only screen and (max-height: 520px) {
	.home-slide-zero .sz-title {
		font-size: 30px;
		line-height: 34px;
		margin-bottom: 2px;
	}

	.home-slide-zero .sz-actions {
		margin-top: 12px;
	}
}
