/*
 * Chrome, layout and every block rule.
 *
 * This is the half of the colour work a stylesheet can express. The other half --
 * "this cell is filled in colour 4" -- is set per cell from js/ui/palette.js as
 * the --block custom property, and every block rule below derives from it. One
 * gradient recipe therefore serves all eight colours, and the palette can change
 * without a stylesheet edit.
 *
 * Near-black indigo rather than grey: the blocks are the only saturated thing on
 * the screen, and they only read as lit if nothing else is competing.
 */

:root {
	--ground: #0e1226;
	--panel: #191f3d;
	--panel-edge: #262e56;
	--slot: #222a4e; /* an empty cell */

	--ink: #eef1ff;
	--ink-soft: #98a1cf;
	--ink-faint: #5c6597;

	--accent: #7c8cff;
	--accent-deep: #5566e8;
	--gold: #ffd76a; /* the high score, and a new record */

	--radius-block: 24%;
	--radius-control: 14px;
	--radius-card: 22px;

	/*
	 * Board metrics, published by BoardView from one measurement of the grid.
	 * Cell corners, tray scale, the dragged piece's size and the drop maths all
	 * come off these three numbers rather than each measuring for itself.
	 */
	--cell: 36px;
	--gap: 4px;
	--step: 40px; /* --cell plus --gap, the pitch from one cell to the next */

	--tray-scale: 0.5;
	--tray-size: 90px;

	color-scheme: dark;
}

* {
	box-sizing: border-box;
}

html,
body {
	/* The dynamic viewport, with the percentage left in for browsers that do not
	   know dvh: a toolbar that collapses and reappears must not leave the tray
	   under the fold. */
	height: 100%;
	height: 100dvh;
	margin: 0;
	/* The app is always one screen, never a document. Nothing scrolls; whatever
	   cannot fit gives up space instead. */
	overflow: hidden;
}

body {
	background: var(--ground);
	color: var(--ink);
	font-family: ui-rounded, "SF Pro Rounded", "Segoe UI Variable Display", system-ui,
		-apple-system, "Helvetica Neue", sans-serif;
	font-size: clamp(0.95rem, 3.4vw, 1.1rem);
	line-height: 1.35;
	/* No rubber-banding and no double-tap zoom: this is a board you drag across,
	   not a document you scroll. */
	overscroll-behavior: none;
	touch-action: manipulation;
	-webkit-text-size-adjust: 100%;
	/* Nothing on screen is text to read and copy. The card that ends a level
	   opens right under the finger, and a press that lands beside its text and
	   drifts becomes a selection: the button lights up, the gesture is taken away
	   from it, and the press is never delivered. */
	-webkit-user-select: none;
	user-select: none;
	-webkit-touch-callout: none;
}

#app {
	position: relative;
	width: 100%;
	/* A phone's design width on a desktop screen. Wider than this and the board
	   grows past any reasonable reach without getting easier to read. */
	max-width: 26rem;
	height: 100%;
	margin-inline: auto;
}

.screen {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	gap: 0.75rem;
	padding: max(0.75rem, env(safe-area-inset-top)) 0.75rem
		max(0.75rem, env(safe-area-inset-bottom));
	/* Nothing scrolls; whatever cannot fit gives up space instead. */
	overflow: hidden;
}

.screen[hidden] {
	display: none;
}

.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* --- Menu ----------------------------------------------------------------- */

.menu {
	/* Centred, but not past the top edge: a stack too tall for the screen would
	   otherwise hang off both ends and take the play button with it. Browsers
	   without `safe` keep the line above. */
	justify-content: center;
	justify-content: safe center;
	gap: 0.625rem;
	padding-inline: 1.5rem;
	text-align: center;
}

/* Three pieces on a seven-wide grid, at a size that reads as a mark rather than
   a board. */
.mark {
	display: grid;
	grid-template-columns: repeat(7, 1.4rem);
	grid-template-rows: repeat(3, 1.4rem);
	gap: 0.2rem;
	margin: 0 auto 0.5rem;
}

.mark .block {
	position: static;
	opacity: 1;
}

.menu h1 {
	margin: 0;
	font-size: clamp(2.25rem, 10vw, 3rem);
	font-weight: 800;
	line-height: 1;
	letter-spacing: -0.02em;
}

.subtitle {
	margin: 0 0 0.5rem;
	color: var(--ink-soft);
	text-wrap: balance;
}

.menu .primary {
	width: 100%;
}

.menu > button:not(.primary) {
	min-height: 2.75rem;
	padding: 0.35rem 1rem;
}

/* Three ways onto the ladder, three across a phone. */
.start-at {
	margin-top: 0.375rem;
}

.start-at h2,
.stats h2 {
	margin: 0 0 0.375rem;
	color: var(--ink-faint);
	font-size: 0.7em;
	font-weight: 700;
	letter-spacing: 0.09em;
	text-transform: uppercase;
}

.difficulty {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 0.5rem;
}

.difficulty button {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 0.1rem;
	min-height: 3.75rem;
	padding: 0.5rem 0.375rem;
}

/*
 * Two lines under the name rather than one wrapping line: at three buttons to a
 * phone's width, "Level 26 · 15 lines" breaks wherever it likes and leaves the
 * separator stranded. Both inherit their colour, so they follow the button
 * through hover and press.
 */
.difficulty .level {
	font-size: 0.8em;
	font-weight: 500;
	line-height: 1.2;
	opacity: 0.85;
}

.difficulty .lines {
	font-size: 0.7em;
	font-weight: 500;
	line-height: 1.2;
	opacity: 0.6;
}

.stats {
	margin-top: 0.5rem;
	color: var(--ink-soft);
	font-size: 0.9em;
}

.stats p {
	margin: 0.15rem 0;
}

/* --- Score bar ------------------------------------------------------------ */

.hud {
	display: grid;
	grid-template-columns: 1fr auto 1fr;
	align-items: center;
	gap: 0.5rem;
}

.hud-side {
	display: flex;
	flex-direction: column;
	gap: 0.1rem;
	min-width: 0;
}

.hud-end {
	align-items: flex-end;
}

.hud-label {
	color: var(--ink-faint);
	font-size: 0.7em;
	font-weight: 700;
	letter-spacing: 0.09em;
	text-transform: uppercase;
}

.hud-value {
	color: var(--gold);
	font-weight: 700;
	font-variant-numeric: tabular-nums;
}

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

.hud-score .hud-label {
	color: var(--ink-soft);
}

.score {
	font-size: clamp(2rem, 10vw, 2.75rem);
	font-weight: 800;
	line-height: 1;
	letter-spacing: -0.02em;
	font-variant-numeric: tabular-nums;
}

/* A score that jumps is easier to feel than one that merely changes. */
.score.is-bumped {
	animation: bump 260ms cubic-bezier(0.25, 1.6, 0.4, 1);
}

@keyframes bump {
	0% { scale: 1; }
	40% { scale: 1.16; }
	100% { scale: 1; }
}

.combo {
	padding: 0.1rem 0.5rem;
	border-radius: 999px;
	background: var(--accent-deep);
	color: var(--ink);
	font-size: 0.7em;
	font-weight: 700;
	letter-spacing: 0.04em;
}

.combo[hidden] {
	display: none;
}

/* --- The goal ------------------------------------------------------------- */

.goal {
	display: grid;
	grid-template-columns: auto 1fr auto;
	align-items: center;
	gap: 0.75rem;
}

.goal-side {
	display: flex;
	flex-direction: column;
	gap: 0.1rem;
}

.goal-end {
	align-items: flex-end;
}

/* Fills left to right as lines go, so the objective reads without the numbers. */
.goal-bar {
	position: relative;
	height: 0.4rem;
	overflow: hidden;
	border-radius: 999px;
	background: var(--slot);
}

.goal-bar::after {
	content: "";
	position: absolute;
	inset: 0;
	width: calc(var(--done, 0) * 100%);
	border-radius: inherit;
	background: linear-gradient(90deg, var(--accent-deep), var(--accent));
	transition: width 260ms ease-out;
}

/* --- Controls ------------------------------------------------------------- */

button {
	border: 2px solid var(--panel-edge);
	border-radius: var(--radius-control);
	background: var(--panel);
	color: var(--ink);
	font: inherit;
	font-weight: 600;
	cursor: pointer;
	/* A thumb tap drifts a few pixels. Panning is allowed further up the page, so
	   without this the browser reads that drift as a pan: it fires pointercancel
	   and never fires the click, and the press is silently lost. */
	touch-action: none;
	-webkit-user-select: none;
	user-select: none;
	transition: background-color 90ms ease-out, border-color 90ms ease-out;
}

button.primary {
	min-height: 3.5rem;
	padding: 0.5rem 1.25rem;
	border-color: var(--accent-deep);
	background: var(--accent);
	color: #10142c;
	font-size: 1.05em;
	font-weight: 800;
}

.icon-button {
	display: grid;
	place-items: center;
	width: 2.75rem;
	height: 2.75rem;
	padding: 0;
}

.icon-button svg {
	width: 1.25rem;
	height: 1.25rem;
	fill: none;
	stroke: var(--ink-soft);
	stroke-width: 2;
	stroke-linecap: round;
	stroke-linejoin: round;
}

@media (hover: hover) {
	button:hover {
		background: #222a52;
		border-color: #35407a;
	}

	button.primary:hover {
		background: #8f9dff;
		border-color: #8f9dff;
	}

	.icon-button:hover svg {
		stroke: var(--ink);
	}
}

button:active {
	scale: 0.96;
}

button:focus-visible {
	outline: none;
	border-color: var(--accent);
	box-shadow: 0 0 0 3px rgb(124 140 255 / 30%);
}

/* --- The board ------------------------------------------------------------ */

.board-wrap {
	position: relative;
	/* Centres the board in whatever is left between the score and the tray, so
	   the two of them stay pinned to the edges the thumbs reach. */
	margin-block: auto;
	/*
	 * The board is as tall as it is wide, so on a short viewport -- a phone turned
	 * sideways, a browser with its toolbars open on a small screen -- it is the
	 * width that has to give. The subtraction is roughly the score bar, the goal
	 * row, the hint and the paddings; the factor leaves room for the tray, which
	 * scales with the board. Portrait phones never reach the cap.
	 */
	width: min(100%, calc((100vh - 13rem) * 0.72));
	width: min(100%, calc((100dvh - 13rem) * 0.72));
	align-self: center;
	padding: 0.5rem;
	border: 1px solid var(--panel-edge);
	border-radius: 20px;
	background: var(--panel);
}

.board {
	display: grid;
	grid-template-columns: repeat(8, 1fr);
	grid-template-rows: repeat(8, 1fr);
	gap: var(--gap);
	width: 100%;
	aspect-ratio: 1;
	/* The gesture owns the pointer outright. Without this the browser claims a
	   drag as a scroll and the piece never leaves the tray. */
	touch-action: none;
	-webkit-user-select: none;
	user-select: none;
	-webkit-tap-highlight-color: transparent;
	-webkit-touch-callout: none;
}

.cell {
	position: relative;
	border-radius: var(--radius-block);
	background: var(--slot);
	transition: background-color 110ms ease-out, box-shadow 110ms ease-out;
}

/*
 * The block itself, one per cell, always present and hidden until the cell is
 * filled. Keeping the element alive means placing and clearing are transitions
 * on something already in the tree rather than a node being added and removed
 * mid-animation.
 */
.block {
	position: absolute;
	inset: 0;
	border-radius: var(--radius-block);
	background: linear-gradient(
		180deg,
		color-mix(in srgb, white 26%, var(--block, #888)) 0%,
		var(--block, #888) 52%,
		color-mix(in srgb, black 20%, var(--block, #888)) 100%
	);
	box-shadow:
		inset 0 8% 0 rgb(255 255 255 / 40%),
		inset 0 -12% 0 rgb(0 0 0 / 18%);
	opacity: 0;
	pointer-events: none;
}

.cell.is-filled .block {
	opacity: 1;
}

/* Only a cell the player has just filled drops in. A repaint of the same board
   -- a resume, a resize -- must not replay every block on it. */
.cell.is-new .block {
	animation: settle 190ms cubic-bezier(0.2, 1.5, 0.35, 1);
}

@keyframes settle {
	from { scale: 0.55; opacity: 0.35; }
	to { scale: 1; opacity: 1; }
}

/*
 * Where the held piece would land. The ghost tints the empty cell rather than
 * drawing a translucent block in it: a half-opacity block next to a real one
 * reads as a block of a different colour, which is exactly the wrong thing to
 * say about a square that is still empty.
 */
.cell.is-ghost {
	background: color-mix(in srgb, var(--ghost, var(--accent)) 30%, var(--slot));
	box-shadow: inset 0 0 0 max(2px, calc(var(--cell) * 0.07))
		color-mix(in srgb, var(--ghost, var(--accent)) 65%, transparent);
}

/*
 * A line the drop would complete, called out before the finger lifts. This is
 * the whole planning loop of the game, so it gets the loudest hint on the board.
 */
.cell.is-doomed {
	box-shadow: inset 0 0 0 max(2px, calc(var(--cell) * 0.07)) var(--gold);
}

/* The ring again, this time on the block, because the cell's own is behind it and
   half the cells in a completing line are already full. */
.cell.is-doomed .block {
	filter: brightness(1.25) saturate(1.05);
	box-shadow:
		inset 0 0 0 max(2px, calc(var(--cell) * 0.07)) var(--gold),
		inset 0 8% 0 rgb(255 255 255 / 40%),
		inset 0 -12% 0 rgb(0 0 0 / 18%);
}

.cell.is-clearing {
	background: var(--slot);
}

/* Kept visible past the model, which has already emptied these cells. */
.cell.is-clearing .block {
	opacity: 1;
	animation: burst 300ms ease-out forwards;
}

@keyframes burst {
	0% { scale: 1; filter: brightness(1); }
	30% { scale: 1.16; filter: brightness(2.1) saturate(0.7); }
	100% { scale: 0.15; opacity: 0; filter: brightness(2.4); }
}

/* --- The points that float off a clear ----------------------------------- */

.flash {
	position: absolute;
	left: 50%;
	top: 44%;
	z-index: 4;
	margin: 0;
	translate: -50% -50%;
	color: var(--gold);
	font-size: clamp(1.4rem, 7vw, 1.9rem);
	font-weight: 800;
	line-height: 1.1;
	text-align: center;
	text-shadow: 0 2px 12px rgb(14 18 38 / 90%), 0 0 3px rgb(14 18 38);
	opacity: 0;
	pointer-events: none;
}

.flash.is-shown {
	animation: rise 950ms ease-out;
}

.flash .flash-note {
	display: block;
	color: var(--ink);
	font-size: 0.62em;
	letter-spacing: 0.02em;
}

@keyframes rise {
	0% { opacity: 0; scale: 0.7; translate: -50% -30%; }
	22% { opacity: 1; scale: 1.06; }
	70% { opacity: 1; scale: 1; translate: -50% -60%; }
	100% { opacity: 0; scale: 1; translate: -50% -95%; }
}

/* --- The tray ------------------------------------------------------------- */

.tray-area {
	display: flex;
	flex-direction: column;
	gap: 0.4rem;
}

.tray {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	align-items: center;
	gap: 0.4rem;
	touch-action: none;
	-webkit-user-select: none;
	user-select: none;
	-webkit-tap-highlight-color: transparent;
	-webkit-touch-callout: none;
}

.slot {
	display: grid;
	place-items: center;
	height: var(--tray-size);
}

/*
 * A piece is laid out at tray size -- every board metric multiplied by the tray
 * scale -- and grows to board size by the inverse scale when lifted. Laying it
 * out at board size and shrinking it visually would leave a layout box up to
 * five cells wide sitting in a slot a third of the tray wide, and a box that
 * pokes past the viewport edge makes the whole page scroll by a few pixels.
 */
.piece {
	display: grid;
	gap: calc(var(--gap) * var(--tray-scale));
	cursor: grab;
	transition: scale 130ms ease-out, opacity 150ms ease-out;
}

.piece .block {
	position: static;
	opacity: 1;
}

.piece.is-dragging {
	z-index: 50;
	scale: calc(1 / var(--tray-scale));
	cursor: grabbing;
	filter: drop-shadow(0 10px 16px rgb(0 0 0 / 45%));
}

/* Spring back to the tray when the drop had nowhere to go. */
.piece.is-returning {
	transition: translate 190ms cubic-bezier(0.2, 0.8, 0.3, 1), scale 190ms ease-out;
}

/* Nowhere left on the board for this one. Dimmed rather than removed: it is
   still the piece you have, and it is why the run is about to end. */
.piece.is-stuck {
	opacity: 0.3;
}

/* The keyboard's idea of "held". Nothing follows a cursor, so the piece has to
   say by itself that it is the one the arrow keys are aiming. */
.piece.is-selected {
	filter: drop-shadow(0 0 7px var(--accent)) drop-shadow(0 0 2px var(--accent));
}

.hint {
	margin: 0;
	min-height: 2.4em;
	color: var(--ink-faint);
	font-size: 0.78em;
	text-align: center;
	text-wrap: balance;
	transition: opacity 400ms ease-out;
}

.hint.is-hidden {
	opacity: 0;
}

/* --- Game over ------------------------------------------------------------ */

.overlay {
	position: absolute;
	inset: 0;
	z-index: 100;
	display: grid;
	place-items: center;
	padding: 1.25rem;
	background: rgb(9 12 26 / 78%);
	animation: fade-in 220ms ease-out;
}

.overlay[hidden] {
	display: none;
}

/* The boot panel: the ground itself, over both screens until the save is read. */
.overlay.solid {
	background: var(--ground);
	color: var(--ink-soft);
	animation: none;
}

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

.card {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 1rem;
	width: 100%;
	padding: 2rem 1.5rem;
	border: 1px solid var(--panel-edge);
	border-radius: var(--radius-card);
	background: var(--panel);
	text-align: center;
	animation: lift-in 260ms cubic-bezier(0.2, 1.3, 0.4, 1);
}

@keyframes lift-in {
	from { opacity: 0; translate: 0 14px; scale: 0.96; }
	to { opacity: 1; translate: 0 0; scale: 1; }
}

.card h2 {
	margin: 0;
	font-size: clamp(1.5rem, 7vw, 2rem);
	font-weight: 800;
}

.final {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.15rem;
	margin: 0;
}

.final-score {
	font-size: clamp(2.5rem, 13vw, 3.5rem);
	font-weight: 800;
	line-height: 1;
	font-variant-numeric: tabular-nums;
}

.detail {
	margin: 0;
	color: var(--ink-soft);
	font-size: 0.85em;
	text-wrap: balance;
}

.detail .record {
	display: block;
	margin-bottom: 0.2rem;
	color: var(--gold);
	font-weight: 700;
	font-size: 1.1em;
}

.card .primary {
	width: 100%;
}

.card .quiet {
	min-height: 2.75rem;
	padding: 0.35rem 1rem;
	border-color: transparent;
	background: none;
	color: var(--ink-soft);
	font-weight: 600;
}

.card .quiet[hidden] {
	display: none;
}

/*
 * A landscape phone, or a short desktop window. The mark and the tag line go,
 * and the headings with them: three buttons reading Easy, Medium and Hard say
 * what they are, and the difficulty group keeps its label for a screen reader.
 * Standalone installs are locked to portrait, so this is a browser tab.
 */
@media (max-height: 34rem) {
	.menu .mark,
	.menu .subtitle,
	.start-at h2,
	.stats h2 {
		display: none;
	}

	.menu {
		gap: 0.375rem;
	}

	.menu h1 {
		font-size: 1.75rem;
	}
}

/*
 * Motion here is feedback, not decoration -- but a burst that scales to nothing
 * is a poor way to tell someone their line cleared if motion makes them ill.
 * Everything animated is also stated by a class change or a number, so turning
 * the animation off loses the flourish and none of the information.
 */
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 1ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 1ms !important;
	}
}
