/*
 * Motion.
 *
 * The rule this file is built around: **nothing is hidden unless something is
 * definitely going to reveal it.** The classic scroll-reveal bug is an element
 * left at `opacity: 0` because the animation that would have restored it never
 * ran — no JavaScript, an observer that failed, or Reduce Motion turned on. So
 * the hidden state lives behind `html.gamife-motion`, a class only added by
 * `motion.js` after it has confirmed both that it is running and that the
 * visitor has not asked for less movement.
 *
 * The easings are the app's own springs, ported as cubic-beziers in tokens.css.
 */

/* ------------------------------------------------------- scroll reveals --- */

.gamife-reveal {
	/* Deliberately empty. Visible is the default and the fallback. */
	transition:
		opacity var(--dur-arrive) var(--ease-arrive),
		transform var(--dur-arrive) var(--ease-arrive);
	transition-delay: var(--gamife-delay, 0ms);
}

html.gamife-motion .gamife-reveal {
	opacity: 0;
	transform: translateY(1.25rem);
}

html.gamife-motion .gamife-reveal.is-visible {
	opacity: 1;
	transform: none;
}

/*
 * A staggered group: children arrive one after another on the app's own
 * 35ms step. The app caps its stagger at 0.28s, which is eight children —
 * past that the delay stops growing rather than the last item arriving late.
 */
html.gamife-motion .gamife-stagger > * {
	opacity: 0;
	transform: translateY(0.75rem);
	transition:
		opacity var(--dur-settle) var(--ease-settle),
		transform var(--dur-settle) var(--ease-settle);
}

html.gamife-motion .gamife-stagger.is-visible > * {
	opacity: 1;
	transform: none;
}

html.gamife-motion .gamife-stagger.is-visible > :nth-child(1) { transition-delay: calc(var(--stagger) * 0); }
html.gamife-motion .gamife-stagger.is-visible > :nth-child(2) { transition-delay: calc(var(--stagger) * 1); }
html.gamife-motion .gamife-stagger.is-visible > :nth-child(3) { transition-delay: calc(var(--stagger) * 2); }
html.gamife-motion .gamife-stagger.is-visible > :nth-child(4) { transition-delay: calc(var(--stagger) * 3); }
html.gamife-motion .gamife-stagger.is-visible > :nth-child(5) { transition-delay: calc(var(--stagger) * 4); }
html.gamife-motion .gamife-stagger.is-visible > :nth-child(6) { transition-delay: calc(var(--stagger) * 5); }
html.gamife-motion .gamife-stagger.is-visible > :nth-child(7) { transition-delay: calc(var(--stagger) * 6); }
html.gamife-motion .gamife-stagger.is-visible > :nth-child(n + 8) { transition-delay: calc(var(--stagger) * 7); }

/* ------------------------------------------------------------ the stage --- */

/*
 * The pinned phone's screen. All six shots occupy it at once and the active one
 * is faded up over the others — which is only safe once the script is running,
 * because six screenshots stacked with no script to choose between them is a
 * stack of six screenshots. Until then sections.css shows the first and hides
 * the rest, and this block never applies.
 *
 * The outgoing shot scales up very slightly as it leaves rather than sliding:
 * the phone is a fixed frame and anything that moves laterally inside it reads
 * as the frame having moved.
 */
html.gamife-motion .gamife-story__shot:not(:first-child) {
	display: block;
}

html.gamife-motion .gamife-story__shot {
	opacity: 0;
	transform: scale(1.035);
	transition:
		opacity var(--dur-arrive) var(--ease-arrive),
		transform var(--dur-arrive) var(--ease-arrive);
}

html.gamife-motion .gamife-story__shot.is-current {
	opacity: 1;
	transform: none;
}

/* With the understudy on, the video steps aside rather than being removed —
   it is still the element the beat is paired to, it just has nothing to show. */
html.gamife-motion .gamife-story__stage.is-stills .gamife-story__reel {
	opacity: 0;
}

/*
 * The stage lifts a little while a beat is actually being read, and settles
 * back between them. Tonal and small — the same restraint the cards use.
 */
html.gamife-motion .gamife-story__stage .gamife-phone {
	transition: transform var(--dur-arrive) var(--ease-arrive);
}

html.gamife-motion .gamife-story__stage.is-reading .gamife-phone {
	transform: translateY(-0.5rem);
}

/* ----------------------------------------------------------- coin pill --- */

/*
 * The signature. The pill is the app's own HUD, and it only appears once the
 * story starts — the page is a focus session, and you have not begun one until
 * you are reading the story.
 *
 * `visibility`, not just `opacity`. Opacity alone would leave the pill in the
 * accessibility tree, so a screen reader would announce a counter reading zero
 * that is never going to move — which is exactly what a visitor with no
 * JavaScript would get, since only the script adds `is-earning`.
 */
.gamife-header__coins {
	opacity: 0;
	visibility: hidden;
	transform: translateY(-0.4rem) scale(0.96);
	pointer-events: none;
	transition:
		opacity var(--dur-settle) var(--ease-settle),
		transform var(--dur-settle) var(--ease-settle),
		visibility 0s linear var(--dur-settle);
}

.gamife-header__coins.is-earning {
	opacity: 1;
	visibility: visible;
	transform: none;
	transition-delay: 0s;
}

.gamife-coin-pill__coin,
.gamife-coin-pill__value {
	margin-right: 0.4rem;
}

/* ----------------------------------------------------------------- cast --- */

/*
 * The cast breathes rather than walks. A sprite-sheet walk cycle would need
 * frames isolated from a live 3D scene, and inventing them would mean shipping
 * artwork the app does not actually contain — so the movement here is a bob,
 * off the same spring the app settles its own cards with, each one offset so
 * no two are ever in unison.
 *
 * The bob is on the image, not on the figure. The figure is what carries the
 * reveal transform, and two rules animating one `transform` means the last one
 * declared wins and the other silently does nothing.
 */
@keyframes gamife-bob {
	0%, 100% { transform: translateY(0); }
	50% { transform: translateY(-6px); }
}

html.gamife-motion .gamife-cast__member img {
	animation: gamife-bob 3.6s var(--ease-settle) infinite;
}

html.gamife-motion .gamife-cast__member:nth-child(2) img { animation-delay: 0.45s; }
html.gamife-motion .gamife-cast__member:nth-child(3) img { animation-delay: 0.9s; }
html.gamife-motion .gamife-cast__member:nth-child(4) img { animation-delay: 1.35s; }
html.gamife-motion .gamife-cast__member:nth-child(5) img { animation-delay: 1.8s; }

/* The row is the one place the cast can be touched, so it is the one place
   they react. */
.gamife-cast__member {
	transition: transform var(--dur-press) var(--ease-press);
}

.gamife-cast__member:hover {
	transform: translateY(-9px);
}

/* ------------------------------------------------------------- the tilt --- */

/*
 * The hero phone leans towards the pointer. motion.js writes the two angles and
 * only ever on a fine pointer — there is no hover to lean towards on a touch
 * screen, and the transform sitting here unset is a no-op either way.
 */
html.gamife-motion .gamife-hero__phone .gamife-phone {
	transform:
		perspective(900px)
		rotateX(var(--tilt-x, 0deg))
		rotateY(var(--tilt-y, 0deg));
	transition: transform 320ms var(--ease-settle);
}

/* -------------------------------------------------------------- the FAQ --- */

/*
 * Opening animates for free: the content is in the DOM the instant `open` is
 * set. Closing does not — the browser removes it immediately, so there is
 * nothing left to animate — which is why motion.js holds `open` for the length
 * of this and clears it afterwards.
 *
 * The animation is on the content rather than on the element's height: there is
 * no wrapper inside core/details to measure, and animating `height: auto` would
 * mean measuring one in JavaScript on every open.
 */
@keyframes gamife-details-in {
	from { opacity: 0; transform: translateY(-0.35rem); }
	to { opacity: 1; transform: none; }
}

@keyframes gamife-details-out {
	from { opacity: 1; transform: none; }
	to { opacity: 0; transform: translateY(-0.35rem); }
}

html.gamife-motion .gamife-faq__list details[open] > :not(summary) {
	animation: gamife-details-in var(--dur-settle) var(--ease-settle);
}

html.gamife-motion .gamife-faq__list details.is-closing > :not(summary) {
	animation: gamife-details-out 200ms var(--ease-settle) forwards;
}

/* ------------------------------------------------------- the hero video --- */

.gamife-hero__still {
	display: none;
}

/*
 * Reduce Motion swaps the looping capture for its own poster frame. Done in CSS
 * rather than in script so it holds even if the script never runs, and so the
 * browser never starts decoding a video it is about to be told to stop.
 */
@media (prefers-reduced-motion: reduce) {
	.gamife-hero__loop {
		display: none;
	}

	.gamife-hero__still {
		display: block;
	}
}

/* ------------------------------------------------------- the global off --- */

/*
 * Belt and braces. `html.gamife-motion` is never added when Reduce Motion is
 * set, so most of this file is already inert — but third-party blocks, core
 * block animations and anything added later are not, and the app honours
 * Reduce Motion, so the site must too.
 */
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}

	/* And the important half: everything stays visible. */
	.gamife-reveal,
	.gamife-stagger > * {
		opacity: 1 !important;
		transform: none !important;
	}
}
