/* ==========================================================================
   UI
   Every component on the page, in document order, inside @layer component.

   Load order is css/tokens.css -> css/core.css -> css/ui.css. The layer
   order is declared once at the top of core.css, so this file's position in
   the cascade is a statement rather than an accident of the <link> list.

   Components consume the material vocabulary from core.css (.glass, .pane,
   .well, .sheen) and the tokens from tokens.css. Where a component still
   carries its own gradient or shadow, it is because the shape is specific to
   that component, not because the system lacked a token for it.
   ========================================================================== */

@layer component {

	/* ==========================================================================
	   NAVIGATION — floating capsule, travelling pill, scroll-spy
	   (was css/components/nav.css)
	   ========================================================================== */

	.nav {
		position: fixed;
		inset-block-start: var(--space-sm);
		inset-inline: 0;
		z-index: var(--z-nav);

		display: flex;
		justify-content: center;
		padding-inline: var(--gutter);
		pointer-events: none;

		transition: inset-block-start var(--dur-slow) var(--ease-out);
	}

	.nav__inner {
		position: relative;
		display: flex;
		align-items: center;
		gap: var(--space-sm);

		max-width: 100%;
		/* Slightly roomier on the outer edges so the monogram and the theme
		   switch aren't crowded against the capsule's rim. */
		/* 7px, not 4px. At 4px the selected pill sat 83% of the bar's height and
		   the two pill outlines read as a rendering artefact rather than as a
		   selection inside a bar. This is the whole fix — the radii are
		   deliberately left matching. */
		padding: 7px var(--space-2xs);
		border-radius: var(--radius-pill);
		pointer-events: auto;

		/*
		 * The bar is RELIEF, not glass — the same treatment as the instrument
		 * dock, so the page's two floating controls read as the same object.
		 *
		 * It used to be a live backdrop-filter surface. A blurred backdrop
		 * over content that is itself moving has to re-sample and re-blur on
		 * every scrolled frame, and the result smears: the bar looked like it
		 * was dragging the page behind it. An opaque bar is steadier to look
		 * at, cheaper to paint, and it hands one of the four backdrop-filter
		 * slots back to the surfaces that actually benefit from translucency.
		 */
		background: var(--well-face);
		box-shadow: var(--relief-well), var(--elev-3);

		transition:
			padding var(--dur-slow) var(--ease-out),
			box-shadow var(--dur-slow) var(--ease-out),
			background-color var(--dur-slow) var(--ease-out);
	}

	/* Scrolled state — the bar lifts and deepens its shadow, as though it has
	   moved forward toward the camera. */
	/* Scrolled: the bar settles a little deeper rather than changing material. */
	.nav.is-scrolled .nav__inner {
		box-shadow: var(--relief-well), var(--elev-4);
	}

	/* --- Brand --------------------------------------------------------------
	   Just the monogram. The full name already dominates the hero directly
	   below it, so repeating it in the bar is redundant. */
	.nav__brand {
		display: inline-flex;
		align-items: center;
		border-radius: var(--radius-pill);
		flex-shrink: 0;
	}

	.nav__mark {
		display: grid;
		place-items: center;
		width: 2.1rem;
		height: 2.1rem;
		border-radius: var(--radius-circle);

		background: linear-gradient(140deg, var(--accent-strong), var(--glow));
		box-shadow: var(--elev-2), inset 0 1px 0 0 rgb(255 255 255 / 0.35);

		color: var(--text-on-accent);
		font-size: 0.7rem;
		font-weight: var(--weight-black);
		letter-spacing: 0.02em;

		transition: transform var(--dur-base) var(--ease-spring),
			box-shadow var(--t-base);
	}

	.nav__brand:hover .nav__mark {
		transform: scale(1.06);
		box-shadow: var(--elev-3), var(--bloom-accent);
	}

	/* --- Link list --------------------------------------------------------- */
	.nav__list {
		position: relative;
		display: flex;
		align-items: center;
		gap: var(--space-3xs);
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.nav__link {
		position: relative;
		z-index: 1;
		display: block;
		padding: var(--space-2xs) var(--space-sm);
		border-radius: var(--radius-pill);

		color: var(--text-3);
		font-size: var(--text-sm);
		font-weight: var(--weight-medium);
		white-space: nowrap;

		/* Stops the browser's native link drag (and its URL ghost image) from
		   stealing the gesture we use to drag the pill. */
		-webkit-user-drag: none;
		-webkit-user-select: none;
		user-select: none;

		transition: color var(--t-base);
	}

	.nav__link:hover {
		color: var(--text-1);
	}

	/* The active link is the drag handle for the pill, on touch as well as with
	   a mouse.

	   `touch-action: pan-y` is what makes that possible on a phone: the row is a
	   horizontal scroller, so without it the browser claims any sideways swipe
	   for scrolling and cancels our pointer stream. Restricting the claim to the
	   ACTIVE link means the rest of the row still scrolls normally, and vertical
	   swipes anywhere still scroll the page. */
	.nav__link[aria-current='true'] {
		color: var(--text-1);
		font-weight: var(--weight-semibold);
		touch-action: pan-y;
	}

	/* Nothing should be selectable mid-drag, on any input type. */
	.nav__list.is-dragging {
		-webkit-user-select: none;
		user-select: none;
	}

	@media (hover: hover) and (pointer: fine) {
		.nav__link[aria-current='true'] {
			cursor: grab;
		}

		.nav__list.is-dragging,
		.nav__list.is-dragging .nav__link {
			cursor: grabbing;
		}
	}

	/* The travelling pill. JS writes --pill-x / --pill-w from the active link's
	   box, so it glides between destinations instead of cross-fading. */
	.nav__pill {
		position: absolute;
		inset-block: 0;
		inset-inline-start: 0;
		z-index: 0;

		width: var(--pill-w, 0);
		border-radius: var(--radius-pill);

		background: var(--surface-3);
		/*
		 * --relief-on-glass, NOT --relief-raised.
		 *
		 * The full relief pair puts a 14px bright highlight OUTSIDE the
		 * element, offset up and left. On a large control that reads as an
		 * edge catching the light; on a small pill sitting inside a bar it
		 * reads as a halo around it — the pill looked like it was glowing.
		 *
		 * This is material rule 5 in css/core.css doing its job: keep the dark
		 * half, move the highlight inside as a top line.
		 */
		box-shadow: var(--relief-on-glass);

		transform: translate3d(var(--pill-x, 0), 0, 0);
		opacity: var(--pill-o, 0);

		transition:
			transform var(--dur-slow) var(--ease-spring),
			width var(--dur-slow) var(--ease-spring),
			opacity var(--dur-base) var(--ease-out),
			box-shadow var(--dur-base) var(--ease-out);
	}

	/* Mid-drag the pill must track the pointer exactly, and it lifts off the
	   surface so it reads as picked up. */
	.nav__pill.is-dragging {
		transition: opacity var(--dur-base) var(--ease-out);
		background: var(--surface-4);
		box-shadow: var(--elev-3), var(--bloom-accent), var(--emboss);
	}

	/* --- Trailing actions -------------------------------------------------- */
/* --------------------------------------------------------------------------
	   ICON BUTTON — shared by nav actions and floating controls
	   A physical key: fully rounded, sinks when pressed.
	   -------------------------------------------------------------------------- */
	.icon-btn {
		position: relative;
		display: grid;
		place-items: center;

		width: 2.25rem;
		height: 2.25rem;
		border-radius: var(--radius-circle);

		color: var(--text-2);
		font-size: var(--text-sm);

		transition:
			background-color var(--t-fast),
			color var(--t-fast),
			box-shadow var(--t-fast),
			transform var(--dur-instant) var(--ease-out);
	}

	.icon-btn:hover {
		background: var(--surface-3);
		color: var(--text-1);
	}

	.icon-btn:active {
		transform: translate3d(0, 1px, 0);
		box-shadow: var(--press);
	}

	/* --------------------------------------------------------------------------
	   RESPONSIVE

	   No hamburger, and no second row. Brand, links and the theme toggle stay
	   on ONE line at every width; the link row simply scrolls horizontally once
	   it outgrows the space, with its edges masked so it reads as scrollable.
	   -------------------------------------------------------------------------- */
	@media (max-width: 62rem) {
		.nav {
			inset-block-start: var(--space-2xs);
		}

		.nav__inner {
			flex-wrap: nowrap;
			gap: var(--space-2xs);
			width: 100%;
			max-width: 38rem;
			padding: var(--space-3xs);
		}

		.nav__brand {
			flex: 0 0 auto;
		}

		/* Takes the leftover space and allows its child to overflow-scroll.
		   `min-width: 0` is what actually permits a flex item to shrink below
		   its content width. */
		.nav__menu {
			flex: 1 1 auto;
			min-width: 0;
		}

		.nav__list {
			gap: var(--space-3xs);

			overflow-x: auto;
			overscroll-behavior-inline: contain;
			scrollbar-width: none;

			/* Fade both ends so a clipped link reads as scrollable, not broken. */
			-webkit-mask-image: linear-gradient(
				90deg,
				transparent,
				#000 5%,
				#000 95%,
				transparent
			);
			mask-image: linear-gradient(
				90deg,
				transparent,
				#000 5%,
				#000 95%,
				transparent
			);

			padding-inline: var(--space-2xs);
		}

		.nav__list::-webkit-scrollbar {
			display: none;
		}

		.nav__list li {
			flex: 0 0 auto;
		}

		.nav__link {
			padding: var(--space-2xs) var(--space-xs);
			font-size: var(--text-sm);
		}
	}

	@media (max-width: 30rem) {
		.nav {
			padding-inline: var(--space-2xs);
		}

		.nav__inner {
			gap: var(--space-3xs);
		}

		.nav__mark {
			width: 1.9rem;
			height: 1.9rem;
			font-size: 0.62rem;
		}

		.nav__link {
			padding: var(--space-3xs) var(--space-2xs);
			font-size: var(--text-xs);
		}
	}

	/* ==========================================================================
	   BUTTONS, CHIPS & TAGS — every pressable thing
	   (was css/components/buttons.css)
	   ========================================================================== */

	/* --------------------------------------------------------------------------
	   SHARED BASE
	   -------------------------------------------------------------------------- */
	.btn,
	.icon-btn {
		position: relative;
		isolation: isolate;

		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: var(--space-2xs);

		border: 1px solid var(--line);
		cursor: pointer;

		transition:
			transform var(--dur-fast) var(--ease-spring),
			box-shadow var(--dur-base) var(--ease-out),
			background var(--dur-base) var(--ease-out),
			border-color var(--dur-base) var(--ease-out),
			color var(--dur-base) var(--ease-out);
	}

	.btn {
		padding: 0.8rem var(--space-md);
		border-radius: var(--radius-pill);

		font-size: var(--text-sm);
		font-weight: var(--weight-semibold);
		letter-spacing: var(--tracking-normal);
		line-height: 1;
		text-align: center;
		white-space: nowrap;
	}

	/* Icons hold still. Sliding them shifts the label's optical centre and
	   makes the whole control feel unstable. */
	.btn .icon {
		font-size: 0.95em;
	}

	.btn:disabled,
	.btn[aria-disabled='true'] {
		cursor: not-allowed;
		opacity: 0.5;
		pointer-events: none;
	}

	/* --- The shared lift + press ------------------------------------------- */
	@media (hover: hover) {
		.btn:hover:not(:disabled),
		.icon-btn:hover {
			transform: translate3d(0, -3px, 0);
		}
	}

	.btn:active:not(:disabled),
	.icon-btn:active {
		transform: translate3d(0, 1px, 0);
	}

	/* Buttons are often <a> elements, and the generic `a:hover` rule in base.css
	   has higher specificity (0,1,1) than a single class (0,1,0). Without an
	   explicit colour on every hover state, the label would be repainted with the
	   link hover colour. In light theme that resolved to the button's own
	   background: a 1.00:1 contrast ratio, i.e. invisible text. */
	.btn--primary:hover:not(:disabled),
	.btn--primary:active:not(:disabled),
	.btn--primary:focus-visible {
		color: var(--text-on-accent);
	}

	/* --------------------------------------------------------------------------
	   PRIMARY
	   -------------------------------------------------------------------------- */
	.btn--primary {
		border-color: transparent;
		background: linear-gradient(180deg, var(--btn-top) 0%, var(--btn-bottom) 100%);
		box-shadow:
			var(--elev-2),
			var(--bloom-accent),
			inset 0 1px 0 0 rgb(255 255 255 / 0.22),
			inset 0 -2px 3px -1px rgb(0 0 0 / 0.28);
		color: var(--text-on-accent);
	}

	.btn--primary:hover:not(:disabled) {
		background: linear-gradient(
			180deg,
			var(--btn-top-hover) 0%,
			var(--btn-bottom-hover) 100%
		);
		box-shadow:
			var(--elev-3),
			0 14px 38px -10px rgb(var(--accent-rgb) / 0.65),
			inset 0 1px 0 0 rgb(255 255 255 / 0.2),
			inset 0 -2px 3px -1px rgb(0 0 0 / 0.3);
	}

	.btn--primary:active:not(:disabled) {
		background: var(--btn-press);
		box-shadow: var(--elev-1), var(--press);
	}

	/* --------------------------------------------------------------------------
	   SECONDARY — raised neutral surface, same as a skill pill
	   -------------------------------------------------------------------------- */
	.btn--secondary,
	.icon-btn {
		background: var(--relief-face);
		box-shadow: var(--elev-2), var(--relief-raised);
		color: var(--text-1);
	}

	@media (hover: hover) {
		.btn--secondary:hover:not(:disabled),
		.icon-btn:hover {
			border-color: rgb(var(--accent-rgb) / 0.45);
			box-shadow: var(--elev-3), var(--bloom-accent), var(--relief-raised-hi);
			color: var(--text-1);
		}
	}

	.btn--secondary:active:not(:disabled),
	.icon-btn:active {
		box-shadow: var(--elev-1), var(--relief-pressed);
	}

	/* --------------------------------------------------------------------------
	   OUTLINE — same motion, lighter resting weight
	   -------------------------------------------------------------------------- */
	.btn--outline {
		border-color: var(--line-strong);
		background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
		box-shadow: var(--elev-1), var(--emboss);
		color: var(--text-2);
	}

	.btn--outline:hover:not(:disabled) {
		border-color: rgb(var(--accent-rgb) / 0.55);
		background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
		box-shadow: var(--elev-2), var(--bloom-accent), var(--emboss);
		color: var(--text-1);
	}

	.btn--outline:active:not(:disabled) {
		box-shadow: var(--elev-1), var(--press);
	}

	/* --------------------------------------------------------------------------
	   GHOST — no resting chrome, but the same press
	   -------------------------------------------------------------------------- */
	.btn--ghost {
		border-color: transparent;
		background: transparent;
		box-shadow: none;
		color: var(--text-2);
	}

	.btn--ghost:hover:not(:disabled) {
		background: var(--surface-3);
		box-shadow: var(--elev-1), var(--emboss);
		color: var(--text-1);
	}

	.btn--ghost:active:not(:disabled) {
		box-shadow: var(--press);
	}

	.btn--secondary:hover:not(:disabled),
	.btn--secondary:active:not(:disabled),
	.btn--secondary:focus-visible,
	.btn--outline:hover:not(:disabled),
	.btn--outline:active:not(:disabled),
	.btn--outline:focus-visible,
	.btn--ghost:hover:not(:disabled),
	.btn--ghost:active:not(:disabled),
	.btn--ghost:focus-visible {
		color: var(--text-1);
	}

	/* --------------------------------------------------------------------------
	   SIZES
	   -------------------------------------------------------------------------- */
	.btn--sm {
		padding: 0.55rem var(--space-sm);
		font-size: var(--text-xs);
	}

	.btn--block {
		width: 100%;
	}

	/* --------------------------------------------------------------------------
	   TAG / CHIP — labels, not controls, so no lift or press
	   -------------------------------------------------------------------------- */
	.tag {
		display: inline-flex;
		align-items: center;
		gap: var(--space-3xs);

		padding: 0.32rem 0.7rem;
		border: 1px solid var(--line);
		border-radius: var(--radius-pill);
		background: var(--surface-3);

		color: var(--text-3);
		font-size: var(--text-2xs);
		font-weight: var(--weight-medium);
		letter-spacing: var(--tracking-normal);
		white-space: nowrap;

		transition:
			color var(--t-fast),
			border-color var(--t-fast),
			background-color var(--t-fast);
	}

	.tag--accent {
		border-color: rgb(var(--accent-rgb) / 0.3);
		background: rgb(var(--accent-rgb) / 0.1);
		color: var(--accent-strong);
	}

	.tag--glow {
		border-color: rgb(var(--glow-rgb) / 0.32);
		background: rgb(var(--glow-rgb) / 0.1);
		color: var(--glow);
	}

	/* --------------------------------------------------------------------------
	   STATUS DOT
	   -------------------------------------------------------------------------- */
	.status-dot {
		position: relative;
		display: inline-block;
		width: 0.5rem;
		height: 0.5rem;
		border-radius: var(--radius-circle);
		background: var(--success);
		flex-shrink: 0;
	}

	.status-dot::after {
		content: '';
		position: absolute;
		inset: 0;
		border-radius: inherit;
		background: var(--success);
		animation: pulse-ring 2.4s var(--ease-out) infinite;
	}

	@media (prefers-reduced-motion: reduce) {
		.status-dot::after {
			animation: none;
		}
	}

	/* ==========================================================================
	   HERO — the material showcase
	   (was css/components/hero.css)
	   ========================================================================== */

	.hero {
		position: relative;
		display: flex;
		flex-direction: column;
		justify-content: center;

		min-height: min(100svh, 58rem);
		padding-block: calc(var(--nav-height, 4.5rem) + var(--space-2xl))
			var(--space-2xl);

		/* The hero is its own camera so children can use real translateZ. */
		perspective: var(--perspective);
		perspective-origin: 50% 45%;
		transform-style: preserve-3d;
		isolation: isolate;
	}

	.hero__inner {
		display: grid;
		grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
		align-items: center;
		gap: var(--space-2xl);
		width: 100%;
	}

	/* --------------------------------------------------------------------------
	   AMBIENT LIGHT
	   The orbs are NOT clipped by the hero's box — clipping them produced a hard
	   rectangular colour edge across the viewport. Instead they extend well past
	   the section and are faded out with a soft radial mask, so the light falls
	   off organically in every direction.
	   -------------------------------------------------------------------------- */
	.hero__orbs {
		position: absolute;
		inset: -40% -25%;
		z-index: -3;
		pointer-events: none;

		-webkit-mask-image: radial-gradient(
			ellipse 62% 58% at 50% 46%,
			#000 30%,
			transparent 78%
		);
		mask-image: radial-gradient(
			ellipse 62% 58% at 50% 46%,
			#000 30%,
			transparent 78%
		);
	}

	.hero__orbs .orb--1 {
		inset-block-start: -8%;
		inset-inline-start: -6%;
	}

	.hero__orbs .orb--2 {
		inset-block-start: 44%;
		inset-inline-end: -4%;
	}

	.hero__orbs .orb--3 {
		inset-block-end: -14%;
		inset-inline-start: 34%;
	}

	/* --------------------------------------------------------------------------
	   COPY
	   -------------------------------------------------------------------------- */
	.hero__copy {
		position: relative;
		z-index: 1;
		max-width: 36rem;
	}

	/* Availability chip — the first thing a recruiter should see. */
	.hero__status {
		display: inline-flex;
		align-items: center;
		gap: var(--space-2xs);

		margin-block-end: var(--space-md);
		padding: 0.4rem 0.85rem 0.4rem 0.7rem;
		border-radius: var(--radius-pill);

		color: var(--text-2);
		font-size: var(--text-xs);
		font-weight: var(--weight-medium);
	}

	.hero__greeting {
		display: block;
		margin-block-end: var(--space-2xs);

		color: var(--text-3);
		font-size: var(--text-md);
		font-weight: var(--weight-medium);
		/* The h1 sets a very tight line-height for the display name. The greeting
		   is ordinary running text and needs its own, or it sits cramped against
		   the name below it. */
		line-height: var(--leading-normal);
		letter-spacing: var(--tracking-normal);
	}

	.hero__name {
		margin-block-end: var(--space-sm);
		font-size: var(--text-4xl);
		/* Not tighter than this. `background-clip: text` on the gradient span
		   clips to the line box, so a very tight line-height shears the
		   descenders off letters like y and g. */
		line-height: 1.04;
		letter-spacing: var(--tracking-tighter);
	}

	/* --- Rotating role ------------------------------------------------------
	   A fixed-height window with a caret. JS swaps the text; the mask keeps
	   the layout from jumping as strings of different lengths cycle. */
	.hero__role {
		display: flex;
		align-items: center;
		gap: 0.15rem;

		min-height: 2.2em;
		margin-block-end: var(--space-md);

		color: var(--text-1);
		font-size: var(--text-lg);
		font-weight: var(--weight-semibold);
		letter-spacing: var(--tracking-tight);
	}

	.hero__role-prefix {
		color: var(--text-3);
		font-weight: var(--weight-regular);
	}

	.hero__role-text {
		background: linear-gradient(96deg, var(--accent-strong), var(--glow));
		-webkit-background-clip: text;
		background-clip: text;
		color: transparent;
	}

	.hero__caret {
		display: inline-block;
		width: 2px;
		height: 1.15em;
		margin-inline-start: 0.12em;
		border-radius: 1px;
		background: var(--glow);
		box-shadow: 0 0 10px rgb(var(--glow-rgb) / 0.8);
		animation: caret-blink 1.15s steps(1, end) infinite;
	}

	.hero__lede {
		max-width: 34rem;
		margin-block-end: var(--space-lg);
		color: var(--text-2);
		font-size: var(--text-md);
		line-height: var(--leading-relaxed);
		text-wrap: pretty;
	}

	.hero__actions {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-sm);
	}

	/* --------------------------------------------------------------------------
	   PORTRAIT
	   A glass plate the photograph sits inside, floating forward of the page
	   and tilting toward the pointer. The plate has its own light: a soft
	   gradient wash behind the subject and a specular rim.
	   -------------------------------------------------------------------------- */
	.hero__portrait {
		position: relative;
		justify-self: center;
		width: min(22rem, 100%);
		transform-style: preserve-3d;
	}

	.hero__plate {
		position: relative;
		aspect-ratio: 4 / 5;
		border-radius: var(--radius-2xl);
		overflow: hidden;

		/*
		 * Floats forward of the copy plane.
		 *
		 * This must NOT be `transform: translateZ(...)`. .tilt owns `transform`
		 * on this element, and a competing declaration here wins on load order
		 * and kills the tilt outright. Feeding the resting offset through
		 * --tilt-z composes with the lean instead of fighting it.
		 */
		--tilt-z: var(--depth-fore);
		box-shadow: var(--elev-float), var(--rim-inset);

		/* The lens ring needs a margin of its own to sit in. */
		padding: var(--lens-w-strong);
	}

	.hero__plate img {
		width: 100%;
		height: 100%;
		border-radius: var(--radius-xl);
		object-fit: cover;
		object-position: center 18%;

		/* Slight forward offset from the plate creates real parallax on tilt. */
		transform: translateZ(18px);

		transition: transform var(--dur-slow) var(--ease-out);
	}

	/* Coloured wash behind the subject, so the plate reads as backlit. */
	.hero__plate::before {
		content: '';
		position: absolute;
		inset: 0;
		z-index: 0;
		background: linear-gradient(
			160deg,
			rgb(var(--accent-rgb) / 0.22),
			transparent 45%,
			rgb(var(--glow-rgb) / 0.16)
		);
		pointer-events: none;
	}

	/* --- Floating detail badges --------------------------------------------
	   Small glass chips pinned to the plate's edges at different depths, so
	   they separate from it as the plate tilts. */
	.hero__badge {
		position: absolute;
		z-index: 2;

		display: inline-flex;
		align-items: center;
		gap: var(--space-2xs);

		padding: var(--space-2xs) var(--space-xs);
		border-radius: var(--radius-md);

		color: var(--text-1);
		font-size: var(--text-2xs);
		font-weight: var(--weight-semibold);
		white-space: nowrap;

		/*
		 * Relief, not glass. These sit ON the portrait plate, and the plate is
		 * the page's one piece of real translucent glass — so by rule 2 of the
		 * material system these are objects resting on that surface, which
		 * makes them .pane. .pane--on-glass then drops the bright half of the
		 * relief pair, because a white highlight floating over a blurred
		 * backdrop reads as a halo rather than an edge.
		 */
		box-shadow: var(--relief-on-glass), var(--elev-3);
	}

	.hero__badge .icon {
		color: var(--accent);
		font-size: 0.95em;
	}

	.hero__badge--tl {
		inset-block-start: 8%;
		inset-inline-start: -12%;
		transform: translateZ(52px);
		animation: float-idle 6.5s var(--ease-in-out) infinite;
	}

	.hero__badge--br {
		inset-block-end: 11%;
		inset-inline-end: -10%;
		transform: translateZ(66px);
		animation: float-idle 7.8s var(--ease-in-out) infinite reverse;
	}

	.hero__badge--bl {
		inset-block-end: 34%;
		inset-inline-start: -16%;
		transform: translateZ(38px);
		animation: float-idle 9s var(--ease-in-out) 0.6s infinite;
	}

	@media (prefers-reduced-motion: reduce) {
		.hero__badge {
			animation: none;
		}
	}

	@media (prefers-reduced-motion: reduce) {

	}

	/* --------------------------------------------------------------------------
	   RESPONSIVE
	   -------------------------------------------------------------------------- */
	@media (max-width: 62rem) {
		.hero__inner {
			grid-template-columns: minmax(0, 1fr);
			justify-items: center;
			gap: var(--space-xl);
			text-align: center;
		}

		/* Portrait leads on narrow screens — the face lands first. */
		.hero__portrait {
			order: -1;
			width: min(16rem, 70%);
		}

		.hero__copy {
			max-width: 38rem;
		}

		.hero__actions,
		.hero__role {
			justify-content: center;
		}

		.hero__lede {
			margin-inline: auto;
		}

		.hero__badge--bl {
			display: none;
		}
	}

	@media (max-width: 40rem) {
		.hero {
			min-height: auto;
			padding-block-end: var(--space-xl);
		}

		.hero__badge--tl,
		.hero__badge--br {
			font-size: 0.62rem;
			padding: 0.3rem 0.5rem;
		}

		.hero__badge--tl {
			inset-inline-start: -4%;
		}

		.hero__badge--br {
			inset-inline-end: -4%;
		}

	}

	/* ==========================================================================
	   PANELS, SPOTLIGHTS & PROJECT CARDS
	   (was css/components/cards.css)
	   ========================================================================== */

	/* --------------------------------------------------------------------------
	   PANEL — generic content block
	   -------------------------------------------------------------------------- */
	.panel {
		padding: var(--space-md);
		border-radius: var(--radius-lg);
	}

	.panel--lg {
		padding: var(--space-lg);
		border-radius: var(--radius-xl);
	}

	.panel__title {
		display: flex;
		align-items: center;
		gap: var(--space-2xs);
		margin-block-end: var(--space-sm);
		color: var(--text-1);
		font-size: var(--text-sm);
		font-weight: var(--weight-semibold);
		letter-spacing: var(--tracking-wide);
		text-transform: uppercase;
	}

	.panel__title .icon {
		color: var(--accent);
	}

	/* Definition rows used by the About "quick facts" panel. */
	.fact {
		display: flex;
		align-items: flex-start;
		justify-content: space-between;
		gap: var(--space-sm);
		padding-block: var(--space-2xs);
	}

	.fact + .fact {
		border-block-start: 1px solid var(--line-faint);
	}

	.fact__key {
		flex-shrink: 0;
		color: var(--text-3);
		font-size: var(--text-xs);
		font-weight: var(--weight-medium);
	}

	.fact__value {
		color: var(--text-1);
		font-size: var(--text-sm);
		font-weight: var(--weight-medium);
		text-align: end;
	}

	/* --------------------------------------------------------------------------
	   SPOTLIGHT — featured project
	   Image plate on one side, copy on the other, alternating. The whole card
	   is a tilt surface; the image sits forward of the card so it separates
	   as the card leans.
	   -------------------------------------------------------------------------- */
	.spotlight {
		display: grid;
		grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
		align-items: center;
		gap: var(--space-xl);

		padding: var(--space-lg);
		border-radius: var(--radius-2xl);
	}

	.spotlight:nth-child(even) .spotlight__media {
		order: -1;
	}

	.spotlight__media {
		position: relative;
		aspect-ratio: 16 / 10;
		border-radius: var(--radius-xl);
		overflow: hidden;

		border: 1px solid var(--line);
		background: var(--surface-3);
		box-shadow: var(--elev-3);
	}

	.spotlight__media img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		object-position: top center;
		transition:
			transform var(--dur-slower) var(--ease-glide),
			filter var(--dur-slow) var(--ease-out);
	}

	.spotlight:hover .spotlight__media img {
		transform: scale(1.03);
	}

	/* Colour wash so screenshots of very different palettes still feel like
	   one family. Lifts on hover to reveal the true image. */
	.spotlight__media::after {
		content: '';
		position: absolute;
		inset: 0;
		background: linear-gradient(
			200deg,
			rgb(var(--accent-rgb) / 0.14),
			transparent 55%
		);
		opacity: 1;
		transition: opacity var(--dur-slow) var(--ease-out);
		pointer-events: none;
	}

	.spotlight:hover .spotlight__media::after {
		opacity: 0;
	}

	.spotlight__body {
		display: flex;
		flex-direction: column;
		gap: var(--space-sm);
	}

	.spotlight__eyebrow {
		display: flex;
		align-items: center;
		gap: var(--space-2xs);
		color: var(--accent);
		font-size: var(--text-2xs);
		font-weight: var(--weight-semibold);
		letter-spacing: var(--tracking-widest);
		text-transform: uppercase;
	}

	.spotlight__title {
		font-size: var(--text-xl);
		letter-spacing: var(--tracking-tight);
	}

	.spotlight__desc {
		color: var(--text-2);
		font-size: var(--text-sm);
		line-height: var(--leading-relaxed);
	}

	.spotlight__actions {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-2xs);
		margin-block-start: var(--space-3xs);
	}

	/* --------------------------------------------------------------------------
	   PROJECT CARD — compact grid item

	   The hover lift uses `transform`, not `translate`. The reveal animation
	   owns the independent `translate` and `scale` properties for the life of
	   the element, so a lift declared there would simply never apply.
	   -------------------------------------------------------------------------- */
	.project-card {
		position: relative;
		display: flex;
		flex-direction: column;

		border-radius: var(--radius-xl);
		overflow: hidden;

		transition:
			transform var(--dur-base) var(--ease-out),
			box-shadow var(--dur-base) var(--ease-out),
			border-color var(--dur-base) var(--ease-out);
	}

	@media (hover: hover) {
		.project-card:hover,
		.project-card:focus-within {
			transform: translate3d(0, -3px, 0);
			border-color: rgb(var(--accent-rgb) / 0.35);
			box-shadow: var(--elev-4), var(--rim-inset);
		}
	}

	.project-card__media {
		position: relative;
		aspect-ratio: 16 / 10;
		overflow: hidden;
		background: var(--surface-3);
		border-block-end: 1px solid var(--line);
	}

	.project-card__media img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		object-position: top center;
		transition: transform var(--dur-slower) var(--ease-glide);
	}

	.project-card:hover .project-card__media img {
		transform: scale(1.03);
	}

	/* Media is washed with the accent at rest and clears on hover, exactly as
	   the spotlights do, so both card types read as one family. */
	.project-card__media::after {
		content: '';
		position: absolute;
		inset: 0;
		background: linear-gradient(
			200deg,
			rgb(var(--accent-rgb) / 0.14),
			transparent 55%
		);
		opacity: 1;
		transition: opacity var(--dur-slow) var(--ease-out);
		pointer-events: none;
	}

	.project-card:hover .project-card__media::after {
		opacity: 0;
	}

	.project-card__body {
		display: flex;
		flex: 1;
		flex-direction: column;
		gap: var(--space-2xs);
		padding: var(--space-md);
	}

	.project-card__title {
		font-size: var(--text-base);
		letter-spacing: var(--tracking-tight);
	}

	.project-card__desc {
		flex: 1;
		color: var(--text-3);
		font-size: var(--text-xs);
		line-height: var(--leading-normal);
	}

	.project-card__tags {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-3xs);
		margin-block-start: var(--space-3xs);
	}

	/* Same action treatment as the spotlights. */
	.project-card__actions {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-2xs);
		margin-block-start: var(--space-2xs);
	}

	/* --------------------------------------------------------------------------
	   PROJECT ARCHIVE — the "View all" expanding region
	   -------------------------------------------------------------------------- */
	.projects__more {
		display: flex;
		justify-content: center;
		margin-block-start: var(--space-xl);
	}

	.projects__archive {
		margin-block-start: var(--space-lg);
		/* Collapsed by default; JS animates the height for a smooth expand. */
		overflow: hidden;
	}

	.projects__archive[hidden] {
		display: block !important;
		height: 0;
		margin-block-start: 0;
		opacity: 0;
		pointer-events: none;
	}

	.projects__archive {
		transition:
			height var(--dur-slower) var(--ease-glide),
			opacity var(--dur-slow) var(--ease-out),
			margin var(--dur-slower) var(--ease-glide);
	}

	/* --------------------------------------------------------------------------
	   RESPONSIVE
	   -------------------------------------------------------------------------- */
	@media (max-width: 62rem) {
		.spotlight {
			grid-template-columns: minmax(0, 1fr);
			gap: var(--space-lg);
			padding: var(--space-md);
		}

		/* Media always leads on narrow screens, regardless of alternation. */
		.spotlight__media,
		.spotlight:nth-child(even) .spotlight__media {
			order: -1;
		}
	}

	@media (max-width: 40rem) {
		.spotlight {
			padding: var(--space-sm);
		}

		.spotlight__actions .btn {
			flex: 1 1 auto;
		}
	}

	@media (prefers-reduced-motion: reduce) {

	}

	@media (hover: hover) {

	}

	/* ==========================================================================
	   EDUCATION RAIL — horizontal, scroll-snapped
	   ========================================================================== */

	.rail {
		position: relative;
		display: grid;
		grid-auto-flow: column;
		grid-auto-columns: minmax(min(17rem, 78vw), 1fr);
		gap: var(--space-md);

		overflow-x: auto;
		overscroll-behavior-inline: contain;
		scroll-snap-type: inline mandatory;
		scroll-padding-inline: var(--gutter);

		/* Bleed to the viewport edges so cards can scroll off-screen cleanly. */
		margin-inline: calc(-1 * var(--gutter));
		padding-inline: var(--gutter);
		padding-block: var(--space-2xs) var(--space-md);

		scrollbar-width: none;
	}

	.rail::-webkit-scrollbar {
		display: none;
	}

	/* ---- Filmstrip progress -------------------------------------------------
	   A debossed track with a raised thumb — the neumorphic object the brief
	   calls for, doing a real job.

	   The thumb is driven by a NAMED scroll timeline published by the rail and
	   made visible to its siblings with `timeline-scope` on the section. That
	   is what lets a control outside the scroller be driven by it without a
	   single line of JavaScript, and it stays exact during a fling because the
	   compositor owns it.

	   Where scroll-driven animations are unsupported the track still renders
	   and the thumb simply sits at the start: a static indicator rather than a
	   broken one. */
	.education {
		timeline-scope: --rail;
	}

	.rail {
		scroll-timeline: --rail inline;
	}

	/* Shown only when the strip actually overflows — js/ui/filmstrip.js adds
	   .is-scrollable to the rail. Hidden by default so a no-JS reader never
	   sees an indicator that cannot move. */
	.rail-progress {
		display: none;
		width: min(14rem, 60%);
		height: 6px;
		margin-inline: auto;
		margin-block-start: var(--space-2xs);
		border-radius: var(--radius-pill);
		overflow: clip;
	}

	.rail.is-scrollable ~ .rail-progress {
		display: block;
	}

	.rail-progress__thumb {
		display: block;
		/* Three items, roughly two visible: a third of the track. */
		width: 34%;
		height: 100%;
		border-radius: inherit;
		background: linear-gradient(90deg, var(--accent), var(--glow));
		box-shadow: 0 0 8px rgb(var(--accent-rgb) / 0.5);
	}

	@supports (animation-timeline: scroll()) {
		.rail-progress__thumb {
			animation: rail-travel linear both;
			animation-timeline: --rail;
		}
	}

	/* Travel is expressed relative to the THUMB, not the track: a 34%-wide
	   thumb has 66% of track left to cross, which is 194% of its own width. */
	@keyframes rail-travel {
		from {
			translate: 0 0;
		}
		to {
			translate: 194% 0;
		}
	}

	@media (max-width: 52rem) {
		/* Below this the rail degrades to a stacked column, so a horizontal
		   position indicator would be describing something that no longer
		   exists. */
		.rail-progress {
			display: none;
		}
	}

	.rail__item {
		scroll-snap-align: start;

		display: flex;
		flex-direction: column;
		align-items: center;
		gap: var(--space-2xs);

		padding: var(--space-md);
		border-radius: var(--radius-xl);
		text-align: center;

		transition:
			transform var(--dur-base) var(--ease-out),
			box-shadow var(--dur-base) var(--ease-out),
			border-color var(--dur-base) var(--ease-out);
	}

	@media (hover: hover) {
		.rail__item:hover {
			transform: translate3d(0, -3px, 0);
			border-color: rgb(var(--accent-rgb) / 0.3);
			box-shadow: var(--elev-3), var(--rim-inset);
		}
	}

	.rail__icon {
		display: grid;
		place-items: center;
		width: 2.75rem;
		height: 2.75rem;
		margin-block-end: var(--space-3xs);
		border: 1px solid var(--line);
		border-radius: var(--radius-circle);

		background: linear-gradient(180deg, var(--surface-4), var(--surface-2));
		box-shadow: var(--emboss);

		color: var(--accent-strong);
		font-size: var(--text-sm);
	}

	.rail__date {
		color: var(--text-4);
		font-family: var(--font-mono);
		font-size: var(--text-2xs);
		letter-spacing: var(--tracking-wide);
	}

	.rail__degree {
		font-size: var(--text-base);
		letter-spacing: var(--tracking-tight);
	}

	.rail__school {
		color: var(--text-2);
		font-size: var(--text-sm);
		font-weight: var(--weight-medium);
	}

	.rail__score {
		width: 100%;
		margin-block-start: auto;
		padding-block-start: var(--space-2xs);
		border-block-start: 1px solid var(--line-faint);
		color: var(--text-3);
		font-size: var(--text-xs);
	}

	.rail__score strong {
		color: var(--glow);
		font-variant-numeric: tabular-nums;
	}

	/* --------------------------------------------------------------------------
	   RESPONSIVE
	   -------------------------------------------------------------------------- */

	/* Below 52rem the horizontal rail becomes a plain vertical stack. A
	   side-scrolling row is easy to miss on a phone — people scroll the page,
	   not the row — so the cards simply stack and everything stays visible. */
	@media (max-width: 52rem) {
		.rail {
			display: flex;
			flex-direction: column;
			gap: var(--space-sm);

			overflow-x: visible;
			scroll-snap-type: none;

			margin-inline: 0;
			padding-inline: 0;
			padding-block: 0;
		}

		.rail__item {
			scroll-snap-align: none;
		}

		.rail__score {
			margin-block-start: var(--space-2xs);
		}
	}

	@media (max-width: 40rem) {

	}

	/* --------------------------------------------------------------------------
	   THE LEDGER

	   Eight roles as a scannable list rather than eight tall cards. Each row
	   collapses to role · org · date; the detail expands in place.

	   PROGRESSIVE ENHANCEMENT: every row ships EXPANDED, with aria-expanded
	   "true". js/ui/ledger.js collapses all but the current one on init. With
	   no JavaScript the whole history is simply readable — which is the right
	   failure mode for the section a recruiter came for.

	   The expansion animates `grid-template-rows: 0fr -> 1fr`, which is how
	   you animate to an unknown height without measuring it in JavaScript.
	   It is a layout property, and it qualifies under the motion law's
	   exception: a discrete, user-initiated toggle, well under 600ms, never
	   concurrent with scrolling.
	   -------------------------------------------------------------------------- */
	.ledger {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: var(--space-2xs);
		padding-inline-start: var(--space-xl);
	}

	/* The spine. Drawn by the scroll timeline so it fills as the section is
	   read, and simply present where that is unsupported. */
	.ledger::before {
		content: '';
		position: absolute;
		inset-block: 0.6rem 0.6rem;
		inset-inline-start: calc(var(--space-xl) / 2 - 1px);
		width: 2px;
		border-radius: var(--radius-pill);
		background: linear-gradient(
			180deg,
			rgb(var(--accent-rgb) / 0.55),
			rgb(var(--glow-rgb) / 0.35) 60%,
			transparent
		);
		transform-origin: top center;
	}

	@supports (animation-timeline: view()) {
		@media (prefers-reduced-motion: no-preference) {
			.ledger::before {
				animation: ledger-spine linear both;
				animation-timeline: view();
				animation-range: entry 20% cover 65%;
			}
		}
	}

	@keyframes ledger-spine {
		from {
			scale: 1 0;
		}
		to {
			scale: 1 1;
		}
	}

	.ledger__item {
		position: relative;
	}

	.ledger__node {
		position: absolute;
		inset-block-start: 0.95rem;
		inset-inline-start: calc(var(--space-xl) * -1 + var(--space-xl) / 2 - 0.7rem);
		z-index: 2;

		display: grid;
		place-items: center;
		width: 1.4rem;
		height: 1.4rem;
		border-radius: var(--radius-circle);

		background: var(--relief-face);
		box-shadow: var(--relief-raised);
		color: var(--text-3);
		font-size: 0.6rem;

		transition:
			color var(--t-fast),
			box-shadow var(--t-fast);
	}

	.ledger__item.is-current .ledger__node,
	.ledger__item.is-open .ledger__node {
		color: var(--accent);
		box-shadow: var(--relief-raised), var(--bloom-accent);
	}

	.ledger__card {
		border-radius: var(--radius-lg);
		overflow: clip;
	}

	.ledger__heading {
		margin: 0;
		font-size: inherit;
		font-weight: inherit;
	}

	.ledger__toggle {
		display: grid;
		grid-template-columns: 1fr auto auto;
		grid-template-areas: 'role date chev' 'org tag chev';
		align-items: center;
		column-gap: var(--space-sm);
		width: 100%;
		padding: var(--space-sm) var(--space-md);
		text-align: start;
		color: inherit;
	}

	.ledger__role {
		grid-area: role;
		color: var(--text-1);
		font-size: var(--text-md);
		font-weight: var(--weight-semibold);
		letter-spacing: var(--tracking-tight);
	}

	.ledger__org {
		grid-area: org;
		color: var(--text-3);
		font-size: var(--text-sm);
	}

	.ledger__date {
		grid-area: date;
		color: var(--text-4);
		font-family: var(--font-mono);
		font-size: var(--text-2xs);
		white-space: nowrap;
	}

	/* The status chip gets its own cell under the date. Sharing the date's
	   cell made the two overlap on the current role, which is the one row
	   guaranteed to have both. */
	.ledger__toggle .tag {
		grid-area: tag;
		justify-self: end;
		align-self: start;
	}

	.ledger__chev {
		grid-area: chev;
		color: var(--text-4);
		transition: rotate var(--dur-base) var(--ease-spring);
	}

	.ledger__item.is-open .ledger__chev {
		rotate: 180deg;
	}

	@media (hover: hover) {
		.ledger__toggle:hover .ledger__role {
			color: var(--accent-strong);
		}
	}

	.ledger__detail {
		display: grid;
		grid-template-rows: 1fr;
		transition: grid-template-rows var(--dur-base) var(--ease-out);
	}

	.ledger__item.is-collapsed .ledger__detail {
		grid-template-rows: 0fr;
	}

	.ledger__detail-inner {
		overflow: hidden;
	}

	.ledger__desc {
		padding-inline: var(--space-md);
		color: var(--text-2);
		font-size: var(--text-sm);
		line-height: var(--leading-relaxed);
	}

	.ledger__tags {
		padding: var(--space-2xs) var(--space-md) var(--space-md);
	}

	@media (max-width: 40rem) {
		.ledger {
			padding-inline-start: var(--space-lg);
		}

		.ledger__node {
			inset-inline-start: calc(var(--space-lg) * -1 + var(--space-lg) / 2 - 0.7rem);
		}

		.ledger__toggle {
			/* Dates drop under the role rather than competing for the row. */
			grid-template-columns: 1fr auto;
			grid-template-areas: 'role chev' 'org chev' 'date chev' 'tag chev';
			row-gap: 0.15rem;
			padding-inline: var(--space-sm);
		}

		.ledger__toggle .tag {
			grid-area: tag;
			justify-self: start;
		}

		.ledger__desc,
		.ledger__tags {
			padding-inline: var(--space-sm);
		}
	}

	/* ==========================================================================
	   SKILLS — the capability console
	   (was css/components/skills.css)
	   ========================================================================== */

	/* --------------------------------------------------------------------------
	   THE CONSOLE

	   A debossed field holding relief filter keys over a single tag cloud.
	   Two relief steps and no more: the console is the well, the keys are the
	   panes, and the tags inside are flat labels rather than a third layer.
	   -------------------------------------------------------------------------- */
	.console {
		padding: var(--space-md);
		border-radius: var(--radius-xl);
	}

	.console__filters {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-2xs);
		margin-block-end: var(--space-md);
		padding-block-end: var(--space-md);
		border-block-end: 1px solid var(--line-faint);
	}

	.chip {
		display: inline-flex;
		align-items: center;
		gap: var(--space-3xs);

		padding: 0.5rem 0.85rem;
		border: 0;
		border-radius: var(--radius-pill);

		color: var(--text-2);
		font-size: var(--text-xs);
		font-weight: var(--weight-semibold);
		white-space: nowrap;
		cursor: pointer;
	}

	.chip .icon {
		color: var(--accent);
		font-size: 0.95em;
	}

	/* Pressed state is the relief pressing in, plus an accent label. Colour
	   alone would not survive a greyscale print or a colour-blind reader, so
	   the depth change carries the meaning and the colour reinforces it. */
	.chip[aria-pressed='true'] {
		color: var(--text-1);
	}

	.chip[aria-pressed='true'] .chip__n {
		background: rgb(var(--accent-rgb) / 0.22);
		color: var(--accent-strong);
	}

	.chip__n {
		display: inline-grid;
		place-items: center;
		min-width: 1.35em;
		padding-inline: 0.35em;
		border-radius: var(--radius-pill);
		background: rgb(var(--text-1-rgb) / 0.07);

		color: var(--text-3);
		font-family: var(--font-mono);
		font-size: 0.85em;
		font-variant-numeric: tabular-nums;
		line-height: 1.5;
	}

	.console__cloud {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-2xs);
		align-content: flex-start;

		/* Reserve roughly two rows so switching between a 5-tag and a 35-tag
		   filter does not yank the rest of the page up and down. Two rows is
		   the compromise: enough to stop the jump, not so much that a short
		   filter leaves the console looking empty. */
		min-height: 5rem;
	}

	/* A tag filtered out is removed from the flow entirely rather than dimmed:
	   the point of the console is to answer a question, and a field of greyed
	   noise answers it worse than a short list. */
	.console__cloud .skill[hidden] {
		display: none;
	}

	@media (max-width: 40rem) {
		.console {
			padding: var(--space-sm);
		}

		.console__filters {
			/* A wrapped six-key grid eats the screen on a phone. One scrollable
			   row keeps the tag field visible while filtering. */
			flex-wrap: nowrap;
			overflow-x: auto;
			overscroll-behavior-inline: contain;
			scrollbar-width: none;
			margin-inline: calc(-1 * var(--space-sm));
			padding-inline: var(--space-sm);
		}

		.console__filters::-webkit-scrollbar {
			display: none;
		}
	}

/* --------------------------------------------------------------------------
	   SKILL CHIP
	   A small physical key: embossed, sinks on press, lights up on hover.
	   -------------------------------------------------------------------------- */
	.skill {
		display: inline-flex;
		align-items: center;
		gap: var(--space-3xs);

		padding: 0.38rem 0.7rem;
		border: 1px solid var(--line);
		border-radius: var(--radius-sm);

		background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
		box-shadow: var(--elev-1), var(--emboss);

		color: var(--text-2);
		font-size: var(--text-xs);
		font-weight: var(--weight-medium);
		white-space: nowrap;
		cursor: default;

		transition:
			transform var(--dur-fast) var(--ease-spring),
			border-color var(--t-fast),
			color var(--t-fast),
			box-shadow var(--t-fast);
	}

	@media (hover: hover) {
		.skill:hover {
			transform: translate3d(0, -3px, 0);
			border-color: rgb(var(--accent-rgb) / 0.45);
			box-shadow: var(--elev-2), var(--bloom-accent), var(--emboss);
			color: var(--text-1);
		}
	}

	.skill:active {
		transform: translate3d(0, 1px, 0);
		box-shadow: var(--press);
	}

	/* Skills the owner considers primary get a quiet accent dot. */
	.skill--core::before {
		content: '';
		width: 0.35rem;
		height: 0.35rem;
		border-radius: var(--radius-circle);
		background: var(--accent);
		box-shadow: 0 0 6px rgb(var(--accent-rgb) / 0.8);
		flex-shrink: 0;
	}

	/* --------------------------------------------------------------------------
	   MARQUEE
	   A slow, continuous band of technologies under the skill groups. Pauses
	   on hover and when focus lands inside it. Duplicated content makes the
	   loop seamless; the copy is aria-hidden so it isn't read twice.
	   -------------------------------------------------------------------------- */
	.marquee {
		position: relative;
		margin-block-start: var(--space-lg);
		padding-block: var(--space-sm);
		border-block: 1px solid var(--line-faint);
		overflow: hidden;

		-webkit-mask-image: linear-gradient(
			90deg,
			transparent,
			#000 12%,
			#000 88%,
			transparent
		);
		mask-image: linear-gradient(
			90deg,
			transparent,
			#000 12%,
			#000 88%,
			transparent
		);
	}

	.marquee__track {
		display: flex;
		width: max-content;
		gap: var(--space-lg);
		animation: marquee-slide 42s linear infinite;
	}

	.marquee:hover .marquee__track,
	.marquee:focus-within .marquee__track {
		animation-play-state: paused;
	}

	.marquee__item {
		display: inline-flex;
		align-items: center;
		gap: var(--space-2xs);
		color: var(--text-4);
		font-size: var(--text-sm);
		font-weight: var(--weight-semibold);
		letter-spacing: var(--tracking-tight);
		white-space: nowrap;
		transition: color var(--t-fast);
	}

	.marquee__item:hover {
		color: var(--accent);
	}

	@keyframes marquee-slide {
		from {
			transform: translate3d(0, 0, 0);
		}
		to {
			/* Exactly half — the track holds two identical copies. */
			transform: translate3d(-50%, 0, 0);
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.marquee__track {
			animation: none;
			flex-wrap: wrap;
			width: 100%;
			justify-content: center;
			gap: var(--space-sm) var(--space-md);
		}

		/* With no motion the duplicate copy is pure noise. */
		.marquee__track [aria-hidden='true'] {
			display: none;
		}
	}

	/* ==========================================================================
	   CONTACT FORM
	   (was css/components/form.css)
	   ========================================================================== */

	.contact {
		display: grid;
		grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
		gap: var(--space-xl);
		align-items: start;
	}

	.contact__aside {
		display: flex;
		flex-direction: column;
		gap: var(--space-md);
	}

	.contact__lede {
		color: var(--text-2);
		font-size: var(--text-md);
		line-height: var(--leading-relaxed);
		text-wrap: pretty;
	}

	/* Direct-contact rows — for people who would rather not use a form. */
	.contact__direct {
		display: flex;
		flex-direction: column;
		gap: var(--space-3xs);
	}

	.contact__row {
		display: flex;
		align-items: center;
		gap: var(--space-sm);

		padding: var(--space-2xs) var(--space-xs);
		border: 1px solid transparent;
		border-radius: var(--radius-md);

		color: var(--text-2);
		font-size: var(--text-sm);

		transition:
			background-color var(--t-fast),
			border-color var(--t-fast),
			transform var(--dur-base) var(--ease-out);
	}

	.contact__row:hover {
		border-color: var(--line);
		background: var(--surface-2);
		color: var(--text-1);
		transform: translate3d(3px, 0, 0);
	}

	.contact__row .icon {
		display: grid;
		place-items: center;
		width: 2rem;
		height: 2rem;
		/* The tile is the element; the artwork needs room inside it. An <svg>
		   with border-box sizing scales its viewBox into the content area, so
		   padding insets the drawing rather than the box. */
		padding: 0.5rem;
		border: 1px solid var(--line);
		border-radius: var(--radius-sm);
		background: linear-gradient(180deg, var(--surface-4), var(--surface-2));
		box-shadow: var(--emboss);
		color: var(--accent);
		font-size: var(--text-xs);
		flex-shrink: 0;
	}

	.contact__row-label {
		display: block;
		color: var(--text-4);
		font-size: var(--text-2xs);
		letter-spacing: var(--tracking-wide);
		text-transform: uppercase;
	}

	.contact__row-value {
		font-weight: var(--weight-medium);
	}

	/* --------------------------------------------------------------------------
	   FORM SHELL
	   -------------------------------------------------------------------------- */
	.form {
		display: flex;
		flex-direction: column;
		gap: var(--space-md);
		padding: var(--space-lg);
		border-radius: var(--radius-xl);
	}

	.form.is-shaking {
		animation: shake var(--dur-slow) var(--ease-in-out);
	}

	/* --------------------------------------------------------------------------
	   FIELD
	   -------------------------------------------------------------------------- */
	.field {
		position: relative;
		display: flex;
		flex-direction: column;
	}

	.field__input,
	.field__textarea {
		width: 100%;
		padding: 1.35rem var(--space-sm) 0.6rem;
		border: 1px solid var(--line);
		border-radius: var(--radius-md);

		background: var(--canvas-deep);
		box-shadow: var(--deboss);

		color: var(--text-1);
		font-size: var(--text-sm);

		transition:
			border-color var(--t-base),
			box-shadow var(--t-base),
			background-color var(--t-base);
	}

	.field__textarea {
		min-height: 8rem;
		padding-block-start: 1.6rem;
		line-height: var(--leading-normal);
	}

	.field__input:hover,
	.field__textarea:hover {
		border-color: var(--line-strong);
	}

	.field__input:focus,
	.field__textarea:focus {
		outline: none;
		border-color: var(--accent);
		box-shadow:
			var(--deboss),
			0 0 0 3px rgb(var(--accent-rgb) / 0.18);
	}

	/* --- Floating label ----------------------------------------------------- */
	.field__label {
		position: absolute;
		inset-block-start: 0.95rem;
		inset-inline-start: calc(var(--space-sm) + 1px);
		z-index: 1;

		color: var(--text-3);
		font-size: var(--text-sm);
		pointer-events: none;
		transform-origin: left center;

		transition:
			transform var(--dur-base) var(--ease-out),
			color var(--dur-base) var(--ease-out);
	}

	/* `:placeholder-shown` is how we detect "empty" without any JS. Fields
	   therefore need a placeholder of " " in the markup. */
	.field__input:focus + .field__label,
	.field__textarea:focus + .field__label,
	.field__input:not(:placeholder-shown) + .field__label,
	.field__textarea:not(:placeholder-shown) + .field__label {
		transform: translateY(-0.62rem) scale(0.78);
		color: var(--accent);
	}

	.field__input:not(:placeholder-shown):not(:focus) + .field__label,
	.field__textarea:not(:placeholder-shown):not(:focus) + .field__label {
		color: var(--text-4);
	}

	/* --- Validation --------------------------------------------------------- */
	.field__error {
		display: block;
		max-height: 0;
		overflow: hidden;
		margin-block-start: 0;

		color: var(--danger);
		font-size: var(--text-2xs);
		font-weight: var(--weight-medium);

		transition:
			max-height var(--dur-base) var(--ease-out),
			margin var(--dur-base) var(--ease-out),
			opacity var(--dur-fast) var(--ease-out);
		opacity: 0;
	}

	.field.has-error .field__error {
		max-height: 3rem;
		margin-block-start: var(--space-3xs);
		opacity: 1;
	}

	.field.has-error .field__input,
	.field.has-error .field__textarea {
		border-color: var(--danger);
		background: var(--danger-soft);
	}

	.field.has-error .field__label {
		color: var(--danger);
	}

	/* --------------------------------------------------------------------------
	   FORM STATUS
	   -------------------------------------------------------------------------- */
	.form__status {
		display: flex;
		align-items: center;
		gap: var(--space-2xs);

		max-height: 0;
		overflow: hidden;
		padding-inline: var(--space-sm);
		border-radius: var(--radius-md);

		font-size: var(--text-sm);
		font-weight: var(--weight-medium);
		opacity: 0;

		transition:
			max-height var(--dur-base) var(--ease-out),
			padding var(--dur-base) var(--ease-out),
			opacity var(--dur-base) var(--ease-out);
	}

	.form__status.is-visible {
		max-height: 6rem;
		padding-block: var(--space-2xs);
		opacity: 1;
	}

	.form__status--success {
		border: 1px solid rgb(74 222 128 / 0.3);
		background: rgb(74 222 128 / 0.1);
		color: var(--success);
	}

	.form__status--error {
		border: 1px solid rgb(251 113 133 / 0.3);
		background: var(--danger-soft);
		color: var(--danger);
	}

	/* --------------------------------------------------------------------------
	   SUBMIT — spinner state
	   -------------------------------------------------------------------------- */
	.btn.is-loading {
		color: transparent;
		pointer-events: none;
	}

	.btn.is-loading::before {
		content: '';
		position: absolute;
		width: 1rem;
		height: 1rem;
		border: 2px solid rgb(255 255 255 / 0.3);
		border-top-color: var(--text-on-accent);
		border-radius: var(--radius-circle);
		animation: spin 0.7s linear infinite;
	}

	/* --------------------------------------------------------------------------
	   RESPONSIVE
	   -------------------------------------------------------------------------- */
	@media (max-width: 62rem) {
		.contact {
			grid-template-columns: minmax(0, 1fr);
			gap: var(--space-lg);
		}
	}

	@media (max-width: 40rem) {
		.form {
			padding: var(--space-md);
		}
	}

	/* ==========================================================================
	   FLOATING CONTROLS & PRELOADER
	   (was css/components/controls.css)
	   ========================================================================== */

	/* --------------------------------------------------------------------------
	   THEME TOGGLE
	   Two states: light and dark. A real switch — debossed track, a knob that
	   travels between two detents and casts its shadow into the well. Icons sit
	   beneath the knob and light up as it arrives.
	   -------------------------------------------------------------------------- */
	.theme-toggle {
		position: relative;
		display: inline-flex;
		align-items: center;

		width: 4rem;
		height: 2.1rem;
		padding: 0.2rem;
		border: 1px solid var(--line-strong);
		border-radius: var(--radius-pill);

		background: var(--canvas-deep);
		box-shadow: var(--deboss);

		cursor: grab;
		flex-shrink: 0;

		/* Claim horizontal gestures for the switch, leave vertical to the page. */
		touch-action: pan-y;
		-webkit-user-select: none;
		user-select: none;
	}

	.theme-toggle:active,
	.theme-toggle.is-dragging {
		cursor: grabbing;
	}

	/* The travelling knob. --knob-i is 0 for light, 1 for dark, and takes
	   fractional values while being dragged. */
	.theme-toggle__knob {
		position: absolute;
		inset-block: 0.2rem;
		inset-inline-start: 0.2rem;
		z-index: 1;

		width: calc((100% - 0.4rem) / 2);
		border-radius: var(--radius-pill);

		background: linear-gradient(180deg, var(--surface-4), var(--surface-2));
		box-shadow:
			var(--elev-2),
			inset 0 1px 0 0 rgb(255 255 255 / 0.2),
			inset 0 -1px 0 0 rgb(0 0 0 / 0.25);

		transform: translateX(calc(var(--knob-i, 0) * 100%));
		transition: transform var(--dur-base) var(--ease-spring);
	}

	/* While dragging the knob must track the pointer exactly, so the spring
	   easing has to get out of the way. */
	.theme-toggle.is-dragging .theme-toggle__knob {
		transition: none;
	}

	.theme-toggle:active .theme-toggle__knob {
		box-shadow: var(--elev-1), var(--press);
	}

	.theme-toggle__opt {
		position: relative;
		z-index: 2;

		display: grid;
		place-items: center;
		flex: 1;
		height: 100%;

		color: var(--text-4);
		font-size: 0.72rem;

		transition: color var(--dur-base) var(--ease-out);
		pointer-events: none;
	}

	.theme-toggle__opt.is-active {
		color: var(--accent-strong);
	}

	/* --------------------------------------------------------------------------
	   FLOATING CONTROLS

	   THE INSTRUMENT BAR

	   One debossed dock holding every page-level control. It replaces four
	   separately-positioned fixed layers, which means one z-index, one set of
	   safe-area calculations, and one composited layer instead of four.

	   This is where neumorphism belongs and where the brief scopes it to:
	   a physical instrument you operate, not a surface you read through. The
	   dock is a .well; every key inside it is a .pane.

	   Kept clear of the scrollbar rail and of any device inset (a home
	   indicator, or a curved screen corner). The safe-area values resolve to
	   zero where there is no inset, and --scrollbar-w is zeroed on touch
	   where the rail is hidden, so this needs no breakpoint of its own.
	   -------------------------------------------------------------------------- */
	.instrument {
		position: fixed;
		inset-block-end: calc(var(--space-md) + env(safe-area-inset-bottom, 0px));
		inset-inline: 0;
		z-index: var(--z-controls);

		display: flex;
		align-items: center;
		gap: var(--space-2xs);

		width: max-content;
		margin-inline: auto;
		padding: var(--space-3xs) var(--space-2xs);
		border-radius: var(--radius-pill);

		/* The dock itself is opaque relief, not glass. A translucent dock over
		   scrolling content is exactly the case where a blurred backdrop
		   reads as smeared rather than as material. */
		background: var(--well-face);
		box-shadow: var(--relief-well), var(--elev-3);
	}

	.instrument__keys {
		display: flex;
		align-items: center;
		gap: var(--space-3xs);
	}

	.instrument__sep {
		width: 1px;
		height: 1.4rem;
		background: var(--line);
		flex-shrink: 0;
	}

	/* --- THE SCROLL CONTROL --------------------------------------------------
	   One button, two states.

	   [data-at-top]  ring empty, chevron points down and falls on a loop
	   (no attr)      ring fills with --scroll-p, chevron points up

	   The state swap animates `rotate` and `opacity` only. The ring itself is
	   driven entirely by --scroll-p, which core/scroll.js already publishes
	   once per frame — no listener, no per-frame JavaScript, and the arc stays
	   exact during a fling because the compositor owns it.

	   The circumference is 2*pi*r with r = 15.6, i.e. 98.02. */
	.scroller {
		position: relative;
		display: grid;
		place-items: center;
		width: var(--to-top-size);
		height: var(--to-top-size);
		border: 0;
		border-radius: var(--radius-circle);
		background: var(--relief-face);
		box-shadow: var(--relief-raised);
		color: var(--text-1);
		cursor: pointer;
		flex-shrink: 0;

		transition:
			box-shadow var(--dur-base) var(--ease-out),
			background var(--dur-base) var(--ease-out);
	}

	@media (hover: hover) {
		.scroller:hover {
			box-shadow: var(--relief-raised-hi);
		}
	}

	.scroller:active {
		box-shadow: var(--relief-pressed);
		background: var(--relief-face-press);
	}

	.scroller__ring {
		position: absolute;
		inset: 3px;
		width: auto;
		height: auto;
		/* Start the arc at 12 o'clock rather than 3. */
		transform: rotate(-90deg);
		overflow: visible;
	}

	.scroller__ring circle {
		fill: none;
		stroke-width: 2.4;
		stroke-linecap: round;
	}

	.scroller__track {
		stroke: var(--line-strong);
	}

	.scroller__value {
		stroke: var(--accent);
		stroke-dasharray: 98.02;
		stroke-dashoffset: calc(98.02 - 98.02 * var(--scroll-p, 0));
		transition: opacity var(--dur-base) var(--ease-out);
	}

	/* At the top there is no progress worth drawing, so the arc is hidden and
	   the ring reads as an empty target rather than as a stuck 0%. */
	.scroller[data-at-top] .scroller__value {
		opacity: 0;
	}

	.scroller__chev {
		display: grid;
		place-items: center;
		font-size: var(--text-sm);
		/* Up is the resting orientation; the top state rotates it down. */
		rotate: 0deg;
		transition: rotate var(--dur-base) var(--ease-spring);
	}

	.scroller[data-at-top] .scroller__chev {
		rotate: 180deg;
		animation: scroller-fall 1.9s var(--ease-in-out) infinite;
	}

	/* The same falling gesture the hero rail used, so the affordance is
	   familiar even though it has moved. */
	@keyframes scroller-fall {
		0%,
		100% {
			translate: 0 -2px;
			opacity: 0.55;
		}
		50% {
			translate: 0 2px;
			opacity: 1;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.scroller[data-at-top] .scroller__chev {
			animation: none;
		}
	}

	/* --------------------------------------------------------------------------
	   PRELOADER
	   The `</>` mark draws itself in step with real load progress rather than on
	   a fixed timer, so the animation reflects what the page is actually doing.
	   Progress is driven from JS via --draw (0 to 1) per path.
	   -------------------------------------------------------------------------- */
	.preloader {
		position: fixed;
		inset: 0;
		z-index: var(--z-preloader);

		display: grid;
		place-items: center;
		background: var(--canvas);

		transition:
			opacity var(--dur-slow) var(--ease-out),
			visibility var(--dur-slow) var(--ease-out);
	}

	.preloader.is-done {
		opacity: 0;
		visibility: hidden;
	}

	.preloader__inner {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: var(--space-md);
		width: min(18rem, 70vw);
	}

	/* --- The mark, with a light bloom behind it -------------------------- */
	.preloader__glyph {
		position: relative;
		display: grid;
		place-items: center;
	}

	.preloader__halo {
		position: absolute;
		width: 9rem;
		height: 9rem;
		border-radius: var(--radius-circle);
		background: radial-gradient(
			circle,
			rgb(var(--accent-rgb) / 0.28),
			transparent 65%
		);
		filter: blur(14px);
		animation: preloader-breathe 2.6s var(--ease-in-out) infinite;
	}

	@keyframes preloader-breathe {
		0%,
		100% {
			opacity: 0.55;
			transform: scale(0.92);
		}
		50% {
			opacity: 1;
			transform: scale(1.08);
		}
	}

	.preloader__mark {
		position: relative;
		z-index: 1;
		width: 4.25rem;
		height: 4.25rem;
		color: var(--accent-strong);
		overflow: visible;
	}

	.preloader__mark path {
		fill: none;
		stroke: currentColor;
		stroke-width: 5;
		stroke-linecap: round;
		stroke-linejoin: round;
		/* dasharray / dashoffset are set from JS using each path's real length. */
		filter: drop-shadow(0 0 7px rgb(var(--accent-rgb) / 0.65));
	}

	/* --- Progress rail ---------------------------------------------------- */
	.preloader__bar {
		width: 100%;
		height: 3px;
		border-radius: var(--radius-pill);
		background: var(--surface-3);
		overflow: hidden;
	}

	.preloader__fill {
		display: block;
		width: 100%;
		height: 100%;
		border-radius: inherit;
		background: linear-gradient(90deg, var(--accent) 0%, var(--glow) 100%);
		box-shadow: 0 0 12px rgb(var(--accent-rgb) / 0.7);

		transform: scaleX(var(--progress, 0));
		transform-origin: left center;
	}

	/* --- Status line ------------------------------------------------------- */
	.preloader__meta {
		display: flex;
		align-items: center;
		gap: var(--space-3xs);
		width: 100%;
		margin: 0;

		color: var(--text-4);
		font-family: var(--font-mono);
		font-size: var(--text-2xs);
		font-variant-numeric: tabular-nums;
		letter-spacing: var(--tracking-wide);
	}

	.preloader__status {
		color: var(--text-3);
	}

	.preloader__caret {
		width: 6px;
		height: 1em;
		background: var(--glow);
		animation: caret-blink 1s steps(1, end) infinite;
	}

	.preloader__pct {
		margin: 0 0 0 auto;
		color: var(--accent-strong);
		font-weight: var(--weight-medium);
	}

	@media (prefers-reduced-motion: reduce) {
		.preloader__halo,
		.preloader__caret {
			animation: none;
		}
	}

	/* ==========================================================================
	   FOOTER
	   (was css/components/footer.css)
	   ========================================================================== */

	.footer {
		position: relative;
		/* No top margin: the final section already contributes a full
		   --section-gap of bottom padding, and stacking the two left an obvious
		   dead band between the contact form and the footer rule. */
		padding-block-start: var(--space-lg);
		/* Keep the closing line clear of the centred instrument dock. ONE
		   declaration: this rule previously carried two padding-block-end
		   lines, and the later --footer-pad-end silently won, which is why the
		   dock sat on top of the copyright on a phone. */
		padding-block-end: var(--dock-clearance);
		border-block-start: 1px solid var(--line);
		isolation: isolate;
	}

	/* A soft accent wash rising from the bottom edge: the page's last light. */
	.footer::before {
		content: '';
		position: absolute;
		inset-block-end: 0;
		inset-inline: 0;
		z-index: -1;
		height: 100%;
		background: linear-gradient(
			180deg,
			transparent,
			rgb(var(--accent-rgb) / 0.06)
		);
		pointer-events: none;
	}

	.footer__bottom {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-2xs) var(--space-md);

		/* The instrument dock is centred at the foot of the viewport, so the
		   footer no longer needs a right-hand lane — it needs bottom
		   clearance, which .footer provides via --dock-clearance. */

		color: var(--text-4);
		font-size: var(--text-xs);
	}

	.footer__copy {
		margin: 0;
	}

	/* Deliberately NOT inline-flex. As a flex container each text run becomes a
	   flex item, so on a narrow screen the line breaks between those runs rather
	   than between words — "Built / with ❤ and rather a lot / of coffee". Left as
	   normal flowing text it wraps like prose, and the icon is aligned with
	   vertical-align instead of align-items. */
	.footer__built {
		margin: 0;
		color: var(--text-4);
		text-wrap: balance;
	}

	.footer__built .icon {
		color: var(--accent-strong);
		vertical-align: -0.06em;
		margin-inline: 0.12em;
	}

	/* Made interactive by unlock.js. Given just enough affordance to invite a
	   tap without announcing itself as a control. */
	.footer__built.is-tappable {
		cursor: pointer;
		border-radius: var(--radius-pill);
		padding-inline: var(--space-2xs);
		transition: color var(--t-fast), background-color var(--t-fast);
	}

	.footer__built.is-tappable:hover,
	.footer__built.is-tappable:focus-visible {
		background: rgb(var(--accent-rgb) / 0.08);
		color: var(--text-2);
	}

	.footer__built.is-tappable:active .icon {
		scale: 1.25;
	}

	.footer__built.is-tappable .icon {
		transition: scale var(--dur-fast) var(--ease-spring);
	}

	/* --------------------------------------------------------------------------
	   RESPONSIVE

	   Below this width the footer stacks and centres. Centred text is padded
	   symmetrically rather than only on the end, so it stays centred while still
	   stopping short of the docked button on the right. That padding replaces the
	   extra bottom padding this used to carry on narrow screens, which pushed the
	   content up and left a visible band of dead space under the last line.
	   -------------------------------------------------------------------------- */
	@media (max-width: 52rem), (max-height: 34rem) {
		.footer__bottom {
			flex-direction: column;
			align-items: center;
			gap: var(--space-2xs);
			text-align: center;

			padding-inline: var(--space-sm);
		}
	}

	/* ==========================================================================
	   AMBIENT BACKDROP
	   (was css/components/ambient.css)
	   ========================================================================== */

	.ambient {
		position: fixed;
		inset: 0;
		z-index: var(--z-behind);
		overflow: hidden;
		pointer-events: none;
		contain: strict;
	}

	/* --------------------------------------------------------------------------
	   SPOTLIGHT BORDER

	   A hairline ring whose bright arc sits where the pointer is. It reads as
	   the card catching light from the cursor, which is the cheapest way to
	   make a grid of cards feel responsive without moving any of them.

	   Cut with the same padding + XOR mask as the edge lens, so it is one
	   pseudo-element and no extra layer. --mx/--my are already published per
	   surface by the pointer pipeline, so this costs nothing new per frame.

	   It sits on ::before, which for a non-lens card is otherwise unused; the
	   pseudo-element budget is unchanged.
	   -------------------------------------------------------------------------- */
	@supports ((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
		.card-spot::before {
			content: '';
			position: absolute;
			inset: 0;
			z-index: 3;
			pointer-events: none;
			border-radius: inherit;
			padding: 1px;

			background: radial-gradient(
				12rem 12rem at var(--mx) var(--my),
				rgb(var(--glow-rgb) / 0.85),
				rgb(var(--accent-rgb) / 0.45) 40%,
				transparent 70%
			);

			-webkit-mask:
				linear-gradient(#000 0 0) content-box,
				linear-gradient(#000 0 0);
			-webkit-mask-composite: xor;
			mask:
				linear-gradient(#000 0 0) content-box,
				linear-gradient(#000 0 0);
			mask-composite: exclude;

			opacity: 0;
			transition: opacity var(--dur-base) var(--ease-out);
		}

		@media (hover: hover) {
			.card-spot:hover::before,
			.card-spot:focus-within::before {
				opacity: 1;
			}
		}
	}

	/* Content depth: the media travels further than the text, so the card
	   gains parallax without tilting — which keeps every glyph crisp. */
	@media (hover: hover) {
		.card-spot:hover .project-card__media,
		.card-spot:hover .spotlight__media {
			translate: 0 -4px;
		}

		.card-spot:hover .project-card__body,
		.card-spot:hover .spotlight__body {
			translate: 0 -1.5px;
		}
	}

	.project-card__media,
	.spotlight__media,
	.project-card__body,
	.spotlight__body {
		transition: translate var(--dur-base) var(--ease-out);
	}

	/* --------------------------------------------------------------------------
	   MESH

	   One element. Four radial stops whose POSITIONS animate, on two long
	   mutually-prime cycles so the composition never visibly repeats.

	   This replaced three blurred bands drifting inside a filtered parent.
	   That version paid for a full-size offscreen buffer and a blur pass on
	   every frame any band moved; this one is a single gradient re-resolved on
	   the compositor, with no children, no filter and no transform.
	   -------------------------------------------------------------------------- */
	.ambient__mesh {
		position: absolute;
		inset: -10%;
		opacity: 0.85;

		background:
			radial-gradient(
				38rem 32rem at var(--m1x) var(--m1y),
				rgb(var(--accent-rgb) / 0.3),
				transparent 62%
			),
			radial-gradient(
				32rem 30rem at var(--m2x) var(--m2y),
				rgb(var(--glow-rgb) / 0.22),
				transparent 64%
			),
			radial-gradient(
				44rem 34rem at var(--m3x) var(--m3y),
				rgb(var(--accent-rgb) / 0.16),
				transparent 66%
			),
			radial-gradient(
				30rem 34rem at var(--m4x) var(--m4y),
				rgb(var(--glow-rgb) / 0.14),
				transparent 62%
			);

		animation:
			mesh-drift-a 47s var(--ease-in-out) infinite alternate,
			mesh-drift-b 61s var(--ease-in-out) infinite alternate;
	}

	[data-theme='light'] .ambient__mesh {
		/* Colour reads far more strongly against a pale canvas. */
		opacity: 0.5;
	}

	@keyframes mesh-drift-a {
		to {
			--m1x: 34%;
			--m1y: 30%;
			--m3x: 44%;
			--m3y: 72%;
		}
	}

	@keyframes mesh-drift-b {
		to {
			--m2x: 66%;
			--m2y: 8%;
			--m4x: 24%;
			--m4y: 88%;
		}
	}

	/* --------------------------------------------------------------------------
	   HORIZON

	   A single wide arc that rises as the page is read. It gives scrolling a
	   sense of travelling through a space rather than sliding over a texture,
	   and it costs one `translate` driven by --scroll-p, which the scroll task
	   already publishes.
	   -------------------------------------------------------------------------- */
	.ambient__horizon {
		position: absolute;
		inset-inline: -20%;
		inset-block-start: 55%;
		height: 90%;

		border-start-start-radius: 50%;
		border-start-end-radius: 50%;

		background: linear-gradient(
			180deg,
			rgb(var(--accent-rgb) / 0.08),
			transparent 45%
		);
		/* A hairline along the crest, faded out toward the edges. Without the
		   mask the arc reads as a drawn line across the page rather than as a
		   horizon; the fade is what turns an edge into distance. */
		box-shadow: 0 -1px 0 0 rgb(var(--accent-rgb) / 0.14);
		mask-image: linear-gradient(
			90deg,
			transparent,
			#000 22%,
			#000 78%,
			transparent
		);

		translate: 0 calc(var(--scroll-p, 0) * -26%);
	}

	@media (prefers-reduced-motion: reduce) {
		.ambient__horizon {
			translate: none;
		}
	}

	/* --------------------------------------------------------------------------
	   2. THE FIELD
	   The grid is not wallpaper: it is a surface receding into the page. A shared
	   perspective parent gives it a horizon, and scrolling changes the angle you
	   are looking at it from, so the background has depth that the content sits
	   in front of rather than on top of.
	   -------------------------------------------------------------------------- */
	.ambient__field {
		position: absolute;
		inset: 0;

		/* No perspective any more. It existed to tilt the ruled grid into a
		   receding plane; a dot field needs no vanishing point, and dropping it
		   removes a preserve-3d subtree from the compositor. */

		/* Velocity shear. --scroll-v is 0 at rest, so this is identity until
		   someone actually moves, then the whole surface stretches into the
		   direction of travel and settles back. */
		transform: scaleY(calc(1 + var(--scroll-v, 0) * 0.06))
			translateY(calc(var(--scroll-v, 0) * var(--scroll-dir, 1) * -14px));
		transform-origin: 50% 30%;
		transition: transform 220ms var(--ease-out);
	}

	/* --------------------------------------------------------------------------
	   THE DOT FIELD

	   A fine matrix of dots rather than a ruled grid.

	   The grid read as graph paper: two sets of hard 1px rules on a 72px
	   pitch, tilted in perspective, is a technical drawing, and it competed
	   with the content for attention the entire way down the page. Dots carry
	   the same sense of a measured surface without drawing a single line —
	   they read as texture, and the eye stops trying to follow them.

	   Two layers on the same pitch: a quiet base, and a brighter copy masked
	   to a soft disc that follows the cursor. Moving the pointer lights the
	   surface under it, which is the interaction — no hover targets, no
	   affordance to learn, the whole background simply responds.
	   -------------------------------------------------------------------------- */
	.ambient__grid,
	.ambient__grid-glow {
		position: absolute;
		inset: 0;
		backface-visibility: hidden;
	}

	.ambient__grid {
		inset: -6%;

		/* One small dot per cell.
		
		   The radius must be EXPLICIT. `closest-side` sizes the circle to the
		   cell, which fills each 34px tile edge to edge and produces a
		   checkerboard of overlapping blobs — the exact pattern this replaced.
		   1.3px is a point of light; 17px is wallpaper. */
		background-image: radial-gradient(
			circle 1.3px at 50% 50%,
			var(--grid-line) 0 99%,
			transparent 100%
		);
		background-size: 34px 34px;
		background-position: 0 0;

		/* Present at the top where there is room for texture, gone before the
		   prose starts. */
		-webkit-mask-image: linear-gradient(
			to bottom,
			#000 0%,
			#000 42%,
			rgb(0 0 0 / 0.35) 66%,
			transparent 86%
		);
		mask-image: linear-gradient(
			to bottom,
			#000 0%,
			#000 42%,
			rgb(0 0 0 / 0.35) 66%,
			transparent 86%
		);
	}

	/* The lit patch: the same field in accent, revealed only where the pointer
	   is. Because both layers share a pitch and an origin, the bright dots sit
	   exactly on top of the quiet ones and the surface appears to illuminate
	   rather than to gain a second pattern. */
	.ambient__grid-glow {
		background-image: radial-gradient(
			circle 1.6px at 50% 50%,
			rgb(var(--accent-rgb) / 0.9) 0 99%,
			transparent 100%
		);
		background-size: 34px 34px;
		background-position: 0 0;

		opacity: var(--grid-lit, 0);
		-webkit-mask-image: radial-gradient(
			circle var(--glow-size, 300px) at var(--gx, 50%) var(--gy, 32%),
			#000,
			rgb(0 0 0 / 0.35) 45%,
			transparent 72%
		);
		mask-image: radial-gradient(
			circle var(--glow-size, 300px) at var(--gx, 50%) var(--gy, 32%),
			#000,
			rgb(0 0 0 / 0.35) 45%,
			transparent 72%
		);

		transition: opacity var(--dur-slow) var(--ease-out);
	}

	/* No cursor to follow — on a phone, or on a desktop where nobody has moved
	   the mouse yet. The light wanders instead, on prime-ish cycles so it never
	   retraces the same path. --gx / --gy only interpolate because tokens.css
	   registers them as real lengths. */
	.ambient__grid-glow.is-wandering {
		animation:
			wander-x 37s var(--ease-in-out) infinite alternate,
			wander-y 23s var(--ease-in-out) infinite alternate,
			wander-lit 19s var(--ease-in-out) infinite alternate;
	}

	@keyframes wander-x {
		from { --gx: 22%; }
		to   { --gx: 78%; }
	}

	@keyframes wander-y {
		from { --gy: 16%; }
		to   { --gy: 54%; }
	}

	@keyframes wander-lit {
		0%   { --grid-lit: 0.30; --glow-size: 260px; }
		50%  { --grid-lit: 0.65; --glow-size: 400px; }
		100% { --grid-lit: 0.38; --glow-size: 300px; }
	}

	/* --------------------------------------------------------------------------
	   REDUCED MOTION
	   The mesh and the field stay — they are part of the page's colour — but
	   they stop moving.
	   -------------------------------------------------------------------------- */
	@media (prefers-reduced-motion: reduce) {
		.ambient__mesh {
			animation: none;
		}

		.ambient__grid-glow.is-wandering,
		/* Keep the surface, drop the response to how hard someone scrolled. */
		.ambient__field {
			transform: none;
			transition: none;
		}
	}

	/* Where the grid would be noise rather than texture, drop it. */
	@media (prefers-contrast: more) {
		.ambient__field,
		.ambient__mesh {
			display: none;
		}
	}

	/* ==========================================================================
	   SIGNATURE MOMENTS — scroll-driven, CSS only
	   (was css/components/moments.css)
	   ========================================================================== */

	@supports (animation-timeline: view()) {
		/* ----------------------------------------------------------------------
		   ABOUT — the portrait develops

		   It arrives out of focus and drained of colour, and resolves as it
		   reaches the reading position, like a print coming up in a tray. Tied
		   to `entry`, so it is the reader's own scrolling that develops it.
		   ---------------------------------------------------------------------- */
		#profile-pic {
			animation: portrait-develop linear both;
			animation-timeline: view();
			animation-range: entry 10% entry 85%;
		}

		@keyframes portrait-develop {
			from {
				filter: blur(14px) saturate(0.25) contrast(0.9);
				scale: 1.06;
			}
			to {
				filter: blur(0) saturate(1) contrast(1);
				scale: 1;
			}
		}

		/* ----------------------------------------------------------------------
		   EDUCATION — cards deal in

		   Each card is dealt onto the surface with a slight rotation, alternating
		   side to side so the group has rhythm instead of a single uniform slide.
		   ---------------------------------------------------------------------- */
		.rail__item {
			animation: deal-in var(--ease-spring) both;
			animation-timeline: view();
			animation-range: entry 5% entry 80%;
		}

		.rail__item:nth-child(even) {
			animation-name: deal-in-alt;
		}

		@keyframes deal-in {
			from {
				opacity: 0;
				translate: -1.4rem 1.6rem;
				rotate: -3.5deg;
			}
			to {
				opacity: 1;
				translate: 0 0;
				rotate: 0deg;
			}
		}

		@keyframes deal-in-alt {
			from {
				opacity: 0;
				translate: 1.4rem 1.6rem;
				rotate: 3.5deg;
			}
			to {
				opacity: 1;
				translate: 0 0;
				rotate: 0deg;
			}
		}

		/* ----------------------------------------------------------------------
		   SKILLS — a wave through the chips

		   --i is already set per chip in the markup for the existing cascade, so
		   the wave rides on data that is there rather than needing new hooks.
		   The range is offset by --i, which is what actually staggers them: a
		   delay would eat into the range instead of postponing it.
		   ---------------------------------------------------------------------- */
		.skill {
			animation: chip-rise var(--ease-spring) both;
			animation-timeline: view();
			animation-range: entry calc(4% + var(--i, 0) * 2%) entry calc(60% + var(--i, 0) * 2%);
		}

		@keyframes chip-rise {
			from {
				opacity: 0;
				translate: 0 0.9rem;
				scale: 0.94;
			}
			to {
				opacity: 1;
				translate: 0 0;
				scale: 1;
			}
		}

		/* ----------------------------------------------------------------------
		   PROJECTS — one specular sweep as a card lands

		   A single pass of light across the card as it enters, and never again.
		   Because it is scroll-linked rather than triggered, scrolling back up
		   plays it in reverse, which reads as the light passing back the other
		   way rather than as a glitch.
		   ---------------------------------------------------------------------- */
		.project-card__media::after {
			content: '';
			position: absolute;
			inset: 0;
			z-index: 1;
			pointer-events: none;

			background: linear-gradient(
				104deg,
				transparent 38%,
				rgb(255 255 255 / 0.16) 48%,
				rgb(255 255 255 / 0.3) 50%,
				rgb(255 255 255 / 0.16) 52%,
				transparent 62%
			);

			animation: card-sweep linear both;
			animation-timeline: view();
			animation-range: entry 15% entry 95%;
		}

		@keyframes card-sweep {
			from {
				translate: -120% 0;
			}
			to {
				translate: 120% 0;
			}
		}

		/* ----------------------------------------------------------------------
		   CONTACT — the form assembles

		   Fields arrive in order rather than as a block, so the form reads as
		   something being set out for you.
		   ---------------------------------------------------------------------- */
		.field {
			animation: field-rise var(--ease-spring) both;
			animation-timeline: view();
			animation-range: entry calc(8% + var(--i, 0) * 6%) entry calc(70% + var(--i, 0) * 6%);
		}

		@keyframes field-rise {
			from {
				opacity: 0;
				translate: 0 1.2rem;
			}
			to {
				opacity: 1;
				translate: 0 0;
			}
		}
	}

	/* --------------------------------------------------------------------------
	   REDUCED MOTION
	   Every one of these animates something into view. Switching them off has to
	   leave the finished state behind, never the starting one.
	   -------------------------------------------------------------------------- */
	@media (prefers-reduced-motion: reduce) {
		#profile-pic,
		.ledger::before,
		.ledger__node,
		.rail__item,
		.skill,
		.field,
		.project-card__media::after {
			animation: none !important;
			opacity: 1 !important;
			filter: none !important;
			translate: none !important;
			scale: none !important;
			rotate: none !important;
		}

		/* The sweep has nothing to show when it cannot move. */
		.project-card__media::after {
			display: none;
		}
	}

	/* ==========================================================================
	   CUSTOM SCROLLBAR RAIL
	   (was css/components/scrollbar.css)
	   ========================================================================== */

	.scrollbar {
		position: fixed;
		inset-block: 0;
		inset-inline-end: 0;
		z-index: var(--z-sticky);

		/* Visible width is small; the pointer target is not. */
		width: var(--scrollbar-w);
		display: flex;
		justify-content: center;

		touch-action: none;
		-webkit-user-select: none;
		user-select: none;

		/* Fades in on scroll or hover, like a native overlay scrollbar. */
		opacity: 0;
		transition: opacity var(--dur-slow) var(--ease-out);
	}

	.scrollbar.is-active,
	.scrollbar:hover,
	.scrollbar.is-dragging {
		opacity: 1;
	}

	/* --------------------------------------------------------------------------
	   TRACK
	   -------------------------------------------------------------------------- */
	.scrollbar__track {
		position: relative;
		width: 4px;
		height: 100%;
		margin-block: var(--space-2xs);
		border-radius: var(--radius-pill);
		background: var(--line-faint);
		transition: width var(--dur-base) var(--ease-out);
	}

	.scrollbar:hover .scrollbar__track,
	.scrollbar.is-dragging .scrollbar__track {
		width: 7px;
	}

	/* --------------------------------------------------------------------------
	   THUMB
	   scrollbar.js writes --thumb-h and --thumb-y in pixels.
	   -------------------------------------------------------------------------- */
	.scrollbar__thumb {
		position: absolute;
		inset-inline: 0;
		height: var(--thumb-h, 60px);
		translate: 0 var(--thumb-y, 0px);

		border-radius: var(--radius-pill);
		background: linear-gradient(180deg, var(--accent) 0%, var(--glow) 100%);
		box-shadow: 0 0 10px -2px rgb(var(--accent-rgb) / 0.55);

		cursor: grab;
		transition: box-shadow var(--dur-base) var(--ease-out);
	}

	.scrollbar:hover .scrollbar__thumb {
		box-shadow: 0 0 16px -2px rgb(var(--accent-rgb) / 0.8);
	}

	.scrollbar.is-dragging .scrollbar__thumb {
		cursor: grabbing;
		box-shadow: 0 0 20px -2px rgb(var(--accent-rgb) / 0.95);
	}

	/* --------------------------------------------------------------------------
	   NATIVE SCROLLBAR
	   Hidden only once ours is running, so a failed script can never leave the
	   page with no scrollbar at all.
	   -------------------------------------------------------------------------- */
	html.has-scrollbar {
		scrollbar-width: none;
	}

	html.has-scrollbar::-webkit-scrollbar {
		width: 0;
		height: 0;
	}

	/* --------------------------------------------------------------------------
	   TOUCH
	   Phones have their own scroll indicator and gesture-sensitive screen edges.
	   Leave both alone.
	   -------------------------------------------------------------------------- */
	@media (hover: none), (max-width: 48rem) {
		.scrollbar {
			display: none;
		}

		/* Nothing occupies the right edge any more, so anything that was keeping
		   clear of the rail can reclaim that space. */
		:root {
			--scrollbar-w: 0px;
		}

		html.has-scrollbar {
			scrollbar-width: auto;
		}

		html.has-scrollbar::-webkit-scrollbar {
			width: 12px;
			height: 12px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.scrollbar,
		.scrollbar__track,
		.scrollbar__thumb {
			transition: none;
		}
	}

	/* ==========================================================================
	   UNLOCKABLE PALETTES — toast, trigger, picker
	   (was css/components/unlock.css)
	   ========================================================================== */

	/* --------------------------------------------------------------------------
	   PREVIEW SWATCHES
	   One source of truth for "what does this palette look like as a dot",
	   shared by the toast, the trigger and the picker. Seeds match the ones in
	   the palette generator.
	   -------------------------------------------------------------------------- */
	[data-palette-preview] {
		background-image: linear-gradient(135deg, var(--preview-a), var(--preview-b));
	}

	[data-palette-preview='default']     { --preview-a: #4ebacf; --preview-b: #9ed3f2; }
	[data-palette-preview='mint']        { --preview-a: #5cbf9c; --preview-b: #91dad5; }
	[data-palette-preview='terminal']    { --preview-a: #7fbc78; --preview-b: #c0d49b; }
	[data-palette-preview='solar']       { --preview-a: #c0a84d; --preview-b: #eac392; }
	[data-palette-preview='ember']       { --preview-a: #e69256; --preview-b: #f5bbac; }
	[data-palette-preview='crimson']     { --preview-a: #f1867e; --preview-b: #f5bba4; }
	[data-palette-preview='sakura']      { --preview-a: #e289b4; --preview-b: #f5b8be; }
	[data-palette-preview='vapor']       { --preview-a: #cd8ddc; --preview-b: #c6c5f5; }
	[data-palette-preview='aurora']      { --preview-a: #a69cf5; --preview-b: #accef5; }
	[data-palette-preview='blueprint']   { --preview-a: #79abf4; --preview-b: #90d6f5; }
	[data-palette-preview='nord']        { --preview-a: #65b3df; --preview-b: #95d7e4; }
	[data-palette-preview='mono']        { --preview-a: #a5aaae; --preview-b: #c7cbce; }

	/* --------------------------------------------------------------------------
	   TOAST
	   Bottom-left, opposite the back-to-top control so the two never meet.
	   -------------------------------------------------------------------------- */
	.unlock-toast {
		position: fixed;
		inset-block-end: var(--space-md);
		inset-inline-start: var(--space-md);
		z-index: var(--z-toast);

		display: flex;
		align-items: center;
		gap: var(--space-sm);

		width: min(27rem, calc(100vw - 2 * var(--space-md)));
		padding: var(--space-sm);
		border: 1px solid var(--glass-border);
		border-radius: var(--radius-lg);

		background-color: var(--glass-tint-strong);
		background-image: var(--glass-fill);
		-webkit-backdrop-filter: blur(var(--glass-blur-heavy)) saturate(var(--glass-saturate));
		backdrop-filter: blur(var(--glass-blur-heavy)) saturate(var(--glass-saturate));
		box-shadow: var(--elev-4), var(--rim-inset);

		opacity: 0;
		translate: 0 1rem;
		scale: 0.97;
		transition:
			opacity var(--dur-base) var(--ease-out),
			translate var(--dur-base) var(--ease-spring),
			scale var(--dur-base) var(--ease-spring);
	}

	.unlock-toast.is-in {
		opacity: 1;
		translate: 0 0;
		scale: 1;
	}

	/* The instrument dock is centred at the foot of the viewport, so the toast
	   always stacks above it rather than beside it. The offset is derived from
	   where the dock actually sits rather than restated, so moving the dock
	   cannot silently put the toast on top of it. */
	.unlock-toast {
		inset-block-end: var(--dock-clearance);
	}

	@media (max-width: 30rem) {
		.unlock-toast {
			inset-inline-start: var(--space-sm);
			width: calc(100vw - 2 * var(--space-sm));
		}
	}

	.unlock-toast__swatch {
		width: 2.25rem;
		height: 2.25rem;
		border-radius: var(--radius-circle);
		box-shadow: var(--elev-2), inset 0 1px 0 0 rgb(255 255 255 / 0.35);
		flex-shrink: 0;
	}

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

	.unlock-toast__title {
		color: var(--text-1);
		font-size: var(--text-sm);
		font-weight: var(--weight-semibold);
		letter-spacing: var(--tracking-tight);
	}

	.unlock-toast__blurb {
		color: var(--text-3);
		font-size: var(--text-xs);
		line-height: var(--leading-snug);
	}

	.unlock-toast__actions {
		display: flex;
		gap: var(--space-3xs);
		margin-inline-start: auto;
		flex-shrink: 0;
	}

	@media (max-width: 34rem) {
		.unlock-toast {
			inset-inline: var(--space-2xs);
			width: auto;
			flex-wrap: wrap;
		}

		.unlock-toast__actions {
			width: 100%;
			margin-inline-start: 0;
			justify-content: flex-end;
		}
	}

	/* --------------------------------------------------------------------------
	   TRIGGER
	   A single dot in the navigation, previewing whatever is applied.
	   -------------------------------------------------------------------------- */
	.palette-trigger {
		display: grid;
		place-items: center;

		width: 2.1rem;
		height: 2.1rem;
		padding: 0;
		border: 1px solid var(--line-strong);
		border-radius: var(--radius-circle);
		background: var(--canvas-deep);
		box-shadow: var(--deboss);

		cursor: pointer;
		flex-shrink: 0;

		transition:
			border-color var(--t-fast),
			box-shadow var(--t-fast),
			transform var(--dur-fast) var(--ease-spring);
	}

	.palette-trigger:hover {
		border-color: rgb(var(--accent-rgb) / 0.5);
		transform: scale(1.06);
	}

	.palette-trigger:active {
		transform: scale(0.95);
	}

	.palette-trigger__dot {
		width: 1.2rem;
		height: 1.2rem;
		border-radius: var(--radius-circle);
		box-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.3);
	}

	/* --------------------------------------------------------------------------
	   PANEL
	   Positioned from JS beneath the trigger. A grid, so ten palettes stay
	   readable instead of overrunning the navigation capsule.
	   -------------------------------------------------------------------------- */
	.palette-panel {
		position: fixed;
		z-index: var(--z-overlay);

		width: 16.5rem;
		padding: var(--space-xs);
		border: 1px solid var(--glass-border);
		border-radius: var(--radius-lg);

		background-color: var(--glass-tint-strong);
		background-image: var(--glass-fill);
		-webkit-backdrop-filter: blur(var(--glass-blur-heavy)) saturate(var(--glass-saturate));
		backdrop-filter: blur(var(--glass-blur-heavy)) saturate(var(--glass-saturate));
		box-shadow: var(--elev-4), var(--rim-inset);

		opacity: 0;
		visibility: hidden;
		translate: 0 -0.5rem;
		scale: 0.97;
		transform-origin: top center;
		transition:
			opacity var(--dur-fast) var(--ease-out),
			visibility var(--dur-fast) var(--ease-out),
			translate var(--dur-fast) var(--ease-spring),
			scale var(--dur-fast) var(--ease-spring);
	}

	.palette-panel.is-open {
		opacity: 1;
		visibility: visible;
		translate: 0 0;
		scale: 1;
	}

	.palette-panel__head {
		display: flex;
		align-items: baseline;
		justify-content: space-between;
		gap: var(--space-2xs);

		margin: 0 0 var(--space-2xs);
		padding-inline: var(--space-3xs);

		color: var(--text-1);
		font-size: var(--text-2xs);
		font-weight: var(--weight-semibold);
		letter-spacing: var(--tracking-widest);
		text-transform: uppercase;
	}

	.palette-panel__count {
		color: var(--text-4);
		font-family: var(--font-mono);
		font-size: var(--text-2xs);
		font-weight: var(--weight-regular);
		letter-spacing: var(--tracking-normal);
		text-transform: none;
	}

	.palette-panel__grid {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: var(--space-3xs);
		max-height: 17rem;
		overflow-y: auto;
		overscroll-behavior: contain;
	}

	.palette-option {
		display: flex;
		align-items: center;
		gap: var(--space-2xs);

		padding: var(--space-3xs) var(--space-2xs);
		border: 1px solid transparent;
		border-radius: var(--radius-md);
		background: transparent;

		color: var(--text-2);
		font-size: var(--text-xs);
		font-weight: var(--weight-medium);
		text-align: start;
		cursor: pointer;

		transition:
			background-color var(--t-fast),
			border-color var(--t-fast),
			color var(--t-fast);
	}

	.palette-option:hover {
		background: var(--surface-3);
		color: var(--text-1);
	}

	.palette-option[aria-pressed='true'] {
		border-color: rgb(var(--accent-rgb) / 0.45);
		background: rgb(var(--accent-rgb) / 0.1);
		color: var(--text-1);
	}

	.palette-option__dot {
		width: 1rem;
		height: 1rem;
		border-radius: var(--radius-circle);
		box-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.3);
		flex-shrink: 0;
	}

	.palette-option__name {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	@media (max-width: 30rem) {
		.palette-panel {
			width: min(18rem, calc(100vw - 1.5rem));
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.unlock-toast,
		.palette-panel,
		.palette-trigger {
			transition: opacity 1ms, visibility 1ms;
			translate: none;
			scale: none;
		}

		.palette-trigger:hover,
		.palette-trigger:active {
			transform: none;
		}
	}

}
