/*
 * Chrome, layout, and the cubes.
 *
 * Two things here are not ordinary styling and are worth knowing about before
 * reading the rest.
 *
 * The stack is real CSS 3D. `.stage` holds the perspective, `.scene` carries the
 * camera rotation, and every cube is six absolutely positioned faces pushed half
 * a cube out along their own axis. The browser sorts them; nothing in JavaScript
 * draws a pixel. What JavaScript does own is `--size` and the scene's rotation,
 * and ui/camera.js reproduces exactly the same projection in numbers so that a
 * tap can be matched to the cube under it.
 *
 * The icons are drawings, not text. Each one is a data URI on `--glyph`, set by
 * an `.icon-*` class alongside the `--tint` that goes with it, so a cube face and
 * a tray tile are the same two custom properties on different boxes. They are
 * original shapes chosen to differ in silhouette as well as in hue -- a stack
 * seen at cube size is a poor place to rely on colour alone.
 */

:root {
	--void: #10152a; /* the page behind everything */
	--deep: #171e38; /* panels and the tray */
	--rise: #212b4e; /* buttons */
	--edge: #303c6d;
	--edge-hover: #3c4a83;

	--ink: #eaeefc;
	--ink-soft: #97a2c9; /* secondary labels */
	--ink-faint: #58628c; /* disabled button text */

	--accent: #64d2ff;
	--accent-deep: #2b9fd6;
	--warn: #ff9772;

	--slot-gap: 0.3rem; /* tray padding, and the space between its sockets */

	--radius-control: 13px;
	--radius-card: 22px;

	/* Cube edge, in pixels. Published by StackView from one measurement of the
	   stage, so face size, tray tiles and corner rounding all follow the same
	   number rather than each recomputing it. */
	--size: 48px;

	color-scheme: dark;
}

* {
	box-sizing: border-box;
}

html,
body {
	height: 100%;
	margin: 0;
}

body {
	background: radial-gradient(120% 90% at 50% 0%, #1c2544 0%, var(--void) 62%) fixed;
	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.4vmin, 1.1rem);
	line-height: 1.35;
	/* No rubber-banding and no double-tap zoom: this is an object you turn, not a
	   document you scroll. */
	overscroll-behavior: none;
	touch-action: manipulation;
	-webkit-text-size-adjust: 100%;
}

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

.screen {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	padding: max(0.875rem, env(safe-area-inset-top)) 0.875rem
		max(0.875rem, env(safe-area-inset-bottom));
}

.screen[hidden] {
	display: none;
}

/*
 * The mascot's definition, which is markup rather than a picture.
 *
 * Collapsed to nothing rather than hidden. An SVG root that is not displayed
 * still claims its default 300x150 box in some engines, and a stray 150 pixels at
 * the top of the document pushes the whole app off the bottom of the screen.
 */
.sprite {
	position: absolute;
	width: 0;
	height: 0;
	overflow: hidden;
}

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

button {
	min-height: 3.25rem;
	padding: 0.5rem 0.875rem;
	border: 2px solid var(--edge);
	border-radius: var(--radius-control);
	background: var(--rise);
	color: var(--ink);
	font: inherit;
	font-weight: 600;
	cursor: pointer;
	-webkit-user-select: none;
	user-select: none;
	transition: background-color 90ms ease-out, border-color 90ms ease-out;
}

button.primary {
	min-height: 3.75rem;
	font-size: 1.1em;
	border-color: var(--accent-deep);
	background: linear-gradient(180deg, #2f7fa8, #22608a);
}

@media (hover: hover) {
	button:hover:not(:disabled) {
		background: #2a3560;
		border-color: var(--edge-hover);
	}

	button.primary:hover:not(:disabled) {
		background: linear-gradient(180deg, #3b93bd, #2a6f9c);
		border-color: var(--accent);
	}
}

button:active:not(:disabled) {
	border-color: var(--accent);
	translate: 0 1px;
}

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

button:disabled {
	background: #1a2140;
	border-color: #232c52;
	color: var(--ink-faint);
	cursor: default;
}

button[hidden] {
	display: none;
}

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

.menu {
	/* Centred, but not past the top edge: a column 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;
	text-align: center;
	padding-inline: 1.625rem;
}

.mascot {
	display: block;
	width: 100%;
	max-width: 17rem;
	max-height: 34vh;
	height: auto;
	margin-inline: auto;
	/* Without this a flex item is never squeezed below its own height. The art is
	   the one thing on the menu that can afford to give. */
	min-height: 0;
}

.menu h1 {
	margin: 0;
	font-size: clamp(2rem, 9vw, 2.7rem);
	font-weight: 700;
	letter-spacing: -0.01em;
}

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

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

.stats h2 {
	margin: 0 0 0.25rem;
	color: var(--ink);
	font-size: 1em;
	font-weight: 700;
}

.stats p {
	margin: 0.15rem 0;
}

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

.start-at h2 {
	margin: 0 0 0.375rem;
	font-size: 1em;
	font-weight: 700;
}

.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: 3rem;
	padding-inline: 0.375rem;
}

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

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

/*
 * A landscape phone, or a short desktop window. The art has shrunk to nothing by
 * this height anyway. The tag line and both headings go with it: three buttons
 * reading Easy, Medium and Hard say what they are, the progress lines say what
 * they are, and the difficulty group keeps its label for a screen reader. Without
 * this the column runs past the bottom of a landscape phone.
 */
@media (max-height: 34rem) {
	.menu .mascot,
	.menu .subtitle,
	.start-at h2,
	.stats h2 {
		display: none;
	}

	.menu {
		gap: 0.375rem;
	}

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

	.stats {
		margin-top: 0;
	}
}

/* --- Game screen --------------------------------------------------------- */

.game {
	/* Room for the anchored Menu button, so the stage can never ride up
	   underneath it on a short screen. */
	padding-top: calc(max(0.875rem, env(safe-area-inset-top)) + 3.5rem);
	gap: 0.5rem;
}

/* Anchored rather than stacked: a way out belongs at the top of the screen, not
   travelling up and down with the play area. */
.corner {
	position: absolute;
	top: max(0.875rem, env(safe-area-inset-top));
	left: 0.875rem;
	min-height: 2.75rem;
	z-index: 2;
}

.header-row {
	display: flex;
	align-items: baseline;
	gap: 0.625rem;
}

.level {
	flex: 1;
	font-weight: 600;
}

.progress,
.time {
	color: var(--ink-soft);
	font-variant-numeric: tabular-nums;
}

/* --- The stack ----------------------------------------------------------- */

/*
 * The stage owns the perspective and the whole orbit gesture. It takes all the
 * height left over once the header and the bottom bar have had theirs, which is
 * what keeps the tray pinned to the thumb on a tall screen and a short one alike.
 */
.stage {
	position: relative;
	flex: 1;
	min-height: 0;
	perspective: 1800px;
	/* The gesture owns the pointer outright. Without this the browser claims a
	   drag as a scroll and the stack stops turning after a few pixels. */
	touch-action: none;
	-webkit-user-select: none;
	user-select: none;
	-webkit-tap-highlight-color: transparent;
	-webkit-touch-callout: none;
	cursor: grab;
}

.stage.is-turning {
	cursor: grabbing;
}

/*
 * A point at the middle of the stage, not a box. The cubes place themselves
 * around the scene's origin, so it needs no size -- and a stage-sized box turned
 * in 3D would poke its invisible corners out past the edge of the screen and give
 * the page something to scroll.
 */
.scene {
	position: absolute;
	top: 50%;
	left: 50%;
	width: 0;
	height: 0;
	transform-style: preserve-3d;
}

.cube {
	position: absolute;
	top: 50%;
	left: 50%;
	width: var(--size);
	height: var(--size);
	margin-top: calc(var(--size) / -2);
	margin-left: calc(var(--size) / -2);
	transform-style: preserve-3d;
}

.cube.is-leaving {
	transition: transform 200ms ease-in;
}

.face {
	position: absolute;
	inset: 0;
	border-radius: 11%;
	background-color: var(--tint);
	background-image: var(--glyph);
	background-position: center;
	background-repeat: no-repeat;
	background-size: 54%;
	/* The inner ring draws the cube's edges; the flood behind it is the face's
	   share of the light. Shadows paint in order, so the ring goes first. */
	box-shadow:
		inset 0 0 0 max(1px, calc(var(--size) * 0.035)) var(--ring, rgb(255 255 255 / 22%)),
		inset 0 0 0 400px var(--shade, transparent);
	backface-visibility: hidden;
}

.face-up {
	--shade: rgb(255 255 255 / 20%);
	transform: rotateX(90deg) translateZ(calc(var(--size) / 2));
}

.face-down {
	--shade: rgb(6 10 26 / 55%);
	transform: rotateX(-90deg) translateZ(calc(var(--size) / 2));
}

.face-south {
	--shade: rgb(6 10 26 / 8%);
	transform: translateZ(calc(var(--size) / 2));
}

.face-north {
	--shade: rgb(6 10 26 / 8%);
	transform: rotateY(180deg) translateZ(calc(var(--size) / 2));
}

.face-east {
	--shade: rgb(6 10 26 / 30%);
	transform: rotateY(90deg) translateZ(calc(var(--size) / 2));
}

.face-west {
	--shade: rgb(6 10 26 / 30%);
	transform: rotateY(-90deg) translateZ(calc(var(--size) / 2));
}

/*
 * A cube with no way out from where you are standing.
 *
 * Drained of colour rather than faded: opacity on a preserve-3d box flattens it,
 * and a flattened cube is a smear rather than a cube. Mixing the tint down leaves
 * every face doing its own job and still reads instantly as "not this one".
 */
.cube.is-locked .face {
	background-color: #2a3352;
	background-color: color-mix(in srgb, var(--tint) 26%, #131a33);
	--ring: rgb(255 255 255 / 9%);
}

.cube.is-selected .face {
	--ring: var(--accent);
}

/* --- Bottom bar ---------------------------------------------------------- */

.bottom-bar {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
}

/*
 * Seven slots, always all seven, whether or not there is anything in them. The
 * empty ones are the warning: a row that grows tells you what you are holding,
 * a row of sockets tells you how much room is left, and that is the number the
 * whole game turns on.
 *
 * The sockets are a flex row of squares, so the tray takes its height from them
 * rather than being told one. Tiles sit above on their own layer, placed by slot
 * number instead of by document order -- which is what lets a tile slide along
 * when something to its left clears, and lets the three that cleared stay put
 * long enough to be seen going.
 */
.tray {
	position: relative;
	display: flex;
	align-items: center;
	gap: var(--slot-gap);
	padding: var(--slot-gap);
	border: 2px solid var(--edge);
	border-radius: 15px;
	background: var(--deep);
}

.tray.is-crowded {
	border-color: var(--warn);
}

.slot {
	flex: 1;
	aspect-ratio: 1;
	border-radius: 22%;
	background: rgb(8 12 28 / 55%);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 5%);
}

/* Two of the eight gaps are the tray's own padding; the other six are between. */
.tile {
	position: absolute;
	top: var(--slot-gap);
	left: var(--slot-gap);
	width: calc((100% - 8 * var(--slot-gap)) / 7);
	aspect-ratio: 1;
	transform: translateX(calc(var(--index) * (100% + var(--slot-gap))));
	transition: transform 190ms cubic-bezier(0.2, 0.8, 0.3, 1);
}

.tile.is-clearing {
	pointer-events: none;
	animation: tile-out 260ms ease-in forwards;
}

.tile-face {
	width: 100%;
	height: 100%;
	border-radius: 22%;
	background-color: var(--tint);
	background-image: var(--glyph);
	background-position: center;
	background-repeat: no-repeat;
	background-size: 58%;
	box-shadow:
		inset 0 0 0 2px rgb(255 255 255 / 26%),
		inset 0 -0.35rem 0 rgb(6 10 26 / 22%);
	animation: tile-in 180ms ease-out;
}

@keyframes tile-in {
	from {
		transform: scale(0.4);
		opacity: 0;
	}
}

@keyframes tile-out {
	to {
		transform: scale(1.5);
		opacity: 0;
	}
}

.status {
	min-height: 2.5em;
	margin: 0;
	color: var(--accent);
	font-size: 0.85em;
	text-align: center;
	text-wrap: pretty;
}

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

.toolbar button {
	padding-inline: 0.25rem;
}

/* --- Overlays ------------------------------------------------------------ */

.overlay {
	position: absolute;
	inset: 0;
	z-index: 3;
	display: grid;
	place-items: center;
	padding: 1.25rem;
	background: rgb(9 13 28 / 72%);
}

.overlay[hidden] {
	display: none;
}

.card {
	display: flex;
	flex-direction: column;
	gap: 0.75rem;
	width: 100%;
	padding: 1.75rem 1.75rem 1.5rem;
	border: 2px solid var(--edge);
	border-radius: var(--radius-card);
	background: var(--deep);
	text-align: center;
}

.card .mascot {
	max-width: 11rem;
	max-height: 22vh;
}

.card h2 {
	margin: 0;
	font-size: clamp(1.6rem, 7.5vw, 2.2rem);
	font-weight: 700;
}

.card p {
	margin: 0 0 0.5rem;
	color: var(--ink-soft);
}

/* --- The icons ----------------------------------------------------------- */

/*
 * One tint and one drawing per kind. Both are inherited, so anything that wants
 * to show an icon only has to take the class.
 */

.icon-drop {
	--tint: #45c3f0;
	--glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%2316203a' d='M50 6C50 6 80 42 80 62A30 30 0 0 1 20 62C20 42 50 6 50 6Z'/%3E%3C/svg%3E");
}

.icon-leaf {
	--tint: #59cf8c;
	--glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%2316203a' d='M16 84C14 44 44 12 86 14C88 56 56 86 16 84Z'/%3E%3C/svg%3E");
}

.icon-bolt {
	--tint: #f7c948;
	--glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%2316203a' d='M60 4 26 54 46 54 40 96 76 44 54 44Z'/%3E%3C/svg%3E");
}

.icon-star {
	--tint: #f98cb6;
	--glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%2316203a' d='M50 8 60 36.2 89.9 37 66.2 55.3 74.7 84 50 67 25.3 84 33.8 55.3 10.1 37 40 36.2Z'/%3E%3C/svg%3E");
}

.icon-moon {
	--tint: #ae8cf2;
	--glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%2316203a' d='M55 10.3A40 40 0 1 0 55 89.7A44 44 0 0 1 55 10.3Z'/%3E%3C/svg%3E");
}

.icon-gem {
	--tint: #37ddc0;
	--glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%2316203a' d='M30 12 70 12 92 40 50 92 8 40Z'/%3E%3C/svg%3E");
}

.icon-ring {
	--tint: #8ea6ff;
	--glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%2316203a' fill-rule='evenodd' d='M10 50a40 40 0 1 0 80 0a40 40 0 1 0-80 0M31 50a19 19 0 1 0 38 0a19 19 0 1 0-38 0'/%3E%3C/svg%3E");
}

.icon-bloom {
	--tint: #ff8f5e;
	--glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='%2316203a'%3E%3Ccircle cx='50' cy='27' r='21'/%3E%3Ccircle cx='73' cy='50' r='21'/%3E%3Ccircle cx='50' cy='73' r='21'/%3E%3Ccircle cx='27' cy='50' r='21'/%3E%3C/g%3E%3C/svg%3E");
}

/*
 * A phone on its side. The installed app is portrait-only, but a browser tab is
 * not, and stacking the tray under an 80-pixel stage leaves nothing to play on.
 * The header keeps the top row; the stack takes the left and the tray, status
 * and toolbar move to a column on the right, where the thumb of the other hand
 * is.
 */
@media (orientation: landscape) and (max-height: 520px) {
	#app {
		max-width: none;
	}

	.game {
		display: grid;
		grid-template-columns: 1fr 46%;
		grid-template-rows: auto minmax(0, 1fr);
		gap: 0.5rem 0.75rem;
		padding-top: max(0.875rem, env(safe-area-inset-top));
		padding-left: calc(max(0.875rem, env(safe-area-inset-left)) + 5.5rem);
		padding-right: max(0.875rem, env(safe-area-inset-right));
	}

	.game[hidden] {
		display: none;
	}

	.corner {
		left: max(0.875rem, env(safe-area-inset-left));
	}

	.header-row {
		grid-column: 1 / -1;
	}

	.stage {
		grid-row: 2;
		grid-column: 1;
	}

	.bottom-bar {
		grid-row: 2;
		grid-column: 2;
		justify-content: flex-end;
	}

	.status {
		min-height: 1.5em;
	}
}

@media (prefers-reduced-motion: reduce) {
	button,
	.cube.is-leaving,
	.tile {
		transition: none;
	}

	.tile-face,
	.tile.is-clearing {
		animation: none;
	}
}
