/* Vedmi film field.
   The page-wide grade: emulsion haze, cross-process pools and vignette, and
   35mm grain, all fixed over the content. Split out of theme.css so
   privacy/index.html can carry the field without loading the whole theme
   layer; index.html links it between theme.css and motion.css. The contacts
   card's own grain (theme.css) reads --grain-url from here. */
/* The page background, ported from design/Background.dc.html with the
   page-scale gradient stops of design/Main.dc.html. The brand slate does not
   move: #e0e1e5 stays where it has always been, on <html> and on body, and
   covers the document however far it runs. Everything below is four layers
   laid over it, none of which is meant to be noticed on its own.

   The field lies OVER the content, not behind it, and that is the one
   structural decision here worth explaining.

   Two sections carry opaque video — the mission clip and the vision clip.
   Neither can carry alpha, so both are encoded onto a flat #e0e1e5 field to
   match the page they sit on, and both are revealed through a mask that is a
   single top-to-bottom fade: fully opaque along the whole top edge. Around
   two thirds of that fully-opaque band is the baked slate rather than the
   photograph. Behind the content, the field would paint everywhere except
   those two rectangles, and because it is fixed while the page scrolls, the
   mismatch would not even hold still — a slab of dead slate sliding against a
   moving field, in the largest media on the page.

   Over the content there is nothing to mismatch: the clips are graded and
   grained along with the copy beside them. This is also the truer reading of
   the brief. Grain sitting behind a photograph is not how film works; the
   emulsion carries the whole frame, subject included.

   The cost is that the two cross-process pools can no longer be opaque colour
   stops — as overlays they would paint over the page rather than tint it — so
   they are expressed as the alpha tints that land on the same result over the
   slate. The cool pool is exact: rgb(84 145 219 / 10%) over #e0e1e5 resolves
   to #d2d9e4 to the byte. The warm one cannot be, since #e7e7db is both
   lighter and warmer than the slate and an alpha tint would need a colour
   brighter than white to reach it; it is set a little gentler instead, in the
   same direction, which is also kinder to the photographs it now crosses.

   Nothing here is measured in cqw. body is not one of the 1440-wide
   inline-size containers — main, .header and .footer are, and their
   containment stops at their own boxes — so a cqw here would silently fall
   back to the small viewport. px, % and the viewport are the honest units for
   a layer measured against the screen rather than against the column. */

:root {
	/* One 180x180 tile of grey fractal noise. It ships as a file rather than a
	   data URI so deploy/stamp.py can content-hash it — the inline form nests a
	   url(#n) inside the CSS url(), which that script's pattern reads as a
	   reference of its own — and so nginx can cache it for a month. */
	--grain-url: url('../img/grain.svg?v=76ad9c103a');

	/* How much of that tile survives the blend. 0.22 is where the design's own
	   grain slider rests; past roughly 0.3 the noise starts arguing with body
	   copy instead of sitting under it. */
	--grain-alpha: 0.22;

	/* The three cross-process pools are the only part of the field that has to
	   be retuned for a phone, so they are the only part kept in tokens: the
	   mobile block at the foot of this file then restates three values instead
	   of a four-layer background shorthand that could drift out of step with
	   this one. The vignette is already framed against the viewport edges and
	   travels as it stands. */
	--field-cool: 90% 46% at 10% -4%;
	--field-warm: 70% 34% at 92% 40%;
	--field-bloom: 60% 26% at 50% 16%;

	/* Above .header and .vision, both of which sit at z-index 10000. */
	--field-z: 10001;
}

/* Emulsion banding read through a moving window: three rows of nothing, one
   row of the faintest blue, drifting a full period every three quarters of a
   minute — never fast enough to be read as motion.

   The design drifts it by animating background-position, which is a paint
   property: it would repaint the full viewport every frame for as long as the
   tab is open. A transform over the same distance stays on the compositor and
   repaints nothing. 180px is 45 whole 4px periods, so the loop closes exactly
   where it started and the seam never shows, and the box is hung 180px below
   the fold so travelling up never walks its bottom edge into view. */
html::before {
	content: '';
	position: fixed;
	z-index: var(--field-z);
	inset: 0 0 -180px;
	background: repeating-linear-gradient(
		to bottom,
		rgb(19 74 146 / 0%) 0 3px,
		rgb(19 74 146 / 1.6%) 3px 4px
	);
	animation: field-haze 44s linear infinite;
	pointer-events: none;
}

@keyframes field-haze {
	from {
		transform: translate3d(0, 0, 0);
	}

	to {
		transform: translate3d(0, -180px, 0);
	}
}

/* Cross-processing pushes shadows cyan-blue and highlights yellow-green. Two
   desaturated pools and a faint accent bloom do that across the screen instead
   of a filter, and the vignette closes the corners in the same shorthand — it
   is another static gradient, and a layer that never animates never needs its
   own compositing. The vignette is listed first because the first background
   layer paints on top. */
body::before {
	content: '';
	position: fixed;
	z-index: calc(var(--field-z) + 1);
	inset: 0;
	background:
		radial-gradient(110% 60% at 50% 40%, rgb(18 18 18 / 0%) 58%, rgb(18 18 18 / 6%) 100%),
		radial-gradient(var(--field-cool), rgb(84 145 219 / 10%) 0%, rgb(84 145 219 / 0%) 62%),
		radial-gradient(var(--field-warm), rgb(255 250 190 / 14%) 0%, rgb(255 250 190 / 0%) 58%),
		radial-gradient(var(--field-bloom), rgb(19 74 146 / 5.5%), rgb(19 74 146 / 0%) 72%);
	pointer-events: none;
}

/* 35mm grain, and the only layer that has to sit on top of the rest. Oversized
   by 14% so the jitter never walks an edge into frame, and stepped so it reads
   as frames changing rather than as a texture sliding across the page.

   steps(1) is what makes this affordable to leave running: ten discrete jumps
   in six seconds, not sixty interpolated frames a second. No will-change — a
   transform animation already gets whatever promotion the compositor thinks it
   needs while it runs, and will-change would instead pin a full-screen texture
   for the life of every page. */
body::after {
	content: '';
	position: fixed;
	z-index: calc(var(--field-z) + 2);
	inset: -14%;
	background-image: var(--grain-url);
	opacity: var(--grain-alpha);
	mix-blend-mode: soft-light;
	animation: field-grain 6s steps(1) infinite;
	pointer-events: none;
}

@keyframes field-grain {
	0%,
	100% {
		transform: translate3d(0, 0, 0);
	}

	10% {
		transform: translate3d(-2%, -3%, 0);
	}

	20% {
		transform: translate3d(-8%, 2%, 0);
	}

	30% {
		transform: translate3d(4%, -4%, 0);
	}

	40% {
		transform: translate3d(-3%, 6%, 0);
	}

	50% {
		transform: translate3d(-4%, 2%, 0);
	}

	60% {
		transform: translate3d(6%, 0, 0);
	}

	70% {
		transform: translate3d(-6%, 4%, 0);
	}

	80% {
		transform: translate3d(2%, 6%, 0);
	}

	90% {
		transform: translate3d(-2%, -4%, 0);
	}
}

/* A reduced-motion visitor keeps the field and loses the two things inside it
   that move. The blanket rule at the foot of this file already collapses every
   animation to 0.01ms, but naming the two layers switches the animation off
   instead of running it once at speed, which is the difference between a grain
   frame that is simply frozen and one that jumps once on load. */
@media (prefers-reduced-motion: reduce) {
	html::before,
	body::after {
		animation: none;
	}
}

/* -------------------------------------------------------------- responsive */

@media (width <= 767px) {
	/* The pools were drawn against 1440px of width, and a percentage that
	   spreads one across 1300px of a laptop spreads it across 350px of a phone
	   — three small blobs where there should be one lit field. Pushing the
	   horizontal radii past 100% restores something near the original spread
	   in pixels: what a phone shows is the middle of the same pool, so the
	   falloff happens down the screen, which is where a phone has room for it.
	   The vertical radii barely move, because a phone is about as tall as a
	   laptop viewport is — it is only the width that broke.

	   The vignette is deliberately not here. It is framed against the edges
	   rather than against the composition, and already lands within a few
	   pixels of its desktop weight at this size. */
	:root {
		--field-cool: 210% 44% at 6% -4%;
		--field-warm: 170% 32% at 96% 42%;
		--field-bloom: 150% 24% at 50% 14%;
	}
}
