/*
CHLORIDE: a no-build CUBE CSS boilerplate with shadcn/ui-styled design tokens.
One plain stylesheet, no cascade-layer wrappers. Sections run reset -> global ->
compositions -> blocks -> utilities; selectors stay flat and low-specificity, so
plain source order sets the CUBE priority (later sections win ties; utilities come
last). The section markers below show the original groupings. Generate fluid
clamp() values with tools/fluid-clamp.mjs.
*/

/* ===== global/reset.css ===== */
/* Modern reset: https://piccalil.li/blog/a-more-modern-css-reset/ */

/* Box sizing rules */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Opt in to system light/dark. The dark palette lives in a
   prefers-color-scheme block in tokens.css; this declaration also makes native
   form controls and scrollbars follow the user's scheme. No toggle; the OS decides. */
:root {
  color-scheme: light dark;
}

/* Prevent font size inflation */
html {
  -moz-text-size-adjust: none;
  -webkit-text-size-adjust: none;
  text-size-adjust: none;
}

/* Remove default margin in favour of better control in authored CSS */
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
figure,
blockquote,
dl,
dd {
  margin-block: 0;
}

/* Remove list styles on ul, ol elements with a list role, which suggests default
   styling will be removed. (Andy Bell's reset, verbatim, just the markers;
   default spacing for layout lists is handled in the Lists section below.) */
ul[role='list'],
ol[role='list'] {
  list-style: none;
}

/* Set core body defaults */
body {
  min-height: 100svh;
  line-height: 1.5;
}

/* Set shorter line heights on headings and interactive elements */
h1,
h2,
h3,
h4,
button,
input,
label {
  line-height: 1.1;
}

/* Balance text wrapping on headings */
h1,
h2,
h3,
h4 {
  text-wrap: balance;
}

/* A elements that don't have a class get default styles */
a:not([class]) {
  text-decoration-skip-ink: auto;
}

/* Make images easier to work with */
img,
picture {
  max-width: 100%;
  display: block;
}

/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
  font: inherit;
}

/* Make sure textareas without a rows attribute are not tiny */
textarea:not([rows]) {
  min-height: 10em;
}

/* Anything that has been anchored to should have extra scroll margin */
:target {
  scroll-margin-block: 5lh;
}

/* Honour a request for less motion: kill transitions, animations and smooth
   scrolling for anyone who asks for it (Andy Bell's reset idiom). */
@media (prefers-reduced-motion: reduce) {
  html:focus-within {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ===== global/tokens.css ===== */
/*
DESIGN TOKENS
Look: shadcn/ui's default theme, a near-neutral grayscale with a single blue
accent for interaction (https://ui.shadcn.com/themes). Type and space are a
fluid CUBE/Utopia scale (https://utopia.fyi). shadcn's own values are OKLCH;
they're transcribed here to rgb()/hex so the palette degrades on pre-2023
engines, matching the no-modern-features stance below.

Colors are semantic only. Light values live on :root; the dark theme overrides
them in a single @media (prefers-color-scheme: dark) block at the end of this
file. This degrades gracefully, engines without prefers-color-scheme (pre-2019)
simply get the light theme, and avoids the light-dark() function, which is only
Baseline 2024 and fails hard (invalid at computed-value time) on older browsers.
`color-scheme: light dark` on :root (set in reset.css) still makes native form
controls and scrollbars follow the OS.

Fluid scales (clamp() between a 360px and 1240px viewport = 22.5→77.5rem):
  Type: a full Utopia scale; every step interpolates between its mobile and
  desktop size. Mobile config base 16px / ratio 1.2; desktop base 18px / 1.25.
  Space: a full Utopia space scale (base 14→16px × each multiplier); the page
  gutter, region padding and a panel-padding pair travel further as one-up pairs.
  slope = (max − min) / 55 → vw term; intercept = min − slope × 22.5 → rem.
Regenerate any value with tools/fluid-clamp.mjs.
*/

:root {
  /* ------------------------------------------------------------------
     Color: backgrounds and surfaces. Light theme; dark overrides at the
     foot of this file. Dark mode has no shadows; elevation = lighter fills.
  ------------------------------------------------------------------ */
  --color-bg: #fff;
  --color-bg-raised: #fff;
  --color-bg-overlay: #fff;
  --color-bg-sunken: #f5f5f5; /* shadcn muted (neutral-100) */

  /* Fixed surfaces, deliberately NOT flipped in the dark block: the scrim is a
     dark wash over content in either theme; the image mat stays light because
     image SVGs often carry dark paint; the toggle thumb is white in both. */
  --color-scrim: rgb(0 0 0 / 0.4);
  --color-image-mat: #f5f5f5;
  --color-toggle-thumb: #fff;

  /* Color: text. Neutral black alphas so they sit correctly on any surface;
     the alphas resolve to shadcn's foreground / muted-foreground grays. */
  --color-text: rgb(0 0 0 / 0.95); /* ≈ shadcn foreground */
  --color-text-weak: rgb(0 0 0 / 0.55); /* ≈ shadcn muted-foreground */
  --color-text-weaker: rgb(0 0 0 / 0.36); /* placeholders, faint hints */
  --color-text-on-fill: #fff;

  /* Color: brand. The one accent hue (shadcn blue): actions, links and focus
     only, never decoration. brand-hover is the hover fill for a filled brand
     surface (the primary button); secondary/ghost use the brand wash and the
     neutral fill instead. */
  /* readable accent (links, focus); was oklch(0.488 0.243 264.376) */
  --color-brand: rgb(20 71 230);
  --color-brand-hover: rgb(8 52 205);
  --color-brand-bg: rgb(20 71 230 / 0.08);
  --color-brand-bg-hover: rgb(20 71 230 / 0.12); /* tonal/outline-brand button hover */
  --color-brand-stroke: rgb(20 71 230 / 0.25);

  /* The primary BUTTON fill = shadcn --primary exactly. Same as --color-brand in
     light; in dark it's a deeper blue than the (readable) link accent. */
  --color-primary: rgb(20 71 230);
  --color-primary-hover: rgb(8 52 205);
  --color-primary-press: rgb(6 42 166 / 0.99); /* :active: 80% primary-hover + 20% text */

  /* Color: strokes and fills (neutral black alphas; ≈ shadcn border/muted) */
  --color-stroke: rgb(0 0 0 / 0.1); /* ≈ shadcn border (neutral-200) */
  --color-stroke-strong: rgb(0 0 0 / 0.36);
  --color-fill: rgb(0 0 0 / 0.04);
  --color-fill-weak: rgb(0 0 0 / 0.02);
  --color-fill-press: rgb(0 0 0 / 0.08);
  --color-disabled: rgb(0 0 0 / 0.1);

  /* Muted SOLID surfaces (shadcn secondary/muted/input). Distinct from the alpha
     fills above; these read as a clear panel, esp. in dark mode. */
  --color-muted: #f5f5f5; /* secondary button, segmented track */
  --color-muted-hover: #ececec;
  --color-muted-raised: #fff; /* a pill lifted above a muted track */
  --color-field: transparent; /* input fill (gains a subtle tint in dark) */
  --segment-active-border: transparent; /* segmented active pill: no border in light… */
  /* outline button fill: opaque so it reads on tinted surfaces */
  --color-outline-bg: var(--color-bg);
  --color-outline-hover: var(--color-fill); /* outline hover = shadcn bg-accent (#f5f5f5) */
  /* hover wash on an inverted surface (bg at 0.2) */
  --color-overlay-invert: rgb(255 255 255 / 0.2);

  /* Color: focus ring. The brand accent (brighter than --color-primary in dark,
     for a focus indicator that clears 3:1 contrast comfortably). */
  --color-focus: var(--color-brand);

  /* Color: status tones. shadcn ships only `destructive` (red); success,
     warning and info are sourced from the Tailwind palette (open, MIT) and
     tuned to read calmly beside the neutral grays. */
  --color-error: rgb(220 38 38); /* red-600 (= shadcn destructive) */
  --color-error-bg: rgb(220 38 38 / 0.05);
  --color-error-stroke: rgb(220 38 38 / 0.18);
  --color-error-strong: rgb(220 38 38 / 0.8); /* errored-field border (error at 0.8) */

  --color-success: rgb(22 163 74); /* green-600 */
  --color-success-bg: rgb(22 163 74 / 0.05);
  --color-success-stroke: rgb(22 163 74 / 0.2);

  --color-warning: rgb(217 119 6); /* amber-600 */
  --color-warning-bg: rgb(217 119 6 / 0.06);
  --color-warning-stroke: rgb(217 119 6 / 0.2);

  --color-info: rgb(2 132 199); /* sky-600 */
  --color-info-bg: rgb(2 132 199 / 0.05);
  --color-info-stroke: rgb(2 132 199 / 0.2);

  /* Status FILLS: solid surfaces under white text (the badge, the destructive
     button). Deliberately NOT flipped in dark: the dark theme's 400-series
     tones above are tuned as text on dark surfaces and are far too light to
     sit under near-white text (≈1.5–2.7:1 contrast). Each fill is picked so
     the white label clears 4.5:1 in both themes; success/warning/info need
     the 700 step (their 600s sit at ~3.2–4.1:1); red-600 already clears it. */
  --color-error-fill: rgb(220 38 38); /* red-600 */
  --color-error-fill-hover: rgb(185 28 28); /* red-700 */
  --color-error-fill-press: rgb(149 23 23 / 0.99); /* :active: 80% fill-hover + 20% text */
  --color-success-fill: rgb(21 128 61); /* green-700 */
  --color-warning-fill: rgb(180 83 9); /* amber-700 */
  --color-info-fill: rgb(3 105 161); /* sky-700 */
  --color-neutral-fill: rgb(82 82 82); /* neutral-600, a quiet grey count */

  /* ------------------------------------------------------------------
     Elevation. Shadows fade to nothing in dark mode (see override below),
     where the lighter surface colors carry the elevation instead.
  ------------------------------------------------------------------ */
  --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); /* shadcn shadow-xs: buttons, inputs */
  --shadow-sm:
    0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); /* shadcn shadow-sm: cards */
  --shadow-md:
    0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); /* shadcn shadow-md: menus, popovers */
  --shadow-overlay: 0 20px 24px -4px rgb(0 0 0 / 0.08), 0 8px 8px -4px rgb(0 0 0 / 0.04);

  /* ------------------------------------------------------------------
     Typography. System stack on purpose: zero font-load latency.
     A full Utopia fluid type scale (utopia.fyi); every step interpolates
     across a 360→1240px viewport. Mobile config: base 16px, ratio 1.2;
     desktop config: base 18px, ratio 1.25, a calm modular scale tuned for
     readability: body never drops below 16 (16→18) and help/label text clears
     ~13→14. Names match Utopia's output (--step--1 … --step-5) so it's
     portable/regenerable. Regenerate any value with tools/fluid-clamp.mjs.
  ------------------------------------------------------------------ */
  --font-base: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  /* Heading face: defaults to the body font; repoint this one token to give
     headings a distinct display typeface without touching any block. */
  --font-display: var(--font-base);

  --step--1: clamp(0.8333rem, 0.806rem + 0.1213vw, 0.9rem); /* 13.3→14.4 tiny, eyebrow */
  --step-0: clamp(1rem, 0.949rem + 0.2273vw, 1.125rem); /* 16→18     body */
  --step-1: clamp(1.2rem, 1.116rem + 0.3751vw, 1.4063rem); /* 19.2→22.5 h4 */
  --step-2: clamp(1.44rem, 1.31rem + 0.5778vw, 1.7578rem); /* 23→28.1   h3 */
  --step-3: clamp(1.728rem, 1.536rem + 0.8533vw, 2.1973rem); /* 27.6→35.2 h2 */
  --step-4: clamp(2.0736rem, 1.798rem + 1.2236vw, 2.7466rem); /* 33.2→44   h1 */
  --step-5: clamp(2.4883rem, 2.102rem + 1.718vw, 3.4332rem); /* 39.8→54.9 display */

  --leading-fine: 1.2; /* display, h1 */
  --leading-snug: 1.3; /* h2, h3 */
  --leading-mid: 1.4; /* h4, tiny */
  --leading-standard: 1.5; /* body */

  --weight-normal: 400; /* body, nav links, legend, checkbox/radio choices */
  --weight-medium: 500; /* buttons, form labels, badges, tags, segments, table headers (shadcn font-medium) */
  --weight-semibold: 600; /* eyebrow (and the default heading weight) */
  --weight-bold: 700; /* a lone accent: ordered-list markers + the brand wordmark */
  /* Heading weight: one knob for all headings; flip to --weight-bold for a
     heavier display voice without editing the heading rules. */
  --weight-heading: var(--weight-semibold);

  --tracking-caps: 0.14em; /* eyebrow / uppercase labels */

  /* ------------------------------------------------------------------
     Space. A full Utopia fluid space scale (utopia.fyi); base 14→16px
     times each multiplier (0.25 … 8), interpolating across 360→1240px. The
     desktop end lands on a clean 4px grid (4/8/12…128); it only steps down on
     smaller screens. Names match Utopia's output (--space-3xs … --space-4xl).
     Regenerate with tools/fluid-clamp.mjs <minPx> <maxPx>.
  ------------------------------------------------------------------ */
  --space-3xs: clamp(0.2188rem, 0.206rem + 0.0567vw, 0.25rem); /* 3.5→4 */
  --space-2xs: clamp(0.4375rem, 0.412rem + 0.1136vw, 0.5rem); /* 7→8 */
  --space-xs: clamp(0.6563rem, 0.618rem + 0.1704vw, 0.75rem); /* 10.5→12 */
  --space-s: clamp(0.875rem, 0.824rem + 0.2273vw, 1rem); /* 14→16 */
  --space-m: clamp(1.3125rem, 1.236rem + 0.3409vw, 1.5rem); /* 21→24 */
  --space-l: clamp(1.75rem, 1.648rem + 0.4545vw, 2rem); /* 28→32 */
  --space-xl: clamp(2.625rem, 2.472rem + 0.6818vw, 3rem); /* 42→48 */
  --space-2xl: clamp(3.5rem, 3.295rem + 0.9091vw, 4rem); /* 56→64 */
  --space-3xl: clamp(5.25rem, 4.943rem + 1.3636vw, 6rem); /* 84→96 */
  --space-4xl: clamp(7rem, 6.591rem + 1.8182vw, 8rem); /* 112→128 */

  /* One-up Utopia pairs; these travel further than a single step, for the
     page gutter, section padding and comfortable panel padding. Custom pairs
     in Utopia's terms: a wider min→max range than the adjacent-step defaults. */
  --space-s-l: clamp(1rem, 0.591rem + 1.8182vw, 2rem); /* 16→32 */
  /* 24→48: panel padding, e.g. a focused card's inline padding via --card-padding */
  --space-m-xl: clamp(1.5rem, 0.886rem + 2.7273vw, 3rem);
  /* Page side padding. Named distinctly (not --gutter) so it isn't inherited
     into the compositions' own `var(--gutter, …)`, which would silently override
     their per-layout fallback gutters. */
  --page-gutter: var(--space-s-l);
  --region-space: clamp(2rem, -0.455rem + 10.9091vw, 8rem); /* 32→128 */

  /* ------------------------------------------------------------------
     Radii. shadcn's tight scale (base --radius 0.625rem ≈ 10px): buttons and
     inputs s, dialogs and alerts m, cards l.
  ------------------------------------------------------------------ */
  --radius-xs: 0.375rem; /* 6 */
  --radius-s: 0.5rem; /* 8: buttons, inputs */
  --radius-m: 0.625rem; /* 10: dialogs, alerts */
  --radius-l: 0.875rem; /* 14: cards */
  --radius-pill: 100vmax;

  /* Motion. Restrained: no bounce, no spring, no scale. */
  --ease: cubic-bezier(0.2, 0, 0, 1);
  --transition-fast: 120ms var(--ease);
  --transition-base: 180ms var(--ease);

  /* Focus: a solid brand outline on every focusable element. */
  --focus-ring: 2px solid var(--color-focus);
  --focus-ring-offset: 2px;

  /* Controls: one shared height every control aligns to, in rem (so it scales
     with the browser font / zoom) and decoupled from text size, so a 16px input
     and a 14px button still line up. */
  --control-h: 2.25rem; /* 36 (h-9) */
  --control-h-sm: 2rem; /* 32 (h-8) */
  --control-h-lg: 2.5rem; /* 40 (h-10) */
  --text-input: 1rem; /* form fields: 16 on phones (iOS zoom), 14 from md; see below */

  /* Layout */
  --wrapper-max: 90rem; /* 1440 */
  --wrapper-max-narrow: 37.5rem; /* 600: forms and reading width */

  /* Convenience strokes */
  --stroke: 1px solid var(--color-stroke);
  --stroke-strong: 1px solid var(--color-stroke-strong);
}

/* Form fields drop to 14px once there's room; on phones they stay 16px so iOS
   Safari doesn't zoom the page when a field is focused. Buttons stay 14px
   throughout (they never trigger the zoom). Mirrors shadcn's text-base md:text-sm. */
@media (width >= 48rem) {
  :root {
    --text-input: 0.875rem;
  }
}

/*
DARK THEME
Overrides only the color and elevation tokens; everything else (type, space,
radii, motion, layout) is theme-agnostic and stays on :root above. Driven by
the OS via prefers-color-scheme; browsers without it (pre-2019) keep the light
theme. Dark elevation comes from lighter surfaces, so shadows are removed.
*/
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0a0a0a; /* shadcn background (neutral-950) */
    --color-bg-raised: #171717; /* card (neutral-900) */
    --color-bg-overlay: #262626; /* popover, one step lighter (neutral-800) */
    --color-bg-sunken: #000;

    --color-text: rgb(255 255 255 / 0.95);
    --color-text-weak: rgb(255 255 255 / 0.62);
    --color-text-weaker: rgb(255 255 255 / 0.45);
    --color-text-on-fill: #fafafa; /* near-white text on the blue button; was oklch(0.985 0 0) */

    /* readable accent for dark (links, focus); was oklch(0.623 0.214 259.815), sRGB-clamped */
    --color-brand: rgb(43 127 255);
    --color-brand-hover: rgb(84 152 255);
    --color-brand-bg: rgb(43 127 255 / 0.14);
    --color-brand-bg-hover: rgb(43 127 255 / 0.2); /* tonal/outline-brand button hover */
    --color-brand-stroke: rgb(43 127 255 / 0.3);

    --color-primary: rgb(25 60 184); /* shadcn dark --primary (the deeper button blue) */
    --color-primary-hover: rgb(36 76 209);
    --color-primary-press: rgb(78 110 218 / 0.99); /* :active: 80% primary-hover + 20% text */

    --color-stroke: rgb(255 255 255 / 0.1); /* ≈ shadcn dark border (white/10%) */
    --color-stroke-strong: rgb(255 255 255 / 0.4);
    --color-fill: rgb(255 255 255 / 0.06);
    --color-fill-weak: rgb(255 255 255 / 0.03);
    --color-fill-press: rgb(255 255 255 / 0.1);
    --color-disabled: rgb(255 255 255 / 0.1);

    --color-muted: #262626; /* neutral-800, clearly lighter than the page */
    --color-muted-hover: #2e2e2e;
    --color-muted-raised: #353535; /* the lifted pill on a muted track */
    --color-field: rgb(255 255 255 / 0.04);
    --segment-active-border: var(--color-stroke); /* …but a border in dark (shadcn) */
    /* a fill lighter than the card, not the page bg (shadcn bg-input/30) */
    --color-outline-bg: rgb(255 255 255 / 0.05);
    /* a clear hover step (~#212121, matching the preset) */
    --color-outline-hover: var(--color-fill-press);
    --color-overlay-invert: rgb(10 10 10 / 0.2); /* hover wash on an inverted surface (bg at 0.2) */

    --color-focus: var(--color-brand);

    --color-error: rgb(248 113 113); /* red-400 */
    --color-error-bg: rgb(248 113 113 / 0.1);
    --color-error-stroke: rgb(248 113 113 / 0.2);
    --color-error-strong: rgb(248 113 113 / 0.8); /* errored-field border (error at 0.8) */

    --color-success: rgb(74 222 128); /* green-400 */
    --color-success-bg: rgb(74 222 128 / 0.1);
    --color-success-stroke: rgb(74 222 128 / 0.2);

    --color-warning: rgb(251 191 36); /* amber-400 */
    --color-warning-bg: rgb(251 191 36 / 0.1);
    --color-warning-stroke: rgb(251 191 36 / 0.2);

    --color-info: rgb(56 189 248); /* sky-400 */
    --color-info-bg: rgb(56 189 248 / 0.1);
    --color-info-stroke: rgb(56 189 248 / 0.2);

    --shadow-xs: none;
    --shadow-sm: none;
    --shadow-md: none;
    --shadow-overlay: none;
  }
}

/* ===== global/base.css ===== */
/*
GLOBAL BASE STYLES
High-level element styles. Most of the site should be styled right here;
compositions, utilities and blocks only pick up what the cascade can't do.
*/

body {
  margin: 0;
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-base);
  font-size: var(--step-0);
  line-height: var(--leading-standard);
  text-wrap: pretty; /* inherited by body copy; headings keep their own balance */
}

/* Push the footer to the bottom of short pages (body is a column flex) */
main {
  flex: 1 0 auto;
}

::selection {
  background: var(--color-brand);
  color: var(--color-text-on-fill);
}

/* Headings. The type scale uses only two weights: strong (Semi Bold, 600) and
   weak (Regular, 400). All headings use the same weight by default; hierarchy
   comes from size, not weight. Face and weight are
   single knobs (--font-display, --weight-heading) so a reskin can give headings
   a display typeface or a heavier voice without touching these rules. */
h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-display);
  font-weight: var(--weight-heading);
}

h1 {
  font-size: var(--step-4);
  line-height: var(--leading-fine);
  letter-spacing: -0.0125em; /* a slight negative tracking on the largest heading */
}

h2 {
  font-size: var(--step-3);
  line-height: var(--leading-snug);
}

h3 {
  font-size: var(--step-2);
  line-height: var(--leading-snug);
}

h4,
h5,
h6 {
  font-size: var(--step-1);
  line-height: var(--leading-mid);
}

small {
  font-size: var(--step--1);
  line-height: var(--leading-mid);
}

/* Inserted text reads as a positive edit: brand wash, no underline. */
ins {
  text-decoration: none;
  background: var(--color-brand);
  color: var(--color-text-on-fill);
  padding-inline: 0.3em;
}

/* A variable is upright and a touch heavier, not the default italic. */
var {
  font-style: normal;
  font-weight: var(--weight-semibold);
}

/* Inline quotes are italic; inside a blockquote they are not (see below). */
q {
  font-style: italic;
}

/* Links. Brand color is reserved for interaction; links are interaction.
   the underline disappears on hover; the color stays. */
a {
  color: var(--color-brand);
  text-underline-offset: 2px;
  transition: text-decoration-color var(--transition-fast);
}

a:hover {
  text-decoration-color: transparent;
}

/* Focus: visible on every focusable element, no exceptions. */
:focus-visible {
  outline: var(--focus-ring);
  /* Per-context nudge via --focus-offset; falls back to the global 2px. */
  outline-offset: var(--focus-offset, var(--focus-ring-offset));
}

/* Lists. Only unclassed lists keep their default styling. */
:is(ul, ol):not([class], [role='list']) {
  padding-inline-start: var(--space-m);
}

/* A list with a list role AND a class is a layout/UI list (laid out by a
   composition), so it's out of standard flow; drop its default spacing. :where()
   keeps it at zero specificity so .flow can still add margin when such a list is a
   flow child (e.g. ul.grid). An unclassed role="list" keeps default spacing, so a
   block that uses one (e.g. pagination) owns its own spacing. */
:where([role='list'][class], [role='tablist'][class]) {
  margin-block: 0;
  padding: 0;
}

/* Markers: slightly smaller bullets, bold numbers. */
ul ::marker {
  font-size: 0.8lh;
}

ol ::marker {
  font-weight: var(--weight-bold);
}

dt {
  font-weight: var(--weight-semibold);
}

dd {
  margin-inline-start: var(--space-m);
}

/* Vertical rhythm within description lists */
dt + dd {
  margin-block-start: var(--space-2xs);
}

dd + dt {
  margin-block-start: var(--space-s);
}

/* Quotes */
blockquote {
  /* Reset the user-agent inline margin (the modern reset only clears margin-block) */
  margin-inline: 0;
  padding-inline-start: var(--space-m);
  border-inline-start: 4px solid var(--color-stroke);
  color: var(--color-text-weak);
}

blockquote > footer,
figcaption {
  font-size: var(--step--1);
  color: var(--color-text-weaker);
}

/* Space between the quote and its attribution. */
blockquote > footer {
  margin-block-start: var(--space-s);
}

/* A quote inside a blockquote drops the inline-quote italics. */
blockquote q {
  font-style: normal;
}

/* Caption sits just under the figure, in the mono voice used for metadata. */
figcaption {
  padding-block-start: 0.5em;
  font-family: var(--font-mono);
}

/* ---- Media ---- */
/* Embedded video/iframe behaves like an image: a responsive 16:9 box rather
   than its intrinsic pixel size, so it never overflows its container. */
:is(video, iframe[src*='youtube'], iframe[src*='vimeo']) {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

/* A <picture> shrink-wraps to its image instead of filling the row. */
picture {
  width: max-content;
  max-width: 100%;
}

/* A decorative SVG marked as an image inside prose keeps its own width/height
   attributes and gets a subtle frame (e.g. the spiral demo), but never wider
   than its container. Scoped to .prose so icon SVGs (which are also role="img"
   with width/height, e.g. badge hosts) are left alone. The mat is a fixed light
   surface in both themes; image SVGs often carry dark/black paint, so a
   theme-darkening background would hide them. */
.prose svg[role='img'][width][height] {
  width: revert;
  block-size: auto;
  max-inline-size: 100%;
  background: var(--color-image-mat);
  padding: var(--space-xs);
}

/* Code */
code,
kbd,
samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--color-fill);
  padding: 0.1em 0.35em;
  border-radius: var(--radius-xs);
  /* Predictable wrapping: don't hyphenate or break inside code tokens, and keep
     code left-aligned even in a centred or right-aligned cell. */
  hyphens: none;
  tab-size: 2;
  text-align: left;
  word-break: normal;
  word-wrap: normal;
  /* Keep the fill, padding and radius intact when inline code wraps lines */
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* <kbd> reads as a physical key: a bordered cap. */
kbd {
  border: var(--stroke-strong);
  padding-block-end: 0.1em;
}

pre {
  background: var(--color-bg-sunken);
  border: var(--stroke);
  border-radius: var(--radius-s);
  padding: var(--space-s);
  overflow-x: auto;
}

/* A code block shrink-wraps to its content (up to full width, then scrolls)
   rather than always spanning the whole measure. */
pre:has(code) {
  width: max-content;
  max-width: 100%;
}

pre code {
  background: none;
  padding: 0;
  font-size: var(--step--1);
}

/* Separator */
hr {
  border: 0;
  border-block-start: var(--stroke);
  /* Cooperate with flow: an <hr> inside a .flow/.prose context tows the flow
     rhythm; otherwise it falls back to a fixed 32px block margin. */
  margin-block: var(--flow-space, var(--space-l));
}

/* Disclosure in prose: the summary is the control: bold, with a pointer, and a
   gap below it once open. Scoped to .prose so block-level disclosures
   (.site-head__menu account menu, .nav-disclosure) keep their own summary
   weight and spacing; a global rule here made the account menu bold and grew
   the nav bar's height when opened. */
.prose summary {
  font-weight: var(--weight-semibold);
  cursor: pointer;
}

.prose details[open] summary {
  margin-block-end: var(--space-s);
}

/* Tables */
table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1); /* 14; shadcn tables are text-sm */
}

/* Wrap a table in .table-wrap so it scrolls sideways when it's wider than its
   container, instead of squashing columns. Give the wrapper tabindex="0" and a
   label in markup so keyboard users can scroll it:
   <div class="table-wrap" role="region" aria-label="…" tabindex="0">…</div> */
.table-wrap {
  overflow-x: auto;
}

caption {
  text-align: start;
  font-size: var(--step--1);
  color: var(--color-text-weaker);
  padding-block-end: var(--space-2xs);
}

th {
  font-weight: var(--weight-medium);
  text-align: start;
}

th,
td {
  padding-block: var(--space-2xs);
  padding-inline: var(--space-s);
  border-block-end: var(--stroke);
}

/* Header: a single hairline beneath it (the cell border-b), medium-weight muted
   labels, h-10 tall; no top rule, no fill (shadcn). */
thead th {
  block-size: 2.5rem; /* h-10 */
  padding-block: 0;
  color: var(--color-text-weak);
}

/* Row hover and a clean last row (shadcn: hover bg-muted, last row borderless) */
tbody tr:hover {
  background: var(--color-fill);
}

tbody tr:last-child :is(td, th) {
  border-block-end: 0;
}

/* ===== global/forms.css ===== */
/*
GLOBAL FORM STYLES
Targets Django's default div form renderer, so a plain {{ form }} works
with no extra classes or template overrides:

  <ul class="errorlist nonfield"><li>…</li></ul>   form-level errors
  <div>                                            one per field
    <label for="id_email">…</label>
    <div class="helptext" id="id_email_helptext">…</div>
    <ul class="errorlist" id="id_email_error"><li>…</li></ul>   errors sit ABOVE the widget
    <input … aria-invalid="true" aria-describedby="id_email_error id_email_helptext">
  </div>

Django wires the widget's aria-describedby to the help text and error ids for you
(Django 5+), so the message is programmatically tied to the field; keep that
attribute when hand-authoring a field so screen readers announce the error.

Most of this section is generic form styling. The rules that only exist for
Django's renderer output sit at the END of the section, under the
"Django's div renderer" marker; a non-Django project that hand-writes its
field markup can delete that whole chunk.
*/

/* Every form runs on a 24px rhythm between fields; set the knob once so it's
   the same whether the form carries .flow (whose own default is 1em) or not,
   and so it matches the auth cards. The rule below then gives a bare {{ form }}
   that rhythm even without the .flow class (markup is often out of our control:
   Django, CMSes). Same custom-property contract either way. */
form {
  --flow-space: var(--space-m);
}

form > * + * {
  margin-block-start: var(--flow-space, var(--space-m));
}

/* A card wrapped around a form (a sign-in panel, a settings form) runs its
   own rows (heading, form, footer link) on the same 21→24 rhythm as the form's
   fields, so the whole panel reads as one column. (A <form class="card"> gets
   this from the form rule above; the card keeps its default padding.)
   Override per-card with --flow-space / --card-padding if needed. */
.card:has(> form) {
  --flow-space: var(--space-m);
}

label {
  display: block;
  font-size: 0.875rem; /* 14, fixed (shadcn FormLabel text-sm) */
  font-weight: var(--weight-medium); /* shadcn labels are medium */
  margin-block-end: var(--space-2xs);
}

/* A legend sits in the fieldset's border notch. Same regular weight as the
   field labels; the notch position alone marks it as the group title, so it
   doesn't need to be bolder than the labels.
   Inline padding keeps the border rule off the text. */
legend {
  font-weight: var(--weight-normal);
  padding-inline: var(--space-xs);
}

/* Required marker: a weak-grey asterisk, not red. Django
   renders the required attribute on the widget; the label precedes it.
   Assumes the field-per-div structure (label + widget alone in a wrapper):
   in one flat parent, a label would match ANY later required sibling.
   The `/ ''` alternative text keeps the star out of the accessible name;
   required is already announced from the attribute; engines without the
   syntax drop the declaration (no asterisk, semantics intact). */
label:has(~ :is(input, select, textarea):required)::after {
  content: ' *' / '';
  color: var(--color-text-weak);
}

input:not([type='checkbox'], [type='radio'], [type='range'], [type='color'], [type='file']),
select,
textarea {
  width: 100%;
  /* 16px on phones so iOS Safari won't zoom on focus, 14px from md. Padding is
     fixed (not fluid) to match the fixed control height: 12 sides = shadcn px-3. */
  font-size: var(--text-input);
  padding-inline: 0.75rem;
  border: var(--stroke);
  border-radius: var(--radius-s);
  /* Transparent in light (takes the surface colour); a subtle tint in dark so
     the field reads as distinct from the card behind it (shadcn bg-input/30). */
  background: var(--color-field);
  color: var(--color-text);
  box-shadow: var(--shadow-xs);
  transition:
    border-color var(--transition-fast),
    box-shadow var(--transition-fast),
    background-color var(--transition-fast);
}

/* Single-line fields take the shared control height (the text centres natively),
   so they align with buttons whatever the text size. A multi-line or sized select
   ([multiple]/[size]) is a list box, not a control; it keeps its natural height. */
input:not([type='checkbox'], [type='radio'], [type='range'], [type='color'], [type='file']),
select:not([multiple], [size]) {
  block-size: var(--control-h);
}

/* A textarea is multi-line, so it keeps vertical padding, not a fixed height.
   Fixed (not fluid) to match the field's fixed control height: 8 = shadcn py-2. */
textarea {
  padding-block: 0.5rem;
}

::placeholder {
  color: var(--color-text-weaker);
  opacity: 1;
}

input:hover:not(:disabled, :focus, [aria-invalid='true'], [type='file']),
select:hover:not(:disabled, :focus, [aria-invalid='true']),
textarea:hover:not(:disabled, :focus, [aria-invalid='true']) {
  background-color: var(--color-fill);
}

/* Native file input: the no-JS / un-enhanced state. The file-upload script
   upgrades these into a drag-and-drop dropzone (see blocks/file-upload.css);
   without it this is what shows, so the native control reads as ours: a
   secondary-style "choose" button + the chosen filename, no box. */
input[type='file'] {
  font-size: var(--step--1);
  color: var(--color-text-weak);
}

input[type='file']::file-selector-button {
  margin-inline-end: 0.75rem;
  padding: 0.5rem 0.75rem; /* fixed, like the real buttons: 8/12, shadcn py-2 px-3 */
  border: var(--stroke-strong);
  border-radius: var(--radius-s);
  background: transparent;
  color: var(--color-brand);
  font: inherit;
  font-weight: var(--weight-semibold);
  cursor: pointer;
  transition: background-color var(--transition-fast);
}

input[type='file']::file-selector-button:hover {
  background-color: var(--color-fill);
}

input:disabled,
select:disabled,
textarea:disabled {
  background: var(--color-fill);
  border-color: var(--color-stroke);
  color: var(--color-text-weaker);
  cursor: not-allowed;
}

/* Single select: the field box above + a chevron, native arrow removed. The
   chevron is a stroked Feather glyph (matching the icon set) in a neutral slate
   that reads on both light and dark. It's a data-URI background, which can't
   carry a CSS variable; a bare <select> has no pseudo-element to mask, and CSS
   gradients only draw a filled wedge, not a clean stroke. So this slate is the
   one fixed colour a reskin can't retint via tokens; edit it here if needed. */
select:not([multiple]) {
  appearance: none;
  /* Fixed, like the field's other padding: reserve 2rem so the value clears the
     chevron, which sits 0.75rem (px-3) in from the right; no viewport drift. */
  padding-inline-end: 2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%237b8499' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  background-size: 1rem;
}

/* Multi-select is a list box, not a dropdown: no chevron, sized to its options
   with comfortable padding; the brand colour tints the selection. Fixed padding,
   like the other fields: 4 around + 8 inline on each option = 12 text inset. */
select[multiple] {
  padding: 0.25rem;
  accent-color: var(--color-brand);
}

select[multiple] option {
  padding: 0.375rem 0.5rem;
  border-radius: var(--radius-xs);
}

/* Checkboxes and radios keep their native rendering, tinted brand. flex:none so
   the control keeps its square size when it sits in a flex label whose text
   wraps (otherwise the flex row squishes it narrower than it is tall). */
input[type='checkbox'],
input[type='radio'] {
  inline-size: 1.25em;
  block-size: 1.25em;
  flex: none;
  margin: 0; /* drop the UA control margin so flex alignment is predictable */
  accent-color: var(--color-brand);
}

/* A label that wraps or follows a checkbox/radio is a choice, not a heading.
   Block-level flex so options stack one per row, top-aligned (baseline) so a
   long, wrapping label keeps the control beside its first line instead of
   floating in the vertical centre. :where() keeps specificity low so block-level
   controls that restyle these labels (.segmented, .tabs) still win. */
label:has(input:where([type='checkbox'], [type='radio'])),
input:where([type='checkbox'], [type='radio']) + label {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
  /* Comfortable leading so a wrapping choice isn't cramped, and so the control
     (which is taller than the reset's 1.1 label leading) lines up with line one. */
  line-height: var(--leading-standard);
  font-weight: var(--weight-normal);
  margin-block-end: 0;
}

/* Centre the control on the first line: flex-start aligns it to the line-box
   top, which leaves it sitting ~2px high; (line 24px − control 20px) / 2 nudges
   it onto the optical centre of line one. The attribute selectors keep this
   above the base input[type=…] rule that zeroes the margin. */
label:has(input:where([type='checkbox'], [type='radio'])) input[type='checkbox'],
label:has(input:where([type='checkbox'], [type='radio'])) input[type='radio'] {
  margin-block-start: 0.125em;
}

/* Fieldsets group related controls (incl. Django's RadioSelect): a bordered,
   padded box with the legend in its top edge. */
fieldset {
  border: var(--stroke);
  border-radius: var(--radius-s);
  padding: var(--space-s);
  /* Only the inline UA margin is zeroed; block spacing comes from .flow / the
     form rhythm (form > * + *), so a boxed fieldset isn't flush to the field above. */
  margin-inline: 0;
  min-inline-size: 0;
}

fieldset > div {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

/* The submit row (Save/Cancel and friends), set off from the fields by a
   divider. */
.form-actions {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  flex-wrap: wrap;
  padding-block-start: var(--space-l);
  border-block-start: var(--stroke);
}

/* An errored field ([aria-invalid], which Django sets on bound fields) gets a
   2px error-strong (0.8) border AND a soft red fill: colour paired with a
   heavier border, not colour alone. */
:is(input, select, textarea)[aria-invalid='true'] {
  border-width: 2px;
  border-color: var(--color-error-strong);
  background: var(--color-error-bg);
}

/* Hover over an errored field deepens the red: the neutral hover fill layered
   over the error wash. Dynamic: no separate darker-red colour needed. */
:is(input, select, textarea)[aria-invalid='true']:hover:not(:disabled) {
  background-image: linear-gradient(var(--color-fill), var(--color-fill));
}

/* ---- Django's div renderer -----------------------------------------------
   Everything from here to the end of the forms section targets the markup
   Django's default div renderer emits for {{ form }}: the per-field <div>,
   div.helptext and ul.errorlist. Hand-writing your field markup (no Django)?
   Delete this whole chunk; the rules above cover everything else.
--------------------------------------------------------------------------- */

/* Each Django field (a div holding label, help text, errors and the widget)
   stacks on one uniform gap, so the error message sits evenly between its label
   and input; no lopsided margins above/below it. */
form > div:has(> label) {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

form > div:has(> label) > * {
  margin-block: 0;
}

/* Help text */
.helptext {
  font-size: var(--step--1);
  color: var(--color-text-weak);
  margin-block-end: var(--space-3xs);
}

.helptext ul {
  margin-block: 0;
  padding-inline-start: var(--space-s);
}

/* Field errors: Django puts these directly above the widget.
   Color is paired with shape: each error line gets an x-circle icon. */
.errorlist {
  list-style: none;
  padding: 0;
  margin-block: 0 var(--space-3xs);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
  color: var(--color-error);
}

.errorlist > li {
  display: flex;
  align-items: flex-start; /* a wrapping message keeps the icon beside line one, not centred over both */
  gap: var(--space-2xs);
}

/* x-octagon icon (the error glyph), always in the error tone
   so it stays red even when the surrounding text is dark (nonfield box) */
.errorlist > li::before {
  content: '';
  inline-size: 1.25em;
  block-size: 1.25em;
  flex: none;
  /* centre the 1.25em icon on line one: (1.5 leading − 1.25) / 2 */
  margin-block-start: 0.125em;
  background: var(--color-error);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86,2,16.14,2,22,7.86,22,16.14,16.14,22,7.86,22,2,16.14,2,7.86,7.86,2'/%3E%3Cline x1='15' y1='9' x2='9' y2='15'/%3E%3Cline x1='9' y1='9' x2='15' y2='15'/%3E%3C/svg%3E")
    center / contain no-repeat;
}

/* Form-level errors get the destructive alert treatment: red icon, red text and
   a red-tinted border over a faint red wash (matches the error alert block). */
.errorlist.nonfield {
  padding: var(--space-xs) var(--space-s);
  border: 1px solid var(--color-error-stroke);
  border-radius: var(--radius-m);
  background: var(--color-error-bg);
  color: var(--color-error);
}

/* ===== compositions/cluster.css ===== */
/*
CLUSTER
More info: https://every-layout.dev/layouts/cluster/
A layout that lets you distribute items with consistent
spacing, regardless of their size

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-m)): This defines the space
between each item.

--cluster-horizontal-alignment (flex-start): How items should align
horizontally. Can be any acceptable flexbox alignment value.

--cluster-vertical-alignment (center): How items should align vertically.
Can be any acceptable flexbox alignment value.
*/

.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gutter, var(--space-m));
  justify-content: var(--cluster-horizontal-alignment, flex-start);
  align-items: var(--cluster-vertical-alignment, center);
}

/* ===== compositions/flow.css ===== */
/*
FLOW COMPOSITION
Like the Every Layout stack: https://every-layout.dev/layouts/stack/
Info about this implementation: https://piccalil.li/blog/my-favourite-3-lines-of-css/
*/
.flow > * + * {
  margin-block-start: var(--flow-space, 1em);
}

/* ===== compositions/grid.css ===== */
/* AUTO GRID
Related Every Layout: https://every-layout.dev/layouts/grid/
More info on the flexible nature: https://piccalil.li/tutorial/create-a-responsive-grid-layout-with-no-media-queries-using-css-grid/
A flexible layout that will create an auto-fill grid with
configurable grid item sizes

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-l)): This defines the space
between each item.

--grid-min-item-size (16rem): How large each item should be
ideally, as a minimum.

--grid-placement (auto-fill): Set either auto-fit or auto-fill
to change how empty grid tracks are handled */

.grid {
  display: grid;
  grid-template-columns: repeat(
    var(--grid-placement, auto-fill),
    minmax(var(--grid-min-item-size, 16rem), 1fr)
  );
  gap: var(--gutter, var(--space-l));
}

/* A split 50/50 layout */
.grid[data-layout='50-50'] {
  --grid-placement: auto-fit;
  --grid-min-item-size: clamp(16rem, 50vw, 33rem);
}

/* Three column grid layout */
.grid[data-layout='thirds'] {
  --grid-placement: auto-fit;
  --grid-min-item-size: clamp(16rem, 33%, 28rem);
}

/* ===== compositions/media.css ===== */
/*
MEDIA COMPOSITION
The classic media object: a fixed lead element (avatar, icon) beside content
that takes the rest of the row, vertically centred. The text block can stack two
lines (name + role/email) and stays narrow-friendly; long values truncate
rather than stretching the row. Any trailing control is pushed to the end.

  <div class="media">
    <span class="avatar">…</span>
    <span class="media__text"><strong>John Smith</strong><small>Designer</small></span>
    <button …>⋯</button>            <!-- optional trailing control, sits at the end -->
  </div>
*/
.media {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  min-inline-size: 0;
}

.media__text {
  display: flex;
  flex-direction: column;
  flex: 1; /* take the middle, so a trailing control lands at the end */
  min-inline-size: 0;
}

.media__text > * {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.media__text > small {
  color: var(--color-text-weak);
  font-size: var(--step--1);
}

/* ===== compositions/repel.css ===== */
/*
REPEL
A little layout that pushes items away from each other where
there is space in the viewport and stacks on small viewports

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-m)): This defines the space
between each item.

--repel-vertical-alignment (center): How items should align vertically.
Can be any acceptable flexbox alignment value.
*/
.repel {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: var(--repel-vertical-alignment, center);
  gap: var(--gutter, var(--space-m));
}

.repel[data-nowrap] {
  flex-wrap: nowrap;
}

/* ===== compositions/sidebar.css ===== */
/*
SIDEBAR
More info: https://every-layout.dev/layouts/sidebar/
A layout that allows you to have a flexible main content area
and a "fixed" width sidebar that sits on the left or right.
If there is not enough viewport space to fit both the sidebar
width *and* the main content minimum width, they will stack
on top of each other

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-s-l)): This defines the space
between the sidebar and main content.

--sidebar-target-width (20rem): How large the sidebar should be

--sidebar-content-min-width(50%): The minimum size of the main content area

EXCEPTIONS
.sidebar[data-direction='rtl']: flips the sidebar to be on the right
*/
.sidebar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gutter, var(--space-s-l));
}

.sidebar:not([data-direction]) > :first-child {
  flex-basis: var(--sidebar-target-width, 20rem);
  flex-grow: 1;
}

.sidebar:not([data-direction]) > :last-child {
  flex-basis: 0;
  flex-grow: 999;
  min-width: var(--sidebar-content-min-width, 50%);
}

.sidebar[data-reversed] {
  flex-direction: row-reverse;
}

.sidebar[data-direction='rtl'] > :last-child {
  flex-basis: var(--sidebar-target-width, 20rem);
  flex-grow: 1;
}

.sidebar[data-direction='rtl'] > :first-child {
  flex-basis: 0;
  flex-grow: 999;
  min-width: var(--sidebar-content-min-width, 50%);
}

/* ===== compositions/switcher.css ===== */
/*
SWITCHER
More info: https://every-layout.dev/layouts/switcher/
A layout that allows you to lay **2** items next to each other
until there is not enough horizontal space to allow that.

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-l)): This defines the space
between each item

--switcher-target-container-width (40rem): How large the container
needs to be to allow items to sit inline with each other

--switcher-vertical-alignment (flex-start): How items should align vertically.
Can be any acceptable flexbox alignment value.
*/
.switcher {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gutter, var(--space-l));
  align-items: var(--switcher-vertical-alignment, flex-start);
}

.switcher > * {
  flex-grow: 1;
  flex-basis: calc((var(--switcher-target-container-width, 40rem) - 100%) * 999);
}

/* Max 2 items,
so anything greater than 2 is full width */
.switcher > :nth-child(n + 3) {
  flex-basis: 100%;
}

/* ===== compositions/wrapper.css ===== */
/*
WRAPPER COMPOSITION
A common centered wrapper/container.

CUSTOM PROPERTIES AND CONFIGURATION
--wrapper-max (90rem): maximum inline size of the content.

EXCEPTIONS
.wrapper[data-wrapper='narrow']: caps at --wrapper-max-narrow (37.5rem)
for forms and reading-width content.
*/
.wrapper {
  margin-inline: auto;
  max-inline-size: var(--wrapper-max, 90rem);
  padding-inline: var(--page-gutter);
  position: relative;
}

.wrapper[data-wrapper='narrow'] {
  max-inline-size: var(--wrapper-max-narrow, 37.5rem);
}

/* A wrapper nested in a wrapper already has its gutter */
.wrapper .wrapper {
  padding-inline: 0;
}

/* ===== blocks/alert.css ===== */
/*
ALERT BLOCK
Status message. Pairs color with shape: every alert gets a tone icon
automatically (CSS mask, Feather icons), so plain text content (like a
Django message) never relies on color alone.

EXCEPTIONS
data-tone: 'info' (default) | 'success' | 'warning' | 'error'
data-variant='strong': a solid neutral (inverted) banner
*/

/* ---- tone + icon ---- */
.alert {
  --alert-tone: var(--color-info);
  --alert-stroke: var(--color-stroke);
  --alert-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cline x1='12' y1='16' x2='12' y2='12'/%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'/%3E%3C/svg%3E");

  background: var(--color-bg);
  color: var(--color-text);
}

/* The tone icon: 16px, in the tone colour (shadcn size-4) */
.alert::before {
  content: '';
  inline-size: 1rem;
  block-size: 1rem;
  flex: none;
  margin-block-start: 0.125rem;
  background: var(--alert-tone);
  mask: var(--alert-icon) center / contain no-repeat;
}

.alert[data-tone='success'] {
  --alert-tone: var(--color-success);
  --alert-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 11.08V12a10 10 0 1 1-5.93-9.14'/%3E%3Cpolyline points='22 4 12 14.01 9 11.27'/%3E%3C/svg%3E");
}

.alert[data-tone='warning'] {
  --alert-tone: var(--color-warning);
  --alert-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cline x1='12' y1='9' x2='12' y2='13'/%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'/%3E%3C/svg%3E");
}

.alert[data-tone='error'] {
  --alert-tone: var(--color-error);
  --alert-stroke: var(--color-error-stroke);
  --alert-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86,2,16.14,2,22,7.86,22,16.14,16.14,22,7.86,22,2,16.14,2,7.86,7.86,2'/%3E%3Cline x1='15' y1='9' x2='9' y2='15'/%3E%3Cline x1='9' y1='9' x2='15' y2='15'/%3E%3C/svg%3E");
}

/* ---- inline alert (in-page, boxed) ---- */
.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-s);
  border: 1px solid var(--alert-stroke);
  border-radius: var(--radius-m);
  font-size: var(--step--1); /* 14; shadcn alerts are text-sm throughout */
}

/* Title and description are both 14; the title is set apart by weight, not size
   (shadcn AlertTitle = text-sm font-medium tracking-tight). The dashboard uses a
   <strong> as the title, the kitchen sink an <h3>. */
.alert :is(h2, h3, h4, strong) {
  color: var(--color-text);
  font-size: var(--step--1);
  font-weight: var(--weight-medium);
  letter-spacing: -0.01em;
}

/* The error tone reads as shadcn's destructive: red title + icon */
.alert[data-tone='error'] :is(h2, h3, h4) {
  color: var(--color-error);
}

.alert p {
  color: var(--color-text-weak);
  font-size: var(--step--1);
}

/* Dismiss button: built by the enhancement script on .alert[data-dismissible],
   so there's no dead control without JS. Sits at the trailing edge, aligned with
   the tone icon; the X is a currentColor SVG. */
.alert__close {
  flex: none;
  align-self: flex-start;
  display: inline-flex;
  padding: 0.375rem;
  /* a bigger hover target that doesn't grow the row or the trailing inset: the
     block pull keeps the row height, the inline-end pull puts the glyph box
     flush with the padding edge so the X sits as close to its edge as the tone
     icon does to its side */
  margin-block: -0.1875rem;
  margin-inline-end: -0.375rem;
  margin-inline-start: auto;
  border: 0;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--color-text-weak);
  cursor: pointer;
  transition:
    background-color var(--transition-fast),
    color var(--transition-fast);
}

.alert__close:hover {
  background: var(--color-fill); /* neutral accent, not a tone tint (shadcn) */
  color: var(--color-text);
}

.alert__close > svg {
  inline-size: 1rem;
  block-size: 1rem;
}

/* EXCEPTION: strong: solid fill, no accent bar */
.alert[data-variant='strong'] {
  background: var(--color-text); /* solid inverted banner: neutral, not a loud tone */
  --alert-stroke: transparent;
  box-shadow: none;
  color: var(--color-bg);
}

.alert[data-variant='strong']::before {
  background: var(--color-bg);
}

.alert[data-variant='strong'] :is(h2, h3, h4, p) {
  color: var(--color-bg);
}

/* The dismiss button on a strong (inverted) alert rides the inverted colour */
.alert[data-variant='strong'] .alert__close {
  color: var(--color-bg);
}

.alert[data-variant='strong'] .alert__close:hover {
  background: var(--color-overlay-invert);
}

/* ===== blocks/app-shell.css ===== */
/*
APP-SHELL BLOCK
A page-level layout module, not a generic composition: it's breakpoint-driven,
stateful, and coupled to the .sidenav block, so it lives in the blocks layer.
A full-height application layout: a fixed-width side column (the .sidenav) and a
flexible main column. Below a threshold it collapses to one column and the side
column becomes an off-canvas drawer, toggled by a checkbox so it works with no
JavaScript. The enhancement script (optional) upgrades the label to a real
button with aria-expanded.

  <div class="app-shell">
    <input type="checkbox" id="app-nav" class="app-shell__state visually-hidden" />
    <aside class="sidenav">…</aside>
    <label for="app-nav" class="app-shell__scrim" hidden></label>
    <div class="app-shell__main">
      <header>… a <label for="app-nav" class="app-shell__menu">☰</label> on mobile …</header>
      <main>…</main>
    </div>
  </div>
*/
.app-shell {
  display: grid;
  grid-template-columns: var(--sidenav-width, 17rem) 1fr;
  min-block-size: 100svh;
  font-size: var(--step--1); /* app chrome is 14px; prose/content keeps 16-18 */
}

/* App headings step down from the Utopia content scale; shadcn dashboards use
   compact headings (the big scale is for marketing/prose pages). */
.app-shell h1 {
  font-size: var(--step-2);
  line-height: var(--leading-snug);
}
.app-shell h2 {
  font-size: var(--step-1);
  line-height: var(--leading-snug);
}
.app-shell h3 {
  font-size: var(--step-0);
}
.app-shell :is(h4, h5, h6) {
  font-size: var(--step--1);
}

/* The drawer machinery is desktop-hidden: the side column just sits in the grid. */
.app-shell__scrim {
  display: none;
}

/* The menu toggle is a self-contained icon button, deliberately NOT the .button
   block, so the shell owns its show/hide and sizing rather than depending on
   another block. Hidden on desktop; shown in the top bar on mobile. */
.app-shell__menu {
  display: none;
  align-items: center;
  justify-content: center;
  inline-size: var(--control-h); /* 36, the shared control height */
  block-size: var(--control-h);
  margin: 0 0 0 -0.5rem; /* zero the global <label> margin-block-end (which
                             would push the icon up off centre) and pull the icon
                             out to the gutter edge so it lines up with content */
  padding: 0;
  border: 0;
  border-radius: var(--radius-s);
  background: none;
  color: var(--color-text-weak);
  cursor: pointer;
}

.app-shell__menu:hover {
  background: var(--color-fill);
  color: var(--color-text);
}

.app-shell__menu > svg {
  inline-size: 1.25rem;
  block-size: 1.25rem;
}

/* No-JS keyboard path: the visually-hidden checkbox is the focusable control,
   so surface its focus on the hamburger it labels. (The enhancement script
   swaps this pairing for a real button with its own focus.) */
.app-shell > .app-shell__state:focus-visible ~ .app-shell__main .app-shell__menu {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* The drawer (and its checkbox) only exist below the shell breakpoint; on a
   desktop-width shell the checkbox would be an invisible tab stop, so remove it. */
@media (width >= 60rem) {
  .app-shell__state {
    display: none;
  }
}

.app-shell__main {
  min-inline-size: 0; /* let wide content (tables) scroll instead of stretching the grid */
  display: flex;
  flex-direction: column;
}

/* The main column owns the page padding, so app pages don't each restate it.
   A .wrapper directly inside keeps its centring/max-width but drops its own
   gutter, the same idiom as nested wrappers. */
.app-shell__main > main {
  padding-block: var(--space-l);
  padding-inline: var(--page-gutter);
}

.app-shell__main > main > .wrapper {
  padding-inline: 0;
}

@media (width < 60rem) {
  .app-shell {
    grid-template-columns: 1fr; /* single column; the sidenav floats over it */
  }

  .app-shell > .sidenav {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    z-index: 30;
    inline-size: min(17rem, 82vw);
    translate: -100% 0;
    transition: translate var(--transition-base);
  }

  /* Checkbox drives the drawer: pure CSS, no JS needed. Sibling selectors
     (the checkbox is the shell's first child), not :has(), so the drawer still
     opens on engines without :has(); never hide content we can't show again. */
  .app-shell > .app-shell__state:checked ~ .sidenav {
    translate: 0 0;
  }

  .app-shell__scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 20;
    background: var(--color-scrim);
    opacity: 0;
    visibility: hidden;
    transition:
      opacity var(--transition-base),
      visibility var(--transition-base);
  }

  .app-shell > .app-shell__state:checked ~ .app-shell__scrim {
    opacity: 1;
    visibility: visible;
  }

  .app-shell__menu {
    display: inline-flex; /* the hamburger appears in the top bar */
  }
}

/* ===== blocks/avatar.css ===== */
/*
AVATAR BLOCK
A round user image. Wrap an <img> (cover-fit, never squashed) or, with no
image, it shows whatever it contains (initials, an icon), centred. Sizes via
data-size. Used in the account menu, table rows, the sidenav footer, settings.

  <span class="avatar"><img src="…" alt="John Smith" /></span>
  <span class="avatar" data-size="small">JS</span>

EXCEPTIONS
data-size: 'small' (24) | (default 40) | 'large' (48)
*/
.avatar {
  --avatar-size: 2.5rem; /* 40 */
  --avatar-bg: var(--color-fill); /* neutral fallback (shadcn bg-muted) */
  --avatar-fg: var(--color-text-weak);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  inline-size: var(--avatar-size);
  block-size: var(--avatar-size);
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--avatar-bg);
  color: var(--avatar-fg);
  /* initials scale with the avatar so small/large read right */
  font-size: calc(var(--avatar-size) * 0.4);
  font-weight: var(--weight-medium);
  line-height: 1;
}

/* Coloured initials: tones for variety when there's no photo. Primary is the
   brand wash, for the signed-in user's own avatar (account menu, sidenav). */
.avatar[data-tone='primary'] {
  --avatar-bg: var(--color-brand-bg);
  --avatar-fg: var(--color-brand);
}
.avatar[data-tone='success'] {
  --avatar-bg: var(--color-success-bg);
  --avatar-fg: var(--color-success);
}
.avatar[data-tone='warning'] {
  --avatar-bg: var(--color-warning-bg);
  --avatar-fg: var(--color-warning);
}
.avatar[data-tone='info'] {
  --avatar-bg: var(--color-info-bg);
  --avatar-fg: var(--color-info);
}
.avatar[data-tone='error'] {
  --avatar-bg: var(--color-error-bg);
  --avatar-fg: var(--color-error);
}
.avatar[data-tone='neutral'] {
  --avatar-bg: var(--color-fill);
  --avatar-fg: var(--color-text-weak);
}

.avatar > img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* EXCEPTION: sizes */
.avatar[data-size='small'] {
  --avatar-size: 1.5rem; /* 24 */
  font-size: 0.75rem; /* 12; initials need to stay legible at this size */
}

.avatar[data-size='large'] {
  --avatar-size: 3rem; /* 48 */
}

/* ===== blocks/badge.css ===== */
/*
BADGE BLOCK
A small notification count or status dot. Stands alone, or, placed inside
an icon button, pins to the top-right corner.

The count is a solid ~20-tall pill (12px medium label); the dot is a solid
10px circle. Neither has a ring or shadow: flat, solid shapes. Default tone
is the notification red; status tones are available.

EXCEPTIONS
data-badge='dot'  → 12px dot, no label
data-tone: 'success' | 'warning' | 'info' | 'neutral'  (default: notification)
*/
.badge {
  --badge-fill: var(--color-error-fill);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 1.125rem; /* 18 */
  block-size: 1.125rem;
  padding-inline: var(--space-3xs); /* 4 */
  border-radius: var(--radius-pill);
  background: var(--badge-fill);
  color: var(--color-text-on-fill);
  font-size: 0.6875rem; /* 11 */
  font-weight: var(--weight-medium);
  line-height: 1;
}

/* EXCEPTION: dot: solid circle, no label (a notification pip) */
.badge[data-badge='dot'] {
  min-inline-size: 0;
  inline-size: 0.625rem; /* 10 */
  block-size: 0.625rem;
  padding: 0;
  border-radius: var(--radius-pill);
}

/* EXCEPTION: status tones: the fixed fill tokens, not the text tones, so the
   white label stays readable in dark mode too. */
.badge[data-tone='success'] {
  --badge-fill: var(--color-success-fill);
}
.badge[data-tone='warning'] {
  --badge-fill: var(--color-warning-fill);
}
.badge[data-tone='info'] {
  --badge-fill: var(--color-info-fill);
}
.badge[data-tone='neutral'] {
  --badge-fill: var(--color-neutral-fill);
}

/* EXCEPTION: subtle: a quiet count on a light surface (sidenav, menus), not a
   red notification: pale fill + weak text. */
.badge[data-variant='subtle'] {
  --badge-fill: var(--color-fill);
  color: var(--color-text-weak);
}

/* Pin to the top-right corner of a host, hugging it. The host is an icon
   button, or any bare element marked .badge-host, e.g. a plain icon with no
   background or border. The indicator stays tight to the corner rather than
   floating off it, so both count and dot sit a touch over the corner (most of
   the shape overlaps the host). */
.badge-host {
  position: relative;
  display: inline-flex;
}

:is(.button[data-icon], .badge-host) > .badge {
  position: absolute;
  inset-block-start: -0.25rem;
  inset-inline-end: -0.25rem;
}

:is(.button[data-icon], .badge-host) > .badge[data-badge='dot'] {
  inset-block-start: -0.125rem;
  inset-inline-end: -0.125rem;
}

/* On a bare icon (no button padding) the count would smother the glyph, so push
   it further into the corner. */
.badge-host > .badge:not([data-badge='dot']) {
  inset-block-start: -0.75rem;
  inset-inline-end: -0.75rem;
}

/* ===== blocks/breadcrumb.css ===== */
/*
BREADCRUMB BLOCK
The trail to the current page. Put an <ol> inside a <nav aria-label="Breadcrumb">;
the last item is the current page: a plain <span aria-current="page">, NOT an
<a>. An anchor with no href isn't a link (not focusable, no role, not announced),
so the current crumb is text. Separators are drawn in CSS so screen readers don't
announce them.

  <nav aria-label="Breadcrumb">
    <ol class="breadcrumb">
      <li><a href="/">Home</a></li>
      <li><a href="/account/">Account settings</a></li>
      <li><span aria-current="page">Edit personal details</span></li>
    </ol>
  </nav>
*/
.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--step--1);
}

.breadcrumb > li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
}

/* A slash before every item except the first (generated, so it's not read out) */
.breadcrumb > li + li::before {
  content: '/';
  color: var(--color-text-weaker);
}

.breadcrumb a {
  color: var(--color-text-weak);
  text-decoration: none;
}

.breadcrumb a:hover {
  color: var(--color-text);
  text-decoration: underline;
}

/* The current page: present but not a link */
.breadcrumb [aria-current='page'] {
  color: var(--color-text);
}

/* ===== blocks/button.css ===== */
/*
BUTTON BLOCK
Sized like shadcn: 14px label (fixed), font-medium, radius 8, subtle shadow-xs.
Height is the shared --control-h (rem, not em) so a 14px button lines up with a
16px input; text size never changes the box height:

  Small   --control-h-sm (32) · 0.75rem px   (shadcn h-8)
  Default --control-h    (36) · 1rem px       (shadcn h-9)
  Large   --control-h-lg (40) · 1.5rem px     (shadcn h-10)

Variants mirror shadcn: default = solid brand; secondary = soft grey fill;
outline = bordered; ghost = quiet hover wash; link = underline. Beyond shadcn:
tonal = brand wash fill (the mid-emphasis partner to the filled primary;
Material "filled tonal", Radix "soft"); outline-brand = brand stroke + label.
Filled, secondary and the outlines carry shadow-xs; tonal, ghost and link
have none.

EXCEPTIONS
data-variant: 'secondary' | 'tonal' | 'outline' | 'outline-brand' | 'ghost'
  | 'link' | 'destructive'
data-size: 'small' | 'large'
data-icon (+ data-shape='circle'): square/round icon-only button
data-block: full-width
*/
.button {
  --button-bg: var(--color-primary);
  --button-text: var(--color-text-on-fill);
  --button-border: transparent;
  --button-fs: 0.875rem; /* 14, fixed (buttons never trigger iOS zoom) */
  --button-height: var(--control-h); /* 36 (h-9), rem; aligns with inputs at any text size */
  --button-px: 1rem; /* 16 (shadcn px-4) */
  --button-icon: 1rem; /* 16 (size-4) */
  --button-radius: var(--radius-s); /* 8 */

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  block-size: var(--button-height);
  padding-inline: var(--button-px);
  border: 1px solid var(--button-border);
  border-radius: var(--button-radius);
  background: var(--button-bg);
  color: var(--button-text);
  font-size: var(--button-fs);
  font-weight: var(--weight-medium);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: var(--shadow-xs);
  transition:
    background var(--transition-fast),
    color var(--transition-fast),
    border-color var(--transition-fast),
    text-decoration-color var(--transition-fast); /* only the link variant moves this */
}

/* Hover and press shift colour only: no scale, no movement */
.button:hover:not(:disabled) {
  --button-bg: var(--color-primary-hover);
}

.button:active:not(:disabled) {
  --button-bg: var(--color-primary-press);
}

.button:disabled,
.button[aria-disabled='true'] {
  opacity: 0.5;
  box-shadow: none;
  cursor: not-allowed;
}

.button > svg {
  inline-size: var(--button-icon);
  block-size: var(--button-icon);
  flex: none;
}

/* EXCEPTION: secondary: soft neutral grey fill, dark label, no border
   (shadcn secondary) */
.button[data-variant='secondary'] {
  --button-bg: var(--color-muted);
  --button-text: var(--color-text);
}

.button[data-variant='secondary']:hover:not(:disabled) {
  --button-bg: var(--color-muted-hover);
}

/* EXCEPTION: tonal: brand wash + brand label, no border or shadow — the wash
   carries the variant in both themes. Reads as kin to the filled primary at
   lower emphasis; unlike inputs (grey stroke) and cards (raised surface),
   nothing else is a brand-tinted panel. */
.button[data-variant='tonal'] {
  --button-bg: var(--color-brand-bg);
  --button-text: var(--color-brand);
  box-shadow: none;
}

.button[data-variant='tonal']:hover:not(:disabled) {
  --button-bg: var(--color-brand-bg-hover);
}

/* EXCEPTION: outline: a bordered button for toolbars and filters: subtle
   border + dark label, hover gains a neutral wash (shadcn outline). */
.button[data-variant='outline'] {
  --button-bg: var(--color-outline-bg);
  --button-text: var(--color-text);
  --button-border: var(--color-stroke);
}

.button[data-variant='outline']:hover:not(:disabled) {
  --button-bg: var(--color-outline-hover); /* bg-accent in light, a clear ~#212121 step in dark */
}

/* EXCEPTION: outline-brand: the outline shape with brand stroke + label; the
   coloured stroke keeps it apart from the grey-stroked inputs. Hover gains
   the brand wash. */
.button[data-variant='outline-brand'] {
  --button-bg: var(--color-outline-bg);
  --button-text: var(--color-brand);
  --button-border: var(--color-brand-stroke);
}

.button[data-variant='outline-brand']:hover:not(:disabled) {
  --button-bg: var(--color-brand-bg);
}

/* EXCEPTION: ghost: quiet, no fill or border, dark label; hover gains a
   neutral wash (shadcn ghost). The default for toolbar/table icon buttons. */
.button[data-variant='ghost'] {
  --button-bg: transparent;
  --button-text: var(--color-text);
  box-shadow: none;
}

.button[data-variant='ghost']:hover:not(:disabled) {
  --button-bg: var(--color-fill);
}

/* EXCEPTION: link: brand label, no fill or shadow, underlined like a plain
   link — the underline fades on hover, matching the global link idiom
   (shadcn link does the inverse; consistency within the system wins). */
.button[data-variant='link'] {
  --button-bg: transparent;
  --button-text: var(--color-brand);
  box-shadow: none;
  text-decoration: underline;
  text-underline-offset: 4px;
}

.button[data-variant='link']:hover:not(:disabled) {
  --button-bg: transparent; /* don't inherit the base button's hover fill */
  text-decoration-color: transparent;
}

/* EXCEPTION: destructive: the fixed fill tokens (600-series in both themes):
   the dark theme's --color-error is a light red meant for text, and the white
   label would be unreadable on it. */
.button[data-variant='destructive'] {
  --button-bg: var(--color-error-fill);
}

.button[data-variant='destructive']:hover:not(:disabled) {
  --button-bg: var(--color-error-fill-hover);
}

.button[data-variant='destructive']:active:not(:disabled) {
  --button-bg: var(--color-error-fill-press);
}

/* (disabled is handled by the base .button opacity rule above) */

/* EXCEPTION: icon-only: square (or circle), centred icon, no label.
   Larger glyph than a labelled button (24 vs 20). position: relative so a
   .badge can pin to the corner. Composes with every variant and size. */
.button[data-icon] {
  inline-size: var(--button-height);
  padding-inline: 0;
  --button-icon: 1.125rem; /* ~18, a touch larger than a labelled button */
  position: relative;
}

.button[data-icon][data-shape='circle'] {
  --button-radius: var(--radius-pill);
}

/* EXCEPTION: block: full-width (e.g. auth buttons) */
.button[data-block] {
  display: flex;
  inline-size: 100%;
}

/* EXCEPTION: sizes */
.button[data-size='small'] {
  --button-height: var(--control-h-sm); /* 32 (h-8) */
  --button-px: 0.75rem; /* 12 (px-3) */
}

.button[data-size='large'] {
  --button-height: var(--control-h-lg); /* 40 (h-10) */
  --button-px: 1.5rem; /* 24 (px-6) */
}

/* ===== blocks/card.css ===== */
/*
CARD BLOCK
Raised surface. In dark mode elevation comes from the lighter
surface color, not the shadow.

EXCEPTIONS
.card[data-width='narrow']: centered, capped at reading/form width;
handy for auth pages.
*/
.card {
  background: var(--color-bg-raised);
  border: var(--stroke);
  border-radius: var(--radius-l); /* ~14 (shadcn rounded-xl) */
  box-shadow: var(--shadow-sm);
  /* shadcn card: p-6 (24) by default. --card-padding feeds the `padding`
     shorthand, so callers can pass a block/inline pair to override per card. */
  padding: var(--card-padding, var(--space-m));
  font-size: var(--step--1); /* card content is 14 (shadcn card text-sm) */
}

/* Card titles are compact (shadcn CardTitle), not the big content heading
   scale; the description sits at the card's 14px. */
.card :is(h1, h2) {
  font-size: var(--step-1);
  line-height: var(--leading-snug);
}
.card :is(h3, h4, h5, h6) {
  font-size: var(--step-0);
}

.card[data-width='narrow'] {
  inline-size: 100%;
  max-inline-size: var(--wrapper-max-narrow, 37.5rem);
  margin-inline: auto;
}

/* A card that IS a link: a feature/stat card that navigates. An <a> is
   inline by default, so make it a column box (icon → title → text stack),
   reset the link paint, and hint interactivity on hover with a stronger
   border. block-size 100% so cards in a grid row stay equal height. */
a.card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  block-size: 100%;
  text-decoration: none;
  color: inherit;
}

a.card:hover {
  border-color: var(--color-stroke-strong);
  /* Layer the wash OVER the raised surface rather than replacing it: in dark
     mode the raised card is lighter than the page, so swapping in the
     translucent fill would make the card darker on hover, not lighter. */
  background-image: linear-gradient(var(--color-fill-weak), var(--color-fill-weak));
}

/* ===== blocks/data-list.css ===== */
/*
DATA-LIST BLOCK
A label/value list for read-only detail panels (e.g. account settings).
A <dl> where each term/description pair sits on a row (label left, value right),
divided by a hairline. On narrow screens the value wraps under its label.

  <dl class="data-list">
    <div><dt>First name</dt><dd>John</dd></div>
    <div><dt>Last name</dt><dd>Smith</dd></div>
  </dl>
*/
.data-list {
  margin: 0;
}

.data-list > div {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-m);
  padding-block: var(--space-m);
  border-block-end: var(--stroke);
}

.data-list dt {
  flex: 1 1 8rem;
  color: var(--color-text);
  font-weight: var(--weight-semibold);
}

.data-list dd {
  flex: 3 1 12rem;
  margin: 0;
  color: var(--color-text-weak);
}

/* ===== blocks/dialog.css ===== */
/*
DIALOG BLOCK
Paints the native <dialog> element used as a modal. The element itself gives
you the focus trap, Esc-to-close, page inertness, and top-layer stacking for
free; this block only styles the surface and backdrop.

  <dialog class="dialog" id="confirm">
    <form method="dialog" class="flow">
      <h2>Delete project?</h2>
      <p>This can't be undone.</p>
      <div class="cluster" style="--gutter: var(--space-2xs); justify-content: flex-end">
        <button class="button" data-variant="secondary" value="cancel">Cancel</button>
        <button class="button" data-variant="destructive" value="delete">Delete</button>
      </div>
    </form>
  </dialog>

Open it declaratively with the Invoker Commands API; no JavaScript on modern
browsers (Baseline late 2025); a one-line feature-detected fallback covers
older ones (see index.html):
  <button class="button" command="show-modal" commandfor="confirm">Delete…</button>
Closing needs no script anywhere: any <button> inside a <form method="dialog">
closes the dialog and reports its value.
*/
.dialog {
  inline-size: min(32rem, calc(100% - var(--space-l)));
  max-block-size: calc(100% - var(--space-xl));
  overflow: auto;
  padding: var(--space-m); /* p-6 (24) */
  border: var(--stroke);
  border-radius: var(--radius-m);
  background: var(--color-bg-overlay);
  color: var(--color-text);
  font-size: var(--step--1); /* 14 chrome text */
  margin: auto; /* centre in the viewport (a modal dialog's UA default) */
  box-shadow: var(--shadow-overlay);
}

/* Dialog title ~18 (shadcn DialogTitle text-lg), not the big content h2 scale */
.dialog :is(h1, h2, h3) {
  font-size: var(--step-1);
  line-height: var(--leading-fine);
}

.dialog p {
  font-size: var(--step--1);
  color: var(--color-text-weak);
}

.dialog::backdrop {
  background: var(--color-scrim);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

/* ===== blocks/file-upload.css ===== */
/*
FILE-UPLOAD BLOCK
A dropzone wrapped around a real <input type="file">. The dropzone IS the
enhancement: the markup is just a native file
input (so Django's default {{ form }} output works as-is), and a small script
wraps it in this structure, hiding the native input and wiring drag & drop. So
without JavaScript you get the plain native control (styled in forms.css); the
"Drag and drop files here" text only appears once the script can back it up.

The script builds, around each <input type="file">:

  <label class="file-upload">
    <input type="file" class="visually-hidden" name="attachment" />
    <span class="file-upload__main">
      <span class="file-upload__icon"><svg…upload/></span>
      <span class="file-upload__text">
        <strong>Drag and drop files here</strong>
        <span class="file-upload__hint">…from the input's data-hint, if any</span>
      </span>
    </span>
    <span class="button" data-variant="outline" data-size="small">Browse files</span>
  </label>
  <ul class="file-list">…one .file-item per chosen file, each removable…</ul>

The label wraps the input, so clicking or keyboard-activating the zone opens the
picker; "Browse files" is a span styled as a button (a real <button> inside the
label would swallow the click), keeping the whole zone one big label. The file
list is a SIBLING of the label, not inside it, so its remove buttons are real
clicks and don't re-open the picker.
*/
.file-upload {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-m); /* 24 */
  padding: var(--space-l); /* 32 */
  border: 1px dashed var(--color-stroke-strong);
  border-radius: var(--radius-m); /* 10 */
  background: var(--color-fill-weak); /* fill/weaker */
  text-align: center;
  cursor: pointer;
  transition:
    background-color var(--transition-fast),
    border-color var(--transition-fast);
}

.file-upload__main {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-s); /* 16 */
}

/* Upload icon in a neutral-tinted circle. */
.file-upload__icon {
  display: inline-flex;
  padding: var(--space-xs); /* 12 */
  border-radius: var(--radius-pill);
  background: var(--color-fill);
  color: var(--color-text-weak);
}

.file-upload__icon > svg {
  inline-size: 1.5rem; /* 24 */
  block-size: 1.5rem;
}

.file-upload__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs); /* 4 */
}

.file-upload strong {
  color: var(--color-text);
  font-weight: var(--weight-semibold);
}

.file-upload__hint {
  font-size: var(--step--1);
  color: var(--color-text-weak);
}

/* The script adds this while a file is dragged over the zone. */
.file-upload[data-dragover] {
  border-color: var(--color-brand);
  background: var(--color-brand-bg);
}

/* The input is visually hidden but still focusable; show its focus on the zone. */
.file-upload:has(input:focus-visible) {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* ERROR STATE
   Django marks an invalid file field's <input> with aria-invalid; the dropzone
   wraps that input, so :has lets the whole zone go red: dashed error border,
   red wash, red icon circle, and a neutral (de-emphasised) Browse button. The
   field's error message is the global .errorlist above. */
.file-upload:has(input[aria-invalid='true']) {
  border-color: var(--color-error-strong);
  background: var(--color-error-bg);
}

.file-upload:has(input[aria-invalid='true']) .file-upload__icon {
  background: var(--color-error-bg); /* a second error wash over the zone → reads slightly deeper */
  color: var(--color-error);
}

/* The Browse button keeps its variant in the error state; the red zone, icon
   and message carry the error, so the button stays consistent. */

/* FILE LIST
   The script lists the chosen files below the dropzone (outside the label, so
   the remove buttons are clickable). Each row: a document icon, the name + size,
   and a remove control. */
.file-list {
  list-style: none;
  margin-block: var(--space-s) 0;
  padding: 0;
}

.file-item {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding-block: var(--space-s);
  border-block-end: var(--stroke);
}

.file-item__icon {
  display: inline-flex;
  flex: none;
  padding: var(--space-xs);
  border-radius: var(--radius-s);
  background: var(--color-fill-weak);
  color: var(--color-text-weak);
}

.file-item__icon > svg {
  inline-size: 1.25rem;
  block-size: 1.25rem;
}

.file-item__info {
  display: flex;
  flex-direction: column;
  min-inline-size: 0; /* let a long name truncate instead of pushing the button off */
  margin-inline-end: auto;
}

.file-item__name {
  color: var(--color-text);
  font-weight: var(--weight-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-item__size {
  font-size: var(--step--1);
  color: var(--color-text-weak);
}

.file-item__remove {
  display: inline-flex;
  flex: none;
  padding: var(--space-2xs);
  border: 0;
  border-radius: var(--radius-s);
  background: transparent;
  color: var(--color-text-weak);
  cursor: pointer;
  transition:
    background-color var(--transition-fast),
    color var(--transition-fast);
}

.file-item__remove:hover {
  background: var(--color-fill);
  color: var(--color-text);
}

.file-item__remove > svg {
  inline-size: 1.25rem;
  block-size: 1.25rem;
}

/* ===== blocks/pagination.css ===== */
/*
PAGINATION BLOCK
Page navigation. A wrapping row of page controls
with an optional "Showing X of Y" summary pushed to the trailing edge; the
summary drops below and falls flush-left when space runs out, the same
auto-margin idiom the site-head uses. Intrinsic: no breakpoints, survives any
page count, font size, or zoom.

  <nav class="pagination" aria-label="Pagination">
    <ul role="list">
      <li><a href="?page=1" rel="prev"><svg…/> Previous</a></li>
      <li><a href="?page=1">1</a></li>
      <li><a href="?page=2" aria-current="page">2</a></li>
      <li><span aria-hidden="true">…</span></li>
      <li><a href="?page=10">10</a></li>
      <li><a href="?page=3" rel="next">Next <svg…/></a></li>
    </ul>
    <p class="pagination__summary">Showing 11–20 of 128</p>
  </nav>

Disabled steps (Previous on page 1, Next on the last page): render a plain <span>
instead of an <a>: non-interactive and out of the tab order. It's not a control,
so it needs no aria-disabled (which is inert on a roleless <span> anyway); the
absence of a link is what conveys "no previous/next page".
*/
.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-m);
}

/* On phones, drop the Previous/Next labels and keep just the chevrons, the same
   move shadcn makes with `hidden sm:block`. */
@media (width <= 30rem) {
  .pagination__text {
    display: none;
  }
}

.pagination > ul {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs); /* 8 */
  /* role="list" but unclassed, so own the spacing instead of leaning on the reset.
     Push the summary to the end; once the row wraps the auto margin collapses and
     the summary falls flush-left. */
  margin-block: 0;
  margin-inline: 0 auto;
  padding: 0;
}

/* Each control is a 40px-min hit target, centred, with small weak-grey text. */
.pagination li > :is(a, span) {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3xs);
  min-inline-size: 2.5rem; /* 40 */
  min-block-size: 2.5rem;
  padding-inline: var(--space-2xs);
  border: 1px solid transparent; /* reserve space so the current page can't shift the row */
  border-radius: var(--radius-s);
  color: var(--color-text-weak);
  font-size: var(--step--1); /* 14 */
  line-height: var(--leading-mid);
  text-decoration: none;
}

.pagination a:hover {
  background: var(--color-fill);
  color: var(--color-text);
}

/* Current page: the outline-button look: subtle border, surface fill, shadow. */
.pagination a[aria-current='page'] {
  border-color: var(--color-stroke);
  background: var(--color-outline-bg);
  box-shadow: var(--shadow-xs);
  color: var(--color-text);
}

/* Non-interactive controls: the ellipsis and disabled (first/last) steps. */
.pagination li > span {
  color: var(--color-text-weaker);
}

.pagination svg {
  inline-size: 1.25rem; /* 20 */
  block-size: 1.25rem;
  flex: none;
}

.pagination__summary {
  color: var(--color-text-weak);
  font-size: var(--step--1);
}

/* ===== blocks/prose.css ===== */
/*
PROSE BLOCK
Long-form text context: sets flow rhythm and a comfortable measure.
Use together with .flow.
*/
.prose {
  --flow-space: var(--space-m);
  text-wrap: pretty;
}

/* Reading measure. Applied to a bare text element that itself carries .prose
   (e.g. <p class="prose">) and to the text-level children of a .prose container,
   so figures, tables, media and code blocks in a prose article still span the
   full width while running text stays at a comfortable measure. */
p.prose,
.prose :where(p, li, dl, blockquote, figcaption) {
  max-inline-size: 65ch;
}

/* Headings hold a tighter measure of their own. */
.prose h1 {
  max-inline-size: 20ch;
}

.prose :is(h2, h3) {
  max-inline-size: 35ch;
}

/* Long words in a big heading can overflow a narrow viewport even with fluid
   type; let the browser break or hyphenate them rather than blow out. */
.prose :is(h1, h2, h3) {
  overflow-wrap: anywhere;
  hyphens: auto;
}

/* More air above headings, less below them */
.prose :is(h2, h3, h4) {
  --flow-space: var(--space-xl);
}

/* …but an eyebrow hugs the heading it introduces, even in prose. */
.prose .eyebrow + * {
  --flow-space: var(--space-2xs);
}

.prose :is(h1, h2, h3, h4) + * {
  --flow-space: var(--space-s);
}

/* Figures and tables get a little extra breathing room, before and after. */
.prose :is(figure, table),
.prose :is(figure, table) + * {
  --flow-space: var(--space-l);
}

/* Tight rhythm between list items (and nested lists) in prose. */
.prose :is(ul, ol):not([class]) li + li,
.prose :is(ul, ol):not([class]) li > :is(ul, ol) {
  --flow-space: var(--space-xs);
}

.prose hr {
  --flow-space: var(--space-2xl);
}

/* Media in prose gets a subtle frame and scales proportionally (height follows
   width even when the element carries width/height attributes). */
.prose :is(img, picture, video) {
  border: var(--stroke-strong);
  block-size: auto;
}

/* Once there's room, stop breaking/hyphenating headings. */
@media (width >= 47.5rem) {
  .prose :is(h1, h2, h3) {
    overflow-wrap: unset;
    hyphens: unset;
  }
}

/* ===== blocks/search.css ===== */
/*
SEARCH BLOCK
A search field: a leading magnifier next to the
input, optionally with a trailing submit button. The wrapper is the field box,
so the icon and button sit inside one border; the <input type="search"> keeps
its native clear button (which respects color-scheme).

  <div class="search">
    <svg…search/>
    <input type="search" placeholder="Search" aria-label="Search" />
  </div>

  <form class="search" role="search" action="…">
    <svg…search/>
    <input type="search" name="q" placeholder="Search" />
    <button class="button" type="submit">Search</button>
  </form>
*/
.search {
  display: flex;
  align-items: center;
  gap: 0.5rem; /* 8, icon→text; shadcn pl-8 (32) minus 8 inset + 16 icon */
  block-size: var(--control-h); /* aligns with buttons + inputs, any text size */
  padding-inline: 0.5rem 0.75rem; /* 8 left (icon at shadcn's left-2), 12 right (px-3) */
  font-size: var(--text-input); /* the input inside inherits 16/14 */
  border: var(--stroke);
  border-radius: var(--radius-s);
  background: var(--color-field);
  color: var(--color-text-weak);
  transition:
    background-color var(--transition-fast),
    border-color var(--transition-fast);
}

/* .search may be a <form> with a submit button; the global "form > * + *" field
   flow would add a top margin to the input/button, but here they're a flex row,
   not stacked fields; that margin would distort the field's height. Cancel it. */
.search > * + * {
  margin-block-start: 0;
}

.search:hover:not(:focus-within) {
  background-color: var(--color-fill);
}

.search:has(input:focus-visible) {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.search > svg {
  inline-size: 1rem; /* 16 (shadcn size-4) */
  block-size: 1rem;
  flex: none;
}

/* The wrapper owns the box, so strip the input's own border/background/padding
   (this sits in the blocks layer, so it wins over the global input styles). */
.search > input {
  flex: 1 0 0;
  min-inline-size: 0;
  min-block-size: 0;
  block-size: auto; /* fill the wrapper's height, not the global control height */
  padding: 0; /* the wrapper sets the field height; a tall input would inflate it */
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none; /* the global input shadow-xs would read as a faint underline */
}

.search > input:hover {
  background: transparent; /* the wrapper carries the hover fill, not the input */
}

/* The wrapper shows the focus ring (see :has above), so the input must NOT draw
   its own; otherwise you get a second, square ring inside the rounded box.
   Guarded: without :has() the wrapper can't show the ring, so the input keeps
   its own outline rather than losing focus visibility entirely. */
@supports selector(:has(*)) {
  .search > input:focus-visible {
    outline: none;
  }
}

/* EXCEPTION: small: a compact field for toolbars (32 tall) */
.search[data-size='small'] {
  block-size: var(--control-h-sm); /* 32, matches the small button + segmented */
}

/* A trailing submit button joins the field's right edge, so the wrapper drops its
   own right padding; the button fills that side instead (the padding is only
   there to keep the placeholder off the right border when there's no button). */
.search:has(.button) {
  padding-inline-end: 0;
}

/* It stretches to the field's inner height (so the field keeps its own height, the
   button doesn't inflate it), and negative margins pull it over the wrapper's 1px
   border on its top, bottom and right so it reads as part of the box. Only its
   right corners are rounded. It's part of the field, not a floating control; no
   raised shadow. */
.search > .button {
  align-self: stretch;
  block-size: auto;
  margin-block: -1px;
  margin-inline: auto -1px;
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  box-shadow: none;
}

/* ===== blocks/segmented.css ===== */
/*
SEGMENTED BLOCK
A compact switch between a few mutually-exclusive options (a view/mode toggle).
Radio inputs styled as a pill group, so it works and is keyboard-navigable
(arrow keys) with no JavaScript. The checked segment reads as a raised pill on a
sunken track.

  <div class="segmented" role="radiogroup" aria-label="View">
    <label><input type="radio" name="view" checked /> Day</label>
    <label><input type="radio" name="view" /> Week</label>
    <label><input type="radio" name="view" /> Month</label>
  </div>
*/
.segmented {
  display: inline-flex;
  min-block-size: var(--control-h); /* aligns with the other controls */
  padding: 0.1875rem; /* p-1 inset so the active pill floats (shadcn tabs) */
  border-radius: var(--radius-m);
  background: var(--color-muted); /* muted track, no border (shadcn TabsList) */
}

.segmented label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  padding: var(--space-3xs) var(--space-s); /* py-1 px-3 (shadcn tab trigger) */
  border: 1px solid transparent; /* reserved so the active border adds no shift */
  border-radius: var(--radius-xs);
  color: var(--color-text-weak);
  font-size: var(--step--1); /* 14, medium (shadcn tab trigger) */
  font-weight: var(--weight-medium);
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  margin-block-end: 0; /* override the global label margin */
  transition:
    color var(--transition-fast),
    background-color var(--transition-fast),
    border-color var(--transition-fast);
}

/* The radio is visually hidden but stays focusable; the label is the control,
   BUT only where :has() can paint the active pill. Without :has() the pill rule
   is dropped, so we keep the native radio visible: it still shows which segment
   is selected. (Progressive enhancement: the fallback marks the selection.) */
@supports selector(:has(*)) {
  .segmented input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
  }
}

/* EXCEPTION: small: a compact track on the shared small control height, so it
   sits level with the small search field and button on the same toolbar row. */
.segmented[data-size='small'] {
  min-block-size: var(--control-h-sm); /* 32, level with the small search + button */
}

.segmented[data-size='small'] label {
  padding: var(--space-3xs) var(--space-xs); /* let the 32 track height drive it */
  font-size: var(--step--1); /* 14 */
}

.segmented label:hover {
  color: var(--color-text); /* lighten the label, no background change (shadcn) */
}

.segmented label:has(input:checked) {
  background: var(--color-muted-raised); /* lifted pill, lighter than the track */
  border-color: var(--segment-active-border); /* none in light, a hairline in dark */
  color: var(--color-text);
  box-shadow: var(--shadow-sm); /* shadcn lifts the active tab with shadow-sm */
}

.segmented label:has(input:focus-visible) {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* ===== blocks/sidenav.css ===== */
/*
SIDENAV BLOCK
The application side navigation, used inside the app-shell composition: a brand
mark, an optional search, a list of nav links (icon + label, optional trailing
badge or tag), and a footer pinned to the bottom (an upgrade card, or the
signed-in user). The app-shell handles the responsive column ↔ drawer behaviour;
this block is just the contents.

  <aside class="sidenav">
    <a class="sidenav__brand" href="/">…logo…</a>
    <form class="search" role="search">…</form>
    <nav class="sidenav__nav" aria-label="Main">
      <ul>
        <li><a class="sidenav__link" aria-current="page"><svg…/>Home</a></li>
        <li><a class="sidenav__link"><svg…/>Reports <span class="badge" …>8</span></a></li>
      </ul>
    </nav>
    <div class="sidenav__foot">…</div>
  </aside>
*/
.sidenav {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
  padding: var(--space-m);
  background: var(--color-bg);
  border-inline-end: var(--stroke);
  overflow-y: auto;
}

.sidenav__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--color-text);
  text-decoration: none;
}

/* The nav pulls out a touch so each link is an inset rounded pill (shadcn
   sidebar), while the link's own padding keeps its content aligned with the
   brand and search above. */
.sidenav__nav {
  margin-inline: calc(var(--space-2xs) * -1);
}

.sidenav__nav ul {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.sidenav__link {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs);
  border-radius: var(--radius-s);
  color: var(--color-text-weak);
  font-weight: var(--weight-normal); /* regular; nav links aren't bold */
  text-decoration: none;
  transition:
    background-color var(--transition-fast),
    color var(--transition-fast);
}

.sidenav__link > svg {
  inline-size: 1.25rem;
  block-size: 1.25rem;
  flex: none;
}

/* A trailing badge/tag (a count, or a "New" pill) hugs the end of the row. */
.sidenav__link > :is(.badge, .tag) {
  margin-inline-start: auto;
}

.sidenav__link:hover {
  background: var(--color-fill);
  color: var(--color-text);
}

/* Active item: a neutral accent fill, rounded, medium-weight dark text; no
   brand wash or bar (shadcn sidebar active = bg-accent). */
.sidenav__link[aria-current='page'] {
  background: var(--color-fill);
  color: var(--color-text);
  font-weight: var(--weight-medium);
}

/* Push the footer (upgrade card / user) to the bottom of the column. */
.sidenav__foot {
  margin-block-start: auto;
}

/* EXCEPTION: sunken: a .card turned into a quiet sunken panel (no raised
   shadow), its link riding the surrounding text colour, not brand. Used for the
   sidenav footer's upgrade promo. Comes after the card block so the box-shadow
   override wins on source order. */
.card[data-variant='sunken'] {
  background: var(--color-bg-sunken);
  box-shadow: none;
}

.card[data-variant='sunken'] a {
  color: currentColor;
}

/* ===== blocks/app-bar.css ===== */
/*
APP-BAR BLOCK
The application top bar, paired with the app-shell + sidenav. A flex row: the
mobile hamburger (.app-shell__menu) and brand at the start, an actions group
(quiet icon buttons + the account menu) pushed to the end with an auto margin.
The brand shows only in the mobile bar (the sidenav carries it on desktop). On
phones the icon actions hide; the account name yields to the avatar via the
shared .site-head__menu-name rule (see site-head).

  <header class="app-bar">
    <label class="app-shell__menu" for="app-nav">☰</label>
    <a class="app-bar__brand" href="/">Brand</a>
    <div class="app-bar__actions">
      <button class="button" data-icon aria-label="Help">…</button>
      <span class="badge-host app-bar__icon">…<span class="badge" data-badge="dot"></span></span>
      <details class="site-head__menu">
        <summary class="app-bar__user">
          <span class="avatar" data-size="small">JS</span>
          <span class="site-head__menu-name">John Smith</span>
          <svg …/>
        </summary>
        <ul>… <li class="site-head__menu-head">…</li> …</ul>
      </details>
    </div>
  </header>
*/
.app-bar {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-xs) var(--page-gutter);
  border-block-end: var(--stroke);
}

.app-bar__actions {
  margin-inline-start: auto;
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}

/* Top-bar icon buttons are quiet (grey, not brand) and a touch larger than
   the shared control height, a generous 40px hit target. */
.app-bar .button[data-icon] {
  --button-text: var(--color-text-weak);
  --button-height: 2.5rem; /* 40 */
}

/* The bell's notification dot sits on the bell glyph (centred in the 40px
   button), not the button's outer corner, matching the bare-icon bell. */
.app-bar__icon.badge-host > .badge[data-badge='dot'] {
  inset-block-start: 0.375rem; /* 6 */
  inset-inline-end: 0.375rem;
}

.app-bar__user {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  white-space: nowrap;
}

/* The brand only shows in the mobile top bar (the sidenav carries it on
   desktop, so the two breakpoints must match). */
.app-bar__brand {
  display: none;
  gap: var(--space-2xs);
  font-size: var(--step-0);
  line-height: 1; /* hug the glyphs so the wordmark centres with the icons */
  color: var(--color-text);
  text-decoration: none;
}

@media (width < 60rem) {
  .app-bar__brand {
    display: inline-flex;
    align-items: center;
  }
}

/* On phones the icon actions hide to make room for the brand; the account
   name's yield-to-avatar rule is shared with the site-head. */
@media (width < 30rem) {
  .app-bar__icon {
    display: none;
  }
}

/* ===== blocks/site-head.css ===== */
/*
SITE HEAD BLOCK
Page header. Expects compositions to do the layout:

  <header class="site-head">
    <div class="wrapper repel">
      <a class="site-head__brand" href="/">Brand</a>
      <nav aria-label="Primary"><ul class="cluster" role="list">…</ul></nav>
      <details class="site-head__menu">…</details>
    </div>
  </header>
*/
.site-head {
  border-block-end: var(--stroke);
  background: var(--color-bg);
  /* Vertical rhythm from padding, not a fixed height: the bar grows with its
     content and never crowds it, however the row wraps. Layout (and wrapping)
     is left entirely to the wrapper + repel + cluster compositions. */
  padding-block: var(--space-s);
}

/* If the bar holds a nav-disclosure, make the wrapper its query container so
   the disclosure can measure the space available to it (see nav-disclosure). */
.site-head > .wrapper:has(.nav-disclosure) {
  container: nav-bar / inline-size;
}

.site-head__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  font-size: var(--step-1);
  font-weight: var(--weight-bold);
  color: var(--color-text);
  text-decoration: none;
}

/* Brand stays at the start; an auto margin on the nav pushes it (and the auth
   group after it) to the end on a full row. When the bar wraps, the auto margin
   has no free space to absorb, so each wrapped row falls flush-left, aligned
   with the brand, at any width, zoom, or number of links. No breakpoints. */
.site-head nav {
  margin-inline-end: auto;
}

/* Nav links carry a brand underline when active or hovered, nothing otherwise.
   text-decoration leaves the box untouched, so links stay aligned with the
   brand and buttons however the bar wraps: no full-height tabs, no inline
   padding to indent a wrapped row (Every Layout / CUBE). Scoped to the direct
   nav so it doesn't reach into a nav-disclosure's dropdown, which styles its
   own links. */
.site-head > .wrapper > nav a {
  color: var(--color-text);
  text-decoration: underline;
  text-decoration-color: var(--color-brand);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.3em;
}

/* Any aria-current value keeps the underline: "page" on a page nav, "true" on
   an in-page section nav like the kitchen sink's. */
.site-head > .wrapper > nav a:not(:hover):not([aria-current]) {
  text-decoration: none;
}

/* Account menu: a <details> disclosure. Opens and closes from its <summary>
   with no JS; a tiny optional script adds outside-click + Escape light-dismiss
   (see the snippet in index.html / your project's JS). */
.site-head__menu {
  position: relative;
}

/* Keep the account-menu dropdown on-screen when the marketing header wraps.

   Fallback (all browsers, no JS): pin the menu to the inline-end so the row wraps
   it to the right and its right-aligned dropdown opens into the viewport. Scoped
   to .site-head so the app-bar's account menu (grouped with its sibling icons) is
   left alone. */
.site-head .site-head__menu {
  margin-inline-start: auto;
}

/* Where anchor positioning is supported, leave the menu in its natural place
   (so it wraps to the left like the rest of the row) and let the dropdown flip
   its inline alignment to stay on-screen instead. position: fixed makes the flip
   evaluate against the viewport rather than the narrow menu box. */
@supports (position-try-fallbacks: flip-inline) {
  .site-head .site-head__menu {
    margin-inline-start: 0;
    position: static;
  }

  .site-head .site-head__menu > summary {
    anchor-name: --account-menu;
  }

  .site-head .site-head__menu > ul {
    position: fixed;
    position-anchor: --account-menu;
    inset: auto;
    inset-block-start: anchor(bottom);
    inset-inline-end: anchor(right);
    margin-block-start: var(--space-3xs);
    position-try-fallbacks: flip-inline;
  }
}

.site-head__menu > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  list-style: none;
  cursor: pointer;
  padding: var(--space-2xs) var(--space-xs);
  border-radius: var(--radius-s);
  color: var(--color-text-weak);
  font-size: var(--step--1);
}

.site-head__menu > summary::-webkit-details-marker {
  display: none;
}

.site-head__menu > summary:hover {
  background: var(--color-fill);
  color: var(--color-text);
}

.site-head__menu > summary > svg {
  inline-size: 1em;
  block-size: 1em;
  transition: rotate var(--transition-fast);
}

.site-head__menu[open] > summary > svg {
  rotate: 180deg;
}

/* On phones the account name yields to the avatar; the chevron stays as the
   menu's open/close affordance. Put the class on the name span only when an
   avatar sits next to it, so the summary never collapses to a bare chevron. */
@media (width < 30rem) {
  .site-head__menu-name {
    display: none;
  }
}

.site-head__menu > ul {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: calc(100% + var(--space-3xs));
  z-index: 10;
  min-inline-size: 14rem;
  /* p-1 around the items so each row's hover highlight is an inset rounded
     rectangle, not a full-bleed band (shadcn dropdown). */
  padding: var(--space-3xs);
  margin: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.125rem; /* a hair between rows, so adjacent highlights don't touch */
  font-size: var(--step--1);
  background: var(--color-bg-overlay);
  border: var(--stroke);
  border-radius: var(--radius-s);
  box-shadow: var(--shadow-md);
}

/* Each row is an icon + label; a trailing badge/tag/toggle floats to the end. */
.site-head__menu > ul :is(a, .site-head__menu-row) {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 0.375rem var(--space-2xs); /* py-1.5 px-2 (shadcn item) */
  border-radius: var(--radius-xs); /* rounded-sm */
  color: var(--color-text);
  text-decoration: none;
}

.site-head__menu > ul :is(a, .site-head__menu-row) > svg {
  inline-size: 1rem;
  block-size: 1rem;
  flex: none;
  color: var(--color-text-weak);
}

.site-head__menu > ul :is(a, .site-head__menu-row) > :is(.badge, .tag, .toggle) {
  margin-inline-start: auto;
}

.site-head__menu > ul a:hover {
  background: var(--color-fill);
}

.site-head__menu > ul a:active {
  background: var(--color-fill-press);
}

/* Current location: mirrors the sidenav's aria-current treatment. Comes after
   :hover and :active (equal specificity, so source order decides) so a hovered or
   pressed current item keeps its brand wash rather than the neutral state fill. */
.site-head__menu > ul a[aria-current='page'] {
  background: var(--color-fill);
  color: var(--color-text);
  font-weight: var(--weight-medium);
}

/* A divider between menu groups (after the header, before sign out). Same
   margin as the menu-head's margin-block-end, so the gap around a mid-menu
   divider matches the gap under the identity header's border. */
.site-head__menu hr {
  margin-block: var(--space-3xs);
  border: 0;
  border-block-start: var(--stroke);
}

/* The account menu's identity header row (avatar + name + email), pinned at the
   top of the dropdown. Even top/bottom: the wrapping <ul> already adds 8px above,
   so the block padding matches that below. */
.site-head__menu-head {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs);
  border-block-end: var(--stroke);
  margin-block-end: var(--space-3xs);
}

/* ===== blocks/nav-disclosure.css ===== */
/*
NAV-DISCLOSURE BLOCK
A navigation that shows its links inline when the bar is wide enough and
collapses them behind a "Menu" toggle when it isn't, built on the native
<details>/<summary>, so the toggle is keyboard-accessible with no JavaScript.
(An optional script adds outside-click + Escape light-dismiss; see index.html.)
This is the alternative to the site-head's default wrapping nav, for menus with
enough items that wrapping into a tall stack isn't wanted.

A nav genuinely needs to know whether its links fit, so this is the one place a
query earns its keep, and it's a CONTAINER query: it reacts to the width of
its container (named `nav-bar`), not the viewport, staying true to the
intrinsic, breakpoint-free spirit. In the site-head the wrapper declares that
container automatically (see site-head.css); elsewhere, put the block inside an
element with `container: nav-bar / inline-size`.

  <details class="nav-disclosure">
    <summary>Menu <svg…chevron/></summary>
    <nav aria-label="Primary">
      <ul class="cluster" role="list"> … many links … </ul>
    </nav>
  </details>
*/
.nav-disclosure {
  position: relative;
}

/* The toggle: a <summary> painted as a ghost button, native marker removed. */
.nav-disclosure > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-s);
  border-radius: var(--radius-s);
  list-style: none;
  cursor: pointer;
  color: var(--color-text);
  font-weight: var(--weight-semibold);
}

.nav-disclosure > summary::-webkit-details-marker {
  display: none;
}

.nav-disclosure > summary:hover {
  background: var(--color-fill);
}

.nav-disclosure > summary > svg {
  inline-size: 1.25rem;
  block-size: 1.25rem;
  transition: rotate var(--transition-fast);
}

.nav-disclosure[open] > summary > svg {
  rotate: 180deg;
}

/* Collapsed: the links sit in a dropdown panel, revealed when [open]. */
.nav-disclosure > nav {
  position: absolute;
  inset-block-start: calc(100% + var(--space-2xs));
  inset-inline-end: 0; /* the toggle sits on the trailing edge, so open inward */
  z-index: 10;
  min-inline-size: 12rem;
  /* p-1 around the items so each row's hover is an inset rounded rectangle. */
  padding: var(--space-3xs);
  font-size: var(--step--1);
  background: var(--color-bg-overlay);
  border: var(--stroke);
  border-radius: var(--radius-s);
  box-shadow: var(--shadow-md);
}

.nav-disclosure > nav ul {
  flex-direction: column;
  align-items: stretch;
  gap: 0.125rem; /* a hair between rows, so adjacent highlights don't touch */
}

.nav-disclosure > nav a {
  display: block;
  padding: 0.375rem var(--space-2xs); /* py-1.5 px-2 (shadcn item) */
  border-radius: var(--radius-xs);
  color: var(--color-text);
  text-decoration: none;
}

.nav-disclosure > nav a:hover {
  background: var(--color-fill);
}

.nav-disclosure > nav a:active {
  background: var(--color-fill-press);
}

/* Current item: a neutral accent fill, rounded; no brand bar (shadcn). */
.nav-disclosure > nav a[aria-current='page'] {
  background: var(--color-fill);
  color: var(--color-text);
  font-weight: var(--weight-medium);
}

/* Wide enough → links inline, toggle gone. Container query: reacts to the
   bar's own width, not the device. Link appearance is handed back to the
   surrounding nav (e.g. the site-head's underline-on-active links). */
@container nav-bar (width >= 40rem) {
  .nav-disclosure > summary {
    display: none;
  }

  .nav-disclosure > nav {
    display: block; /* reveal closed-<details> content (older engines) */
    position: static;
    min-inline-size: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .nav-disclosure > nav ul {
    flex-direction: row;
    align-items: center;
    gap: var(--space-m); /* inline spacing, matching the default (cluster) nav's gutter */
  }

  .nav-disclosure > nav a {
    display: inline;
    padding: 0;
    border-radius: 0;
    text-decoration: underline;
    text-decoration-color: var(--color-brand);
    text-decoration-thickness: 2px;
    text-underline-offset: 0.3em;
  }

  .nav-disclosure > nav a:not(:hover):not([aria-current='page']) {
    text-decoration: none;
  }

  .nav-disclosure > nav a:hover,
  .nav-disclosure > nav a:active,
  .nav-disclosure > nav a[aria-current='page'] {
    background: none;
    box-shadow: none;
  }
}

/* Reveal closed-<details> content on engines that hide it via the modern
   ::details-content pseudo (kept in its own rule so unsupporting engines drop
   only this line, not the block above). */
@container nav-bar (width >= 40rem) {
  .nav-disclosure::details-content {
    content-visibility: visible;
  }
}

/* ===== blocks/site-foot.css ===== */
/*
SITE FOOT BLOCK
Page footer. The body is a column flex container (see base.css),
so margin-block-start: auto keeps it at the bottom of short pages.
*/
.site-foot {
  margin-block-start: auto;
  padding-block: var(--space-l);
  border-block-start: var(--stroke);
  background: var(--color-bg-sunken);
  color: var(--color-text-weak);
  font-size: var(--step--1);
}

/* ===== blocks/skip-link.css ===== */
/*
SKIP LINK BLOCK
First focusable element on the page; visually hidden until focused.

  <a class="skip-link" href="#main">Skip to content</a>
*/
.skip-link {
  position: absolute;
  inset-block-start: var(--space-s);
  inset-inline-start: var(--space-s);
  z-index: 100;
  padding: var(--space-2xs) var(--space-s);
  background: var(--color-brand);
  color: var(--color-text-on-fill);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  border-radius: var(--radius-s);
}

.skip-link:not(:focus) {
  border: 0;
  clip: rect(0 0 0 0);
  /* Same visually-hidden recipe (height:auto, width:1px); see the utility. */
  height: auto;
  width: 1px;
  margin: 0;
  overflow: hidden;
  padding: 0;
  white-space: nowrap;
}

/* ===== blocks/tabs.css ===== */
/*
TABS BLOCK
Tabs that work with NO JavaScript. A radio group switches the panels purely in
CSS, so it can't break: it's keyboard-navigable (arrow keys move between the
radios natively, and only the checked one sits in the tab order), and it's
announced as a labelled radio group. The radio is visually hidden; its <label>
is the tab and carries the focus ring and the brand underline.

Panels show by matching the checked tab's position to the panel's position
(:has + :nth-child); no per-instance CSS or id wiring, up to 8 tabs. The tab
strip carries a full-width baseline; the selected tab a brand underline on it.

Best of both: the optional script (see index.html / your project's JS) upgrades
this to a WAI-ARIA tab widget (labels become role=tab, the radios become hidden
state holders, arrow keys + roving tabindex follow the tabs pattern) while the
CSS above still drives which panel shows. No JS → it's a working radio group; JS
→ it's announced as tabs. Either way it switches.

  <div class="tabs">
    <div class="tabs__list" role="radiogroup" aria-label="Sections">
      <label class="tabs__tab">
        <input type="radio" name="sections" class="tabs__radio" checked />
        <span>Overview</span>
      </label>
      <label class="tabs__tab">
        <input type="radio" name="sections" class="tabs__radio" />
        <span>Activity</span>
      </label>
    </div>
    <div class="tabs__panels">
      <div class="tabs__panel">…</div>
      <div class="tabs__panel">…</div>
    </div>
  </div>
*/
.tabs__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  border-block-end: var(--stroke); /* full-width baseline */
}

/* The radio is visually hidden but stays focusable; the label is the tab, but
   only where :has() can draw the active underline AND switch panels. Without
   :has() we keep the native radio visible so the selected tab is still marked
   (and every panel shows; see the panel rules below). */
@supports selector(:has(*)) {
  .tabs__radio {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
  }
}

.tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs);
  margin-block-end: -1px; /* drop the underline onto the baseline (and clear the global label margin) */
  border-block-end: 2px solid transparent;
  color: var(--color-text-weak);
  font-size: var(--step--1);
  font-weight: var(--weight-medium);
  line-height: 1.1; /* control leading (matches the reset); not the wrapping-label leading */
  white-space: nowrap;
  cursor: pointer;
  transition:
    color var(--transition-fast),
    border-color var(--transition-fast);
}

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

.tabs__tab:has(:checked) {
  color: var(--color-text);
  border-block-end-color: var(--color-brand);
}

/* Focus ring on the label: without JS the radio inside is the focus target
   (:has), with JS the label itself becomes the role=tab focus target. */
.tabs__tab:focus-visible,
.tabs__tab:has(:focus-visible) {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.tabs__panel {
  padding-block-start: var(--space-m);
}

/* Panel switching needs :has(). Where it's supported, hide every panel and show
   only the one whose position matches the checked tab (covers up to 8 tabs; add
   more pairs if you ever need them). Where it's NOT supported, this whole block
   is skipped, so every panel stays visible; all content is reachable, just
   stacked instead of tabbed. Never hide a panel we can't switch back. */
@supports selector(:has(*)) {
  .tabs__panel {
    display: none;
  }

  .tabs__list:has(.tabs__tab:nth-child(1) :checked) ~ .tabs__panels > .tabs__panel:nth-child(1),
  .tabs__list:has(.tabs__tab:nth-child(2) :checked) ~ .tabs__panels > .tabs__panel:nth-child(2),
  .tabs__list:has(.tabs__tab:nth-child(3) :checked) ~ .tabs__panels > .tabs__panel:nth-child(3),
  .tabs__list:has(.tabs__tab:nth-child(4) :checked) ~ .tabs__panels > .tabs__panel:nth-child(4),
  .tabs__list:has(.tabs__tab:nth-child(5) :checked) ~ .tabs__panels > .tabs__panel:nth-child(5),
  .tabs__list:has(.tabs__tab:nth-child(6) :checked) ~ .tabs__panels > .tabs__panel:nth-child(6),
  .tabs__list:has(.tabs__tab:nth-child(7) :checked) ~ .tabs__panels > .tabs__panel:nth-child(7),
  .tabs__list:has(.tabs__tab:nth-child(8) :checked) ~ .tabs__panels > .tabs__panel:nth-child(8) {
    display: block;
  }
}

/* ===== blocks/tag.css ===== */
/*
TAG BLOCK
Small rounded-md badge for statuses and categories (shadcn badge): tone-weak
fill + tone-weak border + tone text, font-medium. ~14px by default; a 12px
small variant via data-size.

EXCEPTIONS
data-tone: 'brand' | 'info' | 'success' | 'warning' | 'error' (default neutral)
data-size: 'small' (12px)
*/
.tag {
  --tag-tone: var(--color-text-weak);
  --tag-bg: var(--color-fill);
  --tag-stroke: var(--color-stroke);

  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  padding: 0.125rem var(--space-2xs); /* py-0.5 px-2 (shadcn badge) */
  border: 1px solid var(--tag-stroke);
  border-radius: var(--radius-pill); /* shadcn badge = rounded-full */
  background: var(--tag-bg);
  color: var(--tag-tone);
  font-size: var(--step--1); /* ~14 */
  font-weight: var(--weight-medium);
  line-height: 1.2;
  white-space: nowrap;
}

.tag > svg {
  inline-size: 0.875rem; /* 14 */
  block-size: 0.875rem;
  flex: none;
}

/* EXCEPTION: small: text-xs status badge */
.tag[data-size='small'] {
  font-size: 0.75rem; /* 12 */
}

.tag[data-size='small'] > svg {
  inline-size: 0.75rem; /* 12 */
  block-size: 0.75rem;
}

/* EXCEPTION: tones */
.tag[data-tone='brand'] {
  --tag-tone: var(--color-brand);
  --tag-bg: var(--color-brand-bg);
  --tag-stroke: var(--color-brand-stroke);
}

.tag[data-tone='info'] {
  --tag-tone: var(--color-info);
  --tag-bg: var(--color-info-bg);
  --tag-stroke: var(--color-info-stroke);
}

.tag[data-tone='success'] {
  --tag-tone: var(--color-success);
  --tag-bg: var(--color-success-bg);
  --tag-stroke: var(--color-success-stroke);
}

.tag[data-tone='warning'] {
  --tag-tone: var(--color-warning);
  --tag-bg: var(--color-warning-bg);
  --tag-stroke: var(--color-warning-stroke);
}

.tag[data-tone='error'] {
  --tag-tone: var(--color-error);
  --tag-bg: var(--color-error-bg);
  --tag-stroke: var(--color-error-stroke);
}

/* EXCEPTION: outline: border + foreground, no fill (shadcn outline badge) */
.tag[data-variant='outline'] {
  --tag-tone: var(--color-text);
  --tag-bg: transparent;
  --tag-stroke: var(--color-stroke);
}

/* ===== blocks/th-sort.css ===== */
/*
TH-SORT BLOCK
A sortable column header: a real <button> inside the <th>, inheriting the
header's type and colour, with a trailing direction arrow. Put aria-sort on
the <th> and flip it (and the arrow) when the sort changes.

  <th scope="col" aria-sort="ascending">
    <button class="th-sort" type="button">Date applied <svg…arrow-up/></button>
  </th>
*/
.th-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.th-sort > svg {
  inline-size: 1rem;
  block-size: 1rem;
}

/* ===== blocks/toggle.css ===== */
/*
TOGGLE BLOCK
A switch: a checkbox styled as a 32×18 pill (shadcn switch).
Off: a grey track. On: solid brand fill. The white thumb slides across.
Wrap a checkbox and a label:

  <label class="toggle">
    <input type="checkbox" name="notify">
    <span class="toggle__track"></span>
    Email notifications
  </label>
*/
.toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs); /* 12 */
  font-size: var(--step--1); /* 14 */
  cursor: pointer;
}

/* The checkbox stays in the a11y tree but is visually replaced by the track */
.toggle input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
}

.toggle__track {
  position: relative;
  flex: none;
  inline-size: 2rem; /* 32 (shadcn w-8) */
  block-size: 1.15rem; /* ~18 (shadcn h-[1.15rem]) */
  border-radius: var(--radius-pill);
  background: var(--color-stroke); /* grey off-track (≈ shadcn bg-input) */
  transition: background var(--transition-fast);
}

/* The thumb: centred, slides across when on */
.toggle__track::after {
  content: '';
  position: absolute;
  inset-block-start: 0.0625rem; /* ~1, centred in the 18px track */
  inset-inline-start: 0.0625rem;
  inline-size: 1rem; /* 16 (shadcn switch thumb = size-4) */
  block-size: 1rem;
  border-radius: 50%;
  background: var(--color-toggle-thumb);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.18);
  /* --transition-fast already carries the easing; a second var(--ease) here
     would make the whole declaration invalid at computed-value time. */
  transition: transform var(--transition-fast);
}

/* On */
.toggle input:checked + .toggle__track {
  background: var(--color-brand);
  box-shadow: none;
}

.toggle input:checked + .toggle__track::after {
  transform: translateX(0.875rem); /* 32 − 16 thumb − 2·1 inset = 14 */
}

.toggle input:focus-visible + .toggle__track {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.toggle input:disabled + .toggle__track {
  opacity: 0.5;
  cursor: not-allowed;
}

/* The whole label reads disabled, not just the track. Needs :has(); older
   engines keep the pointer cursor; cosmetic only. */
.toggle:has(input:disabled) {
  cursor: not-allowed;
}

/* ===== utilities/eyebrow.css ===== */
/*
EYEBROW UTILITY
Small tracked uppercase label above a heading. The only place
uppercase is allowed; everything else is sentence case.
*/
.eyebrow {
  display: block;
  font-size: var(--step--1);
  font-weight: var(--weight-semibold); /* the uppercase eyebrow is Semi Bold (600), not 700 */
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--color-text-weak);
}

/* An eyebrow sits tight above its heading; override flow's gap for the
   element that follows it (the eyebrow-to-heading gap is small). */
.eyebrow + * {
  --flow-space: var(--space-2xs);
}

/* ===== utilities/icon-tile.css ===== */
/*
ICON-TILE UTILITY
An icon sat in a tinted, rounded square: the little glyph holder on feature
cards, empty states and list rows. Neutral by default;
data-tone swaps the wash + glyph colour to a status (or brand) tone.

  <span class="icon-tile"><svg…/></span>
  <span class="icon-tile" data-tone="success"><svg…/></span>
*/
.icon-tile {
  --icon-tile-bg: var(--color-fill);
  --icon-tile-fg: var(--color-text);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xs);
  border-radius: var(--radius-m);
  background: var(--icon-tile-bg);
  color: var(--icon-tile-fg);
}

.icon-tile > svg {
  inline-size: 1.5rem; /* 24 */
  block-size: 1.5rem;
}

.icon-tile[data-tone='success'] {
  --icon-tile-bg: var(--color-success-bg);
  --icon-tile-fg: var(--color-success);
}
.icon-tile[data-tone='warning'] {
  --icon-tile-bg: var(--color-warning-bg);
  --icon-tile-fg: var(--color-warning);
}
.icon-tile[data-tone='error'] {
  --icon-tile-bg: var(--color-error-bg);
  --icon-tile-fg: var(--color-error);
}
.icon-tile[data-tone='info'] {
  --icon-tile-bg: var(--color-info-bg);
  --icon-tile-fg: var(--color-info);
}
.icon-tile[data-tone='neutral'] {
  --icon-tile-bg: var(--color-fill);
  --icon-tile-fg: var(--color-text-weak);
}
.icon-tile[data-tone='brand'] {
  --icon-tile-bg: var(--color-brand-bg);
  --icon-tile-fg: var(--color-brand);
}

/* ===== utilities/text.css ===== */
/*
TEXT UTILITIES
Token-appliers for the two most common text tweaks: de-emphasised colour and
the small (14px) text step. Each applies only its own token, so they compose:
class="tiny text-weak" is a small, weak caption.
*/
.text-weak {
  color: var(--color-text-weak);
}

.tiny {
  font-size: var(--step--1);
  line-height: var(--leading-mid);
}

/* ===== utilities/brand-mark.css ===== */
/*
BRAND-MARK UTILITY
The little logo tile beside the wordmark: the favicon, inline. This utility
only sizes and aligns an inline SVG inside a brand link; the mark itself is
markup, so a project swaps its logo by editing HTML, not CSS. The demo mark
fills with var(--color-brand), so it follows the theme.

  <a class="site-head__brand" href="/">
    <svg class="brand-mark" viewBox="0 0 100 100" aria-hidden="true">…</svg>
    Chloride
  </a>
*/
.brand-mark {
  inline-size: 1.5rem; /* 24 */
  block-size: 1.5rem;
  flex: none;
}

/* ===== utilities/region.css ===== */
/*
REGION UTILITY
Consistent block padding for page sections.
Defaults to the fluid section scale (32→128px).
*/
.region {
  padding-block: var(--region-space, var(--space-2xl));
  position: relative;
}

/* ===== utilities/visually-hidden.css ===== */
/*
VISUALLY HIDDEN UTILITY
Info: https://piccalil.li/quick-tip/visually-hidden/
*/
.visually-hidden {
  border: 0;
  clip: rect(0 0 0 0);
  /* height: auto (not 0/1px) per Piccalilli; a fixed height can truncate or
     mis-announce multi-word hidden text for some AT. width:1px + nowrap + clip
     do the hiding: https://piccalil.li/blog/visually-hidden/ */
  height: auto;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 1px;
  white-space: nowrap;
}
