@layer components {
	/*
	 * The catalogue sits in the open band across the middle of a category's
	 * hero. The photograph lands first and holds alone; the cards arrive after
	 * it, one at a time and slowly, so the frame is read before it fills.
	 */
	.catalog {
		position: absolute;
		inset: auto 0 auto 0;
		/* Above the title block, below the header. */
		top: 50%;
		/*
		 * The lift is an absolute length, not part of the percentage: a
		 * percentage resolves against each element's own height, so the row and
		 * its heading would move by different amounts and close on each other.
		 */
		translate: 0 calc(-60% - var(--catalog-lift, 0px));
		z-index: 2;
		display: flex;
		align-items: center;
		gap: var(--space-s);
		padding-inline: var(--gutter);
		pointer-events: none;
	}

	.catalog.is-open {
		pointer-events: auto;
	}

	/*
	 * The category row, centred and standing well clear of the catalogue. Sat
	 * tight on top of the cards and pushed to one side it read as crowding
	 * them rather than heading them.
	 */
	/*
	 * Sat on top of the catalogue rather than measured down from the middle of
	 * the hero. A fixed offset from the centre held only at one window height:
	 * on a short screen it slid under the site header, and on a tall one it
	 * drifted away from the cards it heads.
	 */
	.catalog-head {
		position: absolute;
		inset: auto 0 100% 0;
		z-index: 2;
		margin-bottom: clamp(var(--space-m), 4vh, var(--space-xl));
		padding-inline: var(--gutter);
		pointer-events: none;
	}

	.catalog-head .filter-bar {
		justify-content: center;
	}

	.catalog-head .filter-bar {
		padding-block: 0;
		opacity: 0;
		translate: 0 0.75rem;
		transition: opacity var(--dur-slow) var(--ease-out),
			translate var(--dur-slow) var(--ease-out);
	}

	.catalog-head.is-open {
		pointer-events: auto;
	}

	.catalog-head.is-open .filter-bar {
		opacity: 1;
		translate: 0 0;
	}

	/*
	 * Over a photograph the chips cannot use the page's ink tokens — they
	 * would vanish on a bright frame. They wear the over-media pair instead.
	 */
	.filter-bar-over .filter-chip {
		border-color: color-mix(in oklab, var(--over-ink) 34%, transparent);
		background: hsl(var(--over-scrim) / 0.32);
		backdrop-filter: blur(10px);
		color: var(--over-ink);
	}

	.filter-bar-over .filter-chip:hover {
		border-color: var(--over-ink);
		background: hsl(var(--over-scrim) / 0.55);
	}

	.filter-bar-over .filter-chip[aria-current="true"] {
		background: var(--over-ink);
		border-color: var(--over-ink);
		color: hsl(var(--over-scrim));
	}

	.filter-bar-over .filter-chip .count {
		background: color-mix(in oklab, currentColor 16%, transparent);
	}

	.catalog-track {
		display: flex;
		gap: var(--space-s);
		/*
		 * The track claims the whole row between the two arrows, whether or not
		 * it has enough cards to fill it. Left to size itself it took its
		 * content's width, so a screen with only two cards — the journal — held
		 * a 620px row against the left edge of a 1900px band, and the centring
		 * set on it had no free space to work with. A galleryful overflows and
		 * shrinks to the same width either way, so nothing else moves.
		 */
		flex: 1;
		min-width: 0;
		/*
		 * `safe` is what makes this usable in a scroller: when the cards do
		 * overflow, centring them would push the first ones past the left edge
		 * with no way to scroll back to them, so the keyword drops the
		 * alignment to `start` exactly in that case. A browser too old to know
		 * it throws the whole declaration away and lands on `start` as well.
		 */
		justify-content: safe center;
		overflow-x: auto;
		scroll-behavior: smooth;
		scrollbar-width: none;
		padding-block: var(--space-2xs);
		/* Both edges fade, so the row reads as continuing past the frame. */
		mask-image: linear-gradient(
			to right,
			transparent 0,
			#000 4rem,
			#000 calc(100% - 4rem),
			transparent 100%
		);
	}

	.catalog-track::-webkit-scrollbar {
		display: none;
	}

	.catalog-track.is-dragging {
		scroll-behavior: auto;
		cursor: grabbing;
	}

	/* ------------------------------------------------------------- the cards */

	.catalog-card {
		position: relative;
		flex: none;
		width: clamp(150px, 15vw, 240px);
		aspect-ratio: 4 / 5;
		overflow: hidden;
		border-radius: var(--radius-lg);
		background: hsl(var(--over-scrim) / 0.4);
		box-shadow: var(--shadow-2);
		text-decoration: none;

		/*
		 * The arrival: in from its own side of the frame, out of a blur, both
		 * halves closing on the middle. `--card-x` is set per card by
		 * catalog.js, which knows which side each one starts from.
		 */
		opacity: 0;
		translate: var(--card-x, 4rem) 0;
		filter: blur(14px);
		transition:
			opacity 900ms var(--ease-out),
			translate 1100ms var(--ease-out),
			filter 900ms var(--ease-out);
		transition-delay: var(--card-delay, 0ms);
	}

	/* Offsets are placed with motion off; see stage() in catalog.js. */
	.catalog.is-staging .catalog-card {
		transition: none;
	}

	.catalog.is-open .catalog-card {
		opacity: 1;
		translate: 0 0;
		filter: blur(0);
	}

	/* Nothing waits on the cards any more, so the arrows come with them. */
	.catalog.is-open .catalog-nav,
	.catalog.is-open .catalog-remain {
		transition-delay: 500ms;
	}

	.catalog-card img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		-webkit-user-drag: none;
		user-select: none;
		transition: transform var(--dur-slow) var(--ease-out);
	}

	.catalog-card:hover img,
	.catalog-card:focus-visible img {
		transform: scale(1.06);
	}

	/* Its place in the set, so the row reads as a catalogue and not a carousel. */
	.catalog-n {
		position: absolute;
		left: var(--space-2xs);
		bottom: var(--space-2xs);
		padding: 0.2em 0.5em;
		border-radius: 100px;
		background: hsl(var(--over-scrim) / 0.55);
		backdrop-filter: blur(6px);
		font-size: 0.6rem;
		letter-spacing: 0.12em;
		font-variant-numeric: tabular-nums;
		color: var(--over-ink);
		opacity: 0;
		transition: opacity var(--dur-fast) var(--ease-out);
	}

	.catalog-card:hover .catalog-n,
	.catalog-card:focus-visible .catalog-n {
		opacity: 1;
	}

	/* --------------------------------------------------------------- arrows */

	/* Arrow with its tally beneath: how many photographs lie that way. */
	.catalog-side {
		flex: none;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 0.4rem;
	}

	.catalog-remain {
		min-height: 1em;
		font-size: 0.66rem;
		letter-spacing: 0.14em;
		font-variant-numeric: tabular-nums;
		color: color-mix(in oklab, var(--over-ink) 58%, transparent);
		opacity: 0;
		transition: opacity var(--dur-slow) var(--ease-out) 900ms;
	}

	.catalog.is-open .catalog-remain {
		opacity: 1;
	}

	.catalog-nav {
		flex: none;
		display: grid;
		place-items: center;
		width: 2.75rem;
		height: 2.75rem;
		border-radius: 50%;
		border: 1px solid color-mix(in oklab, var(--over-ink) 30%, transparent);
		background: hsl(var(--over-scrim) / 0.35);
		backdrop-filter: blur(8px);
		color: var(--over-ink);
		opacity: 0;
		transition: opacity var(--dur-slow) var(--ease-out) 900ms,
			border-color var(--dur-fast) var(--ease-out),
			background var(--dur-fast) var(--ease-out);
	}

	.catalog.is-open .catalog-nav {
		opacity: 1;
	}

	.catalog-nav:hover {
		border-color: var(--over-ink);
		background: hsl(var(--over-scrim) / 0.6);
	}

	.catalog-nav[disabled] {
		opacity: 0.25;
		pointer-events: none;
	}

	/*
	 * When the whole set fits there is nothing to step through, so the arrows
	 * go — hidden rather than removed, to keep the width they occupy and stop
	 * the cards shifting sideways once the row settles.
	 */
	.catalog.is-static .catalog-side {
		visibility: hidden;
	}

	/*
	 * A category hero needs a touch more scrim with cards laid over it.
	 *
	 * The three gradients darken the top, the foot and the left, and between
	 * them they left the middle band almost bare — which is exactly where the
	 * catalogue and its heading sit. Over a bright photograph that band gave
	 * white text nothing to stand on. The flat wash underneath sets a floor no
	 * photograph can rise above.
	 */
	.hero.is-pinned .hero-scrim {
		--hero-wash: 0.3;

		background:
			linear-gradient(to bottom, hsl(var(--over-scrim) / 0.5) 0%, hsl(var(--over-scrim) / 0) 26%),
			linear-gradient(to top, hsl(var(--over-scrim) / 0.8) 0%, hsl(var(--over-scrim) / 0.15) 58%),
			linear-gradient(to right, hsl(var(--over-scrim) / 0.35) 0%, hsl(var(--over-scrim) / 0) 55%),
			linear-gradient(hsl(var(--over-scrim) / var(--hero-wash)), hsl(var(--over-scrim) / var(--hero-wash)));
	}

	/* A phone has the same content over a quarter of the width; it needs more. */
	@media (max-width: 60rem) {
		.hero.is-pinned .hero-scrim {
			--hero-wash: 0.46;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.catalog-card {
			transition: none;
			opacity: 1;
			translate: none;
			filter: none;
		}
	}

	/* The phone has the deck below; a second row here would only crowd it. */
	@media (max-width: 60rem) {
		.catalog,
		.catalog-head {
			display: none;
		}
	}

	/*
	 * The catalogue is centred on the hero, but the title block is anchored to
	 * its foot at a fixed height. On a short screen the two close on each other
	 * and the hint lands across the count — so on those the whole row is lifted
	 * to give the hint back its clearance.
	 */
	@media (min-width: 60rem) and (max-height: 54rem) {
		.catalog,
		.catalog-head {
			--catalog-lift: 3.5rem;
		}
	}

	/* Shorter still: the heading closes on the cards to stay clear of the header. */
	@media (min-width: 60rem) and (max-height: 46rem) {
		.catalog,
		.catalog-head {
			--catalog-lift: 2.5rem;
		}

		.catalog-head {
			margin-bottom: var(--space-s);
		}
	}

	/* ------------------------------------------------- the title block */

	/*
	 * On a screen whose chips and catalogue are both centred, a title pinned
	 * to one corner reads as left over from another layout. Centred, and given
	 * the size the empty lower third can carry.
	 */
	.hero-body.is-centred {
		max-width: min(58rem, 100%);
		margin-inline: auto;
		text-align: center;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: var(--space-2xs);
	}

	.hero-body.is-centred .eyebrow {
		margin: 0;
		justify-content: center;
		letter-spacing: 0.28em;
	}

	/*
	 * Any eyebrow inside a hero is standing on a photograph, so it takes the
	 * over-ink pair rather than the theme's own. Left to `.eyebrow`'s default
	 * it was drawn in --ink-3 — a dim grey that reads as smudged on a dark
	 * frame in the light theme and no better in the dark one, because the
	 * frame does not change when the theme does.
	 */
	.hero .eyebrow,
	.contact .eyebrow {
		color: color-mix(in oklab, var(--over-ink) 60%, transparent);
	}

	.hero .eyebrow::after,
	.contact .eyebrow::after {
		background: color-mix(in oklab, var(--over-ink) 24%, transparent);
	}

	/* The leader rule belongs to left-aligned labels, not a centred one. */
	.hero-body.is-centred .eyebrow::after {
		display: none;
	}

	.hero-body.is-centred .hero-title {
		font-size: clamp(3.5rem, 9vw, 8rem);
		line-height: 0.92;
		letter-spacing: -0.035em;
	}

	.hero-body.is-centred .hero-lede {
		margin-top: var(--space-2xs);
		max-width: 52ch;
	}

	/*
	 * What the category is actually made of. Astro is 82 moons and a handful
	 * of Milky Ways — the breakdown is navigation, and it gives the title
	 * something to rest on instead of open space.
	 */
	.subjects {
		display: flex;
		flex-wrap: wrap;
		justify-content: center;
		gap: var(--space-2xs) var(--space-m);
		margin-top: var(--space-s);
	}

	.subject {
		display: inline-flex;
		align-items: baseline;
		/* Padding, so the target clears 24px while the label stays 0.7rem. */
		padding-block: 0.42em;
		gap: 0.45em;
		font-size: 0.7rem;
		letter-spacing: 0.18em;
		text-transform: uppercase;
		text-decoration: none;
		color: color-mix(in oklab, var(--over-ink) 62%, transparent);
		transition: color var(--dur-fast) var(--ease-out);
	}

	.subject:hover {
		color: var(--over-ink);
	}

	.subject span {
		font-size: 0.85em;
		font-variant-numeric: tabular-nums;
		opacity: 0.6;
	}

	/*
	 * A quiet word on how the row works, gone once it has been used. Anchored
	 * to the catalogue rather than to the screen — positioned by hand against
	 * the viewport it landed on top of the title block.
	 */
	.catalog-hint {
		position: absolute;
		top: 100%;
		left: 0;
		right: 0;
		margin: var(--space-s) 0 0;
		text-align: center;
		font-size: 0.62rem;
		letter-spacing: 0.24em;
		text-transform: uppercase;
		color: color-mix(in oklab, var(--over-ink) 42%, transparent);
		pointer-events: none;
		opacity: 0;
		transition: opacity var(--dur-slow) var(--ease-out) 1200ms;
	}

	body:has(.catalog.is-open) .catalog-hint {
		opacity: 1;
	}

	body:has(.catalog.is-used) .catalog-hint {
		opacity: 0;
		transition-delay: 0ms;
	}

	.catalog-hint span {
		margin-inline: 0.6em;
		opacity: 0.5;
	}

	/* ------------------------------------------------------- the archive screen */

	/*
	 * The archive is one screen, not a page: hero, categories, catalogue, and
	 * nothing else. No sheet, no footer, and no scroll to find them in.
	 */
	html:has(body.dz-screen),
	body.dz-screen {
		height: 100svh;
		overflow: hidden;
	}

	body.dz-screen .site-main {
		min-height: 0;
	}

	/*
	 * Everything after the hero goes, whatever it is. Naming the sheet by its
	 * sibling position missed it as soon as an empty content wrapper sat
	 * between the two — and that wrapper's padding was the overflow.
	 * The dialog is spared: it is the album, and it must still be able to open.
	 */
	body.dz-screen .site-main > *:not(.hero):not(dialog),
	body.dz-screen .site-footer {
		display: none;
	}

	/* Here the catalogue is the only way through the set, phone included. */
	@media (max-width: 60rem) {
		body.dz-screen .catalog {
			display: flex;
		}

		body.dz-screen .catalog-head {
			display: block;
			margin-top: -12rem;
		}

		body.dz-screen .catalog-head .filter-bar {
			flex-wrap: nowrap;
			overflow-x: auto;
			justify-content: flex-start;
			margin-inline: 0;
			padding-inline: 0;
		}

		/* Bigger cards: a phone shows three at most, so they may as well read. */
		body.dz-screen .catalog-card {
			width: clamp(150px, 46vw, 230px);
		}

		/* Centred and sized to match, like the desktop screen. */
		body.dz-screen .hero-body.is-centred .hero-title {
			font-size: clamp(3rem, 16vw, 4.5rem);
		}

		body.dz-screen .hero-body.is-centred .eyebrow {
			letter-spacing: 0.22em;
		}

		body.dz-screen .catalog-head .filter-bar {
			justify-content: flex-start;
		}

		body.dz-screen .catalog-head .filter-chip {
			font-size: 0.82rem;
			padding: 0.8em 1.3em;
		}

		body.dz-screen .subjects {
			gap: var(--space-3xs) var(--space-s);
		}

		body.dz-screen .catalog-hint {
			font-size: 0.58rem;
			letter-spacing: 0.2em;
		}

		/*
		 * The arrows say the row moves — without them a phone gives no sign
		 * there is more than the two cards it can see. They are lifted out of
		 * the flow and floated over the ends of the track, so unlike the
		 * desktop pair they cost the cards no width at all.
		 */
		body.dz-screen .catalog-side {
			display: flex;
			position: absolute;
			top: 50%;
			translate: 0 -50%;
			z-index: 3;
		}

		body.dz-screen .catalog-side:has([data-catalog-prev]) {
			left: calc(var(--gutter) - 0.9rem);
		}

		body.dz-screen .catalog-side:has([data-catalog-next]) {
			right: calc(var(--gutter) - 0.9rem);
		}

		body.dz-screen .catalog-nav {
			width: 2.1rem;
			height: 2.1rem;
			font-size: 0.85rem;
			background: hsl(var(--over-scrim) / 0.55);
			backdrop-filter: blur(6px);
		}

		/* An arrow with nowhere to go is noise, not an affordance. */
		body.dz-screen .catalog-nav[disabled] {
			opacity: 0;
		}

		/* No room for the remaining tally beside a card. */
		body.dz-screen .catalog-remain {
			display: none;
		}

		/* Both ends fade now, so an arrow always sits over a softened edge. */
		body.dz-screen .catalog-track {
			mask-image: linear-gradient(
				to right,
				transparent 0,
				#000 2.5rem,
				#000 calc(100% - 2.5rem),
				transparent 100%
			);
		}

		body.dz-screen .catalog {
			padding-inline: var(--gutter);
		}
	}

	/*
	 * Where the catalogue runs, the sheet of photographs below it is a second
	 * copy of the same set and has no job left. It stays in the markup because
	 * the phone turns it into the deck, and because it is the no-JS gallery.
	 */
	@media (min-width: 60.0625rem) {
		body:has(.catalog) .gallery-grid,
		body:has(.catalog) .gallery-grid + * {
			display: none;
		}

		body:has(.catalog) .hero.is-pinned + .rail {
			display: none;
		}
	}
}
