/*
 * The gate — visual styling for public/index.php.
 *
 * Loaded after tokens.css, so every colour, size and speed below comes from
 * a variable defined there. This page is the dark, restrained face of the
 * brand: an ink background, paper-coloured text, and light purple as the
 * one interactive accent — on the cursor and on the single call-to-action.
 */

*,
*::before,
*::after {
	box-sizing: border-box;
}

html {
	overflow-x: hidden;
}

body.gate-page {
	margin: 0;
	min-height: 100vh;
	min-height: 100dvh;
	overflow-x: hidden;
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--space-large) var(--space-small);
	background: var(--ink);
	color: var(--paper);
	font-family: var(--font);
	font-size: var(--text-body);
	line-height: var(--line-height-text);
}

.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* Clouds — layered, blurred radial gradients drifting on long transforms. */

.gate-clouds {
	/*
	 * Written by portal.js as the pointer moves. Kept as custom properties so
	 * the script never touches layout: it sets two numbers and the transform
	 * below does the rest. The long transition is what makes the fog feel
	 * heavy, as if it is being pushed rather than dragged.
	 */
	--fog-x: 0px;
	--fog-y: 0px;

	position: fixed;
	inset: 0;
	z-index: 0;
	overflow: hidden;
	pointer-events: none;
	background:
		radial-gradient(ellipse 120% 70% at 50% 115%, rgba(168, 139, 255, 0.20), transparent 60%),
		var(--ink);
	/* Subtle vignette at the edges deepens the atmospheric mystery. */
	box-shadow: inset 0 0 100px rgba(20, 20, 15, 0.5);
	/* Scaled up so the parallax never drags an empty edge into view. */
	transform: scale(1.1) translate3d(var(--fog-x), var(--fog-y), 0);
	transition: transform 1100ms var(--ease-out);
}

/*
 * The near plane. Two pools of near-solid ink that sit above the clouds and
 * move roughly three times as far with the pointer, which is what reads as
 * depth: far things barely shift, near things swing. It also does the real
 * work of keeping the corners black, so the middle of the screen is the only
 * place anything is legible.
 */

.gate-veil {
	--veil-x: 0px;
	--veil-y: 0px;

	position: fixed;
	inset: -14%;
	z-index: 0;
	pointer-events: none;
	background:
		radial-gradient(ellipse 58% 48% at 18% 26%, rgba(11, 11, 8, 0.92), transparent 64%),
		radial-gradient(ellipse 62% 52% at 84% 76%, rgba(11, 11, 8, 0.95), transparent 66%),
		radial-gradient(ellipse 70% 40% at 50% 108%, rgba(11, 11, 8, 0.85), transparent 62%);
	transform: translate3d(var(--veil-x), var(--veil-y), 0);
	transition: transform 700ms var(--ease-out);
}

/*
 * The lantern. A pool of light carried by the pointer, so the fog is only ever
 * lit where the visitor is looking. Switched on by portal.js, which adds
 * js-lantern to the body, and only for a precise pointer with no reduced-motion
 * preference: on a phone there is no pointer to carry it and it would just be a
 * bright smear stuck in one place.
 */

.gate-lantern {
	--pointer-x: 50vw;
	--pointer-y: 50vh;

	position: fixed;
	left: 0;
	top: 0;
	width: 44rem;
	height: 44rem;
	margin: -22rem 0 0 -22rem;
	z-index: 0;
	opacity: 0;
	pointer-events: none;
	background: radial-gradient(
		circle,
		rgba(168, 139, 255, 0.17),
		rgba(168, 139, 255, 0.06) 38%,
		transparent 68%
	);
	transform: translate3d(var(--pointer-x), var(--pointer-y), 0);
	transition: opacity 900ms var(--ease-out);
	will-change: transform;
}

body.js-lantern .gate-lantern {
	opacity: 1;
}

/* Breathing light behind the clouds — a faint glow that pulses subtly. */
.gate-clouds::before {
	content: '';
	position: absolute;
	inset: 0;
	background: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(245, 244, 239, 0.05), transparent 70%);
	animation: gate-light-breathe 8s ease-in-out infinite;
	pointer-events: none;
}

/*
 * Position and size live on the layer, the look and the endless drift live on
 * the blob inside it. Keeping them apart is what lets the parting work: both
 * effects want to write transform and opacity, and a CSS animation outranks a
 * plain declaration, so on a single element the drift would silently swallow
 * the parting. Two nested elements multiply their transforms instead.
 */

.gate-clouds__layer {
	position: absolute;
}

.gate-clouds__layer--one {
	left: -18vw;
	top: 0vh;
	width: 62vw;
	height: 62vw;
}

.gate-clouds__layer--two {
	right: -22vw;
	top: 18vh;
	width: 70vw;
	height: 70vw;
}

.gate-clouds__layer--three {
	left: 8vw;
	bottom: -24vh;
	width: 52vw;
	height: 52vw;
}

.gate-clouds__layer--four {
	right: 2vw;
	bottom: -18vh;
	width: 46vw;
	height: 46vw;
}

.gate-clouds__layer--five {
	left: 32vw;
	top: -14vh;
	width: 34vw;
	height: 34vw;
}

.gate-clouds__blob {
	position: absolute;
	inset: 0;
	border-radius: 50%;
	filter: blur(60px);
	will-change: transform, opacity;
}

.gate-clouds__blob--one {
	background: radial-gradient(circle, rgba(168, 139, 255, 0.32), transparent 70%);
	filter: blur(58px);
	/* Vary blur and animate opacity for layered depth. */
	animation: gate-drift-one 28s ease-in-out infinite alternate;
}

.gate-clouds__blob--two {
	background: radial-gradient(circle, rgba(110, 59, 245, 0.26), transparent 70%);
	filter: blur(65px);
	/* Darker purple gets slightly more blur for atmospheric depth. */
	animation: gate-drift-two 34s ease-in-out infinite alternate;
}

.gate-clouds__blob--three {
	background: radial-gradient(circle, rgba(59, 31, 168, 0.38), transparent 70%);
	filter: blur(60px);
	animation: gate-drift-three 22s ease-in-out infinite alternate;
}

.gate-clouds__blob--four {
	background: radial-gradient(circle, rgba(168, 139, 255, 0.18), transparent 70%);
	filter: blur(58px);
	animation: gate-drift-four 30s ease-in-out infinite alternate;
}

.gate-clouds__blob--five {
	background: radial-gradient(circle, rgba(245, 244, 239, 0.07), transparent 70%);
	filter: blur(55px);
	/* Lightest blob: less blur keeps it visible and adds to the illusion of distance. */
	animation: gate-drift-five 25s ease-in-out infinite alternate;
}

/* Stronger, faster drifts now complete a full cycle in 22–34 seconds instead of 65–125. */
/* Movement is now clearly visible within 10 seconds of page load. */
/* Opacity animates to create the illusion of clouds thickening and thinning. */

@keyframes gate-drift-one {
	from { transform: translate(0, 0) scale(1); opacity: 0.4; }
	to   { transform: translate(28vw, 20vh) scale(1.18); opacity: 1; }
}

@keyframes gate-drift-two {
	from { transform: translate(0, 0) scale(1); opacity: 0.35; }
	to   { transform: translate(-35vw, 14vh) scale(1.22); opacity: 0.95; }
}

@keyframes gate-drift-three {
	from { transform: translate(0, 0) scale(1); opacity: 0.45; }
	to   { transform: translate(24vw, -18vh) scale(1.25); opacity: 1; }
}

@keyframes gate-drift-four {
	from { transform: translate(0, 0) scale(1); opacity: 0.25; }
	to   { transform: translate(-20vw, -22vh) scale(1.20); opacity: 0.9; }
}

@keyframes gate-drift-five {
	from { transform: translate(0, 0) scale(1); opacity: 0.5; }
	to   { transform: translate(30vw, 24vh) scale(1.23); opacity: 0.8; }
}

/* Breathing light — pulses subtly to keep the page feeling alive and mysterious. */
@keyframes gate-light-breathe {
	0%   { opacity: 0.2; }
	50%  { opacity: 0.5; }
	100% { opacity: 0.2; }
}

/* Parting — a class the JavaScript enhancement adds on a correct phrase. */

/*
 * The clouds close, they do not open. The opening happens on the other side.
 *
 * A correct phrase pulls all five clouds in toward the middle until they have
 * swallowed the screen, and the page behind the gate is loaded underneath that
 * cover. That page renders the same clouds over itself and pulls them apart
 * once it has painted, so the reveal lands on the real site rather than on a
 * gate pretending to be one. See config/reveal-clouds.php and reveal.css.
 *
 * The drift freezes and the layer around it does the moving. The layer is
 * never animated, only transitioned, so nothing outranks these declarations.
 */

body.is-parting .gate-clouds__blob {
	animation-play-state: paused;
}

body.is-parting .gate-clouds__layer {
	transition: transform 1.25s var(--ease-out), opacity 1.25s var(--ease-out);
}

body.is-parting .gate-clouds__layer--one   { transform: translate(26vw, 20vh) scale(2);    opacity: 1; }
body.is-parting .gate-clouds__layer--two   { transform: translate(-28vw, 12vh) scale(2);   opacity: 1; }
body.is-parting .gate-clouds__layer--three { transform: translate(22vw, -24vh) scale(1.9); opacity: 1; }
body.is-parting .gate-clouds__layer--four  { transform: translate(-22vw, -20vh) scale(1.9); opacity: 1; }
body.is-parting .gate-clouds__layer--five  { transform: translate(0, 26vh) scale(2.4);     opacity: 1; }

/* Pause the breathing light during parting so attention focuses on the light reveal. */
body.is-parting .gate-clouds::before {
	animation-play-state: paused;
}

body.is-parting .gate-clouds::after {
	content: '';
	position: absolute;
	inset: 0;
	opacity: 0;
	background: radial-gradient(ellipse 60% 50% at 50% 55%, rgba(245, 244, 239, 0.9), rgba(168, 139, 255, 0.3) 45%, transparent 75%);
	animation: gate-light-reveal 1.9s var(--ease-out) forwards;
}

/*
 * The bloom is the moment the phrase is accepted, so it peaks early and is
 * then smothered by the clouds coming in over it. Holding it bright to the end
 * would read as a flash instead of a light being covered.
 */

@keyframes gate-light-reveal {
	0%   { opacity: 0; }
	22%  { opacity: 0.85; }
	55%  { opacity: 0.4; }
	100% { opacity: 0; }
}

/*
 * The shockwave. A ring of light that leaves the caret a moment after the
 * phrase lands and pushes outward past the edge of the screen, arriving just
 * ahead of the clouds being blown apart, so the fog looks like it is moving
 * because of the wave rather than alongside it.
 *
 * Drawn as a radial-gradient band rather than a border because a scaled border
 * thickens with the scale factor and would be a fifty-pixel hoop by the end.
 * A gradient band grows in proportion, which is what a shockwave actually does.
 */

.gate-shockwave {
	position: fixed;
	left: 50%;
	top: 50%;
	width: 120vmax;
	height: 120vmax;
	margin: -60vmax 0 0 -60vmax;
	z-index: 2;
	opacity: 0;
	pointer-events: none;
	border-radius: 50%;
	background: radial-gradient(
		circle,
		transparent 54%,
		rgba(168, 139, 255, 0.45) 63%,
		rgba(245, 244, 239, 0.8) 68%,
		rgba(168, 139, 255, 0.3) 74%,
		transparent 84%
	);
	transform: scale(0.02);
}

body.is-parting .gate-shockwave {
	animation: gate-shockwave 1250ms var(--ease-out) 160ms forwards;
}

@keyframes gate-shockwave {
	0%   { opacity: 0;    transform: scale(0.02); }
	10%  { opacity: 0.95; }
	100% { opacity: 0;    transform: scale(1.35); }
}

/*
 * The blackout. It finishes what the clouds start: once they have closed over
 * the screen this takes the last of the light out, so the navigation happens
 * against flat ink and the page underneath is never seen arriving. The reveal
 * on the other side begins from exactly this colour, which is what makes the
 * two pages read as one movement. portal.js navigates when it reaches the end.
 */

.gate-blackout {
	position: fixed;
	inset: 0;
	z-index: 3;
	opacity: 0;
	pointer-events: none;
	background: var(--ink);
}

body.is-parting .gate-blackout {
	animation: gate-blackout 1900ms var(--ease) forwards;
}

@keyframes gate-blackout {
	0%   { opacity: 0; }
	52%  { opacity: 0; }
	100% { opacity: 1; }
}

/*
 * The phrase itself flares before everything goes. Specificity note: the awake
 * rules further down use three classes, so every state that has to overrule
 * them matches that weight and is written after them. This file has had a
 * cascade fight silently swallow an animation once already.
 */

body.is-parting .gate__input {
	color: var(--paper);
	text-shadow: 0 0 28px rgba(168, 139, 255, 0.95);
	transition: color 320ms var(--ease), text-shadow 320ms var(--ease);
}

body.is-parting .gate__field-rule {
	transform: scaleX(1);
	background: var(--paper);
	box-shadow: 0 0 44px rgba(168, 139, 255, 0.9);
	transition: transform 320ms var(--ease-out), box-shadow 320ms var(--ease-out);
}

/* Content --------------------------------------------------------------- */

.gate {
	position: relative;
	z-index: 1;
	width: 100%;
	max-width: 30rem;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--space-medium);
	text-align: center;
}

.gate__logo-heading {
	margin: 0;
}

/*
 * The logo is deliberately faint. This page is a door, not a shopfront, and a
 * full-strength wordmark on it would announce whose door it is before anyone
 * has been let in. It comes up to full strength on the page behind the gate.
 */

.gate__logo {
	display: block;
	width: min(180px, 52vw);
	height: auto;
	opacity: 0.4;
}

.gate__whisper {
	margin: 0;
	min-height: 3.2em;
	max-width: 22em;
	color: var(--paper);
	opacity: 0.62;
	font-size: var(--text-large);
	line-height: var(--line-height-text);
}

.gate__hint {
	margin: 0;
	color: var(--purple-light);
	opacity: 0.5;
	font-size: var(--text-small);
	letter-spacing: 0.04em;
}

.gate__cursor {
	display: inline-block;
	width: 0.5em;
	height: 1em;
	margin-left: 0.16em;
	vertical-align: -0.15em;
	background: var(--purple-light);
	animation: gate-cursor-blink var(--cursor-blink) steps(2, jump-none) infinite alternate;
}

@keyframes gate-cursor-blink {
	from { opacity: 1; }
	to   { opacity: 0; }
}

.gate__locked-notice {
	margin: 0;
	padding: var(--space-tiny) var(--space-small);
	border: 1px solid rgba(168, 139, 255, 0.4);
	border-radius: var(--radius-small);
	background: rgba(168, 139, 255, 0.08);
	color: var(--purple-light);
	font-size: var(--text-small);
}

.gate__form {
	width: 100%;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--space-small);
}

.gate__field {
	position: relative;
	width: 100%;
}

/*
 * No box, no border, no placeholder: the phrase is just letters standing in
 * the dark with a purple caret in front of them. The caret is the logo's own
 * mark, so the thing the visitor types into is the brand.
 */

.gate__input {
	width: 100%;
	min-height: 44px;
	padding: 0.5em 0.5em 0.6em;
	border: none;
	border-radius: 0;
	background: transparent;
	color: var(--paper);
	caret-color: var(--purple-light);
	font-family: var(--font);
	font-size: clamp(1.05rem, 4vw, 1.45rem);
	font-weight: 500;
	letter-spacing: 0.18em;
	text-align: center;
}

/*
 * The default focus ring is replaced rather than removed. A three-pixel box
 * around a field that has deliberately been stripped of its box would undo the
 * whole idea, so the rule underneath becomes the focus indicator instead: it
 * goes from a short dim dash to a full glowing line, which is a larger and
 * more obvious change than the outline it stands in for.
 */

.gate__input:focus,
.gate__input:focus-visible {
	outline: none;
}

.gate__field-rule {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 1px;
	background: rgba(245, 244, 239, 0.3);
	transform: scaleX(0.28);
	transform-origin: center;
	transition:
		transform var(--speed) var(--ease-out),
		background-color var(--speed) var(--ease),
		box-shadow var(--speed) var(--ease);
}

.gate__input:focus ~ .gate__field-rule {
	background: var(--purple-light);
	transform: scaleX(1);
	box-shadow: 0 0 22px rgba(168, 139, 255, 0.55);
}

/*
 * The button starts as an outline and only fills in once there is something to
 * submit. Nothing is hidden from anyone: it is always in the tab order and
 * always readable, it just stops shouting until it has a reason to.
 */

.gate__button {
	min-height: 44px;
	min-width: 11rem;
	padding: 0.72em 2em;
	border: 1px solid rgba(168, 139, 255, 0.3);
	border-radius: var(--radius);
	background: transparent;
	color: var(--purple-light);
	font-family: var(--font);
	font-size: var(--text-body);
	font-weight: 600;
	letter-spacing: 0.06em;
	cursor: pointer;
	transition:
		background-color var(--speed) var(--ease),
		border-color var(--speed) var(--ease),
		color var(--speed) var(--ease),
		box-shadow var(--speed) var(--ease),
		transform var(--speed-fast) var(--ease);
}

body.has-phrase .gate__button {
	border-color: var(--purple-light);
	background: var(--purple-light);
	color: var(--ink);
	box-shadow: 0 0 34px rgba(168, 139, 255, 0.32);
}

.gate__button:hover,
.gate__button:focus-visible {
	border-color: var(--purple-light);
	color: var(--paper);
}

body.has-phrase .gate__button:hover,
body.has-phrase .gate__button:focus-visible {
	color: var(--ink);
	box-shadow: 0 0 46px rgba(168, 139, 255, 0.5);
}

.gate__button:active {
	transform: translateY(1px);
}

.gate__button:disabled {
	cursor: default;
	opacity: 0.6;
	box-shadow: none;
}

.gate__status {
	min-height: 1.5em;
	margin: 0;
	color: var(--purple-light);
	font-size: var(--text-small);
}

.gate__status:empty {
	visibility: hidden;
}

/* The staged arrival ------------------------------------------------------
 * Nothing here hides anything unless the document is marked as scripted, so a
 * visitor without JavaScript gets the whole gate immediately, in one piece,
 * with the prompt already written out. Everything below is written with
 * exactly three classes of weight so the states can overrule one another by
 * order rather than by escalating specificity, and the order is deliberate:
 * asleep, then awake, then refused, then parting.
 */

.javascript-enabled .gate__logo-heading,
.javascript-enabled .gate__whisper,
.javascript-enabled .gate__field,
.javascript-enabled .gate__hint,
.javascript-enabled .gate__button,
.javascript-enabled .gate__status {
	opacity: 0;
	transform: translateY(0.7rem);
}

.javascript-enabled .gate.is-awake .gate__logo-heading,
.javascript-enabled .gate.is-awake .gate__whisper,
.javascript-enabled .gate.is-awake .gate__field,
.javascript-enabled .gate.is-awake .gate__hint,
.javascript-enabled .gate.is-awake .gate__button,
.javascript-enabled .gate.is-awake .gate__status {
	opacity: 1;
	transform: none;
	transition:
		opacity 1.3s var(--ease-out),
		transform 1.3s var(--ease-out);
}

/* The fog settles first, then the prompt, then the way in. */
.javascript-enabled .gate.is-awake .gate__whisper        { transition-delay: 0.4s; }
.javascript-enabled .gate.is-awake .gate__field          { transition-delay: 1.5s; }
.javascript-enabled .gate.is-awake .gate__hint           { transition-delay: 1.9s; }
.javascript-enabled .gate.is-awake .gate__button         { transition-delay: 2.1s; }
.javascript-enabled .gate.is-awake .gate__logo-heading   { transition-delay: 2.4s; }
.javascript-enabled .gate.is-awake .gate__status         { transition-delay: 2.4s; }

/*
 * Refused. The fog surges in and the middle of the screen dims almost to
 * nothing for a moment before settling back, which says "no" without a red
 * box and without telling anyone anything about why.
 */

body.is-refused .gate-clouds {
	transform: scale(1.34) translate3d(var(--fog-x), var(--fog-y), 0);
	transition: transform 460ms var(--ease-out);
}

body.is-refused .gate-veil {
	transform: scale(1.2) translate3d(var(--veil-x), var(--veil-y), 0);
	transition: transform 460ms var(--ease-out);
}

body.is-refused .gate.is-awake .gate__whisper,
body.is-refused .gate.is-awake .gate__field,
body.is-refused .gate.is-awake .gate__hint,
body.is-refused .gate.is-awake .gate__button,
body.is-refused .gate.is-awake .gate__logo-heading {
	opacity: 0.16;
	transition: opacity 260ms var(--ease);
}

/* The gate itself goes as the light takes over. */
body.is-parting .gate.is-awake {
	opacity: 0;
	transition: opacity 0.7s var(--ease-out) 0.5s;
}

/* Reduced motion ---------------------------------------------------------
 * No drifting, no breathing light, no blinking cursor, no parting animation.
 * The page still has to look composed while completely still, so the clouds
 * keep their (now static) positions and the parting state — reachable only
 * through the JavaScript enhancement, which itself skips the delay — is
 * neutralised.
 */

@media (prefers-reduced-motion: reduce) {
	.gate-clouds__blob {
		animation: none;
	}

	.gate-clouds::before {
		animation: none;
	}

	.gate__cursor {
		animation: none;
		opacity: 1;
	}

	/*
	 * The fog keeps its scale so it still fills the screen, but it stops
	 * following anything. The lantern goes entirely: portal.js never switches
	 * it on under this preference, and this makes sure of it either way.
	 */

	.gate-clouds {
		--fog-x: 0px;
		--fog-y: 0px;
		transform: scale(1.1);
		transition: none;
	}

	.gate-veil {
		--veil-x: 0px;
		--veil-y: 0px;
		transform: none;
		transition: none;
	}

	.gate-lantern {
		display: none;
	}

	/*
	 * Everything arrives at once and stays. These selectors carry the same
	 * three-class weight as the staged rules they overrule and sit after them
	 * in the file, which is the only reason they win. Weakening either one
	 * silently empties the screen, which is how this went wrong before.
	 */

	.javascript-enabled .gate__logo-heading,
	.javascript-enabled .gate__whisper,
	.javascript-enabled .gate__field,
	.javascript-enabled .gate__hint,
	.javascript-enabled .gate__button,
	.javascript-enabled .gate__status,
	.javascript-enabled .gate.is-awake .gate__logo-heading,
	.javascript-enabled .gate.is-awake .gate__whisper,
	.javascript-enabled .gate.is-awake .gate__field,
	.javascript-enabled .gate.is-awake .gate__hint,
	.javascript-enabled .gate.is-awake .gate__button,
	.javascript-enabled .gate.is-awake .gate__status {
		opacity: 1;
		transform: none;
		transition: none;
		transition-delay: 0s;
	}

	body.is-refused .gate-clouds {
		transform: scale(1.1);
		transition: none;
	}

	body.is-refused .gate-veil {
		transform: none;
		transition: none;
	}

	body.is-refused .gate.is-awake .gate__whisper,
	body.is-refused .gate.is-awake .gate__field,
	body.is-refused .gate.is-awake .gate__hint,
	body.is-refused .gate.is-awake .gate__button,
	body.is-refused .gate.is-awake .gate__logo-heading {
		opacity: 1;
		transition: none;
	}

	body.is-parting .gate-clouds__layer {
		transition: none;
		transform: none;
		opacity: 0.35;
	}

	body.is-parting .gate-clouds::before {
		animation: none;
	}

	body.is-parting .gate-clouds::after,
	.gate-shockwave {
		display: none;
	}

	body.is-parting .gate-blackout {
		animation: none;
		opacity: 0;
	}

	body.is-parting .gate__input,
	body.is-parting .gate__field-rule {
		transition: none;
	}

	body.is-parting .gate.is-awake {
		opacity: 1;
		transition: none;
	}
}

/* Small screens ----------------------------------------------------------
 * Nothing here changes layout structure — the flex column already reflows —
 * this only tightens spacing so the gate does not feel cramped near 360px.
 */

@media (max-width: 380px) {
	body.gate-page {
		padding: var(--space-medium) var(--space-small);
	}

	.gate {
		gap: var(--space-small);
	}

	.gate__intro {
		font-size: var(--text-body);
	}
}
