/* ==========================================================================
   TOKENS
   The single source of truth for the design system.

   DESIGN LANGUAGE — "Depth of Field II"
   The page is a camera looking through layered glass in a dark studio.
   A single light source sits top-left. Content occupies real z-planes.

   Two materials, one light:
     · LIQUID GLASS  -> content you read THROUGH. Translucent, blurred,
                        with a refracting edge lens and a specular highlight.
     · RELIEF        -> controls you PRESS. Opaque, extruded by a paired
                        light/dark shadow offset along the same light vector.

   The two read as one system because they share a light vector, a radius
   scale, a hairline and a tint. See css/core.css @layer material.

   ORGANISATION
     T0  primitive   space · type · radii · easing · z-scale · widths
     T1  geometry    relief offsets · lens thickness      (driven by dials)
     T2  semantic    surfaces · text ramp · lines · accent roles  (per theme)
     T3  material    glass tokens · relief pairs · elevation      (derived)
     T4  overrides   reduced-motion · high-contrast · fx tiers
     T5  aliases     legacy names css/game.css depends on

   Nothing in this file paints. It only declares.
   ========================================================================== */

/* ==========================================================================
   REGISTERED PROPERTIES

   A plain `--x: 40%` is just a string to the browser, so it snaps between
   values instead of animating. Registering it with a type makes it a real
   animatable value, which is what lets the ambient layer tween its own
   lighting in CSS rather than in a JavaScript frame loop.

   Registration also gives each property a guaranteed initial value, so
   nothing depends on a module having run first.

   `@property` is unsupported in older browsers; there it degrades to the
   current behaviour of snapping, which is exactly what happens today.
   ========================================================================== */

/* ---- Effect intensity dials --------------------------------------------
   Registered as <number> so they can be multiplied inside calc(), and so an
   invalid value falls back to the initial rather than poisoning the whole
   declaration. Every one is a dial from 0 (off) to 1 (full), which is what
   makes the preference overrides in T4 a token-level kill switch instead of
   a pile of per-component media queries. */
@property --motion {
	syntax: '<number>';
	inherits: true;
	initial-value: 1;
}

@property --relief {
	syntax: '<number>';
	inherits: true;
	initial-value: 1;
}

@property --blur-scale {
	syntax: '<number>';
	inherits: true;
	initial-value: 1;
}

@property --parallax {
	syntax: '<number>';
	inherits: true;
	initial-value: 1;
}

@property --tilt-mul {
	syntax: '<number>';
	inherits: true;
	initial-value: 1;
}

@property --grain-mul {
	syntax: '<number>';
	inherits: true;
	initial-value: 1;
}

/* ---- Light vector -------------------------------------------------------
   ONE light source for the entire page, top-left. The relief pair, the glass
   rim, the lens caustic origin and the ambient orbs all read these. Two light
   directions is how a mixed-material design starts looking like two unrelated
   design systems. */
@property --lx {
	syntax: '<number>';
	inherits: true;
	initial-value: -1;
}

@property --ly {
	syntax: '<number>';
	inherits: true;
	initial-value: -1;
}

/* ---- Pointer state ------------------------------------------------------
   Written already-smoothed by js/fx/surface.js, once per frame. Never CSS-
   transitioned: transitioning a registered property that feeds a gradient
   repaints that element every frame, which is exactly what we are avoiding. */
@property --mx {
	syntax: '<percentage>';
	inherits: true;
	initial-value: 50%;
}

@property --my {
	syntax: '<percentage>';
	inherits: true;
	initial-value: 0%;
}

/* Page-level pointer position, -1..1 on each axis. One write per frame for
   the whole document; each parallax layer picks its own depth multiplier. */
@property --px {
	syntax: '<number>';
	inherits: true;
	initial-value: 0;
}

@property --py {
	syntax: '<number>';
	inherits: true;
	initial-value: 0;
}

/* Angle of the travelling caustic arc on a lens rim, in turns. */
@property --rim-spin {
	syntax: '<number>';
	inherits: true;
	initial-value: 0;
}

/* Sweep position for the specular band that crosses a small control. */
@property --sweep-x {
	syntax: '<percentage>';
	inherits: false;
	initial-value: -40%;
}

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

   The background's colour field is four radial-gradient stops on ONE element,
   and what animates is where those stops ARE — not a transform on a blob.

   Registering the positions is what makes that possible: an unregistered
   custom property is a string, so a gradient built from one snaps between
   values instead of interpolating. Typed as <percentage>, the stops glide,
   the gradient re-resolves on the compositor, and the field reads as light
   changing rather than as objects sliding past each other.
   -------------------------------------------------------------------------- */
@property --m1x { syntax: '<percentage>'; inherits: false; initial-value: 18% }
@property --m1y { syntax: '<percentage>'; inherits: false; initial-value: 12% }
@property --m2x { syntax: '<percentage>'; inherits: false; initial-value: 82% }
@property --m2y { syntax: '<percentage>'; inherits: false; initial-value: 26% }
@property --m3x { syntax: '<percentage>'; inherits: false; initial-value: 62% }
@property --m3y { syntax: '<percentage>'; inherits: false; initial-value: 88% }
@property --m4x { syntax: '<percentage>'; inherits: false; initial-value: 8%  }
@property --m4y { syntax: '<percentage>'; inherits: false; initial-value: 70% }

/* --------------------------------------------------------------------------
   AMBIENT LIGHTING
   Where the surface is lit, how large the lit patch is, and how strongly.
   -------------------------------------------------------------------------- */
@property --gx {
	syntax: '<length-percentage>';
	inherits: true;
	initial-value: 50%;
}

@property --gy {
	syntax: '<length-percentage>';
	inherits: true;
	initial-value: 32%;
}

@property --glow-size {
	syntax: '<length>';
	inherits: true;
	initial-value: 260px;
}

@property --grid-lit {
	syntax: '<number>';
	inherits: true;
	initial-value: 0;
}

/* --------------------------------------------------------------------------
   SCROLL STATE
   Written once per frame by js/core/scroll.js and read by everything that
   wants to respond to scrolling.
   -------------------------------------------------------------------------- */
@property --scroll-p {
	syntax: '<number>';
	inherits: true;
	initial-value: 0;
}

@property --scroll-v {
	syntax: '<number>';
	inherits: true;
	initial-value: 0;
}

@property --scroll-dir {
	syntax: '<number>';
	inherits: true;
	initial-value: 1;
}

/* --------------------------------------------------------------------------
   TIMELINE
   Index of an item within a staggered group. Registering it as a number means
   scroll-driven ranges can do arithmetic with it — `calc(4% + var(--i) * 2%)`
   only works because --i is typed. It inherits, as it already did untyped, so
   a group can set it once for all of its children.
   -------------------------------------------------------------------------- */
@property --i {
	syntax: '<number>';
	inherits: true;
	initial-value: 0;
}

/* ==========================================================================
   T0 — PRIMITIVES
   ========================================================================== */
:root {
	/* ----------------------------------------------------------------------
	   TYPOGRAPHY
	   ---------------------------------------------------------------------- */
	--font-sans:
		'Inter', 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI',
		Roboto, 'Helvetica Neue', Arial, sans-serif;
	--font-mono:
		'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo,
		Consolas, 'Liberation Mono', monospace;
	--font-display: var(--font-sans);

	/* Fluid type scale — clamp(min, preferred, max), 1.200 ratio at base */
	--text-2xs: clamp(0.688rem, 0.67rem + 0.09vw, 0.75rem);
	--text-xs: clamp(0.75rem, 0.73rem + 0.11vw, 0.813rem);
	--text-sm: clamp(0.875rem, 0.85rem + 0.12vw, 0.938rem);
	--text-base: clamp(1rem, 0.97rem + 0.15vw, 1.063rem);
	--text-md: clamp(1.063rem, 1.02rem + 0.22vw, 1.188rem);
	--text-lg: clamp(1.25rem, 1.18rem + 0.35vw, 1.5rem);
	--text-xl: clamp(1.5rem, 1.38rem + 0.6vw, 1.938rem);
	--text-2xl: clamp(1.875rem, 1.65rem + 1.1vw, 2.625rem);
	--text-3xl: clamp(2.375rem, 1.95rem + 2.1vw, 3.75rem);
	--text-4xl: clamp(2.875rem, 2.1rem + 3.6vw, 5rem);

	--leading-tight: 1.1;
	--leading-snug: 1.28;
	--leading-normal: 1.55;
	--leading-relaxed: 1.72;

	--tracking-tighter: -0.035em;
	--tracking-tight: -0.02em;
	--tracking-normal: -0.008em;
	--tracking-wide: 0.04em;
	--tracking-widest: 0.14em;

	--weight-regular: 400;
	--weight-medium: 500;
	--weight-semibold: 600;
	--weight-bold: 700;
	--weight-black: 800;

	/* ----------------------------------------------------------------------
	   SPACE  — 4px base, geometric-ish
	   ---------------------------------------------------------------------- */
	--space-3xs: 0.25rem; /*   4 */
	--space-2xs: 0.5rem; /*   8 */
	--space-xs: 0.75rem; /*  12 */
	--space-sm: 1rem; /*  16 */
	--space-md: 1.5rem; /*  24 */
	--space-lg: 2rem; /*  32 */
	--space-xl: 3rem; /*  48 */
	--space-2xl: 4.5rem; /*  72 */
	--space-3xl: 6.5rem; /* 104 */
	--space-4xl: 9rem; /* 144 */

	/* Vertical rhythm between major sections — fluid */
	--section-gap: clamp(4.5rem, 3rem + 7vw, 9rem);
	--section-gap-tight: clamp(3rem, 2rem + 4.5vw, 5.5rem);

	/* Width of the custom scrollbar rail on the right edge. Shared, because
	   the floating controls have to keep clear of it, and the two would
	   otherwise drift apart. Zeroed on touch, where the rail is hidden. */
	--scrollbar-w: 14px;

	/* ----------------------------------------------------------------------
	   FLOATING CONTROL DOCKING

	   The instrument bar docks against the foot of the viewport and the
	   footer reserves room for it, so the two never collide. Expressed as
	   tokens because three files have to agree: the dock itself, the footer
	   that reserves the lane, and the unlock toast that must not cover it.
	   ---------------------------------------------------------------------- */
	--to-top-size: 2.75rem; /* 44px — the minimum comfortable tap target */
	--footer-line: 1.375rem; /* 22px — one line of --text-xs */
	--footer-pad-end: var(--space-lg);

	/* Centring a control on that line means dropping it by half the
	   difference between the two heights. */
	--to-top-dock: calc(
		var(--footer-pad-end) + (var(--footer-line) - var(--to-top-size)) / 2
	);

	/* Vertical room the instrument dock occupies at the foot of the viewport.
	   The footer reserves it as bottom padding and the unlock toast stacks
	   above it, so neither can be covered by the dock. Derived rather than
	   restated in three places, which is how the old right-hand lane and the
	   toast drifted apart. */
	--dock-clearance: calc(
		var(--space-md) + var(--to-top-size) + var(--space-md) +
			env(safe-area-inset-bottom, 0px)
	);

	/* Layout widths */
	--width-content: 68rem; /* 1088px — main column      */
	--width-prose: 44rem; /*  704px — readable text    */
	--width-wide: 82rem; /* 1312px — full-bleed cards */
	--gutter: clamp(1.25rem, 0.7rem + 2.6vw, 2.75rem);

	/* ----------------------------------------------------------------------
	   RADII — generous and consistent.

	   Deliberately plain `border-radius`, not CSS `corner-shape`. Superellipse
	   corners are not inherited by pseudo-elements, so a squircle card with a
	   `border-radius: inherit` overlay renders mismatched corners — the
	   overlay reads as squared-off against the card. Plain radii stay in sync
	   everywhere, including on the ::before lens ring and the ::after
	   specular, both of which inherit the radius.
	   ---------------------------------------------------------------------- */
	--radius-xs: 0.5rem;
	--radius-sm: 0.75rem;
	--radius-md: 1rem;
	--radius-lg: 1.35rem;
	--radius-xl: 1.85rem;
	--radius-2xl: 2.35rem;
	--radius-3xl: 3rem;
	--radius-pill: 999rem;
	--radius-circle: 50%;

	/* ----------------------------------------------------------------------
	   MOTION
	   ---------------------------------------------------------------------- */
	--ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* decelerate, default */
	--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
	--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* overshoot        */
	--ease-glide: cubic-bezier(0.16, 1, 0.3, 1); /* long, luxurious  */

	/* Durations scale with --motion but keep a 1ms FLOOR rather than
	   collapsing to 0s.

	   `transitionend` never fires for a zero-duration transition. Any code
	   that waits for a transition to finish before releasing a height or
	   removing a class silently stalls under reduced motion. A 1ms floor
	   means every completion path still fires while the movement is, for all
	   practical purposes, instantaneous.

	   Corollary, and it is a real constraint: JavaScript must never PARSE a
	   duration out of computed style. These resolve to calc() expressions. */
	--dur-instant: calc(1ms + 89ms * var(--motion));
	--dur-fast: calc(1ms + 159ms * var(--motion));
	--dur-base: calc(1ms + 259ms * var(--motion));
	--dur-slow: calc(1ms + 419ms * var(--motion));
	--dur-slower: calc(1ms + 679ms * var(--motion));
	--dur-reveal: calc(1ms + 519ms * var(--motion));

	/* Composite shorthands used by components */
	--t-fast: var(--dur-fast) var(--ease-out);
	--t-base: var(--dur-base) var(--ease-out);
	--t-slow: var(--dur-slow) var(--ease-glide);

	/* ----------------------------------------------------------------------
	   DEPTH OF FIELD — the spine of the motion language
	   Real 3D. Elements sit on z-planes inside a perspective container and
	   arrive from depth rather than sliding in from an edge.
	   ---------------------------------------------------------------------- */
	--perspective: 1400px; /* camera distance, page-level  */
	--perspective-near: 900px; /* tighter, for card interiors  */
	--perspective-origin: 50% 42%; /* lens sits slightly above mid */

	/* z-plane scale — how far back an element sits, in px of translateZ.
	   Scaled by --parallax so reduced motion flattens the stage. */
	--depth-far: calc(-140px * var(--parallax));
	--depth-back: calc(-70px * var(--parallax));
	--depth-mid: calc(-28px * var(--parallax));
	--depth-flat: 0px;
	--depth-fore: calc(26px * var(--parallax));
	--depth-near: calc(60px * var(--parallax));
	--depth-pop: calc(110px * var(--parallax));

	/* Focal blur — how out-of-focus a plane reads at each depth.

	   ONLY ever applied to decorative layers that contain no glass. `filter`
	   on an ancestor makes it a backdrop root, which flattens every nested
	   backdrop-filter for as long as the filter is applied. */
	--focus-far: calc(16px * var(--parallax));
	--focus-back: calc(9px * var(--parallax));
	--focus-mid: calc(4px * var(--parallax));
	--focus-sharp: 0px;

	/* Tilt — max rotation when a card leans under the pointer */
	--tilt-subtle: calc(3deg * var(--tilt-mul));
	--tilt-base: calc(6deg * var(--tilt-mul));
	--tilt-strong: calc(10deg * var(--tilt-mul));

	/* Velocity response — ceiling for scroll-inertia deformation.

	   Consumers must read THESE tokens rather than hard-coding a magnitude.
	   The previous system hard-coded 3.4deg / -2.2deg / -6px in the component
	   layer while declaring a --skew-max nothing referenced, so zeroing the
	   token under reduced motion did nothing at all. */
	--skew-max: calc(3.4deg * var(--motion));
	--lean-max: calc(2.2deg * var(--motion));
	--lean-rise: calc(6px * var(--motion));

	/* Specular sweep — the band of light that crosses a surface on hover */
	--sweep-width: 16%;
	--sweep-angle: 105deg;

	/* Rack focus — RETIRED, held at identity values.

	   Sections leaving the focal band used to blur, desaturate and recede.
	   Two problems killed it: `filter` on a section makes that section a
	   backdrop root, so every glass pane inside it rendered flat for the
	   whole entry animation; and a full-box blur pass per frame across
	   several concurrent sections is the most expensive thing the page did.

	   The tokens stay, at no-op values, so the keyframe that still reads
	   them in css/animations.css resolves to "no change" rather than to
	   an invalid transform. They go when that file does. */
	--rack-blur: 0px;
	--rack-scale: 1;
	--rack-fade: 1;
	--rack-desaturate: 1;

	/* ----------------------------------------------------------------------
	   Z-INDEX — a real scale, no magic numbers in components
	   ---------------------------------------------------------------------- */
	/* The backdrop paints an opaque canvas, so it has to sit below the ambient
	   layer rather than beside it: at the same depth the later element in the
	   document wins and the ambient field is simply painted over. */
	--z-backdrop: -2;
	--z-behind: -1;
	--z-base: 0;
	--z-raised: 10;
	--z-sticky: 100;
	--z-nav: 200;
	--z-controls: 300;
	--z-overlay: 400;
	--z-modal: 500;
	--z-toast: 600;
	--z-preloader: 900;

	/* ----------------------------------------------------------------------
	   T1 — RELIEF GEOMETRY
	   One light vector, one depth scale, three steps. The colours that fill
	   these offsets are per-theme (T2); the geometry is not.
	   ---------------------------------------------------------------------- */
	--rd1: calc(2px * var(--relief)); /* pressed offset */
	--rd2: calc(6px * var(--relief)); /* rest offset    */
	--rd3: calc(9px * var(--relief)); /* hover offset   */
	--rb1: calc(6px * var(--relief)); /* pressed blur   */
	--rb2: calc(14px * var(--relief)); /* rest blur      */
	--rb3: calc(22px * var(--relief)); /* hover blur     */

	/* Edge lens — thickness of the refracting ring around a glass pane.
	   Zeroing this removes the ring geometrically, with no extra selectors,
	   which is what makes the high-contrast and lite-tier overrides cheap. */
	--lens-w: 14px;
	--lens-w-strong: 26px;
}

/* ==========================================================================
   T2 — COLOR — DARK (default)

   Hue anchors:  violet 258°  ·  cyan 184°
   Backgrounds carry a faint violet cast so glass reads as tinted, not grey.

   Surfaces and text are DECLARED, not derived. A derived text ramp cannot be
   trusted to clear 4.5:1 across 24 palette x theme combinations. Decoration
   derives; text does not.
   ========================================================================== */
:root,
[data-theme='dark'] {
	color-scheme: dark;

	/* --- Surfaces (z-plane ordering: canvas -> 1 -> 2 -> 3) -------------- */
	--canvas: #181c1d;
	--canvas-deep: #0f1314;
	--surface-1: #1c2224;
	--surface-2: #242b2c;
	--surface-3: #2f3739;
	--surface-4: #3c4547;

	/* --- Text ------------------------------------------------------------ */
	--text-1: #eaeeee; /* headings, high emphasis   */
	--text-2: #c4cacb; /* body                      */
	--text-3: #99a0a1; /* secondary / captions      */
	--text-4: #787f80; /* disabled / faint          */
	--text-on-accent: #111114;

	/* --- Semantic -------------------------------------------------------- */
	--accent: #4ebacf;
	--accent-strong: #7bd0e1;
	--accent-deep: #0e6c7b;
	--accent-soft: #3c4547;
	--glow: #9ed3f2;
	--glow-strong: #9ed3f2;

	/* Primary button. The label colour is picked by the generator as
	   whichever of near-white / near-black clears AA against the WORST
	   stop of every state, and hover moves the fill in whichever
	   direction improves contrast — lighter under a dark label, darker
	   under a light one. */
	--btn-top: #54bdd1;
	--btn-bottom: #38a5b9;
	--btn-top-hover: #76d3e5;
	--btn-bottom-hover: #58bbce;
	--btn-press: #2c9caf;

	--accent-rgb: 78 186 207;
	--glow-rgb: 158 211 242;
	--canvas-rgb: 24 28 29;
	--text-1-rgb: 234 238 238;

	--success: #4ade80;
	--warning: #fbbf24;
	--danger: #fb7185;
	--danger-soft: rgb(251 113 133 / 0.12);

	/* --- Borders / hairlines --------------------------------------------- */
	--line: rgb(255 255 255 / 0.08);
	--line-strong: rgb(255 255 255 / 0.14);
	--line-faint: rgb(255 255 255 / 0.045);

	/* The ambient grid needs to be seen, not merely present, so it is its own
	   token rather than the hairline used for borders. */
	--grid-line: rgb(255 255 255 / 0.16);

	/* --- Relief fill colours (dark) --------------------------------------
	   The light half is deliberately weak on dark surfaces. A bright highlight
	   against near-black reads as a plastic sheen rather than an extruded
	   edge; the shadow half does nearly all the work here. */
	--relief-hi: rgb(255 255 255 / 0.055);
	--relief-hi-2: rgb(255 255 255 / 0.085);
	--relief-lo: rgb(0 0 0 / 0.62);
	--relief-lo-2: rgb(0 0 0 / 0.78);

	--shadow-color: 250 65% 3%;
	--grain-opacity: calc(0.038 * var(--grain-mul));
	--grain-blend: overlay;

	/* The glow that follows the pointer across a glass surface. White works
	   on a dark surface; see the light theme for why it cannot stay white. */
	--pointer-light: rgb(255 255 255 / 0.12);

	/* --- Glass, per theme ------------------------------------------------ */
	--glass-blur-base: 20px;
	--glass-blur-heavy-base: 30px;
	--glass-saturate: 165%;
	--glass-border: rgb(255 255 255 / 0.11);
	--glass-sheen-1: rgb(255 255 255 / 0.085);
	--glass-sheen-2: rgb(255 255 255 / 0.035);
	--glass-sheen-3: rgb(255 255 255 / 0.015);
	--glass-tint: rgb(20 20 34 / 0.55);
	--glass-tint-strong: rgb(16 16 27 / 0.78);

	/* Opaque stand-in for every surface that is NOT one of the few real
	   backdrop-filter panes: same sheen, same rim, no blur. At card scale
	   over a low-frequency backdrop the difference is invisible, and it is
	   the single largest lever available on compositing cost. */
	--glass-tint-opaque: #14141f;

	--lens-bright: 1.18;
	--lens-sat: 210%;

	/* Selection + scrollbar */
	--selection-bg: rgb(var(--accent-rgb) / 0.32);
	--selection-fg: var(--text-1);
	--scrollbar-track: #0d0d16;
	--scrollbar-thumb: rgb(255 255 255 / 0.16);
	--scrollbar-thumb-hover: rgb(var(--accent-rgb) / 0.55);
}

/* ==========================================================================
   T2 — COLOR — LIGHT
   Not an inversion. Light mode gets its own tuned ramp: warm-neutral paper,
   deeper accents for AA contrast, and glass that reads as frosted rather
   than smoked.

   This is also where neumorphism actually works. A near-white highlight and
   a soft coloured shadow read as a genuine physical edge in a way that the
   dark theme's compressed relief never quite can.
   ========================================================================== */
[data-theme='light'] {
	color-scheme: light;

	/* --- Surfaces (z-plane ordering: canvas -> 1 -> 2 -> 3) -------------- */
	--canvas: #f4f8f9;
	--canvas-deep: #eaeff0;
	--surface-1: #ffffff;
	--surface-2: #fbfdfe;
	--surface-3: #f2f6f7;
	--surface-4: #e7edee;

	/* --- Text ------------------------------------------------------------ */
	--text-1: #1e2425; /* headings, high emphasis   */
	--text-2: #484e50; /* body                      */
	--text-3: #6b7173; /* secondary / captions      */
	--text-4: #868c8d; /* disabled / faint          */
	--text-on-accent: #ffffff;

	/* --- Semantic -------------------------------------------------------- */
	--accent: #0e7b8c;
	--accent-strong: #0d6472;
	--accent-deep: #0c4e59;
	--accent-soft: #e7edee;
	--glow: #4387ab;
	--glow-strong: #4387ab;

	/* Primary button. The label colour is picked by the generator as
	   whichever of near-white / near-black clears AA against the WORST
	   stop of every state, and hover moves the fill in whichever
	   direction improves contrast — lighter under a dark label, darker
	   under a light one. */
	--btn-top: #0f798a;
	--btn-bottom: #0d616f;
	--btn-top-hover: #0d616f;
	--btn-bottom-hover: #0b4e59;
	--btn-press: #0b454f;

	--accent-rgb: 14 123 140;
	--glow-rgb: 67 135 171;
	--canvas-rgb: 244 248 249;
	--text-1-rgb: 30 36 37;

	--success: #16a34a;
	--warning: #d97706;
	--danger: #dc2626;
	--danger-soft: rgb(220 38 38 / 0.08);

	--line: rgb(19 19 34 / 0.1);
	--line-strong: rgb(19 19 34 / 0.16);
	--line-faint: rgb(19 19 34 / 0.055);

	/* Light canvases hide a hairline far more readily than dark ones, so the
	   grid is drawn proportionally stronger here to land at the same weight. */
	--grid-line: rgb(19 19 34 / 0.14);

	/* --- Relief fill colours (light) ------------------------------------- */
	--relief-hi: rgb(255 255 255 / 0.95);
	--relief-hi-2: #ffffff;
	--relief-lo: rgb(56 48 96 / 0.12);
	--relief-lo-2: rgb(56 48 96 / 0.18);

	--shadow-color: 250 40% 42%;
	--grain-opacity: calc(0.028 * var(--grain-mul));
	--grain-blend: multiply;

	/* A white glow is invisible on an already-white surface, so light mode
	   tints the pointer light with the accent instead. Same effect, opposite
	   direction: the surface picks up colour rather than brightness. */
	--pointer-light: rgb(var(--accent-rgb) / 0.16);

	/* Frosted glass: mostly-white fill, blur pulls colour from beneath */
	--glass-blur-base: 18px;
	--glass-blur-heavy-base: 28px;
	--glass-saturate: 180%;
	--glass-border: rgb(255 255 255 / 0.9);
	--glass-sheen-1: rgb(255 255 255 / 0.82);
	--glass-sheen-2: rgb(255 255 255 / 0.62);
	--glass-sheen-3: rgb(255 255 255 / 0.5);
	--glass-tint: rgb(255 255 255 / 0.62);
	--glass-tint-strong: rgb(255 255 255 / 0.85);
	--glass-tint-opaque: #fbfbff;

	--lens-bright: 1.06;
	--lens-sat: 185%;

	--selection-bg: rgb(var(--accent-rgb) / 0.2);
	--selection-fg: var(--text-1);
	--scrollbar-track: #eeeef7;
	--scrollbar-thumb: rgb(19 19 34 / 0.18);
	--scrollbar-thumb-hover: rgb(var(--accent-rgb) / 0.5);
}

/* ==========================================================================
   T3 — MATERIAL

   Everything below is DERIVED from T2. It is declared once, and every
   alternate palette in css/palettes.css inherits all of it for free by
   overriding only the T2 colours it cares about — which is why 11 extra
   palettes cost roughly 40 lines each instead of a second material system.

   Both [data-palette][data-theme] (0,2,0) and these :root rules (0,1,0)
   match <html>, so all declarations cascade together on one element and
   var() substitution then resolves against the winning value. A palette
   that only sets --surface-3 still gets a correctly re-lit relief face.
   ========================================================================== */
:root {
	/* ----------------------------------------------------------------------
	   LIQUID GLASS
	   A pane is: tinted translucent fill + backdrop blur/saturation + a
	   directional sheen + a specular rim, bright top-left, dark bottom-right.
	   ---------------------------------------------------------------------- */
	--glass-blur: calc(var(--glass-blur-base) * var(--blur-scale));
	--glass-blur-heavy: calc(var(--glass-blur-heavy-base) * var(--blur-scale));

	--glass-fill: linear-gradient(
		148deg,
		var(--glass-sheen-1) 0%,
		var(--glass-sheen-2) 42%,
		var(--glass-sheen-3) 100%
	);
	--glass-fill-strong: var(--glass-fill);
	--glass-sheen: var(--glass-fill);

	/* The lens ring is THICKER GLASS than the pane it edges. A stronger blur
	   plus a lift in brightness and saturation is what sells refraction
	   without an SVG displacement map. */
	--lens-blur: calc(var(--glass-blur) * 2.4);

	/* Specular rim: light catches the top edge, shadow pools at the bottom.
	   Ends with the same 1px hairline the relief pair ends with. */
	--rim-inset:
		inset 0 1px 0 0 var(--relief-hi-2), inset 0 -1px 0 0 var(--relief-lo),
		0 0 0 1px var(--line);
	--glass-rim: var(--rim-inset);
	--rim-light: rgb(255 255 255 / 0.42);
	--rim-dark: rgb(0 0 0 / 0.42);

	/* ----------------------------------------------------------------------
	   SPATIAL ELEVATION
	   Each level = ambient (wide, soft) + key (tight, directional).
	   Shadows are near-black with a violet bias so they sit in the palette.
	   ---------------------------------------------------------------------- */
	--elev-0: none;
	--elev-1:
		0 1px 2px -1px hsl(var(--shadow-color) / 0.5),
		0 2px 6px -2px hsl(var(--shadow-color) / 0.42);
	--elev-2:
		0 2px 4px -2px hsl(var(--shadow-color) / 0.5),
		0 6px 16px -4px hsl(var(--shadow-color) / 0.48),
		0 12px 32px -8px hsl(var(--shadow-color) / 0.35);
	--elev-3:
		0 4px 8px -3px hsl(var(--shadow-color) / 0.5),
		0 12px 28px -6px hsl(var(--shadow-color) / 0.52),
		0 28px 60px -12px hsl(var(--shadow-color) / 0.45);
	--elev-4:
		0 8px 16px -4px hsl(var(--shadow-color) / 0.52),
		0 24px 50px -10px hsl(var(--shadow-color) / 0.58),
		0 52px 110px -20px hsl(var(--shadow-color) / 0.5);
	--elev-float:
		0 10px 20px -6px hsl(var(--shadow-color) / 0.55),
		0 32px 70px -14px hsl(var(--shadow-color) / 0.6),
		0 70px 150px -30px hsl(var(--shadow-color) / 0.52);

	/* Accent-coloured bloom used on primary actions and focus states */
	--bloom-accent: 0 8px 30px -8px rgb(var(--accent-rgb) / 0.55);
	--bloom-glow: 0 8px 30px -8px rgb(var(--glow-rgb) / 0.42);

	/* ----------------------------------------------------------------------
	   RELIEF — the neumorphic pair

	   One light source casts a highlight on the face it strikes and a shadow
	   on the face away from it. Assembled generically from the light vector,
	   so a single --lx/--ly change re-lights every control on the page, and
	   scaled by --relief so intensity is one dial.

	   Every list ends with the same `0 0 0 1px` hairline the glass rim uses.
	   That shared edge is what makes two different fill logics read as one
	   material family rather than two pasted-together design systems.
	   ---------------------------------------------------------------------- */
	--relief-raised:
		calc(var(--rd2) * var(--lx)) calc(var(--rd2) * var(--ly)) var(--rb2) 0
			var(--relief-hi),
		calc(var(--rd2) * -1 * var(--lx)) calc(var(--rd2) * -1 * var(--ly))
			var(--rb2) 0 var(--relief-lo),
		0 0 0 1px var(--line-faint);

	--relief-raised-hi:
		calc(var(--rd3) * var(--lx)) calc(var(--rd3) * var(--ly)) var(--rb3) 0
			var(--relief-hi-2),
		calc(var(--rd3) * -1 * var(--lx)) calc(var(--rd3) * -1.2 * var(--ly))
			var(--rb3) 0 var(--relief-lo),
		0 0 0 1px var(--line-faint);

	/* Pressed inverts the vector: the highlight crosses to the far side and
	   both halves become insets, so the surface reads as pushed in. */
	--relief-pressed:
		inset calc(var(--rd1) * -1 * var(--lx)) calc(var(--rd1) * -1 * var(--ly))
			var(--rb1) var(--relief-lo-2),
		inset calc(var(--rd1) * var(--lx)) calc(var(--rd1) * var(--ly)) 5px
			var(--relief-hi),
		0 0 0 1px var(--line-faint);

	/* A track, well or slot — permanently debossed. */
	--relief-well:
		inset calc(var(--rd2) * -0.85 * var(--lx))
			calc(var(--rd2) * -0.85 * var(--ly)) var(--rb2) var(--relief-lo-2),
		inset calc(var(--rd2) * 0.7 * var(--lx)) calc(var(--rd2) * 0.7 * var(--ly))
			10px var(--relief-hi);

	/* Relief on a TRANSLUCENT parent.

	   The bright half of a neumorphic pair, rendered over a blurred backdrop,
	   reads as a white halo floating in front of the glass rather than as an
	   edge belonging to the control. That mismatch is precisely what makes
	   mixed neumorphic/glass systems look broken. On glass, keep only the
	   dark half plus an internal top line. */
	--relief-on-glass:
		calc(var(--rd2) * -1 * var(--lx)) calc(var(--rd2) * -1 * var(--ly))
			var(--rb2) 0 var(--relief-lo),
		inset 0 1px 0 0 var(--relief-hi-2);

	/* Relief faces. Lit from the same 157deg the glass sheen uses, so a
	   control and the pane it sits on agree about where the light is. */
	--relief-face: linear-gradient(
		157deg,
		var(--surface-4) 0%,
		var(--surface-3) 54%,
		var(--surface-2) 100%
	);
	--relief-face-press: linear-gradient(
		157deg,
		var(--surface-2) 0%,
		var(--surface-3) 70%
	);
	--well-face: var(--canvas-deep);

	/* Ambient light orbs behind the hero.
	   Derived from the accent tokens so an alternate palette inherits them
	   without redeclaring anything.

	   Painted as radial-gradients with soft stops, NOT as solid circles
	   behind a `filter: blur()`. A blurred solid circle produces the same
	   image at the cost of a full-size offscreen buffer and a separable blur
	   pass on every frame it moves. */
	--orb-1: rgb(var(--accent-rgb) / 0.5);
	--orb-2: rgb(var(--glow-rgb) / 0.32);
	--orb-3: rgb(var(--accent-rgb) / 0.32);
}

/* ==========================================================================
   T5 — LEGACY ALIASES

   css/game.css (loaded by game.html, which links THIS FILE directly) reads
   these names off computed style. They are the skeuomorphic vocabulary the
   relief system replaced.

   Do not rename or remove without editing css/game.css in the same commit.
   There is no error path — the game's chrome simply goes quietly wrong.
   ========================================================================== */
:root {
	--emboss: var(--relief-raised);
	--deboss: var(--relief-well);
	--press: var(--relief-pressed);
}

/* ==========================================================================
   T4 — PREFERENCE AND CAPABILITY OVERRIDES
   Every rule below moves a dial. None of them touch a component.
   ========================================================================== */

/* --------------------------------------------------------------------------
   ORDER MATTERS BELOW THIS LINE.

   [data-fx] and the preference media queries are both specificity 0,1,0 on
   the same element, so the later declaration wins. The effect tiers are
   therefore declared FIRST and the accessibility preferences LAST.

   A device that cannot afford full effects may quietly reduce them. It must
   never be able to switch an accessibility preference back on: if a reader
   has asked for high contrast, a mid-tier GPU score is not a reason to give
   them 9px of lens blur anyway.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   EFFECT TIERS

   Set on <html> by js/core/env.js. The page starts at `mid` and is upgraded
   to `full` only after it has measured 40 consecutive frames inside budget,
   so the expensive effects are earned rather than assumed. The frame loop
   can also demote back to `lite` mid-session if it drops three frames.
   -------------------------------------------------------------------------- */
[data-fx='mid'] {
	--blur-scale: 0.7;
	--lens-w: 9px;
	--lens-w-strong: 16px;
	--parallax: 0.5;
	--grain-mul: 0;
}

[data-fx='lite'] {
	--blur-scale: 0;
	--lens-w: 0px;
	--lens-w-strong: 0px;
	--relief: 0.6;
	--parallax: 0;
	--tilt-mul: 0;
	--grain-mul: 0;
}

/* High contrast: drop translucency, harden hairlines, flatten relief.

   Neumorphism is a low-contrast idiom by construction, so it is the first
   thing to go — but it is reduced rather than removed, because a control
   with no edge at all is worse than one with a soft edge. */
@media (prefers-contrast: more) {
	:root {
		--blur-scale: 0;
		--relief: 0.35;
		--grain-mul: 0;
		--lens-w: 0px;
		--lens-w-strong: 0px;
		--line: rgb(255 255 255 / 0.3);
		--line-strong: rgb(255 255 255 / 0.45);
	}

	[data-theme='dark'] {
		--glass-tint: rgb(16 16 27 / 0.97);
		--glass-tint-strong: rgb(12 12 20 / 0.99);
		--glass-tint-opaque: #10101b;
		--text-2: #e2e2ef;
		--text-3: #b8b8cc;
	}

	[data-theme='light'] {
		--glass-tint: rgb(255 255 255 / 0.97);
		--glass-tint-strong: rgb(255 255 255 / 0.99);
		--glass-tint-opaque: #ffffff;
		--line: rgb(19 19 34 / 0.32);
		--line-strong: rgb(19 19 34 / 0.5);
		--text-2: #23233a;
		--text-3: #43435c;
	}
}

/* Reduced motion: collapse durations to their 1ms floor, flatten depth,
   and disable lean, tilt and skew. The layout must remain identical
   — only the movement disappears.

   Because every duration and every displacement token is a multiple of one
   of these dials, this block is the entire kill switch. Components inherit
   correctness instead of each re-declaring it. */
@media (prefers-reduced-motion: reduce) {
	:root {
		--motion: 0;
		--parallax: 0;
		--tilt-mul: 0;
	}
}
