:root {
	/* Accent + fallback. iOS Safari resolves AccentColorText to black against
	   its blue accent, so we keep --accent-fg as a constant white instead of
	   binding it to AccentColorText. */
	--accent: hsl(210 100% 40%);
	--accent-fg: white;
}

@supports (color: AccentColor) {
	:root {
		--accent: AccentColor;
	}
}

body {
	font: 100% system-ui;
	max-width: 60em;
	margin-inline: auto;
	padding: 0 1em;
	margin: 0 auto;
}

main {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: .5em 4em;
}

.controls {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 0.6em;
}

color-picker {
	grid-template-columns: 1fr;
	flex: 1;

	&::part(color-space) {
		margin-block-end: -.5em;
	}
}

.wheel-wrap {
	--size: 520px;
	/* Initial values matching the Vue data defaults, so the wheel paints
	   sensibly during the brief flash before Vue mounts. */
	--max-c: 0.4;
	--l: 0.5;
	--marker-c: 0.2;
	--marker-h: 240deg;

	position: relative;
	width: var(--size);
	max-width: 100%;
	aspect-ratio: 1;
	margin-block-start: 2.5em;
	container-type: inline-size;
	/* OOG backdrop: the layers paint `transparent` outside their in-gamut hue ranges,
	   so this colour shows through anywhere the gamut doesn't reach. */
	background: light-dark(#d8d8d8, #2c2c2c);
	border-radius: 50%;
	cursor: crosshair;
	/* Suppress browser pan/zoom so dragging the marker on touch doesn't scroll. */
	touch-action: none;
	-webkit-user-select: none;
	user-select: none;

	&.dragging {
		cursor: grabbing;
	}

	/* Center mark — small dot at c=0, h=anything. Pseudo (rather than a
	   tick entry) because a c=0 ring collapses to nothing and would need
	   special-casing through the .tick rules anyway. ::after paints last,
	   above all the absolutely-positioned children. */
	&::after {
		content: "";
		position: absolute;
		top: 50%;
		left: 50%;
		width: 4px;
		height: 4px;
		margin: -2px 0 0 -2px;
		background: white;
		border-radius: 50%;
		mix-blend-mode: difference;
		pointer-events: none;
	}

	.layers {
		position: absolute;
		inset: 0;
		clip-path: var(--clip, none);
		/* Inherits to all .layer children. The wheel-wrap is the only intended
		   pointer target; without this, every mouse-move hovering the wheel
		   hit-tests through 80 stacked layer divs, which dominates the frame. */
		pointer-events: none;
	}

	.layer {
		position: absolute;
		/* Each layer is sized so its outer edge corresponds to its --c on the chroma axis.
		   c == max-c → inset 0 (full size); c == 0 → inset 50% (zero size at centre). */
		inset: calc((1 - var(--c) / var(--max-c)) * 50%);
		border-radius: 50%;
		/* Single static gradient: hue 0° at 3 o'clock, math-CCW around the wheel. CSS conic
		   gradients run clockwise from the `from` angle, so we map gradient angle θ to math
		   hue (-θ) — at gradient pos 90° (= 6 o'clock) we want math hue 270°, etc. `in oklch`
		   interpolates the hue on the shorter path, which here is always the right one. */
		background:
			conic-gradient(from 90deg in oklch,
				oklch(var(--l) var(--c) 0deg) 0deg,
				oklch(var(--l) var(--c) 270deg) 90deg,
				oklch(var(--l) var(--c) 180deg) 180deg,
				oklch(var(--l) var(--c) 90deg) 270deg,
				oklch(var(--l) var(--c) 0deg) 360deg);
	}

	/* Reference ring + its label rendered as one element. The element draws
	   the dashed circle (border, alpha-muted so the ring is quieter than the
	   label) and ::before drops the chroma value at the top of the ring.
	   mix-blend-mode on the parent gives both border and label auto-contrast
	   over whatever colour they sit on. Shared by chroma-positioned ticks
	   (outer / gamut, positioned by --tick-c below) and the pointer ring
	   (positioned by pixel offsets via --pointer-x / --pointer-y). */
	.tick {
		position: absolute;
		border-radius: 50%;
		border: 1px dashed rgb(255 255 255 / 0.55);
		mix-blend-mode: difference;
		pointer-events: none;

		&::before {
			content: attr(data-label);
			position: absolute;
			top: 0;
			left: 50%;
			translate: -50% 0;
			font: 600 0.7em / 1 system-ui;
			color: white;
		}
	}

	.tick:not(.tick-pointer) {
		inset: calc((1 - var(--tick-c) / var(--max-c)) * 50%);
	}

	/* Pointer ring positioned entirely from --pointer-x / --pointer-y, set
	   by the pointermove handler on this very element (not the wrapper, so
	   its updates don't invalidate descendants of .wheel-wrap that read
	   other custom properties). Percentages in inset resolve against
	   .wheel-wrap, so 50% is the wheel radius; max() clamps the ring to
	   wheel size when the pointer is dragged outside. */
	.tick-pointer {
		--dx: calc(var(--pointer-x, 50%) - 50%);
		--dy: calc(var(--pointer-y, 50%) - 50%);
		--dist: hypot(var(--dx), var(--dy));
		inset: max(0px, calc(50% - var(--dist)));
	}

	&:not(:hover, .dragging) .tick-pointer {
		opacity: 0;
	}

	.marker {
		position: absolute;
		top: 50%;
		left: 50%;
		width: 18px;
		height: 18px;
		margin: -9px 0 0 -9px;
		border-radius: 50%;
		background: oklch(var(--l) var(--marker-c) var(--marker-h));
		border: 2px solid white;
		box-shadow: 0 0 0 1px rgb(0 0 0 / 0.4), 0 1px 2px rgb(0 0 0 / 0.4);
		pointer-events: none;
		--r: calc(min(var(--marker-c) / var(--max-c), 1) * 50cqi);
		translate:
			calc(var(--r) * cos(var(--marker-h)))
			calc(var(--r) * sin(var(--marker-h)) * -1);
	}

	/* One outline per shown gamut. Each instance gets its own --shape from
	   the inline style, pointing at the wrapper's --shape-<gamut> variable.
	   Rendering is split between two @supports branches at file scope:
	   border-shape (a real stroke along the polygon) where supported, and
	   the mix-blend-mode trick (auto-contrast over varying colour) elsewhere. */
	.outline {
		position: absolute;
		inset: 0;
		pointer-events: none;
	}

}

@supports (border-shape: inset(0)) {
	.wheel-wrap .outline {
		border: 1px solid black;
		box-shadow: 0 0 0 1px white;
		border-shape: var(--shape);
	}
}

@supports not (border-shape: inset(0)) {
	.wheel-wrap .outline {
		background: white;
		clip-path: var(--shape);
		mix-blend-mode: difference;

		&::after {
			content: "";
			position: absolute;
			inset: 0;
			background: black;
			clip-path: var(--shape);
			transform: scale(0.99);
			transform-origin: center;
		}
	}
}

.hue-labels {
	position: absolute;
	inset: 0;
	pointer-events: none;

	span {
		position: absolute;
		inset: 50% auto auto 50%;
		font-size: 0.8em;
		color: color-mix(in oklch, currentColor, transparent 40%);
		--r: calc(50cqi + 1.6em);
		translate:
			calc(-50% + var(--r) * cos(var(--angle)))
			calc(-50% - var(--r) * sin(var(--angle)));
	}

	/* h=0° / h=180° sit on the horizontal axis and overflow narrow viewports. */
	@container (max-width: 32em) {
		span:nth-child(odd) { display: none; }
	}
}

.meta {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.4em 1em;
	font-size: 0.8em;
	color: color-mix(in oklch, currentColor, transparent 30%);

	label {
		display: inline-flex;
		align-items: center;
		gap: 0.3em;
		cursor: pointer;
	}
}

.setting {
	display: inline-flex;
	align-items: center;
	gap: 0.25em;

	label {
		padding: 0.1em 0.45em;
		border-radius: 0.25em;
		border: 1px solid color-mix(in oklch, currentColor, transparent 80%);

		input {
			/* The native control is the source of truth; hide it visually
			   but keep it focusable. */
			position: absolute;
			opacity: 0;
			pointer-events: none;
		}

		&:has(:checked):not(.painted) {
			background: var(--accent);
			border-color: var(--accent);
			color: var(--accent-fg);
		}

		/* Paint gamut: shown via the disc edge regardless of the checkbox.
		   Dashed accent border marks it as the paint gamut; the fill toggles
		   with the underlying checked state so the user can see their preference
		   take effect (it'll matter once paintGamut moves elsewhere). The
		   matching input also has .indeterminate set for AT semantics. */
		&.painted {
			border-color: var(--accent);
			border-style: dashed;

			&:has(:checked) {
				background: color-mix(in oklch, var(--accent), transparent 82%);
			}
		}

		&:has(:focus-visible) {
			outline: 2px solid Highlight;
			outline-offset: 1px;
		}
	}
}
