ui/levelselectscreen.razor.scss
// Designed in 1080-reference space (ScreenPanel AutoScreenScale = ConsistentHeight).
@import "Theme.scss";

// Node brightness tiers. Beaten = brightest, selectable-unbeaten = dull, locked = dullest.
// Lines invert that contrast: traversable paths are substantial and dark; blocked paths are lighter.
$node-beaten: #dddd99;
$node-beaten-bright: #f8f5a8;
$node-open: #3f7c84;
$node-locked: #2c323d;
$node-border: #1f242c;

$line-on: #2c323d;
$line-off: #4f9aa0;

// Resting glow pulse for the selected node's white frame (see .node.selected). Eases the soft outer
// glow between faint and bright and back, so the highlighted node gently breathes without shifting
// colour. Overridden by :hover, which swaps it for a static glow.
@keyframes node-selected-pulse {
	0% {
		box-shadow: 0px 0px 8px 1px rgba( $white, 0.35 );
	}
	50% {
		box-shadow: 0px 0px 26px 4px rgba( $white, 0.85 );
	}
	100% {
		box-shadow: 0px 0px 8px 1px rgba( $white, 0.35 );
	}
}

@keyframes node-beaten-selected-pulse {
	0% {
		box-shadow: 0px 0px 8px 1px rgba( $node-beaten, 0.4 );
	}
	50% {
		box-shadow: 0px 0px 26px 4px rgba( $node-beaten-bright, 0.95 );
	}
	100% {
		box-shadow: 0px 0px 8px 1px rgba( $node-beaten, 0.4 );
	}
}

@keyframes node-selection-bounce {
	0% { transform: scale( 1 ); }
	50% { transform: scale( 1 ); }
	60% { transform: scale( 1.14 ); }
	100% { transform: scale( 1 ); }
}

@keyframes tutorial-node-unlock {
	0% { transform: scale( 0.88 ); box-shadow: 0 0 0 0 rgba( $white, 0 ); }
	45% { transform: scale( 1.12 ); box-shadow: 0 0 34px 7px rgba( $white, 0.95 ); }
	100% { transform: scale( 1 ); box-shadow: 0 0 16px 2px rgba( $node-open, 0.55 ); }
}

@keyframes tutorial-line-unlock {
	0% { opacity: 0.25; }
	50% { opacity: 1; background-color: $white; }
	100% { opacity: 1; background-color: $line-on; }
}

LevelSelectScreen {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	flex-direction: column;
	align-items: center;
	// Anchor the square to the TOP of the screen (not centred) so it hugs the screen top with no gap
	// above it — the camera's teal play-square fills the full screen height, so the map square must
	// start at the very top to line up with it.
	justify-content: flex-start;

	font-family: $font;
	font-smooth: never;

	// The fixed play-square region: the map is laid out and clipped inside this.
	.square {
		position: relative;
		width: 1080px;
		height: 1080px;
		overflow: hidden;
		// Catch the wheel over the WHOLE map region, not just on top of a node — otherwise empty map
		// space (between/around nodes) has no pointer-events target and the wheel never reaches
		// OnMouseWheel, so map scrolling would only work while hovering a node. The footer is a later
		// root sibling drawn on top, so it still intercepts the wheel first when hovered.
		pointer-events: all;

		// Clip the map above the footer. Offset the clip 1px right to align both letterbox edges
		// with the rendered playfield: no preview overhang on the left or empty seam on the right.
		// .map-content is shifted back by the same amount so node coordinates stay in full 1080-square
		// space; only the visible clip window changes.
		.clip {
			position: absolute;
			left: 9px;
			top: 9px;
			right: 8px;
			bottom: 340px; // match the footer height so map content cannot leak beside it
			overflow: hidden;

			.map-content {
				position: absolute;
				left: -9px;
				top: -9px;
				width: 1080px;
				height: 1080px;
			}
		}

		.line {
			position: absolute;
			background-color: $line-off;

			&.on {
				background-color: $line-on;
			}

			&.unlocking {
				animation: tutorial-line-unlock 0.7s ease-out;
			}
		}

		.node-anchor {
			position: absolute;
			transform-origin: 50% 50%;
			pointer-events: none;

			&.selected {
				z-index: 2;
				animation: node-selection-bounce 0.25s ease-out;
			}

			&.unlocking {
				z-index: 3;
			}
		}

		.node {
			position: absolute;
			left: 0;
			top: 0;
			width: 100%;
			height: 100%;
			// The face is a LevelNodePreview miniature of the level; clip it to the node bounds.
			overflow: hidden;
			border: 4px solid $node-border;
			transform-origin: 50% 50%;
			pointer-events: all;
			cursor: blank-hand;

			// State now reads from the FRAME (border) colour, since the fill shows the level preview.
			&.beaten {
				border-color: $node-beaten;
			}

			// Black frame so open-but-unbeaten nodes stand out against the yellow beaten ones.
			&.open {
				border-color: #000000;
			}

			&.locked {
				border-color: $node-locked;
			}

			&.unlocking {
				z-index: 3;
				animation: tutorial-node-unlock 0.7s ease-out;
			}

			// The currently-selected node: a thicker WHITE frame with a gently PULSING glow so it's
			// obvious which node is chosen, without changing colour away from white. Selection is driven
			// by keyboard/click, not hover. Listed BEFORE :hover so hover can still visibly override it.
			&.selected {
				border-width: 6px;
				border-color: $white;
				animation: node-selected-pulse 1.3s ease-in-out infinite;
			}

			// Mouse-over highlights the outline; it does NOT change the selection (only a click or
			// keyboard move does). Placed LAST so it wins even over .selected — hovering a selected node
			// swaps the resting pulse for a solid, static bright glow, so the border still visibly
			// changes on hover/un-hover even while selected.
			&:hover {
				border-color: $white;
				animation: none;
				box-shadow: 0px 0px 24px 3px rgba( $white, 0.9 );
			}

			// A cleared level stays yellow in every interaction state; selection makes it brighter.
			&.beaten.selected {
				border-color: $node-beaten-bright;
				animation: node-beaten-selected-pulse 1.3s ease-in-out infinite;
			}

			&.beaten:hover {
				border-color: $node-beaten-bright;
				animation: none;
				box-shadow: 0px 0px 24px 3px rgba( $node-beaten-bright, 0.95 );
			}

			// White wash over an OPEN (selectable, unbeaten) level's preview. Beaten levels become the
			// only true-colour thumbnails, and the three states read distinctly at a glance:
			// beaten (vivid) -> open (washed out / pale) -> locked (near-black). Washing out instead of
			// darkening keeps open nodes clearly separated from the locked tier.
			.dim-veil {
				position: absolute;
				left: 0;
				top: 0;
				width: 100%;
				height: 100%;
				pointer-events: none;
				background-color: rgba( $white, 0.3 );
				transition: background-color 0.15s ease-out;
			}

			// Highlighting an open node lifts its veil, so selection/hover previews the level in its
			// true colours (and the saturation change itself is extra selection feedback).
			&.selected .dim-veil,
			&:hover .dim-veil {
				background-color: rgba( $white, 0 );
			}

			// Dark veil over a locked level's preview so it still reads as locked.
			.lock-veil {
				position: absolute;
				left: 0;
				top: 0;
				width: 100%;
				height: 100%;
				pointer-events: none;
				background-color: rgba( $black, 0.9 );
			}

			// Shared black semi-transparent treatment for the padlock AND the forced-character
			// silhouette that replaces it on locked forced-character levels (.lock-char geometry +
			// image come inline from the razor; the tint turns the coloured sprite into a shape).
			.lock-icon,
			.lock-char {
				position: absolute;
				pointer-events: none;
				background-tint: #000000;
				background-repeat: no-repeat;
				background-position: center;
				background-size: contain;
				image-rendering: pixelated;
				opacity: 0.8;
			}

			.lock-icon {
				left: 20%;
				top: 20%;
				width: 60%;
				height: 60%;
				background-image: url( "sprites/ui/level_lock.png" );
			}
		}

		// Soft baked oval behind the character marker (same asset as the picker / leaderboard
		// .icon-glow). Anchored per NODE, not to the moving selector — geometry and the faded
		// opacity come inline from the razor (see NodeGlowStyle / TickNodeGlows).
		.node-glow {
			position: absolute;
			pointer-events: none;
			background-image: url( "sprites/ui/icon_glow.png" );
			background-repeat: no-repeat;
			background-size: 100% 100%;
		}

		// Character-sprite marker for the highlighted node (was the smiling cube). The background image,
		// position, and scale are set inline by the razor. Drawn after the nodes so it sits above them;
		// non-interactive so it never swallows clicks or hover on the node beneath it.
		.forced-character,
		.selector {
			position: absolute;
			pointer-events: none;
			background-repeat: no-repeat;
			background-size: contain;
			background-position: center;
			image-rendering: pixelated;
		}

		.forced-character {
			opacity: 0.45;
			z-index: 1;
		}

		.selector {
			z-index: 2;
			background-position: center;
			image-rendering: pixelated;
		}

		// Top bar: the highlighted level's tag + name. Opaque so nodes scrolling under it stay
		// masked. The visible square is inset 9px; overlap both letterbox edges by 3 reference
		// pixels so fractional scaling cannot expose a thin strip of the playfield beside the bar.
		.header {
			position: absolute;
			top: 0;
			left: 7px;
			right: 7px;
			height: 134px;
			pointer-events: none;
			flex-direction: column;
			align-items: center;
			justify-content: center;
			padding: 0;
			background-color: $black;
			// Above the map content so nodes (and their corner tags) scrolling under the bar stay masked.
			z-index: 30;

			.current {
				flex-direction: row;
				align-items: center;
				gap: 24px;

				// The level's map tag (e.g. "2B"): darker + slightly transparent so it reads as a
				// secondary label next to the white level name.
				.tag {
					font-size: 40px;
					color: rgba( #8a97a5, 0.65 );
				}

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

			}
		}

		// Bottom bar moved OUT of .square (see the .footer rule at the LevelSelectScreen level) so its
		// interactive dropdowns/replay sit in the same simple context as the home button.
	}

	// Footer: a root-level child (NOT nested in the square / its overflow clip) anchored to span the
	// centred square's bottom edge (left:50% + negative half-width margin — s&box's Yoga layout does NOT
	// support auto-margin centering on absolute panels, it collapses to the left). Width matches the
	// VISIBLE teal square, which is inset 9px per side — the stage's edge blockers tuck WALL_SIZE
	// (2 world px = 9 ref px) under where the walls would be, and menu stages spawn no walls. Sized
	// The left edge overdraws 1px into the blocker strip, matching the map clip and header; its right
	// edge remains at the exact 9px boundary. This prevents fractional scaling from exposing a teal
	// seam on the left without widening the footer into the right blocker.
	// Structured like the High Scores page: controls in normal flow at the top, then a flex-grow list
	// of rows below.
	.footer {
		position: absolute;
		left: 50%;
		margin-left: -532px;
		bottom: 0;
		width: 1063px;
		height: 340px;
		pointer-events: all;
		flex-direction: column;
		align-items: stretch;
		padding: 16px 40px;
		background-color: $black;
		// Above the map content so nodes (and their corner tags) scrolling under the bar stay masked.
		z-index: 30;

		.foot-controls {
			flex-direction: row;
			justify-content: center;
			gap: 48px;
			flex-shrink: 0;
			z-index: 20;   // keep the open dropdown menu above the leaderboard rows below it

			// Fixed width so the dropdown stretches to it and its value stays on one line — a
			// width-less dropdown collapses to vertical text.
			.foot-control {
				flex-direction: column;
				width: 240px;
			}

			// Trophy shortcut to the full High Scores screen. Shared .square-btn look, shrunk to the
			// dropdown header's 48px height so the row lines up; the 4px borders leave exactly the
			// 40px icon inside. Same neutral teal accent as the home button (plain navigation).
			.board-button {
				width: 48px;
				height: 48px;
				border-color: $node-open;

				&:hover {
					border-color: $white;
				}
			}
		}

		.foot-list {
			position: relative;        // anchor for the absolutely-positioned virtual rows + scrollbar
			flex-grow: 1;
			flex-direction: column;
			margin-top: 14px;
			overflow: hidden;          // clip rows outside the viewport (we render a slice ourselves)
			pointer-events: all;       // needed so the wheel reaches OnMouseWheel

			.foot-info {
				font-size: 30px;
				color: $white;
				opacity: 0.7;
				align-self: center;
				margin-top: 20px;
			}

			// Grabbable scroll thumb; top + height are set inline by the virtualizer. Square, pixel
			// style, tucked to the right edge (overflow:hidden on .foot-list clips it in bounds).
			.foot-scrollbar {
				position: absolute;
				right: 2px;
				width: 12px;
				background-color: rgba( 248, 245, 230, 0.35 );
				border-radius: 0;
				pointer-events: all;
				cursor: blank-hand;

				&:hover {
					background-color: rgba( 248, 245, 230, 0.55 );
				}
			}

			// Each row is a fixed-height line placed by an inline top (the virtualizer); the shared
			// LeaderboardRow fills + centres within it.
			.foot-row {
				position: absolute;
				left: 0;
				right: 0;
				flex-direction: row;
				align-items: center;
				height: 60px;
				background-color: rgba(0, 0, 0, 0.12);

				&.even { background-color: rgba(0, 0, 0, 0.22); }
			}
		}
	}

	// Editor-only square shortcut in the bottom-left letterbox. Look comes from the shared
	// .square-btn (Theme.scss); only position (and accents) live here.
	.editor-button {
		position: absolute;
		bottom: 40px;
		left: 40px;
	}

	// Home button in the bottom-right letterbox (outside the centred square) — returns to the title.
	// Mirrors the in-game HUD's quit button geometry, with a neutral teal accent (plain navigation,
	// not a hard exit).
	.home-button {
		position: absolute;
		bottom: 40px;
		right: 40px;
		border-color: $node-open;

		&:hover {
			border-color: $white;
		}
	}

	// Character picker inside the centred 1080px play square's upper-right corner.
	.character-slot {
		position: absolute;
		z-index: 1000;
		right: 50%;
		margin-right: -512px;
		top: 8px;
		align-items: flex-end;
	}

}