/*
 * Typing games.
 *
 * Everything here uses the theme's own tokens, so a game follows whatever
 * colours, radius and card style the site is set to rather than looking like
 * a bolted-on widget.
 */

/* ==========================================================================
   1. Board shell
   ========================================================================== */

.tpp-game {
	display: flex;
	flex-direction: column;
	gap: var(--tpp-space-3);
	padding: var(--tpp-space-4);
	background: var(--tpp-surface);
	border: 1px solid var(--tpp-border);
	border-radius: var(--tpp-radius-lg);
	box-shadow: var(--tpp-shadow-sm);
	container-type: inline-size;
}

/* Match the framed card style when the site is set to it. */
.tpp-cards-offset .tpp-game {
	border: 2px solid var(--tpp-text);
	border-radius: var(--tpp-radius);
	box-shadow: 9px 9px 0 0 var(--tpp-card-offset, var(--tpp-primary));
}

.tpp-game-bar {
	display: flex;
	flex-wrap: wrap;
	gap: var(--tpp-space-3);
	align-items: center;
	justify-content: space-between;
}

.tpp-game-modes,
.tpp-game-tools {
	display: flex;
	flex-wrap: wrap;
	gap: var(--tpp-space-1);
}

.tpp-game-hint {
	margin: 0;
	padding: var(--tpp-space-2) var(--tpp-space-3);
	background: var(--tpp-bg-alt);
	border-left: 3px solid var(--tpp-accent);
	border-radius: var(--tpp-radius-sm);
	font-size: 0.88rem;
	color: var(--tpp-text-muted);
}

/* ==========================================================================
   2. Scoreboard
   ========================================================================== */

.tpp-game-scores {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
	gap: var(--tpp-space-2);
	padding: var(--tpp-space-3);
	background: var(--tpp-bg-sunken);
	border-radius: var(--tpp-radius);
}

.tpp-game-score {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.15rem;
	text-align: center;
}

.tpp-game-score b {
	font-size: clamp(1.1rem, 2.2vw, 1.5rem);
	font-weight: 700;
	line-height: 1.1;
	font-variant-numeric: tabular-nums;
}

.tpp-game-score span {
	font-size: 0.7rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-game-lives {
	display: flex;
	gap: 4px;
	min-height: 1.5rem;
	align-items: center;
}

/* A life is a shape as well as a colour, so it does not rely on colour alone. */
.tpp-bubble-life {
	width: 13px;
	height: 13px;
	border-radius: 50%;
	background: var(--tpp-primary);
	box-shadow: inset 0 0 0 2px color-mix(in srgb, #fff 55%, transparent);
}

/* ==========================================================================
   3. The playing field
   ========================================================================== */

.tpp-game-field {
	position: relative;
	min-height: 320px;
	height: clamp(320px, 52vh, 520px);
	overflow: hidden;
	background:
		radial-gradient(circle at 20% 110%, color-mix(in srgb, var(--tpp-primary) 14%, transparent), transparent 60%),
		radial-gradient(circle at 80% 110%, color-mix(in srgb, var(--tpp-accent) 12%, transparent), transparent 55%),
		var(--tpp-bg);
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-game-field.is-missed {
	animation: tpp-game-shake 170ms ease;
	border-color: var(--tpp-incorrect);
}

@keyframes tpp-game-shake {
	0%, 100% { transform: translateX(0); }
	25% { transform: translateX(-5px); }
	75% { transform: translateX(5px); }
}

/*
 * The real input sits over the board, invisible but focusable, so a phone
 * raises its keyboard and so every keystroke lands somewhere predictable.
 */
.tpp-game-input {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	padding: 0;
	/*
	 * Forced, because the theme's own form styling is more specific than this
	 * rule and paints the field with a surface colour. On a board this field
	 * covers everything, so a background here is a sheet of white over the
	 * whole game.
	 */
	background: transparent !important;
	border: 0 !important;
	box-shadow: none !important;
	color: transparent;
	caret-color: transparent;
	font-size: 16px; /* Stops iOS zooming when the field takes focus. */
	resize: none;
}

.tpp-game-input:focus { outline: none; }

.tpp-game-field:focus-within { border-color: var(--tpp-primary); }

.tpp-game-help {
	margin: 0;
	font-size: 0.82rem;
	color: var(--tpp-text-muted);
	text-align: center;
}

/* ==========================================================================
   4. Bubbles
   ========================================================================== */

/*
 * A real bubble: a circle, not a pill. The diameter is set per bubble from the
 * length of its word, and the word wraps inside it, so a three letter word and
 * a twelve letter word are both round.
 */
.tpp-bubble {
	position: absolute;
	bottom: 0;
	width: var(--bubble-size, 80px);
	height: var(--bubble-size, 80px);
	/* Multiplying is safer than dividing by a negative in older engines. */
	margin-left: calc(var(--bubble-size, 80px) * -0.5);
	/*
	 * Flex, not grid. Under grid each span became its own row, so the letters
	 * already typed sat on a line above the rest of the word.
	 */
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	/*
	 * Worked out from the bubble's own diameter, never as a percentage.
	 * Percentage padding resolves against the containing block's width, so on
	 * a wide board "14%" became well over two hundred pixels a side and blew
	 * the circle up to several times its intended size.
	 */
	padding: calc(var(--bubble-size, 80px) * 0.15);
	box-sizing: border-box;
	border-radius: 50%;
	background:
		radial-gradient(circle at 32% 28%, color-mix(in srgb, #fff 92%, transparent) 0 14%, transparent 42%),
		radial-gradient(circle at 70% 76%, color-mix(in srgb, var(--tpp-primary) 20%, transparent), transparent 55%),
		color-mix(in srgb, var(--tpp-surface) 78%, transparent);
	border: 2px solid color-mix(in srgb, var(--tpp-primary) 42%, transparent);
	box-shadow:
		0 3px 12px color-mix(in srgb, var(--tpp-text) 14%, transparent),
		inset 0 -6px 12px color-mix(in srgb, var(--tpp-primary) 12%, transparent),
		inset 0 6px 10px color-mix(in srgb, #fff 60%, transparent);
	font-family: var(--tpp-font-mono);
	/* Worked out per bubble, so a long word still fits inside its circle. */
	font-size: var(--bubble-font, 0.9rem);
	font-weight: 600;
	line-height: 1.15;
	text-align: center;
	overflow-wrap: anywhere;
	will-change: transform;
}

/* The bubble being typed is outlined and tinted, not just recoloured. */
.tpp-bubble.is-locked {
	border-color: var(--tpp-primary);
	background: color-mix(in srgb, var(--tpp-primary) 12%, var(--tpp-surface));
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--tpp-primary) 22%, transparent);
}

.tpp-bubble-typed {
	color: var(--tpp-primary);
	text-decoration: underline;
	text-underline-offset: 3px;
}

.tpp-bubble-rest { color: var(--tpp-text); }

.tpp-bubble.is-popped {
	animation: tpp-bubble-pop 240ms ease-out forwards;
}

@keyframes tpp-bubble-pop {
	to { transform: scale(1.5); opacity: 0; }
}

.tpp-bubble.is-escaped {
	border-color: var(--tpp-incorrect);
	animation: tpp-bubble-gone 240ms ease-out forwards;
}

@keyframes tpp-bubble-gone {
	to { opacity: 0; }
}

/* ==========================================================================
   5. Start, pause and result panel
   ========================================================================== */

.tpp-game-overlay {
	position: absolute;
	inset: 0;
	z-index: 3;
	display: grid;
	place-items: center;
	padding: var(--tpp-space-4);
	background: color-mix(in srgb, var(--tpp-bg) 86%, transparent);
	backdrop-filter: blur(2px);
	border-radius: var(--tpp-radius);
	text-align: center;
	overflow-y: auto;
}

.tpp-bubble-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.2rem, 3vw, 1.7rem);
	font-weight: 700;
}

.tpp-bubble-best {
	margin: 0 0 var(--tpp-space-3);
	color: var(--tpp-correct);
	font-weight: 700;
}

.tpp-bubble-stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(70px, 1fr));
	gap: var(--tpp-space-3);
	margin: 0 auto var(--tpp-space-4);
}

.tpp-bubble-stat {
	display: flex;
	flex-direction: column;
	gap: 0.1rem;
}

.tpp-bubble-stat b {
	font-size: 1.25rem;
	font-variant-numeric: tabular-nums;
}

.tpp-bubble-stat span {
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-bubble-actions { margin: 0; }

.tpp-bubble-warning {
	margin: var(--tpp-space-2) auto 0;
	max-width: 34ch;
	font-size: 0.85rem;
	color: var(--tpp-incorrect);
}

/* ==========================================================================
   6. Smaller screens and reduced motion
   ========================================================================== */

@media (max-width: 640px) {
	.tpp-game { padding: var(--tpp-space-3); }
	.tpp-game-field { height: clamp(280px, 56vh, 420px); }
	.tpp-game-bar { gap: var(--tpp-space-2); }
	.tpp-bubble { padding: 0.45rem 0.8rem; }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-game-field.is-missed { animation: none; }
	.tpp-bubble.is-popped,
	.tpp-bubble.is-escaped { animation-duration: 1ms; }
}

/* ==========================================================================
   7. Keyboard jump
   ========================================================================== */

.tpp-game-label {
	align-self: center;
	margin-right: var(--tpp-space-1);
	font-size: 0.72rem;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	color: var(--tpp-text-muted);
}

.tpp-jump-field {
	position: relative;
	height: clamp(360px, 60vh, 560px);
	overflow: hidden;
	background: linear-gradient(to bottom,
		color-mix(in srgb, var(--tpp-primary) 10%, var(--tpp-bg)),
		var(--tpp-bg) 55%);
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-jump-field:focus-within { border-color: var(--tpp-primary); }

.tpp-jump-field.is-missed {
	animation: tpp-game-shake 170ms ease;
	border-color: var(--tpp-incorrect);
}

.tpp-jump-track {
	position: absolute;
	inset: 0;
	will-change: transform;
}

/* A platform carries one word and sits on alternating sides of the climb. */
.tpp-jump-platform {
	position: absolute;
	width: 38%;
	min-height: 34px;
	display: grid;
	place-items: center;
	padding: 0.4rem 0.6rem;
	box-sizing: border-box;
	background: var(--tpp-surface);
	border: 2px solid var(--tpp-border-strong);
	border-radius: var(--tpp-radius-sm);
	box-shadow: 0 3px 0 0 color-mix(in srgb, var(--tpp-text) 12%, transparent);
	font-family: var(--tpp-font-mono);
	font-size: clamp(0.78rem, 1.8vw, 1rem);
	font-weight: 600;
	text-align: center;
	overflow-wrap: anywhere;
}

/* The one you are typing: outlined and tinted, not recoloured alone. */
.tpp-jump-platform.is-next {
	border-color: var(--tpp-primary);
	background: color-mix(in srgb, var(--tpp-primary) 12%, var(--tpp-surface));
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--tpp-primary) 20%, transparent);
}

.tpp-jump-platform.is-cleared { opacity: 0.45; }

.tpp-jump-typed {
	color: var(--tpp-primary);
	text-decoration: underline;
	text-underline-offset: 3px;
}

.tpp-jump-rest { color: var(--tpp-text); }

/* The pace line: water climbing at exactly the chosen words per minute. */
.tpp-jump-water {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 0;
	background: linear-gradient(to top,
		color-mix(in srgb, var(--tpp-primary) 55%, transparent),
		color-mix(in srgb, var(--tpp-primary) 28%, transparent));
	border-top: 3px solid color-mix(in srgb, var(--tpp-primary) 80%, transparent);
}

/*
 * The climber. Drawn as one small inline SVG rather than an image, so it takes
 * the site's own colours, stays sharp at any size and costs no request.
 */
.tpp-jump-player {
	position: absolute;
	bottom: 34px;
	width: 34px;
	height: 42px;
	transition: transform 110ms ease-out;
	will-change: transform;
	filter: drop-shadow(0 3px 4px color-mix(in srgb, var(--tpp-text) 28%, transparent));
}

.tpp-jump-figure { display: block; overflow: visible; }

.tpp-figure-body { fill: var(--tpp-primary); }
.tpp-figure-head { fill: color-mix(in srgb, var(--tpp-primary) 88%, var(--tpp-text)); }
.tpp-figure-eye { fill: #fff; }

.tpp-figure-arm,
.tpp-figure-leg {
	fill: color-mix(in srgb, var(--tpp-primary) 82%, var(--tpp-text));
	transform-box: fill-box;
	transition: transform 120ms ease-out;
}

/* Arms hang from the shoulder, legs swing from the hip. */
.tpp-figure-arm { transform-origin: 50% 8%; }
.tpp-figure-leg { transform-origin: 50% 6%; }

/* Mid-jump: arms thrown up, legs tucked, body stretched. */
.tpp-jump-player.is-jumping .tpp-figure-arm.is-left { transform: rotate(32deg); }
.tpp-jump-player.is-jumping .tpp-figure-arm.is-right { transform: rotate(-32deg); }
.tpp-jump-player.is-jumping .tpp-figure-leg.is-left { transform: rotate(-16deg) translateY(-2px); }
.tpp-jump-player.is-jumping .tpp-figure-leg.is-right { transform: rotate(16deg) translateY(-2px); }
.tpp-jump-player.is-jumping .tpp-jump-figure { transform: scaleY(1.06) scaleX(0.96); }

/*
 * No transition on the figure's own transform. It flips to face the next
 * platform, and animating that flip interpolates through scaleX(0), which
 * flattens the climber to a sliver on every single jump. The arms and legs
 * carry the movement instead.
 */

/* Face the platform being typed, which alternates side as you climb. */
.tpp-jump-player.is-facing-left .tpp-jump-figure { transform: scaleX(-1); }
.tpp-jump-player.is-facing-left.is-jumping .tpp-jump-figure { transform: scaleX(-0.96) scaleY(1.06); }

.tpp-game-score.is-danger b { color: var(--tpp-incorrect); }

.tpp-jump-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.2rem, 3vw, 1.7rem);
	font-weight: 700;
}

.tpp-jump-verdict {
	margin: 0 auto var(--tpp-space-3);
	max-width: 38ch;
	color: var(--tpp-text-muted);
}

.tpp-jump-verdict.is-good { color: var(--tpp-correct); font-weight: 600; }

.tpp-jump-best {
	margin: 0 0 var(--tpp-space-3);
	color: var(--tpp-correct);
	font-weight: 700;
}

.tpp-jump-stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(70px, 1fr));
	gap: var(--tpp-space-3);
	margin: 0 auto var(--tpp-space-4);
}

.tpp-jump-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.tpp-jump-stat b {
	font-size: 1.25rem;
	font-variant-numeric: tabular-nums;
}

.tpp-jump-stat span {
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-jump-actions { margin: 0; }

@media (max-width: 640px) {
	.tpp-jump-field { height: clamp(320px, 56vh, 440px); }
	.tpp-jump-platform { width: 44%; }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-jump-player,
	.tpp-jump-figure,
	.tpp-figure-arm,
	.tpp-figure-leg { transition: none; }
	.tpp-jump-field.is-missed { animation: none; }
}

/* ==========================================================================
   8. Stickman duel
   ========================================================================== */

.tpp-stick-arena {
	position: relative;
	min-height: 320px;
	padding: var(--tpp-space-4);
	overflow: hidden;
	background: linear-gradient(to bottom,
		color-mix(in srgb, var(--tpp-primary) 7%, var(--tpp-bg)),
		var(--tpp-bg) 60%);
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-stick-arena:focus-within { border-color: var(--tpp-primary); }

/* Health */

.tpp-stick-bars {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--tpp-space-4);
}

.tpp-stick-bar-label {
	display: block;
	margin-bottom: 4px;
	font-size: 0.72rem;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	color: var(--tpp-text-muted);
}

.tpp-stick-bar.is-shadow { text-align: right; }

.tpp-stick-bar-track {
	display: block;
	height: 12px;
	background: var(--tpp-bg-sunken);
	border: 1px solid var(--tpp-border);
	border-radius: 999px;
	overflow: hidden;
}

.tpp-stick-bar-fill {
	display: block;
	height: 100%;
	background: var(--tpp-correct);
	transition: width 140ms ease-out;
}

.tpp-stick-bar.is-shadow .tpp-stick-bar-fill {
	background: var(--tpp-text-muted);
	margin-left: auto;
}

.tpp-stick-bar.is-low .tpp-stick-bar-fill { background: var(--tpp-incorrect); }

/* The stage */

.tpp-stick-stage {
	position: relative;
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	min-height: 130px;
	margin: var(--tpp-space-4) 0 0;
	padding: 0 clamp(0px, 6vw, 60px);
}

.tpp-stick-ground {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 2px;
	background: var(--tpp-border-strong);
}

.tpp-stick {
	position: relative;
	transition: transform 140ms ease-out;
	transform-origin: 50% 100%;
}

.tpp-stick-figure { display: block; overflow: visible; }

/* Stick figures: lines, as a stick figure should be. */
.tpp-stick-figure circle,
.tpp-stick-figure line {
	fill: none;
	stroke-width: 3.4;
	stroke-linecap: round;
	transform-box: fill-box;
	transition: transform 140ms ease-out;
}

.tpp-stick-hero .tpp-stick-figure circle,
.tpp-stick-hero .tpp-stick-figure line { stroke: var(--tpp-primary); }

/* The shadow is you, drawn in grey. */
.tpp-stick-shadow .tpp-stick-figure { transform: scaleX(-1); }

.tpp-stick-shadow .tpp-stick-figure circle,
.tpp-stick-shadow .tpp-stick-figure line {
	stroke: color-mix(in srgb, var(--tpp-text) 55%, transparent);
}

/* Poses */

.tpp-stick.is-attacking { transform: translateX(10px); }
.tpp-stick-shadow.is-attacking { transform: translateX(-10px); }

.tpp-stick.is-attacking .tpp-stick-arm.is-front {
	transform-origin: 20% 10%;
	transform: rotate(-38deg);
}

.tpp-stick.is-hurt { transform: translateX(-8px) rotate(-6deg); }
.tpp-stick-shadow.is-hurt { transform: translateX(8px) rotate(6deg); }

.tpp-stick.is-ko {
	transform: translateY(14px) rotate(-82deg);
	opacity: 0.55;
}

.tpp-stick-shadow.is-ko { transform: translateY(14px) rotate(82deg); }

/* The word to type */

.tpp-stick-prompt {
	margin: var(--tpp-space-4) 0 0;
	text-align: center;
	font-family: var(--tpp-font-mono);
	font-size: clamp(1.4rem, 5vw, 2.3rem);
	font-weight: 700;
	letter-spacing: 0.02em;
	min-height: 1.4em;
}

.tpp-stick-typed {
	color: var(--tpp-primary);
	text-decoration: underline;
	text-underline-offset: 6px;
}

.tpp-stick-rest { color: var(--tpp-text); }

/* Result panel */

.tpp-stick-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.2rem, 3vw, 1.7rem);
	font-weight: 700;
}

.tpp-stick-sub,
.tpp-stick-verdict {
	margin: 0 auto var(--tpp-space-3);
	max-width: 40ch;
	color: var(--tpp-text-muted);
}

.tpp-stick-verdict.is-good { color: var(--tpp-correct); font-weight: 600; }

.tpp-stick-stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(70px, 1fr));
	gap: var(--tpp-space-3);
	margin: 0 auto var(--tpp-space-4);
}

.tpp-stick-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.tpp-stick-stat b {
	font-size: 1.25rem;
	font-variant-numeric: tabular-nums;
}

.tpp-stick-stat span {
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-stick-actions { margin: 0; }

@media (max-width: 640px) {
	.tpp-stick-stage { padding: 0; min-height: 110px; }
	.tpp-stick-figure { width: 62px; height: 78px; }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-stick,
	.tpp-stick-figure circle,
	.tpp-stick-figure line,
	.tpp-stick-bar-fill { transition: none; }
}

/* ==========================================================================
   9. Space defence
   ========================================================================== */

.tpp-space-field {
	position: relative;
	min-height: 340px;
	height: clamp(340px, 56vh, 540px);
	overflow: hidden;
	background:
		radial-gradient(circle at 15% 15%, color-mix(in srgb, var(--tpp-primary) 16%, transparent), transparent 55%),
		radial-gradient(circle at 85% 20%, color-mix(in srgb, var(--tpp-accent) 14%, transparent), transparent 50%),
		var(--tpp-bg-sunken);
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-space-field.is-missed {
	animation: tpp-game-shake 170ms ease;
	border-color: var(--tpp-incorrect);
}

.tpp-space-field:focus-within { border-color: var(--tpp-primary); }

/* A still starfield, drawn with shadows rather than a hundred elements. */
.tpp-space-stars {
	position: absolute;
	inset: 0;
	pointer-events: none;
	opacity: 0.5;
	background-image:
		radial-gradient(1.5px 1.5px at 12% 18%, var(--tpp-text-muted), transparent),
		radial-gradient(1.5px 1.5px at 34% 62%, var(--tpp-text-muted), transparent),
		radial-gradient(1.5px 1.5px at 58% 28%, var(--tpp-text-muted), transparent),
		radial-gradient(1.5px 1.5px at 72% 74%, var(--tpp-text-muted), transparent),
		radial-gradient(1.5px 1.5px at 88% 42%, var(--tpp-text-muted), transparent),
		radial-gradient(1.5px 1.5px at 22% 86%, var(--tpp-text-muted), transparent),
		radial-gradient(1.5px 1.5px at 46% 8%, var(--tpp-text-muted), transparent);
}

/*
 * The two flanks are named, so the hand split is visible before you read why.
 * They sit at the foot of the board, beside the ship: the top is where the
 * attackers come in, and a label there is buried within seconds.
 */
.tpp-space-lane {
	position: absolute;
	bottom: var(--tpp-space-2);
	z-index: 1;
	padding: 2px 8px;
	border-radius: 999px;
	background: color-mix(in srgb, var(--tpp-bg) 70%, transparent);
	font-size: 0.66rem;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--tpp-text-muted);
	pointer-events: none;
}

.tpp-space-lane.is-left { left: var(--tpp-space-2); }
.tpp-space-lane.is-right { right: var(--tpp-space-2); }

.tpp-space-ship {
	position: absolute;
	bottom: 8px;
	left: 50%;
	z-index: 2;
	transform: translateX(-50%);
	pointer-events: none;
}

.tpp-space-hull {
	fill: var(--tpp-primary);
	stroke: color-mix(in srgb, #000 20%, var(--tpp-primary));
	stroke-width: 1.5;
}

.tpp-space-flame { fill: var(--tpp-accent); }

.tpp-space-laser {
	position: absolute;
	z-index: 2;
	height: 3px;
	transform-origin: 0 50%;
	border-radius: 2px;
	background: linear-gradient(90deg, var(--tpp-accent), color-mix(in srgb, var(--tpp-accent) 20%, transparent));
	opacity: 0;
	pointer-events: none;
}

.tpp-space-laser.is-firing { opacity: 1; }

/*
 * An attacker. Its size comes from its own content, and the padding is a fixed
 * length: a percentage here would resolve against the board's width instead of
 * the element, which on a wide screen makes these enormous.
 */
.tpp-space-enemy {
	position: absolute;
	top: 0;
	z-index: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 2px;
	padding: 6px 10px;
	transform: translate3d(0, 0, 0);
	border-radius: var(--tpp-radius-sm);
	background: color-mix(in srgb, var(--tpp-bg) 88%, transparent);
	border: 2px solid var(--tpp-border);
	box-shadow: 0 2px 0 color-mix(in srgb, #000 12%, transparent);
	white-space: nowrap;
}

/* Which flank it is attacking down, shown by colour and by position. */
.tpp-space-enemy.is-left { border-color: var(--tpp-primary); }
.tpp-space-enemy.is-right { border-color: var(--tpp-accent); }

.tpp-space-enemy.is-locked {
	background: var(--tpp-bg);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--tpp-primary) 35%, transparent);
}

.tpp-space-craft {
	display: block;
	width: 0;
	height: 0;
	border-left: 9px solid transparent;
	border-right: 9px solid transparent;
	border-top: 13px solid var(--tpp-text-muted);
}

.tpp-space-enemy.is-left .tpp-space-craft { border-top-color: var(--tpp-primary); }
.tpp-space-enemy.is-right .tpp-space-craft { border-top-color: var(--tpp-accent); }

.tpp-space-word {
	font-family: var(--tpp-font-mono, monospace);
	font-size: 0.92rem;
	font-weight: 700;
	letter-spacing: 0.02em;
}

.tpp-space-typed { color: var(--tpp-correct); }
.tpp-space-rest { color: var(--tpp-text); }

.tpp-space-enemy.is-hit {
	transition: transform 220ms ease, opacity 220ms ease;
	transform: scale(1.4);
	opacity: 0;
}

.tpp-space-enemy.is-through {
	transition: opacity 220ms ease;
	opacity: 0;
	border-color: var(--tpp-incorrect);
}

/* A shield is a shape as well as a colour, so it never relies on colour alone. */
.tpp-space-shields {
	display: flex;
	gap: 4px;
	min-height: 1.5rem;
	align-items: center;
	justify-content: center;
}

.tpp-space-shield {
	width: 13px;
	height: 13px;
	background: var(--tpp-primary);
	clip-path: polygon(50% 0, 100% 22%, 100% 62%, 50% 100%, 0 62%, 0 22%);
}

.tpp-game-score.is-left b { color: var(--tpp-primary); }
.tpp-game-score.is-right b { color: var(--tpp-accent); }

.tpp-space-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.2rem, 3vw, 1.7rem);
	font-weight: 700;
}

.tpp-space-sub {
	margin: 0 auto;
	max-width: 34rem;
	font-size: 0.88rem;
	color: var(--tpp-text-muted);
}

.tpp-space-stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(70px, 1fr));
	gap: var(--tpp-space-3);
	margin: 0 auto var(--tpp-space-3);
}

.tpp-space-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.tpp-space-stat b {
	font-size: 1.25rem;
	font-variant-numeric: tabular-nums;
}

.tpp-space-stat span {
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-space-verdict {
	margin: 0 auto var(--tpp-space-4);
	max-width: 32rem;
	font-weight: 600;
}

.tpp-space-actions { margin: 0; }

@media (max-width: 640px) {
	/* The floor leaves room for the result panel, and more sky to react in. */
	.tpp-space-field { height: clamp(380px, 62vh, 460px); }
	.tpp-space-word { font-size: 0.82rem; }
	.tpp-space-lane { font-size: 0.6rem; }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-space-enemy.is-hit,
	.tpp-space-enemy.is-through { transition: none; }
	.tpp-space-field.is-missed { animation: none; }
}

/* ==========================================================================
   10. Car race
   ========================================================================== */

/*
 * The track and the words together, which is what the result panel covers.
 * The overlay is pinned to this rather than to the track, because the track is
 * a short horizontal strip by design and a panel pinned to it would be clipped
 * into a scroll box.
 */
.tpp-race-stage {
	position: relative;
	display: flex;
	flex-direction: column;
	justify-content: center;
	gap: var(--tpp-space-3);
	min-height: 340px;
}

.tpp-race-track {
	position: relative;
	min-height: 150px;
	height: 170px;
	overflow: hidden;
	background:
		linear-gradient(180deg, color-mix(in srgb, var(--tpp-primary) 7%, transparent), transparent 40%),
		var(--tpp-bg-sunken);
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-race-track:focus-within { border-color: var(--tpp-primary); }

/* The corners, shaded so it is clear where grip is about to matter. */
.tpp-race-corners {
	position: absolute;
	inset: 0;
	pointer-events: none;
}

.tpp-race-corner {
	position: absolute;
	top: 0;
	bottom: 0;
	background: repeating-linear-gradient(
		135deg,
		color-mix(in srgb, var(--tpp-accent) 16%, transparent) 0 8px,
		transparent 8px 16px
	);
	border-left: 1px dashed color-mix(in srgb, var(--tpp-accent) 45%, transparent);
	border-right: 1px dashed color-mix(in srgb, var(--tpp-accent) 45%, transparent);
}

.tpp-race-track.is-cornering { border-color: var(--tpp-accent); }

.tpp-race-finish {
	position: absolute;
	top: 0;
	bottom: 0;
	right: 0;
	width: 10px;
	pointer-events: none;
	background:
		repeating-conic-gradient(var(--tpp-text) 0 25%, transparent 0 50%) 0 0 / 10px 10px;
	opacity: 0.55;
}

/* One lane per car, so neither can hide behind the other. */
.tpp-race-lane {
	position: absolute;
	left: 0;
	right: 0;
	z-index: 1;
	height: 50%;
	display: flex;
	align-items: center;
	pointer-events: none;
}

.tpp-race-lane.is-rival { top: 0; }
.tpp-race-lane.is-you { bottom: 0; }

/* The dividing line between the two lanes. */
.tpp-race-lane.is-you::before {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	top: 0;
	border-top: 2px dashed color-mix(in srgb, var(--tpp-border) 80%, transparent);
}

.tpp-race-car {
	position: relative;
	will-change: transform;
}

.tpp-race-car.is-you .tpp-race-body { fill: var(--tpp-primary); }
.tpp-race-car.is-rival .tpp-race-body { fill: var(--tpp-text-muted); }

.tpp-race-glass { fill: color-mix(in srgb, #fff 70%, transparent); }
.tpp-race-wheel { fill: var(--tpp-text); }

/*
 * Losing the back end in a corner, which is what poor rhythm looks like.
 *
 * These animate the car's artwork, never the wrapper. The wrapper carries the
 * transform that places the car on the track, and an animation setting
 * transform on the same element replaces that one outright, which would snap
 * the car back to the start line at exactly the moment grip drops.
 */
.tpp-race-car.is-skidding svg { animation: tpp-race-skid 420ms ease-in-out infinite; }

@keyframes tpp-race-skid {
	0%, 100% { transform: rotate(0deg); }
	35% { transform: rotate(-3.5deg); }
	70% { transform: rotate(3deg); }
}

.tpp-race-car.is-wobbling svg { animation: tpp-race-wobble 220ms ease; }

@keyframes tpp-race-wobble {
	0%, 100% { transform: translate(0, 0); }
	30% { transform: translate(-4px, 2px); }
	65% { transform: translate(2px, -2px); }
}

/* The grip gauge, which is the thing this game is actually about. */
.tpp-race-grip {
	position: relative;
	height: 10px;
	overflow: hidden;
	background: var(--tpp-bg-sunken);
	border: 1px solid var(--tpp-border);
	border-radius: 999px;
}

.tpp-race-grip-fill {
	display: block;
	height: 100%;
	background: var(--tpp-correct);
	transition: width 140ms linear, background-color 220ms ease;
}

.tpp-race-grip-fill.is-low { background: var(--tpp-incorrect); }

.tpp-game-score.is-grip b { color: var(--tpp-correct); }
.tpp-game-score.is-rival b { color: var(--tpp-text-muted); }

.tpp-race-words {
	text-align: center;
}

.tpp-race-word {
	margin: 0;
	font-family: var(--tpp-font-mono, monospace);
	font-size: clamp(1.4rem, 4vw, 2.1rem);
	font-weight: 700;
	letter-spacing: 0.04em;
	min-height: 1.4em;
	word-break: break-all;
}

.tpp-race-typed { color: var(--tpp-correct); }
.tpp-race-rest { color: var(--tpp-text); }

.tpp-race-queue {
	margin: 0.15rem 0 0;
	font-family: var(--tpp-font-mono, monospace);
	font-size: 0.86rem;
	letter-spacing: 0.03em;
	color: var(--tpp-text-muted);
	word-break: break-all;
}

.tpp-race-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.1rem, 2.8vw, 1.6rem);
	font-weight: 700;
}

.tpp-race-sub {
	margin: 0 auto;
	max-width: 34rem;
	font-size: 0.86rem;
	color: var(--tpp-text-muted);
}

.tpp-race-stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(66px, 1fr));
	gap: var(--tpp-space-2) var(--tpp-space-3);
	margin: 0 auto var(--tpp-space-3);
}

.tpp-race-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.tpp-race-stat b {
	font-size: 1.15rem;
	font-variant-numeric: tabular-nums;
}

.tpp-race-stat span {
	font-size: 0.68rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-race-verdict {
	margin: 0 auto var(--tpp-space-3);
	max-width: 34rem;
	font-size: 0.92rem;
	font-weight: 600;
}

.tpp-race-actions { margin: 0; }

@media (max-width: 640px) {
	/* The panel is taller here, because the stats and the verdict both wrap. */
	.tpp-race-stage { min-height: 400px; }
	.tpp-race-track { height: 140px; }
	.tpp-race-car svg { width: 48px; height: 23px; }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-race-car.is-skidding svg,
	.tpp-race-car.is-wobbling svg { animation: none; }
	.tpp-race-grip-fill { transition: none; }
}

/* ==========================================================================
   11. A to Z
   ========================================================================== */

.tpp-az-board {
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	/*
	 * Tall enough to hold the result panel, which is the point of this game:
	 * the overlay is pinned to the board, so a board sized to the letters
	 * alone would clip the heat map and make you scroll inside it.
	 */
	min-height: 480px;
	gap: var(--tpp-space-3);
	padding: var(--tpp-space-4) var(--tpp-space-3);
	background:
		radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--tpp-primary) 10%, transparent), transparent 60%),
		var(--tpp-bg-sunken);
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-az-board:focus-within { border-color: var(--tpp-primary); }

/* The letter being asked for, large enough to read without looking down. */
.tpp-az-target {
	position: relative;
	z-index: 1;
	margin: 0;
	font-family: var(--tpp-font-mono, monospace);
	font-size: clamp(3rem, 12vw, 5.5rem);
	font-weight: 700;
	line-height: 1;
	color: var(--tpp-primary);
	min-height: 1em;
}

.tpp-az-target.is-wrong {
	color: var(--tpp-incorrect);
	animation: tpp-game-shake 170ms ease;
}

/* All twenty-six, so you can see what is coming and what is behind you. */
.tpp-az-strip {
	position: relative;
	z-index: 1;
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 4px;
	max-width: 36rem;
}

.tpp-az-tile {
	display: grid;
	place-items: center;
	width: 1.7rem;
	height: 1.7rem;
	background: var(--tpp-bg);
	border: 1px solid var(--tpp-border);
	border-radius: var(--tpp-radius-sm);
	font-family: var(--tpp-font-mono, monospace);
	font-size: 0.8rem;
	font-weight: 700;
	color: var(--tpp-text-muted);
}

.tpp-az-tile.is-done {
	background: color-mix(in srgb, var(--tpp-correct) 18%, var(--tpp-bg));
	border-color: var(--tpp-correct);
	color: var(--tpp-correct);
}

.tpp-az-tile.is-next {
	background: var(--tpp-primary);
	border-color: var(--tpp-primary);
	color: #fff;
}

/* Where your best run had got to by this point in the clock. */
.tpp-az-ghost-rail {
	position: relative;
	z-index: 1;
	width: min(100%, 36rem);
	height: 6px;
	overflow: hidden;
	background: var(--tpp-bg);
	border: 1px solid var(--tpp-border);
	border-radius: 999px;
}

.tpp-az-ghost {
	display: block;
	height: 100%;
	width: 0;
	background: var(--tpp-text-muted);
}

.tpp-az-ghost-note {
	position: relative;
	z-index: 1;
	margin: 0;
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-az-ghost-note b {
	font-variant-numeric: tabular-nums;
	color: var(--tpp-text);
}

.tpp-game-score.is-best b { color: var(--tpp-correct); }

/* ---- The result panel ---- */

.tpp-az-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.1rem, 2.8vw, 1.6rem);
	font-weight: 700;
}

.tpp-az-sub {
	margin: 0 auto;
	max-width: 32rem;
	font-size: 0.86rem;
	color: var(--tpp-text-muted);
}

.tpp-az-stats {
	display: grid;
	grid-template-columns: repeat(4, minmax(60px, 1fr));
	gap: var(--tpp-space-2);
	margin: 0 auto var(--tpp-space-3);
}

.tpp-az-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.tpp-az-stat b {
	font-size: 1.15rem;
	font-variant-numeric: tabular-nums;
}

.tpp-az-stat span {
	font-size: 0.66rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

/*
 * The heat map: every letter shaded by what it cost. The shade comes from a
 * --heat percentage set per tile, worked out against your own fastest and
 * slowest letter in that run, so it reads the same at any skill level.
 */
.tpp-az-heatmap {
	display: grid;
	grid-template-columns: repeat(13, 1fr);
	gap: 3px;
	margin: 0 auto var(--tpp-space-3);
	max-width: 30rem;
}

.tpp-az-heat {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 1px;
	padding: 3px 1px;
	border-radius: var(--tpp-radius-sm);
	background: color-mix(in srgb, var(--tpp-incorrect) var(--heat, 0%), var(--tpp-correct));
	color: #fff;
}

.tpp-az-heat b {
	font-family: var(--tpp-font-mono, monospace);
	font-size: 0.72rem;
	line-height: 1;
}

.tpp-az-heat i {
	font-style: normal;
	font-size: 0.56rem;
	line-height: 1;
	font-variant-numeric: tabular-nums;
	opacity: 0.85;
}

/* The first letter is the point the rest are timed from, so it has no figure. */
.tpp-az-heat.is-untimed {
	background: var(--tpp-bg-sunken);
	color: var(--tpp-text-muted);
	border: 1px dashed var(--tpp-border);
}

.tpp-az-verdict {
	margin: 0 auto var(--tpp-space-3);
	max-width: 34rem;
	font-size: 0.9rem;
	font-weight: 600;
}

.tpp-az-actions { margin: 0; }

@media (max-width: 640px) {
	/* The panel is taller here, because the stats and the heat map both wrap. */
	.tpp-az-board { min-height: 580px; }
	.tpp-az-tile { width: 1.45rem; height: 1.45rem; font-size: 0.7rem; }
	.tpp-az-stats { grid-template-columns: repeat(2, minmax(60px, 1fr)); }
	.tpp-az-heatmap { grid-template-columns: repeat(7, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-az-target.is-wrong { animation: none; }
}

/* ==========================================================================
   12. Deep dive
   ========================================================================== */

.tpp-fish-dive {
	display: flex;
	gap: var(--tpp-space-2);
	align-items: stretch;
}

/* The air gauge, which is what the whole dive runs on. */
.tpp-fish-gauge {
	position: relative;
	display: flex;
	flex-direction: column-reverse;
	align-items: center;
	flex: 0 0 auto;
	width: 34px;
	overflow: hidden;
	background: var(--tpp-bg-sunken);
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
}

.tpp-fish-tank {
	display: block;
	width: 100%;
	background: linear-gradient(180deg, color-mix(in srgb, var(--tpp-correct) 70%, #fff), var(--tpp-correct));
	transition: height 120ms linear, background-color 240ms ease;
}

.tpp-fish-tank.is-low {
	background: linear-gradient(180deg, color-mix(in srgb, var(--tpp-incorrect) 70%, #fff), var(--tpp-incorrect));
}

.tpp-fish-gauge-label {
	position: absolute;
	top: 6px;
	left: 0;
	right: 0;
	text-align: center;
	font-size: 0.6rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
	pointer-events: none;
}

/*
 * The sea. It darkens as the dive goes on through the --depth custom property,
 * which the script sets from 0 at the surface to 1 in deep water.
 */
.tpp-fish-sea {
	position: relative;
	flex: 1 1 auto;
	/*
	 * Tall enough to hold the result panel, which carries the stamina chart
	 * and is the point of the dive: the overlay is pinned to the sea, so a sea
	 * sized to the fish alone would clip the chart into a scroll box. The
	 * extra depth is not wasted either, since the fish swim in six lanes.
	 */
	min-height: 540px;
	height: clamp(540px, 56vh, 600px);
	overflow: hidden;
	background:
		linear-gradient(180deg,
			color-mix(in srgb, var(--tpp-bg) calc(100% - var(--depth, 0) * 55%), var(--tpp-primary)),
			color-mix(in srgb, var(--tpp-bg-sunken) calc(100% - var(--depth, 0) * 70%), var(--tpp-primary)));
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-fish-sea:focus-within { border-color: var(--tpp-primary); }

.tpp-fish-sea.is-missed {
	animation: tpp-game-shake 170ms ease;
	border-color: var(--tpp-incorrect);
}

/* A shaft of light from the surface, which fades as you go down. */
.tpp-fish-light {
	position: absolute;
	top: 0;
	left: 12%;
	width: 42%;
	height: 100%;
	pointer-events: none;
	opacity: calc(0.5 - var(--depth, 0) * 0.45);
	background: linear-gradient(170deg, color-mix(in srgb, #fff 55%, transparent), transparent 62%);
}

.tpp-fish-floor {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 26px;
	pointer-events: none;
	opacity: var(--depth, 0);
	background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--tpp-text) 28%, transparent));
}

/*
 * A fish. Its position is a transform in pixels worked out from the sea's own
 * width and the fish's measured width, never a percentage, so it enters and
 * leaves cleanly off the edge at any screen size.
 */
.tpp-fish {
	position: absolute;
	left: 0;
	z-index: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 1px;
	padding: 4px 8px;
	border-radius: var(--tpp-radius-sm);
	background: color-mix(in srgb, var(--tpp-bg) 84%, transparent);
	border: 2px solid var(--tpp-border);
	white-space: nowrap;
	will-change: transform;
}

.tpp-fish.is-locked {
	background: var(--tpp-bg);
	border-color: var(--tpp-primary);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--tpp-primary) 32%, transparent);
}

.tpp-fish-body { display: block; line-height: 0; }

/* Swimming the other way means the artwork turns, not the whole box. */
.tpp-fish.is-right .tpp-fish-body { transform: scaleX(-1); }

.tpp-fish-shape { fill: var(--tpp-primary); }

/* The fins sit behind the body, so a darker shade reads as depth. */
.tpp-fish-tail,
.tpp-fish-fin { fill: color-mix(in srgb, var(--tpp-primary) 62%, var(--tpp-accent)); }

.tpp-fish-gill {
	fill: none;
	stroke: color-mix(in srgb, #000 22%, transparent);
	stroke-width: 1.2;
	stroke-linecap: round;
}

.tpp-fish-eye { fill: var(--tpp-bg); }

.tpp-fish.is-locked .tpp-fish-shape { fill: var(--tpp-accent); }
.tpp-fish.is-locked .tpp-fish-tail,
.tpp-fish.is-locked .tpp-fish-fin { fill: color-mix(in srgb, var(--tpp-accent) 62%, var(--tpp-primary)); }

.tpp-fish-word {
	font-family: var(--tpp-font-mono, monospace);
	font-size: 0.86rem;
	font-weight: 700;
}

.tpp-fish-typed { color: var(--tpp-correct); }
.tpp-fish-rest { color: var(--tpp-text); }

.tpp-fish.is-caught {
	transition: transform 240ms ease, opacity 240ms ease;
	opacity: 0;
}

.tpp-fish.is-escaped {
	transition: opacity 240ms ease;
	opacity: 0;
}

.tpp-game-score.is-depth b { color: var(--tpp-primary); }
.tpp-game-score.is-pace b { color: var(--tpp-accent); }

/* ---- The result panel ---- */

.tpp-fish-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.1rem, 2.8vw, 1.6rem);
	font-weight: 700;
}

.tpp-fish-sub {
	margin: 0 auto;
	max-width: 34rem;
	font-size: 0.86rem;
	color: var(--tpp-text-muted);
}

.tpp-fish-stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(66px, 1fr));
	gap: var(--tpp-space-2) var(--tpp-space-3);
	margin: 0 auto var(--tpp-space-3);
}

.tpp-fish-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.tpp-fish-stat b {
	font-size: 1.15rem;
	font-variant-numeric: tabular-nums;
}

.tpp-fish-stat span {
	font-size: 0.66rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

/*
 * The stamina chart: one bar for each fifteen seconds of the dive, drawn from
 * a --bar height so no image or canvas is needed. The shape of this is the
 * point of the game.
 */
.tpp-fish-chart {
	display: flex;
	align-items: flex-end;
	justify-content: center;
	gap: 3px;
	height: 92px;
	margin: 0 auto var(--tpp-space-3);
	padding-top: 14px;
	max-width: 32rem;
	overflow-x: auto;
}

.tpp-fish-bar {
	position: relative;
	flex: 1 1 14px;
	min-width: 12px;
	max-width: 34px;
	height: var(--bar, 10%);
	border-radius: 3px 3px 0 0;
	background: var(--tpp-primary);
}

.tpp-fish-bar.is-peak { background: var(--tpp-correct); }
.tpp-fish-bar.is-faded { background: var(--tpp-incorrect); }

.tpp-fish-bar b {
	position: absolute;
	top: -13px;
	left: 0;
	right: 0;
	text-align: center;
	font-size: 0.56rem;
	font-weight: 700;
	font-variant-numeric: tabular-nums;
	color: var(--tpp-text);
}

.tpp-fish-bar i {
	position: absolute;
	bottom: -13px;
	left: 0;
	right: 0;
	text-align: center;
	font-style: normal;
	font-size: 0.52rem;
	color: var(--tpp-text-muted);
}

.tpp-fish-verdict {
	margin: 0 auto var(--tpp-space-3);
	max-width: 34rem;
	font-size: 0.9rem;
	font-weight: 600;
}

.tpp-fish-actions { margin: 0; }

@media (max-width: 640px) {
	.tpp-fish-sea { min-height: 620px; height: clamp(620px, 78vh, 680px); }
	.tpp-fish-word { font-size: 0.78rem; }
	/* The stats stay in three columns here, because two would add a row the
	   panel has no room for. They are short figures and still read fine. */
	.tpp-fish-stat b { font-size: 1rem; }
	.tpp-fish-stat span { font-size: 0.6rem; }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-fish.is-caught,
	.tpp-fish.is-escaped { transition: none; }
	.tpp-fish-sea.is-missed { animation: none; }
	.tpp-fish-tank { transition: none; }
}

/* ==========================================================================
   13. Rocket launch
   ========================================================================== */

/*
 * The sky, which clears from daylight to space as the rocket climbs. The
 * script sets --climb from 0 on the pad to 1 at orbit.
 */
.tpp-rocket-sky {
	position: relative;
	min-height: 520px;
	height: clamp(520px, 58vh, 600px);
	overflow: hidden;
	background:
		linear-gradient(180deg,
			color-mix(in srgb, var(--tpp-bg) calc(100% - var(--climb, 0) * 88%), #0b1026),
			color-mix(in srgb, var(--tpp-bg-sunken) calc(100% - var(--climb, 0) * 60%), #1a2348));
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-rocket-sky:focus-within { border-color: var(--tpp-primary); }

.tpp-rocket-sky.is-knocked {
	animation: tpp-game-shake 190ms ease;
	border-color: var(--tpp-incorrect);
}

/* Stars, which only come out once you are high enough to see them. */
.tpp-rocket-stars {
	position: absolute;
	inset: 0;
	pointer-events: none;
	opacity: var(--climb, 0);
	background-image:
		radial-gradient(1.4px 1.4px at 14% 16%, #fff, transparent),
		radial-gradient(1.4px 1.4px at 31% 58%, #fff, transparent),
		radial-gradient(1.4px 1.4px at 54% 24%, #fff, transparent),
		radial-gradient(1.4px 1.4px at 69% 71%, #fff, transparent),
		radial-gradient(1.4px 1.4px at 85% 38%, #fff, transparent),
		radial-gradient(1.4px 1.4px at 24% 84%, #fff, transparent),
		radial-gradient(1.4px 1.4px at 44% 7%, #fff, transparent),
		radial-gradient(1.4px 1.4px at 92% 62%, #fff, transparent);
}

.tpp-rocket-ground {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 16px;
	pointer-events: none;
	opacity: calc(1 - var(--climb, 0));
	background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--tpp-text) 30%, transparent));
}

.tpp-rocket-ship {
	position: absolute;
	bottom: 10px;
	left: 50%;
	z-index: 1;
	margin-left: -20px;
	transform-origin: 50% 85%;
	pointer-events: none;
	will-change: transform;
}

/* ---- The rocket itself ---- */

.tpp-rocket-body {
	fill: var(--tpp-bg);
	stroke: var(--tpp-primary);
	stroke-width: 2;
	stroke-linejoin: round;
}

.tpp-rocket-fin {
	fill: var(--tpp-primary);
	stroke: var(--tpp-primary);
	stroke-width: 1.5;
	stroke-linejoin: round;
}

.tpp-rocket-nozzle { fill: color-mix(in srgb, var(--tpp-text) 65%, var(--tpp-bg)); }
.tpp-rocket-stripe { fill: var(--tpp-primary); }

.tpp-rocket-window {
	/* Glass, so it reads as a porthole rather than another painted panel. */
	fill: color-mix(in srgb, var(--tpp-primary) 22%, #eaf2ff);
	stroke: var(--tpp-primary);
	stroke-width: 1.6;
}

/*
 * The exhaust is the one part of the rocket that cannot take a brand colour.
 * On the site's palette the accent is green, and a green flame simply does not
 * read as fire, so the plume gets its own warm pair of tokens.
 */
.tpp-rocket-sky {
	--flame: #f59e0b;
	--flame-hot: #fde68a;
}

.tpp-rocket-plume {
	fill: var(--flame);
	transform-origin: 50% 0;
	animation: tpp-rocket-burn 260ms ease-in-out infinite alternate;
}

@keyframes tpp-rocket-burn {
	from { transform: scaleY(0.72); opacity: 0.75; }
	to { transform: scaleY(1.12); opacity: 1; }
}

/* Off trajectory the burn goes ragged, which you can see before you read it. */
.tpp-rocket-plume-core { fill: var(--flame-hot); }

.tpp-rocket-ship.is-struggling .tpp-rocket-plume {
	fill: var(--tpp-incorrect);
	animation-duration: 110ms;
}

.tpp-rocket-ship.is-struggling .tpp-rocket-plume-core { fill: #fecaca; }

/* ---- The trajectory gauge ---- */

.tpp-rocket-tilt {
	position: relative;
	height: 16px;
	overflow: hidden;
	background: var(--tpp-bg-sunken);
	border: 1px solid var(--tpp-border);
	border-radius: 999px;
}

.tpp-rocket-tilt-fill {
	display: block;
	height: 100%;
	width: 0;
	background: var(--tpp-accent);
	transition: width 120ms linear, background-color 220ms ease;
}

.tpp-rocket-tilt-fill.is-high { background: var(--tpp-incorrect); }

.tpp-rocket-tilt-label {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	font-size: 0.6rem;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--tpp-text-muted);
	pointer-events: none;
}

/* ---- Words ---- */

.tpp-rocket-words { text-align: center; }

.tpp-rocket-word {
	margin: 0;
	font-family: var(--tpp-font-mono, monospace);
	font-size: clamp(1.4rem, 4vw, 2.1rem);
	font-weight: 700;
	letter-spacing: 0.04em;
	min-height: 1.4em;
	word-break: break-all;
}

.tpp-rocket-typed { color: var(--tpp-correct); }
.tpp-rocket-rest { color: var(--tpp-text); }

.tpp-rocket-queue {
	margin: 0.15rem 0 0;
	font-family: var(--tpp-font-mono, monospace);
	font-size: 0.86rem;
	color: var(--tpp-text-muted);
	word-break: break-all;
}

.tpp-game-score.is-altitude b { color: var(--tpp-primary); }
.tpp-game-score.is-slips b { color: var(--tpp-accent); }

/* ---- The result panel ---- */

.tpp-rocket-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.1rem, 2.8vw, 1.6rem);
	font-weight: 700;
}

.tpp-rocket-sub {
	margin: 0 auto;
	max-width: 34rem;
	font-size: 0.86rem;
	color: var(--tpp-text-muted);
}

.tpp-rocket-stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(66px, 1fr));
	gap: var(--tpp-space-2) var(--tpp-space-3);
	margin: 0 auto var(--tpp-space-3);
}

.tpp-rocket-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.tpp-rocket-stat b {
	font-size: 1.15rem;
	font-variant-numeric: tabular-nums;
}

.tpp-rocket-stat span {
	font-size: 0.66rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-rocket-verdict {
	margin: 0 auto var(--tpp-space-3);
	max-width: 34rem;
	font-size: 0.9rem;
	font-weight: 600;
}

.tpp-rocket-actions { margin: 0; }

@media (max-width: 640px) {
	.tpp-rocket-sky { min-height: 560px; height: clamp(560px, 72vh, 620px); }
	.tpp-rocket-stat b { font-size: 1rem; }
	.tpp-rocket-stat span { font-size: 0.6rem; }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-rocket-plume { animation: none; }
	.tpp-rocket-tilt-fill { transition: none; }
	.tpp-rocket-sky.is-knocked { animation: none; }
}

/* ==========================================================================
   14. Word ninja
   ========================================================================== */

.tpp-ninja-field {
	position: relative;
	min-height: 540px;
	height: clamp(540px, 58vh, 620px);
	overflow: hidden;
	background:
		radial-gradient(circle at 78% 18%, color-mix(in srgb, var(--tpp-accent) 12%, transparent), transparent 42%),
		linear-gradient(180deg, color-mix(in srgb, var(--tpp-primary) 16%, var(--tpp-bg-sunken)), var(--tpp-bg-sunken));
	border: 2px solid var(--tpp-border);
	border-radius: var(--tpp-radius);
	cursor: text;
}

.tpp-ninja-field:focus-within { border-color: var(--tpp-primary); }

.tpp-ninja-field.is-missed {
	animation: tpp-game-shake 170ms ease;
	border-color: var(--tpp-incorrect);
}

.tpp-ninja-moon {
	position: absolute;
	top: 34px;
	right: 11%;
	width: 54px;
	height: 54px;
	border-radius: 50%;
	pointer-events: none;
	background: color-mix(in srgb, #fff 72%, var(--tpp-accent));
	box-shadow: 0 0 26px color-mix(in srgb, var(--tpp-accent) 40%, transparent);
	opacity: 0.75;
}

/* The floor a throw must not fall back past. */
.tpp-ninja-floor {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 22px;
	pointer-events: none;
	background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--tpp-text) 34%, transparent));
	border-bottom: 2px solid color-mix(in srgb, var(--tpp-text) 30%, transparent);
}

/*
 * A word in the air. Its position is a transform in pixels worked out from
 * the board's own size and the throw's measured width, never a percentage, so
 * the arc rises the same share of the board at any screen size.
 */
.tpp-ninja-throw {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 1;
	display: grid;
	place-items: center;
	/* A circle, because one character in a pill reads as a stray letter. */
	width: 44px;
	height: 44px;
	padding: 0;
	border-radius: 50%;
	background: color-mix(in srgb, var(--tpp-bg) 92%, transparent);
	border: 2px solid var(--tpp-primary);
	box-shadow: 0 2px 0 color-mix(in srgb, #000 14%, transparent);
	white-space: nowrap;
	will-change: transform;
}

.tpp-ninja-word {
	font-family: var(--tpp-font-mono, monospace);
	font-size: 1.25rem;
	font-weight: 700;
	line-height: 1;
	color: var(--tpp-text);
}

.tpp-ninja-throw.is-sliced {
	transition: transform 260ms ease, opacity 260ms ease;
	opacity: 0;
}

.tpp-ninja-throw.is-dropped {
	transition: opacity 260ms ease;
	opacity: 0;
	border-color: var(--tpp-incorrect);
}

/* The blade, which chains one clean slice to the next. */
.tpp-ninja-trail {
	position: absolute;
	inset: 0;
	z-index: 2;
	pointer-events: none;
}

.tpp-ninja-slash {
	position: absolute;
	height: 3px;
	transform-origin: 0 50%;
	border-radius: 2px;
	background: linear-gradient(90deg,
		color-mix(in srgb, var(--tpp-accent) 10%, transparent),
		color-mix(in srgb, #fff 70%, var(--tpp-accent)),
		color-mix(in srgb, var(--tpp-accent) 10%, transparent));
	animation: tpp-ninja-fade 620ms ease forwards;
}

@keyframes tpp-ninja-fade {
	from { opacity: 1; }
	to { opacity: 0; }
}

/* A life is a shape as well as a colour, so it never relies on colour alone. */
.tpp-ninja-lives {
	display: flex;
	gap: 4px;
	min-height: 1.5rem;
	align-items: center;
	justify-content: center;
}

.tpp-ninja-life {
	width: 13px;
	height: 13px;
	background: var(--tpp-primary);
	clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

.tpp-game-score.is-chain b { color: var(--tpp-accent); }

/* ---- The result panel ---- */

.tpp-ninja-big {
	margin: 0 0 var(--tpp-space-2);
	font-size: clamp(1.1rem, 2.8vw, 1.6rem);
	font-weight: 700;
}

.tpp-ninja-sub {
	margin: 0 auto;
	max-width: 34rem;
	font-size: 0.86rem;
	color: var(--tpp-text-muted);
}

.tpp-ninja-stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(66px, 1fr));
	gap: var(--tpp-space-2) var(--tpp-space-3);
	margin: 0 auto var(--tpp-space-3);
}

.tpp-ninja-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.tpp-ninja-stat b {
	font-size: 1.1rem;
	font-variant-numeric: tabular-nums;
}

.tpp-ninja-stat span {
	font-size: 0.64rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

/*
 * The moves that cost you most. Each is colour coded by what kind of journey
 * it is, and labelled too, so the coding is never the only thing saying it.
 */
.tpp-ninja-moves {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: var(--tpp-space-2);
	margin: 0 auto var(--tpp-space-3);
	max-width: 34rem;
}

.tpp-ninja-move {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 1px;
	padding: 5px 9px;
	border-radius: var(--tpp-radius-sm);
	background: var(--tpp-bg);
	border: 1px solid var(--tpp-border);
	border-left-width: 4px;
}

.tpp-ninja-move.is-same { border-left-color: var(--tpp-incorrect); }
.tpp-ninja-move.is-roll { border-left-color: var(--tpp-accent); }
.tpp-ninja-move.is-swap { border-left-color: var(--tpp-primary); }

.tpp-ninja-move b {
	font-family: var(--tpp-font-mono, monospace);
	font-size: 0.86rem;
	letter-spacing: 0.02em;
}

.tpp-ninja-move i {
	font-style: normal;
	font-size: 0.74rem;
	font-variant-numeric: tabular-nums;
	color: var(--tpp-text);
}

.tpp-ninja-move em {
	font-style: normal;
	font-size: 0.58rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--tpp-text-muted);
}

.tpp-ninja-verdict {
	margin: 0 auto var(--tpp-space-3);
	max-width: 34rem;
	font-size: 0.88rem;
	font-weight: 600;
}

.tpp-ninja-actions { margin: 0; }

@media (max-width: 640px) {
	/*
	 * Taller here because the six named moves stack into several rows and the
	 * result panel is pinned to the board. The height is not wasted on a
	 * phone either: a taller board means a longer arc to read the word in.
	 */
	.tpp-ninja-field { min-height: 720px; height: clamp(720px, 88vh, 780px); }
	.tpp-ninja-throw { width: 40px; height: 40px; }
	.tpp-ninja-word { font-size: 1.1rem; }
	.tpp-ninja-stat b { font-size: 1rem; }
	.tpp-ninja-stat span { font-size: 0.58rem; }
	.tpp-ninja-moves { gap: 5px; }
	.tpp-ninja-move { padding: 4px 7px; }
	.tpp-ninja-move b { font-size: 0.78rem; }
	.tpp-ninja-move i { font-size: 0.68rem; }
	.tpp-ninja-move em { font-size: 0.54rem; }
}

@media (prefers-reduced-motion: reduce) {
	.tpp-ninja-throw.is-sliced,
	.tpp-ninja-throw.is-dropped { transition: none; }
	.tpp-ninja-slash { animation: none; opacity: 0; }
	.tpp-ninja-field.is-missed { animation: none; }
}
