ui/_theme.scss
// The shared visual language for Coilgarden's UI.
//
// The UI is not styled independently of the game. Every colour below is the same colour as
// something in the world (see Code/Core/Palette.cs) - the panels are the tray's terracotta,
// the text is the sand, the accent is the apple. That is what makes the interface feel like
// it belongs to the game rather than sitting on top of it.
//
// Three rules drive every value here.
//
// 1. Rank by consequence. Score is the biggest thing on screen because score is the point.
//    Best is a third of its size. Labels are tiny - a player reads the word "SCORE" once and
//    never again, so it exists only to explain the number the first time.
//
// 2. Nothing over the playfield during play. The arena has to stay readable, so the HUD sits
//    above the tray and only a finished or paused run is allowed to cover it.
//
// 3. One rhythm. Spacing is multiples of $u, radii come from one pair of values, and there is
//    a single easing curve. Consistency is most of what "polished" means.

// ---------------------------------------------------------------- palette
// Mirrors Code/Core/Palette.cs. Change one, change the other.

$backdrop:      #1b171d;
$table:         #2a242b;
$tray:          #965f47;
$tray-light:    #b47b5c;
$tray-dark:     #69402e;

$sand:          #c7c4a9;
$sand-dim:      #bdb99d;

$snake:         #2e6f43;
$snake-light:   #4a9252;
$apple:         #e03d31;
$leaf:          #6fa145;

// Text sits on dark panels, so it is the sand tone rather than pure white - white on warm
// brown reads as clinical, and nothing in this game should.
$text:          #f2ede2;
$text-soft:     rgba(242, 237, 226, 0.66);
$text-faint:    rgba(242, 237, 226, 0.34);

// Panels are the backdrop hue, near-opaque so a card reads as the room dimming without the
// playfield showing through what sits on it. 0.90 was not enough: the sand is the brightest
// thing in the game, and the checker was visible straight through the buttons.
$panel:         rgba(27, 23, 29, 0.985);
$hairline:      rgba(242, 237, 226, 0.10);

// Controls sit *on* a card, so they tint it rather than being transparent themselves - the
// card behind them is already opaque, which is what keeps the world out of them.
$plate:         rgba(56, 50, 58, 1);
$plate-hover:   rgba(74, 66, 76, 1);

// ---------------------------------------------------------------- rhythm

// The spacing unit, and the one rule about it: **never write `$u * n`**. s&box's SCSS does not
// evaluate arithmetic in lengths and drops the whole declaration - with a warning for `gap` and
// in complete silence for margins and padding. The entire spacing scale of this interface was
// resolving to nothing for exactly that reason, which is why every space below is written as a
// literal multiple of 4. The unit is kept as documentation of the rhythm, not as a calculator.
$u: 4px;

$radius: 14px;
$radius-lg: 26px;

// One curve for everything. A settle with a touch of overshoot reads as soft rather than
// mechanical, which is the whole target for this game's feel.
$ease: cubic-bezier(0.22, 1, 0.36, 1);

// ---------------------------------------------------------------- type

// Poppins is a geometric, round-shouldered face that ships with s&box. It is doing real work
// here: the roundness is the same idea as the sphere-based snake.
@mixin face {
	font-family: "Poppins", "Roboto", sans-serif;
	font-weight: 500;
}

@mixin label {
	@include face;
	font-size: 13px;
	font-weight: 600;
	letter-spacing: 3px;
	text-transform: uppercase;
	color: $text-faint;
}

@mixin numeral {
	@include face;
	font-weight: 700;

	// Numbers are tabular so a score counting up does not shift its own layout. A number
	// that jitters while it changes is the most avoidable kind of unpolished.
	font-family: "Poppins", "Roboto", sans-serif;
}

@mixin card {
	background-color: $panel;
	border-radius: $radius-lg;
	border: 1px solid $hairline;
	flex-direction: column;
	align-items: center;

	// Each side written out rather than a `padding: A B` shorthand. s&box's SCSS applies the
	// shorthand only partly - the card came out with a correct top and no bottom, so the last
	// button on every screen sat flush against the rounded corner.
	padding-top: 48px;
	padding-bottom: 48px;
	padding-left: 56px;
	padding-right: 56px;

	// Without this the centring scrim compresses the card to less than its content.
	flex-shrink: 0;
}

// ---------------------------------------------------------------- screen chrome
// Shared by every full-screen state - the main menu, pause, game over and settings all sit
// in a scrim over the arena and rise in on the same card. One treatment for "a screen is up"
// is what makes the four of them read as one system rather than four separate designs.

@keyframes scrim-in {
	from { opacity: 0; }
	to { opacity: 1; }
}

@keyframes card-in {
	0% {
		opacity: 0;
		transform: translateY(18px) scale(0.94);
	}

	100% {
		opacity: 1;
		transform: translateY(0px) scale(1);
	}
}

.scrim {
	position: absolute;
	width: 100%;
	height: 100%;
	align-items: center;
	justify-content: center;
	pointer-events: all;

	// Warm rather than neutral black, so a menu or a pause reads as the lights dimming in
	// the same room rather than a grey sheet dropped over the game.
	background-color: rgba(27, 23, 29, 0.62);

	// Fades up rather than cutting in. A hard cut to a dimmed screen reads as a glitch;
	// over a sixth of a second it reads as the room dimming.
	animation: scrim-in 0.18s $ease both;
}

.card {
	@include card;
	min-width: 460px;
	animation: card-in 0.26s $ease both;

	// A record-beating run gets an apple-red edge. Restating the good news in a second
	// channel, so it lands even if the words are skimmed.
	&.record {
		border: 2px solid $apple;
	}

	.wordmark {
		@include face;
		font-size: 62px;
		font-weight: 700;
		letter-spacing: -1px;
		color: $snake-light;
	}

	.tagline {
		font-size: 19px;
		color: $text-soft;
		margin-top: 8px;
	}

	.heading {
		@include face;
		font-size: 46px;
		font-weight: 700;
		letter-spacing: -0.5px;
		color: $text;
	}

	.cause {
		font-size: 19px;
		color: $text-soft;
		margin-top: 8px;
		text-align: center;
	}

	.tally {
		flex-direction: row;
		margin-top: 28px;
		gap: 40px;

		.tally-item {
			flex-direction: column;
			align-items: center;

			.tally-label { @include label; }

			.tally-value {
				@include numeral;
				font-size: 32px;
				color: $text;
			}
		}
	}

	.prompt {
		@include label;
		font-size: 14px;
		letter-spacing: 2px;
		color: $text-faint;
		margin-top: 24px;
	}
}

// ---------------------------------------------------------------- buttons
// One control, three intents. Ghost is the default - a low-key rectangle that only asserts
// itself on hover - primary is the one action on a screen the player most likely wants, and
// danger is reserved for anything that throws a run away.

.menu {
	flex-direction: column;
	align-items: stretch;
	width: 320px;
	margin-top: 40px;
	gap: 12px;
}

.menu-row {
	flex-direction: row;
	align-items: stretch;
	width: 320px;

	// Same standoff as a stacked menu, so a screen ending in a row of buttons has the same
	// rhythm as one ending in a column of them.
	margin-top: 40px;
	gap: 12px;

	.btn { flex-grow: 1; }
}

.btn {
	@include face;

	// Sides written out - see the note on the card mixin about the padding shorthand.
	padding-top: 14px;
	padding-bottom: 14px;
	padding-left: 24px;
	padding-right: 24px;

	border-radius: $radius;
	border: 1px solid $hairline;
	background-color: $plate;
	color: $text;
	font-size: 17px;
	font-weight: 600;
	text-align: center;
	justify-content: center;
	align-items: center;
	pointer-events: all;
	cursor: pointer;

	transition: background-color 0.12s $ease, border-color 0.12s $ease,
		color 0.12s $ease, transform 0.12s $ease;
	transform: scale(1);

	&:hover {
		background-color: $plate-hover;
		border-color: rgba(242, 237, 226, 0.24);
		transform: scale(1.03);
	}

	// Click feedback: a quick dip below rest, so a press always reads as a press even on a
	// hover state that looks similar at a glance.
	&:active {
		transform: scale(0.97);
		transition-duration: 0.05s;
	}

	&.primary {
		background-color: $apple;
		border-color: $apple;
		color: $text;

		&:hover { background-color: #ff5647; transform: scale(1.03); }
	}

	&.danger {
		&:hover { border-color: $apple; color: $apple; }
	}

	&.small {
		padding-top: 8px;
		padding-bottom: 8px;
		padding-left: 16px;
		padding-right: 16px;
		font-size: 14px;
	}
}

// ---------------------------------------------------------------- settings controls

.setting-row {
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
	width: 100%;
	padding-top: 14px;
	padding-bottom: 14px;
	padding-left: 20px;
	padding-right: 20px;
	border-radius: $radius;
	background-color: $plate;
	border: 1px solid transparent;
	margin-bottom: 8px;
	transition: border-color 0.12s $ease, background-color 0.12s $ease;

	.setting-name {
		@include face;
		font-size: 16px;
		color: $text;

		// Fixed and non-shrinking, so the longest label sets the column and no row wraps its
		// name onto a second line while its neighbours stay on one.
		width: 120px;
		flex-shrink: 0;
	}
}

.stepper {
	flex-direction: row;
	align-items: center;
	gap: 12px;

	.step {
		width: 30px;
		height: 30px;
		border-radius: 999px;
		background-color: rgba(242, 237, 226, 0.08);
		border: 1px solid $hairline;
		color: $text;
		font-size: 17px;
		font-weight: 700;
		text-align: center;
		justify-content: center;
		align-items: center;
		pointer-events: all;
		cursor: pointer;
		transition: background-color 0.1s $ease, border-color 0.1s $ease, transform 0.1s $ease;

		&:hover { background-color: rgba(242, 237, 226, 0.16); border-color: $apple; }
		&:active { transform: scale(0.9); }
	}

	// Ten pips rather than a continuous bar. A single filled element sized from an inline
	// `style` never drew - the attribute did not reach the panel at all - and pips are the
	// better control anyway: one pip is exactly one tap of the stepper beside it, so the meter
	// shows what a click will do rather than only where the value currently sits.
	.meter {
		flex-direction: row;
		flex-shrink: 0;
		align-items: center;
		gap: 3px;

		.pip {
			width: 8px;
			height: 10px;
			flex-shrink: 0;
			border-radius: 3px;
			background-color: rgba(242, 237, 226, 0.14);
			transition: background-color 0.12s $ease;

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

	.setting-value {
		@include numeral;
		font-size: 14px;
		color: $text-soft;
		min-width: 38px;
		text-align: center;
		justify-content: center;
	}
}

// ---------------------------------------------------------------- records readout

.records {
	flex-direction: column;
	align-items: stretch;
	width: 320px;
	margin-top: 28px;
	padding-top: 18px;
	padding-bottom: 18px;
	padding-left: 22px;
	padding-right: 22px;
	background-color: $plate;
	border-radius: $radius;

	.records-title { @include label; margin-bottom: 12px; }

	.record-row {
		flex-direction: row;
		justify-content: space-between;
		padding-top: 4px;
		padding-bottom: 4px;
		font-size: 15px;
		color: $text-soft;

		span:last-child { @include numeral; color: $text; }
	}
}