/* ==========================================================================
   Gemial Digital v2 — Motion
   --------------------------------------------------------------------------
   EVERY HIDDEN STATE ON THE SITE LIVES IN THIS FILE AND NOWHERE ELSE.

   That is the whole point of a separate sheet. If content is ever invisible,
   there is exactly one file to look in. No other v2 stylesheet may set
   opacity: 0 or otherwise hide content, and base.css says so too.

   THE TWO GUARDS, both required on every hidden state:

     1. html.gd2-motion   added by the inline gate in functions.php, removed
                          again if motion.js never signals __gdReady
     2. @media (prefers-reduced-motion: no-preference)

   Redundant on purpose. Either one alone would be enough on a good day;
   CLAUDE.md §5 forbids any failure mode where content stays at opacity 0, and
   a good day is not something to design for.

   --------------------------------------------------------------------------
   THREE TIERS, AND html.gd2-motion IS ONLY ON THE FIRST
   --------------------------------------------------------------------------
   The gate exists for one job: reveal content that JavaScript failed to
   un-hide. It used to carry two other jobs as well, and stripping it did all
   three at once -- so a slow connection, where stripping the gate is CORRECT
   behaviour, also froze every animation and reflowed the hero headline from
   two lines to one. Measured on production 2026-08-21.

     TIER 1  CONTENT       html.gd2-motion + reduced-motion
                           Anything that can leave content invisible.
                           Stripping the gate reveals all of it, which is
                           exactly what the gate is for.

     TIER 2  LAYOUT        no guard at all
                           .gd-ln's box model. Not motion: it is the structure
                           the animation happens inside. A headline must not
                           change shape because of a motion preference or a
                           slow connection, so nothing may gate it.

     TIER 3  DECORATION    reduced-motion only
                           Drift, will-change, the plot sweep, the rail tick.
                           No content, so the gate has no business touching it.
                           Pure CSS with no JS dependency -- there was never a
                           reason for a JS failsafe to stop it.

   Before adding a rule here, decide which tier it is. If it can hide content
   it is tier 1. If it changes layout it is tier 2 and takes no guard. If it
   only moves pixels that are not content, it is tier 3.

   Categories, per CLAUDE.md §7.8:

     B  self-revealing   .gd-ln, .gd-fade      CSS animation un-hides them
                                               unconditionally. No JS involved.
                                               Only used above the fold, since
                                               they play on load.
     C  observer-gated   .gd-reveal, .gd-cascade, the triptych
                                               JS adds .is-in. These are the
                                               only three things on the page
                                               that can stay hidden, and they
                                               are what motion.js guards.
   ========================================================================== */

/* ==========================================================================
   TIER 2 — LAYOUT. No guard, deliberately.
   --------------------------------------------------------------------------
   .gd-ln's box model is what puts each line of the hero headline on its own
   line, and what clips the span the animation slides in from below. It is
   structure, not motion.

   IT USED TO BE INSIDE html.gd2-motion, AND THAT WAS THE BUG. Strip the gate
   and .gd-ln fell back to display: inline, so the two spans flowed together
   and the headline reflowed from two lines to one -- on a slow connection,
   which is precisely when stripping the gate is the right thing to do.
   Measured before and after at 390, 768, 1366 and 1440: heights 135/129/226/226
   gated, 87/124/217/217 stripped, and 135/129/226/226 with this rule ungated.

   It also means reduced-motion visitors used to get a differently shaped
   headline from everyone else, which is a bug in its own right and is fixed
   by the same move.
   ========================================================================== */

.gd-ln {
	display: block;
	overflow: hidden;
	padding-block-end: .08em;
	margin-block-end: -.08em;
}

.gd-ln > span {
	display: block;
}

@media (prefers-reduced-motion: no-preference) {

	/* ---- B: self-revealing, above the fold only ------------------------- */

	/*
	 * Line reveal. The text is pushed out of the overflow:hidden box above and
	 * an unconditional CSS animation brings it back. Safe because CSS
	 * animations with `forwards` do not depend on JavaScript.
	 *
	 * Only the hidden state and the animation are gated. The box model that
	 * makes the clip work is tier 2 above and carries no guard at all.
	 *
	 * NEVER reimplement this as a JS-added class. The hidden state here has
	 * no failsafe of its own; the animation IS the failsafe.
	 */
	html.gd2-motion .gd-ln > span {
		transform: translateY(105%);
		animation: gd-line-in .95s var(--ease) forwards;
		animation-delay: calc(.15s + var(--i, 0) * .11s);
	}

	html.gd2-motion .gd-fade {
		opacity: 0;
		animation: gd-fade-in .8s var(--ease) forwards;
		animation-delay: calc(.5s + var(--i, 0) * .1s);
	}

	/* ---- C: observer-gated --------------------------------------------- */

	html.gd2-motion .gd-reveal {
		opacity: 0;
		transform: translateY(22px);
		transition:
			opacity var(--dur-slow) var(--ease),
			transform var(--dur-slow) var(--ease);
	}

	html.gd2-motion .gd-reveal.is-in {
		opacity: 1;
		transform: none;
	}

	html.gd2-motion .gd-cascade > * {
		opacity: 0;
		transform: translateY(16px);
		transition:
			opacity .65s var(--ease),
			transform .65s var(--ease);
		transition-delay: calc(var(--i, 0) * 70ms);
	}

	html.gd2-motion .gd-cascade.is-in > * {
		opacity: 1;
		transform: none;
	}

	/* ---- The triptych -------------------------------------------------
	   The three process images stay a three-up grid at every width, and the
	   existing cascade staggers them in as the section arrives. Left to right
	   they already read as one field resolving, because that is what they are.

	   A STACKED CROSS-FADE WAS BUILT HERE AND REMOVED. Collapsing three
	   figures into one cell needed display:contents on the figures, grid-row
	   reassignment for the captions, and a calc() opacity formula driven by a
	   custom property. It worked, and it was the most fragile thing in the
	   redesign: display:contents drops a figure out of the accessibility tree
	   in older WebKit, and the opacity formula silently produced a blank cell
	   for any --gd-tri outside 0-2.

	   The cascade gets most of the effect using machinery that already exists
	   and has one failure mode instead of four. MOTION-TECHNIQUES.md §7 argues
	   for four techniques done properly over eight half-considered; this is
	   that argument applied to itself. Revisit in M3 against the real page if
	   the sequence genuinely reads as flat.
	   -------------------------------------------------------------------- */

	/* ---- Calibration wipe on section entry ------------------------------
	   Three beats about 200ms apart, all on the existing .is-in class, so no
	   new JavaScript and no new observer:

	     0ms    the top hairline draws left to right
	     200ms  the eyebrow fades up
	     400ms  the headline clip-reveals

	   The headline uses clip-path rather than the hero's translate trick,
	   because a section heading has no overflow:hidden wrapper to hide inside
	   and adding one would clip descenders.
	   -------------------------------------------------------------------- */

	.gd-section[data-gd-n]::after {
		transform: scaleX(0);
		transform-origin: 0 50%;
		transition: transform .7s var(--ease);
	}

	.gd-section[data-gd-n].is-in::after {
		transform: scaleX(1);
	}

	html.gd2-motion .gd-section[data-gd-n] .gd-eyebrow {
		opacity: 0;
		transform: translateY(6px);
		transition:
			opacity .5s var(--ease) .2s,
			transform .5s var(--ease) .2s;
	}

	html.gd2-motion .gd-section[data-gd-n].is-in .gd-eyebrow {
		opacity: 1;
		transform: none;
	}

	html.gd2-motion .gd-section[data-gd-n] .gd-h2 {
		clip-path: inset(0 100% 0 0);
		transition: clip-path .8s var(--ease) .4s;
	}

	html.gd2-motion .gd-section[data-gd-n].is-in .gd-h2 {
		clip-path: inset(0 0 0 0);
	}

	/* ---- Images resolve rather than fade --------------------------------
	   A clip-path wipe with a 2px lime leading edge travelling ahead of it.
	   Costs the same as an opacity fade and reads as the image being plotted
	   rather than dissolved, which is the instrument language the whole design
	   is built on.

	   The wipe is on .gd-reveal figures only, so it inherits the category C
	   guards already in place. The leading edge is a pseudo-element, so if the
	   transition never runs there is nothing to strand on screen.
	   -------------------------------------------------------------------- */

	html.gd2-motion .gd-wipe {
		position: relative;
		overflow: hidden;
	}

	html.gd2-motion .gd-wipe img {
		clip-path: inset(0 100% 0 0);
		transition: clip-path 1.1s var(--ease);
	}

	html.gd2-motion .gd-wipe.is-in img,
	html.gd2-motion .is-in .gd-wipe img {
		clip-path: inset(0 0 0 0);
	}

	/*
	 * The leading edge is a full-width layer carrying a 2px lime sliver at its
	 * own left edge, translated from 0 to 100%. Sizing it to the element means
	 * the travel distance is the element's width without anything having to
	 * measure it -- no custom property, no script, no container query.
	 */
	html.gd2-motion .gd-wipe::after {
		content: "";
		position: absolute;
		inset-block: 0;
		inset-inline-start: 0;
		width: 100%;
		background: linear-gradient(90deg, var(--accent-ink) 0 2px, transparent 2px);
		opacity: 0;
		transform: translateX(0);
		transition:
			transform 1.1s var(--ease),
			opacity .18s linear 1.02s;
		pointer-events: none;
	}

	html.gd2-motion .gd-wipe.is-in::after,
	html.gd2-motion .is-in .gd-wipe::after {
		opacity: 1;
		transform: translateX(100%);
	}

	/* The triptych wipes in sequence, left to right, so the three states
	   resolve in the order the process describes rather than together. */
	html.gd2-motion .proc__grid .pstep:nth-child(1) .gd-wipe img { transition-delay: 0s; }
	html.gd2-motion .proc__grid .pstep:nth-child(2) .gd-wipe img { transition-delay: .22s; }
	html.gd2-motion .proc__grid .pstep:nth-child(3) .gd-wipe img { transition-delay: .44s; }
	html.gd2-motion .proc__grid .pstep:nth-child(1) .gd-wipe::after { transition-delay: 0s, 1.05s; }
	html.gd2-motion .proc__grid .pstep:nth-child(2) .gd-wipe::after { transition-delay: .22s, 1.27s; }
	html.gd2-motion .proc__grid .pstep:nth-child(3) .gd-wipe::after { transition-delay: .44s, 1.49s; }

	/* ---- Hero plotter pass ----------------------------------------------
	   One lime scan line down the hero figure on load, once, revealing the
	   wireframe as it goes.

	   An animation rather than a transition, and category B: it is not waiting
	   on JavaScript, so the image cannot be stranded behind an unrevealed
	   clip. If the animation never runs the figure is simply visible.
	   -------------------------------------------------------------------- */

	html.gd2-motion .hero__wire img {
		clip-path: inset(0 0 100% 0);
		animation: gd-plot-reveal 1.6s var(--ease) .55s forwards;
	}

	/* Same full-height trick as the wipe edge, rotated: a 2px sliver at the top
	   of a full-height layer, travelling one element height. */
	.hero__plot {
		position: absolute;
		inset-inline: 0;
		inset-block-start: 0;
		height: 100%;
		background: linear-gradient(
			180deg,
			transparent 0,
			var(--accent-ink) 0 2px,
			color-mix(in srgb, var(--accent-ink) 22%, transparent) 2px 10px,
			transparent 10px
		);
		opacity: 0;
		pointer-events: none;
		animation: gd-plot-line 1.6s var(--ease) .55s forwards;
	}

	/* ---- Hero drift, CSS only ------------------------------------------
	   Replaces the canvas particle field ruled out in REDESIGN-PLAN.md §11.

	   Each layer is a repeating tile translated by exactly one tile height,
	   which is pixel-identical to translating by zero. The loop is therefore
	   perfect by construction: no seam to hide, no loop point to time, and
	   nothing to resynchronise on resize.

	   transform only, so it runs on the compositor and costs no main-thread
	   time. This is the entire replacement for 285 million pixels per second
	   of canvas clearRect.
	   -------------------------------------------------------------------- */

	/*
	 * will-change is declared here rather than in home.css so it exists only
	 * when something is actually animating. Held unconditionally it promotes
	 * three full-bleed layers and holds their textures for the whole session,
	 * including under reduced motion where nothing here ever runs.
	 */
	.hero__drift,
	.hero__grid {
		will-change: transform;
	}

	.hero__drift--far {
		animation: gd-drift 46s linear infinite;
	}

	.hero__drift--near {
		animation: gd-drift 27s linear infinite;
	}

	.hero__grid {
		animation: gd-drift 90s linear infinite;
	}
}

@keyframes gd-line-in {
	to { transform: none; }
}

@keyframes gd-fade-in {
	to { opacity: 1; }
}

@keyframes gd-plot-reveal {
	from { clip-path: inset(0 0 100% 0); }
	to   { clip-path: inset(0 0 0 0); }
}

@keyframes gd-plot-line {
	0%   { opacity: 0; transform: translateY(0); }
	6%   { opacity: 1; }
	94%  { opacity: 1; }
	100% { opacity: 0; transform: translateY(100%); }
}

/* 256px is the tile height. Translating by exactly that is a no-op visually,
   which is what makes the loop seamless. Change one and change the other. */
@keyframes gd-drift {
	from { transform: translate3d(0, 0, 0); }
	to   { transform: translate3d(0, -256px, 0); }
}

/* --------------------------------------------------------------------------
   Reduced motion
   --------------------------------------------------------------------------
   Nothing above applies here already, because every hidden state is inside the
   no-preference block. This is the belt-and-braces pass: if a hidden state is
   ever added in the wrong place, this still reveals it.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	.gd-reveal,
	.gd-cascade > *,
	.gd-ln > span,
	.gd-fade,
	.gd-section[data-gd-n] .gd-eyebrow,
	.gd-section[data-gd-n] .gd-h2,
	.gd-wipe img,
	.hero__wire img {
		opacity: 1 !important;
		transform: none !important;
		animation: none !important;
		transition: none !important;
		clip-path: none !important;
	}

	/* The travelling edges have nothing to travel; remove them outright. */
	.gd-wipe::after,
	.hero__plot {
		display: none !important;
	}
}

/* --------------------------------------------------------------------------
   The imagery layer — hero plate resolve, and the phase spine
   --------------------------------------------------------------------------
   TIER 3, DECORATION. reduced-motion only, and deliberately NOT behind
   html.gd2-motion. That gate exists for one job: revealing content that
   JavaScript failed to un-hide. Neither of these can hide content, so neither
   has any business consuming it -- and a plate that went dark on a slow
   connection would be the gate doing harm rather than work.

   THE MARKUP IS THE RESOLVED STATE, AND THE ANIMATION DEPARTS FROM IT.
   CLAUDE.md section 7.8 category D, the same inversion the counting figures
   use: the true value is in the document and the enhancement animates toward
   it. animation-fill-mode: backwards applies the from-keyframe during the
   stagger delay and then hands back to the authored style, so there is no
   state in which the plate is stranded half-drawn. With motion stripped, with
   CSS half-applied, with JavaScript absent, or if the animation simply never
   fires, what renders is the finished diagram.

   That is why VISUAL-SYSTEM.md section 4's "never a frozen half-state" costs
   nothing here: there is no half-state to freeze in.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {

	/*
	 * Each element carries --i, its position in the resolve sequence. Six
	 * steps at 120ms plus a 500ms draw lands the whole plate at roughly
	 * 900ms, per the spec. No loop: a looping hero is a distraction on a page
	 * whose job is to be read.
	 */
	.gd-plate__step {
		animation: gd-plate-resolve .5s var(--ease) backwards;
		animation-delay: calc(var(--i, 0) * .12s);
	}

	/* transform and opacity only. No layout properties, no filters. */
	@keyframes gd-plate-resolve {
		from {
			opacity: 0;
			transform: translateY(8px);
		}
	}

	/* The seam draws itself down the plate as the two halves resolve. */
	.gd-plate__seam {
		animation: gd-plate-seam .7s var(--ease) .1s backwards;
	}

	@keyframes gd-plate-seam {
		from {
			opacity: 0;
			transform: scaleY(0);
		}
	}

	.gd-plate__seam {
		transform-origin: top;
	}

	/*
	 * The spine fills once on entry. ENTRY FILL, NOT SCROLL-LINKED: true
	 * scroll linking needs animation-timeline, which is Chrome-only today, so
	 * it would hand one browser a different page for no argument gained and
	 * leave a second code path to reason about on every future change.
	 *
	 * The rail underneath is --hairline and is not animated, so the sequence
	 * is legible before, during and after.
	 */
	:where(body.gd2-page) .svc-phases::after {
		animation: gd-spine-fill 1.1s var(--ease) .15s backwards;
	}

	@keyframes gd-spine-fill {
		from { transform: scaleY(0); }
	}
}

/* --------------------------------------------------------------------------
   Print
   --------------------------------------------------------------------------
   A visitor printing the page mid-reveal would otherwise print blank sections.
   -------------------------------------------------------------------------- */

@media print {
	.gd-reveal,
	.gd-cascade > *,
	.gd-ln > span,
	.gd-fade,
	.gd-section[data-gd-n] .gd-eyebrow,
	.gd-section[data-gd-n] .gd-h2,
	.gd-wipe img,
	.hero__wire img {
		opacity: 1 !important;
		transform: none !important;
		animation: none !important;
		clip-path: none !important;
	}

	.gd-rail,
	.gd-wipe::after,
	.hero__plot {
		display: none !important;
	}
}
