@layer components {
	/* ------------------------------------------------------ the dust stage */

	.intro-stage {
		position: fixed;
		inset: 0;
		z-index: 200;
		pointer-events: none;
	}

	.intro-stage canvas {
		display: block;
		width: 100%;
		height: 100%;
	}

	/* --------------------------------------------------------- brand mark */

	/*
	 * The signature replaces the wordmark. It is invisible until the dust
	 * lands on it, so the two never appear at once. Without JS the
	 * `intro-done` class is never added — so the no-JS rule below shows it.
	 */
	/* Signature over a letterspaced word — the usual photographer's lockup. */
	.site-brand {
		display: inline-flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 0.15rem;
		line-height: 1;
		text-decoration: none;
	}

	.brand-word {
		font-family: var(--font-display);
		font-size: 0.72rem;
		letter-spacing: 0.46em;
		text-transform: uppercase;
		/* The tracking adds a trailing gap; pull it back so it optically aligns. */
		margin-inline-start: 0.2em;
		text-indent: 0.46em;
		opacity: 0;
		transition: opacity var(--dur-slow) var(--ease-out) 200ms;
	}

	:root.intro-done .brand-word,
	html:not(.has-js) .brand-word,
	body:not(.home) .brand-word {
		opacity: 0.72;
	}

	.brand-mark {
		display: block;
		width: clamp(150px, 16vw, 230px);
		height: auto;
		/* The signature artwork is already white, and the plane is always dark. */
		opacity: 0;
		transition: opacity var(--dur-slow) var(--ease-out);
	}

	/* Over a photograph the mark is white too — nothing to change. */
	.site-header[data-over-media="true"]:not([data-scrolled="true"]):not([data-menu-open="true"]) .brand-mark {
		filter: none;
	}

	:root.intro-done .brand-mark,
	html:not(.has-js) .brand-mark {
		opacity: 1;
	}

	/* Pages that never run the intro show the mark straight away. */
	body:not(.home) .brand-mark {
		opacity: 1;
	}

	/* --------------------------------------------------- hero copy arrival */

	/*
	 * The copy does not blink on. It condenses out of a blur, drifting up,
	 * one element at a time — the same language as the dust.
	 */
	.has-js [data-hero-content] > * {
		opacity: 0;
		translate: 0 1.25rem;
		filter: blur(14px);
		transition:
			opacity 1100ms var(--ease-out),
			translate 1100ms var(--ease-out),
			filter 1100ms var(--ease-out);
		transition-delay: var(--reveal-delay, 0ms);
	}

	.has-js [data-hero-content].is-materialising > * {
		opacity: 1;
		translate: 0 0;
		filter: blur(0);
	}

	/* The headline arrives word by word, so it reads as writing itself in. */
	.hero-word {
		display: inline-block;
		opacity: 0;
		translate: 0 0.5em;
		filter: blur(10px);
		transition:
			opacity 900ms var(--ease-out),
			translate 900ms var(--ease-out),
			filter 900ms var(--ease-out);
		transition-delay: var(--word-delay, 0ms);
	}

	.is-materialising .hero-word {
		opacity: 1;
		translate: 0 0;
		filter: blur(0);
	}

	@media (prefers-reduced-motion: reduce) {
		.has-js [data-hero-content] > *,
		.hero-word {
			opacity: 1;
			translate: none;
			filter: none;
			transition: none;
		}
	}

	/* ------------------------------------------------------ hero rotation */

	.hero-slide {
		position: absolute;
		inset: 0;
		margin: 0;
		opacity: 0;
		transition: opacity 1400ms var(--ease-in-out);
	}

	.hero-slide.is-active {
		opacity: 1;
	}

	.hero-slide img {
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	/* A slow push keeps a still frame from reading as a dead screenshot. */
	@media (prefers-reduced-motion: no-preference) {
		.hero-slide.is-active img {
			animation: hero-push 14s var(--ease-out) forwards;
		}
	}

	@keyframes hero-push {
		from { scale: 1; }
		to { scale: 1.07; }
	}

	/* -------------------------------------------------------- hero index */

	/*
	 * The archive, listed down the left of the frame. It gives the empty half
	 * of the photograph a job, tracks whichever category is on screen, and is
	 * real navigation rather than decoration.
	 */
	/*
	 * The hero is a grid, not a pile of absolutely-positioned blocks: the
	 * index gets the open upper region, the panel gets the lower one, and
	 * they cannot collide however long the copy or the list becomes.
	 */
	.hero.has-sequence {
		display: grid;
		grid-template-rows: 1fr auto;
		align-content: stretch;
	}

	.hero.has-sequence .hero-media,
	.hero.has-sequence .hero-scrim {
		grid-area: 1 / 1 / -1 / -1;
	}

	/*
	 * Pinned to the lower-left. The list rolls as the hero cycles so the
	 * category on screen is always the bottom row — the eye can stay in one
	 * place and still read what it is looking at.
	 */
	/*
	 * One row holds the index and the scroll prompt. They used to be
	 * positioned independently, which is fine until the frame narrows — then
	 * nothing keeps them apart and the caption runs straight into the prompt.
	 * Sharing a flex row makes overlap impossible at any width, and lets the
	 * prompt drop to its own line when there is no room beside the caption.
	 */
	.hero-foot {
		grid-row: 1 / -1;
		align-self: end;
		z-index: 2;
		display: flex;
		flex-wrap: wrap;
		align-items: flex-end;
		justify-content: space-between;
		gap: var(--space-s) var(--space-l);
		width: 100%;
		padding-inline: var(--gutter);
		padding-block-end: clamp(var(--space-l), 6vh, var(--space-2xl));
		color: var(--over-ink);
	}

	.hero-corner {
		flex: 1 1 auto;
		min-width: 0;
		max-width: min(22rem, 36vw);
	}

	@media (max-width: 64rem) {
		.hero-corner {
			max-width: 100%;
		}
	}

	.hero-index {
		display: flex;
		flex-direction: column;
		gap: 1px;
	}

	/* ------------------------------------------------------- scroll cue */

	/* The canvas the dust crosses; it covers the hero and catches no clicks. */
	.cue-stage {
		position: absolute;
		inset: 0;
		z-index: 3;
		pointer-events: none;
	}

	.cue-stage canvas {
		display: block;
		width: 100%;
		height: 100%;
	}

	/*
	 * The prompt sits in the corner opposite the index. Its label is invisible
	 * while the dust spells it out, then takes over so it stays selectable,
	 * translatable and clickable.
	 */
	.scroll-cue {
		flex: 0 0 auto;
		/* Same again: the prompt was 18px tall, which is nothing to press. */
		padding-block: 0.4em;
		/* When the row wraps, the prompt keeps to the right-hand edge. */
		margin-inline-start: auto;
		display: inline-flex;
		align-items: center;
		gap: 0.7em;
		font-size: 0.72rem;
		letter-spacing: 0.3em;
		text-transform: uppercase;
		text-decoration: none;
		color: var(--over-ink);
		transition: gap var(--dur-fast) var(--ease-out);
	}

	.scroll-cue:hover {
		gap: 1.1em;
	}

	.scroll-cue [data-scroll-cue-label],
	.scroll-cue-arrow {
		opacity: 0;
		transition: opacity var(--dur) var(--ease-out);
	}

	.scroll-cue.is-settled [data-scroll-cue-label] {
		opacity: 1;
	}

	.scroll-cue.is-settled .scroll-cue-arrow {
		opacity: 0.7;
	}

	@media (prefers-reduced-motion: no-preference) {
		.scroll-cue.is-settled .scroll-cue-arrow {
			animation: cue-bob 2.4s var(--ease-in-out) infinite;
		}
	}

	.hero-caption {
		display: flex;
		flex-wrap: wrap;
		align-items: baseline;
		/* Packed to the start, not spread — spreading pushed the total toward
		   whatever sat to its right. */
		justify-content: flex-start;
		column-gap: var(--space-m);
		row-gap: 0.2rem;
		margin: var(--space-s) 0 0;
		padding-inline-start: 0.5rem;
		font-size: 0.7rem;
		letter-spacing: 0.16em;
		text-transform: uppercase;
		color: color-mix(in oklab, var(--over-ink) 62%, transparent);
		transition: opacity var(--dur) var(--ease-out);
	}

	.hero-caption [data-hero-caption].is-swapping {
		opacity: 0;
	}

	.hero-caption-total {
		font-variant-numeric: tabular-nums;
		color: color-mix(in oklab, var(--over-ink) 44%, transparent);
	}

	.hero-index-item {
		position: relative;
		display: grid;
		grid-template-columns: 1.9rem 1fr auto;
		align-items: baseline;
		gap: var(--space-xs);
		padding: 0.62rem 0;
		text-decoration: none;
		color: color-mix(in oklab, var(--over-ink) 58%, transparent);
		border-top: 1px solid color-mix(in oklab, var(--over-ink) 16%, transparent);
		transition: color var(--dur) var(--ease-out), padding-left var(--dur) var(--ease-out);
	}

	.hero-index-item:last-child {
		border-bottom: 1px solid color-mix(in oklab, var(--over-ink) 16%, transparent);
	}

	.hero-index-item:hover {
		color: var(--over-ink);
		padding-left: 0.5rem;
	}

	/* The active row is the loudest thing in the list, and sits last. */
	.hero-index-item.is-current {
		color: var(--over-ink);
		padding-left: 0.5rem;
	}

	.hero-index-item.is-current .hero-index-name {
		font-size: var(--step-1);
		letter-spacing: 0.06em;
	}

	.hero-index-item.is-current .hero-index-count,
	.hero-index-item.is-current .hero-index-n {
		opacity: 1;
	}

	.hero-index-n {
		font-size: 0.62rem;
		letter-spacing: 0.14em;
		font-variant-numeric: tabular-nums;
		opacity: 0.65;
	}

	.hero-index-name {
		font-size: var(--step--1);
		letter-spacing: 0.12em;
		text-transform: uppercase;
	}

	.hero-index-count {
		font-size: 0.68rem;
		font-variant-numeric: tabular-nums;
		opacity: 0.6;
	}

	/* The active row draws a line under itself as its slide plays. */
	.hero-index-line {
		position: absolute;
		inset: auto 0 -1px 0;
		height: 1px;
		background: var(--over-ink);
		transform-origin: left;
		scale: 0 1;
	}

	.hero-index-item.is-current .hero-index-line {
		animation: index-fill var(--slide-dur, 6500ms) linear forwards;
	}

	@keyframes index-fill {
		from { scale: 0 1; }
		to { scale: 1 1; }
	}

	/* Arrives after the dust settles, one row at a time. */
	.has-js .hero-index-item {
		opacity: 0;
		translate: -0.75rem 0;
		transition:
			opacity var(--dur-slow) var(--ease-out),
			translate var(--dur-slow) var(--ease-out),
			color var(--dur) var(--ease-out),
			padding-left var(--dur) var(--ease-out);
		transition-delay: var(--reveal-delay, 0ms);
	}

	:root.intro-done .hero-index-item,
	html:not(.has-js) .hero-index-item {
		opacity: 1;
		translate: 0 0;
	}

	@media (prefers-reduced-motion: reduce) {
		.hero-index-item.is-current .hero-index-line {
			animation: none;
			scale: 1 1;
		}
	}

	/* Below this the index would crowd the copy; the filter bar covers it. */
	@media (max-width: 64rem) {
		.hero-index {
			display: none;
		}
	}

	/* --------------------------------------------------------- hero bar */

	/*
	 * A full-width rule along the bottom edge. It gives the composition a
	 * base line and carries what the frame is actually showing.
	 */
	/* ------------------------------------------------------ statement block */

	/*
	 * The site's own words, on the page's own surface rather than laid over a
	 * photograph. Given room, they read; over the picture they only covered it.
	 */
	.statement {
		padding-block: clamp(var(--space-2xl), 14vh, var(--space-4xl));
	}

	.statement-inner {
		display: grid;
		grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
		column-gap: clamp(var(--space-xl), 7vw, var(--space-3xl));
		align-items: start;
	}

	/* First block is the statement; whatever follows stacks in the second column. */
	.statement-inner > * {
		grid-column: 2;
		margin-block: 0 var(--space-m);
		max-width: 44ch;
	}

	.statement-inner > :first-child {
		grid-column: 1;
		grid-row: 1 / -1;
		margin: 0;
		max-width: 14ch;
	}

	.statement-inner > :last-child {
		margin-block-end: 0;
	}

	.statement-inner :is(h1, h2, h3) {
		font-family: var(--font-display);
		font-size: clamp(2.5rem, 5.4vw, 5rem);
		font-weight: 400;
		line-height: 0.98;
		letter-spacing: -0.025em;
		text-wrap: balance;
	}

	.statement-inner p {
		font-size: var(--step-1);
		line-height: 1.65;
		color: var(--ink-2);
	}

	.statement-inner .wp-block-coblocks-social ul {
		display: flex;
		flex-wrap: wrap;
		justify-content: flex-start;
		gap: var(--space-2xs);
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.statement-inner > p:empty {
		display: none;
	}

	@media (max-width: 60rem) {
		.statement-inner {
			grid-template-columns: minmax(0, 1fr);
			row-gap: var(--space-l);
		}

		.statement-inner > *,
		.statement-inner > :first-child {
			grid-column: 1;
			grid-row: auto;
			max-width: 38ch;
		}

		.statement-inner :is(h1, h2, h3) {
			max-width: 12ch;
		}
	}


	/*
	 * The rotation runs bright frames (fog, snow) as well as dark ones, so the
	 * scrim has to guarantee legibility on its own rather than relying on the
	 * photograph being dark. Two axes: down from the header, and up-and-left
	 * across the corner the copy occupies.
	 */
	.hero.has-sequence .hero-scrim {
		background:
			linear-gradient(to bottom, hsl(var(--over-scrim) / 0.55) 0%, hsl(var(--over-scrim) / 0) 26%),
			linear-gradient(
				to top,
				hsl(var(--over-scrim) / 0.92) 0%,
				hsl(var(--over-scrim) / 0.62) 26%,
				hsl(var(--over-scrim) / 0.18) 55%,
				hsl(var(--over-scrim) / 0) 78%
			),
			linear-gradient(to right, hsl(var(--over-scrim) / 0.55) 0%, hsl(var(--over-scrim) / 0) 60%);
	}

	/* The scroll cue would compete with the ceremony; it waits its turn. */
	.hero-cue {
		opacity: 0;
		transition: opacity var(--dur-slow) var(--ease-out) 600ms;
	}

	:root.intro-done .hero-cue,
	html:not(.has-js) .hero-cue {
		opacity: 0.7;
	}

	/* Nothing overlays the photograph now, so the hero keeps a full frame. */
	@media (max-width: 48rem) {
		.hero.has-sequence {
			height: 100svh;
		}
	}
}
