/*
 * Movement.
 *
 * Only transform and opacity, so the work stays on the compositor and a cheap
 * phone does not stutter mid-game. Everything is wrapped in a reduced-motion
 * query: a player who has asked for stillness gets a game that simply appears.
 */

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

	/*
	 * A tile arriving on the board drops onto it.
	 *
	 * Small, because nothing is clipped: a square paints its tile's shadow, so an
	 * animation may not reach further out of the square than that shadow does — see
	 * board.css. A pending tile's own shadow allows 4.5% of a square above it, and
	 * a 4% rise from 6% shy of full size comes to 1% at the top of the movement.
	 * The fade is what carries the arrival at that amplitude; the old 40% rise laid
	 * the tile across most of the square above it.
	 */
	.square--pending .tile {
		animation: tile-drop 160ms cubic-bezier(0.2, 0.9, 0.3, 1.2);
	}

	@keyframes tile-drop {
		from { transform: translateY(-4%) scale(0.94); opacity: 0; }
		to { transform: none; opacity: 1; }
	}

	/*
	 * A word that has just been played settles in, so everyone can see where it
	 * went without being told — which is how the opponent's move announces
	 * itself.
	 *
	 * The flash is a separate layer fading out rather than an animated
	 * box-shadow on the tile: shadows are painted, opacity is not, and a phone
	 * lighting up seven tiles at once notices the difference.
	 *
	 * It grows into place rather than shrinking into it, and that way round is the
	 * shadow's doing: a settled tile's shadow allows 2% of a square above it, so a
	 * scale over 1.04 reaches past its own thickness onto the tile above — where
	 * anything under it is inside the square by definition. 8% is what the old
	 * overshoot came to in pixels, so the movement is the same size as it was.
	 */
	.square--just-played .tile {
		animation: tile-settle 280ms ease-out;
	}

	@keyframes tile-settle {
		from { transform: scale(0.92); }
		to { transform: none; }
	}

	.square--just-played .tile::after {
		content: "";
		position: absolute;
		inset: 0;
		border-radius: inherit;
		box-shadow: inset 0 0 0 0.3cqw var(--board-accent);
		pointer-events: none;
		animation: tile-flash 420ms ease-out forwards;
	}

	@keyframes tile-flash {
		from { opacity: 1; }
		to { opacity: 0; }
	}

	/* The rack and buttons going out of reach behind the teacher's report, and
	   coming back. Opacity, so it stays off the main thread. */
	.rack,
	.controls {
		transition: opacity 180ms ease;
	}

	/* A score that has just gone up draws the eye to itself. */
	.seat__score--gained {
		transform-origin: right center;
		animation: score-bump 280ms cubic-bezier(0.2, 0.9, 0.3, 1.4);
	}

	@keyframes score-bump {
		0% { transform: none; }
		40% { transform: scale(1.28); }
		100% { transform: none; }
	}

	/* A refused word shakes rather than announcing itself twice. The tiles are
	   pending, and a pending tile's shadow reaches 12% of a square to either side,
	   so an 8% shake stays inside what the tile already paints. */
	.square--invalid .tile {
		animation: tile-refuse 320ms ease-in-out;
	}

	@keyframes tile-refuse {
		0%, 100% { transform: none; }
		25% { transform: translateX(-8%); }
		75% { transform: translateX(8%); }
	}

	.rack-tile,
	.sheet__tile {
		transition: transform 120ms ease, box-shadow 120ms ease;
	}

	/* A chosen tile lifts, wherever it is being chosen. */
	.rack-tile--selected,
	.sheet__tile--chosen {
		transform: translateY(-0.35rem);
	}

	.toast {
		animation: toast-in 200ms ease-out;
	}

	.toast--leaving {
		animation: toast-out 280ms ease-in forwards;
	}

	/* Down from the top edge, and back up the way it came. */
	@keyframes toast-in {
		from { transform: translateY(-1.25rem); opacity: 0; }
		to { transform: none; opacity: 1; }
	}

	@keyframes toast-out {
		to { transform: translateY(-0.75rem); opacity: 0; }
	}

	.sheet__panel {
		animation: sheet-up 220ms cubic-bezier(0.2, 0.9, 0.3, 1);
	}

	@keyframes sheet-up {
		from { transform: translateY(1.5rem); opacity: 0; }
		to { transform: none; opacity: 1; }
	}

	/* The chevron turns over rather than swapping ends. Transform only, which is
	   why the caret is nudged with translateY rather than a margin. */
	.chooser__caret {
		transition: transform 140ms ease;
	}

	.chooser__list {
		animation: chooser-open 120ms ease-out;
	}

	@keyframes chooser-open {
		from { transform: translateY(-0.25rem); opacity: 0; }
		to { transform: none; opacity: 1; }
	}

	/*
	 * A press that landed, on every button in the game. See ui/press.js, which is
	 * the one place any of them is asked for.
	 *
	 * The light is a layer fading out rather than a background or a shadow, for the
	 * reason a just-played tile flashes with one: a fill is painted and opacity is
	 * not, and this is the one animation here that any button can ask for as fast
	 * as a thumb can go. It is painted in the button's own box, which is what the
	 * pressables are positioned for — see ui.css.
	 *
	 * It is the button's own ink and never a colour of its own. An ink is already
	 * chosen to contrast with the face it is printed on, so currentColor is light
	 * on the dark buttons and dark on the gold, red, green and blue ones — where a
	 * cream layer lifts the face by 1.15:1 to 1.21:1 and is not there at all, on
	 * the buttons the player came for.
	 *
	 * 0.24 of it is measured rather than chosen: it is the most that leaves every
	 * label above the 4.5:1 floor through its own flash, the red sheet button being
	 * the worst at 4.54:1 against the 6.92:1 it rests at. What that buys is a lift
	 * of 1.50:1 on the gold Play, 1.51:1 and 1.54:1 on Start and Join, 1.67:1 on an
	 * unchosen tab and 2.05:1 on a quiet button. The layer covers the label as well
	 * as the face and costs it nothing, since a colour laid over itself is itself.
	 */
	.pressed-a::after,
	.pressed-b::after {
		content: "";
		position: absolute;
		inset: 0;
		border-radius: inherit;
		background: currentColor;
		opacity: 0;
		/* The layer outlives the flash at opacity 0 — press.js never takes the class
		   off — so it must never be what the next press lands on. */
		pointer-events: none;
		animation-duration: 180ms;
		animation-timing-function: ease-out;
	}

	/*
	 * Two names for one animation, alternated by press.js.
	 *
	 * An animation is restarted by its name changing and by nothing else, so a
	 * second press inside the 180ms would otherwise go unanswered — and the presses
	 * that most want answering are the fast ones, a room code typed on the pad, the
	 * same letter twice. The two are identical and must stay so.
	 */
	.pressed-a::after { animation-name: press-flash-a; }
	.pressed-b::after { animation-name: press-flash-b; }

	@keyframes press-flash-a { from { opacity: 0.24; } }
	@keyframes press-flash-b { from { opacity: 0.24; } }

	/*
	 * An opponent down to their last few tiles, pulsing. See LOW_TILES in
	 * ui/scoreboard.js for when it starts.
	 *
	 * This is the one thing in the game that moves for as long as a situation
	 * lasts rather than for as long as a change takes, and what makes that
	 * affordable is that the situation is short: a rack falls under a full one
	 * only once the bag is empty, so the pulse is the last turn or two of the game
	 * and not a mark the player sits beside. It is exempt from the rule that the
	 * next action lands everything in the air, for the reason the score bump and
	 * the hover transitions are — there is nothing to apply and nothing being
	 * aimed at — and a player who has asked for stillness gets the count alone,
	 * which is the whole of the information.
	 *
	 * It is a dip and cannot be a lift: on a dark card the marks are the tile's own
	 * cream and there is nowhere brighter for them to go. Nor may it be a size —
	 * the marks share their column with the score above them, so a card whose
	 * player is on a single figure sizes that column to the marks exactly and the
	 * row's own clip takes anything that grows. Measured at 320px with four seats:
	 * 24px of room round four marks beside a score of 1,109 and 0.0px beside a
	 * score of 8.
	 *
	 * So the whole of it is the dip, and how far it goes is what makes it visible
	 * at all. The floor is 3:1, which is what a mark carrying information owes:
	 * 0.62 holds 5.26:1 on a dark card and 3.19:1 on a gold one, against the
	 * 11.03:1 and 7.10:1 they rest at. It goes below the status line beside them on
	 * the gold card, and may — the rule that the smallest marks must not also be
	 * the faintest is about how a card rests, and these rest brighter than that
	 * line and carry their count in words besides. What that buys is a change of
	 * 26 in L* against the 12 of a dip to 0.82, which on a 4px mark in the corner
	 * of the eye was invisible on a phone.
	 *
	 * The dwell at each end is the other half of being seen. Eased straight
	 * through, the marks are within a twentieth of full brightness for two fifths
	 * of the cycle and at the trough for an instant, which is a fade rather than a
	 * flash; holding 320ms at each end is what gives it a beat. 1.6s is 0.6Hz, far
	 * under the three a flash may not pass.
	 *
	 * The whole row is one layer rather than a layer a mark, since they pulse as
	 * one anyway.
	 */
	.seat__tiles--low {
		animation: tiles-low 1600ms ease-in-out infinite;
	}

	@keyframes tiles-low {
		0%, 20% { opacity: 1; }
		50%, 70% { opacity: 0.62; }
		100% { opacity: 1; }
	}
}

/*
 * The caret in a box the game draws for itself.
 *
 * Steps rather than a fade, because a caret is on or off and a browser's own
 * blinks that way; 1.06s is roughly what the platform uses. The visibility is the
 * stylesheet's elsewhere — a player who has asked for stillness gets a caret that
 * holds steady rather than none at all, since it is a place marker before it is a
 * movement, and only the blink is behind this query.
 */
@keyframes caret-blink {
	0%, 50% { opacity: 1; }
	50.01%, 100% { opacity: 0; }
}
