/*
 * Landing splash styling, shared by the standalone landing.html preview and the
 * Kirby snippet (site/snippets/landing.php).
 *
 * THE SHAPE OF THIS THING, because three earlier versions each got it wrong in
 * a different way:
 *
 * The splash is one fixed element (.landing) with its OWN scroll container
 * inside it (.landing-scroll). It never uses the document's scroll. The page
 * underneath is completely inert while the splash is up — html.landing-active
 * simply locks it — so nothing here depends on document height, scroll
 * restoration, where <header> sits in the markup, or 100vh vs the mobile URL
 * bar (panels are height: 100% of a fixed box, which has neither problem).
 *
 * Native scroll-snap does the stepping, inside that container. The one rule
 * that has to hold for that to work: MANDATORY SNAP MUST ALWAYS HAVE A VALID
 * SNAP POINT TO REST ON. The previous version snapped the whole document and
 * the last slide was the final snap area, so scrolling "past" it was pulled
 * straight back — the splash could never be scrolled out of, which is what
 * "stuck on the last slide" was. Here the runway ends with an extra blank
 * panel (.landing-panel--exit, written by landing.php), so leaving is itself a
 * normal snap position rather than an overshoot to be detected by heuristics.
 * Reaching it is what dismisses the splash (landing.js).
 *
 * The dismissal is two beats, and only the first is driven by scroll: the words
 * scroll away with the gesture, then the colour fades off them on its own clock
 * (--landing-exit below). Scroll-driving that fade too — as the first version of
 * this did — pins its length to the browser's own snap animation, a couple of
 * hundred milliseconds, and no amount of curve-shaping makes that not feel
 * rushed.
 *
 * Deliberately separate from shader.css: that file sets rules on `body`
 * (height, overflow: hidden) which suit a page that is nothing but a shader,
 * but would stop the homepage scrolling if loaded into the site.
 */

/* The page underneath doesn't scroll while the splash is up — the splash's own
   container does. Dropped the moment it's dismissed, and the document's scroll
   position was never touched, so the grid is simply there at the top. */
html.landing-active {
	overflow: hidden;
}

/* THE EXIT DIALS. Three of them, because the splash leaves at a different pace
   depending on what the reader did, and comes back at a third:
     --landing-exit    read all the way through — a slow lift (see .is-gone)
     --landing-skip    clicked Portfolio / pressed Escape — a fast cut, matching
                       the plain navigation the Info link next to it performs
     --landing-return  brought back by a site-title click
   CSS picks the right one with no JS bookkeeping: adding .is-gone takes its
   transition from the .is-gone rule, removing it takes the transition from this
   one, and .is-skipped (set by landing.js in arrive()) overrides the duration
   inside that. All three are mirrored by EXIT_MS / SKIP_MS / RETURN_MS in
   landing.js — the JS needs to know when each fade has finished to run its
   cleanup, so change each pair together. */
.landing {
	position: fixed;
	inset: 0;
	z-index: 2000;
	/* The two entrance dials, alongside the three exit ones above: how long the
	   splash sits on the bare sky before the first slide's words start to
	   arrive, and how long they then take to fade in on a genuine fresh
	   arrival. Mirrored by REVEAL_DELAY_MS / REVEAL_MS in landing.js — change
	   each pair together, the JS waits out delay + duration before it drops
	   .is-revealing. */
	--landing-reveal-delay: 300ms;
	--landing-reveal: 2000ms;
	--landing-exit: 1600ms;
	--landing-skip: 400ms;
	--landing-return: 600ms;
	transition: opacity var(--landing-return) ease;
}

/* Dismissed state, and the whole of the reveal: the words have already scrolled
   away by the time this lands (see landing.js's paint()), so this fade is the
   colour lifting off the grid — the slow second beat, on its own clock rather
   than tied to how fast the reader happened to scroll.
   Stays in the DOM; landing.js brings it back by removing this class, no page
   reload. visibility trails the fade by its full duration so it doesn't blink
   out early. ease-in-out, not ease: over this long a fade, `ease` dumps most of
   the change into the first third and then crawls. */
.landing.is-gone {
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
	transition:
		opacity var(--landing-exit) ease-in-out,
		visibility 0s linear var(--landing-exit);
}

/* Skipped rather than finished — Portfolio, Escape, or a same-page link in a
   slide. The reader has asked to be somewhere else, so this is a cut, not a
   lift: the splash is out of the way quickly while the grid underneath replays
   its own page-fade-in (landing.js's arrive() triggers that), which together
   read as the ordinary page navigation the Info link beside it performs.
   `ease` rather than the slow lift's ease-in-out — at 400ms there's no long
   middle to hold, and starting immediately is what makes it feel like a cut. */
.landing.is-gone.is-skipped {
	transition:
		opacity var(--landing-skip) ease,
		visibility 0s linear var(--landing-skip);
}

/* No runway to step through — no slides configured, or a reader who asked for
   reduced motion. landing.js sets this, and a click anywhere dismisses. */
.landing.is-plain {
	cursor: pointer;
}

.landing-sky {
	position: absolute;
	inset: 0;
	z-index: 0;
}

/* shader.js sizes its canvas to the window, so this just anchors it.
   The gradient is not only the cold-start stand-in — on mobile the shader is
   skipped entirely (see shader.js) and this IS the sky, so it has to travel the
   same arc landing.js drives the shader through (see the SKY_AMP comment there:
   the journey pushes u_mouse.y past fragment.glsl's normal 0..1 range so both
   ends saturate to a fully pure colour, a bigger shift than the shader's own
   mouseRange alone gives).
   It's a tall gradient landing.js slides through: 348.89% of the viewport,
   because at SKY_AMP 1.6 the whole driven arc spans 1.57 in the shader's `p`
   while one screen shows `windowSize` (0.45) of it. The colours are
   fragment.glsl's dusk/day exactly (#CF6B00 / #194867) — at this arc both ends
   fully saturate, so this needs four stops: flat day at the top (0% to
   21.975%, where the shader would still be clamped to pure day), a linear ramp
   down to flat dusk (85.669% to 100%, where it clamps the other way). Because
   that ramp is linear and so is the shader's own mix(), the two match exactly
   rather than approximate. landing.js drives --sky-pos from 100% (dusk) to 0%
   (twilight). */
.landing-shader {
	position: absolute;
	inset: 0;
	background-image: linear-gradient(
		#194867 0%,
		#194867 21.975%,
		#cf6b00 85.669%,
		#cf6b00 100%
	);
	background-size: 100% 348.89%;
	background-position: 0 var(--sky-pos, 100%);
	background-repeat: no-repeat;
	transition: background-position 0.6s ease;
}

.landing-shader canvas {
	display: block;
}

/* The runway. Its own scroller, so the browser's native wheel/touch/trackpad
   physics and scroll-snap do the stepping — the thing landing.js used to try to
   reconstruct by hand. Keys are handled explicitly in landing.js instead of by
   focusing this (discrete, no momentum to get wrong, and no focus-stealing).
   Scrollbar hidden: this reads as a sequence of cards, and its own scrollbar
   next to the page's would just look broken. */
.landing-scroll {
	position: absolute;
	inset: 0;
	z-index: 1;
	overflow-x: hidden;
	overflow-y: scroll;
	scroll-snap-type: y mandatory;
	/* Never chain a flick at either end out to the page underneath (or to
	   Safari's rubber-band), which would move the grid out of position behind
	   the splash. */
	overscroll-behavior: contain;
	scrollbar-width: none;
	cursor: pointer;
}

.landing-scroll::-webkit-scrollbar {
	width: 0;
	height: 0;
}

/* height: 100% of the fixed .landing box, NOT 100vh: on mobile 100vh is the
   URL-bar-expanded height, which doesn't match what's actually visible and
   changes as the bar hides, moving every snap point mid-scroll.
   Padding matches the site's own left margin: body { padding: 3% } (site.css)
   resolves percentage padding against the containing block's WIDTH on every
   side, so the site's text sits 3vw in — reproduced here directly, since this
   is inside a fixed box rather than in body's flow. */
.landing-panel {
	display: flex;
	align-items: center;
	height: 100%;
	padding: 0 3vw;
	scroll-snap-align: start;
	/* A fast flick can't skip a slide — every one gets its moment, the same
	   guarantee scroll-snap-stop: always gives on any slider. */
	scroll-snap-stop: always;
}

/* The last panel, written by landing.php: deliberately empty. It's what gives
   mandatory snap somewhere legitimate to rest past the final slide (see the file
   header), and settling on it is the dismissal — so what the reader sees of it
   is the sky alone for the length of --landing-exit, while the colour fades off
   the grid. It inherits .landing-panel's height: 100% and MUST keep it: a
   shorter final panel would put its snap point past the container's own maximum
   scroll, leaving it unreachable, and mandatory snap would drag the reader
   straight back to the last slide — the exact trap this panel exists to avoid. */
.landing-panel--exit {
	scroll-snap-stop: normal;
}

/* Opacity is set directly from scroll position (landing.js) so it cross-fades
   under the gesture; a CSS transition here would only lag behind what the
   reader is already doing. */
.landing-panel-text {
	width: min(15em, 100%);
	font-family: "Space Mono", ui-monospace, Menlo, Consolas, monospace;
	font-size: clamp(28px, 5.2vw, 64px);
	font-weight: 400;
	line-height: 1.14;
	letter-spacing: -0.02em;
	color: #000;
	opacity: 0;
}

/* Initial arrival only. landing.js adds .is-revealing for exactly long enough
   to fade the first slide's words in from nothing, then removes it — every
   later cross-fade stays the instant, scroll-driven write described above and
   doesn't lag behind the gesture. */
.landing.is-revealing .landing-panel-text {
	transition: opacity var(--landing-reveal) ease var(--landing-reveal-delay);
}

.landing-panel-text a {
	color: inherit;
	text-decoration: none;
}

.landing-panel-text a:hover {
	text-decoration: underline;
	text-underline-offset: 0.1em;
	text-decoration-thickness: 1px;
}

/* Sits exactly where the real header's site title does — see the .landing-panel
   note above for why that's 3vw on both axes.
   pointer-events stays off on the heading itself and is turned back on for just
   the link inside it (below): this box is up to 640px wide, and clicking the
   empty space beside a short title shouldn't leave the splash — only the words
   should. */
.landing-title {
	position: absolute;
	top: 3vw;
	left: 3vw;
	z-index: 2;
	margin: 0;
	max-width: min(640px, 85vw);
	font-family: "Space Mono", ui-monospace, Menlo, Consolas, monospace;
	font-weight: 700;
	/* Matches site.css's --fs-mono. Hardcoded rather than using the variable
	   because this file is also loaded by the standalone landing.html preview,
	   which has no site.css — change the two together. */
	font-size: 14px;
	color: #000;
	pointer-events: none;
	user-select: none;
}

/* The way out (see landing.php). text-decoration: none is doing real work here,
   not tidying: site.css underlines every <a> by default, and the real header's
   title — which this sits exactly on top of and cross-fades into — is not
   underlined. */
.landing-title a {
	color: inherit;
	text-decoration: none;
	pointer-events: auto;
}

/* The splash's copy of the header's top-right menu (site.css .site-nav) — the
   quick exit. Lands on exactly the same spot as the real one: that sits in
   .header-top, right-aligned inside body's 3% padding and baseline-aligned with
   the site title, and matching the title's own top: 3vw with the same font and
   size puts these links on the same baseline for free.
   Note there's no pointer-events: none here, unlike .landing-title — these are
   the one thing in the splash that has to be clickable in its own right. The
   click still reaches .landing's handler by bubbling, which is what dismisses
   (see landing.php). */
.landing-nav {
	position: absolute;
	top: 3vw;
	right: 3vw;
	z-index: 2;
	display: flex;
	gap: 20px;
	font-family: "Space Mono", ui-monospace, Menlo, Consolas, monospace;
	font-size: 14px;   /* site.css --fs-mono; see .landing-title */
	user-select: none;
}

.landing-nav a {
	color: #000;
	text-decoration: none;
}

.landing-nav a:hover {
	text-decoration: underline;
}

/* Bottom-left, on the same margin as everything else. Fades out as soon as the
   reader starts moving — it has done its job by then. */
.landing-hint {
	position: absolute;
	left: 3vw;
	bottom: 3vw;
	z-index: 2;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 12px;
	/* Mono, like the title and nav above it: this is an instruction, not
	   writing. site.css's --font-mono / --fs-mono, hardcoded because the
	   standalone landing.html preview has no site.css. */
	font-family: "Space Mono", ui-monospace, Menlo, Consolas, monospace;
	font-size: 14px;
	color: #000;
	pointer-events: none;
	user-select: none;
	transition: opacity 0.3s ease;
}

.landing-hint-line {
	position: relative;
	width: 1px;
	height: 34px;
	overflow: hidden;
	background: rgba(0, 0, 0, 0.25);
}

/* A stroke falling down the line — the direction of travel, without an arrow */
.landing-hint-line::after {
	content: "";
	position: absolute;
	inset: 0;
	background: #000;
	transform: translateY(-100%);
	animation: landing-hint-fall 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

@keyframes landing-hint-fall {
	0%        { transform: translateY(-100%); }
	60%, 100% { transform: translateY(100%); }
}

/* How far through you are. One pixel, low contrast — a horizon line more than
   a scrollbar. */
.landing-progress {
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: 2;
	height: 1px;
	background: rgba(0, 0, 0, 0.12);
	pointer-events: none;
}

.landing-progress span {
	display: block;
	height: 100%;
	background: rgba(0, 0, 0, 0.45);
	transform: scaleX(0);
	transform-origin: 0 50%;
	transition: transform 0.25s ease;
}

/* One dot per slide, right-hand margin, vertically centred — how many slides
   there are, always visible as a count. Each dot's opacity is written by the
   same paint() cross-fade weight as that slide's own text (landing.js), so
   the "current" one is simply whichever is lit brightest — not a second
   position concept to keep in step with the first. */
.landing-dots {
	position: absolute;
	top: 50%;
	right: 3vw;
	z-index: 2;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 14px;
	transform: translateY(-50%);
	pointer-events: none;
}

.landing-dot {
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: #000;
}

/* Below 600px the site swaps body's 3% padding for a flat 20px (site.css) —
   3vw would keep shrinking past that breakpoint, drifting from the match. */
@media (max-width: 600px) {
	/* 24px, not body's 20px, and measured rather than reasoned: site.css gives
	   .site-nav a 4px of vertical padding at this breakpoint for a
	   finger-friendly tap target, and .header-top aligns its children on
	   BASELINE — so that padding drags the real site title 4px down the page
	   too, to 24px. These absolutely-positioned copies get no such shift for
	   free, so they have to reproduce it by hand or the title and menu visibly
	   drop 4px as the splash fades off them. If that padding ever changes,
	   these two numbers move with it. */
	.landing-title {
		top: 24px;
		left: 20px;
		/* Steps down with the real header: site.css drops --fs-mono to 14px at
		   this breakpoint, and these copies have to land on the same size or the
		   words visibly resize as the splash fades off them. */
		font-size: 14px;
	}

	/* 20px is the padded box's top; its 4px of padding puts the text itself on
	   24px, level with the title above. Gap is deliberately NOT overridden —
	   it has to stay the 20px the real .site-nav uses, or the first item lands
	   in a different place from the one it's fading into. */
	.landing-nav {
		top: 20px;
		right: 20px;
		font-size: 14px;   /* with the title above; see .landing-title */
	}

	.landing-nav a {
		padding: 4px 0;
	}

	.landing-panel {
		padding: 0 20px;
	}

	.landing-hint {
		left: 20px;
		bottom: max(20px, env(safe-area-inset-bottom));
	}

	/* One progress indicator per screen. The dots and the bottom .landing-progress
	   line say the same thing, and at this width the dots also crowd the slide
	   text (.landing-panel-text resolves to the full content box here), so the
	   bar keeps the job and the dots stand down. Desktop keeps both. */
	.landing-dots {
		display: none;
	}
}

/* No stepping without motion: landing.js skips the scroll machinery entirely
   here and leaves the splash as a plain held card — every slide's text stacked
   and statically visible under the title, dismissed with a click anywhere. */
@media (prefers-reduced-motion: reduce) {
	/* .is-skipped has to be listed explicitly: .landing.is-gone.is-skipped is a
	   more specific selector than .landing.is-gone, so without it the skip's own
	   transition would win here and animate anyway. */
	.landing,
	.landing.is-gone,
	.landing.is-gone.is-skipped {
		transition: none;
	}

	.landing-hint,
	.landing-progress,
	.landing-dots {
		display: none;
	}

	.landing-scroll {
		scroll-snap-type: none;
		/* Clears the fixed .landing-title above it: its own 3vw top offset,
		   plus its line box, plus the About page's title-to-body gap. */
		padding: calc(3vw + 74px) 3vw 3vw;
	}

	.landing-panel {
		display: block;
		height: auto;
		padding: 0;
		max-width: min(600px, 85vw);
		scroll-snap-align: none;
	}

	/* Nothing to rest on and nothing to reveal — the click handles both. */
	.landing-panel--exit {
		display: none;
	}

	.landing-panel-text {
		width: auto;
		margin: 0 0 1em;
		font-family: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;
		font-size: 14px;
		line-height: 1.6;
		letter-spacing: normal;
		opacity: 1;
	}

	.landing-panel-text p {
		margin: 0 0 1em;
	}

	.landing-panel-text p:last-child {
		margin-bottom: 0;
	}

	.landing-hint-line::after {
		animation: none;
	}
}

@media (prefers-reduced-motion: reduce) and (max-width: 600px) {
	.landing-scroll {
		padding: 70px 20px 20px;
	}
}
