/*
 * Emma Dean Photography
 *
 * Font urls are ../../fonts/ because a url() in CSS resolves against the
 * stylesheet, not the page - this file sits in css/, one deeper than
 * index.html, whose own preload is ../fonts/. Relative rather than
 * root-absolute so both resolve over file:// as well as from the server.
 *
 * Six full-viewport walls, each filling the screen with photographs a
 * different way. No gutters, no cards, no paragraphs - the only text on the
 * page is the name in the corner.
 *
 * Kept deliberately plain. Two walls are a grid of a handful of big blocks,
 * two are a single photograph, and only two move: the opener and the river,
 * both driven by CSS scroll timelines so there is no scroll listener in the
 * JS. Both degrade to a still layout on a browser without them.
 */

@font-face {
	font-family: Lato;
	font-style: normal;
	font-weight: 700;
	font-display: swap;
	src: url("../../fonts/lato-latin-700-normal.woff2") format("woff2");
}
/*
 * Jost rather than Playfair. The name is the only display type on the site and
 * a high-contrast serif read as classical where the work is not; letterspaced
 * geometric caps are what a gallery would set it in. It is also a quarter of
 * Playfair's weight - two static cuts at ~9KB against a 38KB variable file.
 *
 * Playfair stays in /fonts/ because the parent portfolio still uses it.
 */
@font-face {
	font-family: Jost;
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("../../fonts/jost-latin-300-normal.woff2") format("woff2");
}
@font-face {
	font-family: Jost;
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url("../../fonts/jost-latin-400-normal.woff2") format("woff2");
}

:root {
	--ink: #08090a;
	--paper: #f2f0ec;
	--glow: #d6a05e;

	--display: Jost, "Century Gothic", "Futura", system-ui, sans-serif;
	--body: Lato, -apple-system, "Segoe UI", Roboto, sans-serif;

	/* The hairline between frames. Not a gutter - just enough to stop two
	   dark photographs bleeding into one another. */
	--seam: 2px;

	--lift: cubic-bezier(0.22, 0.61, 0.36, 1);

	/*
	 * A dark halo tight around the letterforms, so white text holds up over a
	 * snowfield or a white Icelandic sky as well as over a night one. Three
	 * stacked shadows rather than one: a close hard edge for the contrast, and
	 * two softer ones to take the ground down without looking like a box.
	 * Carried on a custom property so the animated glow can sit in front of it
	 * without either overwriting the other.
	 */
	--mark-legible: 0 1px 1px rgba(4, 5, 6, 1), 0 0 4px rgba(4, 5, 6, 0.95),
		0 0 14px rgba(4, 5, 6, 0.85), 0 0 34px rgba(4, 5, 6, 0.6);
}

*,
*::before,
*::after {
	box-sizing: border-box;
}

html {
	scroll-behavior: smooth;
	-webkit-text-size-adjust: 100%;
	background: var(--ink);
}

body {
	margin: 0;
	background: var(--ink);
	color: var(--paper);
	font-family: var(--body);
	overflow-x: hidden;
	/* Proximity rather than mandatory: each wall settles into place if you
	   stop near it, but a deliberate long scroll is never fought. */
	scroll-snap-type: y proximity;
}

img {
	display: block;
	max-width: 100%;
}

:focus-visible {
	outline: 2px solid var(--glow);
	outline-offset: -4px;
}

.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* ===========================================================  the frame  === */

/*
 * One photograph in a wall. Always fills whatever cell it is given, whatever
 * shape that cell is.
 */
.frame {
	position: relative;
	display: block;
	width: 100%;
	height: 100%;
	padding: 0;
	border: 0;
	background: #101214;
	overflow: hidden;
	cursor: pointer;
	/* Stops a 1px rounding seam showing between neighbouring cells. */
	outline: 1px solid var(--ink);
	outline-offset: -1px;
}

/*
 * Full strength by default. An earlier pass dimmed every frame slightly so a
 * hover could brighten one, but these are night photographs already and it
 * left the walls looking muddy - and on a touch screen, where there is no
 * hover to undo it, that dimmed state is the only state there is.
 */
.frame img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	scale: 1.001;
	transition: filter 0.55s var(--lift), scale 0.85s var(--lift);
}

.frame:hover img,
.frame:focus-visible img {
	scale: 1.06;
}

/*
 * There was a :has() rule here that dimmed every frame in a wall except the one
 * under the cursor. It was the cleverest thing in the file and it made the page
 * restless - the whole wall changed brightness every time the pointer crossed
 * it. A small scale on the frame you are actually pointing at says the same
 * thing quietly.
 */

/* =============================================================  the walls  == */

.wall {
	position: relative;
	width: 100%;
	height: 100svh;
	scroll-snap-align: start;
	overflow: hidden;
}

/* ---- 1. opening: one photograph, whole screen ---------------------------- */

.wall--open .frame img {
	filter: none;
	/* A very slow push in, so the first screen is never quite static. */
	animation: creep 32s ease-in-out infinite alternate;
}
.wall--open .frame:hover img {
	scale: 1.001;
	filter: none;
}

@keyframes creep {
	from {
		scale: 1.02;
	}
	to {
		scale: 1.12;
	}
}

/* ---- 2. mosaic: four blocks, exactly one screenful ----------------------- */

.wall--mosaic {
	display: grid;
	gap: var(--seam);
	grid-template-columns: repeat(12, 1fr);
	grid-template-rows: repeat(6, 1fr);
	/*
	 * Four blocks, each six columns by three rows, which lands at 1.6 - near
	 * enough to the 3:2 these photographs are that almost nothing is cropped.
	 * There are no upright cells here any more: every portrait photograph is
	 * on the upright strip, shown whole.
	 */
	grid-template-areas:
		"a a a a a a b b b b b b"
		"a a a a a a b b b b b b"
		"a a a a a a b b b b b b"
		"c c c c c c d d d d d d"
		"c c c c c c d d d d d d"
		"c c c c c c d d d d d d";
}

.wall--mosaic > *:nth-child(1) { grid-area: a; }
.wall--mosaic > *:nth-child(2) { grid-area: b; }
.wall--mosaic > *:nth-child(3) { grid-area: c; }
.wall--mosaic > *:nth-child(4) { grid-area: d; }

/* ---- 3. opener: each photograph draws down over the last ----------------- */

/*
 * A stack of full-screen layers pinned to the viewport. The first is the base
 * and just sits there; each one after it draws down over the one before from
 * the top edge of the screen, one after another, as you scroll past.
 *
 * clip-path does the reveal rather than width/height: it never reflows, and
 * the photograph behind the clip is already full size and sitting exactly
 * where it will end up - so it is uncovered rather than grown, and nothing
 * stretches or re-crops on the way.
 */
.opener {
	position: relative;
	height: 340svh;
	view-timeline-name: --opener;
	view-timeline-axis: block;
}

.opener-stick {
	position: sticky;
	top: 0;
	height: 100svh;
	overflow: hidden;
}

.opener-layer {
	position: absolute;
	inset: 0;
}

.opener-layer .frame {
	width: 100%;
	height: 100%;
}

@supports (animation-timeline: view()) {
	/* The base layer is left alone - something has to be under the first
	   one that opens, or the screen starts empty. */
	.opener-layer:not(:first-child) {
		animation: open-out linear both;
		animation-timeline: --opener;
	}
	/*
	 * `contain` is the stretch where the sticky child is pinned. Each layer
	 * gets a third of it, in order, so they wipe one after another rather than
	 * all at once.
	 */
	.opener-layer:nth-child(2) { animation-range: contain 2% contain 34%; }
	.opener-layer:nth-child(3) { animation-range: contain 34% contain 66%; }
	.opener-layer:nth-child(4) { animation-range: contain 66% contain 98%; }
}

/*
 * A wipe down from the top edge: the bottom inset starts at 100%, so the layer
 * shows nothing at all, and travels to 0 as the photograph draws down over the
 * one behind it.
 *
 * Starting at 100% rather than part-way matters for more than the look. The
 * animation fill holds the `from` state, so every layer still waiting its turn
 * is wearing it - anything visible there would sit on screen as a band, three
 * or four of them stacked at once.
 */
@keyframes open-out {
	from {
		clip-path: inset(0 0 100% 0);
	}
	to {
		clip-path: inset(0 0 0 0);
	}
}

/*
 * Without scroll-driven animations there is nothing to open the layers, so the
 * stack becomes what it would have been anyway: five full-screen photographs
 * you scroll through one after another.
 */
@supports not (animation-timeline: view()) {
	.opener {
		height: auto;
	}
	.opener-stick {
		position: static;
		height: auto;
	}
	.opener-layer {
		position: relative;
		inset: auto;
		height: 100svh;
		scroll-snap-align: start;
	}
}

/* ---- 4. upright: every portrait frame, full height, travelling left ------ */

/*
 * All six upright photographs at the full height of the screen and uncropped,
 * on a track that slides left as you scroll down - the same pinned-and-slid
 * arrangement as the river, but holding whole frames rather than cropped ones.
 *
 * Giving them a strip instead of a fixed pair is what lets every one of them
 * be shown properly. A 2:3 frame at full screen height is two-thirds of the
 * screen height wide, so only two fit across a laptop at once; a strip has no
 * such limit, because the ones that do not fit are simply further along it.
 * That in turn frees every cell on the mosaic and the grid to be landscape,
 * since no portrait photograph needs to be squeezed into one any more.
 */
.upright {
	position: relative;
	height: 360svh;
	view-timeline-name: --upright;
	view-timeline-axis: block;
}

.upright-stick {
	position: sticky;
	top: 0;
	height: 100svh;
	overflow: hidden;
}

.upright-track {
	display: flex;
	align-items: stretch;
	height: 100%;
	width: max-content;
	gap: clamp(0.75rem, 2vw, 2rem);
	padding-inline: clamp(0.75rem, 2vw, 2rem);
	will-change: transform;
}

.upright-track .frame {
	height: 100%;
	/* Matching the photographs exactly, so object-fit: cover crops nothing. */
	aspect-ratio: 2 / 3;
	width: auto;
	flex: 0 0 auto;
	outline: none;
}

@supports (animation-timeline: view()) {
	.upright-track {
		animation: slide-upright linear both;
		animation-timeline: --upright;
		animation-range: contain 0% contain 100%;
	}
}

/* Ends with the last frame flush to the right edge. */
@keyframes slide-upright {
	from {
		translate: 0 0;
	}
	to {
		translate: calc(100vw - 100%) 0;
	}
}

@supports not (animation-timeline: view()) {
	.upright {
		height: 100svh;
	}
	.upright-stick {
		overflow-x: auto;
		scrollbar-width: none;
	}
	.upright-stick::-webkit-scrollbar {
		display: none;
	}
}

/*
 * A phone is narrower than a full-height 2:3 frame is wide, so full height and
 * uncropped cannot both be had. Height wins, and the frame takes nearly the
 * screen width.
 */
@media (max-width: 860px) {
	.upright-track .frame {
		aspect-ratio: auto;
		width: 88vw;
	}
}

/* ---- 5. river: a sideways strip, dragged along by vertical scroll -------- */

/*
 * The tall outer element is only there to buy scroll distance. The inner one
 * sticks to the viewport while you scroll past it, and the track inside slides
 * left - so a vertical gesture reads as a horizontal one.
 */
.river {
	position: relative;
	/* Buys the scroll distance the slide runs over. Longer rows need more of
	   it or the strip races past. */
	height: 460svh;
	view-timeline-name: --river;
	view-timeline-axis: block;
}

.river-stick {
	position: sticky;
	top: 0;
	height: 100svh;
	overflow: hidden;
}

/*
 * Two rows, both travelling the same way. They used to cross in opposite
 * directions, which was the more striking idea and the harder one to look at -
 * two conflicting movements at once. Moving together, it reads as one band of
 * photographs sliding past.
 */
.river-rows {
	display: grid;
	grid-template-rows: 1fr 1fr;
	gap: var(--seam);
	height: 100%;
}

/*
 * Both rows are the same width to the pixel, which is the whole trick: give
 * the frames their natural aspect ratios instead and the two tracks come out
 * different lengths, drift apart as they travel, and one of them runs out
 * mid-scroll leaving half the screen empty. A fixed --span and a shared
 * --count means the travel distance below is identical for both.
 */
.river-row {
	/*
	 * 42vw against a half-height row lands near 3:2, which is what almost
	 * every landscape frame here is. At 30vw the cells came out square and
	 * threw away nearly half of each photograph.
	 */
	--span: 42vw;
	--count: 11;
	display: flex;
	height: 100%;
	/*
	 * A grid item's automatic minimum size is its content, so without this the
	 * 1fr tracks above cannot shrink below the height the photographs want at
	 * their fixed width - each row came out 783px tall in a 447px track, the
	 * pair of them overflowing the screen by most of its own height.
	 */
	min-height: 0;
	min-width: 0;
	/* No gap: the inset hairline on .frame already separates them, and a gap
	   would have to be added into the width maths for no visible gain. */
	gap: 0;
	width: calc(var(--span) * var(--count));
	will-change: transform;
}

.river-row > * {
	width: var(--span);
	height: 100%;
	flex: 0 0 auto;
}

@supports (animation-timeline: view()) {
	.river-row {
		animation: slide-left linear both;
		animation-timeline: --river;
		/* `contain` covers exactly the stretch where the sticky child is
		   pinned, so the slide starts and ends with the pin. */
		animation-range: contain 0% contain 100%;
	}
}

/* Travel = track width minus one screen, so it starts flush left and stops
   flush right with no empty edge at either end. */
@keyframes slide-left {
	from { translate: 0 0; }
	to { translate: calc(100vw - var(--span) * var(--count)) 0; }
}

/* No scroll-driven animations: collapse to one screen you can swipe by hand. */
@supports not (animation-timeline: view()) {
	.river {
		height: 100svh;
	}
	.river-stick {
		overflow-x: auto;
		scrollbar-width: none;
	}
	.river-stick::-webkit-scrollbar {
		display: none;
	}
}

/* ---- 6. grid: six blocks, the last full screen before the end ------------ */

/*
 * This was fifteen blocks and called a swarm, which is exactly how it looked.
 * Six is enough to feel packed without turning the photographs into wallpaper.
 * Still an explicit area map rather than auto-flow: spans that do not tile
 * leave holes the browser has nothing to back-fill from.
 */
.wall--grid {
	display: grid;
	gap: var(--seam);
	/*
	 * Twelve rows rather than six, purely for the finer granularity: on a
	 * 12x12 grid a block with equal column and row counts comes out at 1.6,
	 * so all six blocks here sit near 3:2 despite being three different sizes.
	 */
	grid-template-columns: repeat(12, 1fr);
	grid-template-rows: repeat(12, 1fr);
	grid-template-areas:
		"a a a a a a a a b b b b"
		"a a a a a a a a b b b b"
		"a a a a a a a a b b b b"
		"a a a a a a a a b b b b"
		"a a a a a a a a c c c c"
		"a a a a a a a a c c c c"
		"a a a a a a a a c c c c"
		"a a a a a a a a c c c c"
		"d d d d e e e e f f f f"
		"d d d d e e e e f f f f"
		"d d d d e e e e f f f f"
		"d d d d e e e e f f f f";
}

.wall--grid > *:nth-child(1) { grid-area: a; }
.wall--grid > *:nth-child(2) { grid-area: b; }
.wall--grid > *:nth-child(3) { grid-area: c; }
.wall--grid > *:nth-child(4) { grid-area: d; }
.wall--grid > *:nth-child(5) { grid-area: e; }
.wall--grid > *:nth-child(6) { grid-area: f; }

/* ---- 7. closing --------------------------------------------------------- */

.wall--close .frame img {
	filter: brightness(0.55) saturate(0.9);
}
.wall--close .frame:hover img {
	filter: brightness(0.7) saturate(1);
	scale: 1.02;
}

.close-mark {
	position: absolute;
	inset: 0;
	display: grid;
	place-content: center;
	gap: 1.5rem;
	justify-items: center;
	pointer-events: none;
}

.close-mark a {
	pointer-events: auto;
	position: relative;
	display: grid;
	place-items: center;
	width: 4rem;
	height: 4rem;
	border: 1px solid rgba(242, 240, 236, 0.35);
	border-radius: 50%;
	color: var(--paper);
	transition: color 0.35s var(--lift), border-color 0.35s var(--lift),
		box-shadow 0.45s var(--lift), scale 0.35s var(--lift);
}

/*
 * Hovered, it lights up like a star rather than filling in like a button:
 * Instagram's pink, and glow going outward instead of a solid ground going in.
 * Layered box-shadows do the halo - a tight ring, a near glow and a far one -
 * which on a near-black closing frame reads as something burning rather than
 * something painted.
 */
/*
 * It lights as the closing frame arrives, not only when pointed at. Hover was
 * the whole trigger, which meant that on a phone - where there is no hover -
 * the last thing on the site was a grey ring that never did anything. Driving
 * it from the wall's own view timeline gives every visitor the same moment,
 * and hover stays as an extra for those who have a pointer.
 */
@supports (animation-timeline: scroll()) {
	.close-mark a {
		animation: star-light linear both;
		/*
		 * The document's own scroll, not the closing wall's view timeline.
		 * view() looked right and was lit from the very top of the page: the
		 * wall it sits in has overflow:hidden, which makes it a scroll
		 * container, so the timeline resolved against an element that never
		 * scrolls and read as finished before it had begun. Running off the
		 * root scroll is also a truer reading of the intent - it lights when
		 * you reach the bottom of the site, not when one wall happens by.
		 */
		animation-timeline: scroll(root block);
		animation-range: 80% 100%;
	}
}

/*
 * A long approach and a short arrival. It starts plain white, picks up warmth
 * through the last fifth of the page so you can see it coming, then does most
 * of its work in the final few per cent - so hitting the bottom is an event
 * rather than the end of a fade.
 */
@keyframes star-light {
	0% {
		color: var(--paper);
		border-color: rgba(242, 240, 236, 0.35);
		box-shadow: 0 0 0 1px rgba(225, 48, 108, 0), 0 0 0 rgba(225, 48, 108, 0);
		scale: 1;
	}
	55% {
		color: #ffdce9;
		border-color: rgba(255, 146, 186, 0.45);
		box-shadow: 0 0 0 1px rgba(225, 48, 108, 0.12),
			0 0 10px rgba(225, 48, 108, 0.18);
		scale: 1;
	}
	85% {
		color: #ffa1c3;
		border-color: rgba(255, 120, 170, 0.6);
		box-shadow: 0 0 0 1px rgba(225, 48, 108, 0.26),
			0 0 18px rgba(225, 48, 108, 0.36);
		scale: 1.02;
	}
	96% {
		color: #ff7cae;
		border-color: rgba(255, 105, 160, 0.72);
		box-shadow: 0 0 0 1px rgba(225, 48, 108, 0.34),
			0 0 24px rgba(225, 48, 108, 0.48);
		scale: 1.05;
	}
	100% {
		color: #ff5c92;
		border-color: rgba(255, 92, 146, 0.95);
		box-shadow: 0 0 0 2px rgba(225, 48, 108, 0.6),
			0 0 36px rgba(225, 48, 108, 0.85), 0 0 92px rgba(225, 48, 108, 0.5);
		scale: 1.16;
	}
}

.close-mark a:hover,
.close-mark a:focus-visible {
	color: #ff5c92;
	border-color: rgba(255, 92, 146, 0.85);
	/* Not below the 1.16 the scroll animation reaches at the bottom of the
	   page, or pointing at it there would shrink it. */
	scale: 1.2;
	box-shadow: 0 0 0 1px rgba(225, 48, 108, 0.45),
		0 0 26px rgba(225, 48, 108, 0.6), 0 0 70px rgba(225, 48, 108, 0.32);
	/* Back to a clock, not the scroll, or the breathe has no timeline to run
	   on and never moves. */
	animation-name: star-breathe;
	animation-timeline: auto;
	animation-duration: 2.4s;
	animation-timing-function: ease-in-out;
	animation-iteration-count: infinite;
	animation-fill-mode: none;
}

.close-mark a:hover svg,
.close-mark a:focus-visible svg {
	filter: drop-shadow(0 0 7px rgba(255, 92, 146, 0.95))
		drop-shadow(0 0 20px rgba(225, 48, 108, 0.7));
}

/* The slow swell that keeps it from looking like a static ring. */
@keyframes star-breathe {
	0%,
	100% {
		box-shadow: 0 0 0 1px rgba(225, 48, 108, 0.45),
			0 0 22px rgba(225, 48, 108, 0.5), 0 0 58px rgba(225, 48, 108, 0.26);
	}
	50% {
		box-shadow: 0 0 0 1px rgba(225, 48, 108, 0.6),
			0 0 34px rgba(225, 48, 108, 0.72), 0 0 88px rgba(225, 48, 108, 0.4);
	}
}

.close-mark svg {
	width: 1.4rem;
	height: 1.4rem;
	transition: filter 0.35s var(--lift);
}

/* ==========================================================  without JS  ==== */

/*
 * Plain and vertical on purpose. None of the walls above exist without the
 * script that builds them, so this is not a degraded version of the design -
 * it is a different, simpler thing that happens to contain the photographs.
 */
.noscript {
	max-width: 60rem;
	margin: 0 auto;
	padding: clamp(2rem, 6vw, 5rem) clamp(1rem, 4vw, 3rem);
}
.noscript p {
	margin: 0 0 2rem;
	font-size: 1.05rem;
	line-height: 1.6;
	color: var(--paper);
}
.noscript a {
	color: var(--glow);
}
.noscript ul {
	display: grid;
	gap: 1rem;
	margin: 0;
	padding: 0;
	list-style: none;
}
.noscript img {
	width: 100%;
	height: auto;
}

/* ============================================================  the chrome  == */

/*
 * A breath of shade along the top edge, so plain white text is legible over any
 * photograph. The name used to be mix-blend-mode: difference, which reads over
 * a dark frame or a bright one but turns invisible against a mid-grey - and
 * needed this scrim underneath it anyway to cover that. Keeping the scrim and
 * dropping the blend mode is one idea instead of two.
 */
body::before {
	content: "";
	position: fixed;
	inset: 0 0 auto 0;
	z-index: 20;
	height: clamp(5rem, 12vh, 8rem);
	background: linear-gradient(to bottom, rgba(8, 9, 10, 0.38), transparent);
	pointer-events: none;
}

/*
 * A patch of shade that takes its cue from whatever is behind it. brightness()
 * on a backdrop-filter scales what is already there, so over a night frame it
 * changes almost nothing - the ground is near black either way - while over a
 * white Icelandic sky or a snowfield it pulls the whole area down far enough
 * for white text to read. A flat panel could not do both: dark enough for the
 * snow would be a visible slab over the night.
 */
.mark::before,
.to-instagram::before {
	content: "";
	position: absolute;
	inset: -0.55rem -0.85rem;
	z-index: -1;
	border-radius: 0.7rem;
	backdrop-filter: brightness(0.42) saturate(0.85) blur(2px);
	-webkit-backdrop-filter: brightness(0.42) saturate(0.85) blur(2px);
	pointer-events: none;
}

.to-instagram::before {
	inset: -0.3rem;
	border-radius: 50%;
}

/*
 * The patch is not there on the opening frame. Nothing is behind the marks yet
 * but one photograph chosen to sit under them, and they read perfectly well on
 * it - so the ground would be a box around nothing. It fades in over the first
 * part of the scroll, by which point what is behind them is whatever the page
 * happens to have reached and can no longer be relied on.
 *
 * Where there is no scroll timeline to fade it, the patch simply stays on:
 * legibility is not the thing to make conditional.
 */
@supports (animation-timeline: scroll()) {
	.mark::before,
	.to-instagram::before {
		animation: chrome-ground linear both;
		animation-timeline: scroll(root block);
		animation-range: 0 45vh;
	}
}

@keyframes chrome-ground {
	from {
		opacity: 0;
	}
	to {
		opacity: 1;
	}
}

/* No backdrop-filter: a flat translucent patch instead. Not adaptive, but it
   is the difference between legible and not on a bright frame. */
@supports not (backdrop-filter: brightness(0.5)) {
	.mark::before,
	.to-instagram::before {
		background: rgba(6, 7, 9, 0.5);
	}
}

/*
 * Set as letterspaced caps at a light weight. Tracking does the work that size
 * used to: the name reads as a mark rather than a heading, and stays quiet
 * over a photograph without needing to be big.
 *
 * The right-hand letter-space is trimmed off. Tracking adds a gap after the
 * final letter too, which would leave the name looking a few pixels further
 * from the corner than the Instagram icon opposite it.
 */
.mark {
	position: fixed;
	top: clamp(1rem, 2.5vw, 2rem);
	left: clamp(1rem, 2.5vw, 2rem);
	z-index: 30;
	font-family: var(--display);
	font-size: clamp(0.8rem, 0.68rem + 0.48vw, 1.15rem);
	font-weight: 300;
	line-height: 1;
	letter-spacing: 0.28em;
	text-indent: 0;
	margin-right: -0.28em;
	text-transform: uppercase;
	text-decoration: none;
	color: #fff;
	text-shadow: var(--mark-legible);
	pointer-events: auto;
}
.mark small {
	display: block;
	margin-top: 0.55rem;
	margin-right: -0.3em;
	font-family: var(--display);
	font-size: 0.52rem;
	font-weight: 400;
	letter-spacing: 0.3em;
	text-transform: uppercase;
	opacity: 0.72;
}

.to-instagram {
	position: fixed;
	top: clamp(1rem, 2.5vw, 2rem);
	right: clamp(1rem, 2.5vw, 2rem);
	z-index: 30;
	display: grid;
	place-items: center;
	width: 2.25rem;
	height: 2.25rem;
	color: #fff;
}
.to-instagram svg {
	width: 1.25rem;
	height: 1.25rem;
	/* The same help the name gets, in the form an SVG stroke can take. */
	filter: drop-shadow(0 1px 2px rgba(4, 5, 6, 0.9));
}

/*
 * Both marks run the whole length of the page rather than a fixed distance at
 * the top. An earlier pass ran over 22vh and reset to white, which on a
 * trackpad is one flick - blink and it was over. Over the full scroll there is
 * always something happening, and where you are on the page is legible from
 * the corner of your eye.
 *
 * No range is given, so the timeline is the entire document scroll.
 *
 * steps(1) is what makes the icon flicker rather than fade: the colour snaps
 * from one to the next instead of blending through the muddy midpoints
 * between purple, pink and orange.
 */
@supports (animation-timeline: scroll()) {
	.to-instagram {
		animation: ig-flicker steps(1, end) both;
		animation-timeline: scroll(root block);
	}
	/* Size and glow carry further than hue on a 20px icon, so they do as much
	   of the work as the colour does. */
	.to-instagram svg {
		animation: ig-swell linear both;
		animation-timeline: scroll(root block);
	}

	/*
	 * The name keeps its white, and it is the glow behind it that moves -
	 * amber, green, violet, pink and back, the colours of the photographs it
	 * is sitting on. Quieter than the icon by design: this one is read, not
	 * looked at.
	 */
	.mark {
		animation: mark-aurora linear both;
		animation-timeline: scroll(root block);
	}
}

@keyframes ig-flicker {
	0% { color: #fff; }
	3% { color: #833ab4; }
	6% { color: #fff; }
	10% { color: #e1306c; }
	14% { color: #fd1d1d; }
	18% { color: #fff; }
	23% { color: #f77737; }
	28% { color: #fcaf45; }
	32% { color: #fff; }
	37% { color: #833ab4; }
	43% { color: #e1306c; }
	48% { color: #fff; }
	53% { color: #f56040; }
	58% { color: #fcaf45; }
	63% { color: #fff; }
	68% { color: #c13584; }
	73% { color: #833ab4; }
	78% { color: #fff; }
	83% { color: #e1306c; }
	88% { color: #f77737; }
	93% { color: #fcaf45; }
	97% { color: #fff; }
	100% { color: #c13584; }
}

@keyframes ig-swell {
	0% { scale: 1; filter: none; }
	20% { scale: 1.3; filter: drop-shadow(0 0 10px rgba(225, 48, 108, 0.8)); }
	40% { scale: 1.1; filter: drop-shadow(0 0 7px rgba(252, 175, 69, 0.7)); }
	60% { scale: 1.32; filter: drop-shadow(0 0 11px rgba(131, 58, 180, 0.8)); }
	80% { scale: 1.12; filter: drop-shadow(0 0 7px rgba(225, 48, 108, 0.7)); }
	100% { scale: 1.26; filter: drop-shadow(0 0 10px rgba(193, 53, 132, 0.8)); }
}

@keyframes mark-aurora {
	0% { text-shadow: var(--mark-legible), 0 0 20px rgba(214, 160, 94, 0); }
	20% { text-shadow: var(--mark-legible), 0 0 24px rgba(214, 160, 94, 0.75); }
	40% { text-shadow: var(--mark-legible), 0 0 24px rgba(63, 211, 154, 0.7); }
	60% { text-shadow: var(--mark-legible), 0 0 24px rgba(147, 112, 219, 0.75); }
	80% { text-shadow: var(--mark-legible), 0 0 24px rgba(225, 48, 108, 0.65); }
	100% { text-shadow: var(--mark-legible), 0 0 22px rgba(214, 160, 94, 0.7); }
}


/* A hairline progress bar - the only indication of how far through you are. */
.progress {
	position: fixed;
	inset: auto 0 0 0;
	z-index: 30;
	height: 2px;
	background: var(--glow);
	transform-origin: left;
	scale: 0 1;
}

@supports (animation-timeline: scroll()) {
	.progress {
		animation: fill linear both;
		animation-timeline: scroll(root block);
	}
}
@keyframes fill {
	to {
		scale: 1 1;
	}
}

/*
 * The caption follows the cursor instead of living on the page. Nothing is
 * written on screen until you actually point at a photograph.
 */
.cursor-note {
	position: fixed;
	top: 0;
	left: 0;
	z-index: 40;
	padding: 0.4rem 0.7rem;
	background: var(--paper);
	color: var(--ink);
	font-size: 0.65rem;
	font-weight: 700;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	white-space: nowrap;
	pointer-events: none;
	opacity: 0;
	translate: -50% -180%;
	transition: opacity 0.2s var(--lift);
}
.cursor-note.is-on {
	opacity: 1;
}

/* No hover on touch, so the cursor caption never applies there. */
@media (hover: none) {
	.cursor-note {
		display: none;
	}
	/* Belt and braces: a touch browser that reports a lingering :hover after
	   a tap must not leave the rest of the wall stuck in the dimmed state. */
	.wall:has(.frame:hover) .frame:not(:hover) img,
	.river-rows:has(.frame:hover) .frame:not(:hover) img {
		filter: none;
	}
}

/* ==========================================================  the lightbox  == */

.lightbox {
	position: fixed;
	inset: 0;
	z-index: 60;
	display: none;
	width: 100%;
	height: 100%;
	max-width: none;
	max-height: none;
	margin: 0;
	padding: 0;
	border: 0;
	background: rgba(6, 7, 8, 0.97);
	backdrop-filter: blur(10px);
}
.lightbox[open] {
	display: grid;
	grid-template-rows: minmax(0, 1fr) auto;
}
.lightbox::backdrop {
	background: rgba(6, 7, 8, 0.9);
}

.lightbox-stage {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: clamp(1rem, 3vw, 3rem);
	min-height: 0;
	min-width: 0;
	overflow: hidden;
}

/*
 * flex-shrink is what keeps a tall frame inside the stage: an <img> is a
 * replaced element and will otherwise lay itself out at its intrinsic size and
 * push the caption off the bottom of the screen.
 */
.lightbox-stage img {
	flex: 0 1 auto;
	min-width: 0;
	min-height: 0;
	max-width: 100%;
	max-height: 100%;
	width: auto;
	height: auto;
	object-fit: contain;
	animation: ease-in 0.4s var(--lift);
}

@keyframes ease-in {
	from { opacity: 0; scale: 0.985; }
	to { opacity: 1; scale: 1; }
}

.lightbox-foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	padding: 1rem clamp(1rem, 3vw, 2rem);
	border-top: 1px solid rgba(255, 255, 255, 0.1);
	font-size: 0.68rem;
	font-weight: 700;
	letter-spacing: 0.16em;
	text-transform: uppercase;
}
.lightbox-foot p {
	margin: 0;
	color: rgba(242, 240, 236, 0.55);
}
.lightbox-foot b {
	color: var(--glow);
}
#lightbox-position {
	font-variant-numeric: tabular-nums;
	color: rgba(242, 240, 236, 0.4);
}

.round {
	display: grid;
	place-items: center;
	width: 2.75rem;
	height: 2.75rem;
	flex: 0 0 auto;
	padding: 0;
	border: 1px solid rgba(255, 255, 255, 0.2);
	border-radius: 50%;
	background: rgba(8, 9, 10, 0.6);
	color: var(--paper);
	cursor: pointer;
	transition: background 0.2s var(--lift), color 0.2s var(--lift),
		border-color 0.2s var(--lift);
}
.round:hover {
	background: var(--paper);
	border-color: var(--paper);
	color: var(--ink);
}
.round svg {
	width: 1.1rem;
	height: 1.1rem;
}

.lightbox-prev,
.lightbox-next,
.lightbox-close {
	position: absolute;
	z-index: 2;
}
.lightbox-prev,
.lightbox-next {
	top: 50%;
	translate: 0 -50%;
}
.lightbox-prev { left: clamp(0.5rem, 2vw, 1.5rem); }
.lightbox-next { right: clamp(0.5rem, 2vw, 1.5rem); }
.lightbox-close {
	top: clamp(0.75rem, 2vw, 1.5rem);
	right: clamp(0.5rem, 2vw, 1.5rem);
}

/* ============================================================  narrower  ==== */

@media (max-width: 900px) {
	:root {
		--seam: 1px;
	}

	/* Two blocks on a phone, not four. */
	.wall--mosaic {
		grid-template-columns: repeat(6, 1fr);
		grid-template-rows: repeat(6, 1fr);
		grid-template-areas:
			"a a a a a a"
			"a a a a a a"
			"a a a a a a"
			"b b b b b b"
			"b b b b b b"
			"b b b b b b";
	}
	.wall--mosaic > *:nth-child(n + 3) { display: none; }

	/* Three on a phone, not six, each a wide band rather than a tile. */
	.wall--grid {
		grid-template-columns: repeat(6, 1fr);
		grid-template-rows: repeat(6, 1fr);
		grid-template-areas:
			"a a a a a a"
			"a a a a a a"
			"b b b b b b"
			"b b b b b b"
			"c c c c c c"
			"c c c c c c";
	}
	.wall--grid > *:nth-child(1) { grid-area: a; }
	.wall--grid > *:nth-child(2) { grid-area: b; }
	.wall--grid > *:nth-child(3) { grid-area: c; }
	.wall--grid > *:nth-child(n + 4) { display: none; }

	/* Wider frames, fewer on screen at once - a 30vw frame on a phone is a
	   postage stamp. */
	/* A phone cannot show a 3:2 frame at half the screen height without it
	   being wider than the screen, so this is the nearest it gets. */
	.river-row {
		--span: 88vw;
	}
}

/* ==========================================================  calmer  ======== */

@media (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}
	body {
		scroll-snap-type: none;
	}
	*,
	*::before,
	*::after {
		animation-duration: 0.001s !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.001s !important;
	}
	/* Hand the river back as a strip the reader moves themselves, rather than
	   one that moves on its own. */
	.river {
		height: 100svh;
	}
	.river-stick {
		overflow-x: auto;
	}
	.river-track {
		animation: none;
	}
	/* Same for the upright strip: with nothing sliding, its extra height would
	   be three screens of dead scroll past a still picture. */
	.upright {
		height: 100svh;
	}
	.upright-stick {
		overflow-x: auto;
	}
	.upright-track {
		animation: none;
	}
	/* Same as the no-scroll-timeline fallback: five full screens in a row,
	   nothing opening or scaling. */
	.opener {
		height: auto;
	}
	.opener-stick {
		position: static;
		height: auto;
	}
	.opener-layer {
		position: relative;
		inset: auto;
		height: 100svh;
		animation: none;
		clip-path: none;
	}
	.opener-layer img {
		scale: 1;
	}
	.progress {
		display: none;
	}
	.to-instagram,
	.to-instagram svg,
	.mark {
		animation: none;
		color: #fff;
		scale: 1;
		filter: none;
	}
	/* The legibility shadow and the ground stay - neither is decoration. */
	.mark {
		text-shadow: var(--mark-legible);
	}
	.mark::before,
	.to-instagram::before {
		animation: none;
		opacity: 1;
	}
	/* The star still lights up - it just stops breathing. */
	.close-mark a,
	.close-mark a:hover,
	.close-mark a:focus-visible {
		animation: none;
		color: #ff5c92;
		border-color: rgba(255, 92, 146, 0.8);
		box-shadow: 0 0 0 1px rgba(225, 48, 108, 0.4),
			0 0 26px rgba(225, 48, 108, 0.55);
	}
}
