/*! yeti-css 7.0.0-alpha.0 | FSL-1.1-MIT | https://foundationcss.com/yeti/ */

/* src/layers.css */
/* The cascade layer order for Yeti. Declared once, here, and imported first.
   Every rule Yeti ships lives inside one of these sublayers. A user's plain,
   unlayered CSS beats all of them by default. */
@layer yeti.reset, yeti.base, yeti.layouts, yeti.components, yeti.utilities;

/* src/tokens/scale.css */
/* The scale. Two inputs, base and ratio, each fluid between a narrow and a
   wide viewport. Every size in Yeti is a step on this one progression.
   See docs/guides/theming.md. */
@layer yeti.base {
	:root {
		/* Public inputs. Setting --yeti-base or --yeti-ratio pins both ends
		   of that pair; Yeti never declares those two itself. */
		--yeti-base-min: var(--yeti-base, 1rem);
		--yeti-base-max: var(--yeti-base, 1.125rem);
		--yeti-ratio-min: var(--yeti-ratio, 1.2);
		--yeti-ratio-max: var(--yeti-ratio, 1.333);
		--yeti-viewport-min: 320px;
		--yeti-viewport-max: 1280px;

		/* Internal. --_yeti-t runs from 0 at viewport-min to 1 at viewport-max.
		   tan(atan2(a, b)) equals a / b as a plain number, which is the only
		   way to divide two lengths in calc() on the Baseline 2025 floor. */
		--_yeti-t: clamp(0, tan(atan2(100vw - var(--yeti-viewport-min), var(--yeti-viewport-max) - var(--yeti-viewport-min))), 1);
		--_yeti-base: calc(var(--yeti-base-min) + (var(--yeti-base-max) - var(--yeti-base-min)) * var(--_yeti-t));
		--_yeti-ratio: calc(var(--yeti-ratio-min) + (var(--yeti-ratio-max) - var(--yeti-ratio-min)) * var(--_yeti-t));

		/* Fluid steps. Step 0 is the base; each step multiplies by the ratio. */
		--_yeti-step--2: calc(var(--_yeti-base) / pow(var(--_yeti-ratio), 2));
		--_yeti-step--1: calc(var(--_yeti-base) / var(--_yeti-ratio));
		--_yeti-step-0: var(--_yeti-base);
		--_yeti-step-1: calc(var(--_yeti-base) * var(--_yeti-ratio));
		--_yeti-step-2: calc(var(--_yeti-base) * pow(var(--_yeti-ratio), 2));
		--_yeti-step-3: calc(var(--_yeti-base) * pow(var(--_yeti-ratio), 3));
		--_yeti-step-4: calc(var(--_yeti-base) * pow(var(--_yeti-ratio), 4));
		--_yeti-step-5: calc(var(--_yeti-base) * pow(var(--_yeti-ratio), 5));

		/* Static steps: the narrow-viewport values, for the rare size that must not scale. */
		--_yeti-step--2-static: calc(var(--yeti-base-min) / pow(var(--yeti-ratio-min), 2));
		--_yeti-step--1-static: calc(var(--yeti-base-min) / var(--yeti-ratio-min));
		--_yeti-step-0-static: var(--yeti-base-min);
		--_yeti-step-1-static: calc(var(--yeti-base-min) * var(--yeti-ratio-min));
		--_yeti-step-2-static: calc(var(--yeti-base-min) * pow(var(--yeti-ratio-min), 2));
		--_yeti-step-3-static: calc(var(--yeti-base-min) * pow(var(--yeti-ratio-min), 3));
		--_yeti-step-4-static: calc(var(--yeti-base-min) * pow(var(--yeti-ratio-min), 4));

		/* Wide-viewport steps: the far end for fluid space pairs. */
		--_yeti-step--2-max: calc(var(--yeti-base-max) / pow(var(--yeti-ratio-max), 2));
		--_yeti-step--1-max: calc(var(--yeti-base-max) / var(--yeti-ratio-max));
		--_yeti-step-0-max: var(--yeti-base-max);
		--_yeti-step-1-max: calc(var(--yeti-base-max) * var(--yeti-ratio-max));
		--_yeti-step-2-max: calc(var(--yeti-base-max) * pow(var(--yeti-ratio-max), 2));
		--_yeti-step-3-max: calc(var(--yeti-base-max) * pow(var(--yeti-ratio-max), 3));
		--_yeti-step-4-max: calc(var(--yeti-base-max) * pow(var(--yeti-ratio-max), 4));
	}
}

/* src/tokens/space.css */
/* Space and radius read off the scale, so gaps are proportional to type.
   Widths are container measures in rem: they follow the visitor's root size,
   not the scale, because a threshold is about content, not type. */
@layer yeti.base {
	:root {
		--yeti-space-none: 0;
		--yeti-space-xs: var(--_yeti-step--2);
		--yeti-space-sm: var(--_yeti-step--1);
		--yeti-space-md: var(--_yeti-step-0);
		--yeti-space-lg: var(--_yeti-step-1);
		--yeti-space-xl: var(--_yeti-step-2);
		--yeti-space-2xl: var(--_yeti-step-3);
		--yeti-space-3xl: var(--_yeti-step-4);

		--yeti-space-xs-static: var(--_yeti-step--2-static);
		--yeti-space-sm-static: var(--_yeti-step--1-static);
		--yeti-space-md-static: var(--_yeti-step-0-static);
		--yeti-space-lg-static: var(--_yeti-step-1-static);
		--yeti-space-xl-static: var(--_yeti-step-2-static);
		--yeti-space-2xl-static: var(--_yeti-step-3-static);
		--yeti-space-3xl-static: var(--_yeti-step-4-static);

		/* Internal: the wide-viewport value of each stop, for fluid pairs. */
		--_yeti-space-sm-max: var(--_yeti-step--1-max);
		--_yeti-space-md-max: var(--_yeti-step-0-max);
		--_yeti-space-lg-max: var(--_yeti-step-1-max);
		--_yeti-space-xl-max: var(--_yeti-step-2-max);
		--_yeti-space-2xl-max: var(--_yeti-step-3-max);
		--_yeti-space-3xl-max: var(--_yeti-step-4-max);

		--yeti-radius-sm: calc(0.25 * var(--_yeti-base));
		--yeti-radius-md: calc(0.5 * var(--_yeti-base));
		--yeti-radius-lg: var(--_yeti-base);
		--yeti-radius-full: 9999px;

		--yeti-width-2xs: 12rem;
		--yeti-width-xs: 16rem;
		--yeti-width-sm: 24rem;
		--yeti-width-md: 32rem;
		--yeti-width-lg: 48rem;
		--yeti-width-xl: 64rem;
		--yeti-width-2xl: 80rem;

		/* Block sizes, for the few things that take one; the demo's preview box
		   is the first. */
		--yeti-height-sm: 12rem;
		--yeti-height-md: 20rem;
		--yeti-height-lg: 32rem;
		--yeti-height-xl: 48rem;

		--yeti-cover-height: 100dvh;
	}
}

/* src/tokens/type.css */
/* Type sizes are the same steps as space. Body is md. */
@layer yeti.base {
	:root {
		--yeti-text-xs: var(--_yeti-step--2);
		--yeti-text-sm: var(--_yeti-step--1);
		--yeti-text-md: var(--_yeti-step-0);
		--yeti-text-lg: var(--_yeti-step-1);
		--yeti-text-xl: var(--_yeti-step-2);
		--yeti-text-2xl: var(--_yeti-step-3);
		--yeti-text-3xl: var(--_yeti-step-4);
		--yeti-text-display: var(--_yeti-step-5);

		/* Line height: one em plus half the base. Near 1.5 at body size,
		   tighter as the size grows, because the added space stays constant. */
		--yeti-leading-md: calc(1em + 0.5 * var(--_yeti-base));
		/* A plain ratio, for text that is set smaller than the body and wants
		   its leading to shrink with it. The formula above adds a constant, so
		   below body size its ratio grows instead: a code block at 0.85rem
		   comes out near 1.66, which is why code set that way reads airy. */
		--yeti-leading-sm: 1.5;
		--yeti-leading-tight: 1.15;
		--yeti-measure: 70ch;
		/* Between one item of a prose list and the next. Not on the space scale,
		   which is geometric and tied to the type steps: its smallest step is
		   already two thirds of a line, and a list wants a hair of separation
		   rather than a gap. Prose rhythm, like the measure above it. */
		--yeti-list-gap: 0.25rem;
		/* A lede is set one step up from the body and on a shorter line. The two
		   go together: ch is a unit of the font, so a theme that changes the size
		   changes what the count is worth. */
		--yeti-lede-size: var(--yeti-text-lg);
		--yeti-lede-measure: 50ch;

		--yeti-font-sans: system-ui, sans-serif;
		--yeti-font-mono: ui-monospace, monospace;
	}
}

/* src/tokens/color.css */
/* Color. Inputs are hues and one chroma. Every role derives from them
   through a fixed lightness ladder, declared once per role with light-dark()
   so the two schemes are mirror images. Forcing a scheme on any ancestor
   (color-scheme: light) flips everything below it.
   Ladder (lightness light / dark): subtle 0.95/0.25, soft 0.85/0.35,
   base 0.52/0.70, strong 0.42/0.80, text 0.35/0.85. */

/* Registered so a bad override (say, a color name where a hue angle belongs)
   falls back to the initial value instead of invalidating every derived color.
   @property rules are not layered; validate's layer checks do not cover them. */
@property --yeti-hue-primary { syntax: "<number>"; inherits: true; initial-value: 250; }
@property --yeti-hue-secondary { syntax: "<number>"; inherits: true; initial-value: 300; }
@property --yeti-hue-success { syntax: "<number>"; inherits: true; initial-value: 145; }
@property --yeti-hue-warning { syntax: "<number>"; inherits: true; initial-value: 80; }
@property --yeti-hue-alert { syntax: "<number>"; inherits: true; initial-value: 25; }
@property --yeti-chroma { syntax: "<number>"; inherits: true; initial-value: 0.15; }

@layer yeti.base {
	:root {
		color-scheme: light dark;

		/* Public inputs */
		--yeti-hue-primary: 250;
		--yeti-hue-secondary: 300;
		--yeti-hue-success: 145;
		--yeti-hue-warning: 80;
		--yeti-hue-alert: 25;
		--yeti-hue-neutral: var(--yeti-hue-primary);
		--yeti-chroma: 0.15;

		/* Internal ladder. Tune here, not in the roles below. */
		--_yeti-l-subtle-light: 0.95; --_yeti-l-subtle-dark: 0.25;
		--_yeti-l-soft-light: 0.85;   --_yeti-l-soft-dark: 0.35;
		--_yeti-l-base-light: 0.52;   --_yeti-l-base-dark: 0.70;
		--_yeti-l-strong-light: 0.42; --_yeti-l-strong-dark: 0.80;
		--_yeti-l-text-light: 0.35;   --_yeti-l-text-dark: 0.85;
		--_yeti-c-subtle: calc(var(--yeti-chroma) * 0.4);
		--_yeti-c-soft: calc(var(--yeti-chroma) * 0.7);
		--_yeti-c-text: calc(var(--yeti-chroma) * 0.8);
		--_yeti-c-neutral: 0.02;
		--_yeti-on-light: oklch(0.99 0 0);
		--_yeti-on-dark: oklch(0.15 0 0);

		/* Primary */
		--yeti-color-primary-subtle: light-dark(oklch(var(--_yeti-l-subtle-light) var(--_yeti-c-subtle) var(--yeti-hue-primary)), oklch(var(--_yeti-l-subtle-dark) var(--_yeti-c-subtle) var(--yeti-hue-primary))); /* #d1f3ff */
		--yeti-color-primary-soft: light-dark(oklch(var(--_yeti-l-soft-light) var(--_yeti-c-soft) var(--yeti-hue-primary)), oklch(var(--_yeti-l-soft-dark) var(--_yeti-c-soft) var(--yeti-hue-primary))); /* #98d3ff */
		--yeti-color-primary: light-dark(oklch(var(--_yeti-l-base-light) var(--yeti-chroma) var(--yeti-hue-primary)), oklch(var(--_yeti-l-base-dark) var(--yeti-chroma) var(--yeti-hue-primary))); /* #006bbb */
		--yeti-color-primary-strong: light-dark(oklch(var(--_yeti-l-strong-light) var(--yeti-chroma) var(--yeti-hue-primary)), oklch(var(--_yeti-l-strong-dark) var(--yeti-chroma) var(--yeti-hue-primary))); /* #004d9a */
		--yeti-color-primary-text: light-dark(oklch(var(--_yeti-l-text-light) var(--_yeti-c-text) var(--yeti-hue-primary)), oklch(var(--_yeti-l-text-dark) var(--_yeti-c-text) var(--yeti-hue-primary))); /* #003b75 */
		--yeti-on-primary: light-dark(var(--_yeti-on-light), var(--_yeti-on-dark)); /* #fcfcfc */

		/* Secondary */
		--yeti-color-secondary-subtle: light-dark(oklch(var(--_yeti-l-subtle-light) var(--_yeti-c-subtle) var(--yeti-hue-secondary)), oklch(var(--_yeti-l-subtle-dark) var(--_yeti-c-subtle) var(--yeti-hue-secondary))); /* #f4e6ff */
		--yeti-color-secondary-soft: light-dark(oklch(var(--_yeti-l-soft-light) var(--_yeti-c-soft) var(--yeti-hue-secondary)), oklch(var(--_yeti-l-soft-dark) var(--_yeti-c-soft) var(--yeti-hue-secondary))); /* #d8beff */
		--yeti-color-secondary: light-dark(oklch(var(--_yeti-l-base-light) var(--yeti-chroma) var(--yeti-hue-secondary)), oklch(var(--_yeti-l-base-dark) var(--yeti-chroma) var(--yeti-hue-secondary))); /* #7750b1 */
		--yeti-color-secondary-strong: light-dark(oklch(var(--_yeti-l-strong-light) var(--yeti-chroma) var(--yeti-hue-secondary)), oklch(var(--_yeti-l-strong-dark) var(--yeti-chroma) var(--yeti-hue-secondary))); /* #5b3291 */
		--yeti-color-secondary-text: light-dark(oklch(var(--_yeti-l-text-light) var(--_yeti-c-text) var(--yeti-hue-secondary)), oklch(var(--_yeti-l-text-dark) var(--_yeti-c-text) var(--yeti-hue-secondary))); /* #45266e */
		--yeti-on-secondary: light-dark(var(--_yeti-on-light), var(--_yeti-on-dark)); /* #fcfcfc */

		/* Success */
		--yeti-color-success-subtle: light-dark(oklch(var(--_yeti-l-subtle-light) var(--_yeti-c-subtle) var(--yeti-hue-success)), oklch(var(--_yeti-l-subtle-dark) var(--_yeti-c-subtle) var(--yeti-hue-success))); /* #d6fad6 */
		--yeti-color-success-soft: light-dark(oklch(var(--_yeti-l-soft-light) var(--_yeti-c-soft) var(--yeti-hue-success)), oklch(var(--_yeti-l-soft-dark) var(--_yeti-c-soft) var(--yeti-hue-success))); /* #a2e0a3 */
		--yeti-color-success: light-dark(oklch(var(--_yeti-l-base-light) var(--yeti-chroma) var(--yeti-hue-success)), oklch(var(--_yeti-l-base-dark) var(--yeti-chroma) var(--yeti-hue-success))); /* #1b7e2a */
		--yeti-color-success-strong: light-dark(oklch(var(--_yeti-l-strong-light) var(--yeti-chroma) var(--yeti-hue-success)), oklch(var(--_yeti-l-strong-dark) var(--yeti-chroma) var(--yeti-hue-success))); /* #006002 */
		--yeti-color-success-text: light-dark(oklch(var(--_yeti-l-text-light) var(--_yeti-c-text) var(--yeti-hue-success)), oklch(var(--_yeti-l-text-dark) var(--_yeti-c-text) var(--yeti-hue-success))); /* #004906 */
		--yeti-on-success: light-dark(var(--_yeti-on-light), var(--_yeti-on-dark)); /* #fcfcfc */

		/* Warning */
		--yeti-color-warning-subtle: light-dark(oklch(var(--_yeti-l-subtle-light) var(--_yeti-c-subtle) var(--yeti-hue-warning)), oklch(var(--_yeti-l-subtle-dark) var(--_yeti-c-subtle) var(--yeti-hue-warning))); /* #ffebc2 */
		--yeti-color-warning-soft: light-dark(oklch(var(--_yeti-l-soft-light) var(--_yeti-c-soft) var(--yeti-hue-warning)), oklch(var(--_yeti-l-soft-dark) var(--_yeti-c-soft) var(--yeti-hue-warning))); /* #f2c67c */
		--yeti-color-warning: light-dark(oklch(var(--_yeti-l-base-light) var(--yeti-chroma) var(--yeti-hue-warning)), oklch(var(--_yeti-l-base-dark) var(--yeti-chroma) var(--yeti-hue-warning))); /* #955b00 */
		--yeti-color-warning-strong: light-dark(oklch(var(--_yeti-l-strong-light) var(--yeti-chroma) var(--yeti-hue-warning)), oklch(var(--_yeti-l-strong-dark) var(--yeti-chroma) var(--yeti-hue-warning))); /* #763e00 */
		--yeti-color-warning-text: light-dark(oklch(var(--_yeti-l-text-light) var(--_yeti-c-text) var(--yeti-hue-warning)), oklch(var(--_yeti-l-text-dark) var(--_yeti-c-text) var(--yeti-hue-warning))); /* #5a2f00 */
		--yeti-on-warning: light-dark(var(--_yeti-on-light), var(--_yeti-on-dark)); /* #fcfcfc */

		/* Alert */
		--yeti-color-alert-subtle: light-dark(oklch(var(--_yeti-l-subtle-light) var(--_yeti-c-subtle) var(--yeti-hue-alert)), oklch(var(--_yeti-l-subtle-dark) var(--_yeti-c-subtle) var(--yeti-hue-alert))); /* #ffe0da */
		--yeti-color-alert-soft: light-dark(oklch(var(--_yeti-l-soft-light) var(--_yeti-c-soft) var(--yeti-hue-alert)), oklch(var(--_yeti-l-soft-dark) var(--_yeti-c-soft) var(--yeti-hue-alert))); /* #ffb3ab */
		--yeti-color-alert: light-dark(oklch(var(--_yeti-l-base-light) var(--yeti-chroma) var(--yeti-hue-alert)), oklch(var(--_yeti-l-base-dark) var(--yeti-chroma) var(--yeti-hue-alert))); /* #af3c3a */
		--yeti-color-alert-strong: light-dark(oklch(var(--_yeti-l-strong-light) var(--yeti-chroma) var(--yeti-hue-alert)), oklch(var(--_yeti-l-strong-dark) var(--yeti-chroma) var(--yeti-hue-alert))); /* #8d1a1e */
		--yeti-color-alert-text: light-dark(oklch(var(--_yeti-l-text-light) var(--_yeti-c-text) var(--yeti-hue-alert)), oklch(var(--_yeti-l-text-dark) var(--_yeti-c-text) var(--yeti-hue-alert))); /* #6c1517 */
		--yeti-on-alert: light-dark(var(--_yeti-on-light), var(--_yeti-on-dark)); /* #fcfcfc */

		/* Neutral: the same ladder at the neutral hue and chroma, for data-variant="neutral" */
		--yeti-color-neutral-subtle: light-dark(oklch(var(--_yeti-l-subtle-light) var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(var(--_yeti-l-subtle-dark) var(--_yeti-c-neutral) var(--yeti-hue-neutral)));
		--yeti-color-neutral-soft: light-dark(oklch(var(--_yeti-l-soft-light) var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(var(--_yeti-l-soft-dark) var(--_yeti-c-neutral) var(--yeti-hue-neutral)));
		--yeti-color-neutral: light-dark(oklch(var(--_yeti-l-base-light) var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(var(--_yeti-l-base-dark) var(--_yeti-c-neutral) var(--yeti-hue-neutral)));
		--yeti-color-neutral-strong: light-dark(oklch(var(--_yeti-l-strong-light) var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(var(--_yeti-l-strong-dark) var(--_yeti-c-neutral) var(--yeti-hue-neutral)));
		--yeti-color-neutral-text: light-dark(oklch(var(--_yeti-l-text-light) var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(var(--_yeti-l-text-dark) var(--_yeti-c-neutral) var(--yeti-hue-neutral)));
		--yeti-on-neutral: light-dark(var(--_yeti-on-light), var(--_yeti-on-dark));

		/* Page roles from the neutral hue at low chroma */
		--yeti-color-surface: light-dark(oklch(0.99 var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(0.13 var(--_yeti-c-neutral) var(--yeti-hue-neutral))); /* #f2fdff */
		--yeti-color-surface-raised: light-dark(oklch(0.97 var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(0.17 var(--_yeti-c-neutral) var(--yeti-hue-neutral))); /* #ebf7ff */
		--yeti-color-surface-sunken: light-dark(oklch(0.94 var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(0.10 var(--_yeti-c-neutral) var(--yeti-hue-neutral))); /* #e1edf8 */
		--yeti-color-text: light-dark(oklch(0.15 var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(0.93 var(--_yeti-c-neutral) var(--yeti-hue-neutral))); /* #050c13 */
		--yeti-color-text-muted: light-dark(oklch(0.45 var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(0.70 var(--_yeti-c-neutral) var(--yeti-hue-neutral))); /* #4d5660 */
		--yeti-color-border: light-dark(oklch(0.88 var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(0.30 var(--_yeti-c-neutral) var(--yeti-hue-neutral))); /* #ced9e5 */
		--yeti-color-border-strong: light-dark(oklch(0.75 var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(0.40 var(--_yeti-c-neutral) var(--yeti-hue-neutral))); /* #a5afba */
		--yeti-color-focus: var(--yeti-color-primary); /* #006bbb */
	}
}

/* src/tokens/motion.css */
/* Motion. Components transition on these, so one media query here honours
   the reduced-motion preference everywhere. */
@layer yeti.base {
	:root {
		--yeti-duration-fast: 150ms;
		--yeti-duration-base: 250ms;
		--yeti-ease: cubic-bezier(0.2, 0, 0, 1);
		/* For the animations that run forever: the spinner, the busy stripes.
		   A components-layer shorthand outranks the reset's iteration count the
		   same way it outranks its duration, so a bare infinite in a component
		   survives reduced motion as an endless 0.01ms animation that samples a
		   random phase every frame and strobes. This collapses to one run. */
		--yeti-motion-iterations: infinite;

		/* The crossfade between one page and the next, for a site that has
		   turned cross-document view transitions on. Its own pair rather than
		   the durations above, so tuning how a page arrives never moves a
		   button's hover. */
		--yeti-page-duration: 200ms;
		/* Symmetric, unlike --yeti-ease: one page is leaving as evenly as the
		   other is arriving, and neither end of the crossfade leads. */
		--yeti-page-ease: ease;
	}

	@media (prefers-reduced-motion: reduce) {
		:root {
			--yeti-duration-fast: 0.01ms;
			--yeti-duration-base: 0.01ms;
			--yeti-motion-iterations: 1;
			/* base/transitions.css re-times the browser's own crossfade with
			   this, so collapsing it here is what stops a page transition for
			   a reader who asked for less motion. Nothing else can: the
			   universal selector in the reset does not match a
			   ::view-transition pseudo-element, and @view-transition is an
			   at-rule with no element to select at all. */
			--yeti-page-duration: 0.01ms;
		}
	}
}

/* src/tokens/surface.css */
/* Surfaces and controls: the widths, weights, shadows, and control sizes
   that components read. Every value here is a theme's to change. */
@layer yeti.base {
	:root {
		--yeti-border-width: 1px;

		--yeti-weight-normal: 400;
		--yeti-weight-strong: 600;
		/* Headings and strong text. Kept apart from strong so a theme can set
		   labels and buttons at 600 and leave display text at 700, or the reverse. */
		--yeti-weight-bold: 700;

		/* Shadows are one colour and two offsets, so a theme can retune either. */
		--yeti-shadow-color: light-dark(oklch(0 0 0 / 0.14), oklch(0 0 0 / 0.55));
		--yeti-shadow-sm: 0 1px 2px var(--yeti-shadow-color);
		--yeti-shadow-md: 0 4px 12px var(--yeti-shadow-color);

		/* Controls: the smallest a button or input gets, and its corner. */
		--yeti-control-size: 2.5rem;
		--yeti-control-radius: var(--yeti-radius-sm);
		/* Not the divider colour. A text field, an unchecked box and an off
		   switch are identified by this border alone, so it has to clear 3:1
		   against the surface; the divider sits at 1.4:1 and is meant to. */
		--yeti-control-border: light-dark(oklch(0.58 var(--_yeti-c-neutral) var(--yeti-hue-neutral)), oklch(0.55 var(--_yeti-c-neutral) var(--yeti-hue-neutral)));
		--yeti-control-surface: var(--yeti-color-surface);
		--yeti-control-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%23808080' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
		--yeti-opacity-muted: 0.6;
	}
}

/* src/tokens/components.css */
/* Per-component overrides. Each defaults to a global token; a theme that
   wants pill buttons or square cards sets one line here. */
@layer yeti.base {
	:root {
		--yeti-button-radius: var(--yeti-radius-md);
		--yeti-button-weight: var(--yeti-weight-strong);
		--yeti-button-padding: 2;

		--yeti-badge-radius: var(--yeti-radius-full);
		--yeti-badge-weight: var(--yeti-weight-strong);

		--yeti-card-radius: var(--yeti-radius-md);
		--yeti-card-padding: var(--yeti-space-md);
		--yeti-card-gap: var(--yeti-space-sm);
		--yeti-card-border: var(--yeti-color-border);
		--yeti-card-surface: var(--yeti-color-surface-raised);

		--yeti-field-gap: var(--yeti-space-xs);

		--yeti-table-stripe: var(--yeti-color-surface-sunken);
		--yeti-table-border: var(--yeti-color-border);

		--yeti-nav-padding: var(--yeti-space-sm) var(--yeti-space-md);
		--yeti-nav-radius: var(--yeti-radius-sm);
		--yeti-nav-border: var(--yeti-color-border);
		--yeti-nav-surface: var(--yeti-color-surface);
		--yeti-nav-panel: var(--yeti-color-surface-raised);

		--yeti-breadcrumbs-separator: "/";

		--yeti-pagination-radius: var(--yeti-radius-md);

		--yeti-alert-radius: var(--yeti-radius-md);
		--yeti-alert-padding: var(--yeti-space-md);

		--yeti-progress-radius: var(--yeti-radius-full);

		--yeti-spinner-duration: 0.8s;
		--yeti-spinner-width: 0.15em;

		--yeti-accordion-border: var(--yeti-color-border);
		--yeti-accordion-padding: var(--yeti-space-md);
		--yeti-accordion-surface: var(--yeti-color-surface);
		/* Mixed toward the text colour rather than picked off the surface ramp,
		   because the ramp's own steps are too small to read here: a summary and
		   its answer came out three hundredths of a lightness apart in dark and
		   the rows vanished into the panel. Mixing toward text also moves the
		   right way in both schemes on its own, darkening the row in light and
		   lifting it in dark, and a theme that restyles surface or text gets a
		   header that still works without touching these. */
		--yeti-accordion-summary: color-mix(in oklch, var(--yeti-accordion-surface) 88%, var(--yeti-color-text));
		--yeti-accordion-summary-hover: color-mix(in oklch, var(--yeti-accordion-surface) 80%, var(--yeti-color-text));

		--yeti-tabs-border: var(--yeti-color-border);
		--yeti-tabs-padding: var(--yeti-space-sm) var(--yeti-space-md);

		--yeti-dropdown-surface: var(--yeti-color-surface-raised);
		--yeti-dropdown-radius: var(--yeti-radius-md);
		--yeti-dropdown-padding: var(--yeti-space-xs);
		--yeti-dropdown-min: 12rem;
		/* Read by hover.js, not by CSS: the pause before a hover-triggered panel
		   opens, and the grace after the pointer leaves. */
		--yeti-dropdown-open-delay: 100ms;
		--yeti-dropdown-close-delay: 200ms;

		--yeti-dialog-surface: var(--yeti-color-surface-raised);
		--yeti-dialog-radius: var(--yeti-radius-lg);
		--yeti-dialog-padding: var(--yeti-space-lg);
		--yeti-dialog-backdrop: color-mix(in oklch, var(--yeti-color-text) 45%, transparent);

		--yeti-tooltip-surface: var(--yeti-color-text);
		--yeti-tooltip-text: var(--yeti-color-surface);
		--yeti-tooltip-radius: var(--yeti-radius-sm);

		--yeti-carousel-dot: var(--yeti-color-border-strong);
		--yeti-carousel-dot-size: 0.75rem;
		--yeti-carousel-scroll: smooth;

		/* Per element, not per theme: the filled share of a range's track. */
		--yeti-range-value: 0%;

		/* Demo: a live example in a box the reader can drag. The heights are
		   what a frame gets, since a frame cannot size to its content. */
		--yeti-demo-border: var(--yeti-color-border);
		--yeti-demo-radius: var(--yeti-radius-md);
		--yeti-demo-label: var(--yeti-color-surface-sunken);

		/* Enter: the one-shot arrival. 600ms is longer than any transition in the
		   framework on purpose; an entrance the reader is meant to notice has to
		   outlast the glance that finds it, and the same 600ms is what the
		   foundationcss.com homepage had arrived at by hand. The rise is 2rem,
		   far enough to read as arriving from below and short enough that the
		   element never starts outside its own row. The scale stops at 0.94
		   rather than something smaller because a card that starts at half size
		   reads as a zoom, which is a different gesture. */
		--yeti-enter-duration: 600ms;
		--yeti-enter-distance: 2rem;
		--yeti-enter-scale: 0.94;
		/* The gap between one staggered child and the next. A theme slows the
		   whole run down by raising this one number, since every child's delay
		   is a multiple of it. */
		--yeti-enter-stagger: 200ms;
		/* How long the whole run waits before the first child moves. Zero by
		   default, because an entrance that hesitates reads as a stall; a page
		   that would rather paint first before anything moves raises it. */
		--yeti-enter-delay: 0s;
		/* Its own curve, not the shared one, for the same reason as the
		   durations: a page that wants a different feel for an entrance than
		   for a hover has to be able to say so without moving both. */
		--yeti-enter-ease: var(--yeti-ease);

		/* Lift: how far a card rises under the pointer, and the shadow it casts
		   from up there. Reduced motion takes the distance to zero below and
		   leaves the shadow, which is why the two are separate tokens. */
		--yeti-lift-distance: 0.25rem;
		--yeti-lift-shadow: var(--yeti-shadow-md);
		/* Its own duration rather than the shared fast one, because a hover on
		   a large tile wants longer than a hover on a control, and a consumer
		   that changes a card's colours on the same hover has to be able to
		   match this exactly or the two halves arrive apart. */
		--yeti-lift-duration: var(--yeti-duration-fast);
		--yeti-lift-ease: var(--yeti-ease);

		/* Attention: one pulse or one shake over 600ms, the same length as an
		   entrance, because both are a single gesture the reader has to catch.
		   The shake's throw is deliberately small: a wobble says "look here",
		   while a wide swing says "wrong", and only a field's error should say
		   that. */
		--yeti-attention-duration: 600ms;
		--yeti-attention-distance: 0.35rem;
		--yeti-attention-scale: 1.06;
	}

	/* The ring reads a token so the preference reaches it: a components-layer
	   animation beats the reset's reduced-motion rule, but not the value it
	   resolves. The same idiom as motion.css. */
	@media (prefers-reduced-motion: reduce) {
		:root {
			--yeti-spinner-duration: 0.01ms;
			--yeti-carousel-scroll: auto;
			--yeti-enter-duration: 0.01ms;
			/* The step goes with the duration. A collapsed duration alone would
			   still hand the third card a 400ms wait and then pop it into place,
			   which is a staggered arrival with the smooth part taken out. */
			--yeti-enter-stagger: 0s;
			--yeti-attention-duration: 0.01ms;
			/* A hover lift is the one piece of motion here that is not a one-shot,
			   and collapsing its duration is the wrong answer: the element would
			   still move a quarter of a rem, just instantly, so every pass of the
			   pointer becomes a flinch under the cursor. Reduced motion takes the
			   movement away instead and leaves the deepened shadow to carry the
			   whole hover, which is a change of appearance and not of position. */
			--yeti-lift-distance: 0rem;
		}
	}
}

/* src/base/reset.css */
/* Yeti reset. Protective, not opinionated: each rule undoes a surprising
   or inconsistent user-agent default and says why. Design decisions live
   in the base layer. This file reads no token and can be swapped out. */
@layer yeti.reset {
	/* Sizing that includes padding and border is what everyone expects. */
	*, ::before, ::after {
		box-sizing: border-box;
	}

	/* Spacing belongs to layouts (architecture §6.6). Every element starts
	   with none. dialog is exempt so its native centring survives. */
	*:not(dialog) {
		margin: 0;
	}

	/* Stop mobile browsers inflating text on orientation change. */
	html {
		text-size-adjust: none;
		-webkit-text-size-adjust: none;
	}

	/* Media are blocks that never overflow their container. */
	img, picture, video, canvas, svg {
		display: block;
		max-inline-size: 100%;
		block-size: auto;
	}

	/* Form controls inherit text styling instead of the UA's own. */
	input, button, textarea, select {
		font: inherit;
		color: inherit;
		letter-spacing: inherit;
	}

	/* WebKit renders these controls with non-inheriting chrome. */
	input[type="search"] {
		-webkit-appearance: textfield;
		appearance: textfield;
	}
	::-webkit-search-decoration {
		-webkit-appearance: none;
	}
	::-webkit-file-upload-button {
		-webkit-appearance: button;
		font: inherit;
	}
	::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
		block-size: auto;
	}

	/* A textarea that grows with its content, where supported. */
	@supports (field-sizing: content) {
		textarea {
			field-sizing: content;
		}
	}

	/* Superscripts and subscripts must not disturb line height. */
	sub, sup {
		font-size: 75%;
		line-height: 0;
		position: relative;
		vertical-align: baseline;
	}
	sub { inset-block-start: 0.25em; }
	sup { inset-block-start: -0.5em; }

	/* Tables collapse borders so a single border token draws a grid. */
	table {
		border-collapse: collapse;
		border-spacing: 0;
	}

	/* The hidden attribute must win over everything. This is the one
	   !important in Yeti (validate enforces that). */
	[hidden] {
		display: none !important;
	}

	/* Lists lose their markers only where the author says the list is
	   decorative; a plain ul keeps its semantics and its bullets. */
	:is(ul, ol)[role="list"] {
		list-style: none;
		padding-inline-start: 0;
	}

	/* Let height animate to and from auto where the engine supports it,
	   and only for people who have not asked for reduced motion. */
	@supports (interpolate-size: allow-keywords) {
		@media (prefers-reduced-motion: no-preference) {
			:root {
				interpolate-size: allow-keywords;
			}
		}
	}

	/* Reduced motion for user-agent and third-party animation only. This
	   cannot reach a Yeti component: a declaration in the components layer
	   outranks this one. Yeti's own motion collapses through the tokens in
	   tokens/motion.css and tokens/components.css instead, which is why every
	   duration and iteration count in a component reads one. Durations are
	   collapsed rather than effects removed, so end states still apply. */
	@media (prefers-reduced-motion: reduce) {
		*, ::before, ::after {
			animation-duration: 0.01ms;
			animation-iteration-count: 1;
			transition-duration: 0.01ms;
			scroll-behavior: auto;
		}
	}
}

/* src/base/typography.css */
/* Typography defaults for bare HTML. Every value is a token. */
@layer yeti.base {
	html {
		font-family: var(--yeti-font-sans);
		color: var(--yeti-color-text);
		background-color: var(--yeti-color-surface);
	}

	/* Type size lives on body, not html, so rem keeps the visitor's root
	   default and every rem-based token resolves the same on every element. */
	body {
		font-size: var(--yeti-text-md);
		line-height: var(--yeti-leading-md);
	}

	h1, h2, h3, h4, h5, h6 {
		font-weight: var(--yeti-weight-bold);
		line-height: var(--yeti-leading-tight);
		text-wrap: balance;
	}
	h1 { font-size: var(--yeti-text-3xl); }
	h2 { font-size: var(--yeti-text-2xl); }
	h3 { font-size: var(--yeti-text-xl); }
	h4 { font-size: var(--yeti-text-lg); }
	h5 { font-size: var(--yeti-text-md); }
	h6 { font-size: var(--yeti-text-sm); }

	p {
		text-wrap: pretty;
	}

	small {
		font-size: var(--yeti-text-sm);
	}

	strong, b {
		font-weight: var(--yeti-weight-bold);
	}

	a {
		color: var(--yeti-color-primary);
		text-decoration: underline;
		text-decoration-skip-ink: auto;
		text-underline-offset: 0.15em;
	}
	a:hover {
		color: var(--yeti-color-primary-strong);
	}

	:focus-visible {
		outline: 2px solid var(--yeti-color-focus);
		outline-offset: 2px;
	}

	::selection {
		background-color: var(--yeti-color-primary-subtle);
	}
}

/* src/base/prose.css */
/* Flow rhythm for unclassed content. The one place base adds space, and
   only between siblings in flow; no element sets its own outer margin.
   Every rule here is scoped to direct children of a flow container, so
   layouts and other containers are untouched. Layouts in later layers
   replace this with their own gap. */
@layer yeti.base {
	:is(body, main, article, section, aside, header, footer, blockquote, figure, dd, li, details, fieldset, dialog) > * + * {
		margin-block-start: var(--yeti-space-md);
	}

	/* A heading hugs what follows it. */
	:is(body, main, article, section, aside, header, footer, blockquote, figure, dd, li, details, fieldset, dialog) > :is(h1, h2, h3, h4, h5, h6) + * {
		margin-block-start: var(--yeti-space-sm);
	}

	/* Major headings open up from the content before them. */
	:is(body, main, article, section, aside, header, footer, blockquote, figure, dd, li, details, fieldset) > :not(h1, h2, h3, h4, h5, h6) + :is(h2, h3) {
		margin-block-start: var(--yeti-space-xl);
	}

	/* Measure: prose never runs wider than a comfortable line. */
	p, li, dd, dt, blockquote, figcaption, h1, h2, h3, h4, h5, h6 {
		max-inline-size: var(--yeti-measure);
	}

	:is(ul, ol):not([role="list"]) {
		padding-inline-start: var(--yeti-space-lg);
	}

	/* Only a list that is prose. The reset already reads role="list" as the
	   sign that a list is layout, and every list component carries it, so
	   scoping here means none of them has to zero this again. */
	:is(ul, ol):not([role="list"]) > li + li {
		/* A hair, not a gap: a list is meant to read denser than prose, and the
		   space scale's smallest step is already two thirds of a line, which
		   spaces items like short paragraphs. */
		margin-block-start: var(--yeti-list-gap);
	}
}

/* src/base/controls.css */
/* Neutral form controls and tables. Tidy, undesigned; the real look
   arrives with the form and table components. */
@layer yeti.base {
	label, legend {
		display: block;
		font-weight: var(--yeti-weight-strong);
	}

	input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="submit"], [type="button"], [type="reset"], [type="file"], [type="image"]),
	select,
	textarea {
		inline-size: 100%;
		padding: var(--yeti-space-xs) var(--yeti-space-sm);
		border: var(--yeti-border-width) solid var(--yeti-color-border);
		border-radius: var(--yeti-radius-sm);
		background-color: var(--yeti-color-surface);
	}

	textarea {
		min-block-size: 4lh;
		resize: vertical;
	}

	button,
	input:is([type="submit"], [type="button"], [type="reset"]) {
		padding: var(--yeti-space-xs) var(--yeti-space-sm);
		border: var(--yeti-border-width) solid var(--yeti-color-border);
		border-radius: var(--yeti-radius-sm);
		background-color: var(--yeti-color-surface-raised);
		cursor: default;
	}

	:is(input, select, textarea, button):disabled {
		opacity: var(--yeti-opacity-muted);
	}

	fieldset {
		padding: var(--yeti-space-sm) var(--yeti-space-md);
		border: var(--yeti-border-width) solid var(--yeti-color-border);
		border-radius: var(--yeti-radius-sm);
	}

	table {
		inline-size: 100%;
	}
	caption {
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
		text-align: start;
	}
	th {
		font-weight: var(--yeti-weight-bold);
		text-align: start;
	}
	th, td {
		padding: var(--yeti-space-sm);
		border-block-end: 1px solid var(--yeti-color-border);
	}
}

/* src/base/media.css */
/* Figures, code, rules, and quotes. */
@layer yeti.base {
	figcaption {
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
	}

	code, kbd, samp {
		font-family: var(--yeti-font-mono);
		font-size: 0.9em;
		padding: 0.1em 0.3em;
		border-radius: var(--yeti-radius-sm);
		background-color: var(--yeti-color-surface-raised);
	}

	pre {
		font-family: var(--yeti-font-mono);
		/* Code is read line by line rather than as a paragraph, and a block of
		   it is nearly always set below the body size, where the body's leading
		   formula loosens rather than tightens. A ratio keeps it even. */
		line-height: var(--yeti-leading-sm);
		padding: var(--yeti-space-md);
		border-radius: var(--yeti-radius-md);
		background-color: var(--yeti-color-surface-raised);
		overflow-x: auto;
	}
	pre code {
		padding: 0;
		border-radius: 0;
		background-color: transparent;
		font-size: inherit;
	}

	hr {
		border: 0;
		border-block-start: var(--yeti-border-width) solid var(--yeti-color-border);
	}

	blockquote {
		padding-inline: var(--yeti-space-md);
		border-inline-start: 4px solid var(--yeti-color-border-strong);
	}
}

/* src/base/transitions.css */
/* Page transitions: the crossfade a browser draws between one document and the
   next, re-timed onto Yeti's tokens.
 
   Yeti does not turn this on. Cross-document view transitions are not Baseline
   — Firefox ignores the at-rule as of this writing, where the navigation
   simply snaps — and the README's rule is that a feature below Baseline is
   guarded rather than assumed. There is no guard to put it behind either:
   @view-transition is an at-rule with no element to select, so it cannot be
   scoped to a class the way every other opt-in in Yeti is. A site turns it on
   itself, in its own stylesheet, with one rule:
 
       @view-transition { navigation: auto; }
 
   Both documents have to carry it and they have to be same-origin, which for a
   site built on one stylesheet means it is on everywhere or nowhere.
 
   What is here is the part that is easy to get wrong. The browser's own
   crossfade is already the right animation, and it blends the two snapshots
   correctly, which a hand-written pair of opacity keyframes does not: those dip
   through the page background at the halfway point unless they also carry
   mix-blend-mode. So this changes when it runs and nothing about what it does.
 
   Re-timing it is also the only way to honour a reader who has asked for less
   motion. The reset's universal selector cannot reach a ::view-transition
   pseudo-element, and no selector at all reaches the at-rule, so the collapsed
   --yeti-page-duration in tokens/motion.css is what stills the crossfade. That
   is the same bargain every Yeti component makes: the duration collapses, the
   end state still applies, and the reader arrives on the new page at once
   instead of watching it fade in.
 
   A site that writes its own view-transition animations should read the same
   two tokens, or it opts back out of that. */
@layer yeti.base {
	/* Longhands, not a shorthand: the browser supplies animation-name and the
	   keyframes, and this leaves both alone. An author-origin longhand outranks
	   the user-agent sheet's shorthand, so the name survives and only the
	   timing changes. */
	::view-transition-old(root),
	::view-transition-new(root) {
		animation-duration: var(--yeti-page-duration);
		animation-timing-function: var(--yeti-page-ease);
	}
}

/* src/layouts/attributes.css */
/* The shared attribute vocabulary. Each data-* value sets a private property
   once; layouts read the property and supply their own default only when the
   attribute is absent, so a nested layout never inherits a parent's value. */
@layer yeti.layouts {
	/* data-gap: one stop */
	[data-gap="none"] { --_yeti-gap: var(--yeti-space-none); }
	[data-gap="xs"] { --_yeti-gap: var(--yeti-space-xs); }
	[data-gap="sm"] { --_yeti-gap: var(--yeti-space-sm); }
	[data-gap="md"] { --_yeti-gap: var(--yeti-space-md); }
	[data-gap="lg"] { --_yeti-gap: var(--yeti-space-lg); }
	[data-gap="xl"] { --_yeti-gap: var(--yeti-space-xl); }
	[data-gap="2xl"] { --_yeti-gap: var(--yeti-space-2xl); }
	[data-gap="3xl"] { --_yeti-gap: var(--yeti-space-3xl); }

	/* data-gap: fluid pairs. From the small stop's narrow value to the large
	   stop's wide value, driven by the scale's interpolant. */
	[data-gap="xs-sm"] { --_yeti-gap: calc(var(--yeti-space-xs-static) + (var(--_yeti-space-sm-max) - var(--yeti-space-xs-static)) * var(--_yeti-t)); }
	[data-gap="xs-md"] { --_yeti-gap: calc(var(--yeti-space-xs-static) + (var(--_yeti-space-md-max) - var(--yeti-space-xs-static)) * var(--_yeti-t)); }
	[data-gap="xs-lg"] { --_yeti-gap: calc(var(--yeti-space-xs-static) + (var(--_yeti-space-lg-max) - var(--yeti-space-xs-static)) * var(--_yeti-t)); }
	[data-gap="xs-xl"] { --_yeti-gap: calc(var(--yeti-space-xs-static) + (var(--_yeti-space-xl-max) - var(--yeti-space-xs-static)) * var(--_yeti-t)); }
	[data-gap="xs-2xl"] { --_yeti-gap: calc(var(--yeti-space-xs-static) + (var(--_yeti-space-2xl-max) - var(--yeti-space-xs-static)) * var(--_yeti-t)); }
	[data-gap="xs-3xl"] { --_yeti-gap: calc(var(--yeti-space-xs-static) + (var(--_yeti-space-3xl-max) - var(--yeti-space-xs-static)) * var(--_yeti-t)); }
	[data-gap="sm-md"] { --_yeti-gap: calc(var(--yeti-space-sm-static) + (var(--_yeti-space-md-max) - var(--yeti-space-sm-static)) * var(--_yeti-t)); }
	[data-gap="sm-lg"] { --_yeti-gap: calc(var(--yeti-space-sm-static) + (var(--_yeti-space-lg-max) - var(--yeti-space-sm-static)) * var(--_yeti-t)); }
	[data-gap="sm-xl"] { --_yeti-gap: calc(var(--yeti-space-sm-static) + (var(--_yeti-space-xl-max) - var(--yeti-space-sm-static)) * var(--_yeti-t)); }
	[data-gap="sm-2xl"] { --_yeti-gap: calc(var(--yeti-space-sm-static) + (var(--_yeti-space-2xl-max) - var(--yeti-space-sm-static)) * var(--_yeti-t)); }
	[data-gap="sm-3xl"] { --_yeti-gap: calc(var(--yeti-space-sm-static) + (var(--_yeti-space-3xl-max) - var(--yeti-space-sm-static)) * var(--_yeti-t)); }
	[data-gap="md-lg"] { --_yeti-gap: calc(var(--yeti-space-md-static) + (var(--_yeti-space-lg-max) - var(--yeti-space-md-static)) * var(--_yeti-t)); }
	[data-gap="md-xl"] { --_yeti-gap: calc(var(--yeti-space-md-static) + (var(--_yeti-space-xl-max) - var(--yeti-space-md-static)) * var(--_yeti-t)); }
	[data-gap="md-2xl"] { --_yeti-gap: calc(var(--yeti-space-md-static) + (var(--_yeti-space-2xl-max) - var(--yeti-space-md-static)) * var(--_yeti-t)); }
	[data-gap="md-3xl"] { --_yeti-gap: calc(var(--yeti-space-md-static) + (var(--_yeti-space-3xl-max) - var(--yeti-space-md-static)) * var(--_yeti-t)); }
	[data-gap="lg-xl"] { --_yeti-gap: calc(var(--yeti-space-lg-static) + (var(--_yeti-space-xl-max) - var(--yeti-space-lg-static)) * var(--_yeti-t)); }
	[data-gap="lg-2xl"] { --_yeti-gap: calc(var(--yeti-space-lg-static) + (var(--_yeti-space-2xl-max) - var(--yeti-space-lg-static)) * var(--_yeti-t)); }
	[data-gap="lg-3xl"] { --_yeti-gap: calc(var(--yeti-space-lg-static) + (var(--_yeti-space-3xl-max) - var(--yeti-space-lg-static)) * var(--_yeti-t)); }
	[data-gap="xl-2xl"] { --_yeti-gap: calc(var(--yeti-space-xl-static) + (var(--_yeti-space-2xl-max) - var(--yeti-space-xl-static)) * var(--_yeti-t)); }
	[data-gap="xl-3xl"] { --_yeti-gap: calc(var(--yeti-space-xl-static) + (var(--_yeti-space-3xl-max) - var(--yeti-space-xl-static)) * var(--_yeti-t)); }
	[data-gap="2xl-3xl"] { --_yeti-gap: calc(var(--yeti-space-2xl-static) + (var(--_yeti-space-3xl-max) - var(--yeti-space-2xl-static)) * var(--_yeti-t)); }

	/* data-align: cross-axis alignment */
	[data-align="start"] { --_yeti-align: flex-start; }
	[data-align="center"] { --_yeti-align: center; }
	[data-align="end"] { --_yeti-align: flex-end; }
	[data-align="stretch"] { --_yeti-align: stretch; }
	[data-align="baseline"] { --_yeti-align: baseline; }

	/* data-justify: main-axis distribution */
	[data-justify="start"] { --_yeti-justify: flex-start; }
	[data-justify="center"] { --_yeti-justify: center; }
	[data-justify="end"] { --_yeti-justify: flex-end; }
	[data-justify="between"] { --_yeti-justify: space-between; }
	[data-justify="around"] { --_yeti-justify: space-around; }
	[data-justify="evenly"] { --_yeti-justify: space-evenly; }

	/* Container widths */
	[data-threshold="2xs"] { --_yeti-threshold: var(--yeti-width-2xs); }
	[data-threshold="xs"] { --_yeti-threshold: var(--yeti-width-xs); }
	[data-threshold="sm"] { --_yeti-threshold: var(--yeti-width-sm); }
	[data-threshold="md"] { --_yeti-threshold: var(--yeti-width-md); }
	[data-threshold="lg"] { --_yeti-threshold: var(--yeti-width-lg); }
	[data-threshold="xl"] { --_yeti-threshold: var(--yeti-width-xl); }
	[data-threshold="2xl"] { --_yeti-threshold: var(--yeti-width-2xl); }

	[data-width="2xs"] { --_yeti-width: var(--yeti-width-2xs); }
	[data-width="xs"] { --_yeti-width: var(--yeti-width-xs); }
	[data-width="sm"] { --_yeti-width: var(--yeti-width-sm); }
	[data-width="md"] { --_yeti-width: var(--yeti-width-md); }
	[data-width="lg"] { --_yeti-width: var(--yeti-width-lg); }
	[data-width="xl"] { --_yeti-width: var(--yeti-width-xl); }
	[data-width="2xl"] { --_yeti-width: var(--yeti-width-2xl); }

	/* data-height: a block size, for the few things that take one. It reads
	   the height family the way data-width reads the width family; the demo's
	   preview box is the first taker, and a second reads the same property. */
	[data-height="sm"] { --_yeti-height: var(--yeti-height-sm); }
	[data-height="md"] { --_yeti-height: var(--yeti-height-md); }
	[data-height="lg"] { --_yeti-height: var(--yeti-height-lg); }
	[data-height="xl"] { --_yeti-height: var(--yeti-height-xl); }

	[data-min="none"] { --_yeti-min: 0px; }
	[data-min="2xs"] { --_yeti-min: var(--yeti-width-2xs); }
	[data-min="xs"] { --_yeti-min: var(--yeti-width-xs); }
	[data-min="sm"] { --_yeti-min: var(--yeti-width-sm); }
	[data-min="md"] { --_yeti-min: var(--yeti-width-md); }
	[data-min="lg"] { --_yeti-min: var(--yeti-width-lg); }
	[data-min="xl"] { --_yeti-min: var(--yeti-width-xl); }
	[data-min="2xl"] { --_yeti-min: var(--yeti-width-2xl); }

	[data-max="2xs"] { --_yeti-max: var(--yeti-width-2xs); }
	[data-max="xs"] { --_yeti-max: var(--yeti-width-xs); }
	[data-max="sm"] { --_yeti-max: var(--yeti-width-sm); }
	[data-max="md"] { --_yeti-max: var(--yeti-width-md); }
	[data-max="lg"] { --_yeti-max: var(--yeti-width-lg); }
	[data-max="xl"] { --_yeti-max: var(--yeti-width-xl); }
	[data-max="2xl"] { --_yeti-max: var(--yeti-width-2xl); }

	/* Child placement inside a layer: data-align-self and data-justify-self
	   sit on the child and set the property on the child. */
	[data-align-self="start"] { --_yeti-align-self: start; }
	[data-align-self="center"] { --_yeti-align-self: center; }
	[data-align-self="end"] { --_yeti-align-self: end; }
	[data-align-self="stretch"] { --_yeti-align-self: stretch; }
	[data-align-self="baseline"] { --_yeti-align-self: baseline; }
	[data-justify-self="start"] { --_yeti-justify-self: start; }
	[data-justify-self="center"] { --_yeti-justify-self: center; }
	[data-justify-self="end"] { --_yeti-justify-self: end; }
	[data-justify-self="stretch"] { --_yeti-justify-self: stretch; }

	/* data-ratio */
	[data-ratio="1/1"] { --_yeti-aspect: 1 / 1; }
	[data-ratio="4/3"] { --_yeti-aspect: 4 / 3; }
	[data-ratio="3/2"] { --_yeti-aspect: 3 / 2; }
	[data-ratio="16/9"] { --_yeti-aspect: 16 / 9; }
	[data-ratio="21/9"] { --_yeti-aspect: 21 / 9; }

	/* data-columns: a maximum column count, as a minimum track width for grids
	   and as a count for masonry's multi-column fallback */
	[data-columns="1"] { --_yeti-column-cap: 100%; --_yeti-column-count: 1; }
	[data-columns="2"] { --_yeti-column-cap: calc((100% - 1 * var(--_yeti-gap)) / 2); --_yeti-column-count: 2; }
	[data-columns="3"] { --_yeti-column-cap: calc((100% - 2 * var(--_yeti-gap)) / 3); --_yeti-column-count: 3; }
	[data-columns="4"] { --_yeti-column-cap: calc((100% - 3 * var(--_yeti-gap)) / 4); --_yeti-column-count: 4; }
	[data-columns="5"] { --_yeti-column-cap: calc((100% - 4 * var(--_yeti-gap)) / 5); --_yeti-column-count: 5; }
	[data-columns="6"] { --_yeti-column-cap: calc((100% - 5 * var(--_yeti-gap)) / 6); --_yeti-column-count: 6; }

	/* data-variant: a hue's ladder, for components. A component sets its own
	   default with .name:not([data-variant]), so nothing inherits a parent's. */
	[data-variant="primary"] { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	[data-variant="secondary"] { --_yeti-variant: var(--yeti-color-secondary); --_yeti-variant-subtle: var(--yeti-color-secondary-subtle); --_yeti-variant-soft: var(--yeti-color-secondary-soft); --_yeti-variant-strong: var(--yeti-color-secondary-strong); --_yeti-variant-text: var(--yeti-color-secondary-text); --_yeti-on-variant: var(--yeti-on-secondary); }
	[data-variant="success"] { --_yeti-variant: var(--yeti-color-success); --_yeti-variant-subtle: var(--yeti-color-success-subtle); --_yeti-variant-soft: var(--yeti-color-success-soft); --_yeti-variant-strong: var(--yeti-color-success-strong); --_yeti-variant-text: var(--yeti-color-success-text); --_yeti-on-variant: var(--yeti-on-success); }
	[data-variant="warning"] { --_yeti-variant: var(--yeti-color-warning); --_yeti-variant-subtle: var(--yeti-color-warning-subtle); --_yeti-variant-soft: var(--yeti-color-warning-soft); --_yeti-variant-strong: var(--yeti-color-warning-strong); --_yeti-variant-text: var(--yeti-color-warning-text); --_yeti-on-variant: var(--yeti-on-warning); }
	[data-variant="alert"] { --_yeti-variant: var(--yeti-color-alert); --_yeti-variant-subtle: var(--yeti-color-alert-subtle); --_yeti-variant-soft: var(--yeti-color-alert-soft); --_yeti-variant-strong: var(--yeti-color-alert-strong); --_yeti-variant-text: var(--yeti-color-alert-text); --_yeti-on-variant: var(--yeti-on-alert); }
	[data-variant="neutral"] { --_yeti-variant: var(--yeti-color-neutral); --_yeti-variant-subtle: var(--yeti-color-neutral-subtle); --_yeti-variant-soft: var(--yeti-color-neutral-soft); --_yeti-variant-strong: var(--yeti-color-neutral-strong); --_yeti-variant-text: var(--yeti-color-neutral-text); --_yeti-on-variant: var(--yeti-on-neutral); }

	/* data-size on components: a text step and the space step one below it */
	[data-size="sm"] { --_yeti-size-text: var(--yeti-text-sm); --_yeti-size-space: var(--yeti-space-xs); }
	[data-size="md"] { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }
	[data-size="lg"] { --_yeti-size-text: var(--yeti-text-lg); --_yeti-size-space: var(--yeti-space-md); }

	/* data-span: a child's share of a columns row */
	[data-span="1"] { --_yeti-span: 1; }
	[data-span="2"] { --_yeti-span: 2; }
	[data-span="3"] { --_yeti-span: 3; }
	[data-span="4"] { --_yeti-span: 4; }
	[data-span="5"] { --_yeti-span: 5; }
	[data-span="6"] { --_yeti-span: 6; }

	/* data-ranks: how many parts each child of a ranked grid has */
	[data-ranks="2"] { --_yeti-ranks: 2; }
	[data-ranks="3"] { --_yeti-ranks: 3; }
	[data-ranks="4"] { --_yeti-ranks: 4; }
	[data-ranks="5"] { --_yeti-ranks: 5; }
	[data-ranks="6"] { --_yeti-ranks: 6; }

	/* data-slides: how many slides of a carousel are visible at once */
	[data-slides="1"] { --_yeti-slides: 1; }
	[data-slides="2"] { --_yeti-slides: 2; }
	[data-slides="3"] { --_yeti-slides: 3; }
	[data-slides="4"] { --_yeti-slides: 4; }
}

/* src/layouts/stack/stack.css */
/* Stack: vertical rhythm. A flex column with a gap, so children's own
   margins are irrelevant and a split point costs one attribute. */
@layer yeti.layouts {
	.stack {
		display: flex;
		flex-direction: column;
		align-items: var(--_yeti-align);
		gap: var(--_yeti-gap);
	}
	.stack:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.stack:not([data-align]) { --_yeti-align: stretch; }
	.stack > * { margin: 0; }
	.stack > [data-split] { margin-block-start: auto; }
	.stack[data-fill] { min-block-size: var(--yeti-cover-height); }
}

/* src/layouts/cluster/cluster.css */
/* Cluster: a wrapping row. Items keep their own width and the gap holds on
   both axes when they wrap. */
@layer yeti.layouts {
	.cluster {
		display: flex;
		flex-wrap: wrap;
		align-items: var(--_yeti-align);
		justify-content: var(--_yeti-justify);
		gap: var(--_yeti-gap);
	}
	.cluster:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.cluster:not([data-align]) { --_yeti-align: center; }
	.cluster:not([data-justify]) { --_yeti-justify: flex-start; }
	.cluster > * { margin: 0; }
}

/* src/layouts/sidebar/sidebar.css */
/* Sidebar: a fixed-width companion beside flexible content. The pair sits
   side by side while the content can keep at least half the width, and
   stacks the moment it cannot. */
@layer yeti.layouts {
	.sidebar {
		display: flex;
		flex-wrap: wrap;
		align-items: var(--_yeti-align);
		gap: var(--_yeti-gap);
	}
	.sidebar:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.sidebar:not([data-width]) { --_yeti-width: var(--yeti-width-sm); }
	.sidebar:not([data-align]) { --_yeti-align: stretch; }
	.sidebar > * { margin: 0; }

	/* The sidebar child: first by default, last with data-side="end". */
	.sidebar:not([data-side="end"]) > :first-child,
	.sidebar[data-side="end"] > :last-child {
		flex-basis: var(--_yeti-width);
		flex-grow: 1;
	}

	/* The content child takes everything else and refuses to go below half. */
	.sidebar:not([data-side="end"]) > :last-child,
	.sidebar[data-side="end"] > :first-child {
		flex-basis: 0;
		flex-grow: 999;
		min-inline-size: 50%;
	}
}

/* src/layouts/columns/columns.css */
/* Columns: equal columns above a container width, rows below it. Each child's
   basis flips between negative and positive across the threshold, so the
   switch is decided by the container, not the viewport. */
@layer yeti.layouts {
	.columns {
		display: flex;
		flex-wrap: wrap;
		align-items: var(--_yeti-align);
		justify-content: var(--_yeti-justify);
		gap: var(--_yeti-gap);
	}
	.columns:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.columns:not([data-threshold]) { --_yeti-threshold: var(--yeti-width-md); }
	.columns:not([data-align]) { --_yeti-align: stretch; }
	.columns:not([data-justify]) { --_yeti-justify: flex-start; }
	.columns > * {
		margin: 0;
		flex-grow: 1;
		flex-basis: calc((var(--_yeti-threshold) - 100%) * 999);
	}
	/* data-span: a child's share of the row, as a multiple of its siblings'. */
	.columns > [data-span] { flex-grow: var(--_yeti-span); }

	/* data-limit: the first N share the row; every later child takes a full one. */
	.columns[data-limit="2"] > :nth-child(n + 3),
	.columns[data-limit="3"] > :nth-child(n + 4),
	.columns[data-limit="4"] > :nth-child(n + 5),
	.columns[data-limit="5"] > :nth-child(n + 6) {
		flex-basis: 100%;
	}
}

/* src/layouts/cover/cover.css */
/* Cover: a block at least as tall as the viewport with one child centered
   vertically and anything else pinned above or below it. */
@layer yeti.layouts {
	.cover {
		display: flex;
		flex-direction: column;
		min-block-size: var(--yeti-cover-height);
		gap: var(--_yeti-gap);
	}
	.cover:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.cover > * { margin: 0; }
	.cover > [data-center] { margin-block: auto; }
}

/* src/layouts/grid/grid.css */
/* Grid: as many equal columns as fit at a minimum width, with an optional
   maximum count. The track minimum is the larger of the minimum width and
   the container split N ways, so data-columns caps the count without a
   breakpoint. */
@layer yeti.layouts {
	.grid {
		display: grid;
		gap: var(--_yeti-gap);
		grid-template-columns: repeat(auto-fit, minmax(min(max(var(--_yeti-min), var(--_yeti-column-cap)), 100%), 1fr));
	}
	.grid:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.grid:not([data-min]) { --_yeti-min: var(--yeti-width-xs); }
	.grid:not([data-columns]) { --_yeti-column-cap: 0px; }
	/* data-min="none" describes an exact count and is meaningless without one:
	   with no floor at all, auto-fit repeats until the gaps overflow, which is
	   dozens of tracks, or an engine-defined clamp when the gap is none too. */
	.grid[data-min="none"]:not([data-columns]) { --_yeti-column-cap: 100%; }

	/* data-fold: the count halves as the grid narrows instead of stepping.
	   The grid is its own container and a container query can only style its
	   children, so a folded grid has twelve tracks and the queries set each
	   child's span. Thresholds are count × the width token's default
	   (xs 16rem, sm 24, md 32, lg 48, xl 64, 2xl 80), measured on the content
	   box, and are literal because a container condition cannot read a token.
	   data-columns 2 folds 2 → 1; 4 folds 4 → 2 → 1; 6 folds 6 → 3 → 1. */
	.grid[data-fold] {
		container-type: inline-size;
		grid-template-columns: repeat(12, minmax(0, 1fr));
	}
	.grid[data-fold] > * { grid-column: span 12; }

	/* xs (16rem per column) */
	@container (inline-size >= 32rem) {
		.grid[data-fold]:is(:not([data-min]), [data-min="xs"]):is([data-columns="2"], [data-columns="4"]) > * { grid-column: span 6; }
	}
	@container (inline-size >= 48rem) {
		.grid[data-fold]:is(:not([data-min]), [data-min="xs"])[data-columns="6"] > * { grid-column: span 4; }
	}
	@container (inline-size >= 64rem) {
		.grid[data-fold]:is(:not([data-min]), [data-min="xs"])[data-columns="4"] > * { grid-column: span 3; }
	}
	@container (inline-size >= 96rem) {
		.grid[data-fold]:is(:not([data-min]), [data-min="xs"])[data-columns="6"] > * { grid-column: span 2; }
	}

	/* sm (24rem per column) */
	@container (inline-size >= 48rem) {
		.grid[data-fold][data-min="sm"]:is([data-columns="2"], [data-columns="4"]) > * { grid-column: span 6; }
	}
	@container (inline-size >= 72rem) {
		.grid[data-fold][data-min="sm"][data-columns="6"] > * { grid-column: span 4; }
	}
	@container (inline-size >= 96rem) {
		.grid[data-fold][data-min="sm"][data-columns="4"] > * { grid-column: span 3; }
	}
	@container (inline-size >= 144rem) {
		.grid[data-fold][data-min="sm"][data-columns="6"] > * { grid-column: span 2; }
	}

	/* md (32rem per column) */
	@container (inline-size >= 64rem) {
		.grid[data-fold][data-min="md"]:is([data-columns="2"], [data-columns="4"]) > * { grid-column: span 6; }
	}
	@container (inline-size >= 96rem) {
		.grid[data-fold][data-min="md"][data-columns="6"] > * { grid-column: span 4; }
	}
	@container (inline-size >= 128rem) {
		.grid[data-fold][data-min="md"][data-columns="4"] > * { grid-column: span 3; }
	}
	@container (inline-size >= 192rem) {
		.grid[data-fold][data-min="md"][data-columns="6"] > * { grid-column: span 2; }
	}

	/* lg (48rem per column) */
	@container (inline-size >= 96rem) {
		.grid[data-fold][data-min="lg"]:is([data-columns="2"], [data-columns="4"]) > * { grid-column: span 6; }
	}
	@container (inline-size >= 144rem) {
		.grid[data-fold][data-min="lg"][data-columns="6"] > * { grid-column: span 4; }
	}
	@container (inline-size >= 192rem) {
		.grid[data-fold][data-min="lg"][data-columns="4"] > * { grid-column: span 3; }
	}
	@container (inline-size >= 288rem) {
		.grid[data-fold][data-min="lg"][data-columns="6"] > * { grid-column: span 2; }
	}

	/* xl (64rem per column) */
	@container (inline-size >= 128rem) {
		.grid[data-fold][data-min="xl"]:is([data-columns="2"], [data-columns="4"]) > * { grid-column: span 6; }
	}
	@container (inline-size >= 192rem) {
		.grid[data-fold][data-min="xl"][data-columns="6"] > * { grid-column: span 4; }
	}
	@container (inline-size >= 256rem) {
		.grid[data-fold][data-min="xl"][data-columns="4"] > * { grid-column: span 3; }
	}
	@container (inline-size >= 384rem) {
		.grid[data-fold][data-min="xl"][data-columns="6"] > * { grid-column: span 2; }
	}

	/* 2xl (80rem per column) */
	@container (inline-size >= 160rem) {
		.grid[data-fold][data-min="2xl"]:is([data-columns="2"], [data-columns="4"]) > * { grid-column: span 6; }
	}
	@container (inline-size >= 240rem) {
		.grid[data-fold][data-min="2xl"][data-columns="6"] > * { grid-column: span 4; }
	}
	@container (inline-size >= 320rem) {
		.grid[data-fold][data-min="2xl"][data-columns="4"] > * { grid-column: span 3; }
	}
	@container (inline-size >= 480rem) {
		.grid[data-fold][data-min="2xl"][data-columns="6"] > * { grid-column: span 2; }
	}

	/* data-ranks: every child is a subgrid of this grid's rows, one row per
	   part, so the parts of neighbours line up across the row. */
	.grid[data-ranks] > * {
		display: grid;
		grid-template-rows: subgrid;
		grid-row: span var(--_yeti-ranks);
		row-gap: var(--_yeti-gap);
	}

	.grid > * { margin: 0; }
}

/* src/layouts/frame/frame.css */
/* Frame: a fixed aspect-ratio crop. Media fills and is cropped; anything
   else is centered. */
@layer yeti.layouts {
	.frame {
		display: flex;
		align-items: center;
		justify-content: center;
		aspect-ratio: var(--_yeti-aspect);
		overflow: hidden;
	}
	.frame:not([data-ratio]) { --_yeti-aspect: 16 / 9; }
	/* picture counts as media, as it does in media, hero and card, so the
	   recipe built from a frame renders the same as the one-class form. */
	.frame > :is(img, video, picture),
	.frame > picture > img {
		inline-size: 100%;
		block-size: 100%;
		object-fit: cover;
	}
}

/* src/layouts/scroller/scroller.css */
/* Scroller: a horizontal track. Children keep their width and the track
   scrolls; snap and a fixed child width are opt-in. */
@layer yeti.layouts {
	.scroller {
		display: flex;
		gap: var(--_yeti-gap);
		overflow-x: auto;
		overscroll-behavior-x: contain;
	}
	.scroller:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.scroller > * {
		margin: 0;
		flex: 0 0 auto;
	}
	.scroller[data-width] > * { inline-size: var(--_yeti-width); }
	.scroller[data-snap] { scroll-snap-type: x mandatory; }
	.scroller[data-snap] > * { scroll-snap-align: start; }
}

/* src/layouts/overlay/overlay.css */
/* Overlay: one child held over the rest. The parent is the box being
   covered; the child carrying data-over leaves the flow and is centered on
   it, or on the viewport with data-fixed. The other children flow as usual. */
@layer yeti.layouts {
	.overlay { position: relative; }
	.overlay:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.overlay > [data-over] {
		position: absolute;
		top: 50%;
		left: 50%;
		translate: -50% -50%;
		margin: 0;
		max-inline-size: calc(100% - 2 * var(--_yeti-gap));
		max-block-size: calc(100% - 2 * var(--_yeti-gap));
		overflow: auto;
	}
	.overlay[data-fixed] > [data-over] { position: fixed; }
}

/* src/layouts/box/box.css */
/* Box: padding and an optional border. The one layout that is only about
   the space inside its own edges. */
@layer yeti.layouts {
	.box { padding: var(--_yeti-gap); }
	.box:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.box[data-border] { border: var(--yeti-border-width) solid var(--yeti-color-border); }
}

/* src/layouts/center/center.css */
/* Center: a horizontally centered column with gutters that never lets its
   content touch the edges. content-box so the maximum is the content width. */
@layer yeti.layouts {
	.center {
		box-sizing: content-box;
		max-inline-size: var(--_yeti-max);
		margin-inline: auto;
		padding-inline: var(--_yeti-gap);
	}
	.center:not([data-max]) { --_yeti-max: var(--yeti-width-xl); }
	.center:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	/* data-intrinsic: the column shrinks to its content and is centered as a
	   whole; each child is centered on its own width inside it. */
	.center[data-intrinsic] {
		display: flex;
		flex-direction: column;
		align-items: center;
		inline-size: fit-content;
	}
}

/* src/layouts/icon/icon.css */
/* Icon: an SVG sized to the text beside it and aligned with it. */
@layer yeti.layouts {
	.icon {
		display: inline-flex;
		align-items: var(--_yeti-align);
		gap: var(--_yeti-gap);
	}
	.icon:not([data-gap]) { --_yeti-gap: var(--yeti-space-xs); }
	.icon:not([data-align]) { --_yeti-align: center; }
	.icon > * { margin: 0; }
	.icon > svg {
		inline-size: 1em;
		block-size: 1em;
		flex: none;
	}
	/* On the baseline an icon's bottom edge sits where letters' do, which
	   reads too high; a small nudge settles it. */
	.icon[data-align="baseline"] > svg { translate: 0 0.125em; }
}

/* src/layouts/masonry/masonry.css */
/* Masonry: uneven items packed into columns. Multi-column layout is the
   working fallback everywhere; native masonry takes over where it exists.
   In the fallback items read down each column, then across. */
@layer yeti.layouts {
	.masonry {
		column-width: var(--_yeti-min);
		column-count: var(--_yeti-column-count);
		column-gap: var(--_yeti-gap);
	}
	.masonry:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.masonry:not([data-min]) { --_yeti-min: var(--yeti-width-xs); }
	.masonry:not([data-columns]) {
		--_yeti-column-count: auto;
		--_yeti-column-cap: 0px;
	}
	/* data-min="none" describes an exact count and is meaningless without one:
	   with no floor at all, both the column and the grid path repeat until the
	   gaps overflow. Alone it gives a single full-width column. */
	.masonry[data-min="none"]:not([data-columns]) {
		--_yeti-column-count: 1;
		--_yeti-column-cap: 100%;
	}
	/* The layout sets the space under each item; the item never does. */
	.masonry > * {
		margin: 0 0 var(--_yeti-gap);
		break-inside: avoid;
	}

	@supports (grid-template-rows: masonry) {
		.masonry {
			display: grid;
			grid-template-columns: repeat(auto-fill, minmax(min(max(var(--_yeti-min), var(--_yeti-column-cap)), 100%), 1fr));
			grid-template-rows: masonry;
			gap: var(--_yeti-gap);
			column-width: auto;
			column-count: auto;
		}
		.masonry > * { margin: 0; break-inside: auto; }
	}
}

/* src/layouts/breakout/breakout.css */
/* Breakout: a reading column with gutters, in which a child carrying
   data-bleed spans the full width. Three tracks: gutter, column, gutter;
   the column is the maximum or the space minus two gutters, whichever
   is less. */
@layer yeti.layouts {
	.breakout {
		display: grid;
		grid-template-columns:
			[full-start] minmax(var(--_yeti-gap), 1fr)
			[content-start] min(100% - 2 * var(--_yeti-gap), var(--_yeti-max))
			[content-end] minmax(var(--_yeti-gap), 1fr)
			[full-end];
		row-gap: var(--_yeti-gap);
	}
	/* Only a breakout with a note runs a query, so only that one is a size
	   container; the containment would otherwise cost the layout its intrinsic
	   width wherever a parent let it size itself. */
	.breakout:has(> [data-note]) { container-type: inline-size; }
	.breakout:not([data-max]) { --_yeti-max: var(--yeti-width-md); }
	.breakout:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.breakout > * {
		margin: 0;
		grid-column: content;
	}
	.breakout > [data-bleed] { grid-column: full; }

	/* data-note: an aside in the end gutter beside the child it follows,
	   when the content box is wide enough for a margin (xl, 64rem, so the
	   gutter beside an md column is at least 16rem); in the column otherwise. */
	.breakout > [data-note] {
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
	}
	@container (inline-size >= 64rem) {
		.breakout > [data-note] {
			grid-column: 3;
			align-self: start;
			padding-inline-start: var(--_yeti-gap);
		}
	}
}

/* src/layouts/layer/layer.css */
/* Layer: children stacked in one box, in source order, later ones on top.
   Every child is in flow, so the box is as tall as the tallest of them. */
@layer yeti.layouts {
	.layer {
		display: grid;
		grid-template-areas: "layer";
		align-items: var(--_yeti-align);
	}
	.layer:not([data-align]) { --_yeti-align: stretch; }
	/* Grid has no baseline to share between items of different natural
	   heights stacked in one area; treat data-align="baseline" as start. */
	.layer[data-align="baseline"] { --_yeti-align: start; }
	.layer > * {
		margin: 0;
		grid-area: layer;
		align-self: var(--_yeti-align-self);
		justify-self: var(--_yeti-justify-self);
	}
	/* A child places itself with data-align-self and data-justify-self;
	   without them it follows the layer's data-align. */
	.layer > :not([data-align-self]) { --_yeti-align-self: auto; }
	.layer > :not([data-justify-self]) { --_yeti-justify-self: auto; }
}

/* src/layouts/container/container.css */
/* Container: the box a container query measures. It arranges nothing; it
   exists so that whatever is inside can respond to its width rather than
   the viewport's. Name it in your own CSS when a query must target it. */
@layer yeti.layouts {
	.container { container-type: inline-size; }
}

/* src/layouts/timeline/timeline.css */
/* Timeline: entries along a rail, each with a marker. Single-sided by
   default; data-alternate puts entries on alternate sides of a centred
   rail once the list's content box is lg (48rem) wide. The list is its own
   container, so the rail is a pseudo-element and the sides are the children. */
@layer yeti.layouts {
	.timeline {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: var(--_yeti-gap);
		list-style: none;
		padding-inline-start: 0;
		container-type: inline-size;
	}
	.timeline:not([data-gap]) { --_yeti-gap: var(--yeti-space-lg); }
	.timeline > * {
		position: relative;
		margin: 0;
		padding-inline-start: var(--yeti-space-lg);
	}

	/* The rail, down the start edge, and a marker per entry sitting on it. */
	.timeline::before {
		content: "";
		position: absolute;
		inset-block: 0;
		inset-inline-start: calc(0.5rem - 1px);
		inline-size: 2px;
		background-color: var(--yeti-color-border-strong);
	}
	.timeline > li::before {
		content: "";
		position: absolute;
		inset-block-start: 0.35em;
		inset-inline-start: 0;
		inline-size: 1rem;
		block-size: 1rem;
		border-radius: 50%;
		background-color: var(--yeti-color-border-strong);
	}

	/* Alternate: a centred rail, entries on either side of it. */
	@container (inline-size >= 48rem) {
		.timeline[data-alternate]::before { inset-inline-start: calc(50% - 1px); }
		.timeline[data-alternate] > li {
			inline-size: calc(50% - var(--yeti-space-lg));
			padding-inline-start: 0;
		}
		.timeline[data-alternate] > li:nth-child(odd) {
			align-self: flex-start;
			padding-inline-end: var(--yeti-space-lg);
		}
		.timeline[data-alternate] > li:nth-child(even) {
			align-self: flex-end;
			padding-inline-start: var(--yeti-space-lg);
		}
		.timeline[data-alternate] > li:nth-child(odd)::before {
			inset-inline-start: auto;
			inset-inline-end: calc(-1 * var(--yeti-space-lg) - 0.5rem);
		}
		.timeline[data-alternate] > li:nth-child(even)::before {
			inset-inline-start: calc(-1 * var(--yeti-space-lg) - 0.5rem);
		}
	}
}

/* src/recipes/media/media.css */
/* Media: a figure beside text. The one-class form of a sidebar holding a
   frame and a stack. The figure is whichever child is, or wraps, the
   picture, so source order decides the side; data-side overrides it. */
@layer yeti.layouts {
	.media {
		display: flex;
		flex-wrap: wrap;
		align-items: var(--_yeti-align);
		gap: var(--_yeti-gap);
	}
	.media:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.media:not([data-width]) { --_yeti-width: var(--yeti-width-xs); }
	.media:not([data-align]) { --_yeti-align: stretch; }
	.media:not([data-ratio]) { --_yeti-aspect: 1 / 1; }
	.media > * { margin: 0; }

	/* The figure */
	.media > :is(img, video, picture),
	.media > :has(> :is(img, video, picture)) {
		flex-basis: var(--_yeti-width);
		flex-grow: 1;
		aspect-ratio: var(--_yeti-aspect);
		overflow: hidden;
	}
	.media > :is(img, video) { object-fit: cover; }
	/* :is(..., picture) here too: the reset gives picture display: block, so a
	   picture between the figure and its img (the wrapped-figure case) needs
	   its own 100% size, or the img's percentage height has nothing to resolve
	   against and falls back to the picture's own auto, content-based height. */
	.media > :is(picture, :has(> :is(img, video, picture))) :is(img, video, picture) {
		inline-size: 100%;
		block-size: 100%;
		object-fit: cover;
	}

	/* A figure with a caption: only the media is cropped; the caption sits below. */
	.media > figure:has(> figcaption) {
		display: flex;
		flex-direction: column;
		aspect-ratio: auto;
		overflow: visible;
	}
	.media > figure:has(> figcaption) > :is(img, video, picture) {
		aspect-ratio: var(--_yeti-aspect);
		block-size: auto;
	}
	.media > figure:has(> figcaption) > figcaption {
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
	}

	/* The body: everything that is not the figure */
	.media > :not(:is(img, video, picture), :has(> :is(img, video, picture))) {
		display: flex;
		flex-direction: column;
		gap: var(--yeti-space-sm);
		flex-basis: 0;
		flex-grow: 999;
		min-inline-size: 50%;
	}
	.media > :not(:is(img, video, picture), :has(> :is(img, video, picture))) > * { margin: 0; }

	/* data-side: move the figure without moving it in the source */
	.media[data-side="start"] > :is(img, video, picture),
	.media[data-side="start"] > :has(> :is(img, video, picture)) { order: -1; }
	.media[data-side="end"] > :is(img, video, picture),
	.media[data-side="end"] > :has(> :is(img, video, picture)) { order: 1; }
}

/* src/recipes/hero/hero.css */
/* Hero: a viewport-tall band with copy on one side and a figure on the
   other, both centered, becoming two rows below a container width. One
   element doing the work of a cover holding columns. */
@layer yeti.layouts {
	.hero {
		display: flex;
		flex-wrap: wrap;
		align-content: center;
		align-items: var(--_yeti-align);
		min-block-size: var(--yeti-cover-height);
		gap: var(--_yeti-gap);
	}
	.hero:not([data-gap]) { --_yeti-gap: var(--yeti-space-lg); }
	.hero:not([data-threshold]) { --_yeti-threshold: var(--yeti-width-lg); }
	.hero:not([data-align]) { --_yeti-align: center; }
	.hero:not([data-ratio]) { --_yeti-aspect: 4 / 3; }
	.hero > * {
		margin: 0;
		flex-grow: 1;
		flex-basis: calc((var(--_yeti-threshold) - 100%) * 999);
	}

	/* The figure */
	.hero > :is(img, video, picture),
	.hero > :has(> :is(img, video, picture)) {
		aspect-ratio: var(--_yeti-aspect);
		overflow: hidden;
	}
	.hero > :is(img, video) { object-fit: cover; }
	/* :is(..., picture) here too: the reset gives picture display: block, so a
	   picture between the figure and its img (the wrapped-figure case) needs
	   its own 100% size, or the img's percentage height has nothing to resolve
	   against and falls back to the picture's own auto, content-based height. */
	.hero > :is(picture, :has(> :is(img, video, picture))) :is(img, video, picture) {
		inline-size: 100%;
		block-size: 100%;
		object-fit: cover;
	}

	/* A figure with a caption: only the media is cropped; the caption sits below. */
	.hero > figure:has(> figcaption) {
		display: flex;
		flex-direction: column;
		aspect-ratio: auto;
		overflow: visible;
	}
	.hero > figure:has(> figcaption) > :is(img, video, picture) {
		aspect-ratio: var(--_yeti-aspect);
		block-size: auto;
	}
	.hero > figure:has(> figcaption) > figcaption {
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
	}

	/* The copy */
	.hero > :not(:is(img, video, picture), :has(> :is(img, video, picture))) {
		display: flex;
		flex-direction: column;
		gap: var(--yeti-space-sm);
	}
	.hero > :not(:is(img, video, picture), :has(> :is(img, video, picture))) > * { margin: 0; }

	/* data-side */
	.hero[data-side="start"] > :is(img, video, picture),
	.hero[data-side="start"] > :has(> :is(img, video, picture)) { order: -1; }
	.hero[data-side="end"] > :is(img, video, picture),
	.hero[data-side="end"] > :has(> :is(img, video, picture)) { order: 1; }
}

/* src/recipes/shell/shell.css */
/* Shell: the page skeleton. A filled column of header, body, and footer,
   the body row growing so the footer sits at the bottom of a short page;
   inside the body, nav and aside sit beside main while there is room. */
@layer yeti.layouts {
	.shell {
		display: flex;
		flex-direction: column;
		min-block-size: var(--yeti-cover-height);
		gap: var(--_yeti-gap);
	}
	.shell:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.shell:not([data-width]) { --_yeti-width: var(--yeti-width-sm); }
	.shell > * { margin: 0; }
	.shell > :is(main, div:has(> :is(nav, main, aside))) { flex-grow: 1; }

	/* The body row. align-content: start keeps its regions at their natural
	   height, matching a sidebar, while the row itself takes the free space.
	   Scoped to a div that actually holds nav, main, or aside, so a plain
	   div used for other content in the shell is left alone. */
	.shell > div:has(> :is(nav, main, aside)) {
		display: flex;
		flex-wrap: wrap;
		align-content: start;
		gap: var(--_yeti-gap);
	}
	.shell > div:has(> :is(nav, main, aside)) > * { margin: 0; }
	.shell > div:has(> :is(nav, main, aside)) > :is(nav, aside) {
		flex-basis: var(--_yeti-width);
		flex-grow: 1;
	}
	.shell > div:has(> :is(nav, main, aside)) > main {
		flex-basis: 0;
		flex-grow: 999;
		min-inline-size: 50%;
	}
}

/* src/components/button/button.css */
/* Button: an action with a face. data-variant picks the hue's ladder,
   data-emphasis how loud, data-size scales padding and text together.
   State is native: hover, active, focus-visible, disabled, aria-pressed. */
@layer yeti.components {
	.button {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: var(--yeti-space-xs);
		min-block-size: var(--yeti-control-size);
		padding: 0 calc(var(--_yeti-size-space) * var(--yeti-button-padding));
		font: inherit;
		font-size: var(--_yeti-size-text);
		font-weight: var(--yeti-button-weight);
		line-height: var(--yeti-leading-tight);
		text-decoration: none;
		border: var(--yeti-border-width) solid var(--_yeti-variant);
		border-radius: var(--yeti-button-radius);
		background-color: var(--_yeti-variant);
		color: var(--_yeti-on-variant);
		cursor: pointer;
		user-select: none;
		transition:
			background-color var(--yeti-duration-fast) var(--yeti-ease),
			border-color var(--yeti-duration-fast) var(--yeti-ease),
			color var(--yeti-duration-fast) var(--yeti-ease);
	}
	.button:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.button:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }
	.button > svg {
		inline-size: 1em;
		block-size: 1em;
		flex: none;
	}

	/* High emphasis (the default): a solid fill that steps up on hover. */
	.button:is(:hover, :active) {
		background-color: var(--_yeti-variant-strong);
		border-color: var(--_yeti-variant-strong);
		color: var(--_yeti-on-variant);
	}

	/* Medium: an outline. Low: text alone. Both tint on hover. */
	.button[data-emphasis="medium"] {
		background-color: transparent;
		color: var(--_yeti-variant-text);
	}
	.button[data-emphasis="low"] {
		background-color: transparent;
		border-color: transparent;
		color: var(--_yeti-variant-text);
	}
	.button[data-emphasis="medium"]:is(:hover, :active) {
		background-color: var(--_yeti-variant-subtle);
		border-color: var(--_yeti-variant);
		color: var(--_yeti-variant-text);
	}
	.button[data-emphasis="low"]:is(:hover, :active) {
		background-color: var(--_yeti-variant-subtle);
		border-color: transparent;
		color: var(--_yeti-variant-text);
	}

	/* A pressed medium or low toggle keeps its pressed fill on hover, instead
	   of stepping down to the emphasis hover rules above. */
	.button:is([data-emphasis="medium"], [data-emphasis="low"])[aria-pressed="true"]:is(:hover, :active) {
		background-color: var(--_yeti-variant-strong);
		border-color: var(--_yeti-variant-strong);
		color: var(--_yeti-on-variant);
	}

	/* Pressed toggles, busy, and disabled, from the attributes that mean them. */
	.button[aria-pressed="true"] {
		background-color: var(--_yeti-variant-strong);
		border-color: var(--_yeti-variant-strong);
		color: var(--_yeti-on-variant);
	}
	.button:is(:disabled, [aria-disabled="true"]) {
		opacity: var(--yeti-opacity-muted);
		cursor: not-allowed;
	}
	.button:is(:disabled, [aria-disabled="true"]):is(:hover, :active) {
		background-color: var(--_yeti-variant);
		border-color: var(--_yeti-variant);
		color: var(--_yeti-on-variant);
	}
	.button[data-emphasis="medium"]:is(:disabled, [aria-disabled="true"]):is(:hover, :active) {
		background-color: transparent;
		border-color: var(--_yeti-variant);
		color: var(--_yeti-variant-text);
	}
	.button[data-emphasis="low"]:is(:disabled, [aria-disabled="true"]):is(:hover, :active) {
		background-color: transparent;
		border-color: transparent;
		color: var(--_yeti-variant-text);
	}
	/* Busy is checked after disabled so a busy button (which also carries
	   aria-disabled, since it cannot be pressed again mid-request) keeps its
	   progress cursor instead of the disabled rule's not-allowed. */
	.button[aria-busy="true"] {
		opacity: var(--yeti-opacity-muted);
		cursor: progress;
	}
}

/* src/components/buttons/buttons.css */
/* Buttons: a group. A wrapping row at a small gap, or with data-attach one
   fused control whose members share borders. */
@layer yeti.components {
	.buttons {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--_yeti-gap);
	}
	.buttons:not([data-gap]) { --_yeti-gap: var(--yeti-space-sm); }
	.buttons > * { margin: 0; }

	.buttons[data-attach] { gap: 0; }
	.buttons[data-attach] > .button:not(:first-child) {
		border-start-start-radius: 0;
		border-end-start-radius: 0;
		margin-inline-start: calc(-1 * var(--yeti-border-width));
	}
	.buttons[data-attach] > .button:not(:last-child) {
		border-start-end-radius: 0;
		border-end-end-radius: 0;
	}
	/* A focused member rises above its neighbours so its ring is whole. */
	.buttons[data-attach] > .button:focus-visible {
		position: relative;
		z-index: 1;
	}
}

/* src/components/badge/badge.css */
/* Badge: a small inline label for a status or a count. Quiet by default:
   a subtle tint with the hue's text colour. */
@layer yeti.components {
	.badge {
		display: inline-flex;
		align-items: center;
		gap: var(--yeti-space-xs);
		padding: 0.15em 0.6em;
		font-size: calc(var(--_yeti-size-text) * 0.8);
		font-weight: var(--yeti-badge-weight);
		line-height: var(--yeti-leading-tight);
		white-space: nowrap;
		border: var(--yeti-border-width) solid transparent;
		border-radius: var(--yeti-badge-radius);
		background-color: var(--_yeti-variant-subtle);
		color: var(--_yeti-variant-text);
	}
	.badge:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.badge:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }
	.badge[data-emphasis="high"] {
		background-color: var(--_yeti-variant);
		color: var(--_yeti-on-variant);
	}
	.badge[data-emphasis="low"] {
		background-color: transparent;
		padding-inline: 0;
	}
	.badge > svg {
		inline-size: 1em;
		block-size: 1em;
		flex: none;
	}
}

/* src/components/card/card.css */
/* Card: a bordered surface holding a figure, a body, and a footer. From
   the md width (32rem) of content, a card with a figure puts the picture
   beside the text; narrower than that it stacks, picture on top. Literal
   because container conditions cannot read custom properties. */
@layer yeti.components {
	.card {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: var(--yeti-card-gap);
		padding: var(--yeti-card-padding);
		background-color: var(--yeti-card-surface);
		color: var(--yeti-color-text);
		border: var(--yeti-border-width) solid var(--yeti-card-border);
		border-radius: var(--yeti-card-radius);
		overflow: clip;
	}
	/* Only a card with a figure runs the side-by-side query, so only that
	   card is a size container. Inline-size containment removes the content's
	   contribution to intrinsic width, and a card that is always a container
	   collapsed to its padding wherever a parent let it size itself: 37px in
	   a cluster, the same at max-content. */
	.card:has(> :is(img, video, picture, figure):first-child) { container-type: inline-size; }
	.card:not([data-ratio]) { --_yeti-aspect: 16 / 9; }
	.card > * { margin-block: 0; }

	/* The figure: media, or a figure element, first in the card, bleeding to the edges.
	   inline-size is given explicitly, rather than left to the flex item's
	   default cross-axis stretch, because a stretch-computed width paired
	   with aspect-ratio on a replaced element mis-sizes the height in WebKit. */
	.card > :is(img, video, picture, figure):first-child {
		inline-size: calc(100% + 2 * var(--yeti-card-padding));
		margin: calc(-1 * var(--yeti-card-padding)) calc(-1 * var(--yeti-card-padding)) 0;
		max-inline-size: none;
		aspect-ratio: var(--_yeti-aspect);
		object-fit: cover;
	}
	/* A plain figure is a column: the picture, then the caption below. A
	   figure that is a layer keeps its own stacking (see the layer's page). */
	.card > figure:first-child {
		aspect-ratio: auto;
	}
	.card > figure:first-child:not(.layer) {
		display: flex;
		flex-direction: column;
	}
	.card > figure:first-child > :is(img, video, picture) {
		inline-size: 100%;
		max-inline-size: none;
		aspect-ratio: var(--_yeti-aspect);
		object-fit: cover;
	}
	.card > figure:first-child:not(.layer) > figcaption {
		padding: var(--yeti-space-xs) var(--yeti-card-padding) 0;
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
	}
	/* Over the picture, the caption needs a scrim to stay readable. */
	.card > figure.layer:first-child > figcaption {
		padding: var(--yeti-space-xs) var(--yeti-card-padding);
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text);
		background-color: color-mix(in oklch, var(--yeti-color-surface) 85%, transparent);
	}

	/* The footer sits at the bottom of a taller card. */
	.card > footer {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--yeti-space-sm);
		margin-block-start: auto;
	}
	/* footer IS a flow container, and a block-start margin on a centred flex item shifts it down half its own margin. */
	.card > footer > * { margin-block: 0; }

	/* One link may stretch over the whole card. Everything else that can be
	   pressed is lifted above its pseudo-element, or a footer button would sit
	   under the stretched link and the page would navigate instead. */
	.card [data-stretch]::after {
		content: "";
		position: absolute;
		inset: 0;
	}
	.card :is(a, button, input, select, textarea, summary, [tabindex]):not([data-stretch]) {
		position: relative;
		z-index: 1;
	}

	.card[data-raised] {
		border-color: transparent;
		box-shadow: var(--yeti-shadow-sm);
	}
	.card[data-variant] {
		border-color: var(--_yeti-variant);
		border-block-start-width: calc(var(--yeti-border-width) * 4);
	}

	/* In a ranked grid the card is a subgrid of the grid's rows, so its parts
	   line up with its neighbours'; the footer sits in the last row instead of
	   being pushed. The side-by-side form below is switched off there. The footer
	   is placed in the last row explicitly so a card with fewer parts still
	   aligns its footer. container-type is turned off here: a subgridded axis
	   shares track sizing with its ancestor, which containment forbids, so a
	   query container cannot also be a subgrid; the side-by-side query never
	   matches a ranked card anyway. */
	.grid[data-ranks] > .card {
		display: grid;
		grid-template-rows: subgrid;
		grid-row: span var(--_yeti-ranks);
		row-gap: var(--yeti-card-gap);
		container-type: normal;
	}
	.grid[data-ranks] > .card > footer { margin-block-start: 0; grid-row: -2; }

	/* Wide: the picture beside the text, a media object. Only cards with a
	   figure change, and only once the card has room for both: stacked, the
	   picture is the hero and the text runs full width, which is the form a
	   phone and a grid cell want; side by side, a narrow picture would be a
	   crushed thumbnail and the text a sliver, so this form waits for md.
	   (The first design did the reverse, a thumbnail row below sm; Joe, on
	   2026-09-16, found it backwards, and the crushed thumbnail proved him
	   right.) A container query can only restyle descendants of its
	   container, never the container itself, so the card's own display and
	   flow stay flex column throughout; the figure is pulled out of flow
	   with position and the text is indented to clear it. The picture sits
	   inside the border, like the stacked figure: inset to the padding box,
	   not the border box, so the border (and a data-variant bar) stays
	   visible above and beside it. Its width is 40% of the padding box; the
	   sibling's margin-inline-start percentage resolves against the
	   narrower content box, so the indent is rewritten in content-box
	   terms (40% of padding box = 40% of content box + 0.8 × padding; the
	   content box starts one padding in from the padding box, so the
	   picture's right edge sits 40% - 0.2 × padding from the content
	   box's own start) plus the card's gap. */
	/* data-threshold moves the switch. A container condition cannot read a
	   token, so there is one block per stop rather than one rule reading a
	   property; each names the width token it mirrors, and md, the default,
	   also serves a card with no attribute. The three rules are repeated
	   rather than toggled, because nothing in CSS lets a query set a flag the
	   container's own children can branch on. */
	/* --yeti-width-xs (16rem) */
	@container (inline-size >= 16rem) {
		.card[data-threshold="xs"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child {
			position: absolute;
			inset-block-start: 0;
			inset-inline-start: 0;
			inline-size: 40%;
			block-size: 100%;
			margin: 0;
			aspect-ratio: auto;
		}
		.card[data-threshold="xs"]:not(.grid[data-ranks] > .card) > figure:first-child > :is(img, video, picture) {
			flex: 1;
			min-block-size: 0;
			block-size: 100%;
			aspect-ratio: auto;
		}
		.card[data-threshold="xs"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child ~ * {
			margin-inline-start: calc(40% - 0.2 * var(--yeti-card-padding) + var(--yeti-card-gap));
		}
	}
	/* --yeti-width-sm (24rem) */
	@container (inline-size >= 24rem) {
		.card[data-threshold="sm"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child {
			position: absolute;
			inset-block-start: 0;
			inset-inline-start: 0;
			inline-size: 40%;
			block-size: 100%;
			margin: 0;
			aspect-ratio: auto;
		}
		.card[data-threshold="sm"]:not(.grid[data-ranks] > .card) > figure:first-child > :is(img, video, picture) {
			flex: 1;
			min-block-size: 0;
			block-size: 100%;
			aspect-ratio: auto;
		}
		.card[data-threshold="sm"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child ~ * {
			margin-inline-start: calc(40% - 0.2 * var(--yeti-card-padding) + var(--yeti-card-gap));
		}
	}
	/* --yeti-width-md (32rem) */
	@container (inline-size >= 32rem) {
		.card:is([data-threshold="md"], :not([data-threshold])):not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child {
			position: absolute;
			inset-block-start: 0;
			inset-inline-start: 0;
			inline-size: 40%;
			block-size: 100%;
			margin: 0;
			aspect-ratio: auto;
		}
		.card:is([data-threshold="md"], :not([data-threshold])):not(.grid[data-ranks] > .card) > figure:first-child > :is(img, video, picture) {
			flex: 1;
			min-block-size: 0;
			block-size: 100%;
			aspect-ratio: auto;
		}
		.card:is([data-threshold="md"], :not([data-threshold])):not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child ~ * {
			margin-inline-start: calc(40% - 0.2 * var(--yeti-card-padding) + var(--yeti-card-gap));
		}
	}
	/* --yeti-width-lg (48rem) */
	@container (inline-size >= 48rem) {
		.card[data-threshold="lg"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child {
			position: absolute;
			inset-block-start: 0;
			inset-inline-start: 0;
			inline-size: 40%;
			block-size: 100%;
			margin: 0;
			aspect-ratio: auto;
		}
		.card[data-threshold="lg"]:not(.grid[data-ranks] > .card) > figure:first-child > :is(img, video, picture) {
			flex: 1;
			min-block-size: 0;
			block-size: 100%;
			aspect-ratio: auto;
		}
		.card[data-threshold="lg"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child ~ * {
			margin-inline-start: calc(40% - 0.2 * var(--yeti-card-padding) + var(--yeti-card-gap));
		}
	}
	/* --yeti-width-xl (64rem) */
	@container (inline-size >= 64rem) {
		.card[data-threshold="xl"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child {
			position: absolute;
			inset-block-start: 0;
			inset-inline-start: 0;
			inline-size: 40%;
			block-size: 100%;
			margin: 0;
			aspect-ratio: auto;
		}
		.card[data-threshold="xl"]:not(.grid[data-ranks] > .card) > figure:first-child > :is(img, video, picture) {
			flex: 1;
			min-block-size: 0;
			block-size: 100%;
			aspect-ratio: auto;
		}
		.card[data-threshold="xl"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child ~ * {
			margin-inline-start: calc(40% - 0.2 * var(--yeti-card-padding) + var(--yeti-card-gap));
		}
	}
	/* --yeti-width-2xl (80rem) */
	@container (inline-size >= 80rem) {
		.card[data-threshold="2xl"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child {
			position: absolute;
			inset-block-start: 0;
			inset-inline-start: 0;
			inline-size: 40%;
			block-size: 100%;
			margin: 0;
			aspect-ratio: auto;
		}
		.card[data-threshold="2xl"]:not(.grid[data-ranks] > .card) > figure:first-child > :is(img, video, picture) {
			flex: 1;
			min-block-size: 0;
			block-size: 100%;
			aspect-ratio: auto;
		}
		.card[data-threshold="2xl"]:not(.grid[data-ranks] > .card) > :is(img, video, picture, figure):first-child ~ * {
			margin-inline-start: calc(40% - 0.2 * var(--yeti-card-padding) + var(--yeti-card-gap));
		}
	}
}

/* src/components/field/field.css */
/* Field: a label, a control, a hint, and an error, as one thing. Controls
   stay native; validation shows from the control's own state, so an empty
   form is not red on load and no script is needed. */
@layer yeti.components {
	.field {
		display: flex;
		flex-direction: column;
		gap: var(--yeti-field-gap);
	}
	.field:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.field:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }
	.field > * { margin: 0; }
	.field > :is(label, legend) {
		font-weight: var(--yeti-weight-strong);
		font-size: var(--_yeti-size-text);
	}

	/* Text-like controls */
	.field :is(input:not(:where([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"])), select, textarea) {
		min-block-size: calc(var(--yeti-control-size) + max(0px, var(--_yeti-size-space) - var(--yeti-space-sm)));
		padding: 0 var(--_yeti-size-space);
		font: inherit;
		font-size: var(--_yeti-size-text);
		color: var(--yeti-color-text);
		background-color: var(--yeti-control-surface);
		border: var(--yeti-border-width) solid var(--yeti-control-border);
		border-radius: var(--yeti-control-radius);
		transition: border-color var(--yeti-duration-fast) var(--yeti-ease);
	}
	.field textarea {
		padding-block: var(--_yeti-size-space);
		min-block-size: 4lh;
	}
	.field select {
		appearance: none;
		padding-inline-end: calc(var(--_yeti-size-space) * 2 + 1em);
		background-image: var(--yeti-control-chevron);
		background-repeat: no-repeat;
		background-position: right var(--_yeti-size-space) center;
		background-size: 1em;
	}
	.field :is(input, select, textarea):focus-visible {
		border-color: var(--yeti-color-border-strong);
	}

	/* Checkbox and radio, redrawn so the checked colour is the variant's. */
	.field input:is([type="checkbox"], [type="radio"]) {
		appearance: none;
		inline-size: 1.25em;
		block-size: 1.25em;
		margin: 0;
		font-size: var(--_yeti-size-text);
		background-color: var(--yeti-control-surface);
		border: var(--yeti-border-width) solid var(--yeti-control-border);
		border-radius: var(--yeti-control-radius);
		flex: none;
		transition: background-color var(--yeti-duration-fast) var(--yeti-ease), box-shadow var(--yeti-duration-fast) var(--yeti-ease);
	}
	.field input[type="radio"] { border-radius: 50%; }
	.field input:is([type="checkbox"], [type="radio"]):checked {
		background-color: var(--_yeti-variant);
		border-color: var(--_yeti-variant);
		box-shadow: inset 0 0 0 0.2em var(--yeti-control-surface);
	}

	/* A switch: a checkbox with role="switch", drawn as a track with a thumb
	   that slides to the end and a track that takes the variant colour. */
	.field input[type="checkbox"][role="switch"] {
		inline-size: 2.25em;
		border-radius: var(--yeti-radius-full);
		background-color: var(--yeti-control-border);
		border-color: var(--yeti-control-border);
		background-image: radial-gradient(circle, var(--yeti-control-surface) 0 0.4em, transparent 0.45em);
		background-size: 1.25em 1.25em;
		background-repeat: no-repeat;
		background-position: 0% 50%;
		transition: background-color var(--yeti-duration-fast) var(--yeti-ease), border-color var(--yeti-duration-fast) var(--yeti-ease), background-position var(--yeti-duration-fast) var(--yeti-ease);
	}
	.field input[type="checkbox"][role="switch"]:checked {
		background-color: var(--_yeti-variant);
		border-color: var(--_yeti-variant);
		background-position: 100% 50%;
		box-shadow: none;
	}

	/* A range: a thin track and a round thumb in the variant colour. The
	   engine pseudo-elements take physical sizes; the input itself is a
	   full control's height so the thumb is a comfortable target. The
	   track is filled to --yeti-range-value, a share the author sets on
	   the input, since CSS cannot read the value. */
	.field input[type="range"] {
		appearance: none;
		inline-size: 100%;
		block-size: var(--yeti-control-size);
		margin: 0;
		font-size: var(--_yeti-size-text);
		background: transparent;
		cursor: pointer;
	}
	.field input[type="range"]::-webkit-slider-runnable-track {
		height: 0.25em;
		border-radius: var(--yeti-radius-full);
		background: linear-gradient(var(--_yeti-variant) 0 0) 0 0 / var(--yeti-range-value) 100% no-repeat var(--yeti-control-border);
	}
	.field input[type="range"]::-moz-range-track {
		height: 0.25em;
		border-radius: var(--yeti-radius-full);
		background: linear-gradient(var(--_yeti-variant) 0 0) 0 0 / var(--yeti-range-value) 100% no-repeat var(--yeti-control-border);
	}
	.field input[type="range"]::-webkit-slider-thumb {
		appearance: none;
		width: 1.25em;
		height: 1.25em;
		margin-top: calc((0.25em - 1.25em) / 2);
		border: var(--yeti-border-width) solid var(--_yeti-variant);
		border-radius: 50%;
		background-color: var(--_yeti-variant);
	}
	.field input[type="range"]::-moz-range-thumb {
		width: 1.25em;
		height: 1.25em;
		border: var(--yeti-border-width) solid var(--_yeti-variant);
		border-radius: 50%;
		background-color: var(--_yeti-variant);
	}

	/* Inline: the label beside the control, for checkboxes, radios, and data-inline. */
	.field:is([data-inline], :has(> input:is([type="checkbox"], [type="radio"]))) {
		flex-direction: row;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--yeti-space-xs);
	}
	.field:is([data-inline], :has(> input:is([type="checkbox"], [type="radio"]))) > :is([data-hint], [data-error]) {
		flex-basis: 100%;
	}

	/* Hint and error */
	.field > [data-hint] {
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
	}
	.field > [data-error] {
		display: none;
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-alert-text);
	}
	.field:has(:user-invalid, [aria-invalid="true"]) > [data-error] { display: block; }
	.field:has(> :user-invalid, > [aria-invalid="true"], > .affix :is(:user-invalid, [aria-invalid="true"])) > :is(input, select, textarea),
	.field:has(> .affix :is(:user-invalid, [aria-invalid="true"])) > .affix > :is(input, select) { border-color: var(--yeti-color-alert); }

	/* Required, marked after the label; the attribute carries the meaning. */
	.field:has([required]) > :is(label, legend)::after {
		content: " *";
		color: var(--yeti-color-alert-text);
	}

	/* A fieldset as a field: the base layer's box, with the legend as the label. */
	fieldset.field {
		padding: var(--yeti-space-sm) var(--yeti-space-md);
		border: var(--yeti-border-width) solid var(--yeti-control-border);
		border-radius: var(--yeti-control-radius);
	}
	fieldset.field > legend { padding-inline: var(--yeti-space-xs); }
}

/* src/components/affix/affix.css */
/* Affix: a control with something attached before or after it, a
   unit, a symbol, or a button, sharing one border and one height. It reads
   the size of the field around it, so it sets no defaults of its own. */
@layer yeti.components {
	.affix {
		display: flex;
		align-items: stretch;
	}
	.affix > * { margin: 0; }
	.affix > :is(input, select) {
		flex: 1;
		min-inline-size: 0;
	}
	.affix > span {
		display: flex;
		align-items: center;
		padding-inline: var(--_yeti-size-space, var(--yeti-space-sm));
		font-size: var(--_yeti-size-text, var(--yeti-text-md));
		color: var(--yeti-color-text-muted);
		background-color: var(--yeti-color-surface-sunken);
		border: var(--yeti-border-width) solid var(--yeti-control-border);
		border-radius: var(--yeti-control-radius);
		white-space: nowrap;
	}
	.affix > .button { border-radius: var(--yeti-control-radius); }
	.affix > :not(:first-child) {
		border-start-start-radius: 0;
		border-end-start-radius: 0;
		margin-inline-start: calc(-1 * var(--yeti-border-width));
	}
	.affix > :not(:last-child) {
		border-start-end-radius: 0;
		border-end-end-radius: 0;
	}
	.affix > :focus-visible {
		position: relative;
		z-index: 1;
	}
}

/* src/components/table/table.css */
/* Table: the element styled in place. It stays a table for assistive tech;
   a wide one scrolls inside a scroller. */
@layer yeti.components {
	.table {
		inline-size: 100%;
		border-collapse: collapse;
		font-size: var(--_yeti-size-text);
	}
	.table:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }
	.table > caption {
		padding-block: var(--_yeti-size-space);
		text-align: start;
		font-weight: var(--yeti-weight-strong);
	}
	.table :is(th, td) {
		padding: var(--_yeti-size-space) var(--yeti-space-sm);
		text-align: start;
		vertical-align: top;
		border-block-end: var(--yeti-border-width) solid var(--yeti-table-border);
	}
	.table > thead th {
		font-weight: var(--yeti-weight-strong);
		border-block-end-color: var(--yeti-color-border-strong);
	}
	.table [data-numeric] {
		text-align: end;
		font-variant-numeric: tabular-nums;
	}
	.table[data-striped] > tbody > tr:nth-child(even) { background-color: var(--yeti-table-stripe); }
	.table[data-hover] > tbody > tr:hover { background-color: var(--yeti-color-surface-sunken); }
	.table[data-grid] :is(th, td) { border: var(--yeti-border-width) solid var(--yeti-table-border); }
	.table[data-grid] > thead th { border-block-end-color: var(--yeti-color-border-strong); }
}

/* src/components/seam/seam.css */
/* Seam: a shaped edge on a section, cut from the section's own box, so
   whatever is behind it shows through. Slants are a clip-path polygon;
   curves and waves are a mask made of a solid body and a shaped strip. */
@layer yeti.components {
	.seam {
		--_yeti-seam: var(--yeti-seam-size, calc(var(--_yeti-size-space) * 2));
		--_yeti-seam-curve-bottom: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath d='M0 0H100C75 13.3 25 13.3 0 0Z'/%3E%3C/svg%3E");
		--_yeti-seam-curve-top: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath d='M0 10C25 -3.3 75 -3.3 100 10Z'/%3E%3C/svg%3E");
		--_yeti-seam-wave-bottom: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath d='M0 0H100V5C87.5 10 62.5 0 50 5C37.5 10 12.5 0 0 5Z'/%3E%3C/svg%3E");
		--_yeti-seam-wave-top: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath d='M0 5C12.5 0 37.5 10 50 5C62.5 0 87.5 10 100 5V10H0Z'/%3E%3C/svg%3E");
		--_yeti-seam-wave-bottom-flip: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath d='M0 0H100V5C87.5 0 62.5 10 50 5C37.5 0 12.5 10 0 5Z'/%3E%3C/svg%3E");
		--_yeti-seam-wave-top-flip: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath d='M0 5C12.5 10 37.5 0 50 5C62.5 10 87.5 0 100 5V10H0Z'/%3E%3C/svg%3E");
	}
	.seam:not([data-size]) { --_yeti-size-space: var(--yeti-space-sm); }

	/* The depth is added in flow, after or before the content, so the
	   section's own padding is untouched and text never sits in the cut. */
	.seam:not([data-edge="top"])::after,
	.seam:is([data-edge="top"], [data-edge="both"])::before {
		content: "";
		display: block;
		block-size: var(--_yeti-seam);
	}

	/* Slant */
	.seam:not([data-shape="curve"], [data-shape="wave"]):not([data-edge="top"], [data-edge="both"]) { clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--_yeti-seam)), 0 100%); }
	.seam:not([data-shape="curve"], [data-shape="wave"]):not([data-edge="top"], [data-edge="both"])[data-flip] { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - var(--_yeti-seam))); }
	.seam:not([data-shape="curve"], [data-shape="wave"])[data-edge="top"] { clip-path: polygon(0 var(--_yeti-seam), 100% 0, 100% 100%, 0 100%); }
	.seam:not([data-shape="curve"], [data-shape="wave"])[data-edge="top"][data-flip] { clip-path: polygon(0 0, 100% var(--_yeti-seam), 100% 100%, 0 100%); }
	.seam:not([data-shape="curve"], [data-shape="wave"])[data-edge="both"] { clip-path: polygon(0 var(--_yeti-seam), 100% 0, 100% calc(100% - var(--_yeti-seam)), 0 100%); }
	.seam:not([data-shape="curve"], [data-shape="wave"])[data-edge="both"][data-flip] { clip-path: polygon(0 0, 100% var(--_yeti-seam), 100% 100%, 0 calc(100% - var(--_yeti-seam))); }

	/* Curve and wave: a mask of a solid body and a shaped strip per cut edge. */
	.seam:is([data-shape="curve"], [data-shape="wave"]) {
		mask-repeat: no-repeat;
		--_yeti-seam-top: var(--_yeti-seam-curve-top);
		--_yeti-seam-bottom: var(--_yeti-seam-curve-bottom);
	}
	.seam[data-shape="wave"] {
		--_yeti-seam-top: var(--_yeti-seam-wave-top);
		--_yeti-seam-bottom: var(--_yeti-seam-wave-bottom);
	}
	.seam[data-shape="wave"][data-flip] {
		--_yeti-seam-top: var(--_yeti-seam-wave-top-flip);
		--_yeti-seam-bottom: var(--_yeti-seam-wave-bottom-flip);
	}
	.seam:is([data-shape="curve"], [data-shape="wave"]):not([data-edge="top"], [data-edge="both"]) {
		mask-image: linear-gradient(black, black), var(--_yeti-seam-bottom);
		mask-size: 100% calc(100% - var(--_yeti-seam)), 100% var(--_yeti-seam);
		mask-position: top, bottom;
	}
	.seam:is([data-shape="curve"], [data-shape="wave"])[data-edge="top"] {
		mask-image: var(--_yeti-seam-top), linear-gradient(black, black);
		mask-size: 100% var(--_yeti-seam), 100% calc(100% - var(--_yeti-seam));
		mask-position: top, bottom;
	}
	.seam:is([data-shape="curve"], [data-shape="wave"])[data-edge="both"] {
		mask-image: var(--_yeti-seam-top), linear-gradient(black, black), var(--_yeti-seam-bottom);
		mask-size: 100% var(--_yeti-seam), 100% calc(100% - 2 * var(--_yeti-seam)), 100% var(--_yeti-seam);
		mask-position: top, center, bottom;
	}
}

/* src/components/nav/nav.css */
/* Nav: a bar with a brand, a list of links, and actions. The list carries
   popover. Below the threshold it is a closed popover that the toggle opens
   as a panel; at or above it, the nav's own container query puts the list
   in the bar and hides the toggle. One list, two modes, no script. */
@layer yeti.components {
	.nav {
		display: flex;
		align-items: center;
		gap: var(--_yeti-gap);
		min-block-size: var(--yeti-control-size);
		padding: var(--yeti-nav-padding);
		background-color: var(--yeti-nav-surface);
		color: var(--yeti-color-text);
		border-block-end: var(--yeti-border-width) solid var(--yeti-nav-border);
		container-type: inline-size;
	}
	.nav:not([data-gap]) { --_yeti-gap: var(--yeti-space-sm); }
	.nav:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.nav > * { margin: 0; }
	.nav > [data-brand] {
		font-weight: var(--yeti-weight-strong);
		color: inherit;
		text-decoration: none;
		white-space: nowrap;
	}
	.nav > [data-actions] {
		display: flex;
		align-items: center;
		gap: var(--_yeti-gap);
		margin-inline-start: auto;
	}

	/* The toggle, and the close button inside the panel: bare squares the
	   size of a control. */
	.nav > button[popovertarget],
	.nav > ul[popover] > [data-close] > button {
		appearance: none;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		min-inline-size: var(--yeti-control-size);
		min-block-size: var(--yeti-control-size);
		padding: 0 var(--yeti-space-xs);
		border: 0;
		border-radius: var(--yeti-nav-radius);
		background-color: transparent;
		color: inherit;
		font: inherit;
		cursor: pointer;
	}
	.nav > button[popovertarget]:hover,
	.nav > ul[popover] > [data-close] > button:hover { background-color: var(--_yeti-variant-subtle); }
	.nav > button[popovertarget] > svg {
		inline-size: 1.25em;
		block-size: 1.25em;
	}

	/* The links. */
	.nav > ul[popover] {
		list-style: none;
		margin: 0;
		padding: 0;
	}
	/* The prose layer spaces list items with margin-block-start; the nav
	   owns that space through its own gap instead. A dropdown's trigger
	   inside the list is a button, not a link, and is styled like one. */
	.nav > ul[popover] > li { margin: 0; }
	.nav > ul[popover] :is(a, .dropdown > button) {
		display: inline-flex;
		align-items: center;
		min-block-size: var(--yeti-control-size);
		padding: var(--yeti-space-xs) var(--yeti-space-sm);
		border: 0;
		border-radius: var(--yeti-nav-radius);
		background-color: transparent;
		color: inherit;
		text-decoration: none;
		cursor: pointer;
	}
	.nav > ul[popover] :is(a, .dropdown > button):hover { background-color: var(--_yeti-variant-subtle); }
	.nav > ul[popover] a[aria-current] {
		color: var(--_yeti-variant-text);
		font-weight: var(--yeti-weight-strong);
	}

	/* The panel: the open list below the threshold. The user agent gives a
	   popover inset 0, margin auto, a border, and fit-content sizes; the
	   shapes reset what they need. */
	.nav > ul[popover]:popover-open {
		display: flex;
		flex-direction: column;
		position: fixed;
		inset: auto;
		margin: 0;
		padding: var(--yeti-nav-padding);
		border: 0;
		background-color: var(--yeti-nav-panel);
		color: var(--yeti-color-text);
		box-shadow: var(--yeti-shadow-md);
		overflow: auto;
		transition: opacity var(--yeti-duration-fast) var(--yeti-ease), translate var(--yeti-duration-fast) var(--yeti-ease);
	}
	.nav > ul[popover]:popover-open :is(a, .dropdown > button) { inline-size: 100%; }
	.nav > ul[popover] > [data-close] {
		position: absolute;
		inset-block-start: var(--yeti-space-sm);
		inset-inline-end: var(--yeti-space-sm);
	}

	/* Sheet, the default: under the bar, the full width. Anchored to the bar
	   where anchor positioning exists; at the top of the viewport otherwise,
	   which covers the bar, but Escape and a click outside still close it.
	   The name is scoped to each nav's subtree, so a page with two navs
	   anchors each sheet under its own bar. The cap comes from position-area:
	   it gives the sheet a region running from the bar's far edge to the
	   viewport's own bottom, and align-self plus a percentage cap size the
	   sheet to its content within that region instead of always filling it.
	   anchor-size() cannot do this: it only knows the bar's own height, not
	   its distance from the top, so a page with content above the bar would
	   still let a tall sheet run past the bottom. */
	.nav:is(:not([data-panel]), [data-panel="sheet"]) > ul[popover]:popover-open {
		inset-inline: 0;
		inset-block-start: 0;
		inline-size: auto;
		max-block-size: 100dvh;
		border-block-end: var(--yeti-border-width) solid var(--yeti-nav-border);
	}
	@supports (anchor-name: --a) and (anchor-scope: --a) and (position-anchor: --a) and (position-area: block-end) {
		.nav { anchor-name: --yeti-nav; anchor-scope: --yeti-nav, --yeti-nav-panel; }
		.nav:is(:not([data-panel]), [data-panel="sheet"]) > ul[popover]:popover-open {
			position-anchor: --yeti-nav;
			position-area: block-end span-all;
			align-self: start;
			justify-self: stretch;
			max-block-size: 100%;
		}
	}

	/* Drawer: a column from the start edge, the page dimmed behind it. A
	   click on the backdrop is a click outside, so it closes the drawer. */
	.nav[data-panel="drawer"] > ul[popover]:popover-open {
		inset-block: 0;
		inset-inline-start: 0;
		inline-size: min(20rem, 100%);
		block-size: auto;
		max-block-size: none;
	}
	.nav[data-panel="drawer"] > ul[popover]::backdrop { background-color: var(--yeti-shadow-color); }

	/* Screen: the whole viewport, links large and centred. */
	.nav[data-panel="screen"] > ul[popover]:popover-open {
		inset: 0;
		inline-size: auto;
		block-size: auto;
		max-block-size: none;
		justify-content: center;
		align-items: center;
		font-size: var(--yeti-text-lg);
	}
	.nav[data-panel="screen"] > ul[popover]:popover-open a { inline-size: auto; }

	/* A screen panel with no close item would have no outside to click and no
	   Escape on a phone, so it keeps a margin that dismisses it. */
	.nav[data-panel="screen"]:not(:has(> ul[popover] > [data-close])) > ul[popover]:popover-open {
		inset: var(--yeti-space-xl);
	}

	/* A dropdown inside the open panel. In the bar its panel is a small card
	   anchored under a small trigger; in a full-width list that shape reads as
	   a detached tooltip and hangs past the panel's own edge, so here it
	   becomes a full-width block docked under its trigger, sharing the panel's
	   surface and padding so the two read as one list. The guard is the panel
	   being open, which is exactly the condition the inline rules are gated
	   against, so a dropdown in the bar keeps its card. The wrapper needs
	   display: block, or it shrink-wraps and the trigger sits narrower than
	   the links beside it. It is still a popover, so it stays in the top layer
	   and covers the items below rather than pushing them down; Escape and a
	   click outside unwind it before the panel. Without anchor positioning it
	   docks to the foot of the viewport instead: still full width, still
	   dismissible, still in reach of a thumb. */
	.nav > ul[popover]:popover-open .dropdown { display: block; }
	.nav > ul[popover]:popover-open .dropdown > [popover]:popover-open {
		position: fixed;
		inset-block: auto 0;
		inset-inline: 0;
		inline-size: auto;
		min-inline-size: 0;
		max-block-size: 60dvh;
		margin: 0;
		padding: var(--yeti-nav-padding);
		border-radius: 0;
		overflow: auto;
	}
	@supports (anchor-name: --a) and (anchor-scope: --a) and (position-anchor: --a) and (position-area: block-end) {
		/* Two anchors. The trigger, which position-anchor already names, gives
		   the region: it starts at the trigger's far edge and runs to the
		   viewport's bottom, so align-self and a percentage cap hug the content
		   and still stop the block short of the edge, exactly as the sheet does.
		   The panel gives the inline edges. Spanning the viewport instead would
		   push the block past a drawer's edge and past an inset screen's margin;
		   reading the panel's own box keeps every shape right without restating
		   a single one of their widths. */
		.nav > ul[popover] { anchor-name: --yeti-nav-panel; }
		.nav > ul[popover]:popover-open .dropdown > [popover]:popover-open {
			position-area: block-end span-all;
			align-self: start;
			justify-self: stretch;
			inset-inline-start: anchor(--yeti-nav-panel start);
			inset-inline-end: anchor(--yeti-nav-panel end);
			max-block-size: 100%;
		}
	}

	/* Entering: the sheet and the screen fade in, the drawer slides in.
	   Leaving is instant. The durations are tokens, so reduced motion
	   makes entering instant too. */
	@starting-style {
		.nav > ul[popover]:popover-open { opacity: 0; }
		.nav[data-panel="drawer"] > ul[popover]:popover-open { translate: -100% 0; }
	}

	/* Inline, at or above the threshold: the list joins the bar, the toggle
	   and the close item go. Each block is gated on the panel being closed
	   (:not(:has(> ul[popover]:popover-open))), so these rules and the panel
	   rules never apply at once. The doubled [popover] stays so the inline
	   rules still outrank the user agent's own closed-popover styling.
	   Thresholds are the width tokens' defaults, literal because a container
	   condition cannot read a token; md (32rem) is the default. While a
	   panel is open the guard turns these rules off, so the nav stays in
	   panel mode whatever its width: a resize cannot strip an open panel
	   of its box before it is dismissed. */
	@container (inline-size >= 16rem) {
		/* xs (16rem) */
		.nav[data-threshold="xs"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: var(--_yeti-gap); position: static; inset: auto; inline-size: auto; block-size: auto; max-block-size: none; margin: 0; padding: 0; border: 0; background-color: transparent; color: inherit; box-shadow: none; overflow: visible; font-size: inherit; opacity: 1; translate: none; }
		.nav[data-threshold="xs"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] a { inline-size: auto; }
		.nav[data-threshold="xs"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] > [data-close],
		.nav[data-threshold="xs"]:not(:has(> ul[popover]:popover-open)) > button[popovertarget] { display: none; }
	}
	@container (inline-size >= 24rem) {
		/* sm (24rem) */
		.nav[data-threshold="sm"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: var(--_yeti-gap); position: static; inset: auto; inline-size: auto; block-size: auto; max-block-size: none; margin: 0; padding: 0; border: 0; background-color: transparent; color: inherit; box-shadow: none; overflow: visible; font-size: inherit; opacity: 1; translate: none; }
		.nav[data-threshold="sm"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] a { inline-size: auto; }
		.nav[data-threshold="sm"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] > [data-close],
		.nav[data-threshold="sm"]:not(:has(> ul[popover]:popover-open)) > button[popovertarget] { display: none; }
	}
	@container (inline-size >= 32rem) {
		/* md (32rem), the default */
		.nav:is(:not([data-threshold]), [data-threshold="md"]):not(:has(> ul[popover]:popover-open)) > ul[popover][popover] { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: var(--_yeti-gap); position: static; inset: auto; inline-size: auto; block-size: auto; max-block-size: none; margin: 0; padding: 0; border: 0; background-color: transparent; color: inherit; box-shadow: none; overflow: visible; font-size: inherit; opacity: 1; translate: none; }
		.nav:is(:not([data-threshold]), [data-threshold="md"]):not(:has(> ul[popover]:popover-open)) > ul[popover][popover] a { inline-size: auto; }
		.nav:is(:not([data-threshold]), [data-threshold="md"]):not(:has(> ul[popover]:popover-open)) > ul[popover][popover] > [data-close],
		.nav:is(:not([data-threshold]), [data-threshold="md"]):not(:has(> ul[popover]:popover-open)) > button[popovertarget] { display: none; }
	}
	@container (inline-size >= 48rem) {
		/* lg (48rem) */
		.nav[data-threshold="lg"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: var(--_yeti-gap); position: static; inset: auto; inline-size: auto; block-size: auto; max-block-size: none; margin: 0; padding: 0; border: 0; background-color: transparent; color: inherit; box-shadow: none; overflow: visible; font-size: inherit; opacity: 1; translate: none; }
		.nav[data-threshold="lg"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] a { inline-size: auto; }
		.nav[data-threshold="lg"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] > [data-close],
		.nav[data-threshold="lg"]:not(:has(> ul[popover]:popover-open)) > button[popovertarget] { display: none; }
	}
	@container (inline-size >= 64rem) {
		/* xl (64rem) */
		.nav[data-threshold="xl"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: var(--_yeti-gap); position: static; inset: auto; inline-size: auto; block-size: auto; max-block-size: none; margin: 0; padding: 0; border: 0; background-color: transparent; color: inherit; box-shadow: none; overflow: visible; font-size: inherit; opacity: 1; translate: none; }
		.nav[data-threshold="xl"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] a { inline-size: auto; }
		.nav[data-threshold="xl"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] > [data-close],
		.nav[data-threshold="xl"]:not(:has(> ul[popover]:popover-open)) > button[popovertarget] { display: none; }
	}
	@container (inline-size >= 80rem) {
		/* 2xl (80rem) */
		.nav[data-threshold="2xl"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: var(--_yeti-gap); position: static; inset: auto; inline-size: auto; block-size: auto; max-block-size: none; margin: 0; padding: 0; border: 0; background-color: transparent; color: inherit; box-shadow: none; overflow: visible; font-size: inherit; opacity: 1; translate: none; }
		.nav[data-threshold="2xl"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] a { inline-size: auto; }
		.nav[data-threshold="2xl"]:not(:has(> ul[popover]:popover-open)) > ul[popover][popover] > [data-close],
		.nav[data-threshold="2xl"]:not(:has(> ul[popover]:popover-open)) > button[popovertarget] { display: none; }
	}
}

/* src/components/breadcrumbs/breadcrumbs.css */
/* Breadcrumbs: the trail to the current page. The separator is drawn by
   CSS with empty alternative text, so it is seen and not read. */
@layer yeti.components {
	.breadcrumbs:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }
	.breadcrumbs > ol {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--yeti-space-xs);
		margin: 0;
		padding: 0;
		list-style: none;
		font-size: var(--_yeti-size-text);
	}
	.breadcrumbs > ol > li {
		display: inline-flex;
		align-items: center;
		gap: var(--yeti-space-xs);
		margin: 0;
	}
	.breadcrumbs > ol > li + li::before {
		content: var(--yeti-breadcrumbs-separator) / "";
		color: var(--yeti-color-text-muted);
	}
	.breadcrumbs a {
		color: var(--yeti-color-text-muted);
		text-decoration: none;
	}
	.breadcrumbs a:hover {
		color: var(--yeti-color-text);
		text-decoration: underline;
	}
	.breadcrumbs [aria-current] {
		color: var(--yeti-color-text);
		font-weight: var(--yeti-weight-strong);
	}
}

/* src/components/pagination/pagination.css */
/* Pagination: page links as square targets, the current page filled.
   Below its own threshold (default sm, 24rem) only Previous, the current
   page, and Next remain. */
@layer yeti.components {
	.pagination { container-type: inline-size; }
	.pagination:not([data-justify]) { --_yeti-justify: flex-start; }
	.pagination:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.pagination:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }
	.pagination > ol {
		display: flex;
		flex-wrap: wrap;
		justify-content: var(--_yeti-justify);
		gap: var(--yeti-space-xs);
		margin: 0;
		padding: 0;
		list-style: none;
		font-size: var(--_yeti-size-text);
	}
	.pagination > ol > li { margin: 0; }
	.pagination > ol > li > :is(a, span) {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		min-inline-size: var(--yeti-control-size);
		min-block-size: var(--yeti-control-size);
		padding-inline: var(--yeti-space-sm);
		border-radius: var(--yeti-pagination-radius);
		color: inherit;
		text-decoration: none;
	}
	.pagination > ol > li > a:hover { background-color: var(--_yeti-variant-subtle); }
	.pagination > ol > li > [aria-current] {
		background-color: var(--_yeti-variant);
		color: var(--_yeti-on-variant);
	}
	.pagination > ol > li > span { color: var(--yeti-color-text-muted); }

	/* Compact, below the threshold. Thresholds are the width tokens'
	   defaults, literal because a container condition cannot read a token;
	   sm (24rem) is the default. */
	@container (inline-size < 16rem) {
		/* xs (16rem) */
		.pagination[data-threshold="xs"] > ol > li:not(:has(> [rel~="prev"], > [rel~="next"], > [aria-current])) { display: none; }
	}
	@container (inline-size < 24rem) {
		/* sm (24rem), the default */
		.pagination:is(:not([data-threshold]), [data-threshold="sm"]) > ol > li:not(:has(> [rel~="prev"], > [rel~="next"], > [aria-current])) { display: none; }
	}
	@container (inline-size < 32rem) {
		/* md (32rem) */
		.pagination[data-threshold="md"] > ol > li:not(:has(> [rel~="prev"], > [rel~="next"], > [aria-current])) { display: none; }
	}
	@container (inline-size < 48rem) {
		/* lg (48rem) */
		.pagination[data-threshold="lg"] > ol > li:not(:has(> [rel~="prev"], > [rel~="next"], > [aria-current])) { display: none; }
	}
	@container (inline-size < 64rem) {
		/* xl (64rem) */
		.pagination[data-threshold="xl"] > ol > li:not(:has(> [rel~="prev"], > [rel~="next"], > [aria-current])) { display: none; }
	}
	@container (inline-size < 80rem) {
		/* 2xl (80rem) */
		.pagination[data-threshold="2xl"] > ol > li:not(:has(> [rel~="prev"], > [rel~="next"], > [aria-current])) { display: none; }
	}
}

/* src/components/alert/alert.css */
/* Alert: a message with a hue, an emphasis, an optional icon, and an
   optional close button. The colour is a border and a tint; the words are
   the message. */
@layer yeti.components {
	.alert {
		display: flex;
		align-items: flex-start;
		gap: var(--yeti-space-sm);
		padding: var(--yeti-alert-padding);
		border: var(--yeti-border-width) solid var(--_yeti-variant);
		border-inline-start-width: calc(var(--yeti-border-width) * 4);
		border-radius: var(--yeti-alert-radius);
		background-color: var(--_yeti-variant-subtle);
		color: var(--yeti-color-text);
	}
	.alert:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.alert > * { margin: 0; }
	.alert > :not(svg, [data-dismiss]) { flex: 1; }
	.alert[data-emphasis="high"] {
		background-color: var(--_yeti-variant);
		border-color: var(--_yeti-variant-strong);
		color: var(--_yeti-on-variant);
	}
	.alert[data-emphasis="low"] { background-color: transparent; }

	/* The icon sits on the first line of text. */
	.alert > svg {
		inline-size: 1.25em;
		block-size: 1.25em;
		flex: none;
		margin-block: calc((1lh - 1.25em) / 2);
		color: var(--_yeti-variant-text);
	}
	.alert[data-emphasis="high"] > svg { color: inherit; }

	/* The close button: a control-sized square at the end, pulled into the
	   first line's height so the alert is no taller for it. */
	.alert > [data-dismiss] {
		appearance: none;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		flex: none;
		inline-size: var(--yeti-control-size);
		block-size: var(--yeti-control-size);
		margin-block: calc((1lh - var(--yeti-control-size)) / 2);
		margin-inline-start: auto;
		padding: 0;
		border: 0;
		border-radius: var(--yeti-alert-radius);
		background-color: transparent;
		color: inherit;
		font: inherit;
		cursor: pointer;
	}
	.alert > [data-dismiss]:hover { background-color: color-mix(in oklch, currentColor 12%, transparent); }
}

/* src/components/progress/progress.css */
/* Progress: the native element, styled. With a value it is a bar filled to
   the value; without one it is indeterminate and the track's stripes move. */
@layer yeti.components {
	.progress {
		appearance: none;
		display: block;
		inline-size: 100%;
		block-size: calc(var(--_yeti-size-space) / 2);
		border: 0;
		border-radius: var(--yeti-progress-radius);
		background-color: var(--yeti-color-surface-sunken);
		overflow: clip;
	}
	.progress:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.progress:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }
	.progress::-webkit-progress-bar { background-color: transparent; }
	.progress::-webkit-progress-value {
		background-color: var(--_yeti-variant);
		border-radius: inherit;
		transition: inline-size var(--yeti-duration-base) var(--yeti-ease);
	}
	.progress::-moz-progress-bar {
		background-color: var(--_yeti-variant);
		border-radius: inherit;
		transition: inline-size var(--yeti-duration-base) var(--yeti-ease);
	}

	/* Indeterminate: diagonal stripes sliding along the track. One period of
	   a 45-degree stripe pair 1.5rem wide is 1.5rem × √2 along the track. */
	.progress:indeterminate {
		background-image: repeating-linear-gradient(-45deg, var(--_yeti-variant-subtle) 0 0.75rem, var(--yeti-color-surface-sunken) 0.75rem 1.5rem);
		animation: yeti-progress calc(var(--yeti-duration-base) * 4) linear var(--yeti-motion-iterations);
	}
	.progress:indeterminate::-webkit-progress-bar { background-color: transparent; }
	.progress:indeterminate::-moz-progress-bar { background-color: transparent; }
	@keyframes yeti-progress {
		to { background-position: 2.1213rem 0; }
	}
}

/* src/components/spinner/spinner.css */
/* Spinner: a ring with one bright edge, turning. The same ring appears
   after the label of a busy button, which is why that rule lives here. */
@layer yeti.components {
	.spinner,
	.button[aria-busy="true"]::after {
		display: inline-block;
		inline-size: 1em;
		block-size: 1em;
		flex: none;
		border: var(--yeti-spinner-width) solid var(--_yeti-variant-subtle);
		border-block-start-color: var(--_yeti-variant);
		border-radius: 50%;
		animation: yeti-spin var(--yeti-spinner-duration) linear var(--yeti-motion-iterations);
	}
	.spinner {
		font-size: var(--_yeti-size-text);
		vertical-align: -0.15em;
	}
	.spinner:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.spinner:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }

	/* In a busy button the ring is drawn from the button's own text colour,
	   so it reads on every emphasis; the button's gap spaces it. */
	.button[aria-busy="true"]::after {
		content: "";
		border-color: color-mix(in oklch, currentColor 30%, transparent);
		border-block-start-color: currentColor;
	}
	@keyframes yeti-spin {
		to { rotate: 1turn; }
	}
}

/* src/components/accordion/accordion.css */
/* Accordion: a column of disclosures. details and summary do the work, so
   opening and closing, the keyboard, and the announced state are all the
   browser's. Give every details the same name attribute and the browser
   closes the others for you; CSS cannot set an attribute, so Yeti does not
   pretend to offer that as an option of its own. */
@layer yeti.components {
	.accordion {
		border: var(--yeti-border-width) solid var(--yeti-accordion-border);
		border-radius: var(--yeti-radius-md);
		background-color: var(--yeti-accordion-surface);
		overflow: clip;
	}
	.accordion > details + details { border-block-start: var(--yeti-border-width) solid var(--yeti-accordion-border); }
	.accordion > details > summary {
		display: flex;
		align-items: center;
		gap: var(--yeti-space-sm);
		padding: var(--yeti-accordion-padding);
		background-color: var(--yeti-accordion-summary);
		font-weight: var(--yeti-weight-strong);
		cursor: pointer;
		list-style: none;
	}
	.accordion > details > summary::-webkit-details-marker { display: none; }
	.accordion > details > summary:hover { background-color: var(--yeti-accordion-summary-hover); }
	/* The set clips its corners, so the ring is drawn inside the summary
	   rather than outside it, where the clip would crop it. */
	.accordion > details > summary:focus-visible { outline-offset: -2px; }
	/* An open row otherwise reads as one block with its panel, since the
	   chevron is the only thing that changes. The line sits inside the details,
	   below the summary, so it never lands on the line between one details and
	   the next. */
	.accordion > details[open] > summary {
		border-block-end: var(--yeti-border-width) solid var(--yeti-accordion-border);
	}

	/* The chevron: a square with two borders, turned. It points down when
	   the panel is shut and up when it is open. */
	.accordion > details > summary::after {
		content: "";
		inline-size: 0.5em;
		block-size: 0.5em;
		margin-inline-start: auto;
		border-block-end: 2px solid currentColor;
		border-inline-end: 2px solid currentColor;
		rotate: 45deg;
		translate: 0 -0.15em;
		transition: rotate var(--yeti-duration-fast) var(--yeti-ease);
	}
	.accordion > details[open] > summary::after {
		rotate: -135deg;
		translate: 0 0.1em;
	}

	/* The panel: everything after the summary, padded to match it. The base
	   layer already spaces flow inside a details. */
	.accordion > details > :not(summary) {
		margin-inline: var(--yeti-accordion-padding);
	}
	.accordion > details > :not(summary):last-child { margin-block-end: var(--yeti-accordion-padding); }

	/* The panel grows open rather than appearing. A details cannot animate its
	   own height to auto in every engine, because the keywords that make auto
	   interpolable are Chromium's alone, but a grid row measured in fr can, and
	   that works everywhere. The summary takes the first row and the generated
	   panel the second, so the transition runs on that row's size; the panel
	   clips its overflow so the content is revealed rather than squashed.
	   content-visibility has to come along discretely, or the panel would blink
	   out at the start of a collapse instead of shrinking. The duration is a
	   token because a rule in this layer outranks the reset's reduced-motion
	   rule, but never the value it resolves.

	   A fade alone was the first attempt and it read as a flash: the content
	   arrived at full height instantly and only its opacity moved, so the row
	   jumped and strobed at the same time. */
	.accordion > details {
		display: grid;
		grid-template-rows: auto 0fr;
		/* Without this the summary's row swells by a dozen pixels part way
		   through and settles back, because an fr row is a share of the grid's
		   leftover space and the auto row above it takes part in that sum while
		   the share is being interpolated. Packing the rows to the start takes
		   the summary out of the calculation, so only the panel moves. */
		align-content: start;
		transition: grid-template-rows var(--yeti-duration-base) var(--yeti-ease);
	}
	.accordion > details[open] { grid-template-rows: auto 1fr; }
	.accordion > details::details-content {
		overflow: hidden;
		transition: content-visibility var(--yeti-duration-base) allow-discrete;
	}
}

/* src/components/tabs/tabs.css */
/* Tabs: a list of tabs and their panels. The CSS never hides a panel, so a
   page without tabs.js shows every panel under its own tab and nothing is
   unreachable; the module hides the inactive ones and wires the keys. */
@layer yeti.components {
	.tabs {
		display: flex;
		flex-direction: column;
		gap: var(--_yeti-gap);
	}
	.tabs:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.tabs:not([data-variant]) { --_yeti-variant: var(--yeti-color-primary); --_yeti-variant-subtle: var(--yeti-color-primary-subtle); --_yeti-variant-soft: var(--yeti-color-primary-soft); --_yeti-variant-strong: var(--yeti-color-primary-strong); --_yeti-variant-text: var(--yeti-color-primary-text); --_yeti-on-variant: var(--yeti-on-primary); }
	.tabs > [role="tablist"] {
		display: flex;
		flex-wrap: wrap;
		gap: var(--yeti-space-xs);
		border-block-end: var(--yeti-border-width) solid var(--yeti-tabs-border);
	}
	.tabs [role="tab"] {
		appearance: none;
		padding: var(--yeti-tabs-padding);
		border: 0;
		border-block-end: calc(var(--yeti-border-width) * 2) solid transparent;
		margin-block-end: calc(-1 * var(--yeti-border-width));
		background-color: transparent;
		color: inherit;
		font: inherit;
		font-weight: var(--yeti-weight-strong);
		cursor: pointer;
		transition: color var(--yeti-duration-fast) var(--yeti-ease), border-color var(--yeti-duration-fast) var(--yeti-ease);
	}
	.tabs [role="tab"]:hover { background-color: var(--_yeti-variant-subtle); }
	.tabs [role="tab"][aria-selected="true"] {
		color: var(--_yeti-variant-text);
		border-block-end-color: var(--_yeti-variant);
	}

	/* Vertical: the list stands beside the panels and its edge moves with it. */
	.tabs[data-orientation="vertical"] { flex-direction: row; }
	.tabs[data-orientation="vertical"] > [role="tablist"] {
		flex-direction: column;
		flex-wrap: nowrap;
		border-block-end: 0;
		border-inline-end: var(--yeti-border-width) solid var(--yeti-tabs-border);
	}
	.tabs[data-orientation="vertical"] [role="tab"] {
		text-align: start;
		border-block-end: 0;
		border-inline-end: calc(var(--yeti-border-width) * 2) solid transparent;
		margin-block-end: 0;
		margin-inline-end: calc(-1 * var(--yeti-border-width));
	}
	.tabs[data-orientation="vertical"] [role="tab"][aria-selected="true"] { border-inline-end-color: var(--_yeti-variant); }
	.tabs[data-orientation="vertical"] > [role="tabpanel"] { flex: 1; }
}

/* src/components/dropdown/dropdown.css */
/* Dropdown: a button and a panel of ordinary links and buttons. The panel
   carries popover, so the top layer, light dismiss, Escape and the button's
   expanded state are all the browser's. No menu roles: a disclosure panel of
   links reads better than a half-built application menu. */
@layer yeti.components {
	.dropdown { display: inline-block; }
	.dropdown > [popover] {
		margin: 0;
		padding: var(--yeti-dropdown-padding);
		border: 0;
		background-color: transparent;
	}
	.dropdown > [popover]:popover-open {
		display: flex;
		flex-direction: column;
		min-inline-size: var(--yeti-dropdown-min);
		border-radius: var(--yeti-dropdown-radius);
		background-color: var(--yeti-dropdown-surface);
		color: var(--yeti-color-text);
		box-shadow: var(--yeti-shadow-md);
		transition: opacity var(--yeti-duration-fast) var(--yeti-ease);
	}
	.dropdown > [popover] a,
	.dropdown > [popover] button {
		display: flex;
		align-items: center;
		gap: var(--yeti-space-xs);
		min-block-size: var(--yeti-control-size);
		padding-inline: var(--yeti-space-sm);
		border: 0;
		border-radius: calc(var(--yeti-dropdown-radius) - var(--yeti-dropdown-padding));
		background-color: transparent;
		color: inherit;
		font: inherit;
		text-align: start;
		text-decoration: none;
		cursor: pointer;
	}
	.dropdown > [popover] a:hover,
	.dropdown > [popover] button:hover { background-color: var(--yeti-color-surface-sunken); }

	/* Anchored under its own trigger. The name is scoped to this dropdown, so
	   a page of them does not hang every panel off the last one. Without
	   anchor positioning the user agent centres the panel, which still opens,
	   closes and reads correctly. */
	@supports (anchor-name: --a) and (anchor-scope: --a) and (position-anchor: --a) and (position-area: block-end) {
		.dropdown { anchor-scope: --yeti-dropdown; }
		.dropdown > button[popovertarget] { anchor-name: --yeti-dropdown; }
		.dropdown > [popover]:popover-open {
			position: fixed;
			position-anchor: --yeti-dropdown;
			position-area: block-end span-inline-end;
			margin-block-start: var(--yeti-space-xs);
		}
		.dropdown[data-side="end"] > [popover]:popover-open { position-area: block-end span-inline-start; }
	}
	@starting-style {
		.dropdown > [popover]:popover-open { opacity: 0; }
	}
}

/* src/components/dialog/dialog.css */
/* Dialog: the native dialog element, styled. Opened as a modal by dialog.js,
   which is what makes the rest of the page inert and keeps focus inside. The
   reset already exempts dialog from the universal margin rule, so the
   browser's own centring survives. */
@layer yeti.components {
	.dialog {
		max-inline-size: min(var(--_yeti-width), 100% - 2 * var(--yeti-space-md));
		padding: var(--yeti-dialog-padding);
		border: 0;
		border-radius: var(--yeti-dialog-radius);
		background-color: var(--yeti-dialog-surface);
		color: var(--yeti-color-text);
		box-shadow: var(--yeti-shadow-md);
	}
	.dialog:not([data-width]) { --_yeti-width: var(--yeti-width-md); }
	.dialog::backdrop { background-color: var(--yeti-dialog-backdrop); }
	.dialog > footer {
		display: flex;
		flex-wrap: wrap;
		justify-content: flex-end;
		align-items: center;
		gap: var(--yeti-space-sm);
	}
	/* dialog is not one of the flow containers the base layer spaces, and the
	   reset has zeroed every child's margin, so the dialog spaces its own. */
	/* footer IS one of those containers, and a block-start margin on a centred
	   flex item shifts it down half its own margin. */
	.dialog > footer > * { margin-block: 0; }

	/* Arriving and leaving. display and overlay transition discretely so the
	   fade is seen in both directions; the durations are tokens, so reduced
	   motion makes both instant. */
	.dialog, .dialog::backdrop {
		transition: opacity var(--yeti-duration-fast) var(--yeti-ease), translate var(--yeti-duration-fast) var(--yeti-ease), display var(--yeti-duration-fast) allow-discrete, overlay var(--yeti-duration-fast) allow-discrete;
	}
	.dialog:not([open]), .dialog:not([open])::backdrop { opacity: 0; }
	.dialog[open] { translate: 0 0; }
	@starting-style {
		.dialog[open] { opacity: 0; translate: 0 1rem; }
		.dialog[open]::backdrop { opacity: 0; }
	}
}

/* src/components/tooltip/tooltip.css */
/* Tooltip: a trigger and a bubble that appears on hover and on focus. It is
   deliberately not a popover: nothing but script can open one, and the point
   of this component is that it needs none. Anchor positioning, where it
   exists, lets the bubble escape a scrolling ancestor the way absolute
   positioning cannot. */
@layer yeti.components {
	.tooltip {
		display: inline-block;
		position: relative;
	}
	.tooltip > [role="tooltip"] {
		position: absolute;
		inset-block-end: calc(100% + var(--yeti-space-xs));
		inset-inline-start: 50%;
		translate: -50% 0;
		z-index: 1;
		inline-size: max-content;
		max-inline-size: 16rem;
		padding: var(--yeti-space-xs) var(--yeti-space-sm);
		border-radius: var(--yeti-tooltip-radius);
		background-color: var(--yeti-tooltip-surface);
		color: var(--yeti-tooltip-text);
		font-size: var(--yeti-text-sm);
		opacity: 0;
		visibility: hidden;
		transition: opacity var(--yeti-duration-fast) var(--yeti-ease), visibility var(--yeti-duration-fast);
	}
	/* The caret is as deep as the gap between bubble and trigger, so it bridges
	   the two exactly and asks for no token of its own. Here the bubble is
	   always centred on its trigger, so the caret can hang off the bubble.

	   The extra pixel is the point of --_yeti-caret-overlap. Caret and bubble
	   are separately positioned boxes that meet on a fractional edge, since the
	   space token is fluid, and two such edges leave a hairline of background
	   between them at most zoom levels. Overlapping by a pixel closes it; the
	   caret is the same colour as the bubble, so the overlap cannot show. */
	.tooltip > [role="tooltip"] {
		--_yeti-caret: var(--yeti-space-xs);
		--_yeti-caret-overlap: 1px;
	}
	.tooltip > [role="tooltip"]::after {
		content: "";
		position: absolute;
		inset-block-start: calc(100% - var(--_yeti-caret-overlap));
		inset-inline-start: 50%;
		translate: -50% 0;
		inline-size: calc(var(--_yeti-caret) * 2);
		block-size: calc(var(--_yeti-caret) + var(--_yeti-caret-overlap));
		background-color: var(--yeti-tooltip-surface);
		clip-path: polygon(0 0, 100% 0, 50% 100%);
	}
	.tooltip:hover > [role="tooltip"],
	.tooltip:focus-within > [role="tooltip"] {
		opacity: 1;
		visibility: visible;
	}

	/* Anchored, so a scroller cannot clip it. The name is scoped to this
	   tooltip, so a page of them each hang off their own trigger. */
	@supports (anchor-name: --a) and (anchor-scope: --a) and (position-anchor: --a) and (position-area: block-start) {
		.tooltip { anchor-scope: --yeti-tooltip; }
		.tooltip > :first-child { anchor-name: --yeti-tooltip; }
		.tooltip > [role="tooltip"] {
			position: fixed;
			position-anchor: --yeti-tooltip;
			position-area: block-start span-all;
			inset: auto;
			translate: none;
			margin-block-end: var(--yeti-space-xs);
		}
		/* The caret is anchored to the trigger rather than to the bubble,
		   because a bubble near the edge of the viewport slides sideways to
		   stay on screen and a caret carried along with it would point at
		   nothing. */
		.tooltip > [role="tooltip"]::after {
			position: fixed;
			position-anchor: --yeti-tooltip;
			position-area: block-start;
			justify-self: anchor-center;
			inset: auto;
			translate: none;
			/* The region's own alignment pins the tip to the trigger, so the
			   overlap grows out of the far edge, into the bubble. */
			block-size: calc(var(--_yeti-caret) + var(--_yeti-caret-overlap));
		}
		.tooltip[data-placement="bottom"] > [role="tooltip"] {
			position-area: block-end span-all;
			margin-block: var(--yeti-space-xs) 0;
		}
		.tooltip[data-placement="bottom"] > [role="tooltip"]::after {
			position-area: block-end;
			clip-path: polygon(50% 0, 100% 100%, 0 100%);
		}
		.tooltip[data-placement="start"] > [role="tooltip"] {
			position-area: inline-start span-all;
			margin-inline: 0 var(--yeti-space-xs);
		}
		.tooltip[data-placement="end"] > [role="tooltip"] {
			position-area: inline-end span-all;
			margin-inline: var(--yeti-space-xs) 0;
		}
		.tooltip[data-placement="start"] > [role="tooltip"]::after,
		.tooltip[data-placement="end"] > [role="tooltip"]::after {
			align-self: anchor-center;
			justify-self: auto;
			inline-size: calc(var(--_yeti-caret) + var(--_yeti-caret-overlap));
			block-size: calc(var(--_yeti-caret) * 2);
		}
		.tooltip[data-placement="start"] > [role="tooltip"]::after {
			position-area: inline-start;
			clip-path: polygon(0 0, 100% 50%, 0 100%);
		}
		.tooltip[data-placement="end"] > [role="tooltip"]::after {
			position-area: inline-end;
			clip-path: polygon(100% 0, 100% 100%, 0 50%);
		}
	}
}

/* src/components/carousel/carousel.css */
/* Carousel: slides on a scroll-snap track, and dots that are ordinary links
   to each slide's id, so the browser scrolls and the CSS needs no script;
   carousel.js is optional and only keeps those links out of the history.
   Components come after base in the layer order, so a components-layer
   scroll-behavior would outrank the reset's reduced-motion rule; smooth
   scrolling instead reads a token, so the preference reaches the track
   through the token's own value rather than through the reset. The dots
   jump to a slide; they cannot report which one is showing, because CSS
   has no way to know. */
@layer yeti.components {
	.carousel:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); }
	.carousel:not([data-slides]) { --_yeti-slides: 1; }
	.carousel > [data-track] {
		display: flex;
		gap: var(--_yeti-gap);
		overflow-x: auto;
		overscroll-behavior-x: contain;
		scroll-snap-type: x mandatory;
		scroll-behavior: var(--yeti-carousel-scroll);
		/* The bar is noise under a snapping track: the dots say where you are
		   and the track is focusable, so nothing is lost by hiding it. */
		scrollbar-width: none;
	}
	.carousel > [data-track] > * {
		flex: 0 0 auto;
		margin: 0;
		scroll-snap-align: start;
		inline-size: calc((100% - (var(--_yeti-slides) - 1) * var(--_yeti-gap)) / var(--_yeti-slides));
	}
	.carousel > [data-dots] {
		display: flex;
		justify-content: center;
		gap: var(--yeti-space-xs);
		margin-block-start: var(--_yeti-gap);
		padding: 0;
		list-style: none;
	}
	.carousel > [data-dots] > li { margin: 0; }

	/* A dot is a circle inside a target big enough to press. */
	.carousel > [data-dots] a {
		display: flex;
		align-items: center;
		justify-content: center;
		inline-size: var(--yeti-control-size);
		block-size: var(--yeti-control-size);
		text-decoration: none;
	}
	.carousel > [data-dots] a::before {
		content: "";
		inline-size: var(--yeti-carousel-dot-size);
		block-size: var(--yeti-carousel-dot-size);
		border-radius: 50%;
		background-color: var(--yeti-carousel-dot);
		transition: scale var(--yeti-duration-fast) var(--yeti-ease);
	}
	.carousel > [data-dots] a:hover::before { scale: 1.25; }
}

/* src/components/demo/demo.css */
/* Demo: a live example in a box the reader can drag narrower and wider,
   and taller when the demo allows it. The box is a size container, so whatever Yeti markup is inside
   responds the way it would on a real page; the demo is the framework's own
   model applied to itself, not a picture of it. The resize handle is the
   browser's, which is why there is no script here and why it works with a
   mouse or trackpad only; the docs say so. A bar across the top of the box
   names the example and, at its end, the width stop the box is at; that
   label is in this file's second half. */
@layer yeti.components {
	.demo > * { margin: 0; }
	.demo > figcaption {
		margin-block-end: var(--yeti-space-xs);
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
	}

	/* The box. inline-size reads the shared width mapping so data-width
	   picks a starting width; a frame inside cannot size to its content, so
	   the block size comes from data-height's mapping, md when absent; with
	   data-resize="both" the reader can pull it taller from the corner, down
	   to the smallest height stop. The box has a real border, and the iframe fills the
	   content box inside it: an inset shadow was tried instead, but it
	   paints beneath the box's own content, and a framed example's document
	   is opaque (Yeti's own surface colour), so in the framed case — the
	   docs case — the edge would never show through. The width and height
	   tokens name the box's content size, because that is what @container
	   measures and what the markup inside sees, so data-width="md" reads
	   back as md; the border and the inset padding are added outside that
	   content box, and the max and min caps subtract both so the box never
	   overhangs its own container. The box is a grid of two rows, the bar
	   and the example, so the bar takes its own height and the example the
	   rest. */
	.demo:not([data-height]) { --_yeti-height: var(--yeti-height-md); }
	/* Every [data-width] sets --_yeti-width, and custom properties inherit, so a
	   demo inside a shell or a sidebar would silently take that ancestor's
	   width: the docs site's demos all collapsed to its 16rem nav column. A
	   var() fallback cannot help, because the property is set, not unset; the
	   other readers of this token block inheritance by redeclaring it, and this
	   does the same. initial makes it guaranteed-invalid, so the fallback below
	   applies. */
	.demo:not([data-width]) { --_yeti-width: initial; }
	.demo > [data-preview] {
		position: relative;
		display: grid;
		grid-template-rows: auto 1fr;
		box-sizing: content-box;
		--_yeti-demo-inset: 0px;
		--_yeti-demo-max: calc(100% - 2 * (var(--yeti-border-width) + var(--_yeti-demo-inset)));
		inline-size: var(--_yeti-width, var(--_yeti-demo-max));
		max-inline-size: var(--_yeti-demo-max);
		min-inline-size: min(var(--yeti-width-xs), var(--_yeti-demo-max));
		block-size: var(--_yeti-height);
		min-block-size: var(--yeti-height-sm);
		resize: horizontal;
		overflow: auto;
		container-type: inline-size;
		border: var(--yeti-border-width) solid var(--yeti-demo-border);
		border-radius: var(--yeti-demo-radius);
	}
	/* Height is fixed by default, since the point of the drag is width and a
	   frame that also grows tall invites fiddling; a demo that wants it opts
	   in, and the reader can then pull the box taller, never shorter than the
	   sm height. */
	.demo[data-resize="both"] > [data-preview] { resize: both; }
	.demo > [data-preview] > iframe {
		display: block;
		inline-size: 100%;
		block-size: 100%;
		min-block-size: 0;
		border: 0;
	}
	/* Direct markup that reaches the box's own edge sits under WebKit's
	   resize grip and wins the mousedown there, so the reader drags nothing;
	   clearance at the sides and the bottom keeps the grip reachable and
	   reads as an inset around the content, and the bar's own margin gives
	   the same clearance at the top. An iframe is exempt because it has to
	   fill the box exactly, edge to edge. */
	.demo > [data-preview]:not(:has(> iframe)) {
		--_yeti-demo-inset: var(--yeti-space-xs);
		padding: 0 var(--_yeti-demo-inset) var(--_yeti-demo-inset);
	}

	/* The code. The pre keeps the base layer's look on purpose: on a host
	   site its own code styling should win, and it does, being unlayered.
	   The summary is quiet text at rest and underlines itself under the
	   pointer, the way a link does: a touch or keyboard reader cannot drag
	   the box, so this may be the one part of the demo they use, and it must
	   read as pressable the moment they reach it. The rule is there at rest,
	   transparent, so nothing shifts on hover. */
	.demo > details > summary {
		display: inline-flex;
		align-items: center;
		font-size: var(--yeti-text-sm);
		color: var(--yeti-color-text-muted);
		border-block-end: var(--yeti-border-width) solid transparent;
		cursor: pointer;
		list-style: none;
	}
	.demo > details > summary::-webkit-details-marker { display: none; }
	.demo > details > summary:is(:hover, :focus-visible) {
		color: var(--yeti-color-text);
		border-block-end-color: var(--yeti-control-border);
	}
	/* Once the code is open the pre sits right under the summary, and a rule
	   there would read as the pre's top edge, so only the colour change stays. */
	.demo > details[open] > summary:is(:hover, :focus-visible) { border-block-end-color: transparent; }
	/* The summary sits tight under the box and the code tight under it, so
	   the whole thing reads as one object; the base prose rhythm would put a
	   paragraph's worth of air in both gaps. */
	.demo > details > pre { margin-block-start: 0; }

	/* The bar. Both pseudo-elements share the grid's first row: ::before is
	   the bar itself, showing the name the marker carries, and ::after is
	   the label at its end. They are sticky so the bar stays put when direct
	   markup scrolls. A direct box has no top padding, because a sticky box
	   cannot leave its containing block's content box, so a bar pulled up
	   over padding would be clamped back down; the bar spans the box edge to
	   edge with negative inline margins instead, and its own bottom margin
	   supplies the inset above the content. Pointer events pass through, so
	   the bar never steals a drag. */
	.demo > [data-preview]::before,
	.demo > [data-preview]::after {
		grid-area: 1 / 1;
		position: sticky;
		inset-block-start: 0;
		z-index: 1;
		font-size: var(--yeti-text-sm);
		line-height: var(--yeti-leading-md);
		pointer-events: none;
	}
	.demo > [data-preview]::before {
		content: attr(data-preview);
		margin-inline: calc(-1 * var(--_yeti-demo-inset));
		margin-block-end: var(--_yeti-demo-inset);
		padding: var(--yeti-space-xs) var(--yeti-space-sm);
		background-color: var(--yeti-demo-label);
		border-block-end: var(--yeti-border-width) solid var(--yeti-demo-border);
		color: var(--yeti-color-text);
		font-weight: var(--yeti-weight-strong);
	}

	/* The label names the width stop the box is at. It is what a pixel
	   readout could never be: the framework's own vocabulary, flipping at the
	   exact widths the components inside change shape. Thresholds are the
	   width tokens' defaults, literal because a container condition cannot
	   read a token; each names its token. */
	.demo > [data-preview]::after {
		content: "xs";
		justify-self: end;
		align-self: start;
		margin-block-start: var(--yeti-space-xs);
		margin-inline-end: calc(var(--yeti-space-sm) - var(--_yeti-demo-inset));
		padding: 0 var(--yeti-space-xs);
		border-radius: var(--yeti-demo-radius);
		/* Mixed toward the text colour so the pill stands off the bar in both
		   schemes; a second surface token sits too close to the bar in the dark. */
		background-color: color-mix(in oklch, var(--yeti-demo-label) 82%, var(--yeti-color-text));
		color: var(--yeti-color-text-muted);
		font-family: var(--yeti-font-mono);
	}
	/* --yeti-width-sm (24rem) */
	@container (inline-size >= 24rem) { .demo > [data-preview]::after { content: "sm"; } }
	/* --yeti-width-md (32rem) */
	@container (inline-size >= 32rem) { .demo > [data-preview]::after { content: "md"; } }
	/* --yeti-width-lg (48rem) */
	@container (inline-size >= 48rem) { .demo > [data-preview]::after { content: "lg"; } }
	/* --yeti-width-xl (64rem) */
	@container (inline-size >= 64rem) { .demo > [data-preview]::after { content: "xl"; } }
	/* --yeti-width-2xl (80rem) */
	@container (inline-size >= 80rem) { .demo > [data-preview]::after { content: "2xl"; } }
}

/* src/utilities/attention/attention.css */
/* Attention: one pulse or one shake, on load, to point at something that has
   just changed. Once, not repeatedly: a thing that keeps moving is read as a
   state the page is in rather than a moment that has passed, and it cannot be
   dismissed by looking at it. The iteration count is therefore left at the
   initial 1 rather than reading --yeti-motion-iterations, which exists for
   the animations that never end and resolves to infinite. */
@layer yeti.utilities {
	/* No keyframe moves the element away from where it ends, so no fill mode
	   is needed: both the first frame and the last are the element's own
	   computed style. Nothing here can strand an element off its mark. */
	.attention {
		animation: yeti-attention-pulse var(--yeti-attention-duration) var(--yeti-ease);
	}
	.attention[data-attention="shake"] { animation-name: yeti-attention-shake; }

	/* Swelling and settling, with the peak in the middle so the gesture is
	   symmetrical; an element that grows and snaps back reads as a twitch. */
	@keyframes yeti-attention-pulse {
		50% { scale: var(--yeti-attention-scale); }
	}
	/* Two throws, not one: a single sideways move is a slide, and it takes the
	   return trip to read as a shake. The distance is small enough that a
	   button beside it is never overlapped. */
	@keyframes yeti-attention-shake {
		25% { translate: calc(-1 * var(--yeti-attention-distance)); }
		50% { translate: var(--yeti-attention-distance); }
		75% { translate: calc(-1 * var(--yeti-attention-distance)); }
	}
}

/* src/utilities/enter/enter.css */
/* Enter: the arrival. One run, on load, of an animation that starts the
   element somewhere it is not and ends it exactly where the layout already
   put it. Every keyframe list names only a from, so the to is the element's
   own computed style and nothing here has an opinion about where a thing
   belongs; that is the layout's job. backwards fill is what makes the hidden
   start safe: the first frame applies from the moment the element is painted,
   so no rule ever has to leave it at opacity: 0 on its own, and an element
   whose animation never runs is still visible rather than stranded. */
@layer yeti.utilities {
	/* The element itself, or each of its children in turn, since data-stagger
	   moves the animation down one level rather than changing what it is. */
	.enter:not([data-stagger]),
	.enter[data-stagger] > * {
		animation: yeti-enter-fade var(--yeti-enter-duration) var(--yeti-enter-ease) backwards;
	}
	.enter[data-enter="rise"]:not([data-stagger]),
	.enter[data-enter="rise"][data-stagger] > * { animation-name: yeti-enter-rise; }
	.enter[data-enter="scale"]:not([data-stagger]),
	.enter[data-enter="scale"][data-stagger] > * { animation-name: yeti-enter-scale; }

	/* Each child a step later than the last. Eight rules and a floor, rather
	   than sibling-index(), which is neither Baseline nor in WebKit, and
	   rather than a delay counted in the markup, which would put a number a
	   theme cannot reach back into the page. The floor is the ninth child's
	   delay, and every child past the ninth shares it: a stagger that keeps
	   growing turns a list of thirty into a ten-second wait, and the point of
	   the gesture is long gone by then. The floor is written first and is
	   outranked by every :nth-child() rule below it on specificity, not on
	   order. */
	.enter[data-stagger] > * { animation-delay: calc(var(--yeti-enter-delay) + 8 * var(--yeti-enter-stagger)); }
	.enter[data-stagger] > :nth-child(1) { animation-delay: var(--yeti-enter-delay); }
	.enter[data-stagger] > :nth-child(2) { animation-delay: calc(var(--yeti-enter-delay) + var(--yeti-enter-stagger)); }
	.enter[data-stagger] > :nth-child(3) { animation-delay: calc(var(--yeti-enter-delay) + 2 * var(--yeti-enter-stagger)); }
	.enter[data-stagger] > :nth-child(4) { animation-delay: calc(var(--yeti-enter-delay) + 3 * var(--yeti-enter-stagger)); }
	.enter[data-stagger] > :nth-child(5) { animation-delay: calc(var(--yeti-enter-delay) + 4 * var(--yeti-enter-stagger)); }
	.enter[data-stagger] > :nth-child(6) { animation-delay: calc(var(--yeti-enter-delay) + 5 * var(--yeti-enter-stagger)); }
	.enter[data-stagger] > :nth-child(7) { animation-delay: calc(var(--yeti-enter-delay) + 6 * var(--yeti-enter-stagger)); }
	.enter[data-stagger] > :nth-child(8) { animation-delay: calc(var(--yeti-enter-delay) + 7 * var(--yeti-enter-stagger)); }

	/* data-view hands the same animation to a scroll-driven timeline, so the
	   arrival happens where the reader is rather than all at once above the
	   fold. Two guards, and the fallback is the whole point of both.

	   @supports, because animation-timeline: view() is not Baseline. Chromium
	   and Safari have it and Firefox does not, as of this writing, and that
	   line will move. The rules above already gave every .enter a working
	   animation on load and this block only re-times it, so a reader without
	   view() sees the element arrive on load instead of on scroll, which is a
	   lesser effect and not a broken page. Setting opacity: 0 outside the
	   animation would have been the tidier way to write the waiting state and
	   would have left every one of those readers looking at nothing at all.

	   @media, because a progress-based timeline does not read
	   animation-duration: the animation is paced by the scroll, so the
	   collapsed --yeti-enter-duration that stills every other animation in
	   the framework would sail straight past this one and a reader who asked
	   for less motion would get the most motion Yeti has. Scroll-driven entry
	   is therefore only for a reader who has not asked; everyone else keeps
	   the load-timed animation, which their tokens have already collapsed to
	   nothing. */
	@supports (animation-timeline: view()) {
		@media (prefers-reduced-motion: no-preference) {
			.enter[data-view]:not([data-stagger]),
			.enter[data-view][data-stagger] > * {
				animation-timeline: view();
				/* From the moment the element's edge crosses into the
				   scrollport to the moment it is fully inside. A range that
				   ran on past that would leave a card still fading while the
				   reader was already reading it. */
				animation-range: entry 0% entry 100%;
				/* The delays above are times, and a progress timeline has no
				   use for them; each child is paced by its own crossing. Set
				   to zero so nothing reads as a stagger that is not one. */
				animation-delay: 0s;
			}
		}
	}

	@keyframes yeti-enter-fade {
		from { opacity: 0; }
	}
	/* Individual transform properties, so an element that is already rotated
	   or scaled by something else keeps that while it arrives. */
	@keyframes yeti-enter-rise {
		from { opacity: 0; translate: 0 var(--yeti-enter-distance); }
	}
	@keyframes yeti-enter-scale {
		from { opacity: 0; scale: var(--yeti-enter-scale); }
	}
}

/* src/utilities/lede/lede.css */
/* Lede: the standfirst, the sentence under a page's heading that says what the
   page is before the page begins. It is a role rather than a size, which is
   why it is a class: the alternative is a structural selector like h1 + p,
   and that encodes where the paragraph happens to sit, so wrapping the
   heading and the paragraph in a layout stops it matching. */
@layer yeti.utilities {
	.lede {
		font-size: var(--yeti-lede-size);
		line-height: var(--yeti-leading-md);
		/* Its own measure, because a lede set larger at the body's measure runs
		   to a longer line than the prose it introduces. How much shorter is a
		   judgement rather than a calculation: ch is a unit of the font, so the
		   right count depends on how far above the body the lede is set, and
		   the default here suits the default size step. A theme that moves one
		   moves the other. */
		max-inline-size: var(--yeti-lede-measure);
		text-wrap: pretty;
	}
}

/* src/utilities/lift/lift.css */
/* Lift: the element rises a little and its shadow deepens while the pointer
   is on it or a keyboard is inside it. The one piece of motion in the
   framework that is not a one-shot, which is what makes its reduced-motion
   answer different from everything else's. */
@layer yeti.utilities {
	.lift {
		/* Colour is in the list because a lifted thing is nearly always tinted
		   on the same hover, and transition is a shorthand: a consumer adding its
		   own colour transition would replace this one and stop the shadow
		   animating, which is exactly what happened the first time this was used.
		   Listing the four here means a consumer sets colours and nothing else. */
		transition:
			translate var(--yeti-lift-duration) var(--yeti-lift-ease),
			box-shadow var(--yeti-lift-duration) var(--yeti-lift-ease),
			background-color var(--yeti-lift-duration) var(--yeti-lift-ease),
			border-color var(--yeti-lift-duration) var(--yeti-lift-ease);
	}
	/* :has(:focus-visible) as well as :focus-visible, because the thing that
	   takes focus is usually inside: a card's stretched link, a button in a
	   tile. Without it the lift is a pointer-only affordance and a reader
	   tabbing through the same page never sees which card they are on. */
	.lift:hover,
	.lift:focus-visible,
	.lift:has(:focus-visible) {
		/* translate rather than transform, so a lifted element that is also
		   scaled or rotated by its own component keeps that. */
		translate: 0 calc(-1 * var(--yeti-lift-distance));
		box-shadow: var(--yeti-lift-shadow);
	}

	/* Reduced motion is handled in tokens/components.css by taking
	   --yeti-lift-distance to zero, not by collapsing a duration. Collapsing
	   the duration is the idiom everywhere else in Yeti because everywhere
	   else the animation runs once and ends: a 0.01ms run is the end state,
	   arrived at instantly, which is exactly what was asked for. A hover is
	   not that. Its end state is four pixels up, so a collapsed duration
	   still moves the element, just with the smooth part removed, and the
	   result is a box that jumps out from under the cursor on every pass and
	   drops back the moment it leaves. That is more startling than the
	   animation it replaced. With the distance at zero the element does not
	   move at all and the deeper shadow carries the whole hover, which is a
	   change of appearance rather than of position, and the transition that
	   is left has nothing to animate but the shadow. */
}

/* src/yeti.css */
/* Yeti entry point. layers.css must come first so the layer order is fixed
   before any rule is parsed. Tokens come before base, and the reset before
   the rest of base, because base reads tokens and overrides the reset. */
























































/* Utilities last, because the layer they declare is the last one in the
   cascade order and the file order should read the way the layers resolve.
   Alphabetical within the group: nothing here depends on anything else here. */
