/*
 * Quill Quest — design tokens (light scheme / default).
 *
 * Three layers, in this order, and the order is the architecture:
 *
 *   LAYER 1  primitives   raw values with no meaning attached. Named for what they ARE
 *                         (--qq-pink-700) and never referenced by component CSS.
 *   LAYER 2  semantic      roles. Named for what they DO (--qq-c-text-muted). This is the
 *                         ONLY layer tokens-dark.css remaps, which is what makes dark mode
 *                         a ~40-line override instead of a second design system.
 *   LAYER 3  component     per-primitive values, expressed in terms of layer 2.
 *
 * Component CSS may reference layer 2 and layer 3. It may not reference layer 1. That rule is
 * what lets D9a/D12 be re-decided in one file if the client rules differently.
 *
 * Every colour ratio in the comments here was computed by .dev/bin/contrast, not estimated.
 * Run it after any colour change: architecture.md §4 requires both schemes green.
 *
 * @package quill-quest
 */

:root {

	/* ==================================================================
	 * LAYER 1 — PRIMITIVES
	 * ================================================================== */

	/* ---- Brand -------------------------------------------------------
	 * #FF0090 is the brand pink, taken from the live site. It is a FILL and
	 * GRAPHIC colour in the light scheme, never small text: white on it is
	 * 3.71:1 and black on it is 5.66:1, so it fails 4.5:1 as a text colour on
	 * white and fails as a background for white text. --qq-pink-700 exists for
	 * that job (D12).
	 */
	--qq-pink-50: #FDF2F8;
	--qq-pink-100: #FCE7F3;
	--qq-pink-300: #FF4DB1;   /* 30% tint — brand pink as TEXT on dark surfaces */
	--qq-pink-500: #FF0090;   /* brand */
	--qq-pink-700: #AD0060;   /* D12 — accessible fill, white text 7.13:1 */
	--qq-pink-800: #8A004C;   /* hover/pressed under pink-700. White text 9.61:1. */

	/* Brand amber. Same caveat as the pink but stricter, and the contrast run is what
	 * settled it: #FF9C00 is 2.10:1 on white, so it fails even the 3:1 non-text bar.
	 * In the LIGHT scheme it is DECORATIVE ONLY — it may never be the sole carrier of
	 * meaning. Legal: a section-heading rule, a hairline, a brand flourish, where the
	 * adjacent text carries the meaning and WCAG 1.4.11 does not apply. Illegal: an
	 * icon you must see to understand the page, or a state indicator where the colour
	 * IS the signal. Where amber must convey something in light, use
	 * --qq-hue-amber-fill as a pill with white text (4.95:1).
	 *
	 * There is deliberately no light-scheme amber TEXT token. #9C6406 gets to 4.95:1
	 * on white but only 4.31:1 on the sunken surface and 4.21:1 on the pink band, and
	 * darkening it far enough to pass everywhere turns it brown and stops being the
	 * brand. In the DARK scheme none of this applies: amber is 9.37:1 on the canvas
	 * and is a first-class text colour there.
	 */
	--qq-amber-500: #FF9C00;

	/* ---- Neutrals, light --------------------------------------------- */
	--qq-white: #FFFFFF;
	--qq-grey-50: #F7F7F8;
	--qq-grey-100: #EFEFF1;
	--qq-grey-200: #E6E6EA;   /* decorative rules only — 1.19:1 on white */
	--qq-grey-500: #7A7A87;   /* lightest grey that clears 3:1 on EVERY light surface */
	--qq-grey-600: #656570;   /* lightest grey that clears 4.5:1 on EVERY light surface */
	--qq-grey-800: #3F3F46;
	--qq-ink: #0A0A0A;

	/* ---- Neutrals, dark ---------------------------------------------
	 * Elevation goes lighter, as dark UI conventionally does. #222228 is the
	 * lightest surface any text lands on, so it is the worst case every dark
	 * foreground below was solved against — not the canvas.
	 */
	--qq-night-900: #0B0B0D;
	--qq-night-800: #131316;
	--qq-night-700: #1A1A1E;
	--qq-night-600: #222228;
	--qq-night-500: #2C2C34;   /* decorative rules only — 1.14:1 on #222228 */
	--qq-night-400: #757582;   /* clears 3:1 on every dark surface (worst 3.48) */
	--qq-night-300: #8C8C99;   /* clears 4.5:1 on every dark surface (worst 4.77) */
	--qq-night-200: #B6B6C0;
	--qq-night-100: #E8E8ED;
	--qq-night-50: #F2F2F5;

	/* ---- Category hues ----------------------------------------------
	 * Each hue ships two values because the two schemes use categories
	 * differently, and the numbers forced it:
	 *
	 *   -fill  dark, saturated. Carries WHITE text. Light scheme pills.
	 *   -ink   light, tinted. Two jobs in dark: bare uppercase text on a dark
	 *          surface, AND the pill fill with DARK text on it.
	 *
	 * Dark mode inverts the pill rather than recolouring or dropping it, because a
	 * light -fill on a dark surface is near-invisible (green #11803A is 2.2:1 on
	 * #131316) and would also fail 1.4.11 as a control boundary. Every -ink is the
	 * hue lightened until it clears 4.5:1 on #222228 — the LIGHTEST dark surface,
	 * not the canvas — with headroom; amber and green already did at 0% tint.
	 * A uniform tint was tried first and does not work: at 35% grape is still only
	 * 4.59:1 while amber is at 9.59:1. The hues differ in luminance, so the tints
	 * have to differ too.
	 */
	--qq-hue-pink-fill: #AD0060;    /* white 7.13 */
	--qq-hue-pink-ink: #FF4DB1;     /* 5.23 on #222228 */
	--qq-hue-violet-fill: #7C3AED;  /* white 5.70 */
	--qq-hue-violet-ink: #A679F3;   /* 5.01 */
	--qq-hue-grape-fill: #6D28D9;   /* white 7.10 */
	--qq-hue-grape-ink: #A47AE7;    /* 4.91 */
	--qq-hue-amber-fill: #9C6406;   /* white 4.95 */
	--qq-hue-amber-ink: #F59E0B;    /* 7.37 */
	--qq-hue-green-fill: #11803A;   /* white 5.03 */
	--qq-hue-green-ink: #16A34A;    /* 4.80 */
	--qq-hue-plum-fill: #9333EA;    /* white 5.38 */
	--qq-hue-plum-ink: #B674F1;     /* 5.09 */

	/* ---- Overlays ----------------------------------------------------
	 * The pink veil is the brand tint over hero photography. It CANNOT carry
	 * white text by itself: composited over a white sky it lands at 2.12:1.
	 * The scrim below is what guarantees legibility; the veil only tints.
	 * See --qq-scrim-hero for where the two combine.
	 */
	--qq-veil-brand: rgb(161 0 91 / 37%);
	--qq-scrim-floor: rgb(0 0 0 / 62%);   /* white text 6.19:1 over a pure-white photo */

	/* ---- Font stacks -------------------------------------------------
	 * These are the FALLBACKS. assets/css/fonts.css sets --qq-font-serif and
	 * --qq-font-sans to the real families; it is deferred, so the critical render
	 * uses these and upgrades in place. Layer 2 references them with these as the
	 * var() fallback, which is why a deferred fonts.css cannot cause invisible text.
	 */
	--qq-stack-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
	--qq-stack-serif: Georgia, "Times New Roman", serif;
	--qq-stack-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

	/* ---- Type scale --------------------------------------------------
	 * Fluid, interpolating 320px -> 1600px. Each clamp() was solved for its two
	 * endpoints and verified at both; the inline comments are the checked values.
	 *
	 * WCAG 1.4.4 (Resize Text 200%): the vw term is always paired with a rem term,
	 * and both the floor and ceiling are rem. At 200% text zoom the rem components
	 * double, so the whole step scales. A bare vw font-size would NOT, which is the
	 * usual way fluid type fails 1.4.4.
	 *
	 * --qq-fs-base floors at 16px deliberately: below that, iOS Safari zooms on
	 * focusing a form field, which reads as a layout bug and costs CLS.
	 */
	--qq-fs-2xs: clamp(0.6875rem, 0.0781vw + 0.6719rem, 0.75rem);      /* 11 -> 12 */
	--qq-fs-xs: clamp(0.75rem, 0.0781vw + 0.7344rem, 0.8125rem);       /* 12 -> 13 */
	--qq-fs-sm: clamp(0.875rem, 0.0781vw + 0.8594rem, 0.9375rem);      /* 14 -> 15 */
	--qq-fs-base: clamp(1rem, 0.0781vw + 0.9844rem, 1.0625rem);        /* 16 -> 17 */
	--qq-fs-md: clamp(1.125rem, 0.0781vw + 1.1094rem, 1.1875rem);      /* 18 -> 19 */
	--qq-fs-lg: clamp(1.25rem, 0.1563vw + 1.2188rem, 1.375rem);        /* 20 -> 22 */
	--qq-fs-xl: clamp(1.4375rem, 0.2344vw + 1.3906rem, 1.625rem);      /* 23 -> 26 */
	--qq-fs-2xl: clamp(1.6875rem, 0.3906vw + 1.6094rem, 2rem);         /* 27 -> 32 */
	--qq-fs-3xl: clamp(1.9375rem, 0.7031vw + 1.7969rem, 2.5rem);       /* 31 -> 40 */
	--qq-fs-4xl: clamp(2.125rem, 1.4063vw + 1.8438rem, 3.25rem);       /* 34 -> 52 */
	--qq-fs-5xl: clamp(2.375rem, 2.0313vw + 1.9688rem, 4rem);          /* 38 -> 64 */

	--qq-lh-tight: 1.08;
	--qq-lh-snug: 1.22;
	--qq-lh-normal: 1.45;
	--qq-lh-relaxed: 1.68;   /* WCAG 1.4.8 wants >=1.5 for body blocks */

	--qq-tracking-tight: -0.021em;
	--qq-tracking-normal: 0;
	--qq-tracking-wide: 0.04em;
	--qq-tracking-caps: 0.09em;   /* uppercase eyebrows need it or they close up */

	/* ---- Space scale -------------------------------------------------
	 * Steps up to --qq-sp-md are FIXED, and that is a deliberate narrowing of
	 * architecture §7.4's "clamp() on every spacing step". Two reasons:
	 *   1. A fluid 4px gap interpolates to 5.7px and stops aligning with 1px rules.
	 *   2. WCAG 2.5.8 target size is padding + content. If padding is a function of
	 *      viewport width, the minimum tap target becomes unstateable — and §7.5 P10
	 *      requires stating it. Fixed small steps keep that arithmetic checkable.
	 * Section rhythm (lg and up), where nothing is measured against a hairline, is fluid.
	 */
	--qq-sp-3xs: 0.125rem;   /* 2px */
	--qq-sp-2xs: 0.25rem;    /* 4px */
	--qq-sp-xs: 0.5rem;      /* 8px */
	--qq-sp-sm: 0.75rem;     /* 12px */
	--qq-sp-md: 1rem;        /* 16px */
	--qq-sp-lg: clamp(1.25rem, 0.3125vw + 1.1875rem, 1.5rem);   /* 20 -> 24 */
	--qq-sp-xl: clamp(1.75rem, 0.625vw + 1.625rem, 2.25rem);    /* 28 -> 36 */
	--qq-sp-2xl: clamp(2.25rem, 1.5625vw + 1.9375rem, 3.5rem);  /* 36 -> 56 */
	--qq-sp-3xl: clamp(3rem, 2.5vw + 2.5rem, 5rem);             /* 48 -> 80 */
	--qq-sp-4xl: clamp(4rem, 3.75vw + 3.25rem, 7rem);           /* 64 -> 112 */
	--qq-sp-5xl: clamp(5rem, 6.25vw + 3.75rem, 10rem);          /* 80 -> 160 */

	/* ---- Radii, borders, elevation ----------------------------------- */
	--qq-radius-none: 0;
	--qq-radius-xs: 2px;
	--qq-radius-sm: 4px;
	--qq-radius-md: 8px;
	--qq-radius-lg: 14px;
	--qq-radius-pill: 999px;

	--qq-bw-hair: 1px;
	--qq-bw-thin: 2px;
	--qq-bw-thick: 3px;
	--qq-bw-rule: 4px;   /* the section-heading underline */

	--qq-shadow-none: none;
	--qq-shadow-sm: 0 1px 2px rgb(10 10 10 / 6%), 0 1px 3px rgb(10 10 10 / 8%);
	--qq-shadow-md: 0 2px 6px rgb(10 10 10 / 7%), 0 6px 16px rgb(10 10 10 / 8%);
	--qq-shadow-lg: 0 8px 24px rgb(10 10 10 / 10%), 0 18px 48px rgb(10 10 10 / 10%);

	/* ---- Z-index ----------------------------------------------------
	 * Sparse and named. Nothing in the theme may use a raw z-index.
	 */
	--qq-z-base: 0;
	--qq-z-raised: 10;
	--qq-z-sticky: 100;
	--qq-z-header: 200;
	--qq-z-drawer: 300;
	--qq-z-overlay: 400;
	--qq-z-toast: 500;

	/* ---- Motion -----------------------------------------------------
	 * Zeroed under prefers-reduced-motion at the foot of this file, so no component
	 * needs its own media query. WCAG 2.3.3.
	 */
	--qq-dur-instant: 0ms;
	--qq-dur-fast: 120ms;
	--qq-dur-base: 200ms;
	--qq-dur-slow: 320ms;
	--qq-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
	--qq-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

	/* ---- Breakpoints -------------------------------------------------
	 * Documentation and JS only. Custom properties cannot be used in a media
	 * query condition, so every stylesheet hard-codes these same numbers;
	 * .dev/bin/tokens-check verifies they agree and that no max-width appears.
	 * min-width only, per architecture §7.4. 320px is the base, and is also the
	 * WCAG 1.4.10 Reflow floor.
	 */
	--qq-bp-sm: 480px;
	--qq-bp-md: 768px;
	--qq-bp-lg: 1024px;
	--qq-bp-xl: 1280px;
	--qq-bp-2xl: 1600px;

	/* ---- Measure and containers -------------------------------------- */
	--qq-measure-text: 68ch;    /* article column — 1.4.8 wants <=80ch */
	--qq-measure-narrow: 44ch;
	--qq-w-content: 1200px;
	--qq-w-wide: 1440px;
	--qq-gutter: var(--qq-sp-md);

	/* ---- Ad reservations ---------------------------------------------
	 * §7.5 P1: reserve min-height, never aspect-ratio — a responsive unit's
	 * returned creative height is not a function of its width, so aspect-ratio
	 * reserves the wrong box and shifts anyway.
	 *
	 * FIXED px, never clamp(): a fluid reservation guarantees nothing, and the
	 * CLS budget in §7.2 is only verifiable if the reserved box is exact.
	 * Mobile-first base values; assets/css/ad-slots.css raises them at min-width
	 * breakpoints, which is the one place that override belongs (P8: global layer).
	 */
	--qq-ad-h-banner: 100px;    /* 320x100 mobile -> 90px desktop leaderboard */
	--qq-ad-h-inline: 250px;    /* 300x250 */
	--qq-ad-h-sidebar: 600px;   /* 300x600 — P9: never display:none, reflow instead */
	--qq-ad-h-footer: 250px;


	/* ==================================================================
	 * LAYER 2 — SEMANTIC ROLES
	 *
	 * tokens-dark.css overrides THIS BLOCK ONLY. If a value belongs in dark mode
	 * and is not here, it is in the wrong layer.
	 * ================================================================== */

	/* ---- Surfaces ---------------------------------------------------- */
	--qq-c-canvas: var(--qq-white);
	--qq-c-surface: var(--qq-grey-50);
	--qq-c-raised: var(--qq-white);
	--qq-c-sunken: var(--qq-grey-100);
	--qq-c-wash: var(--qq-pink-50);
	--qq-c-wash-strong: var(--qq-pink-100);

	/* ---- Text -------------------------------------------------------
	 * Each of these clears its ratio on canvas, surface, sunken AND both washes.
	 * --qq-c-text-muted is #656570 rather than the more obvious #6B6B76 because
	 * #6B6B76 lands at 4.48:1 on the pink-100 newsletter band — a 0.02 miss that
	 * only a computed check would ever have caught.
	 */
	--qq-c-text: var(--qq-ink);
	--qq-c-text-secondary: var(--qq-grey-800);
	--qq-c-text-muted: var(--qq-grey-600);
	--qq-c-text-on-brand: var(--qq-white);
	--qq-c-text-on-accent: var(--qq-white);
	--qq-c-text-on-scrim: var(--qq-white);

	/* ---- Borders ----------------------------------------------------
	 * Two tokens, one obligation each, and they resolve to the SAME primitive on
	 * purpose. WCAG 1.4.11 needs 3:1 for anything that identifies a control, and
	 * there is no grey between "visible divider" and "control boundary" that
	 * clears 3:1 on every light surface — #8C8C99 looked right and fails at
	 * 2.82:1 on pink-100. Kept as two names so the obligation stays legible.
	 *
	 * --qq-c-border is the exception: decorative rules ONLY, 1.19:1 on white. It
	 * must never be the sole boundary of an input, button, or checkbox.
	 */
	--qq-c-border: var(--qq-grey-200);
	--qq-c-border-strong: var(--qq-grey-500);
	--qq-c-field-border: var(--qq-grey-500);

	/* ---- Brand roles ------------------------------------------------ */
	--qq-c-brand: var(--qq-pink-500);          /* graphics/rules. NOT text on light. */
	--qq-c-brand-ink: var(--qq-pink-700);      /* brand AS text: 7.13:1 on white */
	--qq-c-brand-fill: var(--qq-pink-700);     /* background under --qq-c-text-on-brand */
	--qq-c-highlight: var(--qq-amber-500);     /* graphics/rules. NOT text on light. */

	/* Hover/active background for anything filled with --qq-c-brand-fill.
	 *
	 * A semantic token rather than a per-component darken() for one reason: the
	 * text ON it is --qq-c-text-on-brand, so the hover state is a contrast pair
	 * in its own right and has to be declared in .dev/contrast-pairs.tsv. A
	 * component computing its own hover colour is a pair the gate cannot see —
	 * which is the failure mode the whole token layer exists to prevent.
	 *
	 * The two schemes move in OPPOSITE directions, and that is not a mistake.
	 * Light: brand-fill is pink-700 under white text, so hover goes DARKER
	 * (pink-800, 9.61:1). Dark: brand-fill is pink-500 under dark ink, so hover
	 * goes LIGHTER (pink-300). "Hover = darker" is a light-mode habit, not a
	 * rule; in dark mode it would reduce contrast against the ink on top.
	 */
	--qq-c-brand-fill-hover: var(--qq-pink-800);

	/* ---- Links ------------------------------------------------------
	 * Visited is grape, which is a real hue difference rather than a lightness
	 * one, so it survives greyscale and most colour-vision deficiencies. Both
	 * clear 4.5:1 on every light surface.
	 */
	--qq-c-link: var(--qq-pink-700);
	--qq-c-link-hover: var(--qq-ink);
	--qq-c-link-visited: var(--qq-hue-grape-fill);

	/* ---- Focus ------------------------------------------------------
	 * The two-tone "sandwich": an inner ring plus an outer halo. Painted as
	 * box-shadow (halo) under outline (ring), because outline paints above an
	 * outer box-shadow — giving halo 0-2px, ring 2-4px, halo 4-6px.
	 *
	 * The two contrast with EACH OTHER at 19.80:1, which is what makes it work on
	 * an unknown backdrop: on any surface the theme can produce, at least one of
	 * the two clears 3:1. Verified across all 11 surfaces in both schemes,
	 * including the awkward one (#AD0060, where the ring is only 2.78:1 but the
	 * halo is 7.13:1). WCAG 1.4.11 + 2.4.11.
	 */
	--qq-c-focus-ring: var(--qq-ink);
	--qq-c-focus-halo: var(--qq-white);

	/* ---- Category accent defaults -----------------------------------
	 * Phase 6 term meta overrides these per category. These are the fallbacks,
	 * and the fallbacks must be accessible or an unconfigured category is a
	 * violation shipped by default.
	 */
	--qq-c-accent-fill: var(--qq-hue-pink-fill);
	--qq-c-accent-ink: var(--qq-hue-pink-fill);

	/* ---- Overlays ----------------------------------------------------
	 * The scrim reaches its full 62% by 32% of the way up, so any text placed in
	 * the bottom third sits on a backdrop that carries white at >=6.19:1 even
	 * over a pure-white photo. The veil rides underneath purely for brand tint.
	 * Order matters: photo, then veil, then scrim.
	 */
	--qq-c-veil: var(--qq-veil-brand);
	--qq-c-scrim-floor: var(--qq-scrim-floor);
	--qq-scrim-hero: linear-gradient(
		to top,
		rgb(0 0 0 / 78%) 0%,
		rgb(0 0 0 / 62%) 32%,
		rgb(0 0 0 / 26%) 64%,
		rgb(0 0 0 / 0%) 100%
	);

	/* ---- Ad wells ----------------------------------------------------
	 * §7.5 P3: background-COLOR only. A background-image on a reservation box is
	 * an LCP candidate and can win LCP, hiding a slow hero behind a fast grey box.
	 */
	--qq-c-ad-well: var(--qq-grey-50);
	--qq-c-ad-label: var(--qq-grey-600);

	/* ---- Semantic type roles ---------------------------------------- */
	--qq-c-font-serif: var(--qq-font-serif, var(--qq-stack-serif));
	--qq-c-font-sans: var(--qq-font-sans, var(--qq-stack-sans));
	--qq-c-font-mono: var(--qq-stack-mono);

	/* ---- Elevation roles --------------------------------------------
	 * Elevation is a semantic role, not a primitive, even though the shadow
	 * values themselves are primitives. A drop shadow tuned for white does almost
	 * nothing on #0B0B0D — dark UI separates planes with a lighter surface plus a
	 * hairline ring instead. Putting the ROLE here is what keeps the "dark remaps
	 * layer 2 only" rule true; if these lived in layer 1, tokens-dark.css would
	 * have to reach into layer 1 and the rule would already be broken.
	 */
	--qq-c-elevation-flat: var(--qq-shadow-none);
	--qq-c-elevation-raised: var(--qq-shadow-sm);
	--qq-c-elevation-float: var(--qq-shadow-md);
	--qq-c-elevation-overlay: var(--qq-shadow-lg);


	/* ==================================================================
	 * LAYER 3 — COMPONENT TOKENS
	 * ================================================================== */

	/* Header */
	--qq-header-bg: var(--qq-c-canvas);
	--qq-header-border: var(--qq-c-border);
	--qq-header-h: 60px;

	/* WCAG 2.4.11 Focus Not Obscured (Minimum), AA.
	 *
	 * A sticky header hides whatever the browser scrolls to. Tab to a link just
	 * below the fold and it lands *under* the header, so focus is entirely
	 * invisible — which fails 2.4.11 even though the ring itself is drawn
	 * perfectly. The focus styles cannot fix this; only the scroll container can.
	 * scroll-padding-block-start is the whole mechanism, and it repairs in-page
	 * anchors, comment permalinks and the skip link in the same stroke.
	 *
	 * Derived from --qq-header-h rather than restated, because the two drifting
	 * apart is exactly how this regresses: raise the header at a breakpoint and
	 * the padding follows on its own. The extra sp-xs (8px) clears the 6px focus
	 * halo of the header border instead of letting them touch.
	 *
	 * Consumed ONCE, on the scroll container. Never per-component.
	 */
	--qq-scroll-pad: calc(var(--qq-header-h) + var(--qq-sp-xs));

	/* Cards */
	--qq-card-bg: var(--qq-c-raised);
	--qq-card-border: var(--qq-c-border);
	--qq-card-radius: var(--qq-radius-sm);
	--qq-card-shadow: var(--qq-c-elevation-flat);
	--qq-card-pad: var(--qq-sp-md);
	--qq-card-title-fs: var(--qq-fs-lg);
	--qq-card-title-lh: var(--qq-lh-snug);

	/* Eyebrow / category label */
	--qq-eyebrow-fs: var(--qq-fs-2xs);
	--qq-eyebrow-tracking: var(--qq-tracking-caps);
	--qq-eyebrow-pad-i: var(--qq-sp-xs);
	--qq-eyebrow-pad-b: var(--qq-sp-3xs);

	/* Article body */
	--qq-prose-fs: var(--qq-fs-md);
	--qq-prose-lh: var(--qq-lh-relaxed);
	--qq-prose-measure: var(--qq-measure-text);
	--qq-prose-space: var(--qq-sp-lg);

	/* Controls — 2.5.8 target size.
	 * 44px is the floor here, not 24px. 2.5.8 AA requires 24x24 with spacing
	 * exceptions; 44px is 2.5.5 AAA and is what a thumb actually needs on the
	 * 320px layout. Fixed, so the arithmetic in §7.5 P10 stays checkable.
	 *
	 * Exceeding the AA minimum is not the same as DEFINING it. 44px comes from
	 * native platform guidance; web conformance remains the 24x24 rule plus its
	 * exceptions. Worth stating because the reverse claim — "44px, therefore
	 * 2.5.8 passes" — is a conformance error even when the number is right.
	 *
	 * DOES NOT APPLY to links inside running prose. 2.5.8 exempts targets in a
	 * sentence or block of text, and padding inline links to 44px would wreck the
	 * line rhythm of every article on the site — the exception exists for exactly
	 * this case. Standalone controls (pagination cells, share icons, category
	 * pills, drawer toggle, scheme toggle) take the 44px floor; inline prose links
	 * take the exception, deliberately, and are never padded to meet it.
	 */
	--qq-control-min: 44px;
	--qq-control-pad-i: var(--qq-sp-md);
	--qq-control-radius: var(--qq-radius-sm);
	--qq-control-fs: var(--qq-fs-sm);

	/* Icons — see assets/icons/sprite.svg.
	 *
	 * --qq-icon-sw HAS TO LIVE IN CSS, and that is a browser constraint rather than a
	 * preference. The sprite's <symbol> elements deliberately do NOT carry a
	 * stroke-width attribute: a presentation attribute inside a <use> shadow tree is a
	 * DECLARED value, no CSS selector can reach in to override it, and a declared value
	 * always beats an inherited one. Put stroke-width on the symbol and the icon's weight
	 * is frozen for the life of the theme. Leave it off and it inherits from the
	 * referencing element, where this token controls it. Verified in a browser, three
	 * ways — the assumption that "CSS beats presentation attributes" is only true when
	 * the rule matches the element, which here it cannot.
	 *
	 * Consequence worth knowing: if the icon component CSS ever fails to load, stroke
	 * width falls back to the CSS initial value of 1px. Icons render thin, not invisible.
	 *
	 * Optical sizing, not one value. A 1.75 stroke on the 24 grid is ~1.2px at 16px and
	 * ~3.5px at 48px, so the big display icons need a lighter relative weight or they
	 * turn into slabs.
	 */
	--qq-icon-sw: 1.75;
	--qq-icon-sw-lg: 1.5;   /* >=32px display sizes */
	--qq-icon-sm: 16px;
	--qq-icon-md: 20px;
	--qq-icon-lg: 24px;

	/* Focus geometry — see --qq-c-focus-ring for why it is built this way. */
	--qq-focus-ring-w: 2px;
	--qq-focus-offset: 2px;
	--qq-focus-halo-w: 6px;

	/* Section heading rule */
	--qq-section-rule-h: var(--qq-bw-rule);
	--qq-section-rule-color: var(--qq-c-brand);
	--qq-section-fs: var(--qq-fs-xl);
}

/*
 * WCAG 2.3.3 Animation from Interactions. Zeroed centrally so no component needs
 * its own media query, and so a component author cannot forget.
 *
 * 0.01ms rather than 0: transitionend/animationend still fire, so JS that waits
 * for them does not hang. A literal 0s silently breaks those listeners.
 */
@media ( prefers-reduced-motion: reduce ) {

	:root {
		--qq-dur-fast: 0.01ms;
		--qq-dur-base: 0.01ms;
		--qq-dur-slow: 0.01ms;
	}
}
