ui/gamehud.razor.scss
// In-game HUD. Designed in 1080-reference space. The play square fills the full screen height and
// is centred horizontally, so the left/right letterbox is empty — the gear lives there (top-right).
@import "Theme.scss";
$teal-dark: #00262a; // letterbox / outside-arena colour (ClearColor)

// First ~0.1s punches the scale; the rest holds scale steady while fading away. Durations live on
// the .dismissing rules below — keep them in sync with GameStage.TUTORIAL_PROMPT_DISMISS_TIME.
@keyframes tutorial-instruction-dismiss {
	0% { opacity: 1; transform: scale( 1 ); }
	12% { opacity: 1; transform: scale( 1.1 ); }
	25% { opacity: 1; transform: scale( 1 ); }
	100% { opacity: 0; transform: scale( 1 ); }
}

@keyframes tutorial-instruction-backdrop-dismiss {
	0%, 25% { background-color: rgba( $black, 0.6 ); }
	100% { background-color: rgba( $black, 0 ); }
}

// "The help lives here" nudge: the character picker's .new-badge pulse (same colours, scale and
// 0.5s beat — see CharacterPicker.razor.scss) transplanted onto the square button. Both phases
// carry the square-button drop shadow so it never pops in/out mid-beat.
@keyframes character-help-button-pulse {
	0%, 100% {
		transform: scale( 0.94 );
		background-color: #b86f6f;
		border-color: #e7dfd2;
		box-shadow: 6px 6px 0 $square-btn-shadow, 0 0 2px rgba( #c98a8a, 0.22 );
	}
	50% {
		transform: scale( 1.08 );
		background-color: #e2a2a2;
		border-color: #fff3c4;
		box-shadow: 6px 6px 0 $square-btn-shadow, 0 0 9px rgba( #e2a2a2, 0.62 );
	}
}

@keyframes character-help-dismiss {
	0% { opacity: 1; }
	100% { opacity: 0; }
}

@keyframes character-help-pose-primary {
	0%, 44.444% { opacity: 1; }
	50%, 94.444% { opacity: 0; }
	100% { opacity: 1; }
}

@keyframes character-help-pose-alternate {
	0%, 44.444% { opacity: 0; }
	50%, 94.444% { opacity: 1; }
	100% { opacity: 0; }
}

@keyframes character-help-marker-primary {
	0%, 44.444% { opacity: 1; }
	50%, 94.444% { opacity: 0; }
	100% { opacity: 1; }
}

@keyframes character-help-marker-alternate {
	0%, 44.444% { opacity: 0; }
	50%, 94.444% { opacity: 1; }
	100% { opacity: 0; }
}

GameHud {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	pointer-events: none;

	.top-buttons {
		position: absolute;
		top: 40px;
		right: 40px;
		flex-direction: row;
		flex-wrap: wrap;
		justify-content: flex-end;
		align-items: flex-start;
		pointer-events: all;

		// Only ever set during the character help overlay's auto-dismiss FADE — while the help panel is
		// actually up the whole row is dropped from the tree (see GameHud.razor). Lifts the full row (the
		// pulsing "?" included) above the fading overlay, and kills clicks for that beat so a press
		// lands on the dismiss layer instead of a button the frozen sim can't act on yet.
		&.help-open {
			z-index: 3100;
			pointer-events: none;

			.hud-button {
				pointer-events: none;
			}
		}

		// Visuals come from the shared .square-btn (Theme.scss); this only spaces the row and
		// colours the per-button accents.
		.hud-button {
			margin-left: 12px;
			margin-bottom: 12px;

			&.replay-save {
				flex-direction: column;

				.img {
					margin-top: 3px;
				}

				.caption {
					font-family: $font;
					font-size: 13px;
					color: $white;
					margin-top: -2px;
				}

				&.saved {
					border-color: rgba($white, 0.65);

					&:hover {
						background-color: rgba(255, 123, 123, 0.35);
						border-color: #ff7b7b;
					}
				}
			}

			&.replay-favorite {
				border-color: rgba($white, 0.75);

				&.active {
					border-color: #ff9b9b;

					.img { background-tint: #ff9b9b; }
				}
			}

			&.help {
				position: relative;

				// The "?" art spans the whole 64x64 face inside the 4px border (not the shared 40x40
				// icon box) so it can share the square with the corner character icon.
				.img {
					width: 64px;
					height: 64px;
				}

				// The character this button explains, tucked into the lower-left corner over the "?"
				// glyph (the icon PNG leaves that corner clear). Same glow-behind-sprite stack as the
				// help header. The clip box spans the button face so the baked glow oval can't spill
				// past the border — the clip lives on this child, NOT the button, so the pulse's
				// box-shadow is left alone.
				.character-icon-clip {
					position: absolute;
					left: 0;
					top: 0;
					right: 0;
					bottom: 0;
					overflow: hidden;
					z-index: 1;
					pointer-events: none;
				}

				// Corner anchor (left/bottom) is inline: HelpButtonIconStyle adds the per-character nudge.
				.character-icon {
					position: absolute;
					pointer-events: none;

					.icon-glow {
						position: absolute;
						left: -61%;
						top: -60.5%;
						width: 222%;
						height: 221%;
						z-index: 0;
						pointer-events: none;
						opacity: 0.3;
						background-image: url( "sprites/ui/icon_glow.png" );
						background-repeat: no-repeat;
						background-size: 100% 100%;
					}

					.sprite-layer {
						position: absolute;
						z-index: 1;
						pointer-events: none;
						background-repeat: no-repeat;
						background-size: contain;
						background-position: center;
						image-rendering: pixelated;
					}

					&.paired {
						.rear { transform: translate( 11%, -9% ) scale( 0.94 ); }
						.front { transform: translate( -11%, 9% ); }
					}
				}

				&.disabled {
					cursor: blank-arrow;
					pointer-events: none;
					border-color: #727985;
					background-color: rgba( $black, 0.35 );

					.img { background-tint: #727985; }
					.character-icon { opacity: 0.45; }

					&:hover {
						background-color: rgba( $black, 0.35 );
					}
				}

				// Pulses until the player opens the help manually once (GameStage.ShouldPulseHelpButton).
				// The icon tints dark for the whole run of the pulse — the face never returns to dark —
				// so it stays readable without a lock-step tint animation.
				&.pulsing {
					transform-origin: 50% 50%;
					animation: character-help-button-pulse 0.5s ease-in-out infinite;

					.img { background-tint: $black; }
				}
			}
		}
	}

	// Square button in the bottom-right letterbox that abandons the run/replay and returns to the
	// title screen. Shared .square-btn visuals; the red accent marks it as a hard exit.
	.quit-button {
		position: absolute;
		bottom: 40px;
		right: 40px;
		border-color: #ff7b7b;

		.img { background-tint: #ff9b9b; }
	}

	// Square back button, sitting immediately left of the quit/home button (which is at right:40px,
	// 72px wide). Returns to the launching menu rather than the title screen; keeps the neutral
	// white accent so it doesn't read as a hard exit.
	.back-button {
		position: absolute;
		bottom: 40px;
		right: 124px;
	}

	// Hold-to-restart overlay: dims the game square while R is held during an ACTIVE run (post-death R
	// restarts instantly and never shows this), with a small progress bar that fills over the hold.
	// The whole overlay's opacity is driven per-frame from GameManager.RestartHoldAlpha (rapid fade in
	// while held, fade back out from the current level on release) — no CSS animation here. It shows
	// during LIVE gameplay, under the retro 240-grid quantize, so the bar is three nested plain rects
	// (frame / trough / fill, no CSS borders) whose geometry GameHud computes on whole grid cells.
	.restart-hold-overlay {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		z-index: 2000;
		pointer-events: none;

		// Dims ONLY the centred arena square (the sim keeps running under it), not the letterbox —
		// same left-offset maths as the tutorial/help backdrops (ArenaSquareBackdropStyle).
		.dim {
			position: absolute;
			top: 0;
			width: 1080px;
			height: 100%;
			background-color: rgba( $black, 0.55 );
		}

		// "RESTARTING..." above the bar, fading with the whole overlay. Grid-snapped 81px pixel font
		// (position comes from GameHud.RestartHoldTextStyle) so it stays crisp under the quantize.
		.caption {
			position: absolute;
			font-family: $font;
			font-smooth: never;
			font-size: 81px;
			color: $white;
			white-space: nowrap;
		}

		.bar {
			position: absolute;
			background-color: $white; // the 1-cell frame, revealed around the inset trough

			.inner {
				position: absolute;
				background-color: rgba( $black, 0.9 );

				.fill {
					position: absolute;
					left: 0;
					top: 0;
					height: 100%;
					background-color: $white;
				}
			}
		}
	}

	// Restart button, sitting immediately left of the back button (back is at right:124px, 72px wide;
	// the restart button only appears after a player death in a normal run, where the back button is
	// always present too). A counter-clockwise "replay" arrow with an R overlaid dead-centre.
	.restart-button {
		position: absolute;
		bottom: 40px;
		right: 208px;

		// The R is laid over the arrow, centred within the full button rect.
		.letter {
			position: absolute;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
			justify-content: center;
			align-items: center;
			font-family: $font;
			font-size: 18px;
			color: $white;
			pointer-events: none;
		}
	}

	// Full-screen transparent hit-catcher, present only while the timeline is being scrubbed. It exists
	// so the mouse-release is processed wherever the cursor is (the arena / letterbox have no panel, so
	// without it the release isn't seen and the drag sticks). Sits behind the transport panel so it
	// never intercepts the controls themselves.
	.scrub-catcher {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		z-index: 0;
		pointer-events: all;
	}

	// Editor test-play exit: a square button at the bottom-RIGHT of the LEFT letterbox (just left of
	// the centred play square — same spot as the editor's Test Play button). Anchored to screen centre
	// so it hugs the square edge on any aspect: right:50% puts its right edge at centre, then
	// margin-right pushes it left past the square half-width (540) + a gap. Back = BACKSPACE also exits.
	.test-exit-button {
		position: absolute;
		bottom: 40px;
		right: 50%;
		// 580 = square half-width (540) + 40, so the gap to the square edge matches the 40px bottom inset.
		margin-right: 580px;
		border-color: #ff7b7b;

		.img { background-tint: #ff9b9b; }
	}

	// Square reload-characters button in the bottom-right of the LEFT letterbox (same screen-centre
	// anchor as the test-exit button, 40px inset from the square edge to match the bottom inset). Runs
	// the reload_characters ConCmd. In live play it sits in the very corner (bottom:40px); in an editor
	// test run the exit button owns that corner, so `.above-exit` lifts it one button-height above it.
	.reload-characters-button {
		position: absolute;
		bottom: 40px;
		right: 50%;
		margin-right: 580px;
		border-color: #c79bff;

		&.above-exit { bottom: 124px; }

		.img { background-tint: #d9bcff; }
	}

	// Replay-mode transport panel, shown instead of the gear while watching a recorded run. Lives in
	// the LEFT letterbox (outside the play square, which fills the screen height and is centred), so
	// it never covers the arena on standard wide aspect ratios.
	// Left-letterbox dock holding the replay transport panel and (beneath it) the HIJACK button, so the
	// button always sits centred right under the variable-height panel.
	.replay-dock {
		z-index: 2; // above the arena-only reaction placement surface
		position: absolute;
		top: 40px;
		left: 32px;
		width: 320px;
		flex-direction: column;
		align-items: stretch;
		pointer-events: none; // children opt back in
	}

	.replay-panel {
		flex-direction: column;
		align-items: stretch;
		padding: 18px 18px 16px 18px;
		pointer-events: none; // children opt back in
		background-color: rgba($black, 0.62);
		border: 3px solid rgba($white, 0.85);
		font-size: 22px;
		color: $white;
		font-family: $font; // pixel font for ALL panel text (icons override with Material Icons)

		.reaction-status {
			font-size: 18px;
			color: #dddd99;
			margin-top: 10px;
		}

		// The level being watched, shown at the top of the panel. Tag (map ordinal) in a muted colour
		// before the white name, mirroring the level-select header.
		.replay-level {
			flex-direction: row;
			align-items: center;
			gap: 5px;
			margin-bottom: 12px;

			.tag {
				font-size: 26px;
				color: rgba(#8a97a5, 0.65);
			}

			.name {
				font-size: 26px;
				color: $white;
			}
		}

		// Who set this run: avatar + name, with the submission date and country underneath.
		.submitter {
			flex-direction: row;
			align-items: center;
			margin-bottom: 14px;

			// 48px face comes from the component's Size; click opens the Steam profile.
			SteamAvatar {
				margin-right: 12px;
				border: 2px solid rgba($white, 0.6);
			}

			.who {
				flex-direction: column;
				justify-content: center;
				flex-grow: 1;

				.nameline {
					flex-direction: row;
					align-items: center;

					.name {
						font-size: 20px;
						color: $white;
					}

					// Country flag (flagcdn 4:3 image) sitting beside the name.
					.flag {
						pointer-events: all;
						// cover+center comes from FlagSheet.Style's inline background-size/position
						// (flags live in one CSS-sprite sheet); box size feeds FlagSheet.Style( code, 28, 20 ).
						width: 28px;
						height: 20px;
						margin-left: 10px;
						flex-shrink: 0;
						border: 1px solid rgba($black, 0.8);
					}
				}

				.meta {
					margin-top: 2px;
					font-size: 15px;
					color: rgba($white, 0.6);
				}
			}
		}

		.replay-view-counts {
			flex-direction: column;
			margin-bottom: 14px;
			padding-bottom: 12px;
			border-bottom: 2px solid rgba($white, 0.22);
			font-size: 20px;
			color: rgba($white, 0.8);

			// Keep the empty row mounted so loading the counts cannot move the controls below.
			.counts-row { width: 100%; height: 24px; flex-shrink: 0; flex-direction: row; gap: 20px; }
			.count { height: 24px; flex-shrink: 0; align-items: center; gap: 8px; pointer-events: all; }
			.reaction-count { margin-left: auto; }
			label { height: 24px; line-height: 24px; white-space: nowrap; pointer-events: none; }
			.icon { font-family: "Material Icons"; font-size: 22px; flex-shrink: 0; pointer-events: none; }
		}

		// Title row: the "▶ REPLAY" / "■ FINISHED" / "● HIJACKED" label, plus (when hijacked) the
		// "PRACTICE RUN" note on the same horizontal line, right after it.
		.title-row {
			flex-direction: row;
			align-items: center;
			justify-content: center;
			gap: 10px;
			margin-bottom: 14px;
		}

		.title {
			justify-content: center;
			font-size: 26px;
			color: #ff5b5b; // record-red accent

			// Live-control (hijack) state: a distinct green accent so it never reads as plain playback.
			&.hijack { color: #5bff8b; }
		}

		// "PRACTICE RUN" note shown beside the title while hijacked.
		.hijack-note {
			justify-content: center;
			text-align: center;
			font-size: 16px;
			color: rgba($white, 0.5);
			// The pixel font's tall line-box sits the smaller note glyphs high; nudge down to visually
			// centre it against the larger title on the same row.
			margin-top: 5px;
		}

		.speed-head {
			flex-direction: row;
			align-items: center;
			justify-content: space-between;
			gap: 12px;
			margin-bottom: 2px;

			.caption { color: rgba($white, 0.7); font-size: 18px; }

			.value {
				font-family: $font-mono;
				color: $white;
				&.slow { color: #6bd0ff; }
				&.fast { color: #ffd23f; }
			}
		}

		.speed-slider {
			pointer-events: all; // draggable
			height: 6px;
		}

		.ends {
			flex-direction: row;
			justify-content: space-between;
			margin-top: 4px;
			margin-bottom: 14px;
			color: rgba($white, 0.5);
			font-size: 15px;
			font-family: $font-mono;
		}

		.ctl {
			flex-direction: row;
			align-items: center;
			pointer-events: all;
			cursor: blank-hand;
			margin-bottom: 8px;
			padding: 8px 12px;
			background-color: rgba($black, 0.7);
			border: 2px solid rgba($white, 0.7);
			color: $white;
			transition: background-color 0.1s ease;

			.icon {
				font-family: "Material Icons";
				font-size: 26px;
				margin-right: 10px;
			}

			label { color: $white; }

			&:hover { background-color: rgba($black, 0.95); }

			&.disabled {
				cursor: none;
				border-color: #667075;
				color: #667075;
				background-color: rgba($black, 0.7);
				label, .icon { color: #667075; }
			}

			// STOP (end hijack) — amber accent, distinct from the red BACK/exit.
			&.stop {
				border-color: #ffc24b;
				label, .icon { color: #ffd584; }
			}
		}

		// Scrubbable replay timeline (ReplayScrubBar self-styles its internals; this just spaces it).
		.timeline {
			margin-top: 8px;
		}

		.clock-row {
			position: relative;
			flex-direction: row;
			justify-content: flex-start;
			align-items: center;
			margin-top: 8px;

			.clock {
				justify-content: flex-start;
				font-size: 18px;
				font-family: $font-mono;
				color: rgba($white, 0.85);
			}

			// Frame counter pinned to the right edge of the row, right-justified, while the clock stays
			// centred under the progress bar.
			.frame {
				position: absolute;
				right: 0;
				justify-content: flex-end;
				font-size: 18px;
				font-family: $font-mono;
				color: rgba($white, 0.85);
			}
		}
	}

	// Per-tick input readout in the bottom-left letterbox, arrow-key layout (U on top, L/D/R below).
	// Each square brightens while its direction is held on the current replayed tick.
	.input-display {
		position: absolute;
		bottom: 40px;
		left: 32px;
		flex-direction: column;
		align-items: center;
		pointer-events: none;

		.row {
			flex-direction: row;
		}

		.key {
			width: 64px;
			height: 64px;
			margin: 3px;
			justify-content: center;
			align-items: center;
			font-size: 30px;
			color: rgba($white, 0.25);
			background-color: rgba($black, 0.5);
			border: 3px solid rgba($white, 0.2);
			opacity: 0.2;

			&.on {
				color: $white;
				background-color: rgba($black, 0.9);
				border-color: $white;
				opacity: 1;
			}

			// Flashes on the single tick the press fires (rising edge). A bright cyan accent +
			// glow makes frame-precise taps (jump/dash/wall-kick timing) pop out from a plain hold.
			&.just {
				color: #6bd0ff;
				border-color: #6bd0ff;
				background-color: rgba(107, 208, 255, 0.35);
				box-shadow: 0 0 16px 2px rgba(107, 208, 255, 0.8);
			}
		}
	}

	// Replay actions sit below the left control panel; green matches the HIJACKED title.
	.hijack-btn {
		align-self: center; // centred under the replay panel
		margin-top: 12px;
		flex-direction: row;
		align-items: center;
		justify-content: center;
		pointer-events: all;
		cursor: blank-hand;
		padding: 10px 16px;
		background-color: rgba($black, 0.7);
		border: 3px solid #5bff8b;
		color: #5bff8b;
		font-family: $font; // pixel font for the label (icon overrides with Material Icons)
		transition: background-color 0.1s ease;

		.icon {
			font-family: "Material Icons";
			font-size: 28px;
			margin-right: 10px;
			color: #5bff8b;
		}

		label {
			font-size: 22px;
			color: #5bff8b;
		}

		&:hover { background-color: rgba($black, 0.95); }

		&.disabled {
			cursor: none;
			border-color: #667075;
			color: #667075;
			background-color: rgba($black, 0.7);
			.icon, label { color: #667075; }
		}

		&.react-btn {
			border-color: #e7ba72;
			color: #e7ba72;
			.icon, label { color: #e7ba72; }
		}
	}

	// Big "THE END" card centred over the arena once a replay has played out. Full-screen, but the
	// play square fills screen HEIGHT and is centred, so a centred child lands on the arena centre.
	// Cosmetic only (pointer-events:none) — the transport panel still owns restart/back.
	.end-overlay {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		pointer-events: none;

		.end-text {
			font-family: $font;
			font-size: 200px;
			color: rgba($white, 0.3);
			text-align: center;
			text-shadow: 6px 6px 0 rgba($black, 0.25);
		}

		// "PLAYBACK DESYNCED" readout under THE END. Shows under the 240-grid quantize (the retro
		// shader keeps quantizing at replay end), so like the tutorial popup every metric is a whole
		// number of grid cells: 81px = the 18-cell em (one art pixel per cell), and the box + per-line
		// indents are snapped onto exact cells in code (GameHud.DesyncWarningStyle / GridLineStyle).
		// No text-shadow — a soft shadow just quantizes to mud.
		.desync-warning {
			position: absolute;
			font-family: $font;
			font-smooth: never;
			flex-direction: column;
			align-items: flex-start;

			// Fixed 18-cell stack pitch, top-anchored labels — same trick as .tutorial-instruction:
			// the engine's natural line box isn't exactly the em, and auto-height stacking would
			// drift every following line half a cell off the grid.
			.line {
				height: 81px;
				flex-direction: row;
				align-items: flex-start;
			}

			label {
				font-size: 81px;
				color: rgba(245, 21, 21, 0.8); // record-red, matching the title accent
				white-space: nowrap;
			}
		}

		// Retry hint shown (instead of "THE END") when a hijacked practice run ends. Renders under the
		// end-of-replay quantize like .desync-warning, so the same rules apply: 81px = the 18-cell
		// grid em (one art pixel per cell), box + per-line indents snapped onto exact cells in code
		// (GameHud.PracticeHintStyle / GridLineStyle). No text-shadow — it just quantizes to mud.
		.practice-hint {
			position: absolute;
			font-family: $font;
			font-smooth: never;
			flex-direction: column;
			align-items: flex-start;

			// Fixed 18-cell stack pitch, top-anchored labels — see .desync-warning.
			.line {
				height: 81px;
				flex-direction: row;
				align-items: flex-start;
			}

			label {
				font-size: 81px;
				color: rgba($white, 0.85);
				white-space: nowrap;
			}
		}
	}

	// Covers the whole screen; the centred panel sits over the arena square.
	.overlay {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		justify-content: center;
		align-items: center;
		pointer-events: all;
		background-color: rgba($teal-dark, 0.6);

		.panel {
			flex-direction: column;
			align-items: center;
			justify-content: center;
			// Positioned + clipped so the absolutely-filled OptionsBackdrop (drifting blocks) anchors to
			// THIS square panel — not the full-screen .overlay — and its blocks stay square and confined.
			position: relative;
			overflow: hidden;
			// Square (not a tall rectangle): the option rows are ~800px wide and the full list a touch
			// taller, so lock a fixed square comfortably larger than the content and let the flex
			// centring sit the rows in the middle of the checkerboard field.
			width: 880px;
			height: 880px;
			padding: 48px 56px;
			// Redraw the main-menu options look in UI space (the arena is paused behind us): the same
			// dithered teal checkerboard field the menu shows as the live world. The world maps the
			// 240px texture 1:1 onto the 240-unit arena that fills the 1080-ref screen height, i.e.
			// 4.5px per texture cell -> background-size 1080px keeps the identical cell scale, tiled to
			// fill the panel and point-sampled so the dither stays crisp. $teal-dark backs it as a fallback.
			background-color: $teal-dark;
			background-image: url( "sprites/background.png" );
			background-repeat: repeat;
			background-size: 1080px 1080px;
			image-rendering: pixelated;
			border: 6px solid $white;
		}
	}

	// Leave-daily-run confirm (GameStage.LeaveConfirmOpen): the options overlay's dim with a compact
	// dialog in the ReplayCodeDialog style. .selected mirrors :hover so keyboard/controller focus
	// reads the same as the mouse. Above the hold-to-restart dim, below the character help.
	.leave-confirm {
		z-index: 2500;

		.dialog {
			width: 820px;
			flex-direction: column;
			align-items: center;
			padding: 36px;
			background-color: #024a54;
			border: 6px solid $white;
			font-family: $font;
			font-smooth: never;
		}

		.title {
			font-size: 48px;
			color: $white;
			text-shadow: 3px 3px 0 $black;
			margin-bottom: 20px;
		}

		.attempts {
			font-size: 28px;
			color: #ff9b9b;
		}

		.buttons {
			flex-direction: row;
			justify-content: center;
			margin-top: 28px;

			button {
				min-width: 260px;
				height: 58px;
				justify-content: center;
				align-items: center;
				margin: 0 10px;
				font-size: 28px;
				color: $black;
				background-color: $white;
				cursor: blank-hand;
				pointer-events: all;
				// Always-present transparent border so the controller focus ring below never resizes
				// the button when it appears.
				border: 4px solid transparent;

				&:hover { background-color: #dddd99; }
				&.selected { background-color: #dddd99; }

				&.leave {
					background-color: #c98a8a;

					&:hover { background-color: #d99a9a; }
					&.selected { background-color: #d99a9a; }
				}
			}

			// Gamepad focus ring: a hard white frame with a dark inset so it reads on both button tints.
			&.controller button.selected {
				border-color: $white;
				box-shadow: inset 0 0 0 3px $black;
			}
		}
	}

	.character-help-overlay {
		position: absolute;
		left: 0;
		top: 0;
		width: 100%;
		height: 100%;
		z-index: 3000;
		pointer-events: none;
		font-family: $font;
		font-smooth: never;

		.dismiss-layer {
			position: absolute;
			left: 0;
			top: 0;
			width: 100%;
			height: 100%;
			align-items: center;
			justify-content: center;
			pointer-events: all;
			cursor: blank-hand;

			&.fading {
				animation: character-help-dismiss 1.1s linear forwards;
			}
		}

		.arena-backdrop {
			position: absolute;
			top: 0;
			width: 1080px;
			height: 100%;
			background-color: rgba( $black, 0.78 );
		}

		.help-panel {
			position: relative;
			width: 820px;
			max-width: 90%;
			max-height: 930px;
			padding: 40px 48px 44px 48px;
			flex-direction: column;
			align-items: stretch;
			pointer-events: all;
			cursor: blank-arrow;
			background-color: $black;
			border: 6px solid $white;
			box-shadow: 0 12px 40px rgba( #000000, 0.55 );

			.help-header {
				height: 128px;
				flex-direction: row;
				align-items: center;
				justify-content: center;
				border-bottom: 4px solid rgba( $white, 0.35 );
				margin-bottom: 28px;

				.help-character {
					position: relative;
					flex-shrink: 0;
					margin-right: 44px;

					.icon-glow {
						position: absolute;
						left: -61%;
						top: -60.5%;
						width: 222%;
						height: 221%;
						z-index: 0;
						pointer-events: none;
						opacity: 0.3;
						background-image: url( "sprites/ui/icon_glow.png" );
						background-repeat: no-repeat;
						background-size: 100% 100%;
					}

					.sprite-layer {
						position: absolute;
						z-index: 1;
						background-repeat: no-repeat;
						background-size: contain;
						background-position: center;
						image-rendering: pixelated;
					}

					&.paired {
						.rear { transform: translate( 11%, -9% ) scale( 0.94 ); }
						.front { transform: translate( -11%, 9% ); }
					}
				}

				.character-name {
					font-size: 64px;
					color: $white;
					white-space: nowrap;
				}
			}

			.ability-list {
				flex-direction: column;
				align-items: stretch;
			}

			.ability-row {
				min-height: 116px;
				padding: 14px 8px;
				flex-direction: column;
				align-items: center;
				border-bottom: 3px solid rgba( $white, 0.16 );

				&:last-child {
					border-bottom: 0;
				}

				.ability-name {
					width: 100%;
					font-size: 32px;
					color: $white;
					margin-bottom: 8px;
					text-align: center;
				}

				.instruction {
					width: 100%;
					min-width: 0;
					flex-direction: row;
					flex-wrap: wrap;
					align-items: center;
					justify-content: center;

					.row-illustration {
						position: relative;
						width: 60px;
						height: 60px;
						margin-right: 18px;
						flex-shrink: 0;

						.icon-glow {
							position: absolute;
							left: -61%;
							top: -60.5%;
							width: 222%;
							height: 221%;
							z-index: 0;
							pointer-events: none;
							opacity: 0.3;
							background-image: url( "sprites/ui/icon_glow.png" );
							background-repeat: no-repeat;
							background-size: 100% 100%;
						}

						.pose {
							position: absolute;
							left: 0;
							top: 0;
							width: 60px;
							height: 60px;
							z-index: 1;
							background-repeat: no-repeat;
							background-size: contain;
							background-position: center;
							image-rendering: pixelated;

							&.alternating.primary {
								animation: character-help-pose-primary 1.8s linear infinite;
							}

							&.alternating.alternate {
								animation: character-help-pose-alternate 1.8s linear infinite;
							}
						}
					}

					.direction-keys {
						flex-direction: row;
						flex-shrink: 0;
						margin-right: 16px;

						&.terminal {
							margin-right: 0;

							.direction-key:last-child, .button-key:last-child {
								margin-right: 0;
							}
						}

						&.compact {
							.direction-key {
								margin-right: -8px;
							}

							// A face button following a compact (overlapped) chord must not be overlapped
							// itself — restore a normal gap after the chord's -8px pull.
							.button-key {
								margin-left: 8px;
							}
						}

						&.terminal.compact {
							.direction-key:last-child, .button-key:last-child {
								margin-right: 0;
							}
						}

						.direction-key {
							width: 42px;
							height: 42px;
							margin-right: 6px;
							flex-shrink: 0;
							background-repeat: no-repeat;
							background-size: 100% 100%;
							image-rendering: pixelated;

							// One baked keycap per direction (tools/bake.py, bake_arrow_keys) instead of
							// rotating a single image: the cap is lit from above, so rotating it carried
							// its shaded bottom edge round to the side. Only the arrow turns. Every key
							// gets one of these four classes (see CharacterHelpDirectionClass).
							&.up { background-image: url( "sprites/ui/arrow_key_up.png" ); }
							&.right { background-image: url( "sprites/ui/arrow_key_right.png" ); }
							&.down { background-image: url( "sprites/ui/arrow_key_down.png" ); }
							&.left { background-image: url( "sprites/ui/arrow_key_left.png" ); }
						}

						// Controller face-button glyphs, shown in place of keyboard patterns while a
						// controller is in use: X (blue) fires the scripted double tap, Y (yellow) the
						// all-four-directions chord, A (green) is jump. Drawn in CSS (round face + letter) so no baked art
						// is needed; sized to sit beside the 42px keycaps.
						.button-key {
							width: 42px;
							height: 42px;
							margin-right: 6px;
							flex-shrink: 0;
							border-radius: 21px;
							font-size: 30px;
							line-height: 40px;
							text-align: center;
							justify-content: center;
							// The pixel font's letter box carries a right-side advance gap, which reads as
							// the glyph sitting left of the circle's centre — nudge it back.
							padding-left: 1px;

							&.x {
								background-color: #2f5faf;
								border: 3px solid #6b9be8;
								color: #e8f0ff;
							}

							&.y {
								background-color: #d8ac10;
								border: 3px solid #f2d95e;
								color: #453400;
							}

							&.a {
								background-color: #2f8a3c;
								border: 3px solid #6fcc7a;
								color: #e6ffe9;
							}
						}

					}

					.direction-separator {
						font-size: 32px;
						line-height: 42px;
						color: rgba( $white, 0.82 );
						margin: 0 8px 0 2px;

						&.comma {
							font-size: 42px;
							margin: 0 9px 0 -2px;
						}
					}

					.teleport-controls {
						flex-direction: row;
						align-items: center;
						flex-shrink: 0;

						.teleport-origin-chords {
							flex-direction: row;

							.teleport-origin-chord:first-child {
								margin-right: 6px;
							}
						}

						.direction-separator {
							flex-shrink: 0;
						}
					}

					.thread-controls {
						flex-direction: row;
						align-items: center;
						flex-shrink: 0;

						.thread-chord:first-child {
							margin-right: 14px;
						}
					}

					.direction-prefix {
						font-size: 27px;
						color: rgba( $white, 0.82 );
						margin-right: 1px;
						white-space: nowrap;
					}

					.row-character-sprites {
						flex-direction: row;
						align-items: center;
						flex-shrink: 0;

						&.compact {
							.row-character-preview {
								margin-right: -12px;

								&:last-child {
									margin-right: 12px;
								}
							}
						}

						&.overlap {
							.row-character-preview {
								margin-right: -42px;

								&:last-child {
									margin-right: 0;
								}
							}
						}

						.row-character-preview {
							position: relative;
							margin-right: 10px;
							flex-shrink: 0;

							.icon-glow {
								position: absolute;
								left: -61%;
								top: -60.5%;
								width: 222%;
								height: 221%;
								z-index: 0;
								pointer-events: none;
								opacity: 0.3;
								background-image: url( "sprites/ui/icon_glow.png" );
								background-repeat: no-repeat;
								background-size: 100% 100%;
							}

							.row-character-sprite {
								position: absolute;
								left: 0;
								top: 0;
								width: 100%;
								height: 100%;
								z-index: 1;
								background-repeat: no-repeat;
								background-size: contain;
								background-position: center;
								image-rendering: pixelated;
							}

							.control-marker {
								position: absolute;
								left: 50%;
								top: -8px;
								width: 20px;
								height: 8px;
								margin-left: -10px;
								z-index: 2;
								pointer-events: none;

								&.alternating.primary {
									animation: character-help-marker-primary 1.8s linear infinite;
								}

								&.alternating.alternate {
									animation: character-help-marker-alternate 1.8s linear infinite;
								}

								.bar {
									position: absolute;
									left: 0;
									top: 0;
									width: 20px;
									height: 4px;
									background-color: $white;
								}

								.tip {
									position: absolute;
									left: 8px;
									top: 4px;
									width: 4px;
									height: 4px;
									background-color: $white;
								}
							}
						}
					}

					// Gesture note: sits on the input line right after the keys, dimmer and smaller than
					// the hint below so it reads as an aside rather than an instruction.
					.gesture-note {
						flex-direction: row;
						align-items: center;
						font-size: 21px;
						color: rgba( $white, 0.38 );
						margin-left: 12px;
						white-space: nowrap;
						flex-shrink: 0;

						.gesture-alternative {
							margin-left: 6px;
						}

						.gesture-glyph {
							width: 28px;
							height: 28px;
							margin: 0 4px;
							flex-shrink: 0;
							opacity: 0.7;
						}
					}

					.hint {
						width: 100%;
						min-width: 0;
						margin-top: 4px;
						font-size: 23px;
						color: rgba( $white, 0.62 );
						white-space: normal;
						text-align: center;

						&.extra-top-padding {
							padding-top: 6px;
						}
					}
				}
			}
		}
	}

	.tutorial-instruction-dismiss {
		position: absolute;
		left: 0;
		top: 0;
		width: 100%;
		height: 100%;
		z-index: 3000;
		pointer-events: all;
		align-items: center;
		justify-content: center;

		// Dims the 240px game screen (the centred 1080-ref arena square) behind the popup — NOT the
		// letterbox, so the click-to-dismiss container above stays transparent. Left edge comes from
		// GameHud.TutorialBackdropStyle() (aspect-dependent).
		.backdrop {
			position: absolute;
			top: 0;
			width: 1080px;
			height: 100%;
			background-color: rgba( $black, 0.6 );
		}

		// 0.4s = GameStage.TUTORIAL_PROMPT_DISMISS_TIME. The sim is already unfrozen for this fade —
		// the hold completing hands input back immediately (see GameStage.BlocksSimulation).
		&.dismissing {
			.backdrop {
				animation: tutorial-instruction-backdrop-dismiss 0.4s linear forwards;
			}

			.tutorial-instruction,
			.hold-bar {
				animation: tutorial-instruction-dismiss 0.4s ease-out forwards;
			}
		}

		// Hold-to-dismiss progress bar floating above the popup box: frame / trough / fill as three
		// nested plain rects, same construction as the hold-to-restart bar. All geometry comes
		// grid-snapped in absolute px from GameHud's TutorialHold*Style helpers so it stays crisp
		// under the quantize.
		.hold-bar {
			position: absolute;
			background-color: $white; // the 1-cell frame, revealed around the inset trough

			.inner {
				position: absolute;
				background-color: rgba( $black, 0.9 );

				.fill {
					position: absolute;
					left: 0;
					top: 0;
					height: 100%;
					background-color: $white;
				}
			}
		}

		// Shown DURING gameplay, i.e. under the 240-grid quantize pass — so unlike the level select's
		// popup twin, every metric here must be a whole number of grid cells (1 cell = 4.5px in this
		// 1080-ref space) or the text mushes. Position + size come from GameHud.TutorialPopupStyle(),
		// which snaps the box onto exact cell boundaries (flex centring would land it between cells);
		// keep these paddings/borders in sync with its POPUP_*_CELLS constants. No soft box-shadow:
		// a smooth glow just quantizes into muddy rings.
		.tutorial-instruction {
			position: absolute;
			padding: 9px 18px;        // 2 / 4 cells
			border: 9px solid $white; // 2 cells
			background-color: $black;
			pointer-events: all;
			font-family: $font;
			font-smooth: never;
			// One .line row per prompt line, stacked; each line is indented onto whole cells by
			// TutorialLineStyle (NOT centred here — flex centring can land on half-cell offsets).
			flex-direction: column;
			align-items: flex-start;

			// The engine's natural label line box is NOT exactly the 81px em, so auto-height
			// stacking drifts every following line off the grid (≈half a cell per line: the
			// crisp/blurry/crisp pattern). Fix the stack pitch at exactly 18 cells and top-anchor
			// the label, so every line renders with the same on-grid offset as the first.
			.line {
				height: 81px;
				flex-direction: row;
				align-items: flex-start;
			}

			label {
				// 81px = 18-cell em: m6x11plus draws each art pixel as exactly one grid cell.
				font-size: 81px;
				color: $white;
				white-space: nowrap;
			}
		}
	}
}