/*
 * The mask, and the plane on it.
 *
 * The whole component is one straight line. A square panel, big enough to cover
 * the viewport's diagonal whatever angle it is turned to, slides along a single
 * axis: one width back is fully clear, zero is fully covering, one width forward
 * is fully clear again on the far side. It never reverses — it goes in one door
 * and out the other, which is what makes the sweep read as continuous even though
 * a whole page load happens in the middle of it.
 *
 * That middle is why the numbers matter. The outgoing page animates to exactly
 * zero and stops there; the arriving page starts at exactly zero and carries on.
 * Both ends are the same declaration, so the seam falls on a frame the two pages
 * agree about, and the join is invisible.
 *
 * Every tunable arrives as a custom property from the settings screen — see
 * Airline_Transition_Assets::custom_properties(). The fallbacks here are the
 * shipped defaults, so the stylesheet still draws correctly on its own.
 */

:root {
	/* Not settings: the character of the movement, which is a design decision
	   rather than a knob. A near-symmetrical ease for the mask, so the wipe
	   leaves and arrives with weight instead of sliding at a constant speed. */
	--airline-transition-ease: cubic-bezier( 0.76, 0, 0.24, 1 );

	/* The picture gets a decelerating one instead, because it is doing a
	   different thing. The mask is a shape crossing the screen and wants weight
	   at both ends; a photograph is travelling to a place it belongs and wants
	   to leave quickly and settle. Given the mask's near-symmetrical curve it
	   crept away, drifted, and arrived — which is the whole of what "clunky"
	   was. */
	--airline-transition-image-ease: cubic-bezier( 0.22, 1, 0.36, 1 );

	/* The plane gets the opposite shape — quick in, slow across the middle,
	   quick out — so that it rushes onto the screen, hangs where it can be
	   looked at, and is gone. A constant speed reads as a sliding image; this
	   reads as something passing. */
	--airline-transition-fly-ease: cubic-bezier( 0, 0.55, 1, 0.45 );

	/* How far the eye is from the drawing, which is what turns the bank from a
	   horizontal squeeze into an aircraft with one wing nearer than the other.
	   Stated against the plane's own size so the amount of foreshortening is the
	   same whether the plane is 8vmax or 60vmax — a fixed distance would flatten
	   a small plane and warp a large one. Not a setting: it is the lens, not the
	   movement. */
	--airline-transition-plane-depth: calc( var( --airline-transition-plane-size, 26vmax ) * 2.4 );
}

.airline-transition {
	display: none;
	position: fixed;
	inset: 0;
	overflow: hidden;

	/* Never interactive, in any state. A full-screen overlay that can swallow a
	   click is a worse failure than any missed animation, and there is nothing
	   in here to click: the panel is decoration and the plane is aria-hidden. */
	pointer-events: none;

	/* Above the admin bar's 99999, so a logged-in editor sees the same wipe a
	   visitor does rather than a bar floating on top of it — and above Blocksy's
	   off-canvas panel (.ct-panel, 999999), which is printed after this element
	   and stays open on a link click, so at an equal index it painted over the
	   sweep and the menu appeared to be hiding the transition. */
	z-index: 1000000;

	contain: layout paint;
}

/* Nothing exists until a navigation is happening. Idle pages pay nothing: no
   layer, no paint, no compositing. */
html[data-airline-transition] .airline-transition {
	display: block;
}

/*
 * Covered means covered, whether or not the mask has been reached yet.
 *
 * "in" is the state between the first paint and the document being parsed, and
 * it is the one window where the mask can fail to do its job: the mask element
 * is printed at wp_body_open, and a theme that never calls it gets the mask from
 * wp_footer instead — at the very end, after everything it was supposed to be
 * hiding has already been painted. This says the same thing without depending on
 * an element being anywhere in particular.
 *
 * Visibility is inherited and can be turned back on further down, which is what
 * makes this safe: the body goes dark and the mask is made visible again wherever
 * it happens to sit, even nested inside a theme's wrapper. Hiding `body > *`
 * instead would hide the mask's own ancestor in exactly that case.
 *
 * It ends at "in-play", which is set once the document is parsed — by which point
 * the mask certainly exists, and the page underneath has to be visible for the
 * uncover to reveal anything. If transition.js never runs at all, the failsafe in
 * the head snippet clears the attribute and the page appears; that is the same
 * guarantee the mask itself relies on.
 */
html[data-airline-transition="in"] body {
	visibility: hidden;
}

html[data-airline-transition="in"] .airline-transition {
	visibility: visible;
}

/* --------------------------------------------------------------- the mask */

.airline-transition__panel {
	position: absolute;
	top: 50%;
	left: 50%;
	width: var( --airline-transition-span, 150vmax );
	height: var( --airline-transition-span, 150vmax );
	margin-top: calc( var( --airline-transition-span, 150vmax ) / -2 );
	margin-left: calc( var( --airline-transition-span, 150vmax ) / -2 );
	background-color: var( --airline-transition-color, #1c557b );

	/* At rest: one full width back down the line, entirely off screen. */
	transform:
		rotate( var( --airline-transition-angle, -45deg ) )
		translateX( calc( var( --airline-transition-span, 150vmax ) * -1 ) );

	backface-visibility: hidden;
}

/* Leaving. Fills forwards and stays covering: the navigation is fired the moment
   this ends, and the covered frame has to survive until the browser paints the
   next document over it. */
html[data-airline-transition="out"] .airline-transition__panel {
	will-change: transform;
	animation:
		airline-transition-cover
		var( --airline-transition-cover, 620ms )
		var( --airline-transition-ease )
		both;
}

/* Arrived, and holding. No animation yet — this is the still frame that has to
   match the last frame of the outgoing sweep exactly. transition.js promotes it
   to "in-play" once the document is ready, so a slow page holds the blue for
   longer rather than uncovering onto something half-built. */
html[data-airline-transition="in"] .airline-transition__panel,
html[data-airline-transition="in-play"] .airline-transition__panel {
	transform: rotate( var( --airline-transition-angle, -45deg ) ) translateX( 0 );
}

/* Leaving again, the same way it came. The hold is the animation's delay rather
   than a state of its own: with a backwards fill, the delay *is* the covered
   pause, which keeps the whole arriving half to a single animation. */
html[data-airline-transition="in-play"] .airline-transition__panel {
	will-change: transform;
	animation:
		airline-transition-uncover
		var( --airline-transition-uncover, 620ms )
		var( --airline-transition-ease )
		var( --airline-transition-hold, 900ms )
		both;
}

@keyframes airline-transition-cover {
	from {
		transform:
			rotate( var( --airline-transition-angle, -45deg ) )
			translateX( calc( var( --airline-transition-span, 150vmax ) * -1 ) );
	}
	to {
		transform: rotate( var( --airline-transition-angle, -45deg ) ) translateX( 0 );
	}
}

@keyframes airline-transition-uncover {
	from {
		transform: rotate( var( --airline-transition-angle, -45deg ) ) translateX( 0 );
	}
	to {
		transform:
			rotate( var( --airline-transition-angle, -45deg ) )
			translateX( var( --airline-transition-span, 150vmax ) );
	}
}

/* -------------------------------------------------------------- the plane */

/*
 * Three nested elements, because three things move at once and a CSS transform
 * is not additive — two animations on one element would mean the second silently
 * replacing the first. So: the box travels, the wrapper fades, the drawing rocks.
 */

.airline-transition__plane {
	position: absolute;
	top: 50%;
	left: 50%;
	width: var( --airline-transition-plane-size, 26vmax );
	height: var( --airline-transition-plane-size, 26vmax );
	margin-top: calc( var( --airline-transition-plane-size, 26vmax ) / -2 );
	margin-left: calc( var( --airline-transition-plane-size, 26vmax ) / -2 );

	/* Same axis as the mask, and the trailing quarter-turn is what points the
	   aircraft down it: the drawing is nose-up, which is a quarter-turn short of
	   the local +x the translate runs along. Change the direction setting and the
	   plane turns with the wipe, because both are built from the one angle. */
	transform:
		rotate( var( --airline-transition-angle, -45deg ) )
		translateX( calc( var( --airline-transition-span, 150vmax ) * -0.6 ) )
		rotate( 90deg );
}

.airline-transition__plane-art {
	width: 100%;
	height: 100%;
	opacity: 0;
}

/*
 * Matched by structure, not by class: the artwork can be replaced from the
 * settings screen and a drawing exported from anywhere will not carry the
 * plugin's class names.
 */
.airline-transition__plane-art > svg {
	display: block;
	width: 100%;
	height: 100%;
}

/*
 * Recolouring, when it is switched on. A CSS rule beats an SVG presentation
 * attribute, so this flattens a drawing that came with its own fills — which is
 * what is wanted on a solid mask. Switched off, the modifier is absent and the
 * artwork keeps its own colours.
 */
.airline-transition--recolor .airline-transition__plane-art > svg,
.airline-transition--recolor .airline-transition__plane-art > svg * {
	fill: var( --airline-transition-plane-color, #ffffff );
}

html[data-airline-transition="in-play"] .airline-transition__plane {
	will-change: transform;
	animation:
		airline-transition-fly
		var( --airline-transition-fly, 1240ms )
		var( --airline-transition-fly-ease )
		both;
}

html[data-airline-transition="in-play"] .airline-transition__plane-art {
	animation:
		airline-transition-plane-fade
		var( --airline-transition-fly, 1240ms )
		linear
		both;
}

html[data-airline-transition="in-play"] .airline-transition__plane-art > svg {
	animation:
		airline-transition-plane-roll
		var( --airline-transition-fly, 1240ms )
		ease-in-out
		both;
}

/*
 * The flight is longer than the hold on purpose, so the plane is still ahead of
 * the mask as the mask starts to leave — it is leading the wipe out rather than
 * being wiped away with it. Airline_Transition_Settings::fly_ms() sets the length.
 */
@keyframes airline-transition-fly {
	from {
		transform:
			rotate( var( --airline-transition-angle, -45deg ) )
			translateX( calc( var( --airline-transition-span, 150vmax ) * -0.6 ) )
			rotate( 90deg );
	}
	to {
		transform:
			rotate( var( --airline-transition-angle, -45deg ) )
			translateX( calc( var( --airline-transition-span, 150vmax ) * 0.6 ) )
			rotate( 90deg );
	}
}

/* It fades up early and is gone before the end, so it never blinks out of
   existence at the edge of the screen — by the time the opacity reaches zero it
   is off the visible area anyway. */
@keyframes airline-transition-plane-fade {
	0%,
	100% {
		opacity: 0;
	}
	10%,
	82% {
		opacity: var( --airline-transition-plane-opacity, 1 );
	}
}

/*
 * One bank to the left, one to the right, level again — a single gesture across
 * the pass rather than a loop. A repeating rock reads as a metronome; a plane
 * banks once, holds it, and comes out of it.
 *
 * It is a roll about the aircraft's own length, not a turn of the picture. The
 * drawing is a plan view — a plane seen from above — and from above a roll does
 * not spin the silhouette round, it tips one wing towards the eye and the other
 * away. That is `rotateY` in the drawing's own frame, whose y axis runs nose to
 * tail; with the perspective in front of it the near wing grows and the far one
 * shortens, which is what makes it read as banking rather than as being scaled.
 * An in-plane `rotate()` here would be a change of heading, and a plane that
 * changes heading twice and still flies dead straight looks like a compass
 * needle rather than an aircraft.
 *
 * The frame is the drawing's, which the travel transform has already turned onto
 * the line of flight, so left and right mean left and right of where it is going
 * whichever way the mask is travelling. The sway goes with the bank because an
 * aircraft that banks also drifts — it is derived from the roll in
 * Airline_Transition_Settings::plane_sway(), so a roll of zero flies it flat and
 * straight rather than flat and wandering.
 */
@keyframes airline-transition-plane-roll {
	0% {
		transform:
			translateX( 0 )
			perspective( var( --airline-transition-plane-depth ) )
			rotateY( 0deg );
	}
	30% {
		transform:
			translateX( calc( var( --airline-transition-plane-sway, 4% ) * -1 ) )
			perspective( var( --airline-transition-plane-depth ) )
			rotateY( calc( var( --airline-transition-plane-roll, 24deg ) * -1 ) );
	}
	70% {
		transform:
			translateX( var( --airline-transition-plane-sway, 4% ) )
			perspective( var( --airline-transition-plane-depth ) )
			rotateY( var( --airline-transition-plane-roll, 24deg ) );
	}
	100% {
		transform:
			translateX( 0 )
			perspective( var( --airline-transition-plane-depth ) )
			rotateY( 0deg );
	}
}

/* ------------------------------------------------------------- the picture */

/*
 * The other gesture: a link with a picture of the post beside it carries the picture
 * to where that post's own picture sits on the next page.
 *
 * There is no animation written here, because none is needed. Two elements in two
 * different documents holding the same `view-transition-name` are morphed into one
 * another by the browser — that is the whole mechanism, and it is the same one
 * Greenshift's page transition is built on. All the plugin does is put the name on
 * the two ends: transition.js on the thumbnail as the page is left, and this rule on
 * the featured image of the page being arrived at.
 *
 * Bound to the attribute so the name exists only for the one navigation that wants
 * it. A name left standing on an ordinary page would be a name looking for a partner
 * on every subsequent navigation, and the head snippet — which is where the attribute
 * is set — is the only code that runs early enough to be sure of being in time.
 *
 * The classes come from Airline_Transition_Image, which marks the featured images as
 * WordPress renders them; the names are stated in Airline_Transition_Settings, and
 * the test harness compares the two so they cannot drift.
 */
html[data-airline-transition-image] .airline-transition-subject {
	view-transition-name: airline-transition-image;
}

::view-transition-group( airline-transition-image ) {
	animation-duration: var( --airline-transition-image, 620ms );
	animation-timing-function: var( --airline-transition-image-ease );
}

/*
 * And the picture does not cross-fade with itself.
 *
 * This is the rule that turns the gesture from something that often looked like a
 * plain dissolve into something that travels, and the reason is worth stating
 * because the symptom pointed the wrong way entirely. Everything about the pairing
 * was already right — the console said `marked: true, holding: 1, expected: 1` on
 * exactly the navigations that looked wrong. What was wrong was what the browser had
 * to draw with.
 *
 * A cross-document transition captures the arriving page at its first rendering
 * opportunity, and images do not block rendering. So the new snapshot of the
 * photograph is very often an empty box, and the default animations — a fade-out on
 * the old and a fade-in on the new — then dissolve a real thumbnail into nothing,
 * over the two page snapshots dissolving behind it. Which is a fade. It was
 * intermittent because a cached photograph was painted in time and a cold one
 * was not.
 *
 * With no isolation and no animation on either half, both sit at full opacity and
 * the new paints straight over the old:
 *
 *   - where the new has content, that is what is seen — sharp, at the destination's
 *     own resolution, rather than two copies ghosting through each other;
 *   - where it is empty, the old shows through it and is still carried and resized
 *     by the group.
 *
 * So a slow destination degrades to "the thumbnail travelled and settled" instead of
 * to a dissolve, which is the failure worth designing for: the group animation is
 * the gesture, and the images are only what fills it.
 */
/* `auto`, not `none` — `isolation` takes auto or isolate and nothing else, and a
   value it does not take is a declaration the browser drops on the floor. What is
   being undone here is the UA stylesheet's `isolation: isolate` on the pair, which
   exists so that the two halves can blend with `plus-lighter` and cross-fade
   without darkening through the middle. Neither is wanted once they no longer
   cross-fade at all. */
::view-transition-image-pair( airline-transition-image ) {
	isolation: auto;
}

::view-transition-old( airline-transition-image ),
::view-transition-new( airline-transition-image ) {
	animation: none;
	mix-blend-mode: normal;
}

/*
 * The same photograph at two sizes, and usually at two crops: a wide thumbnail in a
 * card becoming a tall picture at the top of an article. Left alone the browser
 * stretches each snapshot to the box the other one is heading for, and the picture
 * squashes on the way. Filling the box and cropping instead is the difference between
 * a photograph moving and a photograph being pulled about.
 */
::view-transition-old( airline-transition-image ),
::view-transition-new( airline-transition-image ) {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/*
 * A picture that led somewhere with no picture of its own — a post whose featured
 * image was never set, a link that turned out to go elsewhere. `:only-child` is how
 * the pair says it has no other half. It goes with the page it belonged to rather
 * than hanging over the new one waiting for something that is not coming.
 */
::view-transition-old( airline-transition-image ):only-child {
	animation: airline-transition-picture-out 180ms ease-out both;
}

@keyframes airline-transition-picture-out {
	to {
		opacity: 0;
	}
}

/*
 * The two pages crossing over behind the picture — and finished before it lands.
 *
 * Held to a share of the picture's time rather than all of it. Matching the two
 * exactly meant the whole screen was still dissolving at the moment the photograph
 * arrived, so there was never a frame where the new page was simply there with the
 * picture settling into it; everything moved for the same length of time and the eye
 * had nothing to follow. Letting the pages change over first leaves the last stretch
 * of the animation to the one thing worth watching.
 */
html[data-airline-transition-image]::view-transition-old( root ),
html[data-airline-transition-image]::view-transition-new( root ) {
	animation-duration: calc( var( --airline-transition-image, 620ms ) * 0.6 );
	animation-timing-function: var( --airline-transition-image-ease );
}

/*
 * What is *not* here: the opt-in that makes any of this happen, the rule that hands
 * every other navigation back to the site, and the reduced-motion guard. All three
 * belong to settings that can be switched off, and a stylesheet cannot ask whether
 * they were — Airline_Transition_Assets::custom_properties() prints them, the same
 * way it prints the mask's own reduced-motion backstop.
 */

/* ------------------------------------------------------------------ leaving */

/*
 * A page on its way out. Nothing on it can be clicked any more and the cursor
 * says so, whichever way it is leaving: under the mask (which lets pointer
 * events through, so a hand cursor would still show on the links beneath the
 * blue) or by the picture, where the page stays fully visible until the server
 * answers and a second click on the same thumbnail was still an invitation.
 * transition.js sets the attribute on both paths and clears it wherever the page
 * is put back — the failsafe, a bfcache restore.
 *
 * pointer-events: none also drops :hover, so a card's hover effect reverts while
 * the next page loads. That is the intended reading: the link has been spent.
 */
html[data-airline-transition-leaving] {
	cursor: progress;
}

html[data-airline-transition-leaving] a,
html[data-airline-transition-leaving] button {
	pointer-events: none;
}

/* ------------------------------------------------------------------ opt-outs */

/*
 * The page-level opt-out marker. An element carrying either of these says "no
 * transition starts on this page": transition.js finds it and leaves every link
 * alone, and PHP looks for the same token in the content so the intro can be
 * called off before anything is painted.
 *
 * It is hidden here because the marker is a statement about the page, not
 * something to look at — an editor should be able to drop an empty block in and
 * have it not appear. The names are stated in
 * Airline_Transition_Settings::OFF_CLASS and ::OFF_ATTR, and a stylesheet cannot
 * read a PHP constant; the test harness compares the two so they cannot drift.
 */
[data-airline-transition-off],
.airline-transition-off {
	display: none;
}

/*
 * The reduced-motion backstop is *not* here. It belongs to a setting that can be
 * switched off, and a stylesheet cannot ask whether it was: hiding the mask from
 * a visitor whose links transition.js is still intercepting would give them the
 * delay with none of the picture. Airline_Transition_Assets::custom_properties()
 * prints that block only when the setting is on.
 */

@media print {
	.airline-transition {
		display: none !important;
	}
}

/* ------------------------------------------------------------ the word */

/*
 * "CHARGEMENT", rolled onto the mask one letter at a time, like a departures
 * board — and only when a load has kept the screen blue for longer than a
 * moment. The attribute is set by the head snippet (arriving) or transition.js
 * (leaving) after the delay in the settings, and taken off as the uncover
 * begins, so a fast page never sees it. Until transition.js has run the word
 * is plain text; the cells are its doing.
 */
.airline-transition__loading {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 0 6vw;
	opacity: 0;
	pointer-events: none;
	transition: opacity 240ms ease-out;
	z-index: 1;
}

html[data-airline-transition-loading] .airline-transition__loading {
	opacity: 1;
}

.airline-transition__flap {
	display: inline-flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 0.08em;
	/* The size of the site's ordinary text — it is a status line, not a headline. */
	font: inherit;
	font-weight: 600;
	line-height: 1.2;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var( --airline-transition-loading-color, #ffffff );
	perspective: 600px;
}

.airline-transition__cell {
	display: inline-block;
	min-width: 0.9em;
	padding: 0.18em 0.08em;
	text-align: center;
	border-radius: 0.08em;
	background: rgba( 0, 0, 0, 0.22 );
	box-shadow: inset 0 -0.5px 0 rgba( 255, 255, 255, 0.18 ), inset 0 0.5px 0 rgba( 0, 0, 0, 0.35 );
	font-variant-numeric: tabular-nums;
	transform-origin: 50% 50%;
	backface-visibility: hidden;
}

.airline-transition__cell--gap {
	background: none;
	box-shadow: none;
	min-width: 0.4em;
}

.airline-transition__cell.is-flip {
	animation: airline-transition-flip 90ms ease-out;
}

@keyframes airline-transition-flip {
	from {
		transform: rotateX( -80deg );
	}
	to {
		transform: rotateX( 0 );
	}
}

@media ( prefers-reduced-motion: reduce ) {
	.airline-transition__cell.is-flip {
		animation: none;
	}
}
