/* Flow - short strokes combed by an unseen current.
 *
 * The fourth populated backdrop, and the one where the population is not a
 * crowd of individuals but a single field: every stroke's heading is computed
 * *in CSS*, from the two coordinates backdrop.js already writes, by summing
 * two sine harmonics of its position. Iron filings over an unseen magnet. No
 * script computes an angle, no stroke knows any other exists, and the whole
 * current is a handful of numbers on .backdrop.
 *
 * The division of labour inside a stroke leans on the fixed order the
 * individual transform properties compose in - translate, then rotate, then
 * the transform list:
 *
 *   rotate       the heading, resolved once from --x/--y at style time
 *   translate    the push away from the pointer, written by the script
 *   transform    the ambient slide - and because it composes *after* the
 *                rotation, one shared translateX keyframe slides every
 *                stroke along its own heading
 *
 * So at rest this costs exactly what particles costs: one compositor
 * animation per stroke and no script running at all. An engine without CSS
 * sin() drops the rotate declaration and renders the field combed flat -
 * a calm horizontal current, never breakage.
 */

.backdrop {
	/* The brightness dial rides into --glow once, at 1 when untouched; the
	 * page states scale on top of it. See particles.css. */
	--glow: var(--bright, 1);

	/* The population. This is the whole of the script's instructions. */
	--motes: 64;

	/* How hard the pointer pushes, and how far the effect reaches. The push is
	 * the whole of Reactive here: strokes shoved off their line while their
	 * heading holds is exactly what parting water looks like. */
	--mote-push: 110;
	--mote-reach: 190;

	/* The current itself, and the rest of the tunables a variant may restate.
	 * The heading of a stroke at (x, y), both 0 to 1, is
	 *
	 *   sin(x*f1 + y*g1) * amp1 + cos(x*f2 + y*g2) * amp2 + base
	 *
	 * The frequencies are unitless - radians once multiplied by the unitless
	 * coordinates - and the amplitudes and base carry the degrees. Keeping the
	 * units on exactly one side of each multiplication is load-bearing: a unit
	 * slip inside sin() fails silently to a flat field in some engines. */
	--flow-f1: 5.1;
	--flow-g1: 3.7;
	--flow-f2: 3.3;
	--flow-g2: 6.2;
	--flow-amp1: 42deg;
	--flow-amp2: 28deg;
	--flow-base: -8deg;

	/* Stroke length per unit of --size, the hairline's weight, how far a
	 * stroke slides along its heading, and how briskly it does so. */
	--stroke-scale: 34px;
	--stroke-weight: 1.5px;
	--flow-drift: 1;
	--flow-pace: 1;

	--stroke-far: light-dark(
		oklch(0.48 var(--chroma-full) var(--hue-seed) / 0.42),
		oklch(0.76 var(--chroma-full) var(--hue-seed) / 0.4)
	);
	--stroke-near: light-dark(
		oklch(0.42 var(--chroma-accent) var(--hue-match) / 0.48),
		oklch(0.84 var(--chroma-accent) var(--hue-match) / 0.46)
	);
	--stroke-lit: light-dark(
		oklch(0.38 var(--chroma-loud) var(--hue-match) / 0.55),
		oklch(0.9 var(--chroma-loud) var(--hue-match) / 0.58)
	);

	/* The strokes fade out of the middle of the window, where the type is.
	 * The ellipse is wider and taller than traces' because the layers carrying
	 * it also carry the scroll: the clear zone rides up with them, so it is
	 * cut generous enough that the type column stays inside it at full travel
	 * - which is also why the depths below are the shallowest of any populated
	 * backdrop. */
	--fade: radial-gradient(115% 100% at 50% 44%, transparent 18%, #000 84%);
}

/* --------------------------------------------------------------- the field - */

/* One quiet wash behind the current, so the strokes hang in weather rather
 * than on the theme's flat canvas. */
.backdrop__layer--far::before {
	content: "";
	position: absolute;
	inset: -20%;
	background: radial-gradient(64vmax 48vmax at 50% 38%,
		color-mix(in oklab, var(--stroke-far) 20%, transparent), transparent 70%);
	opacity: calc(0.65 * var(--glow));
	transition: opacity 600ms ease;
	animation: flow-breathe var(--motion-slow) ease-in-out infinite alternate;
	animation-play-state: var(--play-ambient);
}

@keyframes flow-breathe {
	to {
		transform: scale(calc(1 + 0.16 * var(--travel)));
	}
}

/* -------------------------------------------------------------- the strokes - */

/* Every number below --x, --y, --size, --phase comes from the script and
 * means nothing on its own. The heading is where they become a field: two
 * harmonics of the stroke's own position, resolved once at style time -
 * nothing here costs a frame.
 */
.backdrop__mote {
	position: absolute;
	left: calc(var(--x, 0.5) * 100%);
	top: calc(var(--y, 0.5) * 100%);
	width: calc(var(--size, 1) * var(--stroke-scale));
	height: var(--stroke-weight);
	margin-left: calc(var(--size, 1) * var(--stroke-scale) / -2);
	border-radius: calc(var(--stroke-weight) / 2);
	background: var(--stroke-far);
	opacity: calc(var(--glow) * (0.14 + 0.3 * var(--size, 1)));

	rotate: calc(
		sin(var(--x, 0.5) * var(--flow-f1) + var(--y, 0.5) * var(--flow-g1)) * var(--flow-amp1)
		+ cos(var(--x, 0.5) * var(--flow-f2) + var(--y, 0.5) * var(--flow-g2)) * var(--flow-amp2)
		+ var(--flow-base)
	);

	/* The push. --dx and --dy are zero until the script has a reason to write
	 * them, so a field nobody has touched is a field with no inline styles. */
	translate: calc(var(--dx, 0) * 1px) calc(var(--dy, 0) * 1px);

	/* Negative delay starts each stroke part-way through the cycle, which is
	 * what stops the whole current surging in unison. */
	animation: flow-slide calc(var(--motion-mid) / var(--flow-pace)) ease-in-out infinite alternate;
	animation-delay: calc(var(--phase, 0) * -1s);
	animation-play-state: var(--play-ambient);
	transition: opacity 600ms ease;
}

/* The nearer strokes carry the matched hue, and a few catch the light - the
 * same depth cues as particles, drawn as current rather than dust. */
.backdrop__mote:nth-child(7n) {
	background: var(--stroke-near);
	opacity: calc(var(--glow) * (0.2 + 0.34 * var(--size, 1)));
}

.backdrop__mote:nth-child(13n) {
	background: var(--stroke-lit);
	box-shadow: 0 0 6px color-mix(in oklab, var(--stroke-lit) 45%, transparent);
}

/* The slide is stated in the stroke's own frame: transform composes after
 * rotate, so this one keyframe moves every stroke along its own heading.
 * Distances carry --travel like every other backdrop's, so Still leaves the
 * current hanging exactly where the script placed it. */
@keyframes flow-slide {
	from {
		transform: translate3d(calc(-12px * var(--flow-drift) * var(--travel)), 0, 0);
	}
	to {
		transform: translate3d(calc(14px * var(--flow-drift) * var(--travel)), 0, 0);
	}
}

/* --------------------------------------------------------------- the depth - */

/* The scroll pulls the field past the page, the two ranks by different amounts
 * - and these depths are deliberately shallow, because the masks below travel
 * with the layers and must not carry the clear zone away from the type.
 * --shift is computed in palette.css from the depths below and the visitor's
 * scroll dials.
 *
 * The ranks are deliberately *not* stretched past the window the way the other
 * populations are. The clear zone is a mask stated in percentages of this
 * element, so a taller element is a wider hole in a different place - and
 * keeping the type legible outranks keeping the last stroke on screen. */
.backdrop__layer--mid,
.backdrop__layer--near {
	mask-image: var(--fade);
	transform: translate3d(0, var(--shift), 0);

	--depth-flat: 12vh;
}

.backdrop__layer--mid {
	--depth: 8vh;
}

.backdrop__layer--near {
	--depth: 16vh;
}

/* -------------------------------------------------------------- the states - */

/* The rail's poll crosses the field as one gust: a soft band sweeping the mid
 * layer, once. A response to something that happened, not a clock - so it is
 * deliberately not on the ambient play state, and the layer's mask keeps it
 * out of the type on its way through. */
.backdrop__layer--mid::after {
	content: "";
	position: absolute;
	inset: -30% -60%;
	background: linear-gradient(
		calc(90deg + var(--flow-base)),
		transparent 40%,
		color-mix(in oklab, var(--stroke-near) 55%, transparent) 50%,
		transparent 60%
	);
	opacity: 0;
}

body:has(.htmx-settling) .backdrop__layer--mid::after {
	animation: flow-gust 900ms ease-out;
}

@keyframes flow-gust {
	from {
		opacity: calc(0.32 * var(--travel));
		transform: translate3d(calc(-18% * var(--travel)), 0, 0);
	}
	to {
		opacity: 0;
		transform: translate3d(calc(18% * var(--travel)), 0, 0);
	}
}

body:has(.branch--live:hover) .backdrop {
	--glow: calc(1.2 * var(--bright, 1));
}

body:has(.field__input:focus) .backdrop {
	--glow: calc(0.45 * var(--bright, 1));
}

body:has(.designer__sheet:popover-open) .backdrop {
	--glow: calc(0.35 * var(--bright, 1));
}

body:has(.htmx-request) .backdrop {
	--glow: calc(1.3 * var(--bright, 1));
}

/* ------------------------------------------------------------- the pointer - */

/* A faint brightening of the current where the pointer rests, and a slow
 * ellipse breathing out from the same spot - flattened along the prevailing
 * heading, because a ring standing square to the current would be from a
 * different page. The real reaction is the push, which lives on the
 * Animation axis. */
.backdrop__tint {
	width: 30vmax;
	height: 30vmax;
	margin: -15vmax 0 0 -15vmax;
	border-radius: 50%;
	background: radial-gradient(closest-side, var(--stroke-lit) 0%,
		color-mix(in oklab, var(--stroke-lit) 28%, transparent) 42%, transparent 76%);
	opacity: calc(0.12 * var(--pointer) * var(--pointer-tint) * var(--glow));
	transition:
		translate var(--tint-follow) cubic-bezier(0.2, 0, 0.2, 1),
		opacity 600ms ease;
}

.backdrop__wave {
	width: 26vmax;
	height: 17vmax;
	margin: -8.5vmax 0 0 -13vmax;
	border-radius: 50%;
	border: 1px solid color-mix(in oklab, var(--stroke-near) 55%, transparent);
	rotate: var(--flow-base);
	opacity: 0;
	transition: translate calc(var(--tint-follow) * 1.6) cubic-bezier(0.2, 0, 0.2, 1);
	animation: flow-wave var(--wave-period) cubic-bezier(0.2, 0, 0.3, 1) infinite;
}

@keyframes flow-wave {
	from {
		opacity: calc(0.15 * var(--pointer) * var(--pointer-wave) * var(--glow));
		scale: 0.2;
	}
	60%,
	to {
		opacity: 0;
		scale: 1;
	}
}
