@layer vendor, tokens, base, components;

/* ── base/fonts.css ── */
@layer vendor {
/*
 * HaTchi-MaXchi type foundation — Geist + Geist Mono, self-hosted.
 *
 * Variable woff2s vendored from vercel/geist-font v1.7.2 (SIL OFL 1.1,
 * licence at ../fonts/OFL.txt). No third-party request, no FOIT:
 * font-display: swap falls back to the system stack until loaded.
 * TASTE-5 (docs/reference/taste.md): type does the hierarchy — the
 * family loads here; the scale/weight discipline lives in tokens.
 */

@font-face {
  font-family: "Geist";
  src: url("fonts/geist-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("fonts/geist-mono-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

}
/* ── tokens/tokens.css ── */
@layer tokens {
/*
 * tokens.css — Dazzle design system as CSS custom properties.
 *
 * Every length, colour, type-size, radius, shadow, and motion value
 * used in component CSS MUST resolve to a token defined here.
 * Literal values (px, rem, em, hex, hsl numerics) are permitted only
 * inside this file. Stylelint enforces this rule.
 *
 * UK English throughout: --colour-*, not --color-*.
 *
 * ─────────────────────────────────────────────────────────────────
 *
 * Portions of this file derived from Open Props by Adam Argyle (MIT).
 * Source: https://github.com/argyleink/open-props
 * Values have been renamed to Dazzle's vocabulary and selectively
 * adopted. Open Props is NOT a runtime dependency — values are
 * vendored in source.
 */

:root {
  /* ─────────────────────────────── spacing ─────────────────────────
   * 4px base. Geometric-ish progression: each step roughly 1.5x.
   * Use semantic names; fall back to scale when truly arbitrary.
   * ───────────────────────────────────────────────────────────────── */
  --space-xs: 0.25rem;   /* 4px */
  --space-sm: 0.5rem;    /* 8px */
  --space-md: 0.75rem;   /* 12px */
  --space-lg: 1rem;      /* 16px */
  --space-xl: 1.5rem;    /* 24px */
  --space-2xl: 2rem;     /* 32px */
  --space-3xl: 3rem;     /* 48px */

  /* ─────────────────────────── fluid spacing ───────────────────────
   * For responsive padding/gap that adapts smoothly. Lifted formulae
   * pattern from Open Props (props.sizes.css). */
  --space-fluid-sm: clamp(0.5rem, 1vw, 0.75rem);
  --space-fluid-md: clamp(0.75rem, 1.5vw, 1rem);
  --space-fluid-lg: clamp(1rem, 2.5vw, 1.5rem);

  /* ────────────────────────────── type scale ───────────────────────
   * Fluid via clamp(). Lifted from Open Props (props.fonts.css)
   * fluid-font-size block, renamed to Dazzle's vocabulary. The
   * clamp expressions are non-trivial to derive correctly.
   * ───────────────────────────────────────────────────────────────── */
  --text-xs: clamp(0.75rem, 0.7rem + 0.2vw, 0.875rem);
  --text-sm: clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
  --text-base: clamp(1rem, 0.9rem + 0.3vw, 1.125rem);
  --text-lg: clamp(1.125rem, 1rem + 0.5vw, 1.375rem);
  --text-xl: clamp(1.375rem, 1.2rem + 0.7vw, 1.75rem);
  --text-2xl: clamp(1.75rem, 1.5rem + 1vw, 2.25rem);
  --text-3xl: clamp(2.25rem, 1.9rem + 1.5vw, 3rem);
  --text-display: clamp(3rem, 2.4rem + 2.5vw, 4.5rem);

  /* ─────────────────────────── heading scale (#983) ────────────────
   * Two scales for two contexts. The app shell is dense and
   * compact (h1 ~18px); the marketing site is editorial and
   * expansive (h1 ~3.5rem). Pre-#983 every renderer hard-coded its
   * own font-size, so promoting `.table-title` from `--text-base`
   * to `--text-lg` was a one-off tweak instead of a tokenised
   * decision.
   *
   * The marketing tokens are also defined in `design-system.css`
   * (canonical home for site/marketing variables). Mirroring them
   * here keeps app-shell + marketing token discovery in one file
   * for adopters reading tokens.css to understand the system.
   */
  --dz-heading-app-page-title: var(--text-lg);
  --dz-heading-app-section-title: var(--text-base);
  --dz-heading-app-subsection-title: var(--text-sm);

  --dz-heading-marketing-hero: var(--dz-font-size-hero-headline, 3.5rem);
  --dz-heading-marketing-cta: var(--dz-font-size-cta-headline, 3rem);
  --dz-heading-marketing-section: var(--dz-font-size-section-headline, 2.25rem);

  /* ─────────────────────────── font families ─────────────────────
   * `--font-system` is the OS-default sans stack; `--font-mono` is
   * used by tabular numerics in chart summaries (bar_track etc.). */
  --font-system: "Geist", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", "Cascadia Mono", "Roboto Mono",
               Consolas, "Liberation Mono", monospace;

  /* ─────────────────────────── line height ─────────────────────────
   * Unitless; multiplies the font-size of the same element. */
  --leading-tight: 1.2;
  --leading-normal: 1.5;
  --leading-loose: 1.8;

  /* ─────────────────────────── font weight ─────────────────────────
   * Variable-font friendly; only the weights we actually use. */
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* ─────────────────────────── radii ───────────────────────────────
   * Use semantic names. Full = pill. */
  /* Focus ring (HaTchi-MaXchi TASTE-1): accent at 40% alpha, 2px, offset
     2px — one ring vocabulary for every interactive element. */
  --focus-ring-color: color-mix(in oklab, var(--colour-brand) 40%, transparent);
  --focus-ring-width: 2px;
  --focus-ring-offset: 2px;

  --radius-sm: 0.25rem;  /* 4px — small inputs, badges */
  --radius-md: 0.375rem; /* 6px — cards, buttons */
  --radius-lg: 0.75rem;  /* 12px — large cards, modals */
  --radius-full: 9999px;

  /* ─────────────────────────── shadows ─────────────────────────────
   * Layered for depth. Sources: Open Props shadow blocks adjusted
   * for Dazzle's visual identity (lower contrast, warmer ambient). */
  --shadow-sm:
    0 1px 2px hsl(0 0% 0% / 0.04),
    0 1px 3px hsl(0 0% 0% / 0.06);
  --shadow-md:
    0 2px 4px hsl(0 0% 0% / 0.04),
    0 4px 8px hsl(0 0% 0% / 0.08);
  --shadow-lg:
    0 4px 8px hsl(0 0% 0% / 0.04),
    0 12px 24px hsl(0 0% 0% / 0.12);

  /* ─────────────────────────── motion ──────────────────────────────
   * Easings lifted from Open Props (props.easings.css). The
   * cubic-bezier values for spring-like easings are well-tuned. */
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --ease-out: cubic-bezier(0, 0, 0.2, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-spring: cubic-bezier(0.5, -0.5, 0.5, 1.5);

  /* Spring-easing ladder, Open Props convention. Progressively
   * more bounce — pick the smallest one that conveys the affordance.
   * #960 layer 1. */
  --ease-spring-1: cubic-bezier(0.5, 0.75, 0.75, 1.25);
  --ease-spring-2: cubic-bezier(0.5, 1, 0.75, 1.25);
  --ease-spring-3: cubic-bezier(0.5, 1.25, 0.75, 1.25);
  --ease-spring-4: cubic-bezier(0.5, 1.5, 0.75, 1.25);
  --ease-spring-5: cubic-bezier(0.5, 1.75, 0.75, 1.25);

  /* Elastic easings — for dismiss/snap-back motion. */
  --ease-elastic-1: cubic-bezier(0.5, 0.75, 0.75, 1.25);
  --ease-elastic-2: cubic-bezier(0.5, 1, 0.75, 1.25);
  --ease-elastic-3: cubic-bezier(0.5, 1.25, 0.75, 1.25);

  --duration-fast: 80ms;
  --duration-base: 150ms;
  --duration-slow: 300ms;

  /* Named animation shorthands, Open Props convention. Each combines
   * keyframe + duration + easing in one var so templates can write
   * `animation: var(--animation-fade-in)` directly. The keyframes
   * themselves are defined below the :root block. #960 layer 1. */
  --animation-fade-in: fade-in var(--duration-base) var(--ease-out);
  --animation-fade-out: fade-out var(--duration-base) var(--ease-in);
  --animation-scale-up: scale-up var(--duration-base) var(--ease-spring-2);
  --animation-scale-down: scale-down var(--duration-base) var(--ease-in);
  --animation-slide-in-up: slide-in-up var(--duration-base) var(--ease-out);
  --animation-slide-in-down: slide-in-down var(--duration-base) var(--ease-out);
  --animation-slide-in-left: slide-in-left var(--duration-base) var(--ease-out);
  --animation-slide-in-right: slide-in-right var(--duration-base) var(--ease-out);
  --animation-slide-out-up: slide-out-up var(--duration-base) var(--ease-in);
  --animation-slide-out-down: slide-out-down var(--duration-base) var(--ease-in);
  --animation-slide-out-left: slide-out-left var(--duration-base) var(--ease-in);
  --animation-slide-out-right: slide-out-right var(--duration-base) var(--ease-in);
  --animation-shake-x: shake-x 0.4s var(--ease-out);
  --animation-pulse: pulse 2s var(--ease-in-out) infinite;

  /* ─────────────────────────── widths ──────────────────────────────
   * Common content widths. */
  --width-prose: 65ch;
  --width-content: 56rem;
  --width-page: 80rem;

  /* App-shell content measure (``data-measure`` on ``.app-main``).
   * pick-a-measure.md — product readable form/detail; wide intermediate;
   * app soft ultrawide for list/board scan. */
  --dz-content-measure: 48rem;        /* product */
  --dz-content-measure-wide: 56rem;   /* wide (= --width-content) */
  --dz-content-measure-app: 96rem;    /* app soft cap */

  /* ─────────────────────────── touch targets ───────────────────────
   * Minimum hit-area on touch devices. Apple HIG calls for 44×44pt;
   * Material recommends 48×48dp. We use 44px as a good middle —
   * matches Apple, hits Material's 80% threshold, doesn't visually
   * inflate dense desktop UIs.
   *
   * The min-size is enforced under `@media (pointer: coarse)` only
   * (see `components/touch-targets.css`) so desktop pixel-perfect
   * sizing is preserved while touch users get a comfortable target.
   *
   * #958 cycle 1.
   */
  --touch-target-min: 44px;

  /* ─────────────────────────── colour ramps ────────────────────────
   * Raw OKLCH ramps lifted from Open Props
   * (props.colors-oklch.css), renamed to Dazzle's vocabulary. Use
   * semantic tokens (--colour-*) below for almost all styling;
   * raw ramps only when no semantic token fits.
   * ───────────────────────────────────────────────────────────────── */
  --neutral-50: oklch(98.5% 0.002 247.84);
  --neutral-100: oklch(96.7% 0.003 264.54);
  --neutral-200: oklch(92.8% 0.006 264.53);
  --neutral-300: oklch(86.9% 0.012 252.89);
  --neutral-400: oklch(70.6% 0.022 261.32);
  --neutral-500: oklch(55.1% 0.027 264.36);
  --neutral-600: oklch(44.6% 0.030 256.80);
  --neutral-700: oklch(37.3% 0.034 259.73);
  --neutral-800: oklch(27.8% 0.030 256.85);
  --neutral-900: oklch(21.0% 0.034 264.66);
  --neutral-950: oklch(13.0% 0.028 261.69);

  --brand-50: oklch(97.0% 0.014 254.60);
  --brand-100: oklch(93.2% 0.032 255.59);
  --brand-200: oklch(88.2% 0.059 254.13);
  --brand-300: oklch(80.9% 0.105 251.81);
  --brand-400: oklch(70.7% 0.165 254.62);
  --brand-500: oklch(62.3% 0.214 259.81);
  --brand-600: oklch(54.6% 0.245 262.88);
  --brand-700: oklch(48.8% 0.243 264.05);
  --brand-800: oklch(42.4% 0.199 265.64);
  --brand-900: oklch(37.9% 0.146 265.52);
  --brand-950: oklch(28.2% 0.091 267.94);

  --success-500: oklch(64.8% 0.150 142.50);
  --success-600: oklch(53.0% 0.140 142.50); /* 4.5:1 on the soft wash AND white (axe-verified) */
  --warning-500: oklch(76.9% 0.188 70.08);
  --warning-600: oklch(52.0% 0.150 70.08); /* 4.5:1 on the soft wash AND white (axe-verified) */
  --danger-500: oklch(63.7% 0.208 25.33);
  --danger-600: oklch(51.0% 0.195 25.33); /* 4.5:1 as text on wash/white; fills carry white text */

  /* ─────────────────────────── colour: semantic ────────────────────
   * Semantic tokens that components reference. Project themes
   * override THESE, not the raw ramps. light-dark() handles
   * scheme switching when supported; @media fallback below.
   * ───────────────────────────────────────────────────────────────── */
  --colour-bg: light-dark(var(--neutral-50), var(--neutral-950));
  --colour-surface: light-dark(white, var(--neutral-900));
  --colour-surface-muted: light-dark(var(--neutral-100), var(--neutral-800));
  --colour-text: light-dark(var(--neutral-900), var(--neutral-50));
  --colour-text-muted: light-dark(var(--neutral-600), var(--neutral-400));
  --colour-border: light-dark(var(--neutral-200), var(--neutral-700));
  --colour-brand: var(--brand-600);
  /* Brand as TEXT (badges/avatars/tags on washes): scheme-aware, unlike
     --colour-brand which stays the vivid FILL accent in both schemes. */
  --colour-brand-text: light-dark(var(--brand-600), var(--brand-300));
  --colour-brand-contrast: white;  /* foreground on filled brand fills (CTAs) */
  --colour-accent: var(--brand-500);
  /* Tone TEXT semantics are scheme-aware: the -600 steps carry 4.5:1 on
   light washes/white; dark mode needs LIGHT tone text on dark washes.
   Solid fills that carry white text must use the fixed ramp steps
   (--success-600 etc.), never these flippable text tokens. */
  --colour-success: light-dark(var(--success-600), oklch(74% 0.13 142.50));
  --colour-warning: light-dark(var(--warning-600), oklch(80% 0.13 70.08));
  --colour-danger: light-dark(var(--danger-600), oklch(75% 0.15 25.33));
  /* Info: cyan-leaning so chart series stay distinguishable from
   * --colour-brand (hue ~263). Scheme-stable, like the tone hues. */
  --colour-info: oklch(60% 0.13 220);

  /* Tone tints (background washes for surfaces) — semantic tokens
   * the tones replacements consume. Authors don't hand-craft
   * `hsl(var(--success) / 0.08)` strings any more. */
  --colour-success-soft: light-dark(oklch(from var(--colour-success) 96.5% 0.04 h), oklch(from var(--colour-success) 28% 0.06 h));
  --colour-warning-soft: light-dark(oklch(from var(--colour-warning) 96.5% 0.04 h), oklch(from var(--colour-warning) 28% 0.06 h));
  --colour-danger-soft: light-dark(oklch(from var(--colour-danger) 96.5% 0.04 h), oklch(from var(--colour-danger) 28% 0.06 h));
  --colour-brand-soft: light-dark(oklch(from var(--colour-brand) 96.5% 0.04 h), oklch(from var(--colour-brand) 28% 0.06 h));

  /* Default colour-scheme advertisement — lets browser-chrome
   * (scrollbars, form controls) pick appropriate variants. */
  color-scheme: light dark;
}

/* #938 — bind `data-theme="dark"` (the JS toggle's output) to the
 * actual `color-scheme` property so `light-dark()` flips when the
 * user toggles. Without this, the toggle would set `data-theme` but
 * `light-dark()` would still resolve based on `prefers-color-scheme`,
 * leaving framework components stuck on the light variant of
 * `--colour-bg` / `--colour-text` / `--colour-border` even though
 * the topbar swapped its sun/moon icon. The marketing-site `site.js`
 * sets `style.colorScheme` imperatively for the same reason; this
 * CSS rule covers the in-app shell whose Alpine controller only
 * sets `data-theme`. */
[data-theme="dark"] {
  color-scheme: dark;
}
[data-theme="light"] {
  color-scheme: light;
}

/* Fallback for browsers without light-dark() support. Modern
 * Chrome / Firefox / Safari all support it as of 2024 but the
 * @supports gate keeps the door open for outliers. */
@media (prefers-color-scheme: dark) {
  @supports not (color: light-dark(white, black)) {
    :root {
      --colour-bg: var(--neutral-950);
      --colour-surface: var(--neutral-900);
      --colour-surface-muted: var(--neutral-800);
      --colour-text: var(--neutral-50);
      --colour-text-muted: var(--neutral-400);
      --colour-border: var(--neutral-700);
      --colour-success: oklch(74% 0.13 142.50);
      --colour-warning: oklch(80% 0.13 70.08);
      --colour-danger: oklch(75% 0.15 25.33);
      --colour-brand-text: var(--brand-300);
      --colour-success-soft: oklch(from var(--colour-success) 28% 0.06 h);
      --colour-warning-soft: oklch(from var(--colour-warning) 28% 0.06 h);
      --colour-danger-soft: oklch(from var(--colour-danger) 28% 0.06 h);
      --colour-brand-soft: oklch(from var(--colour-brand) 28% 0.06 h);
    }
  }
}

/* ─────────────────────────── animation keyframes ────────────────────
 * Backing keyframes for the `--animation-*` named tokens above.
 * Prefixed `` so they don't collide with project-defined
 * keyframes. Open Props convention; #960 layer 1.
 *
 * Honoured by `prefers-reduced-motion: reduce` further down. */
@keyframes fade-in {
  from { opacity: 0; }
}
@keyframes fade-out {
  to { opacity: 0; }
}
@keyframes scale-up {
  from { transform: scale(0.92); opacity: 0; }
}
@keyframes scale-down {
  to { transform: scale(0.92); opacity: 0; }
}
@keyframes slide-in-up {
  from { transform: translateY(8%); opacity: 0; }
}
@keyframes slide-in-down {
  from { transform: translateY(-8%); opacity: 0; }
}
@keyframes slide-in-left {
  from { transform: translateX(8%); opacity: 0; }
}
@keyframes slide-in-right {
  from { transform: translateX(-8%); opacity: 0; }
}
@keyframes slide-out-up {
  to { transform: translateY(-8%); opacity: 0; }
}
@keyframes slide-out-down {
  to { transform: translateY(8%); opacity: 0; }
}
@keyframes slide-out-left {
  to { transform: translateX(-8%); opacity: 0; }
}
@keyframes slide-out-right {
  to { transform: translateX(8%); opacity: 0; }
}
@keyframes shake-x {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-4px); }
  40%, 80% { transform: translateX(4px); }
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* Reduced-motion guard: collapse all * animations to a near-instant
 * fade so users who request reduced motion still get state changes
 * without parallax/translate/scale. Operates on the keyframe name
 * (`animation-name`) so component-local @keyframes definitions
 * (dashboard, etc.) aren't affected by this rule. */
@media (prefers-reduced-motion: reduce) {
  [style*="animation"], [class*="anim-"], .toast {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

}
/* ── base/design-system.css ── */
@layer tokens {
/* =============================================================================
   DAZZLE Design System (app-shell layer)

   Non-colour design tokens (type/space/radius/shadow/motion + --dz-*
   site tokens) and app-shell component overrides. All colour comes
   from the HaTchi-MaXchi OKLCH semantic tokens (--colour-*,
   packages/hatchi-maxchi/tokens/tokens.css) — the legacy shadcn-era
   HSL token system was removed in the Stage 2b fossil migration.

   Sections:
   1. Design Tokens (CSS custom properties)
   2. (removed — DaisyUI theme bridge, #1113)
   3. Typography
   4. Global Refinements
   5. Component Overrides
   6. Loading States & Animations
   7. Responsive Refinements
   ============================================================================= */

/* =============================================================================
   1. Design Tokens — Light Mode (default)

   Colour comes from the HaTchi-MaXchi OKLCH semantic tokens
   (--colour-*, tokens/tokens.css). The legacy shadcn-era HSL token
   block (--background/--foreground/--primary/…) was removed in the
   Stage 2b fossil migration — every consumer now references the
   semantic tokens directly, and light/dark switching is handled by
   light-dark() in tokens.css (no per-scheme block needed here).
   ============================================================================= */

:root {
  /* --- Typography --- */
  --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, 'Cascadia Code', monospace;

  /* --- Spacing scale --- */
  --space-0: 0;
  --space-0-5: 0.125rem;
  --space-1: 0.25rem;
  --space-1-5: 0.375rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;

  /* --- Border radius --- */
  --radius-sm: 0.25rem;
  --radius: 0.375rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.625rem;
  --radius-xl: 0.75rem;
  --radius-2xl: 1rem;
  --radius-full: 9999px;

  /* --- Shadows (zinc-tinted) --- */
  --shadow-xs: 0 1px 2px 0 hsl(240 5.9% 10% / 0.03);
  --shadow-sm: 0 1px 3px 0 hsl(240 5.9% 10% / 0.06), 0 1px 2px -1px hsl(240 5.9% 10% / 0.06);
  --shadow-md: 0 4px 6px -1px hsl(240 5.9% 10% / 0.07), 0 2px 4px -2px hsl(240 5.9% 10% / 0.05);
  --shadow-lg: 0 10px 15px -3px hsl(240 5.9% 10% / 0.07), 0 4px 6px -4px hsl(240 5.9% 10% / 0.05);
  --shadow-xl: 0 20px 25px -5px hsl(240 5.9% 10% / 0.08), 0 8px 10px -6px hsl(240 5.9% 10% / 0.05);

  /* --- Transitions --- */
  --duration-fast: 150ms;
  --duration-normal: 200ms;
  --duration-slow: 300ms;
  --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --ease-out: cubic-bezier(0, 0, 0.2, 1);

  /* --- Legacy --dz-* tokens (backward compat) --- */
  --dz-shadow-sm: var(--shadow-sm);
  --dz-shadow-card: 0 1px 2px oklch(0 0 0 / 0.04), 0 8px 20px -10px oklch(0 0 0 / 0.14);
  --dz-shadow-md: var(--shadow-md);
  --dz-shadow-lg: var(--shadow-lg);
  --dz-transition-fast: var(--duration-fast);
  --dz-transition-normal: var(--duration-normal);
  --dz-accent-glow: color-mix(in oklab, var(--colour-brand) 15%, transparent);

  /* --- Site section tokens (marketing pages) --- */

  /* Hero gradient — overridden by ThemeSpec when a custom palette is set */
  --dz-hero-bg-from: oklch(0.45 0.18 265);
  --dz-hero-bg-to: oklch(0.38 0.20 280);
  --dz-shadow-hero: 0 20px 60px -12px oklch(0 0 0 / 0.25);
  --dz-shadow-xl: var(--shadow-xl);

  /* Text hierarchy (oklch neutrals) */
  --dz-text-foreground: oklch(0.15 0 0);
  --dz-text-emphasis: oklch(0.35 0 0);
  --dz-text-body: oklch(0.45 0 0);
  --dz-text-muted: oklch(0.5 0 0);

  /* Surfaces */
  --dz-surface-card: white;
  --dz-feature-icon-bg: oklch(0.92 0.03 260);
  --dz-section-alt-bg: oklch(0.97 0.01 260);
  --dz-pricing-highlight-bg: oklch(0.45 0.18 270);

  /* Marketing card finish (features / cards / testimonials) — families reflavor */
  --dz-card-shadow-hover: 0 2px 4px oklch(0 0 0 / 0.06), 0 20px 36px -14px oklch(0 0 0 / 0.22);
  --dz-card-border: 1px solid oklch(0 0 0 / 0.06);
  --dz-card-icon-ring: inset 0 0 0 1px oklch(0 0 0 / 0.06);

  /* Marketing CTA buttons — families reflavor (glow) */
  --dz-cta-fill: white;
  --dz-cta-text: var(--dz-hero-bg-from);
  --dz-cta-fill-hover: var(--dz-hero-btn-hover, oklch(0.98 0 0));
  --dz-cta-shadow: 0 6px 20px -8px oklch(0 0 0 / 0.35);
  --dz-cta-shadow-hover: 0 12px 28px -8px oklch(0 0 0 / 0.45);

  /* Borders (transparent black) */
  --dz-border-default: oklch(0 0 0 / 0.1);
  --dz-border-subtle: oklch(0 0 0 / 0.08);
  --dz-border-input: oklch(0 0 0 / 0.15);

  /* Hero overlays (transparent white, for dark backgrounds) */
  --dz-hero-overlay: oklch(1 0 0 / 0.1);
  --dz-hero-border: oklch(1 0 0 / 0.5);
  --dz-hero-border-hover: oklch(1 0 0 / 0.7);
  --dz-hero-text-dimmed: oklch(1 0 0 / 0.8);
  --dz-hero-border-dim: oklch(1 0 0 / 0.15);
  --dz-hero-btn-hover: oklch(0.98 0 0);

  /* Header */
  --dz-header-bg: oklch(1 0 0 / 0.95);

  /* Footer */
  --dz-footer-bg: oklch(0.15 0.01 260);
  --dz-footer-text: oklch(0.7 0 0);
  --dz-footer-link: oklch(0.6 0 0);
  --dz-footer-border: oklch(1 0 0 / 0.1);
  --dz-footer-heading: oklch(1 0 0);

  /* Logo */
  --dz-logo-color: oklch(0.25 0.02 260);

  /* Error feedback */
  --dz-error-bg: oklch(0.95 0.03 25);
  --dz-error-text: oklch(0.45 0.15 25);

  /* Focus ring */
  --dz-ring-glow: 0 0 0 3px oklch(0.50 0.18 270 / 0.15);

  /* Section spacing */
  --dz-spacing-hero-y: 6rem;
  --dz-spacing-section-y: 5rem;
  --dz-spacing-feature-gap: 2rem;
  --dz-spacing-card-padding: 1.5rem;

  /* Fluid typography — clamp() for responsive sizing without breakpoints */
  --dz-font-size-fluid-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
  --dz-font-size-fluid-sm: clamp(0.875rem, 0.8rem + 0.375vw, 1rem);
  --dz-font-size-fluid-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
  --dz-font-size-fluid-lg: clamp(1.125rem, 1rem + 0.625vw, 1.375rem);
  --dz-font-size-fluid-xl: clamp(1.25rem, 1rem + 1.25vw, 1.75rem);
  --dz-font-size-fluid-2xl: clamp(1.5rem, 1.1rem + 2vw, 2.25rem);
  --dz-font-size-fluid-3xl: clamp(1.875rem, 1.3rem + 2.875vw, 3rem);

  /* Typography — hero */
  --dz-font-size-hero-headline: clamp(2.25rem, 1.5rem + 3.75vw, 3.5rem);
  --dz-font-weight-hero-headline: 800;
  --dz-line-height-hero-headline: 1.1;
  --dz-letter-spacing-hero-headline: -0.02em;
  --dz-font-size-subhead: 1.25rem;
  --dz-line-height-subhead: 1.5;

  /* Typography — CTA (#983). Pre-tokenisation, site-sections.css
   * inlined the 3rem fallback. Defining it here keeps the marketing
   * heading scale discoverable in one place. */
  --dz-font-size-cta-headline: 3rem;

  /* Typography — sections */
  --dz-font-size-section-headline: 2.25rem;
  --dz-font-weight-section-headline: 700;
  --dz-line-height-section-headline: 1.2;
  --dz-letter-spacing-section-headline: -0.01em;
  --dz-font-size-body: 1rem;
  --dz-line-height-body: 1.6;

  /* Radii */
  --dz-radius-button: 0.5rem;
  --dz-radius-card: 0.75rem;
  --dz-radius-lg: 1rem;
  --dz-radius-sm: 0.375rem;
}

/* =============================================================================
   1b. Dark Mode Tokens
   ============================================================================= */

.dark,
[data-theme="dark"] {
  --shadow-xs: 0 1px 2px 0 hsl(0 0% 0% / 0.1);
  --shadow-sm: 0 1px 3px 0 hsl(0 0% 0% / 0.2), 0 1px 2px -1px hsl(0 0% 0% / 0.15);
  --shadow-md: 0 4px 6px -1px hsl(0 0% 0% / 0.2), 0 2px 4px -2px hsl(0 0% 0% / 0.15);
  --shadow-lg: 0 10px 15px -3px hsl(0 0% 0% / 0.25), 0 4px 6px -4px hsl(0 0% 0% / 0.15);
  --shadow-xl: 0 20px 25px -5px hsl(0 0% 0% / 0.3), 0 8px 10px -6px hsl(0 0% 0% / 0.2);

  --dz-sidebar-from: hsl(240 10% 5%);
  --dz-sidebar-to: hsl(240 10% 8%);
  --dz-accent-glow: color-mix(in oklab, var(--colour-brand) 20%, transparent);

  /* Site section overrides for dark mode */
  --dz-section-alt-bg: oklch(0.08 0.01 260);
  --dz-feature-icon-bg: oklch(0.20 0.04 260);

  /* Text hierarchy */
  --dz-text-foreground: oklch(0.95 0 0);
  --dz-text-emphasis: oklch(0.85 0 0);
  --dz-text-body: oklch(0.70 0 0);
  --dz-text-muted: oklch(0.55 0 0);

  /* Surfaces */
  --dz-surface-card: oklch(0.18 0.01 260);

  /* Borders */
  --dz-border-default: oklch(1 0 0 / 0.12);
  --dz-border-subtle: oklch(1 0 0 / 0.08);
  --dz-border-input: oklch(1 0 0 / 0.15);

  /* Header / Footer */
  --dz-header-bg: oklch(0.15 0.01 260 / 0.95);
  --dz-footer-bg: oklch(0.08 0.01 260);
  --dz-footer-text: oklch(0.65 0 0);
  --dz-footer-link: oklch(0.55 0 0);
  --dz-footer-border: oklch(1 0 0 / 0.1);
  --dz-footer-heading: oklch(1 0 0);

  /* Logo */
  --dz-logo-color: oklch(0.95 0 0);

  /* Error feedback */
  --dz-error-bg: oklch(0.20 0.06 25);
  --dz-error-text: oklch(0.80 0.15 25);

  /* Focus ring */
  --dz-ring-glow: 0 0 0 3px oklch(0.60 0.18 270 / 0.15);
}

/* No-JS / pre-paint parity (#tranche2): the shadow + --dz-* site
   tokens must follow prefers-color-scheme like the OKLCH set does,
   otherwise consumers render light values on dark until theme JS
   sets [data-theme]. Mirrors the tokens.css fallback. (The legacy
   HSL colour tokens this block also carried were removed in the
   Stage 2b fossil migration — light-dark() in tokens.css covers
   the --colour-* set natively.) */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  --shadow-xs: 0 1px 2px 0 hsl(0 0% 0% / 0.1);
  --shadow-sm: 0 1px 3px 0 hsl(0 0% 0% / 0.2), 0 1px 2px -1px hsl(0 0% 0% / 0.15);
  --shadow-md: 0 4px 6px -1px hsl(0 0% 0% / 0.2), 0 2px 4px -2px hsl(0 0% 0% / 0.15);
  --shadow-lg: 0 10px 15px -3px hsl(0 0% 0% / 0.25), 0 4px 6px -4px hsl(0 0% 0% / 0.15);
  --shadow-xl: 0 20px 25px -5px hsl(0 0% 0% / 0.3), 0 8px 10px -6px hsl(0 0% 0% / 0.2);

  --dz-sidebar-from: hsl(240 10% 5%);
  --dz-sidebar-to: hsl(240 10% 8%);
  --dz-accent-glow: color-mix(in oklab, var(--colour-brand) 20%, transparent);

  /* Site section overrides for dark mode */
  --dz-section-alt-bg: oklch(0.08 0.01 260);
  --dz-feature-icon-bg: oklch(0.20 0.04 260);

  /* Text hierarchy */
  --dz-text-foreground: oklch(0.95 0 0);
  --dz-text-emphasis: oklch(0.85 0 0);
  --dz-text-body: oklch(0.70 0 0);
  --dz-text-muted: oklch(0.55 0 0);

  /* Surfaces */
  --dz-surface-card: oklch(0.18 0.01 260);

  /* Borders */
  --dz-border-default: oklch(1 0 0 / 0.12);
  --dz-border-subtle: oklch(1 0 0 / 0.08);
  --dz-border-input: oklch(1 0 0 / 0.15);

  /* Header / Footer */
  --dz-header-bg: oklch(0.15 0.01 260 / 0.95);
  --dz-footer-bg: oklch(0.08 0.01 260);
  --dz-footer-text: oklch(0.65 0 0);
  --dz-footer-link: oklch(0.55 0 0);
  --dz-footer-border: oklch(1 0 0 / 0.1);
  --dz-footer-heading: oklch(1 0 0);

  /* Logo */
  --dz-logo-color: oklch(0.95 0 0);

  /* Error feedback */
  --dz-error-bg: oklch(0.20 0.06 25);
  --dz-error-text: oklch(0.80 0.15 25);

  /* Focus ring */
  --dz-ring-glow: 0 0 0 3px oklch(0.60 0.18 270 / 0.15);
  }
}


/* =============================================================================
   2. (DaisyUI theme bridge removed in #1113.)

   This block previously mapped our design tokens to DaisyUI's internal
   --b1/--b2/--p/--s/etc. variables. DaisyUI is no longer loaded — the
   CDN tags were dropped from `site_renderer.get_shared_head_html` in
   the same release — so those variable mappings became dead weight.
   Any selector below that uses `bg-base-100` etc. as a class name is
   now styled by our own rules (drawer-side block in section 5), not
   by DaisyUI's component CSS.
   ============================================================================= */

/* =============================================================================
   3. Typography
   ============================================================================= */

body {
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: var(--colour-text);
  background-color: var(--colour-bg);
}

code, pre, kbd, samp {
  font-family: var(--font-mono);
}

/* =============================================================================
   4. Global Refinements
   ============================================================================= */

/* Smooth transitions on interactive elements */
a, button, .btn, input, select, textarea, .card, .menu li a {
  transition: all var(--duration-fast) var(--ease-default);
}

/* Focus visibility — universal net, same ring vocabulary as the
   HaTchi-MaXchi per-element rules in base.css (TASTE-1). */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

/* Remove default focus outline — rely on :focus-visible only */
:focus:not(:focus-visible) {
  outline: none;
}

/* High contrast mode adjustments */
@media (prefers-contrast: high) {
  :root {
    --colour-border: light-dark(var(--neutral-600), var(--neutral-400));
    --colour-text-muted: light-dark(var(--neutral-700), var(--neutral-300));
  }
  .btn {
    border: 2px solid currentColor;
  }
  .badge {
    border: 1px solid currentColor;
  }
  .card {
    border-width: 2px;
  }
  .input,
  .select,
  .textarea {
    border-width: 2px;
  }
}

/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* =============================================================================
   5. Component Overrides
   ============================================================================= */

/* --- Sidebar --- */

.drawer-side aside,
.drawer-side > aside.bg-base-200,
.drawer-side > aside.bg-base-100 {
  background: var(--colour-surface);
  border-inline-end: 1px solid var(--colour-border);
}

.drawer-side aside > div:first-child {
  border-block-end-color: var(--colour-border);
}

.drawer-side aside > div:first-child a {
  color: var(--colour-text);
  letter-spacing: -0.01em;
}

.drawer-side .menu a {
  color: var(--colour-text-muted);
  border-radius: var(--radius-md);
  margin-bottom: 0.125rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  font-weight: 500;
}

.drawer-side .menu a:hover {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}

.drawer-side .menu a.active {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  font-weight: 500;
  box-shadow: none;
}

.drawer-side aside > div:last-child {
  border-block-start-color: var(--colour-border);
}

/* --- Navbar --- */

.navbar {
  backdrop-filter: blur(8px);
  background: color-mix(in oklab, var(--colour-bg) 85%, transparent);
  border-block-end: 1px solid var(--colour-border);
}

.navbar .text-xl {
  font-weight: 600;
  letter-spacing: -0.02em;
}

/* --- Cards --- */
/* HYPERPART: card */

.card {
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  background: var(--colour-surface);
  color: var(--colour-text);
  transition: box-shadow var(--duration-normal) var(--ease-default),
              transform var(--duration-normal) var(--ease-default);
}

.card:hover {
  box-shadow: var(--shadow-md);
}

.card[onclick]:hover,
.card a:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-lg);
}

/* Card content primitives — semantic classes so demos and consumers don't
   reach for inline styles. A stat/KPI card is label + value (+ optional
   delta); `card-body` is the padded content wrapper. */
.card-body {
  padding: var(--space-xl);
}
.card-label {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}
.card-value {
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
}
.card-delta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--colour-success);
}
.card-delta svg {
  width: 0.875rem;
  height: 0.875rem;
}

.card-title {
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Legacy DaisyUI shim classes (.card-body, .btn, .btn-primary, .btn-ghost,
   .btn-outline, .btn-error, .input, .select, .textarea, .label-text,
   .table, .table thead th, .table tbody td) removed in v0.56.0.
   All components now use the OKLCH semantic tokens (var(--colour-*)).
   See the component contracts at ~/.claude/skills/ux-architect/components/. */

/* --- Modal/Dialog (kept — used by the native <dialog> modal component) --- */

.modal-box {
  box-shadow: var(--shadow-xl);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  background: var(--colour-surface);
}

.modal-backdrop {
  background: color-mix(in oklab, var(--colour-text) 50%, transparent);
  backdrop-filter: blur(4px);
}

/* Legacy DaisyUI shim classes removed in v0.56.0:
   .badge, .badge-primary, .badge-success, .badge-warning, .badge-error,
   .badge-info, .badge-ghost, .stats, .stat, .stat-title, .stat-value,
   .stat-desc, .alert, .alert-info, .alert-success, .alert-warning,
   .alert-error — all replaced by inline Tailwind with design-system
   tokens. See macros/status_badge.html and the component contracts. */

/* --- Page Header --- */

.page-header {
  margin-block-end: 1.5rem;
}

.page-header h1,
.page-header .text-2xl {
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--colour-text);
}

.page-header .text-sm {
  color: var(--colour-text-muted);
}

/* Empty State — consolidated into components/fragment-primitives.css (the
 * single home for the `.empty-state` primitive). The base container +
 * `__` parts now live together there; the broad `.empty-state svg` fossil
 * was dropped in favour of the `__icon` wrapper contract. */

/* --- Action Bar --- */

.action-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-block-end: 1rem;
  gap: 1rem;
}

.action-bar .btn {
  gap: 0.5rem;
}

/* --- Dropdown menus --- */

.menu.shadow {
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  background: var(--colour-surface);
  color: var(--colour-text);
}

/* =============================================================================
   6. Loading States & Animations
   ============================================================================= */

.loading {
  color: var(--colour-brand);
}

/* NOTE: the skeleton shimmer that lived here was superseded by the
   `skeleton` Hyperpart (components/skeleton.css) — the single canonical
   `.skeleton` with data-shape variants. Two `.skeleton` blocks
   resolved only by @layer order was a footgun; retired 2026-07-04. */

/* Spinner for HTMX loading */
.spinner {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  border: 2px solid var(--colour-surface-muted);
  border-top-color: var(--colour-brand);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

.spinner--sm {
  width: 0.75rem;
  height: 0.75rem;
  border-width: 1.5px;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* HTMX indicator visibility — moved to dz.css (loaded on all pages) */
.htmx-request .htmx-hide-on-request {
  display: none;
}

/* Toast slide-in — keyframes owned by components/toast.css (HYPERPART: toast).
 * Legacy Daisy-style `.toast .alert` still rides the shared enter motion. */
.toast .alert {
  animation: toast-in var(--duration-slow) var(--ease-out);
}

/* =============================================================================
   7. Responsive Refinements
   ============================================================================= */

/* Responsive refinements for DaisyUI .card-body / .table / .stat removed
   in v0.56.0 — components now use inline Tailwind with responsive prefixes. */

/* =============================================================================
   8. Container Queries
   ============================================================================= */

/* Table container — enable container queries for responsive stacking */
.overflow-x-auto[role="region"] {
  container-type: inline-size;
  container-name: table-container;
}

/* Stack table on narrow containers */
@container table-container (max-width: 480px) {
  .table thead {
    display: none;
  }
  .table tbody tr {
    display: block;
    margin-block-end: 0.75rem;
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-md);
    padding: 0.5rem;
  }
  .table tbody td {
    display: flex;
    justify-content: space-between;
    padding-block: 0.25rem;
    padding-inline: 0.5rem;
    border-block-end: none;
  }
  .table tbody td::before {
    content: attr(data-label);
    font-weight: 500;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--colour-text-muted);
  }
}

/* Tom Select token overrides removed (Dazzle HMC-018 slice 3): the vendored
   TomSelect library it themed was retired — widget:combobox + widget:tags are
   HM-native progressive-enhancement controllers (combobox / tags) and
   multiselect had zero usage. No TomSelect-generated markup exists anymore. */

/* ── Range slider token overrides (UX-023 widget:slider) ──────────────── */
/* HYPERPART: slider */
/* Scoped to input[type="range"][data-slider] so generic ranges elsewhere */
/* keep their browser-default appearance.                                    */
input[type="range"][data-slider] {
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  cursor: pointer;
  height: 16px; /* enough room for the thumb */
  padding: 0;
}
input[type="range"][data-slider]:focus { outline: none; }

/* Track — WebKit / Blink */
input[type="range"][data-slider]::-webkit-slider-runnable-track {
  height: 4px;
  background: var(--colour-surface-muted);
  border-radius: 2px;
  border: 0;
}
/* Track — Gecko */
input[type="range"][data-slider]::-moz-range-track {
  height: 4px;
  background: var(--colour-surface-muted);
  border-radius: 2px;
  border: 0;
}

/* Thumb — WebKit / Blink */
input[type="range"][data-slider]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  height: 16px;
  width: 16px;
  border-radius: 50%;
  background: var(--colour-brand);
  border: 0;
  cursor: pointer;
  margin-top: -6px; /* (thumb - track) / 2 = (16 - 4) / 2 = 6 */
  transition: filter 80ms cubic-bezier(0.2, 0, 0, 1),
              box-shadow 80ms cubic-bezier(0.2, 0, 0, 1);
}
input[type="range"][data-slider]::-webkit-slider-thumb:hover {
  filter: brightness(1.05);
}
input[type="range"][data-slider]:focus::-webkit-slider-thumb {
  box-shadow: 0 0 0 2px var(--focus-ring-color);
}

/* Thumb — Gecko */
input[type="range"][data-slider]::-moz-range-thumb {
  height: 16px;
  width: 16px;
  border-radius: 50%;
  background: var(--colour-brand);
  border: 0;
  cursor: pointer;
  transition: filter 80ms cubic-bezier(0.2, 0, 0, 1),
              box-shadow 80ms cubic-bezier(0.2, 0, 0, 1);
}
input[type="range"][data-slider]::-moz-range-thumb:hover {
  filter: brightness(1.05);
}
input[type="range"][data-slider]:focus::-moz-range-thumb {
  box-shadow: 0 0 0 2px var(--focus-ring-color);
}

input[type="range"][data-slider]:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* ── Pickr token overrides (UX-024 widget:colorpicker) ────────────────── */
/* Linear-aesthetic override of vendor/pickr.css (nano theme). Scoped to   */
/* .pcr-app so the rules don't leak into non-Pickr elements.                */
.pcr-app {
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
  border-radius: 6px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.08), 0 1px 3px rgb(0 0 0 / 0.06);
  font-family: inherit;
  padding: 12px;
}
.pcr-app .pcr-selection .pcr-color-preview {
  border-radius: 3px;
  border: 1px solid var(--colour-border);
}
.pcr-app .pcr-selection .pcr-color-chooser,
.pcr-app .pcr-selection .pcr-color-opacity {
  margin-inline-start: 8px;
}
.pcr-app .pcr-interaction {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--colour-border);
}
.pcr-app .pcr-interaction input,
.pcr-app .pcr-interaction .pcr-result {
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: 3px;
  color: var(--colour-text);
  font-size: 12px;
  font-family: ui-monospace, Menlo, monospace;
  padding: 4px 8px;
  height: 26px;
}
.pcr-app .pcr-interaction input:focus,
.pcr-app .pcr-interaction .pcr-result:focus {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
  border-color: var(--colour-brand);
}
.pcr-app .pcr-interaction .pcr-type.active,
.pcr-app .pcr-interaction .pcr-type:hover {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}
.pcr-app .pcr-interaction .pcr-type {
  background: transparent;
  color: var(--colour-text-muted);
  border: 1px solid var(--colour-border);
  border-radius: 3px;
  padding: 4px 8px;
  font-size: 11px;
  font-weight: 500;
}
.pcr-app .pcr-interaction .pcr-save {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  border: 1px solid var(--colour-brand);
  border-radius: 3px;
  padding: 4px 10px;
  font-size: 12px;
  font-weight: 500;
  transition: filter 80ms cubic-bezier(0.2, 0, 0, 1);
}
.pcr-app .pcr-interaction .pcr-save:hover {
  filter: brightness(1.1);
}
.pcr-app .pcr-interaction .pcr-cancel,
.pcr-app .pcr-interaction .pcr-clear {
  background: transparent;
  color: var(--colour-text-muted);
  border: 1px solid var(--colour-border);
  border-radius: 3px;
  padding: 4px 10px;
  font-size: 12px;
}
.pcr-app .pcr-swatches {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--colour-border);
}
.pcr-app .pcr-swatches > button {
  border: 1px solid var(--colour-border);
  border-radius: 3px;
  margin: 2px;
}
/* Error state propagation via the hidden input */
input[aria-invalid="true"][type="hidden"] + .pcr-app,
[data-widget="colorpicker"] .pcr-trigger[aria-invalid="true"] {
  outline: 1px solid var(--colour-danger);
}

/* Quill .ql-* token overrides removed in #1001 — Quill replaced by
 * richtext (Dazzle-native) in #977 cycle 4. Caught by /fuzz
 * runtime pass: every rich_text page was 404ing on quill.snow.css.
 * The .richtext-* rules in components/richtext.css handle styling.
 */


/* ── Progress bar token overrides (UX-027 file upload, UX-030 review queue) */
/* Scoped to progress[data-file-progress] and progress[data-progress]   */
/* so generic <progress> elements elsewhere keep their browser-default look.  */
progress[data-file-progress],
progress[data-progress] {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  height: 4px;
  overflow: hidden;
  border-radius: 2px;
  background: var(--colour-surface-muted);
}
progress[data-file-progress]::-webkit-progress-bar,
progress[data-progress]::-webkit-progress-bar {
  background: var(--colour-surface-muted);
  border-radius: 2px;
}
progress[data-file-progress]::-webkit-progress-value,
progress[data-progress]::-webkit-progress-value {
  background: var(--colour-brand);
  border-radius: 2px;
  transition: width 120ms cubic-bezier(0.2, 0, 0, 1);
}
progress[data-file-progress]::-moz-progress-bar,
progress[data-progress]::-moz-progress-bar {
  background: var(--colour-brand);
  border-radius: 2px;
}

}
/* ── base/base.css ── */
@layer base {
/*
 * base.css — element defaults that apply before any component CSS.
 *
 * Sets typography, link colour, form-control surface, and the
 * canonical body background. References tokens only; never literal
 * values. Wrapped in @layer base (declared in dazzle.css).
 *
 * The dividing line between reset.css and base.css:
 *   reset    — strips browser defaults to a neutral baseline
 *   base     — applies Dazzle's defaults (the brand voice in CSS)
 */

body {
  font-family:
    system-ui,
    -apple-system,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Arial,
    sans-serif;
  font-size: var(--text-base);
  font-weight: var(--weight-regular);
  color: var(--colour-text);
  background: var(--colour-bg);
}

/* Headings — sizes and weights, not spacing. Spacing is the
 * caller's concern (component or utility). */

h1 {
  font-size: var(--text-3xl);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.02em;
}

h2 {
  font-size: var(--text-2xl);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.015em;
}

h3 {
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
}

h4 {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
}

h5 {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

h6 {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* Links — inherit colour by default; components apply brand tint
 * where appropriate (e.g. inline body copy). */

a {
  color: inherit;
  text-decoration-color: var(--colour-border);
  text-underline-offset: 0.15em;
}

a:hover,
a:focus-visible {
  text-decoration-color: currentColor;
}

/* Form controls — minimal Dazzle-flavoured baseline so an
 * un-styled <input> looks intentional rather than browser-default.
 * Components extend this; they do not fight it.
 *
 * box-sizing: border-box is mandatory here. UAs treat <select> as
 * border-box for height purposes but <input type=text> as content-box.
 * Progressive enhancement (combobox: select → text input) then jumps
 * outer height by padding+border (~18px) while min-height stays 2.5rem.
 * Force one box model so form-input / combobox-input / bare controls match. */

input,
textarea,
select {
  box-sizing: border-box;
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
}

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

button {
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  cursor: pointer;
  font-weight: var(--weight-medium);
  transition: background var(--duration-fast) var(--ease-out);
}

button:hover:not(:disabled) {
  background: var(--colour-bg);
}

button:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

/* TASTE-1 safety net: every other focusable gets the same ring — links,
 * summary, ARIA widgets, tabindex hosts. :where() keeps specificity 0 so
 * components can still opt into richer treatments without fighting it. */
:where(a, summary, [tabindex]:not([tabindex="-1"]), [role="button"], [role="tab"], [role="menuitem"]):focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* Code + pre — monospace family, neutral surface. */

code,
kbd,
samp,
pre {
  font-family:
    ui-monospace,
    "SF Mono",
    Menlo,
    Monaco,
    Consolas,
    monospace;
  font-size: 0.95em;
}

pre {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  overflow-x: auto;
}

/* Tables — a sensible default. Components override for data grids. */

table {
  border-collapse: collapse;
  width: 100%;
}

th,
td {
  text-align: start;
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--colour-border);
}

th {
  font-weight: var(--weight-semibold);
  color: var(--colour-text-muted);
  font-size: var(--text-sm);
}

/* ── screen-reader-only utility ──────────────────────────────────
 * The standard clip pattern (matches Dazzle utilities.css verbatim).
 * Un-prefixed by design: it is generic a11y plumbing referenced by
 * emitter markup (e.g. the wizard stepper's SR status spans), not a
 * design-system token. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

}
/* ── components/accordion.css ── */
@layer components {
/* HYPERPART: accordion */
/*
 * accordion — native <details> disclosure group.
 *
 * Zero JS. Single-open exclusivity is the HTML `name=` attribute: items
 * sharing a name auto-close their siblings (the shadcn `type="single"`
 * behaviour, with no controller). Drop `name` for an independent
 * (multi-open) group. Keyboard + AT semantics are the browser's own
 * <details>/<summary> — nothing to re-implement.
 */

.accordion {
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  overflow: hidden;
}

.accordion__item + .accordion__item {
  border-block-start: 1px solid var(--colour-border);
}

.accordion__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  cursor: pointer;
  list-style: none;
  transition: background var(--duration-fast) var(--ease-out);
}

.accordion__trigger::-webkit-details-marker { display: none; }

.accordion__trigger:hover {
  background: var(--colour-surface-muted);
}

.accordion__trigger:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  /* inset: the accordion clips its corners (overflow:hidden), so an
     outward offset ring would be cropped at the item edges. */
  outline-offset: calc(var(--focus-ring-offset) * -1);
}

/* CSS chevron — inherits currentColor, rotates when the item is open. */
.accordion__trigger::after {
  content: "";
  flex: none;
  width: 1rem;
  height: 1rem;
  background: currentColor;
  opacity: 0.55;
  mask: center / contain no-repeat
    url('data:image/svg+xml;utf8,<svg 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"><path d="m6 9 6 6 6-6"/></svg>');
  transition: transform var(--duration-base) var(--ease-out);
}

.accordion__item[open] .accordion__trigger::after {
  transform: rotate(180deg);
}

.accordion__panel {
  padding: 0 var(--space-lg) var(--space-md);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  line-height: var(--leading-normal);
}

@media (prefers-reduced-motion: reduce) {
  .accordion__trigger,
  .accordion__trigger::after {
    transition: none;
  }
}

}
/* ── components/alert.css ── */
@layer components {
/*
 * alert — HaTchi-MaXchi tranche 1 (spec: hatchi-maxchi-standalone).
 *
 * Identity signals: tone wash surface (--colour-*-soft) + registry icon +
 * text — colour never carries meaning alone (WCAG 1.4.1 as aesthetic).
 *
 * Contract:
 *   <div class="alert" data-tone="warning" role="alert">
 *     <span class="alert__icon" aria-hidden="true"><svg…/></span>
 *     <div class="alert__body">
 *       <div class="alert__title">Heads up</div>
 *       <div class="alert__description">Something needs attention.</div>
 *     </div>
 *   </div>
 * Tones: info (default) | success | warning | destructive | neutral.
 */

.alert {
  display: flex;
  gap: var(--space-md);
  align-items: flex-start;
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  font-size: var(--text-sm);
}

.alert__icon {
  display: inline-flex;
  width: 1.125rem;
  height: 1.125rem;
  flex-shrink: 0;
  margin-block-start: 0.125rem;
}

.alert__icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

.alert__title {
  font-weight: var(--weight-semibold);
  margin-block-end: 0.125rem;
}

.alert__description {
  color: var(--colour-text-muted);
  line-height: var(--leading-normal);
}

.alert[data-tone="info"] {
  background: var(--colour-brand-soft);
  border-color: color-mix(in oklab, var(--colour-brand) 25%, var(--colour-border));
}

.alert[data-tone="info"] .alert__icon { color: var(--colour-brand); }

.alert[data-tone="success"] {
  background: var(--colour-success-soft);
  border-color: color-mix(in oklab, var(--colour-success) 25%, var(--colour-border));
}

.alert[data-tone="success"] .alert__icon { color: var(--colour-success); }

.alert[data-tone="warning"] {
  background: var(--colour-warning-soft);
  border-color: color-mix(in oklab, var(--colour-warning) 25%, var(--colour-border));
}

.alert[data-tone="warning"] .alert__icon { color: var(--colour-warning); }

.alert[data-tone="destructive"] {
  background: var(--colour-danger-soft);
  border-color: color-mix(in oklab, var(--colour-danger) 25%, var(--colour-border));
}

.alert[data-tone="destructive"] .alert__icon { color: var(--colour-danger); }

/* ── separator ────────────────────────────────────────────────────── */
/* HYPERPART: separator */

.separator {
  border: none;
  border-block-start: 1px solid var(--colour-border);
  margin-block: var(--space-lg);
  /* The UA gives <hr> margin-inline auto; in a column flexbox those auto
   * margins absorb ALL free space and the rule's width collapses to zero
   * (rendered as a blank line). Pin the inline margins. */
  margin-inline: 0;
}

.separator--vertical {
  display: inline-block;
  width: 0;
  align-self: stretch;
  border-block-start: none;
  border-inline-start: 1px solid var(--colour-border);
  margin-block: 0;
  margin-inline: var(--space-md);
}

/* ── data-tooltip — CSS-only attribute tooltip ────────────────────── */

[data-tooltip] {
  position: relative;
}

[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  inset-block-end: calc(100% + 0.375rem);
  inset-inline-start: 50%;
  translate: -50% 0;
  padding: 0.25rem 0.5rem;
  border-radius: var(--radius-sm);
  background: var(--colour-text);
  color: var(--colour-bg);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: 1.3;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
  z-index: 50;
}

[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
  opacity: 1;
  transition-delay: 300ms;
}

/* Forced open — gallery capture / docs / dual-lock stills that must show the
   hint without hover (coherence + Playwright static shots). Do not use on
   product surfaces that should stay hover-only. */
[data-tooltip][data-tooltip-open]::after {
  opacity: 1;
  transition-delay: 0ms;
}

@media (prefers-reduced-motion: reduce) {
  [data-tooltip]::after { transition: none; }
}

/* HYPERPART: menu */
/* ── menu — details-based dropdown (the hypermedia answer) ──────────
 * Disclosure chrome: CSS chevron on the summary trigger (same family as
 * accordion / navigation-menu) — not Unicode ▾ in the label text
 * (affordance-disclosure-chrome). Summary usually carries .button.
 * Light-dismiss: controllers/details-light-dismiss.js (overlay-light-dismiss).
 */

.menu {
  position: relative;
  display: inline-block;
}

.menu > summary {
  list-style: none;
  cursor: pointer;
  /* .button is already inline-flex; ensure room for ::after chevron */
  gap: var(--space-2xs);
}

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

/* House disclosure chevron — 1rem, rotates when open (not part of label text) */
.menu > summary::after {
  content: "";
  flex: none;
  width: 1rem;
  height: 1rem;
  background: currentColor;
  opacity: 0.65;
  -webkit-mask: center / contain no-repeat
    url('data:image/svg+xml;utf8,<svg 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"><path d="m6 9 6 6 6-6"/></svg>');
  mask: center / contain no-repeat
    url('data:image/svg+xml;utf8,<svg 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"><path d="m6 9 6 6 6-6"/></svg>');
  transition: transform var(--duration-base, 150ms) var(--ease-out, ease);
}

.menu[open] > summary::after {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .menu > summary::after {
    transition: none;
  }
}

.menu__panel {
  position: absolute;
  inset-block-start: calc(100% + 0.25rem);
  inset-inline-end: 0;
  min-width: 11rem;
  padding: var(--space-xs);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 40;
}

.menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--colour-text);
  font-size: var(--text-sm);
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}

.menu__item:hover {
  background: var(--colour-bg);
}

.menu__item[data-tone="destructive"] { color: var(--colour-danger); }

.menu__separator {
  border: none;
  border-block-start: 1px solid var(--colour-border);
  margin-block: var(--space-xs);
}

/* ── alert-dialog — designed hx-confirm surface ───────────────────── */
/* HYPERPART: confirm */
dialog.alert-dialog {
  width: min(26rem, calc(100vw - 2rem));
  padding: var(--space-2xl);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  background: var(--colour-surface);
  color: var(--colour-text);
  box-shadow: var(--shadow-lg);
  text-align: center;
}

dialog.alert-dialog::backdrop {
  background: oklch(13% 0.028 261.69 / 0.5);
  backdrop-filter: blur(2px);
}

.alert-dialog__icon {
  display: inline-flex;
  width: 2rem;
  height: 2rem;
  color: var(--colour-danger);
  margin-block-end: var(--space-md);
}

.alert-dialog__icon svg { width: 100%; height: 100%; }

.alert-dialog__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  margin: 0 0 var(--space-sm);
}

.alert-dialog__message {
  color: var(--colour-text-muted);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-xl);
}

.alert-dialog__actions {
  display: flex;
  gap: var(--space-md);
  justify-content: center;
}

}
/* ── components/toast.css ── */
@layer components {
/*
 * HYPERPART: toast
 *
 * Auto-dismissing notifications for the shell toast stack.
 * Server OOB (`with_toast`) and the client bridge (`toast.js`) share
 * this markup. Levels key off data-toast-level (info | success |
 * warning | error). Optional title + action row are slots — message-only
 * markup remains valid.
 *
 * Contract (contracts/toast.py):
 *   <div id="toast" class="toast-stack" aria-live="polite"
 *        data-toast-cap="8">
 *     <div class="toast" data-toast-level="success"
 *          data-remove-after="8s" role="status">
 *       <div class="toast__body">
 *         <div class="toast__title">Saved</div>
 *         <div class="toast__message">Your changes are live.</div>
 *         <div class="toast__actions">
 *           <a class="toast__action" href="/…">View</a>
 *           <button type="button" class="toast__action"
 *                   data-toast-dismiss>Dismiss</button>
 *         </div>
 *       </div>
 *       <button type="button" class="toast__close"
 *               data-toast-dismiss aria-label="Dismiss"></button>
 *     </div>
 *   </div>
 *
 * Host behaviour (controllers/toast.js): auto-dismiss via
 * data-remove-after (default 8s), hover/focus pause, stack cap,
 * enter/leave motion, dismiss clicks.
 */

/* ── stack — viewport overlay (whole page, not content column) ───── */

.toast-stack {
  position: fixed;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
  z-index: 80;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-sm);
  width: min(24rem, calc(100vw - 2rem));
  max-width: min(24rem, calc(100vw - 2rem));
  /* Children re-enable hits; empty chrome does not steal clicks. */
  pointer-events: none;
}

/* ── toast unit ────────────────────────────────────────────────────── */

.toast {
  position: relative;
  overflow: hidden;
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  padding-inline: var(--space-md);
  padding-block: var(--space-md);
  padding-block-end: calc(var(--space-md) + 2px);
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  border-inline-start-width: 4px;
  background: var(--colour-surface);
  color: var(--colour-text);
  box-shadow: var(--shadow-md);
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
  will-change: transform, opacity;
}

/* TTL progress — host sets animation-duration from data-remove-after. */
.toast__progress {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 2px;
  transform-origin: inline-start;
  background: color-mix(in oklch, var(--colour-text) 28%, transparent);
  pointer-events: none;
  animation-name: toast-ttl;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}

.toast[data-toast-level="info"] .toast__progress {
  background: color-mix(in oklch, var(--colour-brand) 55%, transparent);
}
.toast[data-toast-level="success"] .toast__progress {
  background: color-mix(in oklch, var(--colour-success) 55%, transparent);
}
.toast[data-toast-level="warning"] .toast__progress {
  background: color-mix(in oklch, var(--colour-warning) 55%, transparent);
}
.toast[data-toast-level="error"] .toast__progress {
  background: color-mix(in oklch, var(--colour-danger) 55%, transparent);
}

@keyframes toast-ttl {
  from {
    transform: scaleX(1);
  }
  to {
    transform: scaleX(0);
  }
}

.toast[data-toast-level="info"] {
  border-inline-start-color: var(--colour-brand);
  background: color-mix(in oklch, var(--colour-brand) 6%, var(--colour-surface));
}

.toast[data-toast-level="success"] {
  border-inline-start-color: var(--colour-success);
  background: color-mix(in oklch, var(--colour-success) 6%, var(--colour-surface));
}

.toast[data-toast-level="warning"] {
  border-inline-start-color: var(--colour-warning);
  background: color-mix(in oklch, var(--colour-warning) 6%, var(--colour-surface));
}

.toast[data-toast-level="error"] {
  border-inline-start-color: var(--colour-danger);
  background: color-mix(in oklch, var(--colour-danger) 6%, var(--colour-surface));
}

/* Level icon — decorative (aria-hidden); severity also on border + progress. */
.toast-icon,
.toast__icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.125rem;
  height: 1.125rem;
  margin-block-start: 0.1rem;
  color: var(--colour-text-muted);
}

.toast__icon svg,
.toast-icon svg {
  width: 1.125rem;
  height: 1.125rem;
  display: block;
}

.toast[data-toast-level="info"] .toast-icon,
.toast[data-toast-level="info"] .toast__icon {
  color: var(--colour-brand);
}

.toast[data-toast-level="success"] .toast-icon,
.toast[data-toast-level="success"] .toast__icon {
  color: var(--colour-success);
}

.toast[data-toast-level="warning"] .toast-icon,
.toast[data-toast-level="warning"] .toast__icon {
  color: var(--colour-warning);
}

.toast[data-toast-level="error"] .toast-icon,
.toast[data-toast-level="error"] .toast__icon {
  color: var(--colour-danger);
}

.toast__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.toast__title {
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  font-size: var(--text-sm);
}

.toast__subtitle {
  font-size: var(--text-xs, 0.75rem);
  color: var(--colour-text-muted);
}

.toast-message,
.toast__message {
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.toast__title + .toast__message,
.toast__subtitle + .toast__message {
  color: var(--colour-text-muted);
}

/* Person / message composition (decision 0011 phase E) — not a severity. */
.toast__avatar {
  flex-shrink: 0;
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  object-fit: cover;
  background: color-mix(in oklch, var(--colour-brand) 12%, var(--colour-surface));
  color: var(--colour-brand);
  font-size: var(--text-xs, 0.75rem);
  font-weight: var(--weight-semibold);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-block-start: 0.05rem;
}

.toast__avatar--fallback {
  border: 1px solid var(--colour-border);
}

.toast[data-toast-composition="person"] .toast__actor {
  font-weight: var(--weight-semibold);
}

/* Swipe-dismiss feedback (host sets inline transform while dragging). */
.toast {
  touch-action: pan-y;
  user-select: none;
}
.toast__body,
.toast__action,
.toast__close {
  user-select: text;
}

.toast__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-block-start: var(--space-sm);
}

.toast__action {
  appearance: none;
  border: none;
  background: transparent;
  color: var(--colour-brand);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

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

.toast__action:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.toast__close {
  appearance: none;
  flex-shrink: 0;
  width: 1.5rem;
  height: 1.5rem;
  margin: -0.25rem -0.25rem 0 0;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--colour-text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  font-size: 1rem;
}

.toast__close::before {
  content: "×";
  font-weight: var(--weight-semibold);
}

.toast__close:hover {
  color: var(--colour-text);
  background: color-mix(in oklch, var(--colour-text) 8%, transparent);
}

.toast__close:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* ── motion (Penguin-shaped: slide in from edge, slide out + fade) ──
 * Enter ~300ms ease-out; leave ~300ms ease-in. Host waits for leave
 * before DOM remove (see controllers/toast.js). */

.toast-enter {
  animation: toast-in var(--duration-slow) var(--ease-out) both;
}

.toast-leave {
  animation: toast-out var(--duration-slow) var(--ease-in) both;
  pointer-events: none;
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateX(1rem) translateY(-0.35rem);
  }
  to {
    opacity: 1;
    transform: translateX(0) translateY(0);
  }
}

@keyframes toast-out {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(1.5rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .toast-enter,
  .toast-leave {
    animation-duration: 1ms;
  }
  .toast__progress {
    /* Still shrink so remaining time is visible, but no enter/leave flourish. */
    animation-timing-function: linear;
  }
}

/* ── gallery live stage (demo chrome only — not product surface) ── */

.hm-toast-stage {
  min-block-size: 18rem;
  padding: 1.25rem 1.5rem 2rem;
  background:
    linear-gradient(
      180deg,
      color-mix(in oklch, var(--colour-brand) 4%, var(--colour-bg)) 0%,
      var(--colour-bg) 8rem
    );
}

.hm-toast-stage__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
  margin-block-end: 1rem;
  padding-block-end: 0.75rem;
  border-block-end: 1px solid var(--colour-border);
  font-size: var(--text-sm);
}

.hm-toast-stage__hint {
  color: var(--colour-text-muted);
  font-size: 0.75rem;
}

.hm-toast-stage__body {
  max-width: 36rem;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--colour-text);
}

.hm-toast-stage__body p {
  margin: 0 0 1rem;
}

}
/* ── components/badge.css ── */
@layer components {
/*
 * badge.css — status / state / enum badge component.
 *
 * Single semantic class (.badge) plus tone modifier via
 * data-tone attribute. Used by the render_status_badge macro
 * (templates/macros/status_badge.html) — 19 import sites across
 * tables, kanban cards, detail views, review queues.
 *
 * The macro owns the tone-resolution logic (Jinja `badge_tone`
 * filter resolves enum values to one of five tones); this CSS
 * file owns the visual styling. Authors don't think about
 * specific colour values — they invoke the macro with a value
 * and trust the design system.
 *
 * Wrapped in @layer components (declared in dazzle.css). All
 * lengths and colours route via design tokens — Stylelint enforces
 * this in components/.
 */

.badge {
  display: inline-flex;
  align-items: center;
  border-radius: var(--radius-sm);
  font-weight: var(--weight-medium);
  /* Defaults match the macro's `size="md"` branch. The .badge-sm
   * modifier overrides for dense regions (timeline, bar_chart). */
  padding-inline: var(--space-sm);
  height: 1.25rem;
  font-size: var(--text-xs);
}

.badge-sm {
  padding-inline: var(--space-xs);
  height: 1rem;
  /* Slightly smaller than the default --text-xs for micro contexts. */
  font-size: 0.625rem;
  line-height: 1;
}

/* ── WCAG colour+icon+text (#1493 slice 2 part 3) ─────────────────
 * Non-neutral tones lead with a decorative glyph so state is never
 * conveyed by colour alone (WCAG 1.4.1). Emitted by `badge_icon_html`
 * as the first child of .badge; neutral badges carry no icon. */

.badge-icon {
  margin-inline-end: var(--space-xs);
  font-size: 0.875em;
  line-height: 1;
}

/* ── Tone tints ───────────────────────────────────────────────────
 * Five tone tokens matching the wider Dazzle palette: neutral
 * (default), success, warning, info, destructive. Selected via
 * `data-tone` attribute emitted by the macro. */

.badge[data-tone="success"] {
  background: var(--colour-success-soft);
  color: var(--colour-success);
}

.badge[data-tone="warning"] {
  background: var(--colour-warning-soft);
  color: var(--colour-warning);
}

.badge[data-tone="info"] {
  background: var(--colour-brand-soft);
  color: var(--colour-brand-text);
}

.badge[data-tone="destructive"] {
  background: var(--colour-danger-soft);
  color: var(--colour-danger);
}

.badge[data-tone="neutral"],
.badge:not([data-tone]) {
  background: var(--colour-bg);
  color: var(--colour-text-muted);
}

/* ── Optional border ──────────────────────────────────────────────
 * The macro accepts a `bordered` boolean for emphasis (used in the
 * detail region). Border colour matches the tone. */

.badge.bordered {
  border: 1px solid var(--colour-border);
}

.badge.bordered[data-tone="success"] {
  border-color: var(--colour-success);
}
.badge.bordered[data-tone="warning"] {
  border-color: var(--colour-warning);
}
.badge.bordered[data-tone="info"] {
  border-color: var(--colour-brand);
}
.badge.bordered[data-tone="destructive"] {
  border-color: var(--colour-danger);
}

/* ── Em-dash placeholder ──────────────────────────────────────────
 * When the macro is called with empty value, it renders a single-
 * char placeholder (—) for absent status. Same muted treatment as
 * any other "no value" token. */

.badge-empty {
  color: var(--colour-text-muted);
  font-size: var(--text-xs);
}

/* Registry SVG badge glyphs (Phase 3, supersedes #1493 entity glyphs). */
.badge-icon {
  display: inline-flex;
  width: 0.875em;
  height: 0.875em;
  flex-shrink: 0;
}

.badge-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

}
/* ── components/button.css ── */
@layer components {
/*
 * button.css — button component family.
 *
 * One semantic class `.button` + a `data-variant` attribute for the
 * visual variant (canonical, matching `data-tone` on badges/alerts):
 *   data-variant="ghost"        no border, hover-tint only (back, cancel)
 *   data-variant="outline"      bordered neutral (default action button)
 *   data-variant="destructive"  bordered danger (delete, revoke)
 *   data-variant="primary"      filled brand (the recommended action)
 * and `data-size="sm"` for the compact inline-row size.
 *
 * A variant NEVER drops the base class — `.button` carries the shared
 * geometry, `[data-variant]` layers the skin. (The standalone
 * `.button-primary` rule in components/table.css is a separate, smaller
 * table-header variant and is intentionally left as a class.)
 *
 * The `<button>` element baseline in base.css already provides focus ring,
 * disabled state, font inheritance — this extends it with size + variant.
 *
 * Wrapped in @layer components.
 */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Icon + label: gap owns spacing — do not rely on a text-node space after
     an icon span (flex collapses that inconsistently vs sprite <svg> siblings). */
  gap: var(--space-xs);
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  text-decoration: none;
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.button[data-variant="ghost"] {
  background: transparent;
  border: 1px solid transparent;
  color: var(--colour-text-muted);
}

.button[data-variant="ghost"]:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

/* Subtle press: one step past hover via tokens — no translate/shadow
   (primary owns the physical press). */
.button[data-variant="ghost"]:active {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}

.button[data-variant="outline"] {
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
}

.button[data-variant="outline"]:hover {
  background: var(--colour-bg);
}

.button[data-variant="outline"]:active {
  background: var(--colour-surface-muted);
}

.button[data-variant="destructive"] {
  background: var(--colour-surface);
  color: var(--colour-danger);
  border: 1px solid var(--colour-danger);
}

.button[data-variant="destructive"]:hover {
  background: var(--colour-danger-soft);
}

.button[data-variant="destructive"]:active {
  /* Token mix — deeper wash than danger-soft, not a brightness filter. */
  background: color-mix(in oklch, var(--colour-danger) 16%, var(--colour-surface));
}

.button[data-variant="primary"] {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  border: 1px solid var(--colour-brand);
  box-shadow: var(--shadow-sm);
}

/* TASTE-2/8: explicit hover step from the ramp (not a brightness filter)
   and a physical press — shadow collapses, button settles 0.5px. */
.button[data-variant="primary"]:hover {
  background: var(--brand-700);
  border-color: var(--brand-700);
}

.button[data-variant="primary"]:active {
  box-shadow: none;
  transform: translateY(0.5px);
}

.button[data-variant="primary"]:disabled,
.button[data-variant="primary"][aria-disabled="true"] {
  opacity: 0.6;
  pointer-events: none;
}

/* Size modifier — smaller button used by inline action rows
 * (e.g. 2FA settings row buttons). Composes with .button base. */
.button[data-size="sm"] {
  height: 1.75rem;
  padding-inline: var(--space-md);
  font-size: var(--text-xs);
}

}
/* ── components/dialog.css ── */
@layer components {
/* HYPERPART: dialog */
/*
 * dialog — modal built on the native <dialog>. Opening is the only
 * behaviour that needs script: dialog.js calls showModal() when a
 * [data-dialog-open="id"] trigger is clicked. Everything else is
 * native — CLOSE is a <form method="dialog"> submit (the ✕ and footer
 * buttons), Esc, or a backdrop tap (closedby="any"); focus trapping and
 * the inert background come free; showModal() moves focus into the dialog.
 *
 * Instance-safe: the trigger names its dialog by id, so any number of
 * dialogs coexist on a page (no shared global handle).
 */

dialog.dialog {
  width: min(32rem, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  background: var(--colour-surface);
  color: var(--colour-text);
  box-shadow: var(--shadow-lg);
  /* Shell may hold initial focus after open (dialog.js) — no ring on the panel. */
  outline: none;
}

dialog.dialog:focus,
dialog.dialog:focus-visible {
  outline: none;
}

dialog.dialog::backdrop {
  background: oklch(13% 0.028 261.69 / 0.4);
  backdrop-filter: blur(2px);
}

.dialog__header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  padding: var(--space-lg) var(--space-lg) var(--space-md);
}

.dialog__title {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}

/* Always-visible dismiss affordance — a native submit inside the
   method="dialog" form, so the tap closes with no JS (touch-safe). */
.dialog__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  margin: calc(var(--space-xs) * -1) calc(var(--space-xs) * -1) 0 0;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--colour-text-muted);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.dialog__close:hover {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}

.dialog__close:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

.dialog__close svg {
  width: 1rem;
  height: 1rem;
}

/* Body is a composition host (same rule as drawer): primary text so nested
   Hyperparts keep their own colour scale. Leading prose stays muted. */
.dialog__body {
  padding: 0 var(--space-lg);
  font-size: var(--text-sm);
  color: var(--colour-text);
  line-height: var(--leading-normal);
}

.dialog__body > p {
  margin: 0;
  color: var(--colour-text-muted);
}

.dialog__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  padding: var(--space-lg);
}

@media (prefers-reduced-motion: reduce) {
  .dialog__close { transition: none; }
}

}
/* ── components/drawer.css ── */
@layer components {
/* HYPERPART: drawer */
/*
 * drawer — an edge-anchored panel built on the native <dialog>, so it is
 * a drawer with a modal's guarantees: focus is trapped, the background goes
 * inert, Esc and a backdrop tap dismiss it — none of which a plain <aside>
 * toggle gets for free. It shares the dialog's opener (dialog.js: a
 * [data-dialog-open="id"] trigger calls showModal()) and native close
 * (<form method="dialog"> + Esc + closedby="any") — no drawer-specific JS.
 *
 * `data-side="right"` (default) | `"left"` anchors the edge; the panel
 * slides in from that edge (native <dialog> enter transition via
 * @starting-style), honouring prefers-reduced-motion.
 */

dialog.drawer {
  width: min(24rem, calc(100vw - 3rem));
  max-width: none;
  height: 100dvh;
  max-height: none;
  padding: 0;
  border: none;
  border-radius: 0;
  background: var(--colour-surface);
  color: var(--colour-text);
  box-shadow: var(--shadow-lg);
  /* Shell may hold initial focus after open (dialog.js) — no ring on the panel. */
  outline: none;
  /* right edge (default): pin to the inline-end */
  margin-block: 0;
  margin-inline: auto 0;
  border-inline-start: 1px solid var(--colour-border);
  /* enter transition: slide from the anchored edge. `allow-discrete` lets
     the dialog animate on open despite the display:none→block flip.
     Width is animated so Expand/Restore is visibly functional (not a silent
     data-attr flip). */
  translate: 0 0;
  transition:
    translate var(--duration-base) var(--ease-out),
    width var(--duration-base) var(--ease-out),
    overlay var(--duration-base) var(--ease-out) allow-discrete,
    display var(--duration-base) var(--ease-out) allow-discrete;
}

dialog.drawer:focus,
dialog.drawer:focus-visible {
  outline: none;
}

/* Width presets (data-width) — promoted from Dazzle's peek
 * slide_over feature (Tier F2): the author's declared panel width.
 * Unset/other values keep the drawer's default width above; presets
 * only widen on viewports that can afford it. */
@media (min-width: 40rem) {
  dialog.drawer[data-width="sm"] { width: min(24rem, calc(100vw - 3rem)); }
  dialog.drawer[data-width="md"] { width: min(32rem, calc(100vw - 3rem)); }
  dialog.drawer[data-width="lg"] { width: min(42rem, calc(100vw - 3rem)); }
  dialog.drawer[data-width="xl"] { width: min(56rem, calc(100vw - 3rem)); }
  dialog.drawer[data-width="full"] { width: calc(100vw - 3rem); }
}

dialog.drawer[data-side="left"] {
  margin-inline: 0 auto;
  border-inline: none;
  border-inline-end: 1px solid var(--colour-border);
}

/* start state (opening) + exit state (closing) sit off the anchored edge.
   NOTE: the slide uses a physical `translate` while anchoring uses logical
   margins — correct for LTR; under RTL a right-anchored panel would slide
   from the wrong edge. The package has no RTL/dir support today, so this is
   a tracked follow-up, not a live defect. */
@starting-style {
  dialog.drawer[open] { translate: 100% 0; }
  dialog.drawer[data-side="left"][open] { translate: -100% 0; }
}
dialog.drawer:not([open]) { translate: 100% 0; }
dialog.drawer[data-side="left"]:not([open]) { translate: -100% 0; }

dialog.drawer::backdrop {
  background: oklch(13% 0.028 261.69 / 0.4);
  backdrop-filter: blur(2px);
}

/* The open drawer is a flex column (pinned header + footer, scrolling body).
   Scoped to [open] so a CLOSED dialog falls through to the UA
   `dialog:not([open]){display:none}` — an unconditional `display:flex` would
   keep the closed dialog rendered off-screen, leaking its controls into the
   tab order + a11y tree. The base `transition: display allow-discrete` still
   animates the exit. */
dialog.drawer[open] {
  display: flex;
  flex-direction: column;
}

/* header/body/footer are the dialog's flex column whether they sit directly
   in the <dialog> (a JS-driven drawer, e.g. a detail panel) or are wrapped in
   a <form method="dialog"> (the native-close form). display:contents makes the
   optional form transparent to layout so both shapes stack the same. */
dialog.drawer > form {
  display: contents;
}

.drawer__header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  flex: none;
  padding: var(--space-lg);
  border-block-end: 1px solid var(--colour-border);
}

.drawer__title {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}

.drawer__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  margin: calc(var(--space-xs) * -1) calc(var(--space-xs) * -1) 0 0;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--colour-text-muted);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.drawer__close:hover {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}

.drawer__close:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

.drawer__close svg {
  width: 1rem;
  height: 1rem;
}

/* Body is a composition host: primary text colour so nested Hyperparts
   (field labels, card values, switch labels, badges) match their standalone
   demos. Do not paint the whole body muted — guests inherit. Leading prose
   (direct children <p>, gallery hm-demo-muted) is the secondary voice. */
.drawer__body {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-lg);
  font-size: var(--text-sm);
  color: var(--colour-text);
  line-height: var(--leading-normal);
}

.drawer__body > p { margin: 0; color: var(--colour-text-muted); }

.drawer__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  flex: none;
  padding: var(--space-lg);
  border-block-start: 1px solid var(--colour-border);
}

/* Flat-named internal chrome (moved from Dazzle dashboard.css, HMC-007b —
   the drawer's chrome travels with the drawer component). These are the class
   names the detail-drawer emitter produces (distinct from the __header/__body
   BEM parts above); kept verbatim, already tokenised. */
.drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-block-end: 1px solid var(--colour-border);
  flex-shrink: 0;
}

.drawer-button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-out);
}

.drawer-button:hover {
  background: var(--colour-bg);
}

.drawer-content {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-md);
}

@media (prefers-reduced-motion: reduce) {
  dialog.drawer { transition: none; }
  dialog.drawer[open],
  dialog.drawer:not([open]) { translate: 0 0; }
  .drawer__close { transition: none; }
}

/* Detail-drawer width instance (migrated from Dazzle dashboard.css, HMC-020):
   the entity detail slide-over is wider than the HM drawer default. */
#detail-drawer.drawer {
  width: min(42rem, calc(100vw - 3rem));
}

}
/* ── components/form.css ── */
@layer components {
/*
 * form.css — form field component family.
 *
 * Used by the render_field macro (templates/macros/form_field.html)
 * which dispatches across ~14 field types: checkbox, ref-entity
 * select, search-select, plain text/number/email/date/datetime
 * (native <input type=date>/datetime-local — the JS calendar `picker`
 * widget was dropped 2026-07-09), textarea, select, money, file, plus
 * widget-driven combobox / tags (Tom Select) / color / rich_text.
 * Native <input type="color"> covers the colour widget.
 *
 * Pre-CSS-refactor every branch composed its own inline Tailwind
 * class string with conditional Jinja for the error/idle border.
 * Post-refactor, the macro emits semantic classes only and styling
 * lives here. Error state derives from the `aria-invalid="true"`
 * attribute the macro already emits for accessibility — single
 * source of truth, no parallel `.is-error` modifier needed.
 *
 * Wrapped in @layer components.
 */

/* ─────────────────────────── outer wrapper ──────────────────────
 * Vertical flow of label / hint / input / error. Tight gap so
 * forms feel cohesive; .stack base utility would be too generous. */

.form-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  width: 100%;
}

/* ─────────────────────────── label ──────────────────────────────
 * Block label above the input. Required-mark is a child span. */

.form-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.form-required {
  color: var(--colour-danger);
  margin-inline-start: var(--space-xs);
}

/* ─────────────────────────── hint + error ───────────────────────
 * Both small + muted; error is danger-coloured. The macro emits
 * <p id="hint-{name}"> and <p id="error-{name}"> for aria-describedby
 * referencing. */

.form-hint {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  line-height: var(--leading-normal);
}

.form-error {
  font-size: var(--text-xs);
  color: var(--colour-danger);
  margin-block-start: var(--space-xs);
}

/* ─────────────────────────── base input ─────────────────────────
 * The canonical row input — used by text, email, number, date,
 * datetime, select. Single class .form-input handles the lot.
 * Error state derives from `aria-invalid="true"` (selector below);
 * focus state via :focus-visible. */

.form-input {
  width: 100%;
  /* border-box: see base.css form controls — select vs input UA mismatch. */
  box-sizing: border-box;
  /* #930: previously `height: 2rem` (32px) with `padding: 8px 12px`
     left only 16px of vertical content area for a ~22px line-box,
     clipping descenders (g/p/y) on every input + select placeholder.
     Switch to a min-height + explicit line-height + padding-block so
     the line-box always fits. `height: auto` lets future multi-line
     content (group labels in selects, wrapped placeholders) scale. */
  height: auto;
  min-height: 2.5rem;
  line-height: 1.4;
  padding-block: var(--space-sm);
  padding-inline: var(--space-md);
  font-size: var(--text-sm);
  color: var(--colour-text);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  transition:
    box-shadow var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out);
}

/* WebKit (and some UA styles) ignore min-height on native <select>, so a
 * pre-enhance combobox seed can paint ~23px while the post-enhance text
 * input honors min-height 2.5rem — a visible jump on first click. Pin
 * the used height for selects only; text inputs stay height:auto. */
select.form-input {
  height: 2.5rem;
  /* Flatten UA chrome so padding+height share one border-box model. */
  -webkit-appearance: none;
  appearance: none;
}

.form-input::placeholder {
  color: var(--colour-text-muted);
}

.form-input:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
  border-color: var(--colour-brand);
}

.form-input[aria-invalid="true"] {
  border-color: var(--colour-danger);
}

.form-input[aria-invalid="true"]:focus-visible {
  box-shadow: 0 0 0 1px var(--colour-danger);
}

/* ─────────────────────────── textarea ───────────────────────────
 * Same chrome as .form-input but multi-line. Override height
 * + padding-block; resize handle vertical only. */

.form-textarea {
  height: auto;
  min-height: 6rem;
  padding-block: var(--space-sm);
  resize: vertical;
}

/* ─────────────────────────── checkbox ───────────────────────────
 * Inline row: checkbox + text label. The macro wraps both in
 * <label class="form-checkbox-label"> for click-target convenience. */

.form-checkbox-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.form-checkbox {
  width: 1rem;
  height: 1rem;
  border-radius: var(--radius-sm);
  accent-color: var(--colour-brand);
}

/* HYPERPART: money */
/* ─────────────────────────── money widget ───────────────────────
 * Three sub-elements:
 *   .form-money-group — flex row container
 *   .form-money-prefix — currency symbol box (pinned currency)
 *   .form-money-select — currency dropdown (unpinned currency)
 * The amount input uses .form-input with a .form-input-trailing
 * modifier to round only the right corners. */

.form-money-group {
  display: flex;
  width: 100%;
}

.form-money-prefix {
  display: inline-flex;
  align-items: center;
  /* #930: align with .form-input height to keep prefix flush. */
  min-height: 2.5rem;
  padding-inline: var(--space-md);
  background: var(--colour-bg);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  border: 1px solid var(--colour-border);
  border-inline-end: 0;
  border-start-start-radius: var(--radius-sm);
  border-end-start-radius: var(--radius-sm);
}

.form-money-select {
  /* #930: matches updated input height; also gets line-height + padding
     so descenders in the currency option list don't clip. */
  box-sizing: border-box;
  -webkit-appearance: none;
  appearance: none;
  height: 2.5rem;
  min-height: 2.5rem;
  line-height: 1.4;
  padding-block: var(--space-sm);
  width: 7rem;
  padding-inline: var(--space-sm);
  padding-inline-end: 1.5rem;
  background-color: var(--colour-surface);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.4rem center;
  background-size: 0.75rem 0.75rem;
  color: var(--colour-text);
  font-size: var(--text-sm);
  border: 1px solid var(--colour-border);
  border-inline-end: 0;
  border-start-start-radius: var(--radius-sm);
  border-end-start-radius: var(--radius-sm);
}

.form-money-select:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
}

/* Modifier on .form-input that joins it to a leading prefix —
 * rounds only the right corners. Used by the money widget. */
.form-input-trailing {
  border-start-start-radius: 0;
  border-end-start-radius: 0;
}

/* ─────────────────────────── file upload ────────────────────────
 * Two states (dragging via Alpine x-show; preview vs dropzone via
 * x-show). Container uses .file-upload (existing class on the
 * x-data root). */

.file-upload-preview {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-md);
  margin-block-end: var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
}

.file-upload-preview-icon {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  color: var(--colour-success);
}

.file-upload-preview-name {
  font-size: var(--text-sm);
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.file-upload-preview-clear {
  width: 1.5rem;
  height: 1.5rem;
  margin-inline-start: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--colour-text-muted);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.file-upload-preview-clear:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.file-upload-zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 8rem;
  border: 2px dashed var(--colour-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition:
    background var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out);
}

.file-upload-zone:hover,
.file-upload-zone.dragging {
  background: var(--colour-bg);
  border-color: var(--colour-brand);
}

.file-upload-zone-icon {
  width: 2rem;
  height: 2rem;
  margin-block-end: var(--space-sm);
  color: var(--colour-text-muted);
  opacity: 0.6;
}

.file-upload-zone-prompt {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.file-upload-progress {
  width: 100%;
  height: 0.25rem;
  margin-block-start: var(--space-sm);
  border-radius: 2px;
}

/* ─────────────────────────── search-select wrapper ──────────────
 * UX-028 search-select dynamic search; the wrapper is the include
 * `fragments/search_select.html` which has its own internal
 * structure. We don't override that here — only style the label /
 * hint / error around it. The fragment-level CSS would migrate
 * separately when we get to fragments/. */

/* No rules needed — search-select inherits .form-label + hint
 * + error from the surrounding macro structure. */

/* ─────────────────────────── colorpicker (UX-024) ───────────────
 * Native <input type="color"> + hex display sit inline next to each
 * other. #976 dropped the Pickr wrapper; the native input ships ~36 KB
 * lighter and covers every actual usage in the example apps. */

.form-color-group {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.form-color-input {
  /* Strip the user-agent native chrome down to a flat 2rem swatch
   * so the button matches the visual rhythm of the other form-*
   * inputs. Cross-browser quirks: WebKit's swatch renders inside
   * `::-webkit-color-swatch-wrapper`; Firefox renders the swatch
   * directly on the input. Both are squared-off here. */
  block-size: 2rem;
  inline-size: 2rem;
  padding: 0;
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  background: transparent;
  cursor: pointer;
  transition: filter var(--duration-fast) var(--ease-out);
}

.form-color-input:hover {
  filter: brightness(1.05);
}

.form-color-input::-webkit-color-swatch-wrapper {
  padding: 0;
}

.form-color-input::-webkit-color-swatch {
  border: 0;
  border-radius: calc(var(--radius-sm) - 1px);
}

.form-color-input::-moz-color-swatch {
  border: 0;
  border-radius: calc(var(--radius-sm) - 1px);
}

.form-color-hex {
  /* Companion hex field (cycle 2133) — editable, not submitted. Compact
   * mono input so leftover junk stays visible instead of a muted span. */
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  inline-size: 9ch;
  min-inline-size: 7.5ch;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
}

.form-color-hex:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

.form-time-group {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.form-time-iso {
  /* Companion ISO field (cycle 2144) — editable, not submitted.
   * Compact mono input so leftover junk stays visible. */
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
  color: var(--colour-text);
  inline-size: 11ch;
  min-inline-size: 8ch;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
}

.form-time-group:has(input[type="datetime-local"]) .form-time-iso {
  inline-size: 18ch;
  min-inline-size: 16ch;
}

.form-time-iso:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

.form-date-group {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.form-date-iso {
  /* Companion ISO field (cycle 2145) — editable, not submitted.
   * Compact mono input so leftover junk stays visible. */
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
  color: var(--colour-text);
  inline-size: 12ch;
  min-inline-size: 10ch;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
}

.form-date-iso:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

.form-number-group {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.form-number-value {
  /* Companion number field (cycle 2149) — editable, not submitted.
   * Compact mono input so leftover junk stays visible. */
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
  color: var(--colour-text);
  inline-size: 8ch;
  min-inline-size: 6ch;
  text-align: end;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
}

.form-number-value:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

/* ─────────────────────────── richtext (UX-025) ──────────────────
 * Quill editor wrapper. Border + radius live on the wrapper; Quill
 * internals manage their own toolbar/editor styling. */

.form-richtext {
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--colour-bg);
}

.form-richtext[aria-invalid="true"] {
  border-color: var(--colour-danger);
}

/* ─────────────────────────── slider (UX-023) ────────────────────
 * Native <input type="range"> + dzRangeTooltip controller. */

.form-slider-group {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.form-slider {
  inline-size: 100%;
}

.form-slider-value {
  /* Companion number field (cycle 2134) — editable, not submitted.
   * Compact mono input so leftover junk stays visible instead of a
   * muted span. Legacy span readouts still pick up the type scale. */
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
  color: var(--colour-text);
  inline-size: 6ch;
  min-inline-size: 4.5ch;
  text-align: end;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
}

.form-slider-value:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

/* ─────────────────────────── companion regions (#923) ───────────
 * Read-only panels that sit alongside form sections — top, bottom,
 * or below a named section anchor. The panel is a muted card with a
 * tighter typographic hierarchy than the form itself. */

.form-companion {
  margin-block: var(--space-md);
  padding: var(--space-md);
  border-radius: var(--radius-md);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
}

.form-companion-eyebrow {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--colour-text-muted);
  margin-block-end: var(--space-2xs);
}

.form-companion-title {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin-block-end: var(--space-sm);
}

/* summary_row — horizontal metric tiles. */
.form-companion-summary-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--space-sm);
}

.form-companion-metric {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
}

.form-companion-metric-label {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  text-transform: capitalize;
}

.form-companion-metric-value {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  font-variant-numeric: tabular-nums;
}

.form-companion-metric-expr {
  display: none;  /* runtime-evaluated expression — debug only */
}

/* status_list — vertical title + caption rows. */
.form-companion-status-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.form-companion-entry {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding-inline-start: var(--space-md);
  border-inline-start: 2px solid var(--colour-border);
}

.form-companion-entry--ok {
  border-inline-start-color: var(--colour-success, hsl(145 55% 45%));
}

.form-companion-entry--warn {
  border-inline-start-color: var(--colour-warn, hsl(40 90% 55%));
}

.form-companion-entry--pending {
  border-inline-start-color: var(--colour-text-muted);
  opacity: 0.85;
}

.form-companion-entry-title {
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.form-companion-entry-caption {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  line-height: 1.4;
}

/* pipeline_steps — left-to-right ordered stage cards. */
.form-companion-pipeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: var(--space-sm);
  counter-reset: pipeline-step;
}

.form-companion-stage {
  counter-increment: pipeline-step;
  padding: var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  position: relative;
}

.form-companion-stage::before {
  content: counter(pipeline-step);
  display: inline-block;
  width: 1.25rem;
  height: 1.25rem;
  text-align: center;
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  color: var(--colour-text-muted);
  margin-block-end: var(--space-2xs);
}

.form-companion-stage-label {
  display: block;
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.form-companion-stage-caption {
  display: block;
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  line-height: 1.4;
}

.form-companion-placeholder {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  font-style: italic;
}

/* ── Selection controls (HaTchi-MaXchi tranche 1) ───────────────────────
   Designed checkbox/radio/switch on NATIVE inputs — appearance:none +
   tokens; keyboard/AT semantics stay the browser's. */

input.checkbox,
input.radio {
  appearance: none;
  width: 1rem;
  height: 1rem;
  margin: 0;
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  cursor: pointer;
  flex-shrink: 0;
  display: inline-grid;
  place-content: center;
  transition: background var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

input.checkbox { border-radius: var(--radius-sm); }
input.radio { border-radius: var(--radius-full); }

input.checkbox:checked,
input.radio:checked {
  background: var(--colour-brand);
  border-color: var(--colour-brand);
}

input.checkbox:checked::before {
  content: "";
  width: 0.625rem;
  height: 0.625rem;
  background: var(--colour-brand-contrast);
  clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 16%, 84% 4%, 38% 71%);
}

input.radio:checked::before {
  content: "";
  width: 0.375rem;
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: var(--colour-brand-contrast);
}

input.switch {
  appearance: none;
  width: 2rem;
  height: 1.125rem;
  margin: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  background: var(--neutral-300);
  cursor: pointer;
  flex-shrink: 0;
  position: relative;
  transition: background var(--duration-base) var(--ease-out);
}

input.switch::before {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 2px;
  translate: 0 -50%;
  width: 0.875rem;
  height: 0.875rem;
  border-radius: var(--radius-full);
  background: white;
  box-shadow: var(--shadow-sm);
  transition: inset-inline-start var(--duration-base) var(--ease-out);
}

input.switch:checked {
  background: var(--colour-brand);
}

input.switch:checked::before {
  inset-inline-start: calc(100% - 0.875rem - 2px);
}

@media (prefers-color-scheme: dark) {
  input.switch { background: var(--neutral-700); }
}

[data-theme="dark"] input.switch { background: var(--neutral-700); }
[data-theme="dark"] input.switch:checked { background: var(--colour-brand); }

@media (prefers-reduced-motion: reduce) {
  input.switch, input.switch::before { transition: none; }
}

}
/* ── components/richtext.css ── */
@layer components {
/*
 * richtext.css — rich-text editor internals (HaTchi-MaXchi).
 *
 * The `.form-richtext` editor surface: toolbar buttons, the
 * `[data-editor]` contenteditable area, and its typographic content
 * (headings, lists, marks). Complements the `.form-richtext` field
 * wrapper base defined in form.css. Migrated from Dazzle into HM
 * (HMC-021, aggressive-mode convergence 2026-07-09).
 */

.form-richtext .richtext-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--dz-space-1, 0.25rem);
  padding: var(--dz-space-1, 0.25rem);
  border: 1px solid var(--dz-color-border, #e5e7eb);
  border-bottom: none;
  border-radius: var(--dz-radius-sm, 0.25rem) var(--dz-radius-sm, 0.25rem) 0 0;
  background: var(--dz-color-surface-subtle, #f9fafb);
}

.form-richtext .richtext-toolbar button[data-cmd] {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  color: var(--dz-color-text, #111827);
  min-width: 2rem;
  min-height: 2rem;
  padding: 0 var(--dz-space-2, 0.5rem);
  border-radius: var(--dz-radius-sm, 0.25rem);
  font-family: var(--dz-font-sans);
  font-size: var(--text-sm, 0.875rem);
  cursor: pointer;
  line-height: 1;
}

.form-richtext .richtext-toolbar button[data-cmd]:hover,
.form-richtext .richtext-toolbar button[data-cmd]:focus-visible {
  background: var(--dz-color-surface-hover, #f3f4f6);
  border-color: var(--dz-color-border, #e5e7eb);
  outline: none;
}

.form-richtext .richtext-toolbar button[data-cmd][aria-pressed="true"],
.form-richtext .richtext-toolbar button[data-cmd].is-active {
  background: var(--dz-color-surface-selected, #dbeafe);
  border-color: var(--dz-color-primary, #3b82f6);
  color: var(--dz-color-primary, #3b82f6);
}

.form-richtext [data-editor] {
  min-height: 8rem;
  padding: var(--dz-space-3, 0.75rem);
  border: 1px solid var(--dz-color-border, #e5e7eb);
  border-radius: 0 0 var(--dz-radius-sm, 0.25rem) var(--dz-radius-sm, 0.25rem);
  background: var(--dz-color-surface, #ffffff);
  color: var(--dz-color-text, #111827);
  font-family: var(--dz-font-sans);
  font-size: var(--text-base, 1rem);
  line-height: 1.5;
  outline: none;
}

.form-richtext [data-editor]:focus-visible,
.form-richtext [data-editor]:focus-within {
  border-color: var(--dz-color-primary, #3b82f6);
  box-shadow: 0 0 0 2px var(--dz-color-primary-soft, rgba(59, 130, 246, 0.2));
}

.form-richtext [data-editor] p {
  margin: 0 0 var(--dz-space-2, 0.5rem) 0;
}

.form-richtext [data-editor] p:last-child {
  margin-bottom: 0;
}

.form-richtext [data-editor] strong { font-weight: 700; }
.form-richtext [data-editor] em { font-style: italic; }
.form-richtext [data-editor] u { text-decoration: underline; }
.form-richtext [data-editor] s { text-decoration: line-through; }

/* Cycle 2: blocks + lists + links + inline code. */
.form-richtext [data-editor] h2 {
  font-size: var(--dz-heading-app-section-title, var(--text-base));
  font-weight: 700;
  margin: var(--dz-space-3, 0.75rem) 0 var(--dz-space-2, 0.5rem) 0;
  line-height: 1.25;
}
.form-richtext [data-editor] h3 {
  font-size: var(--dz-heading-app-subsection-title, var(--text-sm));
  font-weight: 600;
  margin: var(--dz-space-2, 0.5rem) 0;
  line-height: 1.3;
}
.form-richtext [data-editor] ul,
.form-richtext [data-editor] ol {
  margin: 0 0 var(--dz-space-2, 0.5rem) 0;
  padding-left: 1.5rem;
}
.form-richtext [data-editor] li {
  margin: 0;
}
.form-richtext [data-editor] blockquote {
  margin: var(--dz-space-2, 0.5rem) 0;
  padding-left: var(--dz-space-3, 0.75rem);
  border-left: 3px solid var(--dz-color-border, #e5e7eb);
  color: var(--dz-color-text-subtle, #6b7280);
}
.form-richtext [data-editor] code {
  font-family: var(--dz-font-mono, ui-monospace, SFMono-Regular, monospace);
  font-size: 0.9em;
  background: var(--dz-color-surface-subtle, #f3f4f6);
  padding: 0.1em 0.3em;
  border-radius: var(--dz-radius-xs, 0.125rem);
}
.form-richtext [data-editor] a {
  color: var(--dz-color-primary, #3b82f6);
  text-decoration: underline;
}

.form-richtext .richtext-toolbar-sep {
  display: inline-block;
  width: 1px;
  align-self: stretch;
  background: var(--dz-color-border, #e5e7eb);
  margin: 0 var(--dz-space-1, 0.25rem);
}

}
/* ── components/fragment-primitives.css ── */
@layer components {
/*
 * fragment-primitives.css — visual chrome for typed Fragment renderer output.
 *
 * The Fragment renderer (src/dazzle/render/fragment/renderer.py) emits a
 * deterministic class vocabulary derived from the typed primitive types.
 * Each class needs a matching rule here so flipped surfaces (those with
 * `render: fragment` in DSL) render with the same visual treatment as
 * their Jinja-path equivalents.
 *
 * Rules use the framework's design tokens — never hardcoded colours,
 * spaces, or radii — so theme overrides and token-sheet swaps cascade
 * automatically.
 *
 * Plan 4 scope: simple_task.task_list (Surface + Heading + Region.list +
 * Text + Table). Plan 5+ extends as more surfaces flip.
 */

@layer components {

  /* ───────────────────────── Page ────────────────────────────
   * Top-level body class emitted by the Page primitive. The page
   * primitive wraps `<html>`/`<head>`/`<body>` for typed-Fragment
   * chrome rendering; this rule provides the body-level baseline so
   * Page-rendered pages cohabit cleanly with the existing Jinja
   * shells until the chrome migration completes.
   */

  .page {
    margin: 0;
    min-height: 100vh;
    background: var(--colour-bg, var(--colour-surface));
    color: var(--colour-text);
    font-family: var(--font-sans, "Inter", system-ui, sans-serif);
  }

  /* Bare-Page chrome: when the Page body is a single Stack (the
   * shape used by `build_app_403_view` / `build_site_403_view` and
   * their 404/500 cousins — see #1081) the typed view has no
   * AppShell wrapper to provide padding or maximum width. Without
   * these rules the content sits flush against the viewport edge
   * as raw browser-default text. The `:only-child` selector
   * deliberately narrows the rule so a normal Page → AppShell →
   * Stack layout is unaffected — the AppShell intervenes between
   * Page and Stack, so the selector doesn't match. */
  .page > .stack:only-child {
    max-width: 36rem;
    margin: var(--space-xl) auto;
    padding: var(--space-xl) var(--space-lg);
  }

  /* ───────────────────────── AppShell ────────────────────────
   * Multi-slot application layout — sidebar / header / main / footer.
   * Mirrors the legacy app_shell.html structure so the same component
   * CSS in `components/fragments.css` (`.app-shell`, `.app-content`)
   * applies. The Fragment AppShell adds the missing CSS hooks for
   * the typed slots (`.app-sidebar`, `.app-header`, `.app-main`,
   * `.app-footer`) so a Fragment-rendered shell still gets layout
   * structure even when components/fragments.css isn't loaded.
   */

  .app-sidebar {
    /* Composes with the existing sidebar styling when present;
       on its own, provides a minimal column. Width tracks the
       legacy template's lg:w-64 (16rem). */
    flex: 0 0 auto;
    width: 16rem;
  }

  .app-header {
    /* Top bar; vertical rhythm matches the topbar component. */
    display: flex;
    align-items: center;
    padding: var(--space-sm) var(--space-md);
    border-block-end: 1px solid var(--colour-border);
  }

  .app-main {
    /* The main content slot — flex-grows to fill the column. */
    flex: 1 1 auto;
    padding: var(--space-md);
  }

  .app-footer {
    padding: var(--space-sm) var(--space-md);
    border-block-start: 1px solid var(--colour-border);
  }

  /* ───────────────────────── ErrorPage ───────────────────────
   * Standalone error page (404, 500, auth fallback).
   * Centred column with large status code, message, optional
   * home link. Used inside Page.body for routes without AppShell.
   */

  .error-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-md);
    min-height: 60vh;
    padding: var(--space-xl) var(--space-md);
    text-align: center;
  }

  .error-page__code {
    margin: 0;
    font-size: var(--text-4xl, 3rem);
    font-weight: var(--weight-bold);
    color: var(--colour-text-muted);
    letter-spacing: -0.02em;
  }

  .error-page__message {
    margin: 0;
    font-size: var(--text-lg);
    color: var(--colour-text);
    max-width: 40rem;
  }

  .error-page__action {
    margin-block-start: var(--space-md);
    padding: var(--space-sm) var(--space-lg);
    color: var(--colour-brand-contrast);
    background: var(--colour-brand);
    border-radius: var(--radius-sm);
    text-decoration: none;
  }

  /* ───────────────────────── Topbar ──────────────────────────
   * Application top bar — leading / title / trailing layout.
   * Class names match the legacy template's `topbar` /
   * `topbar-leading` / `topbar-title` / `topbar-trailing`
   * so existing component CSS applies unchanged.
   */

  .topbar {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-sm) var(--space-md);
    border-block-end: 1px solid var(--colour-border);
  }

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

  .topbar-title {
    flex: 1 1 auto;
    min-width: 0;  /* allow truncation if title overflows */
  }

  .topbar-title-text {
    font-weight: var(--weight-semibold);
    color: var(--colour-text);
  }

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

  /* ───────────────────────── Sidebar / Nav ───────────────────
   * Navigation primitives — Sidebar, NavGroup, NavItem.
   * Mirrors the legacy template's nav-link conventions; in particular
   * `[aria-current="page"]` styling so active state has one source of
   * truth between the visual and accessibility layers.
   */

  .sidebar {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding: var(--space-md);
    height: 100%;
    background: var(--colour-surface);
    border-inline-end: 1px solid var(--colour-border);
  }

  .sidebar__header {
    padding-block-end: var(--space-sm);
    border-block-end: 1px solid var(--colour-border);
  }

  .sidebar__items,
  .nav-group__items {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs, 2px);
  }

  .nav-item {
    display: block;
  }

  .nav-link {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    color: var(--colour-text);
    text-decoration: none;
    border-radius: var(--radius-sm);
  }

  .nav-link:hover {
    background: light-dark(rgba(0, 0, 0, 0.04), rgba(255, 255, 255, 0.04));
  }

  .nav-link[aria-current="page"] {
    /* Active state — visible distinction without overriding the
       hover treatment. */
    background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08));
    font-weight: var(--weight-medium);
  }

  .nav-group {
    /* `<details>` element — no list-style on summary in modern browsers. */
  }

  .nav-group__header {
    cursor: pointer;
    padding: var(--space-xs) var(--space-sm);
    color: var(--colour-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    list-style: none;  /* hide the default <details> marker */
  }

  .nav-group__header::-webkit-details-marker {
    display: none;
  }

  /* ───────────────────────── Surface ─────────────────────────
   * Top-level rendered surface — the typed Fragment equivalent of
   * a workspace's main page or a list/detail page wrapper.
   * Slots: header (titles), body (content), footer (optional).
   */

  .surface {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    padding: var(--space-md);
    background: var(--colour-surface);
    color: var(--colour-text);
  }

  .surface__header {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding-block-end: var(--space-sm);
    border-block-end: 1px solid var(--colour-border);
  }

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

  .surface__footer {
    padding-block-start: var(--space-sm);
    border-block-start: 1px solid var(--colour-border);
  }

  /* ───────────────────────── Heading ─────────────────────────
   * Level-parameterised heading. Level 1 is the page-title weight;
   * 2-3 are section headings; 4-6 are sub-headings used inside
   * regions and panels. Sizing pulls from the same scale as the
   * Jinja-path heading variants.
   */

  .heading {
    margin: 0;
    color: var(--colour-text);
    font-weight: var(--weight-semibold);
    line-height: 1.25;
  }

  .heading--level-1 { font-size: var(--text-xl); }
  .heading--level-2 { font-size: var(--text-lg); }
  .heading--level-3 { font-size: var(--text-base); }
  .heading--level-4 {
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
  }
  .heading--level-5 {
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--colour-text-muted);
  }
  .heading--level-6 {
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--colour-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }

  /* ───────────────── Layout primitives ──────────────────────
   * Stack (vertical), Row (horizontal), Grid (n-column), Split
   * (two-panel). Emitted by src/dazzle/render/fragment/renderer
   * /_render_layout.py as `<div class="{primitive} {primitive}--
   * gap-{none|sm|md|lg}">…</div>`. Without these base rules the
   * children flow inline with zero spacing — the 403/404/500 marketing
   * pages all rendered "Go to DashboardGo Home" run-together until
   * #1079 added these. Split has its own grid in site-sections.css
   * (different naming scheme) and is not styled here.
   */

  /* Layouts L2: the Stack/Row layout rules retired — the Stack primitive
   * emits the HM stack Hyperpart contract (base rule + data-gap scale
   * in layout.css) and Row emits the cluster Hyperpart. .stack's base
   * rule moved to layout.css with the rest of its Hyperpart. */

  .grid {
    display: grid;
    gap: var(--space-md);
  }

  /* Column counts: Grid.columns is an int in [1, 12]. Generated
   * exhaustively rather than via `repeat(auto-fit, ...)` so the
   * column count is exactly what the primitive declares. */
  .grid--columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
  .grid--columns-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid--columns-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .grid--columns-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .grid--columns-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .grid--columns-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .grid--columns-7 { grid-template-columns: repeat(7, minmax(0, 1fr)); }
  .grid--columns-8 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
  .grid--columns-9 { grid-template-columns: repeat(9, minmax(0, 1fr)); }
  .grid--columns-10 { grid-template-columns: repeat(10, minmax(0, 1fr)); }
  .grid--columns-11 { grid-template-columns: repeat(11, minmax(0, 1fr)); }
  .grid--columns-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }

  /* ───────────────────────── Region ──────────────────────────
   * A semantic content region inside a Surface. The `kind`
   * modifier drives the layout: list (table-shaped), detail
   * (definition-list-shaped), form (FormStack), dashboard
   * (Grid), kanban, calendar, report.
   *
   * Plan 4 scope: list only. Other kinds get base treatment but
   * no kind-specific styling until later plans.
   */

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

  .region--kind-list {
    /* List-mode region wraps a Table primitive. The table itself
       inherits its core styling from table.css; this rule provides
       the list-context spacing and any list-specific cascade
       (e.g. table inside .region--kind-list gets the same
       row-hover treatment as the Jinja .list-table path). */
    overflow-x: auto;
  }

  .region--kind-list > .table {
    width: 100%;
    border-collapse: collapse;
    background: var(--colour-surface);
  }

  .region--kind-list > .table thead > tr {
    border-block-end: 1px solid var(--colour-border);
    text-align: left;
  }

  .region--kind-list > .table th {
    padding: var(--space-sm) var(--space-md);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--colour-text-muted);
  }

  .region--kind-list > .table > tbody > tr {
    border-block-end: 1px solid var(--colour-border);
    transition: background-color 0.12s ease;
  }

  .region--kind-list > .table > tbody > tr:last-child {
    border-block-end: none;
  }

  .region--kind-list > .table > tbody > tr:hover {
    /* No --colour-surface-hover token exists yet; light/dark fallback
       picks a subtle tint that works in both modes. Promote to a token
       when more surfaces need it. */
    background: light-dark(rgba(0, 0, 0, 0.04), rgba(255, 255, 255, 0.04));
  }

  .region--kind-list > .table td {
    padding: var(--space-sm) var(--space-md);
    color: var(--colour-text);
  }

  /* Detail kind — definition-list-shaped layout. Each child Row
     renders as label (Heading level 4) + value (Text). The Stack
     wrapper provides the row separation; this rule provides the
     intra-row layout (label-column + value-column). */

  .region--kind-detail {
    overflow-x: auto;
  }

  /* L2: the contextual Row-as-grid rules retired — the detail field
   * layout is the DetailGrid primitive's <dl> (detail-region-grid),
   * not a hijacked Row. */

  /* ───────────────────────── Related ─────────────────────────
   * Related-group region — appended to detail surfaces, one per
   * related entity group (e.g. user_detail showing tasks +
   * comments). Skeleton placeholder lives here today; htmx-loaded
   * rows arrive in a later plan.
   */

  .region--kind-related {
    margin-block-start: var(--space-lg);
    padding-block-start: var(--space-md);
    border-block-start: 1px solid var(--colour-border);
  }

  .region--kind-related .heading--level-2 {
    margin-block-end: var(--space-sm);
  }

  /* ───────────────────────── Form ────────────────────────────
   * Form-mode region — vertical stack of fields with label-above-input
   * layout. Used by CREATE and EDIT surfaces.
   */

  .region--kind-form {
    /* Region container; FormStack inside provides actual layout. */
  }

  .form-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    max-width: 40rem;
  }

  .field {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
  }

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

  .field__input {
    padding: var(--space-sm) var(--space-md);
    font-size: var(--text-base);
    color: var(--colour-text);
    background: var(--colour-surface);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-sm);
  }

  .field__input:focus {
    outline: 2px solid var(--colour-accent);
    outline-offset: 1px;
  }

  /* NOTE: do NOT use `.combobox` here. That class is the combobox
   * Hyperpart enhancement root (components/combobox.css). A prior
   * fragment "labeled select stack" under the same name made the
   * Hyperpart root `display:flex; flex-direction:column; gap:…` and
   * fought progressive enhancement layout. Unused BEM removed 2026-07. */

  /* Plan 14: RefPicker — REF field selector. Labeled select stack;
     the data-ref-api attribute drives client-side option population
     via dz.filterRefSelect. */
  .ref-picker {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
  }

  .ref-picker__label {
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--colour-text);
  }

  .ref-picker__select {
    padding: var(--space-sm) var(--space-md);
    font-size: var(--text-base);
    color: var(--colour-text);
    background: var(--colour-surface);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-sm);
  }

  .submit {
    align-self: flex-start;
    padding: var(--space-sm) var(--space-lg);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    color: var(--colour-brand-contrast);
    background: var(--colour-brand);
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
  }

  .submit--variant-secondary {
    background: var(--colour-surface);
    color: var(--colour-text);
    border: 1px solid var(--colour-border);
  }

  .submit--variant-danger {
    /* fixed step: fill carries white text; the semantic token flips light in dark */
    background: var(--danger-600);
    color: var(--colour-brand-contrast);
  }

  /* ───────────────────────── Text ────────────────────────────
   * Inline text primitive — used inside cells, in EmptyState
   * descriptions, badges, and as a bare leaf primitive. The tone
   * modifier controls colour role (default/muted/danger/success/
   * warning); other tones map to existing token roles.
   */

  .text {
    color: var(--colour-text);
  }

  .text--tone-default { color: var(--colour-text); }
  .text--tone-muted   { color: var(--colour-text-muted); }
  .text--tone-danger  { color: var(--colour-danger); }
  .text--tone-success { color: var(--colour-success); }
  .text--tone-warning { color: var(--colour-warning); }

}  /* @layer components */

/* ── Icon primitive + inline-SVG icons (HaTchi-MaXchi TASTE-6) ──────────
   Server-rendered Lucide SVGs fill their fixed-size wrapper and inherit
   currentColor; sizes sit on the type scale. */
/* Canonical icon contract — see the Icon Hyperpart page. Sized in `em` so an
   icon scales with its context's font-size; the size scale pins absolute sizes. */
.icon,
.task-inbox-item-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  vertical-align: -0.125em;
  width: 1em;
  height: 1em;
  color: inherit;
  stroke: currentColor;
  /* Lucide stroke defaults must live on the REFERENCING element: `<use>`
     shadow content inherits these from here, NOT from the sprite sheet's
     outer <svg> — without them, sprite icons render at the browser default
     stroke-width:1 / butt caps (thin, squared). */
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}
.icon-solid { fill: currentColor; stroke: none; }

.task-inbox-item-icon {
  width: 1.25rem;
  height: 1.25rem;
  color: var(--colour-text-muted);
}

.icon--size-xs { width: 0.75rem; height: 0.75rem; }
.icon--size-sm { width: 1rem; height: 1rem; }
.icon--size-md { width: 1.25rem; height: 1.25rem; }
.icon--size-lg { width: 1.5rem; height: 1.5rem; }
.icon--size-xl { width: 2rem; height: 2rem; }

.icon svg,
.action-card-icon svg,
.status-list-icon svg,
.task-inbox-item-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Icon gallery demo — the size scale in a row (currentColor from the accent). */
.icon-demo {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  color: var(--colour-brand);
}

/* ── Nav icons (HaTchi-MaXchi Phase 3, TASTE-6) ─────────────────────────
   Every sidebar item carries a registry SVG — authored or inferred. Muted
   at rest; inherits the link colour on hover/active. */
.nav-link__icon,
.nav-group__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  color: var(--colour-text-muted);
}

.nav-link:hover .nav-link__icon,
.nav-link[aria-current="page"] .nav-link__icon {
  color: currentColor;
}

.nav-link__icon svg,
.nav-group__icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

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

/* Empty state — the "no data" primitive. Consolidated here (the single home)
 * from the legacy base/design-system.css: the base container + `__` parts now
 * live together. The old broad `.empty-state svg { 3rem }` fossil was
 * dropped — it fought `.empty-state__icon` on specificity (0,1,1 vs 0,1,0),
 * so a bare-svg icon rendered 3rem while a wrapped `__icon svg` rendered 2rem.
 * The `__icon` wrapper (2rem) is now the one icon contract. */
.empty-state {
  text-align: center;
  padding: 3rem 1.5rem;
  color: var(--colour-text-muted);
  /* Column-centred layout folded in from the legacy Dazzle `dazzle-layer.css`
   * `.empty-state` (HMC-003c). Same selector, disjoint properties → the
   * served computed style is the byte-identical union of the two former rules. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 12rem;
}

/* Empty-state icon (TASTE-8): quiet registry glyph above the message. */
.empty-state__title {
  color: var(--colour-text); /* tranche-1 nit: was too dim on dark surfaces */
  /* weight + spacing folded from legacy `.empty-state h3` (HMC-003c). Every
   * emitted title is `<h3 class="empty-state__title">`, so the element→BEM
   * move is computed-identical (the old 0,1,1 `h3` rule out-specified this 0,1,0
   * rule only on `color`, which was the same value). */
  font-weight: 600;
  margin-bottom: 0.25rem;
}

/* Description line — folded from legacy `.empty-state p` (HMC-003c). Every
 * emitted description is `<p class="empty-state__description">`. */
.empty-state__description {
  color: var(--colour-text-muted);
  font-size: 0.875rem;
}

.empty-state__icon {
  display: inline-flex;
  width: 2rem;
  height: 2rem;
  color: var(--colour-text-muted);
  margin-block-end: var(--space-sm);
}

.empty-state__icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* ── Error pages (403/404/500) — HaTchi-MaXchi #1536 ────────────────────
   The app-shell-lite error views render Page > Stack > Card(EmptyState).
   Centre that lone card in the viewport; everything else on these pages
   inherits the empty-state styling. */
.page > .stack:has(> .card .empty-state) {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--space-2xl);
  background: var(--colour-bg);
}

.page > .stack:has(> .card .empty-state) > .card {
  width: 100%;
  max-width: 28rem;
  text-align: center;
  padding: var(--space-2xl);
}

.page > .stack:has(> .card .empty-state) .empty-state__action a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2.25rem;
  padding-inline: var(--space-lg);
  border-radius: var(--radius-md);
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  font-weight: var(--weight-medium);
  text-decoration: none;
  box-shadow: var(--shadow-sm);
}

.page > .stack:has(> .card .empty-state) .empty-state__action a:hover {
  background: var(--brand-700);
}

}
/* ── components/hm-core.css ── */
@layer components {
/*
 * hm-core — HaTchi-MaXchi tranche 2A components (spec:
 * hatchi-maxchi-standalone). Accumulates new standalone-system
 * components until the extraction restructures per-component.
 *
 * Components: progress, avatar, breadcrumb, toggle-group,
 * popover, kbd.
 */

/* ── progress ─────────────────────────────────────────────────────
 * <div class="progress" role="progressbar" aria-valuenow="60"
 *      aria-valuemin="0" aria-valuemax="100">
 *   <div class="progress__bar" style="width:60%"></div>
 * </div>
 */

.progress {
  width: 100%;
  height: 0.5rem;
  border-radius: var(--radius-full);
  background: color-mix(in oklab, var(--colour-brand) 12%, var(--colour-bg));
  overflow: hidden;
}

.progress__bar {
  /* Dynamic value via a custom property, not an inline width: the markup
     sets `--progress-value: 62%` and CSS owns the property mapping. */
  width: var(--progress-value, 0%);
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--colour-brand);
  transition: width var(--duration-slow) var(--ease-out);
}

.progress[data-tone="success"] .progress__bar { background: var(--colour-success); }
.progress[data-tone="warning"] .progress__bar { background: var(--colour-warning); }
.progress[data-tone="destructive"] .progress__bar { background: var(--colour-danger); }

@media (prefers-reduced-motion: reduce) {
  .progress__bar { transition: none; }
}

/* ── avatar ───────────────────────────────────────────────────────
 * <span class="avatar" data-size="md">AB</span>
 * <span class="avatar"><img src="…" alt=""></span>
 * Initials inherit a deterministic tone via data-hue (0-359) set by
 * the emitter (hash of the name), falling back to brand.
 */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-full);
  /* The semantic wash, not a hand-rolled mix — the 15% mix put initials
     at 4.29:1 (axe); the wash token is calibrated for 4.5:1 tone text. */
  background: var(--colour-brand-soft);
  color: var(--colour-brand-text);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.02em;
  overflow: hidden;
  flex-shrink: 0;
  user-select: none;
}

.avatar[data-size="sm"] { width: 1.5rem; height: 1.5rem; font-size: 0.625rem; }
.avatar[data-size="lg"] { width: 2.75rem; height: 2.75rem; font-size: var(--text-sm); }

.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.avatar-group {
  display: inline-flex;
}

.avatar-group .avatar {
  border: 2px solid var(--colour-surface);
}

.avatar-group .avatar + .avatar {
  margin-inline-start: -0.5rem;
}

/* Deterministic initials wash when emitter sets --dz-avatar-hue (0–359)
   via inline style (and data-hue for tests/DOM inspection). */
.avatar[data-hue] {
  background: hsl(var(--dz-avatar-hue, 210) 45% 42% / 0.18);
  color: hsl(var(--dz-avatar-hue, 210) 40% 28%);
}

/* ── user-chip ────────────────────────────────────────────────────
 * Person-ref cell: avatar + name. Emitted by ``render_user_chip_html``.
 * <span class="user-chip"><span class="avatar" data-size="sm">…</span>
 *   <span class="user-chip__name">Ada Lovelace</span></span>
 */
.user-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
  max-width: 100%;
  vertical-align: middle;
  line-height: 1.2;
}

.user-chip__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
  color: var(--colour-text);
}

/* Workspace region person-ref links wrap the chip (keep chip layout, no underline). */
a.user-chip-link {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  color: inherit;
  max-width: 100%;
}
a.user-chip-link:hover .user-chip__name {
  text-decoration: underline;
  color: var(--colour-primary, var(--colour-text));
}

/* ── breadcrumb ───────────────────────────────────────────────────
 * <nav class="breadcrumb" aria-label="Breadcrumb"><ol>
 *   <li><a href="/app">Home</a></li>
 *   <li><a href="/app/invoices">Invoices</a></li>
 *   <li aria-current="page">INV-0042</li>
 * </ol></nav>
 * Separator is CSS-generated (chevron) — markup stays list-clean.
 */

.breadcrumb ol {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--text-sm);
}

.breadcrumb li {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--colour-text-muted);
}

.breadcrumb li + li::before {
  content: "";
  width: 0.875rem;
  height: 0.875rem;
  background: currentColor;
  opacity: 0.5;
  mask: center / contain no-repeat
    url('data:image/svg+xml;utf8,<svg 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"><path d="m9 18 6-6-6-6"/></svg>');
}

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

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

.breadcrumb [aria-current="page"] {
  color: var(--colour-text);
  font-weight: var(--weight-medium);
}

/* ── toggle-group ─────────────────────────────────────────────────
 * The hypermedia answer: native radios, styled as a segmented control.
 * <fieldset class="toggle-group" role="radiogroup">
 *   <label><input type="radio" name="view" checked><span>List</span></label>
 *   <label><input type="radio" name="view"><span>Board</span></label>
 * </fieldset>
 */

.toggle-group {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: color-mix(in oklab, var(--colour-text) 4%, var(--colour-bg));
  margin: 0;
}

.toggle-group label {
  position: relative;
  cursor: pointer;
}

.toggle-group input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.toggle-group span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.25rem var(--space-md);
  border-radius: calc(var(--radius-md) - 2px);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.toggle-group input:checked + span {
  background: var(--colour-surface);
  color: var(--colour-text);
  box-shadow: var(--shadow-sm);
}

.toggle-group input:focus-visible + span {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

/* HYPERPART: popover */
/* ── popover — details-based generic panel ────────────────────────
 * <details class="popover"><summary>…trigger…</summary>
 *   <div class="popover__panel">…free content…</div></details>
 * data-align="start|end" on the root positions the panel.
 * Light-dismiss: controllers/details-light-dismiss.js (overlay-light-dismiss).
 */

.popover {
  position: relative;
  display: inline-block;
}

.popover > summary {
  list-style: none;
  cursor: pointer;
}

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

.popover__panel {
  position: absolute;
  inset-block-start: calc(100% + 0.375rem);
  inset-inline-start: 0;
  min-width: 14rem;
  padding: var(--space-lg);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 40;
}

.popover[data-align="end"] .popover__panel {
  inset-inline-start: auto;
  inset-inline-end: 0;
}

/* ── kbd ──────────────────────────────────────────────────────────
 * <kbd class="kbd">⌘K</kbd> — shortcut chips (command palette etc.)
 *
 * Stem shortcut-hint-chrome:
 *   - Visually secondary: mono, small, muted keycap (this block).
 *   - Spatially secondary: never glue to a primary label — two layout roles:
 *       adjacent → parent flex gap (see .button:has(.kbd))
 *       trailing → margin-inline-start: auto (see .command__item .kbd)
 */

.kbd {
  display: inline-flex;
  align-items: center;
  padding: 0.125rem 0.375rem;
  border: 1px solid var(--colour-border);
  border-block-end-width: 2px;
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--colour-text-muted);
}

/* Adjacent role: label + keycap on a compact control (opener buttons, etc.) */
.button:has(> .kbd),
button:has(> .kbd) {
  gap: var(--space-sm);
}

/* ── command — the hx-get command palette (htmx4 flagship) ─────────
 * <dialog class="command">
 *   <input class="command__input" placeholder="Search…" name="q"
 *          hx-get="/app/command" hx-trigger="input changed delay:150ms"
 *          hx-target="next .command__results">
 *   <div class="command__results">…server-rendered items…</div>
 * </dialog>
 * Server renders .command__item entries (icon + label + kbd hint).
 * command.js: ⌘K/Ctrl-K opens, Esc closes, arrows move [aria-selected].
 */

/* HYPERPART: command */
dialog.command {
  width: min(34rem, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  background: var(--colour-surface);
  box-shadow: var(--shadow-lg);
  /* top-anchored, horizontally centered (native dialog centers by
     default; override only the block axis) */
  margin-block: 12vh auto;
  margin-inline: auto;
}

dialog.command::backdrop {
  background: oklch(13% 0.028 261.69 / 0.4);
  backdrop-filter: blur(2px);
}

/* input + close share a flex row (the bar); results follow. Flex, not
   absolute positioning — abspos against a modal <dialog> is fragile on
   Safari/iPadOS (the button landed at its static position there, so taps
   on the expected top-right corner missed it). */
.command__bar {
  display: flex;
  align-items: center;
  border-block-end: 1px solid var(--colour-border);
}

.command__input {
  flex: 1;
  min-width: 0;
  padding: var(--space-lg);
  border: none;
  background: transparent;
  color: var(--colour-text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  outline: none;
  box-sizing: border-box;
}

/* Always-visible dismiss affordance — essential on touch (no Esc key).
   A native <button> so its tap fires reliably, incl. on iOS Safari. */
.command__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  margin-inline-end: var(--space-sm);
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--colour-text-muted);
  cursor: pointer;
}
.command__close:hover {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}
.command__close:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}
.command__close svg {
  width: 1rem;
  height: 1rem;
}

.command__results {
  max-height: 20rem;
  overflow-y: auto;
  padding: var(--space-xs);
}

.command__group {
  padding: var(--space-sm) var(--space-md) var(--space-xs);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--colour-text-muted);
}

.command__item {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--colour-text);
  font-size: var(--text-sm);
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}

.command__item:hover,
.command__item[aria-selected="true"] {
  background: color-mix(in oklab, var(--colour-brand) 8%, var(--colour-surface));
}

/* Trailing role: label leads; shortcut at the end of the row (cmdk/Linear) */
.command__item .kbd {
  margin-inline-start: auto;
}

.command__empty {
  padding: var(--space-xl);
  text-align: center;
  color: var(--colour-text-muted);
  font-size: var(--text-sm);
}

/* ── hover-card ────────────────────────────────────────────────────
 * Full styles live in components/hover-card.css (HMC-035).
 * Anatomy: .hover-card > .hover-card__trigger + .hover-card__content
 * (__panel is a legacy alias of __content). Do not re-define here.
 */

/* ── scroll-area ──────────────────────────────────────────────────
 * Thin, token-coloured scrollbars for constrained panels.
 */

.scroll-area {
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklab, var(--colour-text) 25%, transparent) transparent;
}

.scroll-area::-webkit-scrollbar { width: 8px; height: 8px; }
.scroll-area::-webkit-scrollbar-thumb {
  background: color-mix(in oklab, var(--colour-text) 25%, transparent);
  border-radius: var(--radius-full);
}
.scroll-area::-webkit-scrollbar-track { background: transparent; }

/* HYPERPART: toolbar — inline composite (button + toggle-group + menu). */
.toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
  padding: var(--space-sm);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
}

/* HYPERPART: master-detail — exchange composite (list → hx-get detail card). */
.master-detail {
  display: grid;
  grid-template-columns: 14rem 1fr;
  gap: 0;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  min-height: 12rem;
  background: var(--colour-surface);
}
/* Workspace dual_pane pair: wider list column for tabular rows. */
.master-detail-pair .master-detail {
  grid-template-columns: minmax(18rem, 2fr) minmax(20rem, 3fr);
  min-height: 22rem;
  /* Outer card-wrapper already participates in the dashboard grid. */
  border-radius: var(--radius-lg);
}
.master-detail__list {
  list-style: none;
  margin: 0;
  padding: var(--space-xs);
  border-inline-end: 1px solid var(--colour-border);
  background: var(--colour-surface);
  min-width: 0;
  overflow: auto;
  max-height: min(70vh, 40rem);
}
.master-detail-pair .master-detail__list {
  padding: 0;
}
/* Nested dashboard card chrome: flatten so the pair reads as one unit. */
.master-detail-pair .master-detail__list .card {
  border: none;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
}
.master-detail-pair .master-detail__list .card-header {
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--colour-border);
}
.master-detail-pair .master-detail__list .card-body {
  padding: 0;
}
/* Gallery / link items (anchor or button). */
a.master-detail__item,
button.master-detail__item {
  display: block;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  color: var(--colour-text);
  text-decoration: none;
  font-size: var(--text-sm);
  border: none;
  background: transparent;
  width: 100%;
  text-align: start;
  cursor: pointer;
}
a.master-detail__item:hover,
button.master-detail__item:hover {
  background: var(--colour-bg);
}
a.master-detail__item[aria-current="true"],
button.master-detail__item[aria-current="true"] {
  background: var(--colour-brand-soft);
  color: var(--colour-brand-text);
  font-weight: var(--weight-medium);
}
/* Workspace list table rows (tr) — keep table-row display. */
tr.master-detail__item {
  cursor: pointer;
}
tr.master-detail__item:hover > td {
  background: var(--colour-bg);
}
tr.master-detail__item[aria-current="true"] > td {
  background: var(--colour-brand-soft);
  color: var(--colour-brand-text);
  font-weight: var(--weight-medium);
}
.master-detail__detail {
  padding: var(--space-md);
  min-width: 0;
  overflow: auto;
  max-height: min(70vh, 40rem);
}
.master-detail-pair .master-detail__detail {
  padding: var(--space-md) var(--space-lg);
}
.master-detail__empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-xs);
  min-height: 10rem;
  color: var(--colour-text-muted, var(--colour-text));
  opacity: 0.85;
}
.master-detail__empty .empty-state-title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
}
.master-detail__empty .empty-state-description {
  margin: 0;
  font-size: var(--text-sm);
}

}
/* ── components/htmx-states.css ── */
@layer components {
/*
 * htmx-states.css — HTMX runtime-class styling.
 *
 * HTMX applies these classes to elements during request lifecycles:
 *
 *   .htmx-request          — element has an in-flight request
 *   .htmx-indicator        — visibility-toggled spinner / loader
 *   .htmx-swapping         — element is about to be swapped out (300ms)
 *   .htmx-settling         — element was just swapped in (settle phase)
 *   .htmx-added            — newly-inserted element (highlight transition)
 *
 * Migrated from dz.css during the v0.62 CSS refactor. All values
 * route through design tokens. Wrapped in @layer components.
 */

/* ── Row swap animations ─────────────────────────────────────────── */
/* Row fades out during delete swap (swap:300ms on hx-swap). */

tr.htmx-swapping {
  opacity: 0;
  transform: translateX(var(--space-lg));
  transition:
    opacity var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out);
}

/* Row highlights briefly after creation or update. */

tr.htmx-added,
tr.htmx-settling {
  animation: row-highlight 600ms var(--ease-out);
}

@keyframes row-highlight {
  from {
    background-color: var(--colour-success-soft);
  }
  to {
    background-color: transparent;
  }
}

/* ── Submit button feedback ─────────────────────────────────────── */
/* Form button shows an in-flight state on submit. */

button.htmx-request,
.button.htmx-request {
  pointer-events: none;
  opacity: 0.7;
}

/* ── Inline loading-spinner two-tone ────────────────────────────── */
/* The inline SVG spinners the Dazzle emitters render (search-select,
 * table-loading, tab-loading, workspace-save-busy) are the classic
 * two-tone loader: a dim background track ring + a brighter rotating
 * head arc. These carry the canonical opacities HM owns, replacing the
 * Tailwind `opacity-25` / `opacity-75` utilities the emitters used
 * before Tailwind was unbundled (markup-reservoir drain, HMC-002). */

.spinner-track {
  opacity: 0.25;
}

.spinner-head {
  opacity: 0.75;
}

/* ── Indicator visibility ───────────────────────────────────────── */
/* Override HTMX's built-in opacity-based hiding with display:none —
 * hidden indicators must not occupy layout space (opacity:0 still
 * reserves vertical space for the element). */

.htmx-indicator {
  display: none;
}

/* Descendant case: indicator is a child of the requesting element. */
.htmx-request .htmx-indicator {
  display: inline-block;
}

/* Same-element case: hx-indicator="#id" adds htmx-request to the
 * indicator itself rather than its parent. */
.htmx-request.htmx-indicator {
  display: inline-block;
}

/* ── Reduced-motion accommodation ───────────────────────────────── */
/* Skip the row-highlight + slide-out animations when the user has
 * set prefers-reduced-motion. The reset already does the global
 * disable; this override is here so the comment explains *why* —
 * row highlights are pure decoration for status feedback. */

@media (prefers-reduced-motion: reduce) {
  tr.htmx-swapping {
    transform: none;
  }

  tr.htmx-added,
  tr.htmx-settling {
    animation: none;
  }
}

}
/* ── components/transitions.css ── */
@layer components {
/*
 * transitions.css — page-level transitions & overlays (HaTchi-MaXchi).
 *
 * Migrated from Dazzle dz.css (HMC-006b part 2, 2026-07-09): toast enter/leave,
 * cross-document view-transitions for fragment navigation, the native <dialog>
 * backdrop + open animation, and the resize-drag body helpers. Values route
 * through HM tokens where one exists (--ease-*, --duration-*); a few remain
 * literal (specific durations / the scrim / the translate offset).
 *
 * HMC-011 FIX: the view-transition fade keyframes are named `view-fade-*`
 * (NOT `fade-*`) so they no longer collide with HM's own fade-only
 * `@keyframes fade-in/out` in tokens.css — before this, dz.css's unlayered
 * slide+fade `fade-*` shadowed HM's, so `--animation-fade-*` consumers
 * rendered the wrong (slide) animation. View-transitions keep their slide+fade.
 */

/* ── Toast notifications ─────────────────────────────────────────────── */
.toast-enter {
  opacity: 0;
  transform: translateX(1rem);
  transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
}

.toast-leave {
  opacity: 0;
  transform: translateX(1rem);
  transition:
    opacity var(--duration-base) var(--ease-in),
    transform var(--duration-base) var(--ease-in);
}

#toast .alert {
  transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
}

/* ── View transitions for fragment navigation ────────────────────────── */
@view-transition {
  navigation: auto;
}

#main-content {
  view-transition-name: main-content;
}

::view-transition-old(main-content) {
  animation: view-fade-out var(--duration-base) var(--ease-in) forwards;
}

::view-transition-new(main-content) {
  animation: view-fade-in 200ms var(--ease-out);
}

@keyframes view-fade-out {
  to {
    opacity: 0;
    transform: translateY(4px);
  }
}

@keyframes view-fade-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

/* ── Native dialog backdrop + open animation ─────────────────────────── */
dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(2px);
}

dialog[open] {
  animation: dialog-in 200ms var(--ease-out);
}

@keyframes dialog-in {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* ── Resize-drag body helpers ────────────────────────────────────────── */
body.select-none {
  user-select: none;
  -webkit-user-select: none;
}

body.cursor-col-resize,
body.cursor-col-resize * {
  cursor: col-resize !important;
}

/* ── Reduced motion ──────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(main-content),
  ::view-transition-new(main-content) {
    animation: none;
  }
}

}
/* ── components/pagination.css ── */
@layer components {
/* HYPERPART: pagination */
/*
 * pagination — the footer beneath a data table: a summary on one side,
 * page-number buttons on the other. Each button is an htmx affordance
 * (hx-get a page → swap the list body), so this is an Exchange component,
 * not a client widget. Numbered buttons with an ellipsis for long ranges;
 * the current page is marked `.is-current` + `aria-current="page"`.
 */

.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-inline: 0.25rem;
  padding-block: var(--space-sm);
  font-size: var(--text-sm);
}

.pagination-summary {
  color: var(--colour-text-muted);
}

.pagination-pages {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.pagination-page {
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.pagination-page:hover {
  background: var(--colour-bg);
}

.pagination-page:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

.pagination-page.is-current {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
}

/* prev / next arrows are just page buttons; disabled at the range ends */
.pagination-page:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.pagination-page:disabled:hover {
  background: transparent;
}

/* #984: ellipsis marker between non-contiguous page-button ranges. Sized to
 * match the page-button height so the row stays flat, but non-interactive
 * (no hover, no cursor). aria-hidden in markup — screen readers skip it; the
 * surrounding buttons carry the page numbers. */
.pagination-ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding-inline: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  user-select: none;
}

}
/* ── components/skeleton.css ── */
@layer components {
/* HYPERPART: skeleton */
/*
 * skeleton — loading placeholder. A muted block with a low-contrast
 * sheen sweep derived from the request lifecycle (taste principle 9,
 * TASTE-9): drop it into a swap target while `.htmx-request` is in flight,
 * so the loading state IS the design language, not a spinner bolted on.
 *
 * Shapes via data-shape: `text` (a line of copy), `circle` (avatar),
 * `block` (media / card body). Purely decorative — hide the placeholder
 * from AT (aria-hidden on the region) and announce "loading" on the live
 * region that owns the swap, not here.
 */

.skeleton {
  /* internal tokens (double-dash → never reprefixed, kept out of the API) */
  --dz-skeleton-base: var(--colour-surface-muted);
  --dz-skeleton-sheen: color-mix(in oklab, var(--colour-text) 8%, var(--colour-surface-muted));
  display: block;
  width: 100%;
  border-radius: var(--radius-sm);
  background-color: var(--dz-skeleton-base);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    var(--dz-skeleton-sheen) 50%,
    transparent 100%
  );
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: skeleton-sweep 1.4s var(--ease-out, ease) infinite;
}

.skeleton[data-shape="text"] {
  height: 0.75rem;
  border-radius: var(--radius-full);
}

.skeleton[data-shape="text"]:last-child {
  width: 65%;
}

.skeleton[data-shape="circle"] {
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  border-radius: var(--radius-full);
}

.skeleton[data-shape="block"] {
  min-height: 4rem;
  border-radius: var(--radius-md);
}

/* A stack of text-line skeletons — the most common placeholder shape
   (a paragraph loading). The last line is shortened by the [data-shape]
   rule above for a natural ragged edge. */
.skeleton-lines {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

@keyframes skeleton-sweep {
  from { background-position: 150% 0; }
  to { background-position: -150% 0; }
}

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

}
/* ── components/switch.css ── */
@layer components {
/* HYPERPART: switch */
/*
 * On/off control — progressive enhancement over a native checkbox.
 * Root: label.switch > input[type=checkbox][data-switch] + span.switch__track
 * Thumb is the track's ::after. State is the checkbox's .checked (DOM), never JS.
 *
 * PLACEHOLDER shell for shadcn parity (HMC-031). Token-only; no controller
 * required — native checkbox semantics + keyboard already work.
 */

.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
  user-select: none;
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.switch input[data-switch] {
  /* Visually hide the checkbox but keep it focusable / AT-accessible */
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.switch__track {
  position: relative;
  display: inline-block;
  width: 2.5rem;
  height: 1.375rem;
  flex: none;
  border-radius: var(--radius-full);
  background: var(--colour-surface-muted);
  border: 1px solid var(--colour-border);
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

.switch__track::after {
  content: "";
  position: absolute;
  top: 0.125rem;
  left: 0.125rem;
  width: 1rem;
  height: 1rem;
  border-radius: var(--radius-full);
  background: var(--colour-surface);
  box-shadow: var(--shadow-sm, 0 1px 2px color-mix(in oklab, var(--colour-text) 12%, transparent));
  transition: transform var(--duration-fast) var(--ease-out);
}

.switch input[data-switch]:checked + .switch__track {
  background: var(--colour-brand);
  border-color: var(--colour-brand);
}

.switch input[data-switch]:checked + .switch__track::after {
  transform: translateX(1.125rem);
  background: var(--colour-brand-contrast, var(--colour-surface));
}

.switch input[data-switch]:focus-visible + .switch__track {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.switch input[data-switch]:disabled + .switch__track {
  opacity: 0.5;
  cursor: not-allowed;
}

.switch:has(input[data-switch]:disabled) {
  cursor: not-allowed;
  opacity: 0.7;
}

@media (prefers-reduced-motion: reduce) {
  .switch__track,
  .switch__track::after {
    transition: none;
  }
}

}
/* ── components/toggle.css ── */
@layer components {
/* HYPERPART: toggle */
/*
 * Single pressable control — toolbar-style on/off for a mode (Bold, Italic).
 * State is aria-pressed on the button (DOM). Pair with toggle-group for
 * exclusive segments; use switch for form boolean fields.
 *
 * PLACEHOLDER — shadcn parity (HMC-032). No controller yet: server sets
 * aria-pressed; a follow-up can flip on click with a tiny document-level
 * listener. Token-only styling.
 */

.toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: 2rem;
  padding-inline: var(--space-sm);
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  color: var(--colour-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.toggle:hover {
  background: var(--colour-surface-muted);
}

.toggle[aria-pressed="true"] {
  background: var(--colour-brand-soft);
  border-color: color-mix(in oklab, var(--colour-brand) 40%, var(--colour-border));
  color: var(--colour-brand-text);
}

.toggle:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

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

.toggle[data-size="sm"] {
  min-height: 1.75rem;
  padding-inline: var(--space-xs);
  font-size: var(--text-xs);
}

}
/* ── components/aspect-ratio.css ── */
@layer components {
/* HYPERPART: aspect-ratio */
/*
 * Media frame that locks width/height ratio. Child fills the box
 * (object-fit cover for images/video).
 *
 * PLACEHOLDER — shadcn parity (HMC-036). Pure CSS; data-ratio selects
 * common ratios. Custom ratios can set style="aspect-ratio: …" on the root.
 */

.aspect-ratio {
  position: relative;
  display: block;
  width: 100%;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--colour-surface-muted);
  aspect-ratio: 16 / 9; /* default */
}

.aspect-ratio[data-ratio="1/1"] {
  aspect-ratio: 1 / 1;
}

.aspect-ratio[data-ratio="4/3"] {
  aspect-ratio: 4 / 3;
}

.aspect-ratio[data-ratio="16/9"] {
  aspect-ratio: 16 / 9;
}

.aspect-ratio[data-ratio="21/9"] {
  aspect-ratio: 21 / 9;
}

.aspect-ratio > * {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

}
/* ── components/item.css ── */
@layer components {
/* HYPERPART: item */
/*
 * Generic list row: optional media + title/description + trailing actions.
 * Compose into lists, menus, and search results. State-free presentation;
 * interactive children (links/buttons) own their own exchanges.
 *
 * PLACEHOLDER — shadcn parity (HMC-033).
 */

.item {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-block: var(--space-sm);
  padding-inline: var(--space-md);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  background: transparent;
  color: var(--colour-text);
  text-align: start;
  width: 100%;
  box-sizing: border-box;
}

.item[data-variant="outline"] {
  border-color: var(--colour-border);
  background: var(--colour-surface);
}

.item[data-variant="muted"] {
  background: var(--colour-surface-muted);
}

a.item {
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

a.item:hover,
button.item:hover {
  background: var(--colour-surface-muted);
}

.item:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.item__media {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-md);
  background: var(--colour-brand-soft);
  color: var(--colour-brand-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  overflow: hidden;
}

.item__media > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.item__content {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.item__title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.item__description {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

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

.item-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

}
/* ── components/hover-card.css ── */
@layer components {
/* HYPERPART: hover-card */
/*
 * Rich preview on hover/focus/click — progressive enhancement.
 * Trigger + floating panel opens on:
 *   :hover / :focus-within  — fine pointers + keyboard
 *   [data-open] / [data-open] / .is-open — controller (touch / click)
 *
 * iPadOS Safari does not keep :hover or button :focus after tap; relying on
 * focus-within alone made the gallery demo appear dead. controllers/hover-card.js
 * toggles data-open on click/tap.
 *
 * Anatomy (canonical):
 *   .hover-card
 *     .hover-card__trigger
 *     .hover-card__content   (alias: .hover-card__panel — legacy)
 *
 * Hit-target: a transparent ::before bridge above the panel covers the
 * visual gap so the cursor can move trigger → panel without leaving
 * :hover on the root (classic "death zone" bug when top > 100%).
 *
 * shadcn parity (HMC-035). Distinct from popover (details exclusive open).
 */

.hover-card {
  position: relative;
  display: inline-block;
}

.hover-card__trigger {
  color: var(--colour-brand-text);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  cursor: pointer;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
}

.hover-card__trigger:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Content + legacy __panel alias (hm-core stub used __panel) */
.hover-card__content,
.hover-card__panel {
  position: absolute;
  z-index: var(--z-popover, 40);
  /* Sit just below the trigger; visual gap via margin, hit bridge via ::before */
  top: 100%;
  left: 0;
  margin-top: var(--space-xs);
  min-width: 14rem;
  max-width: 18rem;
  padding: var(--space-md);
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  box-shadow: var(--shadow-md, 0 8px 24px color-mix(in oklab, var(--colour-text) 12%, transparent));
  color: var(--colour-text);
  font-size: var(--text-sm);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-0.25rem);
  pointer-events: none;
  transition:
    opacity var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out),
    visibility var(--duration-fast) var(--ease-out);
}

/* Bridge the visual gap so moving the pointer into the panel keeps :hover */
.hover-card__content::before,
.hover-card__panel::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* Cover margin-top gap + a little extra for diagonal mouse paths */
  bottom: 100%;
  height: calc(var(--space-xs) + 0.25rem);
}

/* Open: hover + focus-within (desktop/keyboard) OR explicit open (touch click) */
.hover-card:hover .hover-card__content,
.hover-card:hover .hover-card__panel,
.hover-card:focus-within .hover-card__content,
.hover-card:focus-within .hover-card__panel,
.hover-card[data-open] .hover-card__content,
.hover-card[data-open] .hover-card__panel,
.hover-card[data-open] .hover-card__content,
.hover-card[data-open] .hover-card__panel,
.hover-card.is-open .hover-card__content,
.hover-card.is-open .hover-card__panel {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  pointer-events: auto;
}

.hover-card__title {
  font-weight: var(--weight-medium);
  margin: 0 0 var(--space-xs);
}

.hover-card__description {
  margin: 0;
  color: var(--colour-text-muted);
  font-size: var(--text-xs);
  line-height: 1.4;
}

@media (prefers-reduced-motion: reduce) {
  .hover-card__content,
  .hover-card__panel {
    transition: none;
  }
}

/*
 * Coarse pointers: do not rely on sticky :hover (iOS first-tap quirks).
 * Keep explicit open + focus-within; ignore bare :hover so a leftover sticky
 * hover state does not fight click-to-close.
 */
@media (hover: none) {
  .hover-card:hover .hover-card__content,
  .hover-card:hover .hover-card__panel {
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.25rem);
    pointer-events: none;
  }
  .hover-card:focus-within .hover-card__content,
  .hover-card:focus-within .hover-card__panel,
  .hover-card[data-open] .hover-card__content,
  .hover-card[data-open] .hover-card__panel,
  .hover-card[data-open] .hover-card__content,
  .hover-card[data-open] .hover-card__panel,
  .hover-card.is-open .hover-card__content,
  .hover-card.is-open .hover-card__panel {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    pointer-events: auto;
  }
}

}
/* ── components/carousel.css ── */
@layer components {
/* HYPERPART: carousel */
/*
 * Content strip — media and/or HTML fragments. Decision 0009 +
 * stems/pragmatic-gallery-aesthetics.md.
 *
 * Stage: fixed aspect (data-ratio). Media defaults to full-bleed cover so
 * demos look intentional; teach source aspect with captions, not letterbox gutters.
 * Hero slides: media + gradient overlay (hypermedia stays in-family).
 */

.carousel {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  width: 100%;
  max-width: 42rem;
}

.carousel[data-size="sm"] {
  max-width: 28rem;
}

.carousel[data-size="lg"] {
  max-width: 56rem;
}

.carousel__viewport {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  /* Match media letterbox-free stage (no dark gutter bands). */
  background: var(--colour-surface);
  aspect-ratio: 16 / 9;
}

.carousel__viewport[data-ratio="1/1"] {
  aspect-ratio: 1 / 1;
}
.carousel__viewport[data-ratio="4/3"] {
  aspect-ratio: 4 / 3;
}
.carousel__viewport[data-ratio="16/9"] {
  aspect-ratio: 16 / 9;
}
.carousel__viewport[data-ratio="21/9"] {
  aspect-ratio: 21 / 9;
}

.carousel__track {
  display: grid;
  grid-template-areas: "slide";
  height: 100%;
}

.carousel__slide {
  grid-area: slide;
  display: none;
  position: relative;
  align-items: center;
  justify-content: center;
  min-height: 0;
  min-width: 0;
  padding: 0;
  color: var(--colour-text-muted);
  font-size: var(--text-sm);
}

.carousel__slide[data-active] {
  display: flex;
}

/* Full-bleed media — cover fills the stage (no black bars). */
.carousel__slide--media {
  background: var(--colour-surface);
}

.carousel__media {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 0;
  box-sizing: border-box;
  position: relative;
}

.carousel__media img,
.carousel__media svg {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}

/* Explicit contain (letterbox) when the lesson is mixed intrinsic size */
.carousel__media--contain {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--colour-surface-muted);
  padding: 0;
}
.carousel__media--contain img,
.carousel__media--contain svg {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Cover frame via composed aspect-ratio (fills media slot). */
.carousel__media--cover {
  display: block;
}
.carousel__media--cover .aspect-ratio {
  width: 100%;
  height: 100%;
  border-radius: 0;
  background: transparent;
}

/* Source-aspect chip (teaches policy without ugly gutters) */
.carousel__chip {
  position: absolute;
  top: var(--space-sm);
  inset-inline-start: var(--space-sm);
  z-index: 1;
  margin: 0;
  padding: 0.2rem 0.5rem;
  border-radius: var(--radius-sm);
  background: oklch(0% 0 0 / 0.55);
  color: oklch(100% 0 0);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: 1.2;
  pointer-events: none;
}

/* Hero: media + bottom gradient + in-family CTA (not a floating admin card) */
.carousel__slide--hero {
  display: none;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-end;
}
.carousel__slide--hero[data-active] {
  display: flex;
}
.carousel__slide--hero > .carousel__media {
  position: absolute;
  inset: 0;
}
.carousel__hero {
  position: relative;
  z-index: 1;
  margin-top: auto;
  padding: var(--space-lg) var(--space-md) var(--space-md);
  background: linear-gradient(
    to top,
    oklch(12% 0.02 260 / 0.92) 0%,
    oklch(12% 0.02 260 / 0.55) 55%,
    oklch(12% 0.02 260 / 0) 100%
  );
  color: oklch(98% 0 0);
  text-align: start;
}
.carousel__hero-kicker {
  margin: 0 0 0.15rem;
  font-size: var(--text-xs);
  opacity: 0.85;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.carousel__hero-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: 1.25;
  color: inherit;
}
.carousel__hero-text {
  margin: 0.35rem 0 0;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  opacity: 0.9;
  max-width: 28rem;
}
.carousel__hero .button {
  margin-top: var(--space-sm);
}
.carousel__hero-live {
  margin-top: var(--space-xs);
  min-height: 1.25rem;
  font-size: var(--text-xs);
}

.carousel__caption {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  line-height: var(--leading-normal);
}

.carousel__status {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  text-align: center;
}

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

.carousel__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  min-height: 2.25rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  color: var(--colour-text);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: pointer;
}

.carousel__btn:hover {
  background: var(--colour-surface-muted);
}

.carousel__btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

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

.carousel__dot {
  position: relative;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 999px;
  border: none;
  padding: 0;
  background: transparent;
  cursor: pointer;
  flex: none;
}

.carousel__dot::before {
  content: "";
  position: absolute;
  inset: 50%;
  width: 0.5rem;
  height: 0.5rem;
  transform: translate(-50%, -50%);
  border-radius: 999px;
  background: var(--colour-border);
}

.carousel__dot[aria-current="true"]::before {
  background: var(--colour-brand);
  width: 1rem;
}

@media (prefers-reduced-motion: reduce) {
  /* Autoplay disabled in JS; status text explains off state. */
}

}
/* ── components/menubar.css ── */
@layer components {
/* HYPERPART: menubar */
/*
 * Horizontal app menubar — File / Edit / View style. Each item is a
 * details/summary so open state is DOM-native.
 *
 * Exclusive open across items: controllers/menubar.js (toggle capture).
 * Gated by gallery probe menubar.exclusive_open
 * (packages/hatchi-maxchi/tools/gallery_probes.py).
 */

.menubar {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  padding-inline: var(--space-2xs);
}

.menubar__item {
  position: relative;
}

.menubar__trigger {
  display: inline-flex;
  align-items: center;
  min-height: 2rem;
  padding-inline: var(--space-sm);
  border: none;
  background: transparent;
  color: var(--colour-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  border-radius: var(--radius-sm);
  list-style: none;
}

.menubar__trigger::-webkit-details-marker {
  display: none;
}

.menubar__item[open] > .menubar__trigger,
.menubar__trigger:hover {
  background: var(--colour-surface-muted);
}

.menubar__panel {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  min-width: 10rem;
  margin-top: 0.125rem;
  padding: var(--space-2xs);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  box-shadow: var(--shadow-md, 0 4px 16px color-mix(in oklab, #000 12%, transparent));
}

.menubar__panel a,
.menubar__panel button {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-xs) var(--space-sm);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--colour-text);
  font-size: var(--text-sm);
  text-decoration: none;
  cursor: pointer;
}

.menubar__panel a:hover,
.menubar__panel button:hover {
  background: var(--colour-surface-muted);
}

}
/* ── components/bubble.css ── */
@layer components {
/* HYPERPART: bubble */
/*
 * Chat bubble — rounded content shell for inbound/outbound speech.
 * Orientation via data-from="in|out". Tone via data-tone when needed.
 *
 * PLACEHOLDER — shadcn parity (HMC-040). Compose inside message rows.
 * No JS controller.
 */

.bubble {
  display: inline-block;
  max-width: min(36rem, 100%);
  padding-block: var(--space-xs);
  padding-inline: var(--space-sm);
  border-radius: var(--radius-lg, 0.75rem);
  background: var(--colour-surface-muted);
  color: var(--colour-text);
  font-size: var(--text-sm);
  line-height: 1.45;
  word-wrap: break-word;
  overflow-wrap: anywhere;
}

.bubble[data-from="out"] {
  background: var(--colour-brand-soft, var(--colour-brand));
  color: var(--colour-brand-text, var(--colour-text));
}

.bubble[data-from="in"] {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}

.bubble[data-tone="danger"] {
  background: color-mix(in oklab, var(--colour-danger, #c0392b) 18%, var(--colour-surface));
  color: var(--colour-text);
}

.bubble p {
  margin: 0;
}

.bubble p + p {
  margin-top: var(--space-2xs);
}

}
/* ── components/message.css ── */
@layer components {
/* HYPERPART: message */
/*
 * Chat message row — optional media (avatar) + column of meta + bubble.
 * data-from="in|out" mirrors bubble orientation (flex reverse for out).
 *
 * PLACEHOLDER — shadcn parity (HMC-041). Compose bubble Hyperpart inside.
 * No JS controller; live updates are OOB/fragment swaps.
 */

.message {
  display: flex;
  align-items: flex-end;
  gap: var(--space-sm);
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}

.message[data-from="out"] {
  flex-direction: row-reverse;
}

.message__media {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  background: var(--colour-brand-soft);
  color: var(--colour-brand-text);
  font-size: var(--text-xs, 0.75rem);
  font-weight: var(--weight-medium);
  overflow: hidden;
}

.message__media > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.message__body {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
  max-width: min(36rem, calc(100% - 3rem));
}

.message[data-from="out"] .message__body {
  align-items: flex-end;
}

.message[data-from="in"] .message__body {
  align-items: flex-start;
}

.message__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  /* --space-2xs does not exist in tokens.css (invalidates the whole gap
     declaration → author/time collide: "Maya Reyes10:02"). Use xs/sm. */
  gap: var(--space-xs) var(--space-sm);
  font-size: var(--text-xs, 0.75rem);
  color: var(--colour-text-muted, var(--colour-text-secondary, #6b7280));
}

.message[data-from="out"] .message__meta {
  flex-direction: row-reverse;
}

.message__author {
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.message__time {
  font-variant-numeric: tabular-nums;
}

}
/* ── components/message-scroller.css ── */
@layer components {
/* HYPERPART: message-scroller */
/*
 * Chat transcript scroller — vertical stack of message rows with
 * max-height + overflow. Newest-at-bottom is document order (append
 * messages at the end; scrollHeight is host/controller concern).
 *
 * PLACEHOLDER — shadcn parity (HMC-042). Compose message Hyperparts inside.
 * Auto-scroll-to-bottom controller deferred.
 */

.message-scroller {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  width: 100%;
  max-height: min(28rem, 70vh);
  overflow-x: hidden;
  overflow-y: auto;
  padding: var(--space-md);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  box-sizing: border-box;
  scroll-behavior: smooth;
}

.message-scroller[data-size="sm"] {
  max-height: 14rem;
  padding: var(--space-sm);
  gap: var(--space-sm);
}

.message-scroller[data-size="lg"] {
  max-height: min(40rem, 80vh);
}

.message-scroller:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* Empty / loading affordance when no message children yet */
.message-scroller__empty {
  margin: auto;
  padding: var(--space-lg);
  color: var(--colour-text-muted, var(--colour-text-secondary, #6b7280));
  font-size: var(--text-sm);
  text-align: center;
}

}
/* ── components/navigation-menu.css ── */
@layer components {
/* HYPERPART: navigation-menu */
/*
 * Top navigation mega-menu — horizontal site/product nav (distinct from
 * menubar File/Edit/View and from app-shell's sidebar). Each trigger is a
 * native details/summary; open panel is a multi-column mega list.
 * Exclusive open + outside dismiss: controllers/navigation-menu.js.
 * Gated by gallery probes navigation_menu.exclusive_open /
 * navigation_menu.dismiss_outside (packages/hatchi-maxchi/tools/gallery_probes.py).
 *
 * Disclosure chrome: CSS chevron on the trigger (::after), same family as
 * accordion — not a Unicode ▾ span (stem: affordance-disclosure-chrome).
 */

.navigation-menu {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
  width: 100%;
  border-bottom: 1px solid var(--colour-border);
  background: var(--colour-surface);
  padding-inline: var(--space-sm);
}

.navigation-menu__list {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.navigation-menu__item {
  position: relative;
}

.navigation-menu__link,
.navigation-menu__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-height: 2.75rem;
  padding-inline: var(--space-md);
  border: none;
  background: transparent;
  color: var(--colour-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  cursor: pointer;
  border-radius: var(--radius-sm);
  list-style: none;
}

.navigation-menu__trigger::-webkit-details-marker {
  display: none;
}

/* CSS chevron — inherits currentColor; rotates when the branch is open.
 * Sized in rem (not em-of-label) so the affordance stays peer to the label
 * (stem: affordance-disclosure-chrome). */
.navigation-menu__trigger::after {
  content: "";
  flex: none;
  width: 1rem;
  height: 1rem;
  background: currentColor;
  opacity: 0.65;
  -webkit-mask: center / contain no-repeat
    url('data:image/svg+xml;utf8,<svg 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"><path d="m6 9 6 6 6-6"/></svg>');
  mask: center / contain no-repeat
    url('data:image/svg+xml;utf8,<svg 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"><path d="m6 9 6 6 6-6"/></svg>');
  transition: transform var(--duration-base, 150ms) var(--ease-out, ease);
}

.navigation-menu__branch[open] > .navigation-menu__trigger::after,
details.navigation-menu__branch[open] > .navigation-menu__trigger::after {
  transform: rotate(180deg);
}

.navigation-menu__item[open] > .navigation-menu__trigger,
.navigation-menu__link:hover,
.navigation-menu__trigger:hover,
.navigation-menu__link[aria-current="page"] {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}

/* Open branch: surface highlight on trigger (details is the open element) */
.navigation-menu__branch[open] > .navigation-menu__trigger {
  background: var(--colour-surface-muted);
  color: var(--colour-text);
}

.navigation-menu__link:focus-visible,
.navigation-menu__trigger:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.navigation-menu__panel {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 30;
  min-width: 16rem;
  max-width: min(36rem, 90vw);
  margin-top: 0;
  padding: var(--space-md);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  box-shadow: var(--shadow-md, 0 4px 16px color-mix(in oklab, #000 12%, transparent));
}

.navigation-menu__panel[data-layout="mega"] {
  display: grid;
  grid-template-columns: repeat(2, minmax(10rem, 1fr));
  gap: var(--space-md) var(--space-lg);
  min-width: 24rem;
}

.navigation-menu__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
}

.navigation-menu__group-title {
  margin: 0 0 var(--space-2xs);
  padding-inline: var(--space-xs);
  font-size: var(--text-xs, 0.75rem);
  font-weight: var(--weight-semibold, 600);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--colour-text-muted, var(--colour-text-secondary, #6b7280));
}

.navigation-menu__panel a {
  display: block;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  color: var(--colour-text);
  font-size: var(--text-sm);
  text-decoration: none;
}

.navigation-menu__panel a:hover {
  background: var(--colour-surface-muted);
}

.navigation-menu__panel a small {
  display: block;
  margin-top: 0.125rem;
  font-size: var(--text-xs, 0.75rem);
  font-weight: normal;
  color: var(--colour-text-muted, var(--colour-text-secondary, #6b7280));
}

@media (prefers-reduced-motion: reduce) {
  .navigation-menu__trigger::after {
    transition: none;
  }
}

}
/* ── components/marker.css ── */
@layer components {
/* HYPERPART: marker */
/*
 * Map / location marker chrome — pin silhouette + optional label.
 * Pure presentation; host supplies map projection / absolute position.
 *
 * PLACEHOLDER — shadcn parity (HMC-043). No map SDK; compose over a host
 * map canvas or static plan. data-tone for status pins.
 */

.marker {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  width: max-content;
  max-width: 12rem;
  color: var(--colour-brand);
  font-size: var(--text-xs, 0.75rem);
  text-align: center;
  cursor: default;
}

.marker__pin {
  display: block;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50% 50% 50% 0;
  background: currentColor;
  transform: rotate(-45deg);
  box-shadow: 0 1px 3px color-mix(in oklab, #000 25%, transparent);
}

.marker__pin::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  background: var(--colour-surface);
  transform: translate(-50%, -50%) rotate(45deg);
}

/* Keep ::after positioned relative to the pin box */
.marker__pin {
  position: relative;
}

.marker__label {
  margin-top: 0.25rem;
  padding: 0.125rem 0.375rem;
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  color: var(--colour-text);
  font-weight: var(--weight-medium);
  line-height: 1.2;
  box-shadow: var(--shadow-sm, 0 1px 2px color-mix(in oklab, #000 8%, transparent));
}

.marker[data-tone="success"] {
  color: var(--colour-success, #1a7f37);
}

.marker[data-tone="warning"] {
  color: var(--colour-warning, #b45309);
}

.marker[data-tone="danger"] {
  color: var(--colour-danger, #c0392b);
}

.marker[data-size="lg"] .marker__pin {
  width: 2rem;
  height: 2rem;
}

.marker[data-size="lg"] .marker__pin::after {
  width: 0.625rem;
  height: 0.625rem;
}

.marker:focus-within .marker__label,
.marker:hover .marker__label {
  border-color: currentColor;
}

}
/* ── components/table.css ── */
@layer components {
/* HYPERPART: grid */
/* ─────────────────────────── outer wrapper ──────────────────────
 * Table card root — vertical flow of header / toolbar / scroll
 * container / pagination footer. */

.table {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  /* TASTE-5: tabular numerals keep data columns optically aligned (Geist
     ships tnum); harmless no-op on fallback stacks without the feature. */
  font-variant-numeric: tabular-nums;
}

/* ─────────────────────────── toolbar row ────────────────────────
 * search input / filter chips / bulk actions. Wraps on narrow
 * containers. */

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

.table-toolbar-filters {
  flex: 1 1 0;
  min-width: 0;
}

/* ─────────────────────────── column visibility menu ─────────────
 * A native <details> disclosure (the .menu idiom — no open/close
 * JS); the toggles ride the delegated grid-cols extension (C2.1). */

.table-col-menu {
  position: relative;
}

/* The trigger is the <summary> — strip the UA disclosure marker so the
 * existing button chrome applies cleanly. */
summary.table-col-menu-trigger {
  list-style: none;
}
summary.table-col-menu-trigger::-webkit-details-marker {
  display: none;
}

.table-col-menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  height: 1.75rem;
  padding-inline: var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  cursor: pointer;
  user-select: none;
  transition:
    background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}

.table-col-menu-trigger:hover {
  background: var(--colour-surface);
}

/* Open state — matches popover triggers (shadcn/DropdownMenu): pressed surface + focus ring. */
.table-col-menu[open] > .table-col-menu-trigger {
  background: var(--colour-surface);
  color: var(--colour-text);
  border-color: color-mix(in oklch, var(--colour-brand, #2563eb) 45%, var(--colour-border));
  box-shadow: 0 0 0 2px color-mix(in oklch, var(--colour-brand, #2563eb) 22%, transparent);
}

.table-col-menu-panel {
  position: absolute;
  right: 0;
  top: 100%;
  margin-block-start: var(--space-xs);
  width: 11rem;
  z-index: 100;
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding-block: var(--space-xs);
  overflow: hidden;
}

.table-col-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-inline: var(--space-md);
  padding-block: var(--space-xs);
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--colour-text);
  transition: background var(--duration-fast) var(--ease-out);
}

.table-col-menu-item:hover {
  background: var(--colour-bg);
}

/* #853 escape hatch: the "Show all columns" reset at the panel's foot. */
.table-col-menu-reset {
  display: block;
  width: 100%;
  padding-inline: var(--space-md);
  padding-block: var(--space-xs);
  margin-block-start: var(--space-xs);
  border: none;
  border-block-start: 1px solid var(--colour-border);
  background: none;
  text-align: start;
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  transition: background var(--duration-fast) var(--ease-out);
}

.table-col-menu-reset:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.table-col-menu-checkbox {
  width: 0.875rem;
  height: 0.875rem;
  border-radius: var(--radius-sm);
  accent-color: var(--colour-brand);
  cursor: pointer;
}

/* ─────────────────────────── scroll container ───────────────────
 * Holds the loading overlay + horizontal scroll region + table.
 * Border + radius + clip live here so the sticky header doesn't
 * bleed past the rounded corners. */

.table-scroll {
  position: relative;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  /* Reserve vertical space for the upcoming htmx fetch so the page below
     doesn't shift when rows arrive (#965 — CLS regression on list views).
     Floor: `--list-rows` rows × row height + header. The template emits
     --list-rows from table.page_size; without it we default to 10 rows,
     which catches the common case (CLS lands cleanly under 0.1 even when
     actual results are smaller than the reservation). */
  min-height: calc(min(var(--list-rows, 10), 10) * 2.75rem + 3rem);
}

.table-scroll-x {
  overflow-x: auto;
}

/* ─────────────────────────── loading overlay ────────────────────
 * #972: switched from Alpine `x-show="loading"` to pure-CSS keyed off
 * htmx's `.htmx-request` class on the outer dzTable wrapper. The Alpine
 * binding tripped ADR-0022 (idiomorph evaluates child bindings before
 * the ancestor x-data scope rebinds on morph). htmx applies
 * `.htmx-request` to the request-initiating element for the duration
 * of the in-flight request — that's the cleanest signal here.
 *
 * Default state: hidden (no overlay flash on initial paint, replaces
 * the prior x-cloak guard). htmx adds .htmx-request when a request is
 * in flight; CSS reveals the overlay. Backdrop blur + dimmed
 * background + centred spinner. Pointer-events none so the user can
 * still scroll mid-load. */

.table-loading {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: none;
  align-items: center;
  justify-content: center;
  background: color-mix(in oklch, var(--colour-bg) 60%, transparent);
  backdrop-filter: blur(1px);
  pointer-events: none;
  opacity: 0;
  transition: opacity 80ms;
}

/* htmx applies `.htmx-request` to the element named by hx-indicator
 * (the SR-only `#{table_id}-loading-sr` inside .table) for the
 * duration of the in-flight request. `:has()` lets us trigger the
 * overlay from any descendant — Chrome 105+, Safari 15.4+, Firefox
 * 121+ all support it. */
.table:has(.htmx-request) .table-loading {
  display: flex;
  opacity: 1;
}

.table-loading-spinner {
  width: 1.25rem;
  height: 1.25rem;
  color: var(--colour-text-muted);
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ─────────────────────────── the table itself ───────────────────
 * border-collapse so the sticky-header shadow sits flush. */

.table-grid {
  width: 100%;
  /* Fixed layout so text cells can ellipsize against real column widths
     instead of a hard-coded max-width on .tr-cell-truncate (humanqa
     trace: titles "Board-visible: migrate hero C…" + dates "21 Jul 20…"). */
  table-layout: fixed;
  border-collapse: collapse;
}

/* Sticky header — bg + bottom shadow stand in for a 1px border that
 * collapse would have stripped. */
.table-th {
  position: relative;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  text-align: start;
  white-space: nowrap;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--colour-text-muted);
  user-select: none;
}

.table-th-select {
  width: 2.5rem;
}

/* Actions is chrome, not a data field: no sort/resize, no data-col.
 * Width must fit icon cluster AND optional state-transition chips
 * (humanqa: "Review"/"Done" stacked in 1.75rem squares).
 * Header uses .table-th + .table-th-actions (visible "Actions" label). */
.table-th-actions,
.table-col-actions {
  width: 11rem;
  min-width: 7rem;
  text-align: end;
  white-space: nowrap;
}

/* Keep header label flush with the chip strip (both end-aligned). */
.table-th.table-th-actions {
  text-align: end;
}

/* HYPERPART: grid (extension) */
/* Inline-edit editor elements (grid-edit.js builds these on dblclick —
 * JS-assigned classNames, so the registry-class gate can't see them). */
.inline-edit-input {
  width: 100%;
  height: 1.75rem;
  padding-inline: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--colour-text);
  background: var(--colour-surface);
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-brand);
  outline: none;
  transition:
    border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}

.inline-edit-input:focus-visible {
  box-shadow: 0 0 0 2px color-mix(in oklch, var(--colour-brand) 20%, transparent);
}

.inline-edit-input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.inline-edit-select {
  padding-inline-end: var(--space-lg);
  appearance: none;
}

.inline-edit-checkbox {
  width: 1rem;
  height: 1rem;
  border-radius: var(--radius-sm);
  accent-color: var(--colour-brand);
  cursor: pointer;
}

.inline-edit-checkbox:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Date leftover honesty (cycle 2150): native picker + ISO companion.
 * Rest-state gallery (display spans) is unchanged — editor-only chrome. */
.inline-edit-date {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
}

.inline-edit-date-native {
  width: auto;
  min-width: 11ch;
  flex: 0 0 auto;
}

.inline-edit-date-iso {
  flex: 1 1 12ch;
  min-width: 10ch;
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
}

.inline-edit-date-iso:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

/* Time leftover honesty (cycle 2153): native clock + ISO companion.
 * Rest-state gallery (display spans) is unchanged — editor-only chrome. */
.inline-edit-time {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
}

.inline-edit-time-native {
  width: auto;
  min-width: 11ch;
  flex: 0 0 auto;
}

.inline-edit-time-iso {
  flex: 1 1 12ch;
  min-width: 10ch;
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
}

.inline-edit-time-iso:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

/* Number leftover honesty (cycle 2155): native number + decimal companion.
 * Rest-state gallery (display spans) is unchanged — editor-only chrome. */
.inline-edit-number {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
}

.inline-edit-number-native {
  width: auto;
  min-width: 8ch;
  flex: 0 0 auto;
}

.inline-edit-number-out {
  flex: 1 1 8ch;
  min-width: 6ch;
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
}

.inline-edit-number-out:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

/* NB the select-column <col> class (table-col-select, in the grid's
 * colgroup) is SEMANTIC_ONLY: a structural marker with no rule — the table
 * stays layout:auto, so the col carries no width. */
/* Column-resize handle (grid-resize.js extension): an 8px pointer strip
 * at the header's right edge driving col[data-col] widths via
 * grid-resize.js. Decorative (aria-hidden); a faint hairline appears on
 * hover; while a drag is live the root carries data-resizing. */
.table-resize-handle {
  position: absolute;
  top: 0;
  /* Fully inside the th: a -4px overhang on the LAST column gets clipped by
   * .table-scroll's overflow:hidden, halving its pointer target. */
  right: 0;
  width: 8px;
  height: 100%;
  cursor: col-resize;
  touch-action: none;
  z-index: 1;
}

.table-resize-handle::after {
  content: "";
  position: absolute;
  top: 20%;
  bottom: 20%;
  right: 1px;
  width: 2px;
  border-radius: 1px;
  background: transparent;
  transition: background var(--duration-fast) var(--ease-out);
}

.table-resize-handle:hover::after,
[data-resizing] .table-resize-handle::after {
  background: var(--colour-border);
}

[data-grid][data-resizing] {
  cursor: col-resize;
  user-select: none;
}

/* Touch (coarse pointer): the 8px strip is a mouse-precision target. Widen
 * the strip to 24px — the web-data-grid touch idiom (an edge slop, not a
 * 44px HIG block, which would eat half the header) — and keep the hairline
 * ALWAYS visible: hover doesn't exist on touch, so the affordance must not
 * depend on it. touch-action:none above already stops the drag being
 * swallowed by scroll panning. Worst case: at the 80px column minimum the
 * strip is 30% of the header and the sort label's rightmost text sits under
 * it — accepted for a user-chosen minimum width. */
@media (pointer: coarse) {
  .table-resize-handle {
    width: 24px;
  }

  .table-resize-handle::after {
    background: var(--colour-border);
  }
}

/* Sort button — inline within the th when the column is sortable. */
.table-sort-button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  cursor: pointer;
  background: transparent;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  transition: color var(--duration-fast) var(--ease-out);
}

.table-sort-button:hover {
  color: var(--colour-text);
}

/* Sort caret: hidden while the column is unsorted (a faint hint on hover),
 * revealed + oriented by the th's `aria-sort` — the same attribute the
 * controller writes and assistive tech reads (no parallel state). chevron-up =
 * ascending; descending flips it 180deg. */
.table-sort-icon {
  /* The caret is decorative (aria-hidden) and must be transparent to
   * pointer/touch so a tap on the <svg>/<use> icon (a natural target at the
   * header's right edge) never becomes the click's event.target. On older iOS
   * Safari an event target inside an SVG <use> can retarget into the use's
   * shadow content, from which `closest()` cannot escape → the delegated sort
   * handler receives null and the tap does nothing. Standard hardening for an
   * icon inside an interactive control: every tap falls through to the
   * <button>, so event.target is always the button. (Not reproduced locally —
   * headless WebKit taps the button fine — but a documented older-Safari
   * failure mode and the correct mitigation regardless.) */
  pointer-events: none;
  opacity: 0;
  transition:
    opacity var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

.table-sort-button:hover .table-sort-icon {
  opacity: 0.4;
}

.table-th[aria-sort="ascending"] .table-sort-icon {
  opacity: 1;
  transform: none;
}

.table-th[aria-sort="descending"] .table-sort-icon {
  opacity: 1;
  transform: rotate(180deg);
}

/* Resize handle — 4px wide vertical strip on the right edge of each
 * th, shows brand tint on hover. */
/* Body — divide rows with subtle border-top via adjacent-sibling. */
.table-body > tr + tr {
  border-block-start: 1px solid var(--colour-border);
}

/* ─────────────────────────── empty state ────────────────────────
 * Visible (via :has selector) when tbody has no data rows. */

.table-empty {
  display: none;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  padding-block: var(--space-3xl);
  color: var(--colour-text-muted);
}

/* Show the empty state only when the preceding table has no body cells (#1450).
 * The `tbody` is HTMX-loaded and lives INSIDE the `<table class="table-grid">`,
 * which is a preceding SIBLING of `.table-empty` — so the guard keys off the
 * grid (`.table-grid ~ .table-empty`), not a non-existent following-sibling
 * `tbody`. Using `tbody tr td` (any body cell) also drops the brittle
 * `data-row-id` dependency the old guard required. Populated → no match → base
 * `display: none` hides it; empty (or pre-load) → shown. */
.table-grid:not(:has(tbody tr td)) ~ .table-empty {
  display: flex;
}

.table-empty-icon {
  width: 2.5rem;
  height: 2.5rem;
  opacity: 0.3;
}

.table-empty-title {
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  margin: 0;
}

.table-empty-hint {
  font-size: var(--text-sm);
  margin: 0;
}

/* ─────────────────────────── pagination footer ──────────────────
 * Wrapper for the pagination fragment (own internal layout). */

.table-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Registry row-action icons (Phase 3): fixed 14px optical size. */
.tr-action-icon {
  width: 0.875rem;
  height: 0.875rem;
  display: block;
}


/* ── data-table rows / filter bar / bulk actions ─────────────────────
 * Moved verbatim from Dazzle fragments.css (grid Hyperpart Slice 0b).
 * The `.table` ancestor selectors here gate the bulk chrome (#978). */
/* #978: bulk-actions visibility is CSS-driven by `[data-bulk-count]`
 * on the ancestor grid root — historically the `.table` element (the
 * gallery), generalized to `[data-grid]` for consumers whose root is a
 * wrapping region (Dazzle's list <section>, convergence C1.1). The count is
 * written by grid.js's sync().
 * Default state: hidden (no flash on initial paint, replaces the prior
 * Alpine x-cloak guard). Show only when bulkCount > 0. */

.bulk-actions {
  display: none;
  align-items: center;
  gap: var(--space-md);
}

.table:not([data-bulk-count="0"]) .bulk-actions,
[data-grid]:not([data-bulk-count="0"]) .bulk-actions {
  display: flex;
}

/* "items" vs "item" — show plural unless count is exactly 1. */
.bulk-plural {
  display: inline;
}
.table[data-bulk-count="1"] .bulk-plural,
[data-grid][data-bulk-count="1"] .bulk-plural {
  display: none;
}

/* Pagination summary — selected vs row-count visibility, mirrors the
 * #978 pattern. The two spans render in the same slot; one shows when
 * the bulk count is positive, the other when it's zero. */
.bulk-summary-selected {
  display: none;
}
.bulk-summary-rows {
  display: inline;
}
.table:not([data-bulk-count="0"]) .bulk-summary-selected,
[data-grid]:not([data-bulk-count="0"]) .bulk-summary-selected {
  display: inline;
}
.table:not([data-bulk-count="0"]) .bulk-summary-rows,
[data-grid]:not([data-bulk-count="0"]) .bulk-summary-rows {
  display: none;
}

.bulk-delete {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--colour-danger);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.bulk-delete:hover {
  background: color-mix(in oklch, var(--colour-danger) 8%, transparent);
}

.bulk-clear {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out);
}

.bulk-clear:hover {
  color: var(--colour-text);
}

/* "Select all N matching" — the all-matching escalation affordance. A quiet
 * brand-coloured text button (it sits between the count and Delete). Hidden
 * once the mode is active: its job is done, and the header select-all is the
 * exit. */
.bulk-matching {
  font-size: var(--text-sm);
  color: var(--colour-brand-text);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out);
}

.bulk-matching:hover {
  text-decoration: underline;
}

.table[data-grid-all-matching="true"] .bulk-matching,
[data-grid][data-grid-all-matching="true"] .bulk-matching {
  display: none;
}

/* SR-only announcer for result-window changes: the pagination footer is
 * server-repainted wholesale, so screen readers can't track it — the grid
 * controller mirrors the summary into this aria-live region instead.
 * Standard visually-hidden pattern (present in the a11y tree, no pixels). */
.grid-announce {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ─────────────────────────── filter_bar fragment ────────────────
 * Per-column filter inputs (selects + text + ref-loaded selects).
 * Outer row layout + label sizing only — inputs use the form
 * input chrome from form.css. */

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  align-items: flex-end;
}

.filter-cell {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  width: 10rem;
}

.filter-label {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
}

.filter-select,
.filter-input {
  /* min-height (not a hard height) + an explicit padding-block: a fixed
   * `height: 2rem` on a border-box <select> left only ~14px of content
   * once the UA's default ~8px vertical padding + 1px borders were
   * subtracted, clipping the bottom of the text. min-height keeps the
   * ~2rem look but lets the control grow rather than crop. */
  box-sizing: border-box;
  min-height: 2rem;
  width: 100%;
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  background-color: var(--colour-surface);
  padding-inline: var(--space-sm);
  padding-block: 0.25rem;
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--colour-text);
  outline: none;
  transition: box-shadow var(--duration-base) var(--ease-out);
}

/* WebKit ignores min-height on native <select> (#930 / CyFuture
 * letter filters). Pin height + flatten UA chrome so the line-box
 * fits; keep a chevron after appearance:none. */
select.filter-select {
  height: 2rem;
  -webkit-appearance: none;
  appearance: none;
  padding-inline-end: 1.5rem;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.4rem center;
  background-size: 0.75rem 0.75rem;
}

.filter-select:focus-visible,
.filter-input:focus-visible {
  box-shadow: 0 0 0 1px var(--colour-brand);
}

/* ─────────────────────────── table_rows fragment ────────────────
 * Inline-editable data table rows with bulk-select checkboxes,
 * per-cell display/edit modes, and hover-revealed action buttons.
 * Row state matrix:
 *   - .is-selected  → bulk-select checkbox checked → 10% brand tint
 *   - .is-saving    → editing.saving → 70% opacity
 *   - .is-error     → editing.error  → 2px danger left border
 *   - default       → hover row tint
 * Alpine `:class="{...}"` binds these modifiers based on its state. */

.tr-row {
  height: 2.25rem;
  font-size: var(--text-sm);
  color: var(--colour-text);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
              opacity var(--duration-fast) var(--ease-out);
}

.tr-row:hover {
  background: var(--colour-surface);
}

.tr-row.is-selected {
  background: color-mix(in oklch, var(--colour-brand) 10%, transparent);
}

.tr-row.is-saving {
  opacity: 0.7;
}

.tr-row.is-error {
  border-inline-start: 2px solid var(--colour-danger);
}

.tr-checkbox-cell {
  width: 2.5rem;
  padding-inline: var(--space-md);
}

.tr-checkbox {
  width: 0.875rem;
  height: 0.875rem;
  border-radius: var(--radius-sm);
  accent-color: var(--colour-brand);
  cursor: pointer;
}

.tr-cell {
  padding-inline: var(--space-md);
  /* Allow flex/grid-like shrink so ellipsis children respect column width */
  min-width: 0;
  overflow: hidden;
  vertical-align: middle;
}

.tr-cell.is-numeric {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

/* Identifying text columns take remaining width under table-layout: fixed. */
.tr-cell[data-col="title"],
.tr-cell[data-col="name"],
.tr-cell[data-col="label"],
.tr-cell[data-col="subject"],
.table-th[data-col="title"],
.table-th[data-col="name"],
.table-th[data-col="label"],
.table-th[data-col="subject"] {
  width: auto;
}

/* Temporal / compact columns: reserve readable width, never mid-label clip. */
.tr-cell[data-kind="date"],
.tr-cell[data-kind="datetime"],
.tr-cell[data-col$="_date"],
.tr-cell[data-col$="_at"],
.table-th[data-col$="_date"],
.table-th[data-col$="_at"],
.table-th[data-col="due_date"] {
  width: 8.5rem;
  white-space: nowrap;
}

/* Badge/enum columns: wide enough for icon + multi-word labels
 * ("In Progress", "High") without wrap (humanqa STATUS annotate). */
.tr-cell[data-kind="badge"],
.table-th[data-col="status"],
.table-th[data-col="priority"] {
  width: 9.5rem;
  min-width: 8.5rem;
  white-space: nowrap;
}

.tr-cell[data-kind="badge"] .badge {
  max-width: 100%;
}

.tr-cell[data-kind="ref"] {
  width: 7rem;
}

.tr-cell-display {
  display: block;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
  border-radius: var(--radius-sm);
  padding-inline: 0.25rem;
  margin-inline: -0.25rem;
  padding-block: 0.125rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: outline var(--duration-fast) var(--ease-out),
              background var(--duration-fast) var(--ease-out);
  /* double-tap must open the editor, not zoom (dblclick on touch) */
  touch-action: manipulation;
}

/*
 * Drillable rows (hx-get on <tr>): the row owns the bare click (#1592/#1598).
 * The edit display must NOT look/act like a text field — a hover outline +
 * cursor:text made the title box intercept perceived row interaction
 * (humanqa LATEST_TRACE: click title → no drill). Keep edit on dblclick only.
 */
.tr-row[hx-get] .tr-cell-display,
.tr-row[hx-get] .tr-cell-truncate {
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}

.tr-row[hx-get] .tr-cell-display:hover {
  outline: none;
  cursor: pointer;
  background: transparent;
}

/* Non-drill lists (no row hx-get): keep a light edit-target affordance. */
.tr-row:not([hx-get]) .tr-cell-display:hover {
  outline: 1px solid var(--colour-border);
  cursor: text;
}

/* Truncate spans fill the cell — no fixed 15rem cap (that forced early
   ellipsis while neighbour columns still had free space). */
.tr-cell-truncate {
  display: block;
  max-width: 100%;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Date/datetime (and common temporal col keys): never ellipsize calendar days
   (fleet/humanqa: "21 Jul 20…"). Covers display wrapper AND truncate child. */
.tr-cell[data-kind="date"] .tr-cell-truncate,
.tr-cell[data-kind="datetime"] .tr-cell-truncate,
.tr-cell[data-col$="_date"] .tr-cell-truncate,
.tr-cell[data-col$="_at"] .tr-cell-truncate,
.table-td[data-type="date"] .tr-cell-truncate,
.table-td[data-type="datetime"] .tr-cell-truncate,
.tr-cell[data-kind="date"] .tr-cell-display,
.tr-cell[data-kind="datetime"] .tr-cell-display,
.tr-cell[data-col$="_date"] .tr-cell-display,
.tr-cell[data-col$="_at"] .tr-cell-display,
.tr-cell:has([data-edit-kind="date"]) .tr-cell-display,
.tr-cell:has([data-edit-kind="date"]) .tr-cell-truncate {
  max-width: none;
  min-width: 7rem;
  overflow: visible;
  text-overflow: clip;
}

.tr-actions-cell {
  width: 11rem;
  min-width: 7rem;
  padding-inline: var(--space-sm);
  text-align: end;
  vertical-align: middle;
  white-space: nowrap;
  overflow: visible;
}

/* Full-cell flex so chips sit under the right-aligned ACTIONS header
 * (inline-flex + text-align was inconsistent — humanqa annotate). */
.tr-actions {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.25rem;
  width: 100%;
  box-sizing: border-box;
  /* Icon affordances stay hover-reveal; transition chips stay visible so
   * state moves are discoverable without hunting empty chrome. */
  opacity: 1;
}

/*
 * Resting state: icon-only chrome must leave the flow (not just opacity:0).
 * Opacity-hidden icons still occupied 1.75rem each → chips looked left-stacked
 * under a right-aligned ACTIONS title (humanqa perception mismatch).
 *
 * Exception: `.tr-peek-toggle` is disclosure chrome — always visible so
 * expand is discoverable without hunting hover (humanqa expand inconsistency).
 */
.tr-row:not(:hover):not(:focus-within)
  .tr-action:not(.tr-transition):not(.tr-peek-toggle) {
  opacity: 0;
  width: 0;
  min-width: 0;
  max-width: 0;
  height: 0;
  min-height: 0;
  padding: 0;
  margin: 0;
  border: 0;
  overflow: hidden;
  pointer-events: none;
}

.tr-row:hover .tr-action,
.tr-row:focus-within .tr-action {
  opacity: 1;
}

.tr-row:hover .tr-action:not(.tr-transition),
.tr-row:focus-within .tr-action:not(.tr-transition) {
  width: 1.75rem;
  min-width: 1.75rem;
  max-width: 1.75rem;
  height: 1.75rem;
  min-height: 1.75rem;
  pointer-events: auto;
}

/* Expand affordance: always on (not hover-collapsed). */
.tr-action.tr-peek-toggle {
  opacity: 1;
  width: 1.75rem;
  min-width: 1.75rem;
  max-width: 1.75rem;
  height: 1.75rem;
  min-height: 1.75rem;
  pointer-events: auto;
}

.tr-peek-chevron {
  transition: transform var(--duration-fast, 0.15s) var(--ease-out, ease);
}

.tr-peek-toggle[aria-expanded="true"] .tr-peek-chevron {
  transform: rotate(180deg);
}

/* Peek panel empty / denied body (auth miss, 403) — not a nested surface. */
.tr-peek-empty {
  padding: var(--space-md, 1rem);
  color: var(--colour-text-muted, #64748b);
  font-size: var(--text-sm, 0.875rem);
}

.tr-peek-panel .tr-peek-cell {
  padding: 0;
  background: color-mix(in oklch, var(--colour-bg, #f8fafc) 92%, var(--colour-border, #e2e8f0));
  border-block-end: 1px solid var(--colour-border, #e2e8f0);
}

.tr-peek-panel .tr-peek-cell > .region,
.tr-peek-panel .tr-peek-cell > .stack {
  padding: var(--space-md, 1rem);
}

/* Icon-only row chrome (view / edit / delete / peek). */
.tr-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border-radius: var(--radius-sm);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  font: inherit;
  line-height: 1;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              opacity var(--duration-fast) var(--ease-out);
  text-decoration: none;
}

/* State-machine transitions are *labels*, not icons — never force square. */
.tr-action.tr-transition {
  width: auto;
  min-width: 0;
  height: auto;
  min-height: 1.5rem;
  padding: 0.125rem 0.4rem;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  border: 1px solid var(--colour-border);
  background: var(--colour-surface, var(--colour-bg));
  color: var(--colour-text);
  border-radius: var(--radius-sm);
}

.tr-action.tr-transition:hover {
  border-color: var(--colour-brand);
  background: color-mix(in oklch, var(--colour-brand) 8%, transparent);
  color: var(--colour-text);
}

.tr-action:hover {
  background: color-mix(in oklch, var(--colour-bg) 50%, transparent);
  color: var(--colour-text);
}

.tr-action.is-destructive:hover {
  background: color-mix(in oklch, var(--colour-danger) 10%, transparent);
  color: var(--colour-danger);
}

.tr-action-icon {
  width: 0.875rem;
  height: 0.875rem;
  flex-shrink: 0;
}

/* Empty-state row spans the full table */
.tr-empty-cell {
  text-align: center;
  padding-block: var(--space-2xl);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.tr-empty-link {
  margin-inline-start: var(--space-sm);
  color: var(--colour-brand);
  text-decoration: none;
}

.tr-empty-link:hover {
  text-decoration: underline;
}

}
/* ── components/layout.css ── */
@layer components {
/* Layout — the Every-Layout vocabulary as CSS-only Hyperparts (L1).
 *
 * Intrinsic, media-query-free responsiveness: the sidebar wraps when its
 * CONTENT would get too narrow, the auto-grid packs columns to fit — the
 * same markup behaves in a page, a card, or a drawer. Variants ride
 * data-* attributes; the consumer-set widths are PUBLIC_CSS_PROPS
 * knobs (they follow the namespace prefix — see build.py).
 *
 * The gap scale is shared by stack / cluster / auto-grid via one
 * attribute contract (data-gap: xs|sm|md|lg|xl; unset = md). */

/* HYPERPART: stack */
/* L2: with the legacy --gap-* modifier scale retired (the emitters now
 * speak data-gap), the base rule lives here with the rest of the
 * Hyperpart — the sublayer-precedence trap it once had to dodge has no
 * competing rules left. */
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* HYPERPART: cluster */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
}

.cluster[data-align="start"] {
  align-items: flex-start;
}

.cluster[data-align="end"] {
  align-items: flex-end;
}

.cluster[data-align="baseline"] {
  align-items: baseline;
}

.cluster[data-align="stretch"] {
  align-items: stretch;
}

.cluster[data-justify="end"] {
  justify-content: flex-end;
}

.cluster[data-justify="between"] {
  justify-content: space-between;
}

.cluster[data-justify="center"] {
  justify-content: center;
}

/* The shared gap scale — one contract for the three gapped layouts. */
.stack[data-gap="none"],
.cluster[data-gap="none"],
.auto-grid[data-gap="none"] {
  gap: 0;
}

.stack[data-gap="xs"],
.cluster[data-gap="xs"],
.auto-grid[data-gap="xs"] {
  gap: var(--space-xs);
}

.stack[data-gap="sm"],
.cluster[data-gap="sm"],
.auto-grid[data-gap="sm"] {
  gap: var(--space-sm);
}

.stack[data-gap="md"],
.cluster[data-gap="md"],
.auto-grid[data-gap="md"] {
  gap: var(--space-md);
}

.stack[data-gap="lg"],
.cluster[data-gap="lg"],
.auto-grid[data-gap="lg"] {
  gap: var(--space-lg);
}

.stack[data-gap="xl"],
.cluster[data-gap="xl"],
.auto-grid[data-gap="xl"] {
  gap: var(--space-xl);
}

/* HYPERPART: sidebar-layout */
/* The Every-Layout sidebar: the side takes its preferred width, the
 * content grows aggressively but never below its comfortable minimum —
 * when the line can't hold that, the content wraps to a full row. The
 * breakpoint is the CONTENT'S minimum, not a viewport width. */
.sidebar-layout {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg);
}

.sidebar-layout > :first-child {
  flex-basis: var(--sidebar-width, 16rem);
  flex-grow: 1;
}

.sidebar-layout > :last-child {
  flex-basis: 0;
  flex-grow: 999;
  min-inline-size: var(--sidebar-content-min, 50%);
}

/* data-side="end": the side pane comes AFTER the content in source
 * order too (accessibility: keep DOM order = reading order), so the
 * grow/basis roles simply swap ends. */
.sidebar-layout[data-side="end"] > :first-child {
  flex-basis: 0;
  flex-grow: 999;
  min-inline-size: var(--sidebar-content-min, 50%);
}

.sidebar-layout[data-side="end"] > :last-child {
  flex-basis: var(--sidebar-width, 16rem);
  flex-grow: 1;
  min-inline-size: 0;
}

/* HYPERPART: auto-grid */
/* The inner min() stops overflow when the container is narrower than the
 * column minimum — the classic auto-fit footgun. */
.auto-grid {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(var(--grid-min, 14rem), 100%), 1fr)
  );
  gap: var(--space-md);
}

/* HYPERPART: center */
.center {
  box-sizing: content-box;
  margin-inline: auto;
  max-inline-size: 65ch;
}

.center[data-measure="wide"] {
  max-inline-size: 90ch;
}

.center[data-measure="full"] {
  max-inline-size: none;
}

}
/* ── components/app-shell.css ── */
@layer components {
/* HYPERPART: app-shell */
/* app-shell — the modern SaaS/admin application shell: persistent left
 * navigation, optional sticky top bar, routed main workspace, and a
 * responsive/collapsible sidebar. Promoted VERBATIM from Dazzle's
 * fragments.css (2026-07-06 directive: all core UX expression from HM);
 * the emitted contract (`app-shell` + `data-sidebar` state on the
 * root) predates the move and is unchanged.
 *
 * NB this is a COMPONENT, not a layout primitive — the 64rem media query
 * is deliberate: "persistent-collapsible on desktop, off-canvas overlay
 * on narrow" is viewport policy, not intrinsic wrapping. The layout
 * primitives inside the shell stay media-query-free.
 *
 * State contract: `data-sidebar="open|closed"` on `.app-shell`
 * (server-rendered from the `dz_sidebar` cookie; the app-shell.js
 * controller flips it and re-writes the cookie). */

/* ─────────────────────────── layouts/app_shell ──────────────────
 * Main application shell with collapsible left sidebar, sticky top
 * bar, and main content area. Sidebar open/collapsed state is
 * exposed on the shell root via `data-sidebar="open"` so CSS
 * selectors can drive both the slide transform AND the content
 * area's left padding offset. */

.app-shell {
  /* Root container — sidebarOpen attribute lives here. */
}

.app-content {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  transition: padding var(--duration-base) var(--ease-out);
}

@media (min-width: 64rem) {
  [data-sidebar="open"] .app-content {
    padding-inline-start: 16rem;  /* matches sidebar width */
  }
}

/* Top bar */
.topbar {
  position: sticky;
  inset-block-start: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  height: 3.5rem;
  padding-inline: var(--space-md);
  gap: var(--space-md);
  background: color-mix(in oklch, var(--colour-bg) 85%, transparent);
  backdrop-filter: blur(8px);
  border-block-end: 1px solid
    color-mix(in oklch, var(--colour-border) 50%, transparent);
  box-shadow: 0 1px 3px oklch(0% 0 0 / 0.04);
}

@media (min-width: 64rem) {
  .topbar {
    padding-inline: var(--space-lg);
  }
}

.topbar-leading,
.topbar-trailing {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

/* #1294 / #1602 — sidebar toggle (hamburger).
 * Two placements share data-sidebar-toggle:
 *   --chrome: topbar leading (mobile always; desktop when sidebar closed)
 *   --rail:   sidebar header (desktop when sidebar open only)
 * CSS shows exactly one visible control per state/viewport. */
.sidebar-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: none;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--colour-text);
  cursor: pointer;
  flex: none;
}

.sidebar-toggle:hover {
  background: var(--colour-surface);
}

/* Rail toggle: hidden by default (mobile + desktop-closed). */
.sidebar-toggle--rail {
  display: none;
}

@media (min-width: 64rem) {
  /* Desktop open: collapse control lives on the sidebar chrome, not title row. */
  [data-sidebar="open"] .sidebar-toggle--chrome {
    display: none;
  }
  [data-sidebar="open"] .sidebar-toggle--rail {
    display: inline-flex;
  }
}

.sidebar__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-block-end: 1px solid var(--colour-border);
}

.sidebar__header-main {
  flex: 1 1 0;
  min-width: 0;
}

.sidebar-toggle__icon,
.sidebar-toggle__icon::before,
.sidebar-toggle__icon::after {
  display: block;
  width: 1.125rem;
  height: 2px;
  background: currentColor;
  border-radius: 1px;
}

.sidebar-toggle__icon {
  position: relative;
}

.sidebar-toggle__icon::before,
.sidebar-toggle__icon::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
}

.sidebar-toggle__icon::before {
  inset-block-start: -6px;
}

.sidebar-toggle__icon::after {
  inset-block-start: 6px;
}

.topbar-title {
  flex: 1 1 0;
  min-width: 0;
}

.topbar-title-text {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}

.topbar-user {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 12.5rem;
  display: none;
}

@media (min-width: 40rem) {
  .topbar-user {
    display: inline;
  }
}

/* Generic icon button used in top bar + sidebar header */
.icon-button {
  width: 2rem;
  height: 2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.icon-button:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.icon-button > svg {
  width: 1.25rem;
  height: 1.25rem;
}

/* Mobile-only and desktop-only icon variants */
.icon-button.is-mobile-only { display: inline-flex; }
@media (min-width: 64rem) {
  .icon-button.is-mobile-only { display: none; }
}

.icon-button.is-desktop-only { display: none; }
@media (min-width: 64rem) {
  .icon-button.is-desktop-only { display: inline-flex; }
}

/* Main content area */
.app-main {
  flex: 1 1 0;
  padding: var(--space-md);
}

/* Content measure — shell max-width from ux.measure / PageContext
 * (pick-a-measure.md). Attribute omitted for true full-bleed (full). */
.app-main[data-measure="product"] {
  max-width: var(--dz-content-measure);
  width: 100%;
  margin-inline: auto;
  box-sizing: border-box;
}

.app-main[data-measure="wide"] {
  max-width: var(--dz-content-measure-wide);
  width: 100%;
  margin-inline: auto;
  box-sizing: border-box;
}

.app-main[data-measure="app"] {
  max-width: min(100%, var(--dz-content-measure-app));
  width: 100%;
  margin-inline: auto;
  box-sizing: border-box;
}

@media (min-width: 64rem) {
  .app-main {
    padding: var(--space-lg);
  }
}

.page-purpose {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  margin-block: 0.25rem var(--space-md);
}

/* Mobile backdrop overlay */
.app-backdrop {
  position: fixed;
  inset: 0;
  background: oklch(0% 0 0 / 0.4);
  z-index: 30;
}

@media (min-width: 64rem) {
  .app-backdrop {
    display: none !important;
  }
}

/* Sidebar
 *
 * Dual-lock persona nav is ``nav.sidebar > details.nav-group…`` with no
 * ``.sidebar-nav`` wrapper — overflow must live on ``.sidebar`` itself
 * so long sidebars scroll. ``.sidebar-nav`` keeps overflow for legacy
 * brand+nav+footer layouts. Sticky brand/header keeps identity visible while
 * scrolling (#1596).
 */
.sidebar {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  z-index: 40;
  width: 16rem;
  background: var(--colour-surface);
  border-inline-end: 1px solid var(--colour-border);
  display: flex;
  flex-direction: column;
  transform: translateX(-100%);
  transition: transform var(--duration-base) var(--ease-out);
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
}

[data-sidebar="open"] .sidebar {
  transform: translateX(0);
}

.sidebar-brand,
.sidebar__header {
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: var(--colour-surface);
}

.sidebar-brand {
  padding: var(--space-md);
  border-block-end: 1px solid var(--colour-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.sidebar-brand-text {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin: 0;
}

.sidebar-nav {
  flex: 1 1 0;
  overflow-y: auto;
}

.sidebar-nav-list {
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  list-style: none;
  margin: 0;
}

.sidebar-nav-link {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-inline: var(--space-md);
  padding-block: 0.375rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.sidebar-nav-link:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

/* Active link — keys off aria-current="page" so the visual state
 * and the accessibility attribute share one source of truth. */
.sidebar-nav-link[aria-current="page"] {
  background: var(--colour-bg);
  color: var(--colour-text);
  font-weight: var(--weight-medium);
}

.sidebar-nav-icon {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}

.sidebar-nav-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Collapsible nav group */
.sidebar-nav-group-summary {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-inline: var(--space-md);
  padding-block: 0.375rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  cursor: pointer;
  list-style: none;
  transition: background var(--duration-fast) var(--ease-out);
}

.sidebar-nav-group-summary::-webkit-details-marker {
  display: none;
}

.sidebar-nav-group-summary:hover {
  background: var(--colour-bg);
}

.sidebar-nav-group-children {
  padding-inline-start: var(--space-md);
  margin-block-start: 0.125rem;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  list-style: none;
}

/* Sidebar footer */
.sidebar-footer {
  margin-block-start: auto;
  border-block-start: 1px solid var(--colour-border);
}

.sidebar-user-block {
  padding: 0.75rem;
  padding-block-end: 0.25rem;
}

.sidebar-user-name {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding-inline: var(--space-md);
}

.sidebar-action-row {
  margin-block-start: 0.25rem;
}

.sidebar-action-button {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  padding-inline: var(--space-md);
  padding-block: 0.375rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.sidebar-action-button:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.sidebar-action-button > svg {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}

.sidebar-darkmode {
  padding: 0.75rem;
  padding-block-start: 0.25rem;
  display: none;
}

@media (min-width: 64rem) {
  .sidebar-darkmode {
    display: block;
  }
}

}
/* ── components/workspace-shell.css ── */
@layer components {
/* Internal workspace chrome (not a standalone gallery Hyperpart — no HYPERPART
 * marker, matching the other migrated chrome components detail/fragments/etc.). */
/* Moved from Dazzle dashboard.css (HMC-007c) — the workspace composition
 * shell: outer padding + heading row + primary actions, context selector,
 * and the edit toolbar (Reset/Save with the 5 saveState values). Byte-faithful,
 * already HM-tokenised; the spin keyframe it uses lives in HM base. */

/* ─────────────────────────── outer shell ────────────────────────
 * Padded root for the workspace; no max-width constraint — caller
 * provides the layout container. */

.workspace {
  padding: var(--space-md);
}

.workspace-heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  margin-block-end: var(--space-md);
  /* Heading wraps when title + actions exceed the row — actions
   * fall under the title rather than bleeding past <main>'s right
   * edge (#985). */
  flex-wrap: wrap;
}

.workspace-title {
  /* #983 — canonical app page-title scale. Was hard-coded to
   * `--text-lg`; promoting to the named token unifies workspace
   * titles with list-page titles + leaves the visual size
   * unchanged (token resolves to --text-lg). */
  font-size: var(--dz-heading-app-page-title);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  margin: 0;
}

.workspace-primary-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  /* #985: workspaces with many entities surface a long row of
   * "New <Entity>" buttons. Without flex-wrap the row would bleed
   * past <main>'s right edge (5 × 200px buttons + gaps overflows a
   * 1024px content column). flex-shrink:0 is intentionally dropped:
   * shrinking would squeeze button labels, wrapping is cleaner.
   * justify-content:flex-end keeps wrapped rows right-aligned to
   * match the heading's space-between layout. */
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* Workspace-level primary action — same shape as .button-primary
 * but slightly taller (h-2 instead of h-1.75) to match the heading. */
.workspace-action {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  text-decoration: none;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.workspace-action:hover {
  opacity: 0.9;
}

/* Suppress the default <summary> disclosure triangle cross-browser. */
/* ─────────────────────────── context selector ───────────────────
 * Workspace-level context filter. Just a label + select. */

.workspace-context {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-block-end: var(--space-md);
}

.workspace-context-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.workspace-context-select {
  box-sizing: border-box;
  -webkit-appearance: none;
  appearance: none;
  height: 2rem;
  line-height: 1.4;
  padding-block: 0.25rem;
  padding-inline: var(--space-sm);
  padding-inline-end: 1.5rem;
  font-size: var(--text-sm);
  color: var(--colour-text);
  background-color: var(--colour-surface);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.4rem center;
  background-size: 0.75rem 0.75rem;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
}

.workspace-context-select:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
  border-color: var(--colour-brand);
}

/* ─────────────────────────── edit toolbar ───────────────────────
 * Right-aligned row above the grid: Reset + Save buttons. Save
 * reflects saveState (clean/dirty/saving/saved/error) via a
 * data-save-state attribute the template binds with x-bind. */

.workspace-toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin-block-end: var(--space-md);
}

.workspace-toolbar-spacer {
  flex: 1 1 0;
}

.workspace-reset {
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.workspace-reset:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.workspace-save {
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  border: 1px solid transparent;
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
}

/* Save-state span visibility (Tier F4e: replaces the Alpine
 * x-cloak/x-show pair) — the button's data-save-state selects
 * exactly one label span. */
.workspace-save [data-when] {
  display: none;
}

.workspace-save[data-save-state="clean"] [data-when="clean"],
.workspace-save[data-save-state="dirty"] [data-when="dirty"],
.workspace-save[data-save-state="saving"] [data-when="saving"],
.workspace-save[data-save-state="saved"] [data-when="saved"],
.workspace-save[data-save-state="error"] [data-when="error"] {
  display: inline-flex;
}

.workspace-save[data-save-state="clean"] {
  color: var(--colour-text-muted);
  cursor: default;
}

.workspace-save[data-save-state="dirty"] {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
}

.workspace-save[data-save-state="saving"] {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  opacity: 0.7;
}

.workspace-save[data-save-state="saved"] {
  background: var(--success-600); /* fixed step — white text on fill */
  color: white;
}

.workspace-save[data-save-state="error"] {
  border-color: var(--colour-danger);
  color: var(--colour-danger);
}

.workspace-save-busy {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

.workspace-save-busy-icon {
  width: 0.75rem;
  height: 0.75rem;
  animation: spin 1s linear infinite;
}

}
/* ── components/status-list.css ── */
@layer components {
/* HYPERPART: status-list */
/* Promoted VERBATIM from Dazzle regions.css (W1, fleet convergence).
 * The Dazzle emitter contract predates the move and is unchanged. */

/* ─────────────────────────── status_list region ─────────────────
 * Vertical list of icon + title + caption + state pill rows.
 * Pill colour keys off data-state on the row (positive /
 * warning / destructive / accent / neutral). Pill is omitted for
 * neutral entries — the row reads as plain info rather than a
 * status row. */

.status-list {
  display: flex;
  flex-direction: column;
  list-style: none;
  margin: 0;
  padding-inline-start: 0;
}

.status-list > li + li {
  border-block-start: 1px solid var(--colour-border);
}

.status-list-entry {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  padding-block: var(--space-md);
}

.status-list-entry:first-child {
  padding-block-start: 0.25rem;
}

.status-list-entry:last-child {
  padding-block-end: 0.25rem;
}

.status-list-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  margin-block-start: 0.125rem;
  flex-shrink: 0;
}

.status-list-icon-spacer {
  width: 1.25rem;
  flex-shrink: 0;
}

.status-list-text {
  flex: 1 1 0;
  min-width: 0;
}

.status-list-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  color: var(--colour-text);
}

.status-list-caption {
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--colour-text-muted);
  margin-block-start: 0.125rem;
}

.status-list-pill {
  flex-shrink: 0;
  padding-inline: var(--space-sm);
  padding-block: 0.125rem;
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* State tints (moved from Dazzle tones.css with the component). */
.status-list-entry[data-state="positive"] .status-list-pill {
  background: var(--colour-success-soft);
  color: var(--colour-success);
}
.status-list-entry[data-state="warning"] .status-list-pill {
  background: var(--colour-warning-soft);
  color: var(--colour-warning);
}
.status-list-entry[data-state="destructive"] .status-list-pill {
  background: var(--colour-danger-soft);
  color: var(--colour-danger);
}
.status-list-entry[data-state="accent"] .status-list-pill {
  background: var(--colour-brand-soft);
  color: var(--colour-brand);
}

/* Icon colour matches the state. Neutral entries fall back to the
 * default muted-foreground. */
.status-list-entry[data-state="positive"] .status-list-icon {
  color: var(--colour-success);
}
.status-list-entry[data-state="warning"] .status-list-icon {
  color: var(--colour-warning);
}
.status-list-entry[data-state="destructive"] .status-list-icon {
  color: var(--colour-danger);
}
.status-list-entry[data-state="accent"] .status-list-icon {
  color: var(--colour-brand);
}
.status-list-entry[data-state="neutral"] .status-list-icon,
.status-list-entry:not([data-state]) .status-list-icon {
  color: var(--colour-text-muted);
}

}
/* ── components/action-grid.css ── */
@layer components {
/* HYPERPART: action-grid */
/* Promoted VERBATIM from Dazzle regions.css (W1, fleet convergence).
 * The Dazzle emitter contract predates the move and is unchanged. */

/* ─────────────────────────── action_grid region ─────────────────
 * Responsive 1/2/3-col grid of CTA cards. Each card carries a
 * label, optional icon, optional count badge — and a tone token
 * (positive / warning / destructive / accent / neutral) routed
 * through tones.css via data-tone / data-tone-badge
 * (unchanged). The hover/cursor affordance is gated on the
 * <a>-vs-<div> wrapper choice in the template; the .action-card
 * rule covers the shared shape. */

.action-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
  margin-block-start: var(--space-sm);
}

@media (min-width: 40rem) {
  .action-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .action-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.action-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  text-decoration: none;
  color: var(--colour-text);
  transition: background var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

a.action-card {
  cursor: pointer;
}

a.action-card:hover {
  background: var(--colour-bg);
}

.action-card-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.action-card-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--colour-text-muted);
}

.action-card-icon-spacer {
  width: 1.25rem;
  /* height too — a label-only card (no icon, no count) otherwise
   * collapses its row to 0 height (the zero-paint gate's catch; the
   * bug shipped in Dazzle for as long as the spacer existed). */
  height: 1.25rem;
  display: inline-block;
}

.action-card-count {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  padding-inline: var(--space-sm);
  padding-block: 0.125rem;
  border-radius: var(--radius-full);
}

.action-card-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  line-height: var(--leading-tight);
}

/* Tone tints (moved from Dazzle tones.css with the component —
 * the tinting IS the component's point). */
/* v0.61.74 (#906 cleanup): action_grid was the second template using
 * the buried-dynamic-class pattern. Card surface tint + count-badge
 * tint both route via `data-tone` / `data-tone-badge`.
 * Hardcoded `hsl(145,55%,45%)` / `hsl(40,90%,55%)` literals in the
 * template are gone — both now use design-system slots so theming
 * applies. */

.action-card[data-tone="positive"] {
  border-color: var(--colour-success-soft);
  background: var(--colour-success-soft);
}
.action-card[data-tone="positive"]:hover {
  background: var(--colour-success-soft);
}
.action-card[data-tone="warning"] {
  border-color: var(--colour-warning-soft);
  background: var(--colour-warning-soft);
}
.action-card[data-tone="warning"]:hover {
  background: var(--colour-warning-soft);
}
.action-card[data-tone="destructive"] {
  border-color: var(--colour-danger-soft);
  background: var(--colour-danger-soft);
}
.action-card[data-tone="destructive"]:hover {
  background: var(--colour-danger-soft);
}
.action-card[data-tone="accent"] {
  border-color: var(--colour-brand-soft);
  background: var(--colour-brand-soft);
}
.action-card[data-tone="accent"]:hover {
  background: var(--colour-brand-soft);
}
.action-card[data-tone="neutral"],
.action-card:not([data-tone]) {
  border-color: var(--colour-border);
  background: var(--colour-surface);
}
.action-card[data-tone="neutral"]:hover,
.action-card:not([data-tone]):hover {
  background: color-mix(in oklab, var(--colour-surface-muted) 40%, transparent);
}

/* Count-badge tints — same five tones, slightly different opacities
 * so the badge reads as denser than the card surface. */
.action-card-count[data-tone-badge="positive"] {
  background: var(--colour-success-soft);
  color: var(--colour-success);
}
.action-card-count[data-tone-badge="warning"] {
  background: var(--colour-warning-soft);
  color: var(--colour-warning);
}
.action-card-count[data-tone-badge="destructive"] {
  background: var(--colour-danger-soft);
  color: var(--colour-danger);
}
.action-card-count[data-tone-badge="accent"] {
  background: var(--colour-brand-soft);
  /* the scheme-aware TEXT token (brand-600 light / brand-300 dark) —
   * raw --colour-brand fails contrast on the dark soft tint (axe). */
  color: var(--colour-brand-text);
}
.action-card-count[data-tone-badge="neutral"],
.action-card-count:not([data-tone-badge]) {
  background: var(--colour-surface-muted);
  color: var(--colour-text-muted);
}

/* ── Metric delta arrow colour (#884 / sibling #906 cleanup) ──────── */
/* v0.61.74: the period-over-period delta arrow used to compute its
 * tone via a dynamic Tailwind class string. Now driven by
 * `data-delta-tone` matching the standard tone palette. The
 * hardcoded `hsl(142 76% 36%)` literal is gone — `--success` is the
 * canonical green slot. */

.metric-delta[data-delta-tone="positive"] {
  color: var(--colour-success);
}
.metric-delta[data-delta-tone="destructive"] {
  color: var(--colour-danger);
}
.metric-delta[data-delta-tone="neutral"],
.metric-delta:not([data-delta-tone]) {
  color: var(--colour-text-muted);
}

/* ── Status list pill + icon colour (#3 / roadmap #3) ─────────────── */

/* Pill backgrounds (rendered only for non-neutral states; neutral
 * entries omit the pill via template branch). */

}
/* ── components/queue.css ── */
@layer components {
/* HYPERPART: queue */
/* Promoted VERBATIM from Dazzle regions.css (W1, fleet convergence).
 * The Dazzle emitter contract predates the move and is unchanged. */

/* ─────────────────────────── queue region ───────────────────────
 * Review queue: count chip + metrics row + filter bar + rows with
 * inline transition buttons. Each row has plain padding (no full
 * border — dashboard slot owns chrome) and a hover tint when no
 * attention accent is present. */

.queue-count-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  margin-block-end: var(--space-sm);
}

.queue-count {
  display: inline-flex;
  align-items: center;
  padding-inline: var(--space-sm);
  height: 1.25rem;
  border-radius: var(--radius-xs, 0.1875rem);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
}

.queue-metrics {
  display: flex;
  gap: var(--space-md);
  margin-block-end: var(--space-md);
}

.queue-metric {
  flex: 1 1 0;
  text-align: center;
  padding: var(--space-sm);
  border-radius: var(--radius-sm);
  background: color-mix(in oklch, var(--colour-bg) 60%, transparent);
  border: 1px solid var(--colour-border);
}

.queue-metric-value {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}

.queue-metric-label {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.queue-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-block-end: var(--space-sm);
}

.queue-filter-select {
  /* Native <select> clip: base.css `select { padding: space-sm }` plus
   * a 1.75rem height left ~10px of content (CyFuture letter-type /
   * status filters). WebKit also ignores min-height on <select>.
   * Same #930 treatment as select.form-input: flatten UA chrome,
   * pin height, keep a chevron. Combobox is the form hyperpart;
   * filter bars stay native for hx-change region reload. */
  box-sizing: border-box;
  -webkit-appearance: none;
  appearance: none;
  height: 2rem;
  line-height: 1.4;
  padding-block: 0.25rem;
  padding-inline: var(--space-sm);
  padding-inline-end: 1.5rem;
  border-radius: var(--radius-sm);
  background-color: var(--colour-surface);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.4rem center;
  background-size: 0.75rem 0.75rem;
  border: 1px solid var(--colour-border);
  font-size: var(--text-xs);
  color: var(--colour-text);
}

.queue-filter-select:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
  border-color: var(--colour-brand);
}

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

.queue-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-block: var(--space-sm);
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--ease-out);
}

/* Hover tint only when no attention accent — attention accents
 * carry their own tint via attention_classes macro. */
.queue-row:not([data-attn]):hover {
  background: var(--colour-bg);
}

.queue-row-main {
  flex: 1 1 0;
  min-width: 0;
}

.queue-row-main.is-clickable {
  cursor: pointer;
}

.queue-row-headline {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.queue-row-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.queue-row-attn {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: 0.125rem;
}

/* #1626 R1 — space meta chips so "Amount: xCurrency: y" never glues. */
.queue-row-meta-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem var(--space-sm);
  margin-block-start: 0.125rem;
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.queue-row-meta,
.queue-row-date {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.queue-row-meta-sep {
  color: var(--colour-text-muted);
  opacity: 0.55;
  user-select: none;
}

/* #1626 R5 / P0-8 — palette chips on brand desks (list + queue). */
.color-swatch {
  display: inline-block;
  width: 0.9rem;
  height: 0.9rem;
  border-radius: var(--radius-sm, 4px);
  border: 1px solid var(--colour-border, #e5e7eb);
  vertical-align: middle;
  flex-shrink: 0;
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 6%);
}

.color-swatch-hex {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs, 0.75rem);
  color: var(--colour-text-muted);
  letter-spacing: 0.01em;
}

/* Goal B media — logo/preview thumbs on brand/asset queues. */
.media-thumb {
  display: inline-block;
  width: 2.5rem;
  height: 2.5rem;
  object-fit: cover;
  border-radius: var(--radius-sm, 4px);
  border: 1px solid var(--colour-border, #e5e7eb);
  vertical-align: middle;
  background: var(--colour-surface-muted, #f3f4f6);
  flex-shrink: 0;
}

.queue-row-actions {
  display: flex;
  gap: 0.25rem;
  flex-shrink: 0;
}

.queue-action {
  height: 1.75rem;
  padding-inline: 0.625rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  background: transparent;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  transition: background var(--duration-fast) var(--ease-out);
}

.queue-action:hover {
  background: var(--colour-bg);
}

.queue-overflow {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

.queue-empty {
  text-align: center;
  padding-block: var(--space-md);
}

}
/* ── components/kanban.css ── */
@layer components {
/* HYPERPART: kanban */
/* Promoted VERBATIM from Dazzle regions.css (W2, fleet convergence).
 * The Dazzle emitter contract predates the move and is unchanged. */

/* ─────────────────────────── kanban region ──────────────────────
 * Horizontal column board. Each column is a fixed-width tinted
 * surface with a header (status badge + count) and a vertically-
 * scrolling card stack. Cards are tinted surfaces; .is-clickable
 * gives them shadow/border affordance when wired to action_url. */

.kanban-board {
  display: flex;
  gap: var(--space-md);
  overflow-x: auto;
  padding-block-end: var(--space-sm);
  min-height: 12.5rem;
}

.kanban-column {
  flex-shrink: 0;
  width: 16.25rem;
  min-width: 16.25rem;
  background: color-mix(in oklch, var(--colour-bg) 60%, transparent);
  border-radius: var(--radius-md);
  padding: var(--space-sm);
}

.kanban-column-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-block-end: var(--space-sm);
  padding-inline: 0.25rem;
}

.kanban-column-count {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.kanban-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  max-height: 60vh;
  overflow-y: auto;
}

.kanban-card {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 1px 3px oklch(0% 0 0 / 0.04);
}

/* Linear-class rearrange: only when SSR stamped draggable / drop targets */
.kanban-card[draggable="true"] {
  cursor: grab;
  touch-action: none;
}

.kanban-card[draggable="true"]:active,
.kanban-card.is-dragging {
  cursor: grabbing;
  opacity: 0.35;
  box-shadow: none;
}

/* In-transit snapshot (setDragImage clone) — solid, not washed out. */
.kanban-card.is-drag-ghost {
  opacity: 1;
  box-shadow: 0 12px 28px oklch(0% 0 0 / 0.18);
  transform: rotate(1.5deg);
  cursor: grabbing;
}

.kanban-card.is-moving {
  opacity: 0.5;
  pointer-events: none;
}

.kanban-board.is-busy {
  cursor: progress;
}

/*
 * Drop affordance while rearranging (Linear/Trello grammar):
 * - Whole column is the hit surface (not “land on a card”).
 * - Populated stacks grow to fill the column so empty space under cards
 *   is still a valid target.
 * - Cards ignore pointer events so dragover always reaches the stack/column.
 */
.kanban-board.is-dragging .kanban-column {
  display: flex;
  flex-direction: column;
  min-height: 12rem;
  transition: background var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out),
    outline-color var(--duration-fast) var(--ease-out);
}

.kanban-board.is-dragging .kanban-stack[data-to-state] {
  flex: 1 1 auto;
  min-height: 6rem;
  outline: 1px dashed color-mix(in oklch, var(--colour-border) 80%, transparent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
  padding: var(--space-xs);
  transition: background var(--duration-fast) var(--ease-out),
    outline-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}

/* Cards must not steal the drop hit-test from the stack. */
.kanban-board.is-dragging .kanban-card:not(.is-dragging) {
  pointer-events: none;
}

/* Column-level glow — clearer than a tight stack outline between cards. */
.kanban-column.is-drop-column {
  background: color-mix(in oklch, var(--colour-brand) 10%, var(--colour-bg));
  box-shadow: 0 0 0 2px var(--colour-brand);
}

.kanban-column.is-drop-column-deny {
  background: color-mix(
    in oklch,
    var(--colour-destructive, oklch(55% 0.2 25)) 8%,
    var(--colour-bg)
  );
  box-shadow: 0 0 0 2px var(--colour-destructive, oklch(55% 0.2 25));
}

.kanban-stack.is-drop-target {
  outline-color: var(--colour-brand);
  outline-width: 2px;
  outline-style: solid;
  background: color-mix(in oklch, var(--colour-brand) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--colour-brand) 40%, transparent);
}

.kanban-stack.is-drop-deny {
  outline-color: var(--colour-destructive, oklch(55% 0.2 25));
  outline-width: 2px;
  outline-style: solid;
  background: color-mix(
    in oklch,
    var(--colour-destructive, oklch(55% 0.2 25)) 10%,
    transparent
  );
}

/* Placeholder stripe at the end of a stack when appending (no .is-drop-before). */
.kanban-stack.is-drop-target:not(:has(.is-drop-before))::after {
  content: "";
  display: block;
  flex: 0 0 2.5rem;
  min-height: 2.5rem;
  margin-block-start: var(--space-xs);
  border-radius: var(--radius-sm);
  border: 1px dashed color-mix(in oklch, var(--colour-brand) 55%, var(--colour-border));
  background: color-mix(in oklch, var(--colour-brand) 6%, transparent);
  pointer-events: none;
}

/* Insert-before line: between cards for in-column (and cross-column) reorder. */
.kanban-card.is-drop-before {
  box-shadow: 0 -3px 0 0 var(--colour-brand);
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.kanban-card-move {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin-block-start: var(--space-xs);
}

.kanban-card-move label {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.kanban-card-move select {
  font-size: var(--text-xs);
  max-width: 100%;
}

.kanban-announce {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.kanban-card.is-clickable {
  cursor: pointer;
  transition: box-shadow var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.kanban-card.is-clickable:hover {
  box-shadow: 0 2px 8px oklch(0% 0 0 / 0.06);
  border-color: color-mix(in oklch, var(--colour-brand) 50%, var(--colour-border));
}

.kanban-card-body {
  padding: var(--space-sm);
}

.kanban-card-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  margin-block-end: 0.25rem;
}

.kanban-card-field {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.kanban-card-attn {
  font-size: var(--text-xs);
  margin-block-start: 0.25rem;
}

.kanban-card-attn[data-attn="critical"] { color: var(--colour-danger); }
.kanban-card-attn[data-attn="warning"]  { color: var(--colour-warning); }
.kanban-card-attn[data-attn="notice"]   { color: var(--colour-brand); }

.kanban-empty {
  font-size: var(--text-xs);
  /* full muted token — the 60%-transparent mix failed WCAG contrast
   * (axe gate on first gallery exposure; shipped that way in Dazzle). */
  color: var(--colour-text-muted);
  text-align: center;
  padding-block: var(--space-md);
}

.kanban-overflow {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-block-start: var(--space-sm);
}

.kanban-overflow-text {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.kanban-load-all {
  height: 1.5rem;
  padding-inline: var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.kanban-load-all:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

}
/* ── components/timeline.css ── */
@layer components {
/* HYPERPART: timeline */
/* Promoted VERBATIM from Dazzle regions.css (W2, fleet convergence).
 * The Dazzle emitter contract predates the move and is unchanged. */

/* ─────────────────────────── timeline region ────────────────────
 * Vertical timeline (similar shape to activity_feed). Bullet
 * marker on the left rail, date column, content pad. Shares the
 * .activity-* dot/bullet pattern but renders item-shape rather
 * than bare message. */

.timeline-list {
  position: relative;
  padding-inline-start: var(--space-md);
  border-inline-start: 1px solid var(--colour-border);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  list-style: none;
  margin: 0;
}

.timeline-item {
  position: relative;
}

.timeline-bullet-wrap {
  /* Size shared with .timeline-bullet so the rail centre calc tracks the box. */
  --timeline-bullet-size: 0.75rem;
  position: absolute;
  /* Centre on the list border-inline-start rail: back across padding + half
   * border + half bullet. Hard-coded -1.3125rem predated --space-md: 0.75rem
   * and sat ~2.5px left of the line. */
  inset-inline-start: calc(
    -1 * var(--space-md) - 0.5px - (var(--timeline-bullet-size) / 2)
  );
  top: 0.375rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--timeline-bullet-size);
  height: var(--timeline-bullet-size);
}

.timeline-bullet {
  width: var(--timeline-bullet-size, 0.75rem);
  height: var(--timeline-bullet-size, 0.75rem);
}

.timeline-row {
  display: flex;
  gap: var(--space-md);
}

.timeline-date {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  min-width: 5rem;
  padding-block-start: 0.25rem;
}

.timeline-content {
  flex: 1 1 0;
  padding-inline: var(--space-sm);
  padding-block: var(--space-xs);
  border-radius: var(--radius-sm);
}

.timeline-content.is-clickable {
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.timeline-content.is-clickable:hover {
  background: var(--colour-bg);
}

.timeline-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  margin: 0;
}

.timeline-field {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin: 0;
}

.timeline-overflow {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

/* attention_accent macro (moved with its consumer — the timeline's
 * bullets carry attn-bullet + attn-tone-*; kanban's card-attn
 * text also keys the attn contract). */
/* ─────────────────────────── attention_accent macro ─────────────
 * Cross-region attention/severity tier visual treatment. The macro
 * `attention_classes` returns a class string the caller pastes into
 * the element's `class=` attribute; this CSS resolves the (style ×
 * tone) matrix.
 *
 *   - border  — 4px inline-start accent (grid)
 *   - tint    — 0.06–0.08 background tint (list)
 *   - both    — border + 0.04 background tint (queue)
 *   - bullet  — text colour for the timeline dot (timeline)
 *
 * Tone semantics:
 *   - critical → danger
 *   - warning  → warning
 *   - notice   → brand
 *   - default  → brand (bullet only — falls back when no attention)
 */

.attn-border {
  border-inline-start-width: 4px;
  border-inline-start-style: solid;
}

.attn-border.attn-tone-critical { border-inline-start-color: var(--colour-danger); }
.attn-border.attn-tone-warning  { border-inline-start-color: var(--colour-warning); }
.attn-border.attn-tone-notice   { border-inline-start-color: var(--colour-brand); }

.attn-tint.attn-tone-critical {
  background: color-mix(in oklch, var(--colour-danger) 8%, transparent);
}
.attn-tint.attn-tone-warning {
  background: color-mix(in oklch, var(--colour-warning) 8%, transparent);
}
.attn-tint.attn-tone-notice {
  background: color-mix(in oklch, var(--colour-brand) 6%, transparent);
}

.attn-both {
  border-inline-start-width: 4px;
  border-inline-start-style: solid;
}

.attn-both.attn-tone-critical {
  border-inline-start-color: var(--colour-danger);
  background: color-mix(in oklch, var(--colour-danger) 4%, transparent);
}

.attn-both.attn-tone-warning {
  border-inline-start-color: var(--colour-warning);
  background: color-mix(in oklch, var(--colour-warning) 4%, transparent);
}

.attn-both.attn-tone-notice {
  border-inline-start-color: var(--colour-brand);
  background: color-mix(in oklch, var(--colour-brand) 4%, transparent);
}

.attn-bullet.attn-tone-critical { color: var(--colour-danger); }
.attn-bullet.attn-tone-warning  { color: var(--colour-warning); }
.attn-bullet.attn-tone-notice   { color: var(--colour-brand); }
.attn-bullet.attn-tone-default  { color: var(--colour-brand); }

}
/* ── components/activity-feed.css ── */
@layer components {
/* HYPERPART: activity-feed */
/* Promoted VERBATIM from Dazzle regions.css (W2, fleet convergence).
 * The Dazzle emitter contract predates the move and is unchanged. */

/* ─────────────────────────── activity_feed region ───────────────
 * Vertical timeline: dot on a left rail, time on the left of each
 * row, message bubble on the right. Bubble gets a hover-shadow
 * when wired to an action_url (HTMX-driven detail drawer open). */

.activity-feed {
  position: relative;
  padding-inline-start: var(--space-md);
  border-inline-start: 1px solid var(--colour-border);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  list-style: none;
  margin: 0;
}

.activity-row {
  position: relative;
}

.activity-dot {
  /* Same rail geometry as .timeline-bullet-wrap (padding + 1px border). */
  --activity-dot-size: 0.75rem;
  position: absolute;
  inset-inline-start: calc(
    -1 * var(--space-md) - 0.5px - (var(--activity-dot-size) / 2)
  );
  top: 0.375rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--activity-dot-size);
  height: var(--activity-dot-size);
}

.activity-dot > svg {
  width: var(--activity-dot-size, 0.75rem);
  height: var(--activity-dot-size, 0.75rem);
  display: block;
  color: var(--colour-brand);
}

.activity-row-inner {
  display: flex;
  gap: var(--space-md);
}

.activity-time {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  min-width: 5rem;
  padding-block-start: 0.25rem;
}

/* Bubble — no border (the dashboard slot already owns card chrome,
 * adding a border re-creates a card-within-a-card per #852). Subtle
 * background tint provides visual grouping. */
.activity-bubble {
  flex: 1 1 0;
  padding-inline: var(--space-sm);
  padding-block: var(--space-xs);
  border-radius: var(--radius-sm);
  background: color-mix(in oklch, var(--colour-bg) 70%, transparent);
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.activity-bubble.is-clickable {
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.activity-bubble.is-clickable:hover {
  background: var(--colour-bg);
}

.activity-actor {
  font-weight: var(--weight-medium);
}

.activity-empty {
  text-align: center;
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  padding-block: var(--space-2xl);
}

}
/* ── components/related-tables.css ── */
@layer components {
/* HYPERPART: related-tables */
/* Promoted VERBATIM from Dazzle fragments.css (W2, fleet convergence).
 * The Dazzle emitter contract predates the move and is unchanged. */

/* ─────────────────────────── related-* fragments ────────────────
 * Shared chrome across related_status_cards, related_table_group,
 * related_file_list — each renders one or more "tabs" of related
 * entities. Group label, create button, and card/row hover all
 * share the same shape. */

.related-group {
  margin-block-end: var(--space-md);
}

.related-tab-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  margin-block-end: var(--space-sm);
}

.related-create-row {
  display: flex;
  justify-content: flex-end;
  margin-block-end: var(--space-sm);
}

.related-create-button {
  display: inline-flex;
  align-items: center;
  height: 1.75rem;
  padding-inline: 0.625rem;
  border-radius: var(--radius-sm);
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition: filter var(--duration-fast) var(--ease-out);
}

.related-create-button:hover {
  filter: brightness(1.1);
}

.related-empty {
  text-align: center;
  padding-block: var(--space-lg);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* status_cards — auto-fit so invoice id + amount stay readable (minmax
 * floor ~14rem). Fixed 2/3-col tracks in a narrow gallery column were
 * clipping primary amounts mid-value (coherence HMC-063). */
.related-status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-md);
}

.related-status-card {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  cursor: pointer;
  transition: box-shadow var(--duration-base) var(--ease-out);
}

.related-status-card:hover {
  box-shadow: 0 2px 8px oklch(0% 0 0 / 0.06);
}

.related-status-card-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
}

.related-status-card-text {
  min-width: 0;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.related-status-card-primary {
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  /* Prefer wrap over ellipsis on money/id — amounts must stay legible */
  white-space: normal;
  overflow: visible;
  margin: 0;
  line-height: 1.35;
}

.related-status-card-secondary {
  display: block;
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  white-space: normal;
  overflow: visible;
  margin: 0;
  line-height: 1.35;
}

.related-status-card-badge {
  flex-shrink: 0;
}

/* table_group — tab strip rides the HM tabs Hyperpart (Tier F4):
 * .tabs__list/.tabs__tab from tabs.css, driven by tabs.js.
 * The bespoke .related-tabs/.related-tab pill family (visually
 * the same underline idiom) was retired with the Alpine activeTab
 * island. Only the count chip stays related-specific. */
.related-tab-count {
  margin-inline-start: 0.25rem;
  display: inline-flex;
  align-items: center;
  padding-inline: 0.375rem;
  height: 1rem;
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.related-table-card {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  margin-block-start: var(--space-sm);
}

.related-table-create-row {
  display: flex;
  justify-content: flex-end;
  padding: var(--space-md);
  padding-block-end: 0;
}

.related-table-scroll {
  overflow-x: auto;
}

.related-table {
  width: 100%;
  border-collapse: collapse;
}

.related-table > thead > tr {
  background: color-mix(in oklch, var(--colour-bg) 70%, transparent);
  border-block-end: 1px solid var(--colour-border);
}

.related-table th {
  text-align: start;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  letter-spacing: -0.01em;
}

.related-table > tbody > tr {
  border-block-end: 1px solid var(--colour-border);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.related-table > tbody > tr:last-child {
  border-block-end: 0;
}

.related-table > tbody > tr:hover {
  background: var(--colour-bg);
}

.related-table td {
  font-size: var(--text-sm);
  color: var(--colour-text);
  padding-inline: var(--space-md);
  padding-block: 0.625rem;
}

.related-table-empty-cell {
  text-align: center;
  padding-block: var(--space-2xl);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* file_list — compact rows for documents/evidence */
.related-file-list {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.related-file-list > * + * {
  border-block-start: 1px solid var(--colour-border);
}

.related-file-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-inline: var(--space-md);
  padding-block: var(--space-md);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.related-file-row:hover {
  background: var(--colour-bg);
}

.related-file-icon {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  color: color-mix(in oklch, var(--colour-text-muted) 60%, transparent);
}

.related-file-text {
  flex: 1 1 0;
  min-width: 0;
}

.related-file-primary {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 0;
}

.related-file-secondary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 0;
}

.related-file-date {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  flex-shrink: 0;
}

}
/* ── components/metrics.css ── */
@layer components {
/* HYPERPART: metrics */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-a, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── metrics region ─────────────────────
 * Responsive 1/2/4-col grid of KPI tiles. Each tile is plain (no
 * border — the dashboard slot owns card chrome) with a soft
 * background tint, label / value, and optional delta row.
 * Per-tile tone tinting travels with the component (moved verbatim
 * from Dazzle tones.css, keyed off data-tone). */

.metrics-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
}

@media (min-width: 40rem) {
  .metrics-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .metrics-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.metric-tile {
  padding: var(--space-md);
  border-radius: var(--radius-sm);
  background: color-mix(in oklch, var(--colour-bg) 60%, transparent);
}

/* Tone tints (moved from Dazzle tones.css with the component, #894).
 * Keyed off data-tone; the [attr] selector out-specifies the base
 * rule above. neutral falls back to the base background. */
.metric-tile[data-tone="positive"] {
  background: color-mix(in oklab, var(--colour-success) 10%, transparent);
}
.metric-tile[data-tone="warning"] {
  background: color-mix(in oklab, var(--colour-warning) 10%, transparent);
}
.metric-tile[data-tone="destructive"] {
  background: color-mix(in oklab, var(--colour-danger) 10%, transparent);
}
.metric-tile[data-tone="accent"] {
  background: color-mix(in oklab, var(--colour-brand) 10%, transparent);
}

.metric-label {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  letter-spacing: -0.01em;
}

.metric-value {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin-block-start: 0.125rem;
  font-variant-numeric: tabular-nums;
}

.metric-delta {
  margin-block-start: var(--space-xs);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

}
/* ── components/sparkline.css ── */
@layer components {
/* HYPERPART: sparkline */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-a, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── sparkline region ───────────────────
 * Compact KPI tile: headline (value + bucket) stacked above a tiny
 * area glyph. Not a side-by-side KPI — the chart is the second row of
 * one coherent unit. SVG must FILL the host width (preserveAspectRatio
 * none on the markup); default "meet" letterboxes a 180×32 viewBox into
 * a wide short box and the glyph floats mid-card like a glitch. */

.sparkline-region {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-xs);
  min-width: 0;
}

.sparkline-headline {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  min-width: 0;
}

.sparkline-value {
  font-size: var(--text-2xl);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  line-height: 1;
}

.sparkline-bucket-label {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  font-family: var(--font-mono);
}

.sparkline-svg {
  display: block;
  width: 100%;
  height: 2rem;
  /* Stretch the fixed viewBox to the host — trend shape over aspect fidelity. */
  overflow: visible;
}

.sparkline-empty {
  text-align: center;
  padding-block: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

}
/* ── components/funnel.css ── */
@layer components {
/* HYPERPART: funnel */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-a, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── funnel_chart region ────────────────
 * Stacked proportional bars rendering ordered stages. Each stage
 * gets a width derived from `count / first_count` and a tone tint
 * via data-funnel-step (0..N) so the depth-by-position fade can
 * happen in CSS. Replaces the dynamic
 * `bg-[hsl(var(--primary)/{{ pct }})]` Tailwind arbitrary value
 * (which was JIT-invisible). */

.funnel-stages {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-block-start: var(--space-sm);
}

.funnel-stage-row {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.funnel-stage {
  border-radius: var(--radius-sm);
  padding-inline: var(--space-md);
  padding-block: 0.375rem;
  text-align: center;
  /* The step fade lives on the BAR SURFACE via --dz-funnel-fade, never on
   * element opacity — the promoted rules faded whole stages to 20%,
   * taking the TEXT with them (unreadable + a WCAG contrast failure the
   * axe gate caught on first gallery exposure). Text keeps full-contrast
   * scheme-aware ink on the mixed surface. */
  background: color-mix(
    in oklab,
    var(--colour-brand) calc(var(--dz-funnel-fade, 100) * 1%),
    var(--colour-surface)
  );
  color: var(--colour-text);
  min-width: 7.5rem;
}

/* Position-driven surface fade (steps 0..7). Beyond step 7 the surface
 * stays at the step-7 mix. */
.funnel-stage[data-funnel-step="0"] { --dz-funnel-fade: 60; }
.funnel-stage[data-funnel-step="1"] { --dz-funnel-fade: 50; }
.funnel-stage[data-funnel-step="2"] { --dz-funnel-fade: 41; }
.funnel-stage[data-funnel-step="3"] { --dz-funnel-fade: 33; }
.funnel-stage[data-funnel-step="4"] { --dz-funnel-fade: 26; }
.funnel-stage[data-funnel-step="5"] { --dz-funnel-fade: 20; }
.funnel-stage[data-funnel-step="6"] { --dz-funnel-fade: 15; }
.funnel-stage[data-funnel-step="7"] { --dz-funnel-fade: 11; }

.funnel-stage-label {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
}

.funnel-stage-count {
  font-size: var(--text-xs);
  /* inherits the stage's full-contrast ink — muted text fails WCAG on
   * the brand-tinted surfaces; size alone differentiates it. */
  margin-inline-start: 0.25rem;
}

.funnel-summary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

}
/* ── components/bar-chart.css ── */
@layer components {
/* HYPERPART: bar-chart */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-a, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── bar_chart region ───────────────────
 * Horizontal bar rows: label (right) / track / value (right). The
 * track has rounded-full chrome; the fill is brand-coloured and
 * its width is the inline value-derived percentage. */

.bar-chart-bars {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-block-start: var(--space-sm);
}

.bar-chart-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.bar-chart-label {
  width: 6rem;
  text-align: end;
  font-size: var(--text-xs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bar-chart-label.is-wide {
  width: 7rem;
  color: var(--colour-text-muted);
}

.bar-chart-track {
  flex: 1 1 0;
  height: 1.25rem;
  background: var(--colour-bg);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.bar-chart-fill {
  height: 100%;
  background: var(--colour-brand);
  border-radius: var(--radius-full);
  transition: width var(--duration-base) var(--ease-out);
}

.bar-chart-value {
  width: 2.5rem;
  text-align: end;
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  color: var(--colour-text-muted);
}

.bar-chart-value.is-wide {
  width: 2.5rem;
  color: var(--colour-text);
}

.bar-chart-summary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

}
/* ── components/chart-legend.css ── */
@layer components {
/* HYPERPART: chart-legend */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-a, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── chart legend / summary ─────────────
 * Shared across the SVG chart family (line / area / radar). Each
 * chart template renders its own SVG body but ends with a legend
 * row of (swatch + series-name) chips and a summary line of
 * sample/series counts. Single class set instead of duplicating
 * inline `flex flex-wrap gap-x-3 ...` Tailwind across every
 * template. */

.chart-svg,
.line-chart-svg,
.area-chart-svg {
  width: 100%;
  height: auto;
}

/* Radar-specific: long axis labels at the -90°/90° spokes can extend
 * past the SVG viewBox because the rotation pivot of the existing
 * spoke-label markup doesn't perfectly counter-translate. Allow the
 * labels to render past the SVG bounds rather than clipping mid-word
 * (closes #917). The radar SVG is always the rightmost element in its
 * card, and CSS Grid gives each card an isolated overflow region, so
 * letting labels spill past the SVG won't bleed into neighbouring
 * regions.
 *
 * Note: trig-placed labels (per the issue's "option 1") would
 * eliminate the need for this rule entirely. Worth a follow-up; this
 * is the smaller-blast-radius fix while the template still uses the
 * nested-rotate workaround. */
.radar-svg {
  overflow: visible;
}

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-md);
  row-gap: 0.25rem;
  margin-block-start: var(--space-sm);
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}

.chart-legend-swatch {
  display: inline-block;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--radius-sm);
}

.chart-legend-name {
  font-family: var(--font-mono);
}

.chart-summary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
  font-family: var(--font-mono);
}

/* The chart-degenerate-* fallback family that lived here was DEAD
 * on arrival (Jinja-era shapes retired with ADR-0023; zero emitters in
 * the typed substrate) — dropped rather than published as fake API. */

}
/* ── components/heatmap.css ── */
@layer components {
/* HYPERPART: heatmap */
/* Promoted from Dazzle regions.css (Tier C-b, fleet convergence).
 * The Dazzle emitter contract is unchanged. One deliberate delta from
 * the source: the three tone-cell backgrounds moved from
 * `color-mix(... 15%, transparent)` recipes to the engineered
 * `--colour-*-soft` tokens (the HM contrast recipe — same change the
 * status-list/alert promotions made). */

/* ─────────────────────────── heatmap region ─────────────────────
 * Matrix table with row labels and column labels, value cells
 * tinted by threshold band (success / warning / destructive).
 * Tints route through data-heatmap-tone so the threshold
 * branching can stay in CSS (template emits the data-attribute,
 * not a class string). */

.heatmap-scroll {
  overflow-x: auto;
}

.heatmap-grid {
  width: 100%;
  font-size: var(--text-sm);
  border-collapse: collapse;
}

.heatmap-grid > thead > tr {
  border-block-end: 1px solid var(--colour-border);
}

.heatmap-grid th {
  padding-inline: var(--space-sm);
  padding-block: var(--space-xs);
  text-align: center;
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
}

.heatmap-grid > tbody > tr {
  border-block-end: 1px solid var(--colour-border);
}

.heatmap-grid > tbody > tr:last-child {
  border-block-end: 0;
}

.heatmap-grid > tbody > tr.is-clickable {
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.heatmap-grid > tbody > tr.is-clickable:hover {
  background: var(--colour-bg);
}

.heatmap-row-label {
  padding-inline: var(--space-sm);
  padding-block: var(--space-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.heatmap-cell {
  padding-inline: var(--space-sm);
  padding-block: var(--space-xs);
  text-align: center;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.heatmap-cell[data-heatmap-tone="bad"] {
  background: var(--colour-danger-soft);
  color: var(--colour-danger);
}

.heatmap-cell[data-heatmap-tone="warn"] {
  background: var(--colour-warning-soft);
  color: var(--colour-warning);
}

.heatmap-cell[data-heatmap-tone="good"] {
  background: var(--colour-success-soft);
  color: var(--colour-success);
}

.heatmap-overflow {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

}
/* ── components/pivot.css ── */
@layer components {
/* HYPERPART: pivot */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-b, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── pivot_table region ─────────────────
 * Multi-dimension cross-tab. Same shape as the heatmap grid but
 * with per-row dimension labels on the left and measure values
 * (right-aligned, mono-numeric) on the right. */

.pivot-scroll {
  overflow-x: auto;
}

.pivot-grid {
  width: 100%;
  font-size: var(--text-sm);
  border-collapse: collapse;
}

.pivot-grid > thead > tr {
  border-block-end: 1px solid var(--colour-border);
}

.pivot-grid th {
  padding-inline: var(--space-sm);
  padding-block: var(--space-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  text-align: start;
}

.pivot-grid th.is-measure {
  text-align: end;
  font-family: var(--font-mono);
}

.pivot-grid > tbody > tr {
  border-block-end: 1px solid var(--colour-border);
}

.pivot-grid > tbody > tr:last-child {
  border-block-end: 0;
}

.pivot-grid td {
  padding-inline: var(--space-sm);
  padding-block: var(--space-xs);
  color: var(--colour-text);
}

.pivot-grid td.is-measure {
  text-align: end;
  font-family: var(--font-mono);
}

.pivot-null {
  color: var(--colour-text-muted);
}

.pivot-summary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

}
/* ── components/bullet.css ── */
@layer components {
/* HYPERPART: bullet */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-b, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── bullet chart region ───────────────
 * Stephen Few bullet rows. Three fixed columns: label (right) /
 * track / value. Track contains: optional reference bands behind,
 * actual bar, target tick. Band/bar/tick are absolutely positioned
 * inside the relative track. */

.bullet-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-block-start: var(--space-sm);
}

.bullet-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--text-xs);
}

.bullet-label {
  width: 6rem;
  text-align: end;
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bullet-track {
  flex: 1 1 0;
  position: relative;
  height: 1.25rem;
  background: var(--colour-bg);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.bullet-band {
  position: absolute;
  inset-block: 0;
  opacity: 0.18;
}

.bullet-actual {
  position: absolute;
  inset-block: 0.25rem;
  inset-inline-start: 0;
  background: var(--colour-brand);
  border-radius: var(--radius-sm);
}

.bullet-target {
  position: absolute;
  inset-block: 0;
  width: 2px;
  background: var(--colour-text);
}

.bullet-value {
  width: 4rem;
  text-align: end;
  font-family: var(--font-mono);
  color: var(--colour-text-muted);
}

.bullet-summary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
  font-family: var(--font-mono);
}

}
/* ── components/histogram.css ── */
@layer components {
/* HYPERPART: histogram */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-b, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── histogram region ───────────────────
 * Server-rendered SVG histogram. Bar fill, axis line, and reference
 * lines all use design tokens via their own SVG attributes; this
 * file owns only the host shape + summary footer. */

.histogram-svg {
  width: 100%;
  height: auto;
}

.histogram-summary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
  font-family: var(--font-mono);
}

}
/* ── components/box-plot.css ── */
@layer components {
/* HYPERPART: box-plot */
/* Promoted from Dazzle regions.css (Tier C-b), then refined for
 * whisker geometry + hover figures (key-point marks). */

/* ─────────────────────────── box_plot region ────────────────────
 * Server-rendered SVG quartile boxes per group. SVG fill/stroke
 * tokens stay inline (per-element shape colour); host wrapper sized
 * via .box-plot-svg + summary uses .box-plot-summary.
 *
 * Geometry (in the SVG body from dazzle.render.svg.box_plot_svg):
 *   - whiskers only outside the IQR box (min→Q1, Q3→max) + end caps
 *   - hairline median (stroke-width 1)
 *   - .box-plot-mark groups at min/Q1/median/Q3/max — hover reveals
 *     the numeric figure (CSS only, no controller). */

.box-plot-svg {
  width: 100%;
  height: auto;
  overflow: visible; /* hover labels sit just outside the box half-width */
  /* Decorative geometry does not steal hits — only .box-plot-mark-hit. */
  pointer-events: none;
}

.box-plot-summary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
  font-family: var(--font-mono);
}

/* Geometry classes on server-SVG parts — named hooks for dual-lock / tests;
 * stroke/fill stay inline on the elements themselves. */
.box-plot-whisker,
.box-plot-whisker-cap,
.box-plot-box,
.box-plot-median {
  /* structural identity only — presentation is inline SVG attrs */
}

/* ── hover marks (five-number figures) ─────────────────────────── */

.box-plot-mark {
  pointer-events: auto;
  cursor: default;
}

.box-plot-mark-hit {
  pointer-events: all;
  cursor: help;
}

.box-plot-mark-dot {
  opacity: 0;
  transition: opacity var(--duration-fast, 120ms) var(--ease-out, ease);
  pointer-events: none;
}

.box-plot-mark-label {
  opacity: 0;
  transition: opacity var(--duration-fast, 120ms) var(--ease-out, ease);
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--colour-surface);
  stroke-width: 3px;
  stroke-linejoin: round;
}

.box-plot-mark:hover .box-plot-mark-dot,
.box-plot-mark:focus-within .box-plot-mark-dot,
.box-plot-mark:hover .box-plot-mark-label,
.box-plot-mark:focus-within .box-plot-mark-label {
  opacity: 1;
}

}
/* ── components/bar-track.css ── */
@layer components {
/* HYPERPART: bar-track */
/* Promoted from Dazzle regions.css (Tier C-b), then refined so the
 * footer summary shares the same column stops as the rows. */

/* ─────────────────────────── bar_track region ───────────────────
 * Compact horizontal value bars. Label | track | value share one
 * column template with the footer summary so the meta/scale line
 * sits on the same stops as the rows (not a free-form left flush). */

.bar-track-region {
  --bar-track-label-col: 7rem;
  --bar-track-value-col: 4rem;
  --bar-track-gap: var(--space-sm);
}

.bar-track-rows {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-block-start: var(--space-sm);
}

.bar-track-row {
  display: grid;
  grid-template-columns:
    var(--bar-track-label-col)
    minmax(0, 1fr)
    var(--bar-track-value-col);
  column-gap: var(--bar-track-gap);
  align-items: center;
  font-size: var(--text-xs);
}

.bar-track-label {
  min-width: 0;
  text-align: end;
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bar-track {
  position: relative;
  min-width: 0;
  width: 100%;
  height: 1rem;
  background: var(--colour-bg);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.bar-track-fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  background: var(--colour-brand);
  border-radius: var(--radius-full);
  transition: width var(--duration-base) var(--ease-out);
}

.bar-track-value {
  min-width: 0;
  text-align: end;
  font-family: var(--font-mono);
  color: var(--colour-text-muted);
  font-variant-numeric: tabular-nums;
}

/* Summary reuses the row grid: empty label gutter · meta under tracks ·
 * scale under the value column. */
.bar-track-summary {
  display: grid;
  grid-template-columns:
    var(--bar-track-label-col)
    minmax(0, 1fr)
    var(--bar-track-value-col);
  column-gap: var(--bar-track-gap);
  align-items: baseline;
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.bar-track-summary-gutter {
  min-width: 0;
}

.bar-track-summary-meta {
  min-width: 0;
}

.bar-track-summary-scale {
  min-width: 0;
  text-align: end;
}

}
/* ── components/progress-region.css ── */
@layer components {
/* HYPERPART: progress-region */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-b, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── progress region ────────────────────
 * Header row: <progress> bar + percentage label. Stages row: chips
 * coloured by status (complete=success, in-progress=warning,
 * empty=neutral). Optional summary line beneath. */

.progress-header {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-block-end: var(--space-sm);
}

.progress-header > progress {
  flex: 1 1 0;
  height: 0.5rem;
  width: 100%;
}

.progress-header > span {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  color: var(--colour-text);
}

.progress-stages {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.progress-chip {
  display: inline-flex;
  align-items: center;
  padding-inline: var(--space-sm);
  padding-block: 0.125rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  border: 1px solid var(--colour-border);
  background: var(--colour-bg);
  color: var(--colour-text-muted);
}

/* Per-stage tone — driven by data-stage-tone (template emits
 * "complete" / "active" / "empty"). Mirror of the badge tone system. */
.progress-chip[data-stage-tone="complete"] {
  background: var(--colour-success-soft);
  color: var(--colour-success);
  border-color: color-mix(in oklch, var(--colour-success) 30%, transparent);
}

.progress-chip[data-stage-tone="active"] {
  background: var(--colour-warning-soft);
  color: var(--colour-warning);
  border-color: color-mix(in oklch, var(--colour-warning) 30%, transparent);
}

.progress-summary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

}
/* ── components/task-inbox.css ── */
@layer components {
/* HYPERPART: task-inbox */
/* Promoted VERBATIM from Dazzle regions.css (Tier C-b, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── task_inbox region ──────────────────
 * Summary chips (count + label pill) above a list of task rows
 * (#1015). The `.task-inbox-chip*` family had NO css at all, so the
 * count and label spans rendered glued together ("0manuscripts");
 * the chip is now an inline-flex pill with a gap between count and
 * label, mirroring `.progress-chip` (#1326). */

.task-inbox-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-block-end: var(--space-sm);
}

.task-inbox-chip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-xs);
  padding-inline: var(--space-sm);
  padding-block: 0.125rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  border: 1px solid var(--colour-border);
  background: var(--colour-bg);
  color: var(--colour-text-muted);
  text-decoration: none;
}

.task-inbox-chip-count {
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}

.task-inbox-chip-label {
  color: var(--colour-text-muted);
}

/* Items list — icon is the leading mark; suppress default ul bullets
 * so they do not double the affordance next to Lucide icons (HMC
 * coherence: decorative_noise on task-inbox). */
.task-inbox-items {
  display: flex;
  flex-direction: column;
  list-style: none;
  margin: 0;
  padding-inline-start: 0;
  gap: 0;
}

.task-inbox-item + .task-inbox-item {
  border-block-start: 1px solid var(--colour-border);
}

.task-inbox-item {
  margin: 0;
  padding: 0;
  list-style: none;
}

.task-inbox-item-link {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  padding-block: var(--space-sm);
  padding-inline: 0;
  color: inherit;
  text-decoration: none;
  min-width: 0;
}

.task-inbox-item-link:hover {
  color: var(--colour-brand);
}

.task-inbox-item-body {
  flex: 1 1 0;
  min-width: 0;
}

.task-inbox-item-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  color: var(--colour-text);
}

.task-inbox-item-link:hover .task-inbox-item-title {
  color: var(--colour-brand);
}

.task-inbox-item-meta {
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--colour-text-muted);
  margin-block-start: 0.125rem;
}

/* Urgency tint on the leading icon (data-urgency from the emitter). */
.task-inbox-item[data-urgency="overdue"] .task-inbox-item-icon {
  color: var(--colour-danger);
}

.task-inbox-item[data-urgency="due"] .task-inbox-item-icon {
  color: var(--colour-warning);
}

.task-inbox-item[data-urgency="soon"] .task-inbox-item-icon {
  color: var(--colour-brand);
}

.task-inbox-empty {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

}
/* ── components/grid-list.css ── */
@layer components {
/* HYPERPART: grid-list */
/* Promoted VERBATIM from Dazzle regions.css (W-leftovers, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── grid region ────────────────────────
 * Responsive cell grid (1 → 2 → 3 columns at sm/lg breakpoints).
 * Cells are intentionally plain — the dashboard slot owns the card
 * chrome. Each cell gets an attention-accent border via the
 * existing attention_classes macro (untouched), and .is-clickable
 * when wired to action_url. */

.grid-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
}

@media (min-width: 40rem) {
  .grid-list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .grid-list {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.grid-cell {
  border-radius: var(--radius-sm);
  padding: var(--space-md);
  transition: background var(--duration-fast) var(--ease-out);
}

.grid-cell.is-clickable {
  cursor: pointer;
}

.grid-cell.is-clickable:hover {
  background: var(--colour-bg);
}

.grid-cell-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  margin-block-end: 0.375rem;
}

.grid-cell-field {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin: 0;
}

/* Goal B media thumbs inside grid cards (logo/preview fields). */
.grid-cell-field .media-thumb {
  display: block;
  width: 100%;
  max-width: 7rem;
  height: 4.5rem;
  object-fit: cover;
  border-radius: var(--radius-sm, 4px);
  border: 1px solid var(--colour-border, #e5e7eb);
  background: var(--colour-surface-muted, #f3f4f6);
  margin-block: 0.15rem 0.35rem;
}

/* A creative preview is the content of a grid card, not a queue avatar. */
.grid-cell-field .aspect-ratio[data-media-size="card"] {
  width: 100%;
  margin-block: 0.5rem;
}

.grid-cell-field .aspect-ratio[data-media-size="card"] .media-thumb {
  width: 100%;
  max-width: none;
  height: 100%;
  margin: 0;
  border: 0;
  border-radius: inherit;
}

.grid-cell-field-label {
  font-size: var(--text-xs);
}

}
/* ── components/list-region.css ── */
@layer components {
/* HYPERPART: list-region */
/* Promoted from Dazzle regions.css (W-leftovers, fleet convergence).
 * The Dazzle emitter contract is unchanged. Two DEAD rule families
 * from the source were deleted rather than promoted (proven
 * un-emitted across py/html/js/examples): the filter bar
 * (.list-filters/.list-filter-select — the substrate list
 * region never grew filter chrome) and .list-action-button (the
 * actions row only ever renders the CSV export button). */

/* ─────────────────────────── list region ────────────────────────
 * Data table inside a region card. Has its own actions row (region
 * actions + CSV export) and table. Row attention tint via
 * .is-clickable + attention_classes macro (unchanged). */

.list-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  margin-block-end: var(--space-md);
}

.list-action-group {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.list-csv-button {
  height: 1.75rem;
  width: 1.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.list-csv-button:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.list-csv-button > svg {
  width: 1rem;
  height: 1rem;
}

.list-scroll {
  overflow-x: auto;
}

.list-table {
  width: 100%;
  border-collapse: collapse;
}

.list-table > thead > tr {
  background: color-mix(in oklch, var(--colour-bg) 70%, transparent);
  border-block-end: 1px solid var(--colour-border);
}

.list-table th {
  text-align: start;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  letter-spacing: -0.01em;
}

.list-table > tbody > tr {
  border-block-end: 1px solid var(--colour-border);
  transition: background var(--duration-fast) var(--ease-out);
}

.list-table > tbody > tr:last-child {
  border-block-end: 0;
}

.list-row.is-clickable {
  cursor: pointer;
}

.list-row.is-clickable:hover {
  background: var(--colour-bg);
}

.list-table td {
  font-size: var(--text-sm);
  color: var(--colour-text);
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
}

.list-sort-link {
  cursor: pointer;
  user-select: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  text-decoration: none;
  color: inherit;
  transition: color var(--duration-fast) var(--ease-out);
}

.list-sort-link:hover {
  color: var(--colour-text);
}

.list-overflow {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

}
/* ── components/detail.css ── */
@layer components {
/*
 * detail.css — entity detail-view component family (HaTchi-MaXchi).
 *
 * The record/detail view: an entity's fields as a definition list inside a
 * card, header (back / title / actions), state-machine transition buttons, and
 * related-entity groups beneath; plus the workspace `display: detail` region
 * grid and the shared in-region empty placeholder. All values route through HM
 * tokens. Migrated from Dazzle `page/runtime/static/css/components/detail.css`
 * into HM (HMC-012, aggressive-mode convergence 2026-07-08).
 */
/* ─────────────────────────── outer wrapper ──────────────────────
 * Width changes when related-groups are present (more horizontal
 * room for the grouped tables). The component template branches on
 * `detail.related_groups` to apply .detail-wide. */

.detail {
  max-width: 48rem;  /* ~max-w-3xl */
  margin-inline: auto;
}

.detail-wide {
  max-width: 64rem;  /* ~max-w-5xl — when related groups present */
}

/* ─────────────────────────── header row ─────────────────────────
 * Back-button + title on the left, action buttons on the right. */

.detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-block-end: var(--space-xl);
}

.detail-header-title {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.detail-title {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin: 0;
}

.detail-actions {
  display: flex;
  gap: var(--space-sm);
}

/* ─────────────────────────── transition / external rows ──────────
 * State-machine transitions, integration actions, external link
 * actions — all share the same horizontal-button-row shape with
 * a small bottom margin. */

.detail-toolbar {
  display: flex;
  gap: var(--space-sm);
  margin-block-end: var(--space-lg);
}

/* ─────────────────────────── detail card ────────────────────────
 * The main field-list lives inside a card. */

.detail-card {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.detail-card-body {
  padding: var(--space-lg);
}

/* ─────────────────────────── definition list ───────────────────
 * Each row is a 3-column grid on wider viewports (label takes 1,
 * value takes 2). On narrow viewports rows stack with a small gap.
 * Uses container queries so the grid adapts to the container's
 * width, not the viewport. */

.detail-list {
  container-type: inline-size;
}

.detail-list > * + * {
  border-block-start: 1px solid var(--colour-border);
}

.detail-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding-block: var(--space-md);
}

@container (width > 32rem) {
  .detail-row {
    display: grid;
    grid-template-columns: 1fr 2fr;
    gap: var(--space-lg);
  }
}

.detail-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
}

.detail-value {
  font-size: var(--text-sm);
  color: var(--colour-text);
}

/* ─────────────────────────── file value link ────────────────────
 * `field.type == "file"` renders an icon + filename link. */

.detail-file-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--colour-brand);
  text-decoration: none;
}

.detail-file-link:hover {
  text-decoration: underline;
}

.detail-file-icon {
  width: 1rem;
  height: 1rem;
}

/* ─────────────────────────── related groups ─────────────────────
 * After the main field card, related entities (one-to-many or
 * many-to-many) render as separate groups with their own labels
 * and content (table / status_cards / file_list). */

.detail-related-group {
  margin-block-start: var(--space-xl);
}

.detail-related-label {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin-block-end: var(--space-md);
}

/* ─────────────────────────── workspace detail region ───────────
 * The workspace `display: detail` region shape is different from
 * the standalone detail_view component above — it renders inside
 * a region card slot, so the chrome (border, shadow, padding) is
 * provided by .region-card; here we only style the dl grid and
 * the empty placeholder. Two-column label/value grid (label left,
 * value right) — distinct from the row-stacked .detail-row above. */

.detail-region-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-xs) var(--space-md);
  margin: 0;
}

.detail-region-grid > dt.detail-label {
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  color: var(--colour-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0;
}

.detail-region-grid > dd.detail-value {
  font-size: var(--text-sm);
  color: var(--colour-text);
  margin: 0;
}

/* ─────────────────────────── shared empty placeholder ──────────
 * `.empty-dense` is the in-region "no data" line used by the
 * detail region and ~20 other region templates. Centralised here
 * during the detail migration; remains in this file until the
 * region migrations proper move it to a shared location. */

.empty-dense {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  margin: 0;
}

}
/* ── components/onboarding.css ── */
@layer components {
/*
 * onboarding.css — guided-onboarding overlay primitives (HaTchi-MaXchi).
 *
 * Styles for the 8 step kinds (popover, spotlight, inline_card, empty_state,
 * banner, checklist_item, blocking_task, nudge) emitted by
 * `dazzle.render.onboarding.renderer`. Every overlay shares the outer
 * `<onboarding-step>` custom element; per-kind appearance is a
 * `.onboarding-<kind>` block + BEM child classes. The bulk routes through HM
 * tokens; the spotlight-ring / scrim geometry (6rem ring, 999px cutout shadow,
 * scrim opacity) is intentionally bespoke, not design-system-tokenizable.
 * Migrated from Dazzle into HM (HMC-014, aggressive-mode convergence 2026-07-08).
 * NOTE: no `@layer components {` wrapper here — HM build.py adds it.
 */

/* ─────────────────────────────────────────────────────────────────
 * Shared outer element. Custom elements default to display: inline
 * — that's almost always wrong for an overlay. Make every kind
 * `block` by default; per-kind overrides take it from there.
 * ───────────────────────────────────────────────────────────────── */
onboarding-step {
  display: block;
}

/* Hide overlay visibly during dismiss-fade so the htmx outer-swap
 * isn't jarring. Reduced motion respects the user preference. */
@media (prefers-reduced-motion: no-preference) {
  onboarding-step {
    transition: opacity 0.15s ease;
  }
}

/* ─────────────────────────────────────────────────────────────────
 * popover — floating callout, anchored or bottom-fixed
 * ───────────────────────────────────────────────────────────────── */
.onboarding-popover {
  position: fixed;
  bottom: var(--space-xl);
  left: 50%;
  transform: translateX(-50%);
  z-index: 9000;
  max-width: min(28rem, calc(100vw - var(--space-xl) * 2));
  padding: var(--space-lg);
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-sm);
}

.onboarding-popover__chrome {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
}

.onboarding-popover__title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.onboarding-popover__body {
  margin: 0 0 var(--space-md);
  color: var(--colour-text-muted);
  line-height: 1.5;
}

.onboarding-popover__actions {
  display: flex;
  justify-content: flex-end;
}

/* ─────────────────────────────────────────────────────────────────
 * spotlight — dimmed page + halo ring + callout
 * ───────────────────────────────────────────────────────────────── */
.onboarding-spotlight {
  position: fixed;
  inset: 0;
  z-index: 9100;
}

.onboarding-spotlight__backdrop {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 0.55);
}

.onboarding-spotlight__ring {
  /* JS positions this against the target in v0.71.6+; the default
   * shape is a centered translucent ring as a fallback so the dim
   * is non-blocking. */
  position: absolute;
  top: 50%;
  left: 50%;
  width: 6rem;
  height: 6rem;
  margin: -3rem 0 0 -3rem;
  border-radius: var(--radius-full);
  box-shadow:
    0 0 0 999px rgb(0 0 0 / 0.0),
    0 0 0 4px var(--colour-brand);
  pointer-events: none;
}

.onboarding-spotlight__card {
  position: absolute;
  top: 50%;
  left: 50%;
  /* Offset by 8rem so the ring (3rem half-height + 4px border-shadow)
   * has visible breathing room before the card starts. Tighter than
   * this and the ring and card visually fuse. */
  transform: translate(-50%, calc(-50% + 8rem));
  max-width: min(26rem, calc(100vw - var(--space-xl) * 2));
  padding: var(--space-lg);
  background: var(--colour-surface);
  color: var(--colour-text);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.onboarding-spotlight__title {
  margin: 0 0 var(--space-sm);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
}

.onboarding-spotlight__body {
  margin: 0 0 var(--space-md);
  color: var(--colour-text-muted);
  line-height: 1.5;
}

.onboarding-spotlight__dismiss {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
}

/* ─────────────────────────────────────────────────────────────────
 * inline_card — solid card in page flow
 * ───────────────────────────────────────────────────────────────── */
.onboarding-inline-card {
  margin: var(--space-lg) 0;
  padding: var(--space-lg);
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.onboarding-inline-card__chrome {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
}

.onboarding-inline-card__title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
}

.onboarding-inline-card__body {
  margin: 0 0 var(--space-md);
  color: var(--colour-text-muted);
  line-height: 1.5;
}

.onboarding-inline-card__actions {
  display: flex;
  gap: var(--space-sm);
}

/* ─────────────────────────────────────────────────────────────────
 * empty_state — large-format replace-the-list prompt
 * ───────────────────────────────────────────────────────────────── */
.onboarding-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-md);
  padding: var(--space-3xl) var(--space-xl);
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px dashed var(--colour-border);
  border-radius: var(--radius-lg);
  margin: var(--space-xl) 0;
}

.onboarding-empty-state__icon {
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-full);
  background: var(--colour-brand);
  opacity: 0.15;
}

.onboarding-empty-state__title {
  margin: 0;
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
}

.onboarding-empty-state__body {
  margin: 0;
  max-width: 36rem;
  color: var(--colour-text-muted);
  line-height: 1.5;
}

.onboarding-empty-state__actions {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-top: var(--space-sm);
}

/* ─────────────────────────────────────────────────────────────────
 * banner — full-width strip across the top
 * ───────────────────────────────────────────────────────────────── */
.onboarding-banner {
  position: sticky;
  top: 0;
  z-index: 8000;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-lg);
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  font-size: var(--text-sm);
}

.onboarding-banner__message {
  flex: 1 1 auto;
  min-width: 0;
}

.onboarding-banner__title {
  font-weight: var(--weight-medium);
}

.onboarding-banner__separator {
  opacity: 0.7;
}

.onboarding-banner__body {
  opacity: 0.9;
}

.onboarding-banner__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex: 0 0 auto;
}

/* ─────────────────────────────────────────────────────────────────
 * checklist_item — list row with checkbox indicator
 * ───────────────────────────────────────────────────────────────── */
.onboarding-checklist-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  margin: var(--space-sm) 0;
}

.onboarding-checklist-item__indicator {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--radius-sm);
  border: 2px solid var(--colour-border);
  background: transparent;
}

.onboarding-checklist-item__content {
  min-width: 0;
}

.onboarding-checklist-item__title {
  margin: 0 0 var(--space-xs);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
}

.onboarding-checklist-item__body {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  line-height: 1.4;
}

.onboarding-checklist-item__actions {
  display: flex;
  gap: var(--space-sm);
}

.onboarding-checklist-item__dismiss--hidden {
  display: none;
}

/* ─────────────────────────────────────────────────────────────────
 * blocking_task — native <dialog> modal with scrim
 * ───────────────────────────────────────────────────────────────── */
.onboarding-blocking-task {
  /* Outer custom element is just a container; the <dialog> handles
   * positioning + the modal stacking context. */
  display: contents;
}

.onboarding-blocking-task__backdrop {
  position: fixed;
  inset: 0;
  background: rgb(0 0 0 / 0.55);
  z-index: 9199;
}

.onboarding-blocking-task__dialog {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 9200;
  max-width: min(32rem, calc(100vw - var(--space-xl) * 2));
  padding: var(--space-xl);
  background: var(--colour-surface);
  color: var(--colour-text);
  border: none;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.onboarding-blocking-task__title {
  margin: 0 0 var(--space-md);
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
}

.onboarding-blocking-task__body {
  margin: 0 0 var(--space-lg);
  color: var(--colour-text-muted);
  line-height: 1.5;
}

.onboarding-blocking-task__actions {
  display: flex;
  justify-content: flex-end;
}

/* ─────────────────────────────────────────────────────────────────
 * nudge — small toast, bottom-right
 * ───────────────────────────────────────────────────────────────── */
.onboarding-nudge {
  position: fixed;
  bottom: var(--space-xl);
  right: var(--space-xl);
  z-index: 8500;
  display: flex;
  align-items: center;
  gap: var(--space-md);
  max-width: min(24rem, calc(100vw - var(--space-xl) * 2));
  padding: var(--space-md) var(--space-lg);
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  font-size: var(--text-sm);
}

.onboarding-nudge__content {
  flex: 1 1 auto;
  min-width: 0;
}

.onboarding-nudge__title {
  display: block;
  margin-bottom: var(--space-xs);
  font-weight: var(--weight-medium);
}

.onboarding-nudge__body {
  display: block;
  color: var(--colour-text-muted);
}

.onboarding-nudge__actions {
  display: flex;
  gap: var(--space-xs);
  flex: 0 0 auto;
}

/* ─────────────────────────────────────────────────────────────────
 * Shared dismiss + CTA styling for every kind. The renderer emits
 * per-kind classes (`__dismiss`, `__cta`) but every kind uses the
 * same visual baseline; per-kind blocks above override positioning
 * only where they need to.
 * ───────────────────────────────────────────────────────────────── */
/* Wrapped in :where() so the shared baseline has specificity (0,0,0).
 * That lets per-kind overrides (e.g. `.onboarding-banner__cta`) win
 * on a single class selector — otherwise the attribute-pair (0,2,0)
 * would beat the class (0,1,0) and per-kind chrome inversions would
 * silently lose the cascade. */
:where([class*="onboarding-"][class$="__dismiss"]) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  color: inherit;
  cursor: pointer;
  font-size: var(--text-base);
  line-height: 1;
  opacity: 0.6;
  transition: opacity 0.1s ease;
}

:where([class*="onboarding-"][class$="__dismiss"]):hover {
  opacity: 1;
}

:where([class*="onboarding-"][class$="__cta"]) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xs) var(--space-md);
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  border-radius: var(--radius-sm);
  cursor: pointer;
  border: none;
  transition: filter 0.1s ease;
}

:where([class*="onboarding-"][class$="__cta"]):hover {
  filter: brightness(0.92);
}

/* Empty-state dismiss is a labelled "Skip" — give it ghost-button
 * shape rather than the bare ✕ chrome. */
.onboarding-empty-state__dismiss {
  width: auto;
  padding: var(--space-xs) var(--space-md);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  opacity: 1;
  color: var(--colour-text-muted);
}

/* Banner CTA + dismiss sit on a coloured ground — invert chrome
 * colours to stay legible. */
.onboarding-banner__cta {
  background: var(--colour-brand-contrast);
  color: var(--colour-brand);
}

.onboarding-banner__cta:hover {
  filter: brightness(0.96);
}

.onboarding-banner__dismiss {
  color: var(--colour-brand-contrast);
}

}
/* ── components/tree.css ── */
@layer components {
/* HYPERPART: tree */
/* Nested <details>/<summary> hierarchy. Disclosure chrome is CSS —
 * not a chevron SVG stuffed into the summary as content. Branches
 * (:has(> .tree-children)) get a rotating ::before affordance;
 * leaves are .tree-leaf (or a node with no children) and never
 * advertise expand. */

/* ─────────────────────────── tree region ──────────────────────── */

.tree-node {
  background: color-mix(in oklch, var(--colour-bg) 70%, transparent);
  border-radius: var(--radius-sm);
  margin-block-end: var(--space-xs);
}

.tree-summary {
  --tree-chevron-size: 0.75rem;
  font-size: var(--text-sm);
  padding-inline: var(--space-md);
  padding-block: 0.375rem;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--ease-out);
}

/* Kill the UA disclosure marker — we own the affordance as ::before. */
.tree-summary::-webkit-details-marker {
  display: none;
}

.tree-summary::marker {
  content: "";
}

.tree-summary:hover {
  background: var(--colour-bg);
}

/* Chevron slot: always present on branch summaries so labels align;
 * the glyph itself is only drawn when this summary is a branch. */
.tree-summary::before {
  content: "";
  flex-shrink: 0;
  width: var(--tree-chevron-size);
  height: var(--tree-chevron-size);
  /* empty slot for non-branch / legacy leaf-as-details */
}

/* Branch only — rotating chevron is style, not content. */
.tree-node:has(> .tree-children) > .tree-summary::before {
  background-color: var(--colour-text-muted);
  /* chevron-right as mask so colour tracks the token */
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg 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'><path d='M9 5l7 7-7 7'/></svg>");
  mask-image: url("data:image/svg+xml;utf8,<svg 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'><path d='M9 5l7 7-7 7'/></svg>");
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  transition: transform var(--duration-fast) var(--ease-out);
}

.tree-node[open]:has(> .tree-children) > .tree-summary::before {
  transform: rotate(90deg);
}

/* Legacy SVG chevron in content (older emitters) — hide; CSS owns it. */
.tree-chevron {
  display: none;
}

.tree-label {
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.tree-count {
  margin-inline-start: auto;
  display: inline-flex;
  align-items: center;
  padding-inline: 0.375rem;
  height: 1rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  background: var(--colour-bg);
}

.tree-children {
  padding-inline-start: var(--space-xl);
  padding-inline-end: var(--space-sm);
  padding-block-end: var(--space-xs);
}

/* Leaf — content only, no disclosure chrome. Indent matches branch
 * summary (padding + chevron slot + gap) so labels line up. */
.tree-leaf {
  --tree-chevron-size: 0.75rem;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-inline: var(--space-md);
  padding-block: 0.375rem;
  font-size: var(--text-sm);
  border-radius: var(--radius-sm);
  margin-block-end: var(--space-xs);
  min-width: 0;
}

/* Spacer equal to the branch chevron slot so leaf labels align. */
.tree-leaf::before {
  content: "";
  flex-shrink: 0;
  width: var(--tree-chevron-size);
  height: var(--tree-chevron-size);
}

.tree-leaf .tree-label {
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

/* Flat list fallback (non-hierarchical region content). */
.tree-flat-item {
  padding-inline: var(--space-sm);
  padding-block: 0.375rem;
  font-size: var(--text-sm);
  color: var(--colour-text);
  border-block-end: 1px solid var(--colour-border);
}

.tree-flat-item:last-child {
  border-block-end: 0;
}

}
/* ── components/diagram.css ── */
@layer components {
/* HYPERPART: diagram */
/* Promoted VERBATIM from Dazzle regions.css (Tier F, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── diagram region ─────────────────────
 * Mermaid-rendered SVG inside a horizontal-scroll container.
 * Mermaid's own CSS handles the diagram chrome; we own the wrapper. */

.diagram-scroll {
  overflow-x: auto;
  /* Give Mermaid a real box so SVG isn't squeezed to a hairline column. */
  min-height: 8rem;
  width: 100%;
}

.diagram-source {
  font-size: var(--text-sm);
  color: var(--colour-text);
  /* Mermaid replaces this <pre> with rendered SVG once initialised;
   * the source-text styling matters only on the initial paint flash. */
  margin: 0;
  white-space: pre-wrap;
}

/* Post-render: Mermaid injects an SVG (or a div wrapping one). Let it
 * size to content width inside the scroll host. */
.diagram-scroll > svg,
.diagram-scroll .mermaid > svg {
  max-width: none;
  height: auto;
}

.diagram-empty {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  margin: 0;
}

}
/* ── components/confirm-panel.css ── */
@layer components {
/* HYPERPART: confirm-panel */
/* Promoted from Dazzle regions.css (Tier F, fleet convergence). Two
 * deliberate deltas from the source: (1) the
 * `.confirm-primary.is-disabled` selector was dropped — the gate
 * state now lives SOLELY in `aria-disabled` (state-in-DOM, set by
 * controllers/confirm-gate.js, which replaces the Alpine
 * `dzConfirmGate` island); (2) `.confirm-title`/`.confirm-caption`
 * gained `display: block` — the emitter's divs-in-label became spans
 * (a <label> only admits phrasing content; the gallery vnu gate caught
 * the invalid nesting the legacy template shipped). */

/* ─────────────────────────── confirm panel ──────────────────────
 * Irreversible-action consent primitive. Renders a checklist of
 * obligations + dual primary/draft buttons in the off state, a
 * "Currently live" summary + revoke in the live state, and an audit
 * footer + (optional) re-enable button in the revoked state. State
 * branch keys off the panel root's data-state-value so future
 * enrichments (e.g. tone tinting) can key off it without touching
 * every branch's class string. */

/* #1546: the wrapper's card chrome — previously an accidental cascade
 * dependency on a DORMANT Dazzle block (dead since the Jinja teardown);
 * the Hyperpart is self-sufficient now. Values verbatim from the
 * shipped bundle, so in-app pixels are unchanged. */
.confirm-panel {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  max-inline-size: 24rem;
  inline-size: 100%;
  padding: var(--space-lg);
}

.confirm-summary {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  margin-block-end: var(--space-md);
}

.confirm-summary[data-confirm-tone="success"] {
  border-color: color-mix(in oklch, var(--colour-success) 40%, transparent);
  background: color-mix(in oklch, var(--colour-success) 6%, transparent);
}

.confirm-summary[data-confirm-tone="muted"] {
  background: color-mix(in oklch, var(--colour-bg) 60%, transparent);
}

.confirm-summary-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.confirm-summary-body {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: 0.125rem;
}

.confirm-checklist {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  list-style: none;
  margin: 0 0 var(--space-md);
  padding-inline-start: 0;
}

.confirm-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
}

.confirm-checkbox {
  margin-block-start: 0.125rem;
  flex-shrink: 0;
  width: 1rem;
  height: 1rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  accent-color: var(--colour-brand);
}

.confirm-row-label {
  flex: 1 1 0;
  cursor: pointer;
}

.confirm-title {
  /* span (a <label> only admits phrasing content) styled as block */
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold); /* #1546: folded from the dormant block (shipped look) */
  line-height: var(--leading-tight);
  color: var(--colour-text);
  margin-block-end: var(--space-xs);
}

.confirm-caption {
  display: block;
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--colour-text-muted);
  margin-block-start: 0.125rem;
}

.confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin-block-start: var(--space-sm);
  list-style: none;
}

.confirm-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  text-decoration: none;
  border: 0;
  cursor: pointer;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.confirm-primary:hover {
  opacity: 0.9;
}

/* While the gate is unsatisfied the controller keeps aria-disabled on
 * the primary (and withholds its href) — muted-tone surface, pointer
 * events off, so the a-tag-as-button affordance falls inert. */
.confirm-primary[aria-disabled="true"] {
  background: var(--colour-bg);
  color: var(--colour-text-muted);
  cursor: not-allowed;
  pointer-events: none;
}

.confirm-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  border: 1px solid var(--colour-border);
  background: transparent;
  color: var(--colour-text);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.confirm-secondary:hover {
  background: var(--colour-bg);
}

.confirm-revoke {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  border: 1px solid color-mix(in oklch, var(--colour-danger) 50%, transparent);
  background: transparent;
  color: var(--colour-danger);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.confirm-revoke:hover {
  background: color-mix(in oklch, var(--colour-danger) 8%, transparent);
}

.confirm-audit {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-md);
  line-height: var(--leading-tight);
}

}
/* ── components/search-box.css ── */
@layer components {
/* HYPERPART: search-box */
/* Tier F3, fleet convergence. Two lineages in one file:
 *
 * 1. The `.search-box-*` family is AUTHORED here — the Dazzle FTS
 *    search_box region emitted this markup UNSTYLED (no rules existed
 *    anywhere), so these are new HM styles for the live contract, not
 *    a move. The coaching-line toggle is pure CSS (`:has` +
 *    `:placeholder-shown`) — it replaced the Alpine `q` island.
 *
 * 2. The `.select-result-*` / `.search-result-*` families are
 *    promoted VERBATIM from Dazzle fragments.css (the search-flow
 *    autofill pattern; also consumed by the search-select widget). */

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

.search-box-input-row {
  display: flex;
  align-items: center;
}

.search-box-input {
  width: 100%;
  height: 2.25rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.search-box-input:focus-visible {
  outline: none;
  border-color: var(--colour-brand);
  box-shadow: 0 0 0 1px var(--colour-brand);
}

/* Coaching line — visible until the user types. Pure CSS: while the
 * input shows its placeholder the query is empty; the first keystroke
 * hides the line (and the first result swap replaces it entirely). */
.search-box-region:has(.search-box-input:not(:placeholder-shown))
  .search-box-empty:not(.search-box-empty--no-results) {
  display: none;
}

.search-box-empty {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  padding-block: var(--space-sm);
}

.search-box-result-count {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-end: var(--space-xs);
}

.search-box-result-list {
  list-style: none;
  margin: 0;
  padding-inline-start: 0;
  display: flex;
  flex-direction: column;
}

.search-box-result {
  border-block-end: 1px solid var(--colour-border);
}

.search-box-result:last-child {
  border-block-end: 0;
}

.search-box-result-link {
  display: block;
  padding-block: var(--space-sm);
  padding-inline: var(--space-xs);
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--ease-out);
}

.search-box-result-link:hover {
  background: var(--colour-bg);
}

.search-box-result-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.search-box-result-snippets {
  list-style: none;
  margin: 0.125rem 0 0;
  padding-inline-start: 0;
}

.search-box-result-snippet {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.search-box-result-snippet-field {
  font-weight: var(--weight-medium);
  margin-inline-end: 0.25rem;
}

/* mark = the FTS highlight the server emits inside snippets/titles */
.search-box-result-link mark {
  background: var(--colour-warning-soft);
  color: inherit;
  border-radius: 2px;
}

/* ── search-flow fragments (VERBATIM from Dazzle fragments.css) ──
 * select_result + search_results fragments — used by the
 * search-flow autofill pattern (cycle 288). select_result uses a
 * success-tinted "Selected:" line; search_results renders a list
 * of clickable rows + an empty/no-results state. */

.select-result-confirm {
  padding: var(--space-md);
  font-size: var(--text-sm);
  color: var(--colour-success);
}

.select-result-input {
  width: 100%;
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  font-size: var(--text-sm);
  color: var(--colour-text);
}

/* Result-row anatomy (search-select listbox + fragment search flow).
 * FIXED micro-pattern — domain data maps INTO these slots; do not invent a
 * new row Hyperpart per entity. Optional media is free HTML (img, initials,
 * icon <svg>); primary + secondary text cover the common cases.
 *
 *   .search-result-row[role=option]  (hx-get → select exchange)
 *     .search-result-media?          (optional leading 2rem slot)
 *     .search-result-body
 *       .search-result-name          (primary — required)
 *       .search-result-secondary?    (meta line)
 */
.search-result-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.search-result-row:hover,
.search-result-row:focus-visible {
  background: var(--colour-bg);
  outline: none;
}

.search-result-media {
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-full);
  background: var(--colour-brand-soft);
  color: var(--colour-brand-text);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  line-height: 1;
  user-select: none;
}

.search-result-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.search-result-media svg {
  width: 1rem;
  height: 1rem;
}

.search-result-body {
  min-width: 0;
  flex: 1 1 auto;
}

.search-result-name {
  font-weight: var(--weight-medium);
  font-size: var(--text-sm);
  color: var(--colour-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-result-secondary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-result-empty {
  padding: var(--space-md);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

}
/* ── components/form-chrome.css ── */
@layer components {
/* HYPERPART: form-chrome */
/* Promoted from Dazzle fragments.css (Tier F3, fleet convergence):
 * the form SECTION chrome, the form_errors validation summary, the
 * form_stepper progress list, and the steps_indicator (experience
 * wizard). Emitter contracts unchanged. NOT promoted (deleted at the
 * source instead): .form-shell/-header/-title/-body/-actions — that
 * wrapper family's producer was the legacy form_renderer.py deleted in
 * ADR-0049 Phase 3b; the substrate form path never emitted it — and
 * .form-errors-message (the errors emitter renders title+list only). */

/* ── form sections (live: FormSection primitive) ── */

.form-section-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin-block-end: var(--space-md);
}

.form-section-note {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  margin-block-end: var(--space-md);
}

.form-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-block-end: var(--space-lg);
}

/* ─────────────────────────── form_errors fragment ───────────────
 * Form-level validation error block (HTMX-swapped). Always uses the
 * destructive/danger tone — no need for an attribute selector. */

.form-errors {
  display: flex;
  gap: var(--space-md);
  padding: var(--space-md);
  margin-block-end: var(--space-md);
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-danger);
  background: color-mix(in oklch, var(--colour-danger) 8%, transparent);
}

.form-errors-icon {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  color: var(--colour-danger);
}

.form-errors-body {
  min-width: 0;
}

.form-errors-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin: 0;
}


.form-errors-list {
  font-size: var(--text-sm);
  color: var(--colour-text);
  list-style: disc inside;
  margin: 0.125rem 0 0;
  padding-inline-start: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

/* ─────────────────────────── form_stepper fragment ──────────────
 * Multi-section form progress list. Each <li> is a (number-circle
 * + label + connector). Active state via .is-active modifier on
 * the whole li. */

.form-stepper {
  display: flex;
  align-items: center;
  width: 100%;
  margin-block-end: var(--space-xl);
  gap: var(--space-sm);
  list-style: none;
  padding-inline-start: 0;
}

.form-stepper-item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
  cursor: pointer;
}

.form-stepper-item.is-not-last {
  flex: 1 1 0;
}

.form-stepper-circle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--radius-full);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  transition: background var(--duration-base) var(--ease-out),
              color var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out);
}

.form-stepper-circle.is-active {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  border-color: var(--colour-brand);
}

.form-stepper-circle > svg {
  width: 0.75rem;
  height: 0.75rem;
}

.form-stepper-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--colour-text-muted);
  transition: color var(--duration-base) var(--ease-out);
}

/* HYPERPART: wizard */
/* The keyboard-operable step trigger — a real <button> inside the li,
 * visually transparent (the circle/label carry the design). */
.form-stepper-button {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.form-stepper-button:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm);
}

/* Completed steps swap the number for a pure-CSS checkmark — keyed
 * off data-state (set by wizard.js; the SSR default is
 * current/pending, so no-JS renders numbers throughout). */
[data-state="complete"] .form-stepper-circle span {
  display: none;
}

[data-state="complete"] .form-stepper-circle::after {
  content: "\2713";
}

.form-stepper-label.is-active {
  color: var(--colour-text);
}

.form-stepper-connector {
  flex: 1 1 0;
  height: 1px;
  background: var(--colour-border);
  transition: background var(--duration-base) var(--ease-out);
}

.form-stepper-connector.is-active {
  background: var(--colour-brand);
}

/* ─────────────────────────── steps_indicator fragment ───────────
 * Visual step indicator (cycle 251). Position-based — every step at
 * or before the current is "complete" and rendered with brand fill.
 * Distinct from form_stepper which uses Alpine + per-step state. */

.steps {
  display: flex;
  align-items: center;
  width: 100%;
  list-style: none;
  padding-inline-start: 0;
  margin: 0;
}

.steps-item {
  display: flex;
  align-items: center;
}

.steps-item.is-not-last {
  flex: 1 1 0;
}

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

.steps-circle {
  flex-shrink: 0;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  background: var(--colour-bg);
  color: var(--colour-text-muted);
}

.steps-circle.is-completed {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
}

.steps-label {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.steps-label.is-completed {
  color: var(--colour-text);
  font-weight: var(--weight-medium);
}

.steps-connector {
  flex: 1 1 0;
  margin-inline: var(--space-md);
  height: 1px;
  background: var(--colour-border);
}

.steps-connector.is-completed {
  background: var(--colour-brand);
}

}
/* ── components/date-range.css ── */
@layer components {
/* HYPERPART: date-range */
/* Promoted VERBATIM from Dazzle fragments.css (Tier F3, fleet
 * convergence). The Dazzle emitter contract is unchanged. */

/* ─────────────────────────── date_range_picker fragment ─────────
 * From / To date inputs in a horizontal bar. Inputs use the form
 * input chrome from form.css already; this owns only the row layout
 * and label sizing. */

.date-range-picker {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-block-end: var(--space-sm);
}

.date-range-label {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.date-range-input {
  height: 1.75rem;
  padding-inline: var(--space-sm);
  width: 9rem;
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  font-size: var(--text-xs);
  color: var(--colour-text);
}

.date-range-input:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
  border-color: var(--colour-brand);
}

.date-range-group {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.date-range-iso {
  /* Companion ISO field (cycle 2139) — editable, not submitted.
   * Compact mono input so leftover junk stays visible. */
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
  color: var(--colour-text);
  height: 1.75rem;
  inline-size: 11ch;
  min-inline-size: 10ch;
  padding: 0.15rem 0.35rem;
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
}

.date-range-iso:invalid {
  border-color: var(--colour-danger, var(--colour-destructive, #dc2626));
}

}
/* ── components/search-select.css ── */
@layer components {
/* HYPERPART: search-select */
/* Promoted from Dazzle fragments.css (Tier F4b, fleet convergence).
 * One deliberate delta: the results panel's visibility moved from
 * Alpine x-show/x-cloak onto state-in-DOM — the input SSRs
 * aria-expanded="false", controllers/search-select.js flips it on
 * focusin/focusout, and the sibling rule below hides the panel off
 * the attribute. */

/* ─────────────────────────── search_select fragment ─────────────
 * Debounced search with dropdown results. Standard form input
 * chrome + spinner + ARIA combobox listbox panel below. Error
 * state (border colour) keyed off `aria-invalid="true"` — same
 * convention as form.css. */

.search-select {
  position: relative;
  width: 100%;
}

.search-select-input {
  width: 100%;
  /* Match .form-input box model (border-box + min-height 2.5rem). */
  box-sizing: border-box;
  height: auto;
  min-height: 2.5rem;
  line-height: 1.4;
  padding-block: var(--space-sm);
  padding-inline-start: var(--space-md);
  padding-inline-end: 2rem; /* room for the spinner */
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  font-size: var(--text-sm);
  color: var(--colour-text);
  outline: none;
  transition:
    box-shadow var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out);
}

.search-select-input::placeholder {
  color: var(--colour-text-muted);
}

.search-select-input:focus-visible {
  box-shadow: 0 0 0 1px var(--colour-brand);
  border-color: var(--colour-brand);
}

.search-select-input[aria-invalid="true"] {
  border-color: var(--colour-danger);
}

.search-select-input[aria-invalid="true"]:focus-visible {
  box-shadow: 0 0 0 1px var(--colour-danger);
}

.search-select-spinner {
  position: absolute;
  inset-inline-end: var(--space-md);
  inset-block-start: 50%;
  transform: translateY(-50%);
  width: 1rem;
  height: 1rem;
  color: var(--colour-text-muted);
}

.search-select-spinner-icon {
  width: 1rem;
  height: 1rem;
  animation: spin 1s linear infinite;
}

.search-select-results {
  position: absolute;
  z-index: 50;
  width: 100%;
  margin-block-start: 0.25rem;
  max-height: 15rem;
  overflow-y: auto;
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

.search-select-prompt {
  padding-inline: var(--space-md);
  padding-block: var(--space-md);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* Results visibility rides `data-open` on the ROOT (set by
 * search-select.js) — closed at SSR (attribute absent), open on
 * focus, closed 200ms after focus leaves the widget (the grace lets a
 * result-row click land before the panel hides). Root-keyed, NOT
 * input-keyed: the select exchange OOB-replaces the input element, so
 * an input-anchored sibling rule would wedge the panel open after a
 * successful selection. aria-expanded on the input is the a11y mirror. */
.search-select:not([data-open="true"]) .search-select-results {
  display: none;
}

}
/* ── components/combobox.css ── */
@layer components {
/* HYPERPART: combobox */
/* combobox — HM-native searchable enum single-select (HMC-018 slice 1).
 *
 * Progressive enhancement over a real <select data-combobox>: with JS
 * off the native select is fully usable (submits, native required). On
 * first interaction controllers/combobox.js wraps it in a `.combobox`
 * root — a role=combobox text input + a role=listbox <ul> of options — and
 * hides the native select (kept in the DOM as the submit value + no-JS
 * fallback). State lives in the DOM as `data-open` on the ROOT; the
 * sibling rule below hides the listbox off the attribute (same idiom as
 * search-select.css). Chrome mirrors the search-select family. */

.combobox {
  position: relative;
  /* block, not flex: a legacy fragment rule once set .combobox to a
   * column flex stack (label+select). The Hyperpart root must not inherit
   * that layout or the enhance wrapper gains gap/flex quirks. */
  display: block;
  width: 100%;
}

/* Native select is the no-JS control; once the controller wraps it and
 * marks the root `data-enhanced`, the visible combobox input takes
 * over and the native select is removed from the visual layer (it stays
 * in the DOM to carry the submitted value). */
.combobox[data-enhanced] > select[data-combobox] {
  display: none !important; /* beat any residual form/fragment display rules */
}

.combobox-input {
  width: 100%;
  /* Match .form-input box model exactly. Without border-box, <input>
   * is content-box while the pre-enhance <select class="form-input"> is
   * border-box — same min-height 2.5rem jumps ~18px (padding+border) on
   * enhance, and width:100% overflows by the horizontal padding. */
  box-sizing: border-box;
  height: auto;
  min-height: 2.5rem;
  line-height: 1.4;
  padding-block: var(--space-sm);
  padding-inline-start: var(--space-md);
  padding-inline-end: 2rem; /* room for trailing chrome (if any) */
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  font-size: var(--text-sm);
  color: var(--colour-text);
  outline: none;
  transition:
    box-shadow var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out);
}

.combobox-input::placeholder {
  color: var(--colour-text-muted);
}

.combobox-input:focus-visible {
  box-shadow: 0 0 0 1px var(--colour-brand);
  border-color: var(--colour-brand);
}

.combobox-input[aria-invalid="true"] {
  border-color: var(--colour-danger);
}

.combobox-input[aria-invalid="true"]:focus-visible {
  box-shadow: 0 0 0 1px var(--colour-danger);
}

.combobox-listbox {
  position: absolute;
  z-index: 50;
  inset-inline: 0;
  margin-block-start: 0.25rem;
  padding: 0;
  max-height: 15rem;
  overflow-y: auto;
  list-style: none;
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

.combobox-option {
  display: block;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--colour-text);
  cursor: pointer;
}

/* A filtered-out option is removed from flow (the `hidden` attribute the
 * controller toggles) — restated because the row's own `display` would
 * otherwise win over the UA `[hidden]` rule. */
.combobox-option[hidden] {
  display: none;
}

/* Hover + keyboard active-descendant highlight share one wash; the chosen
 * option carries aria-selected for the persistent tick of state. */
.combobox-option:hover,
.combobox-option[data-active="true"] {
  background: var(--colour-surface-muted);
}

.combobox-option[aria-selected="true"] {
  color: var(--colour-brand);
  font-weight: 500;
}

/* Growing-list "Add «query»" row — same option role, distinct chrome.
 * Shown only when data-allow-create is set and the filter has no exact match. */
.combobox-create {
  border-block-start: 1px solid var(--colour-border);
  color: var(--colour-brand-text);
  font-weight: var(--weight-medium);
}

.combobox-create[hidden] {
  display: none;
}

.combobox-empty {
  padding-inline: var(--space-md);
  padding-block: var(--space-md);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* Listbox visibility rides `data-open` on the ROOT (set by
 * combobox.js) — closed until focus/typing opens it, closed 200ms
 * after focus leaves the widget. aria-expanded on the input is the a11y
 * mirror. */
.combobox:not([data-open="true"]) .combobox-listbox {
  display: none;
}

}
/* ── components/code.css ── */
@layer components {
/* HYPERPART: code
 *
 * Server-emitted fenced code surface — chrome for docs and in-app samples.
 * Syntax colour is build-time (span.code__tok--*); this file owns surface,
 * meta bar (language + copy), and token colours from design tokens.
 *
 * Scheme: theme-aware via light-dark() (follows [data-theme] / color-scheme).
 * Light pages get a light code surface so dense doc pages stay scannable;
 * dark pages keep the classic elevated dark block. Always-dark is a common
 * docs convention — we deliberately follow the page theme instead so the
 * code Hyperpart dogfoods the same scheme tokens as the rest of HM.
 *
 * Contract (copy-paste structure — keep this exact nesting):
 *   <figure class="code" data-code data-language="python">
 *     <div class="code__meta">
 *       <span class="code__lang">python</span>   <!-- omit when no language -->
 *       <button type="button" class="code__copy" data-code-copy …>…</button>
 *     </div>
 *     <pre class="code__pre" tabindex="0" role="region" aria-label="…">
 *       <code class="code__source">…</code>
 *     </pre>
 *   </figure>
 *
 * Layout: flex meta row — lang at start, copy pushed to the end with
 * margin-inline-start: auto. Do NOT absolute-position the copy control
 * (containing-block / min-width:0 chains on nested part pages left-shift it).
 *
 * data-language is optional; data-code-copy is optional (omit the button).
 */

.code {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  max-width: 100%;
  width: 100%;
  margin: 0;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  /* Page-theme surface — not a permanent “code is always night”. */
  background: light-dark(var(--neutral-50), var(--neutral-900));
  color: light-dark(var(--neutral-900), var(--neutral-100));
  overflow: hidden;
  box-sizing: border-box;
}

/* Meta bar: language chip + copy. Always a full-width flex row. */
.code__meta {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  flex: 0 0 auto;
  gap: var(--space-sm);
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  padding: var(--space-xs) var(--space-sm) var(--space-xs) var(--space-md);
  min-height: 2.25rem;
  border-block-end: 1px solid light-dark(var(--neutral-200), var(--neutral-700));
  background: light-dark(var(--neutral-100), var(--neutral-800));
}

.code__lang {
  flex: 0 1 auto;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: var(--weight-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: light-dark(var(--neutral-600), var(--neutral-400));
  user-select: none;
  white-space: nowrap;
}

/* Push copy to the trailing edge whether or not a lang chip is present. */
.code__copy {
  flex: 0 0 auto;
  margin-inline-start: auto;
  position: static; /* never absolute — see header comment */
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: light-dark(var(--neutral-200), var(--neutral-700));
  color: light-dark(var(--neutral-900), var(--neutral-100));
  border: 1px solid light-dark(var(--neutral-300), var(--neutral-600));
  border-radius: var(--radius-sm);
  padding: 0.3rem 0.65rem;
  font-size: 0.75rem;
  font-family: var(--font-sans);
  font-weight: var(--weight-medium);
  line-height: 1.2;
  cursor: pointer;
  transition: background 120ms var(--ease-default, ease);
}

/* Beat base.css `button` / `button:hover` on the meta bar. */
.code .code__copy,
.code__meta .code__copy {
  background: light-dark(var(--neutral-200), var(--neutral-700));
  color: light-dark(var(--neutral-900), var(--neutral-100));
  border: 1px solid light-dark(var(--neutral-300), var(--neutral-600));
}

.code .code__copy:hover,
.code .code__copy:hover:not(:disabled),
.code__meta .code__copy:hover,
.code__meta .code__copy:hover:not(:disabled) {
  background: light-dark(var(--neutral-300), var(--neutral-600));
  color: light-dark(var(--neutral-900), var(--neutral-100));
}

.code .code__copy:active,
.code__meta .code__copy:active {
  background: light-dark(var(--neutral-400), var(--neutral-500));
}

.code .code__copy:focus-visible,
.code__meta .code__copy:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

.code__copy svg {
  width: 0.875rem;
  height: 0.875rem;
  flex-shrink: 0;
}

.code__copy-idle,
.code__copy-done {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.code__copy-done {
  display: none;
  color: var(--colour-success);
}

.code__copy[data-copied] .code__copy-idle {
  display: none;
}

.code__copy[data-copied] .code__copy-done {
  display: inline-flex;
}

/* Kill base.css `pre { background/border/padding }` inside this surface. */
.code .code__pre,
.code pre {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  padding: var(--space-md) var(--space-lg);
  overflow-x: auto;
  max-width: 100%;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  line-height: 1.6;
  tab-size: 4;
  background: transparent;
  border: 0;
  border-radius: 0;
  color: inherit;
  box-sizing: border-box;
}

.code .code__source,
.code code {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  background: transparent;
  padding: 0;
  border-radius: 0;
  white-space: pre;
}

/* ── Syntax tokens (build-time spans; plain text when unhighlighted) ──
 * Light mode: deeper hues for contrast on pale surfaces.
 * Dark mode: lighter hues on the elevated dark block.
 */

.code__tok--kw {
  color: light-dark(var(--brand-700), var(--brand-300));
}

.code__tok--str {
  color: light-dark(var(--success-600), var(--success-500));
}

.code__tok--cmt {
  /* WCAG AA on code surface: neutral-500 on dark failed color-contrast. */
  color: light-dark(var(--neutral-600), var(--neutral-400));
  font-style: italic;
}

.code__tok--num {
  color: light-dark(var(--warning-600), var(--warning-500));
}

.code__tok--dec {
  color: light-dark(var(--brand-600), var(--brand-200));
}

.code__tok--op {
  color: light-dark(var(--neutral-600), var(--neutral-300));
}

.code__tok--name {
  color: light-dark(var(--neutral-900), var(--neutral-100));
}

.code__tok--builtin {
  color: light-dark(var(--brand-600), var(--brand-200));
}

}
/* ── components/tags.css ── */
@layer components {
/* HYPERPART: tags */
/* tags — HM-native multi-value chips input (HMC-018 slice 2).
 *
 * Progressive enhancement over a real `<input type="text" data-tags>`
 * carrying a comma-joined value: with JS off the native input is fully
 * usable (the user types "a, b, c"; the server splits on comma). Controllers
 * wrap it in a `.tags` root on ready when the value is seeded, otherwise
 * on first interaction — a role=list of removable chips + a borderless entry
 * input — and hide the native input (kept in the DOM as the submit value +
 * no-JS fallback). The
 * `.tags` box carries the input chrome + focus-within ring + aria-invalid;
 * the entry is borderless inside it. Chrome mirrors the combobox /
 * search-select family. */

.tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
  min-height: 2rem;
  padding-block: var(--space-xs);
  padding-inline: var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  cursor: text;
  transition:
    box-shadow var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out);
}

.tags:focus-within {
  box-shadow: 0 0 0 1px var(--colour-brand);
  border-color: var(--colour-brand);
}

.tags[aria-invalid="true"] {
  border-color: var(--colour-danger);
}

.tags[aria-invalid="true"]:focus-within {
  box-shadow: 0 0 0 1px var(--colour-danger);
}

/* Native <input data-tags> is the no-JS control + the submitted value;
 * once the controller wraps it and marks the root `data-enhanced`, the
 * chips UI takes over and the native input leaves the visual layer (it stays
 * in the DOM — display:none still submits — to carry the comma value). */
.tags[data-enhanced] > input[data-tags] {
  display: none;
}

/* The chips list participates directly in the box's flex flow (role=list
 * kept for AT via display:contents), so chips and the entry share one line
 * and wrap together. */
.tags-list {
  display: contents;
}

/* A chip: a small rounded token with a remove control. */
.tags-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding-block: 0.125rem;
  padding-inline: var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--colour-surface-muted);
  color: var(--colour-text);
  font-size: var(--text-sm);
  line-height: 1.4;
}

.tags-chip-label {
  white-space: nowrap;
}

.tags-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--colour-text-muted);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--duration-base) var(--ease-out);
}

.tags-remove:hover {
  color: var(--colour-text);
}

.tags-remove:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
}

/* Borderless entry inside the box — the box owns the chrome + focus ring. */
.tags-entry {
  flex: 1 1 6rem;
  min-width: 6rem;
  height: 1.5rem;
  padding: 0;
  border: none;
  outline: none;
  background: transparent;
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.tags-entry::placeholder {
  color: var(--colour-text-muted);
}

}
/* ── components/profile-card.css ── */
@layer components {
/* HYPERPART: profile-card */
/* Promoted from Dazzle regions.css (Tier A1), then refined so the
 * stats strip reads as KPI chips — not a fixed-width 3-col table
 * that crams long uppercase labels. */

/* ─────────────────────────── profile_card region ────────────────
 * Identity panel: avatar/initials + primary/secondary text,
 * optional stats strip, optional bulleted facts list. */

.profile-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  min-width: 0;
}

.profile-identity {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  min-width: 0;
}

.profile-avatar,
.profile-initials {
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-full);
  background: var(--colour-bg);
  flex-shrink: 0;
}

.profile-avatar {
  object-fit: cover;
}

.profile-initials {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-text-muted);
}

.profile-text {
  flex: 1 1 0;
  min-width: 0;
}

.profile-primary {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.profile-secondary {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: 0.125rem;
}

/* Stats — flexible KPI strip. Columns size to available space
 * (auto-fit) rather than forcing three equal table cells. Value sits
 * above the label so long labels wrap under a shared numeric baseline
 * instead of shoving values out of horizontal alignment. */
.profile-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(4.5rem, 1fr));
  gap: var(--space-md) var(--space-lg);
  border-block-start: 1px solid var(--colour-border);
  padding-block-start: var(--space-md);
  margin: 0;
  min-width: 0;
}

.profile-stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
  min-width: 0;
  margin: 0;
}

.profile-stat-value {
  order: -1; /* number first — shared baseline across the strip */
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
  color: var(--colour-text);
  margin: 0;
}

.profile-stat-label {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  /* Sentence case + normal tracking: uppercase + letter-spacing made
   * multi-word labels (OPEN WORK / ORDERS) look like a broken table. */
  text-transform: none;
  letter-spacing: normal;
  line-height: 1.3;
  color: var(--colour-text-muted);
  margin: 0;
  text-wrap: balance;
  overflow-wrap: break-word;
  max-width: 100%;
}

.profile-facts {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  border-block-start: 1px solid var(--colour-border);
  padding-block-start: var(--space-md);
  font-size: var(--text-xs);
  color: var(--colour-text);
  list-style: none;
  margin: 0;
  padding-inline-start: 0;
}

.profile-fact {
  display: flex;
  gap: var(--space-sm);
  line-height: var(--leading-tight);
}

.profile-fact-bullet {
  color: var(--colour-text-muted);
  flex-shrink: 0;
}

.profile-fact-text {
  min-width: 0;
}

}
/* ── components/two-factor.css ── */
@layer components {
/* HYPERPART: two-factor */
/* Promoted from Dazzle fragments.css (Tier A2, fleet convergence) —
 * the 2FA setup/settings/challenge chrome (two_factor_views.py
 * scaffolds + the 2fa-*.js-created rows/pills). Deltas from the
 * source, each fixing a latent mismatch:
 *   - .auth-card-title/-subtitle: the scaffolds emit these names,
 *     but the rules were named .auth-title/-subtitle (never
 *     matched — the 2FA card heading rendered unstyled). Renamed to
 *     the emitted classes.
 *   - .auth-qr-container img: the setup JS appends a CLASSLESS
 *     <img>; the old .auth-qr-image rule never matched. Restyled
 *     via the container.
 *   - .auth-page: gains centering for standalone HM consumers. In
 *     Dazzle's own bundle the later site-sections.css gradient rule
 *     still wins (same layer, later) — the site-sections auth-block
 *     collision is filed as #1549.
 * The dead subset (product/title/subtitle/link-muted/link-button and pseudo-states /
 * qr/qr-image/qr-secret) was deleted at source. */

.auth-page {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl) var(--space-md);
  background: var(--colour-bg);
}

.auth-card-title {
  text-align: center;
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin-block-end: var(--space-lg);
}

.auth-card-subtitle {
  text-align: center;
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  margin-block: calc(var(--space-md) * -0.5) var(--space-md);
}

.auth-qr-container img {
  inline-size: 12rem;
  block-size: 12rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
}

.auth-card {
  width: 100%;
  max-width: 24rem;
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-lg);
}

.auth-error {
  display: none;  /* revealed by JS clearing the hidden attribute */
  flex-direction: row;
  gap: var(--space-sm);
  padding: var(--space-md);
  margin-block-end: var(--space-md);
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-danger);
  background: color-mix(in oklch, var(--colour-danger) 8%, transparent);
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.auth-error:not([hidden]) {
  display: flex;
}

/* Success alert — mirror of .auth-error but brand-toned. Used by
 * the forgot-password flow's "email sent" confirmation. */
.auth-success {
  display: none;
  flex-direction: row;
  gap: var(--space-sm);
  padding: var(--space-md);
  margin-block-end: var(--space-md);
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-brand);
  background: color-mix(in oklch, var(--colour-brand) 8%, transparent);
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.auth-success:not([hidden]) {
  display: flex;
}

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

.auth-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.auth-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
}

.auth-submit {
  inline-size: 100%;
  margin-block-start: var(--space-md);
  block-size: 2.25rem;
}

.auth-input-code {
  inline-size: 100%;
  block-size: 2.5rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  color: var(--colour-text);
  font-size: 1.25rem;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.4em;
  text-align: center;
  transition: box-shadow var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out);
}

.auth-input-code::placeholder {
  color: var(--colour-text-muted);
  letter-spacing: 0.4em;
}

.auth-input-code:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
}

.auth-section-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin-block-end: 0.25rem;
}

.auth-section-body {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  margin-block-end: var(--space-md);
}

.auth-qr-container {
  display: flex;
  justify-content: center;
  margin-block: var(--space-md);
}

.auth-secret-inline {
  font-family: var(--font-mono, monospace);
  font-size: 0.6875rem;
  color: var(--colour-text);
  background: var(--colour-bg);
  padding-inline: 0.25rem;
  padding-block: 0.125rem;
  border-radius: var(--radius-xs, 0.1875rem);
}

.auth-hr {
  border: 0;
  border-block-start: 1px solid var(--colour-border);
  margin-block: var(--space-xl);
}

.auth-recovery-alert {
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  background: var(--colour-bg);
  padding: var(--space-md);
}

.auth-recovery-alert-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}

.auth-recovery-alert-body {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  margin-block-start: 0.25rem;
}

.auth-recovery-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-sm);
  margin-block-start: var(--space-md);
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
}

.auth-recovery-pill {
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  padding-inline: var(--space-sm);
  padding-block: var(--space-sm);
  text-align: center;
  font-size: var(--text-xs);
  color: var(--colour-text);
}

.auth-back-link {
  display: block;
  inline-size: 100%;
  block-size: 2.25rem;
  line-height: 2.25rem;
  text-align: center;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  text-decoration: none;
  margin-block-start: var(--space-xl);
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.auth-back-link:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.auth-status-loading {
  text-align: center;
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  margin-block-start: var(--space-sm);
}

.auth-status-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-block: var(--space-md);
  border-block-end: 1px solid var(--colour-border);
}

.auth-status-row.is-last {
  border-block-end: 0;
}

.auth-status-label {
  font-size: var(--text-sm);
  color: var(--colour-text);
}

}
/* ── components/pdf.css ── */
@layer components {
/* HYPERPART: pdf */
/*
 * pdf — the hx-pdf viewing shell. The application owns the chrome;
 * PDF.js paints into the viewer region's canvas. Authored for the HM
 * promotion of the ratified hx-pdf spec (§7 markup contract); the
 * legacy #946 pdf-viewer chrome retires onto this in P3.
 */

.pdf {
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  overflow: hidden;
}

.pdf-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border-block-end: 1px solid var(--colour-border);
}

.pdf-toolbar label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.pdf-page-input {
  width: 3.5rem;
  height: 1.75rem;
  text-align: center;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  color: var(--colour-text);
  font-size: var(--text-sm);
}

.pdf-page-count {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.pdf-toolbar-spacer {
  flex: 1 1 0;
}

.pdf-status {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  padding-inline: var(--space-md);
}

.pdf-status:empty {
  display: none;
}

.pdf-stage {
  overflow: auto;
  background: var(--colour-bg);
  padding: var(--space-md);
  min-height: 12rem;
  max-height: 40rem;
}

.pdf-stage canvas {
  display: block;
  margin-inline: auto;
  box-shadow: var(--shadow-md, 0 2px 8px oklch(0% 0 0 / 0.15));
  background: white;
}

[data-pdf-error="true"] .pdf-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--colour-text-muted);
  font-size: var(--text-sm);
}

}
/* ── components/pdf-viewer.css ── */
@layer components {
/*
 * pdf-viewer.css — PDF detail-view chrome (HaTchi-MaXchi).
 *
 * The full PDF viewer surface: header (back / title / prev-next nav), body +
 * embed, optional footer slot, right-side panels (checkbox-toggled), and the
 * keyboard cheat-sheet <dialog>. Sits on top of the HM `pdf` embed primitive
 * (pdf.css: .pdf / .pdf-stage / toolbar), which it references as
 * descendants. Bespoke a11y/geometry values (1.75rem controls, 360px panel,
 * visually-hidden clip, focus outlines) are intentionally non-token.
 * Migrated from Dazzle into HM (HMC-015, aggressive-mode convergence 2026-07-08).
 */
.pdf-viewer {
  display: grid;
  grid-template-rows: auto 1fr auto;
  height: 100vh;
  background: var(--colour-bg);
  color: var(--colour-text);
}

/* ── Header ──────────────────────────────────────────────────── */

.pdf-viewer-header {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-md);
  padding-block: var(--space-sm);
  padding-inline: var(--space-lg);
  background: var(--colour-surface);
  border-block-end: 1px solid var(--colour-border);
}

.pdf-viewer-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding-block: var(--space-xs);
  padding-inline: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition: background var(--duration-base) var(--ease-out);
}

.pdf-viewer-back:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.pdf-viewer-back:focus-visible {
  /* Framework focus-ring convention — same shape as
   * `.workspace-context-select`, `.toggle-item`, etc. The
   * brand-tinted box-shadow stays visible in both light and dark
   * themes; a bare `outline: none` left keyboard users guessing
   * where focus lived (#942 cycle 1f). */
  background: var(--colour-bg);
  color: var(--colour-text);
  outline: none;
  box-shadow: 0 0 0 2px var(--colour-brand);
}

.pdf-viewer-title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  text-align: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.pdf-viewer-nav {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

.pdf-viewer-nav-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 1.75rem;
  block-size: 1.75rem;
  color: var(--colour-text-muted);
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition: background var(--duration-base) var(--ease-out);
}

.pdf-viewer-nav-link:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.pdf-viewer-nav-link:focus-visible {
  /* Same framework focus-ring convention as the back button. */
  background: var(--colour-bg);
  color: var(--colour-text);
  outline: none;
  box-shadow: 0 0 0 2px var(--colour-brand);
}

/* Forced-colors / Windows High Contrast mode: the box-shadow rings
 * above are suppressed by the user agent, leaving keyboard users
 * with no focus indicator. Re-introduce a real outline using the
 * system ``Highlight`` colour so the chrome stays accessible.
 * Same fallback applies to both interactive elements. */
@media (forced-colors: active) {
  .pdf-viewer-back:focus-visible,
  .pdf-viewer-nav-link:focus-visible {
    outline: 2px solid Highlight;
    outline-offset: 2px;
    box-shadow: none;
  }
}

.pdf-viewer-nav-link[aria-disabled="true"] {
  opacity: 0.4;
  pointer-events: none;
}

/* ── Body ────────────────────────────────────────────────────── */

.pdf-viewer-body {
  position: relative;
  /* Semantic token, not a raw ramp value — flips correctly under
   * `[data-theme="dark"]` (binding from #938). Earlier draft used
   * `var(--neutral-100)` which is light-only and left a jarring
   * pale panel inside an otherwise-dark viewer; #942 cycle 1d's
   * dark-mode parity gates caught it. The body sits a step darker
   * than the chrome surfaces in both themes so the content area
   * reads as distinct from the header/footer. */
  background: var(--colour-bg);
  overflow: hidden;
}

.pdf-viewer-embed {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  border: 0;
}

/* hx-pdf P3: the embed slot now hosts the HM pdf Hyperpart. Inside the
 * full-page grid the SECTION becomes a grid (toolbar/status/stage) so
 * the stage takes the remainder — a bare block-size:100% stage would
 * overflow by the toolbar height and the section's overflow:hidden
 * would clip the document's bottom band. Corners squared: the region
 * is full-bleed here. */
.pdf-viewer .pdf.pdf-viewer-embed {
  display: grid;
  grid-template-rows: auto auto 1fr;
  border-radius: 0;
}

.pdf-viewer .pdf-stage {
  max-height: none;
  block-size: auto;
  min-block-size: 0;
}

/* ── Footer ──────────────────────────────────────────────────── */

.pdf-viewer-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-xs);
  padding-block: var(--space-xs);
  padding-inline: var(--space-lg);
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  background: var(--colour-surface);
  border-block-start: 1px solid var(--colour-border);
}

.pdf-viewer-kbd {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  padding-block: 0;
  padding-inline: var(--space-xs);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
}

.pdf-viewer-kbd-label {
  margin-inline-end: var(--space-md);
}

.pdf-viewer-kbd-sep {
  color: var(--colour-border);
}

/* #944 cycle 6a — optional project-supplied footer slot.
 * Renders before the keyboard legend in the same flex row. The
 * slot's children are project-shaped (page counter, zoom buttons,
 * pill toggles, etc); chrome stays minimal so projects don't have
 * to compete with framework layout. */
.pdf-viewer-footer-slot {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* Hide the back-link label on narrow viewports to keep the header
 * tidy — the icon alone communicates the action. */
@media (max-width: 640px) {
  .pdf-viewer-back-label {
    display: none;
  }
}

/* ── #942 cycle 2a — right-side panel ─────────────────────────
 *
 * Optional slide-in panel for related-entity content. Toggle is a
 * hidden checkbox; CSS-only show/hide via the parent's :has(:checked).
 * Keeps the wrapper free of x-data (preserves the widget contract
 * from #940 — the bridge mounts the keyboard handler; Alpine doesn't
 * touch this node).
 *
 * Drawer behaviour: panel `position: absolute`, slides in from the
 * trailing edge; default `transform: translateX(100%)` hides it
 * off-screen, `:has(:checked)` swaps to `translateX(0)`. CSS
 * transition respects the global `prefers-reduced-motion` rule
 * already in reset.css. Mobile (≤768px): panel goes full-width
 * over the content (drawer pattern). */

.pdf-viewer-panel-toggle {
  /* Visually hidden but keyboard-accessible (the keyboard handler
   * .click()s it). Don't use display:none — that removes the
   * input from the DOM, breaking :has(:checked). */
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.pdf-viewer-panel {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  inline-size: 360px;
  display: grid;
  grid-template-rows: auto 1fr;
  background: var(--colour-surface);
  border-inline-start: 1px solid var(--colour-border);
  box-shadow: -4px 0 12px hsl(0 0% 0% / 0.06);
  transform: translateX(100%);
  transition: transform var(--duration-base) var(--ease-out);
  z-index: 1;
}

/* #943 cycle 5a — multi-panel exclusivity uses the adjacent-sibling
 * combinator so each toggle's checked state only affects its
 * immediately-following <aside>. The cycle 2a `:has(:checked)`
 * pattern can't disambiguate which checkbox is checked when N
 * exist, so it would slide every panel in. Adjacent-sibling does
 * the right thing for any N — including the cycle 2a single-panel
 * case (the toggle is still right before its panel). */
.pdf-viewer-panel-toggle:checked + .pdf-viewer-panel {
  transform: translateX(0);
}

.pdf-viewer-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-sm);
  padding-inline: var(--space-lg);
  background: var(--colour-bg);
  border-block-end: 1px solid var(--colour-border);
}

.pdf-viewer-panel-title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
}

.pdf-viewer-panel-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 1.75rem;
  block-size: 1.75rem;
  color: var(--colour-text-muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--duration-base) var(--ease-out);
}

.pdf-viewer-panel-close {
  /* `<button>`-styled like a borderless icon affordance. */
  background: transparent;
  border: 0;
  cursor: pointer;
}

.pdf-viewer-panel-close:hover {
  background: var(--colour-surface);
  color: var(--colour-text);
}

.pdf-viewer-panel-close:focus-visible {
  /* Same framework focus-ring convention as the back/nav buttons
   * (cycle 1f). The button is the actual focus target now (not a
   * visually-hidden checkbox), so the ring lands directly on the
   * affordance. */
  outline: none;
  box-shadow: 0 0 0 2px var(--colour-brand);
}

@media (forced-colors: active) {
  .pdf-viewer-panel-close:focus-visible {
    outline: 2px solid Highlight;
    outline-offset: 2px;
    box-shadow: none;
  }
}

.pdf-viewer-panel-body {
  overflow-y: auto;
  padding: var(--space-lg);
}

@media (max-width: 768px) {
  .pdf-viewer-panel {
    inline-size: 100%;
  }
}

/* #943 cycle 5c — keyboard cheat-sheet overlay. Renders via the
 * native <dialog> element so backdrop + focus trap come for free. */
.pdf-viewer-help {
  inline-size: min(420px, calc(100vw - var(--space-2xl)));
  max-block-size: 80vh;
  padding: 0;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  color: var(--colour-text);
  box-shadow: 0 16px 48px hsl(0 0% 0% / 0.16);
}

.pdf-viewer-help::backdrop {
  background: hsl(0 0% 0% / 0.32);
}

.pdf-viewer-help-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-sm);
  padding-inline: var(--space-lg);
  border-block-end: 1px solid var(--colour-border);
}

.pdf-viewer-help-title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
}

.pdf-viewer-help-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 1.75rem;
  block-size: 1.75rem;
  background: transparent;
  border: 0;
  color: var(--colour-text-muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.pdf-viewer-help-close:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.pdf-viewer-help-close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--colour-brand);
}

@media (forced-colors: active) {
  .pdf-viewer-help-close:focus-visible {
    outline: 2px solid Highlight;
    outline-offset: 2px;
    box-shadow: none;
  }
}

.pdf-viewer-help-list {
  margin: 0;
  padding-block: var(--space-sm);
  padding-inline: 0;
  font-size: var(--text-sm);
}

.pdf-viewer-help-row {
  display: grid;
  grid-template-columns: 8rem 1fr;
  align-items: baseline;
  gap: var(--space-md);
  padding-block: var(--space-xs);
  padding-inline: var(--space-lg);
}

.pdf-viewer-help-row dt {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.pdf-viewer-help-row dd {
  margin: 0;
  color: var(--colour-text-muted);
}

.pdf-viewer-help-row kbd {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  padding-block: 0;
  padding-inline: var(--space-xs);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-sm);
}

}
/* ── components/tabs.css ── */
@layer components {
/* HYPERPART: tabs */
/*
 * tabs — a lazy tab strip. An honest link-strip, NOT a half-built ARIA
 * tabs widget: the tabs are <button type="button">s (natively focusable +
 * Tab-navigable) with `aria-current="true"` on the active one — no
 * `role="tablist"` it can't back with roving-tabindex/arrow-key navigation
 * (the candor HM already applies to menu: "disclosure, not ARIA menu").
 *
 * Taxonomy (agent-facing):
 *   - tab (class `__tab`) — the clickable label in the strip (a button)
 *   - tab list (`__list`) — the strip container
 *   - panel (`__panel`) — the content region shown for the current tab
 *
 * Why <button>, not <a>: in-page state change (which panel is visible), not
 * navigation to a new URL. Links would imply a destination; buttons are the
 * platform control for "activate something here". See stem
 * selection-strip-honest.
 *
 * Indicator geometry: the active mark is a 2px bottom border. base.css gives
 * every button `border-radius: var(--radius-sm)`, which would curve that
 * underline at the corners — incongruous for a flush tab rail. Force
 * `border-radius: 0` so the indicator is a straight bar (stem
 * selection-strip-honest › indicator).
 *
 * Each panel lazy-loads via htmx (`hx-get` on `intersect once`); tabs.js
 * toggles active tab + shows/hides panels, scoped to each `.tabs` root.
 */

.tabs {
  display: block;
}

.tabs__list {
  display: flex;
  border-block-end: 1px solid var(--colour-border);
  margin-block-end: var(--space-md);
}

.tabs__tab {
  /* Reset base button chrome — this is strip chrome, not a filled action */
  appearance: none;
  border-radius: 0; /* straight active underline; do not inherit radius-sm */
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-regular, 400);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  border-block-end: 2px solid transparent;
  margin-block-end: -1px; /* overlap the list border so the active bar is flush */
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out);
}

.tabs__tab:hover {
  color: var(--colour-text);
  background: transparent; /* beat base button:hover surface fill */
}

.tabs__tab:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: calc(var(--focus-ring-offset) * -1);
}

.tabs__tab[aria-current="true"] {
  border-block-end-color: var(--colour-brand);
  color: var(--colour-text);
  font-weight: var(--weight-medium);
  /* corners stay square — curved indicator is a presentation bug */
  border-radius: 0;
}

.tabs__loading {
  display: flex;
  justify-content: center;
  padding-block: var(--space-2xl);
  color: var(--colour-text-muted);
}

.tabs__loading > svg {
  width: 1rem;
  height: 1rem;
  animation: spin 1s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .tabs__tab { transition: none; }
  .tabs__loading > svg { animation: none; }
}

}
/* ── components/touch-targets.css ── */
@layer components {
/*
 * touch-targets.css — mobile touch hit-area enforcement (#958 cycle 1).
 *
 * Bumps every interactive primitive's min-height + min-width to
 * `--touch-target-min` (44px, Apple HIG) on touch devices. Desktop
 * keeps its dense pixel-perfect sizing.
 *
 * Why `@media (pointer: coarse)` rather than `(max-width: ...)`?
 *   - Touch tablets in landscape look "wide" to width queries but
 *     still need the bigger hit area.
 *   - Mouse users on small windows DON'T need 44px buttons (precise
 *     pointer).
 *   - `pointer: coarse` is the explicit "primary input is finger"
 *     signal, supported in all modern browsers.
 *
 * Selectors covered:
 *   - `.button` (and primary/ghost/outline/destructive variants)
 *   - `.icon-button`
 *   - `.card-action-button`
 *   - `.list-csv-button` (list-region CSV export, 28px at rest)
 *   - `.sidebar-action-button`
 *   - `.add-card-button`
 *   - Close buttons: `.drawer__close`, `.pdf-viewer-help-close`,
 *     `.pdf-viewer-panel-close`
 *
 * Wrapped in @layer components so component-specific rules win when
 * they need to.
 */
@layer components {
  @media (pointer: coarse) {
    /* .button covers every button — variants are data-variant on the
       same base element, so no per-variant selector is needed here. */
    .button,
    .icon-button,
    .card-action-button,
    .list-csv-button,
    .sidebar-action-button,
    .add-card-button {
      min-height: var(--touch-target-min);
      min-width: var(--touch-target-min);
    }

    /* Close-X buttons are typically small icon-only — most likely
     * to under-deliver hit area. Bump explicitly. */
    .drawer__close,
    .pdf-viewer-help-close,
    .pdf-viewer-panel-close {
      min-height: var(--touch-target-min);
      min-width: var(--touch-target-min);
    }

    /* Bare native buttons / links inside .button regions still
     * benefit — a `<button>` without a Dazzle class shouldn't fall
     * below 44px on touch. */
    button,
    [role="button"],
    a.link {
      min-height: var(--touch-target-min);
    }
  }
}

}
/* ── components/mobile-scroll.css ── */
@layer components {
/*
 * mobile-scroll.css — touch-friendly scroll containment (HaTchi-MaXchi).
 *
 * Two properties make scrollable regions feel native on touch:
 * `-webkit-overflow-scrolling: touch` (iOS momentum) and
 * `overscroll-behavior: contain` (no scroll-chain / browser-back swipe).
 * Applied to the scroll viewports (card/detail/drawer/app/form bodies,
 * pdf-viewer panel) + the `.scroll`/`.scroll-x`/`.table-wrap`
 * utilities. Migrated from Dazzle into HM (HMC-022, aggressive-mode
 * convergence 2026-07-09). NOTE: no `@layer components {` wrapper — HM build adds it.
 */
  .card-body,
  .drawer-content,
  .detail-card-body,
  .app-content,
  .app-body,
  .drawer__body,
  .pdf-viewer-panel,
  .scroll {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }

  /* Horizontal-scroll containers (tables, charts, region grids).
   * `overscroll-behavior-x: contain` prevents the browser-back
   * "swipe-from-edge" from firing when the user pans inside a
   * wide table — common iOS frustration where horizontal scroll
   * navigates history instead. */
  .table-wrap,
  .scroll-x {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }

}
/* ── components/fragments.css ── */
@layer components {
/*
 * fragments.css — shared HTML-fragment chrome (HaTchi-MaXchi).
 *
 * Assorted fragment/region primitives: sentinel + loading spinners, command
 * palette, toast/bulk/stepper/date-range/context-menu/slide-over/search-flow/
 * 2FA/a11y/related/tooltip/inline-edit/ref-cell/search-input/search-select/
 * app-shell/toast-stack/form-chrome/review-queue/modal/island/error-pages/
 * card-picker/pipeline-steps/experience-shell families. Heavily HM-token-based
 * (293 var(--…) refs). Migrated wholesale from Dazzle into HM (HMC-016,
 * aggressive-mode convergence 2026-07-08); the redundant `.empty-dense`
 * (HM detail.css owns the canonical one) was dropped in the move.
 */

/* ─────────────────────────── empty_state fragment ───────────────
 * The `.empty-state` primitive is now owned by the HM
 * `empty-state` Hyperpart (components/fragment-primitives.css:
 * base container + `__icon`/`__title`/`__description`/`__action`),
 * ingested into the dist. All emitters (the typed substrate + the
 * table/chart read-only empties) emit the `__` form; the bespoke
 * `.empty-state`/`-icon`/`-message`/`-cta` rules were deleted in
 * the Bucket B empty-state convergence (build-to-replace, ADR-0003). */

/* table_pagination — the `.pagination*` family moved to the HM `pagination`
 * Hyperpart (packages/hatchi-maxchi/components/pagination.css) as the canonical
 * definition; Dazzle's `_emit_pagination` consumes it unchanged (byte-identical
 * values + a focus ring). Retired here 2026-07-04. The data-table row / filter
 * bar / bulk-action families (`.tr-*`, `.filter-*`, `.bulk-*`) likewise
 * moved to the HM `table.css` (grid Hyperpart, Slice 0b, 2026-07-04). */

/* ─────────────────────────── toast fragment ─────────────────────
 * MOVED to components/toast.css (HYPERPART: toast). Stack host,
 * title/message/actions, and level tones live there. */

/* ─────────────────────────── bulk_actions fragment ──────────────
 * Delete-only bulk actions toolbar. Visible via Alpine x-show
 * gated on bulkCount > 0. */

/* ─────────────────────────── form_stepper + steps_indicator ─────
 * MOVED to HaTchi-MaXchi components/form-chrome.css (Tier F3, the
 * fleet-convergence directive). Same selectors, via the HM dist. */

/* ─────────────────────────── date_range_picker fragment ─────────
 * MOVED to HaTchi-MaXchi components/date-range.css (Tier F3). */

/* ─────────────────────────── detail_fields fragment ─────────────
 * Definition-list renderer used by the API read handler for
 * HTMX/browser content negotiation. Lighter version of the
 * detail_view component (no header / actions / related groups). */

.detail-fields-card {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.detail-fields-body {
  padding: var(--space-lg);
}

.detail-fields-title {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin: 0;
}

.detail-fields-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-xs) var(--space-md);
  margin-block-start: var(--space-md);
}

/* #1533: full-length prose on detail pages — the list-cell truncate
 * wrapper never appears here. pre-wrap keeps textarea paragraphs. */
.detail-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.detail-fields-key {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  text-align: end;
  padding-block: 0.25rem;
}

.detail-fields-value {
  font-size: var(--text-sm);
  padding-block: 0.25rem;
  word-break: break-all;
}

/* ─────────────────────────── context_menu fragment ──────────────
 * Right-click triggered menu. Panel positioned absolutely at cursor
 * coordinates via Alpine `:style`. */

.context-menu {
  position: relative;
}

.context-menu-panel {
  z-index: 50;
  min-width: 11rem;
  padding-block: 0.25rem;
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  box-shadow: var(--shadow-md);
  font-size: var(--text-sm);
  list-style: none;
  margin: 0;
  padding-inline-start: 0;
}

.context-menu-divider {
  margin-block: 0.25rem;
  border-block-start: 1px solid var(--colour-border);
}

.context-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-inline: var(--space-md);
  padding-block: 0.375rem;
  color: var(--colour-text);
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-out);
}

.context-menu-item:hover {
  background: var(--colour-bg);
}

.context-menu-item-icon {
  width: 0.875rem;
  height: 0.875rem;
  color: var(--colour-text-muted);
}

/* ─────────────────────────── slide-over ─────────────────────────
 * DELETED (Tier F2, 2026-07-06): the peek slide_over panel converged
 * onto the HM drawer Hyperpart (native <dialog class="drawer">,
 * packages/hatchi-maxchi/components/drawer.css) — focus trap, inert
 * background, Esc/backdrop dismissal are native. The .slideover-*
 * family had no other producers. */

/* ─────────────────────────── table_sentinel fragment ────────────
 * Infinite-scroll sentinel row. Triggers loading more rows when
 * revealed via htmx hx-trigger="revealed". The visible spinner is
 * just a sized box with animation; sr-only text lives in the
 * fragment template. */

.sentinel-cell {
  text-align: center;
  padding-block: var(--space-md);
}

.sentinel-spinner {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  animation: spin 1s linear infinite;
}

/* ─────────────────────────── form_errors fragment ───────────────
 * MOVED to HaTchi-MaXchi components/form-chrome.css (Tier F3). */

/* ─────────────────────────── search-flow fragments ──────────────
 * MOVED to HaTchi-MaXchi components/search-box.css (Tier F3). */

/* ─────────────────────────── experience_transition macro ────────
 * Single source of truth for the 5 transition-button branches in
 * experience/_content.html (#EX-053). Three style variants — primary,
 * ghost, border (default) — keyed off `data-transition-style`. */

.experience-transition {
  display: inline-flex;
  align-items: center;
  height: 2.25rem;
  padding-inline: var(--space-md);
  font-weight: var(--weight-medium);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  border: 0;
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    filter var(--duration-fast) var(--ease-out);
}

.experience-transition[data-transition-style="primary"] {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
}

.experience-transition[data-transition-style="primary"]:hover {
  filter: brightness(1.1);
}

.experience-transition[data-transition-style="ghost"] {
  background: transparent;
  color: var(--colour-text-muted);
}

.experience-transition[data-transition-style="ghost"]:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.experience-transition[data-transition-style="default"],
.experience-transition:not([data-transition-style]) {
  background: transparent;
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
}

.experience-transition[data-transition-style="default"]:hover,
.experience-transition:not([data-transition-style]):hover {
  background: var(--colour-bg);
}

/* ─────────────────────────── auth chrome (2FA views) ────────────
 * MOVED to HaTchi-MaXchi components/two-factor.css (Tier A2,
 * 2026-07-07 — with three latent-mismatch fixes: card-title/-subtitle
 * rule names, the classless QR img, and honest auth-page
 * centering). The dead subset (product/title/subtitle/link-muted/
 * link-button and pseudo-states /qr/qr-image/qr-secret — zero producers) was deleted. */

/* ─────────────────────────── a11y helpers ───────────────────────
 * Loading spinner used as an HTMX indicator. Sized + animated host
 * for the inline span. (.visually-hidden + sr-only-fallback already
 * defined in utilities.css.) */

.loading-spinner {
  display: flex;
  justify-content: center;
  padding-block: var(--space-md);
}

.loading-spinner-icon {
  display: inline-block;
  width: 1.25rem;
  height: 1.25rem;
  animation: spin 1s linear infinite;
}

/* Skip link — visually hidden by default, focusable via Tab. */
.skip-link {
  position: absolute;
  inset-block-start: -100vh;  /* off-screen by default */
  inset-inline-start: 0;
  padding: var(--space-sm);
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  border-radius: var(--radius-sm);
  z-index: 9999;
  text-decoration: none;
  transition: top var(--duration-fast) var(--ease-out);
}

.skip-link:focus,
.skip-link:focus-visible {
  inset-block-start: 0.5rem;
  inset-inline-start: 0.5rem;
}

/* ─────────────────────────── layouts/single_column ──────────────
 * Top bar + main + footer layout used by simpler app shells. */

.layout-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.layout-topbar {
  display: flex;
  align-items: center;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  background: var(--colour-surface);
  border-block-end: 1px solid var(--colour-border);
  box-shadow: var(--shadow-sm);
}

.layout-topbar-title {
  flex: 1 1 0;
}

.layout-topbar-title-text {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
}

.layout-topbar-nav {
  display: flex;
  gap: var(--space-sm);
  flex: none;
}

.layout-nav-link {
  display: inline-flex;
  align-items: center;
  height: 2rem;
  padding-inline: var(--space-md);
  border-radius: var(--radius-sm);
  color: var(--colour-text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.layout-nav-link:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

.layout-main {
  flex: 1 1 0;
  margin-inline: auto;
  padding: var(--space-md);
  max-width: 64rem;
  width: 100%;
}

@media (min-width: 64rem) {
  .layout-main {
    padding: var(--space-lg);
  }
}

.layout-footer {
  text-align: center;
  padding: var(--space-md);
  background: var(--colour-surface);
  color: var(--colour-text);
  border-block-start: 1px solid var(--colour-border);
}

/* Hide default disclosure marker */
/* ─────────────────────────── related-* fragments ────────────────
 * MOVED to HaTchi-MaXchi components/related-tables.css (W2, 2026-07-06 — the
 * fleet-convergence directive). Same selectors, via the HM dist. */

/* ─────────────────────────── tooltip_rich fragment ──────────────
 * HTML-content tooltip with x-anchor positioning. Trigger wraps
 * caller content; panel sits absolutely positioned + transitions
 * via Alpine x-show. */

/* ─────────────────────────── inline_edit fragment ───────────────
 * The InlineEdit fragment primitive's display span. */

.inline-edit {
  position: relative;
  margin-inline: -0.25rem;
  margin-block: -0.125rem;
}

/* The editor-element rules (inline-edit-input/-select/-checkbox) moved
 * to HaTchi-MaXchi table.css (HM 0.1.26 — the grid-edit.js extension owns
 * them; Dazzle consumes the HM dist). The Alpine-era bool-row/spinner/error
 * chrome died with the template in C2.3. `.inline-edit` above stays: the
 * InlineEdit fragment primitive still emits it. */

/* The `.skeleton*` placeholder family that lived here was dead (zero
 * emitters) and collided with the HM `skeleton` Hyperpart's canonical
 * `.skeleton`. Retired 2026-07-04; the live dashboard loading state is
 * `.card-skeleton` (dashboard.css), which keeps the `pulse` keyframe. */

/* ─────────────────────────── ref_cell macro ─────────────────────
 * Reference link styling — used by detail / grid / list / queue
 * regions. Three modes (link / link_stop / htmx_drawer) all render
 * the same anchor chrome. */

.ref-link {
  color: var(--colour-brand);
  text-decoration: none;
  transition: text-decoration-color var(--duration-fast) var(--ease-out);
}

.ref-link:hover {
  text-decoration: underline;
}

/* ─────────────────────────── search_input fragment ──────────────
 * DELETED (Tier F3, 2026-07-06): the search-input / search-clear
 * family had ZERO producers (py/html/js/examples/tests) — the reusable
 * search-input fragment died in an earlier convergence. The live FTS
 * search box (.search-box-*) is styled by HM search-box.css. */

/* ─────────────────────────── attention_accent macro ─────────────
 * MOVED to HaTchi-MaXchi components/timeline.css (W2 — travels with
 * its consumers: timeline bullets + kanban card-attn). */

/* ─────────────────────────── command_palette fragment ───────────
 * Spotlight-style modal triggered by Cmd+K. Backdrop covers the
 * viewport; palette card sits centred at 20vh from the top. Three
 * regions: search input, results listbox, keyboard-hint footer.
 * Selected/hover row distinction driven by .is-selected modifier
 * Alpine binds. */

.cmdk-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-block-start: 20vh;
}

.cmdk-backdrop {
  position: fixed;
  inset: 0;
  background: oklch(0% 0 0 / 0.4);
}

.cmdk-card {
  position: relative;
  width: 100%;
  max-width: 32rem;
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.cmdk-search-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-block-end: 1px solid var(--colour-border);
}

.cmdk-search-icon {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  color: var(--colour-text-muted);
}

.cmdk-search-input {
  width: 100%;
  background: transparent;
  font-size: var(--text-base);
  color: var(--colour-text);
  border: 0;
  outline: none;
}

.cmdk-search-input::placeholder {
  color: var(--colour-text-muted);
}

.cmdk-results {
  max-height: 20rem;
  overflow-y: auto;
  padding: 0.375rem;
  list-style: none;
  margin: 0;
}

.cmdk-item {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.cmdk-item:hover {
  background: var(--colour-bg);
}

.cmdk-item.is-selected {
  background: color-mix(in oklch, var(--colour-brand) 10%, transparent);
  color: var(--colour-brand);
}

.cmdk-item-icon {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}

.cmdk-item-text {
  min-width: 0;
}

.cmdk-item-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cmdk-item-group {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cmdk-empty {
  padding-inline: var(--space-md);
  padding-block: var(--space-lg);
  text-align: center;
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--colour-text-muted);
}

.cmdk-footer {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-block-start: 1px solid var(--colour-border);
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.cmdk-hint {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}

.cmdk-key {
  display: inline-flex;
  align-items: center;
  height: 1.25rem;
  padding-inline: 0.375rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--colour-border);
  background: var(--colour-bg);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  color: var(--colour-text-muted);
}

/* ─────────────────────────── search_select fragment ─────────────
 * MOVED to HaTchi-MaXchi components/search-select.css (Tier F4b),
 * where the results-panel visibility also converged from Alpine
 * x-show onto aria-expanded (search-select.js). */

/* ─────────────────────────── layouts/app_shell ──────────────────
 * MOVED to HaTchi-MaXchi components/app-shell.css (S1, 2026-07-06 —
 * the app-shell directive): the whole family (shell/content/topbar/
 * sidebar-toggle/icon-button/app-main/page-purpose/backdrop/sidebar)
 * now ships via the HM dist; Dazzle's emitted contract is unchanged. */

/* ─────────────────────────── app body chrome ────────────────────
 * <body> level chrome — full-viewport background + min-height so
 * short pages still fill the screen with the surface colour.
 * Replaces inline `min-h-screen bg-[hsl(var(--background))]`. */

.app-body {
  min-height: 100vh;
  background: var(--colour-bg);
}

/* ─────────────────────────── toast stack ────────────────────────
 * MOVED to components/toast.css (HYPERPART: toast). */

/* ─────────────────────────── form chrome (UX-016) ───────────────
 * Live subset (.form-section/-title/-note) MOVED to HaTchi-MaXchi
 * components/form-chrome.css (Tier F3). The wrapper family
 * (.form-shell/-header/-title/-body/-actions) was DELETED — its
 * producer was the legacy form_renderer.py removed in ADR-0049 Phase
 * 3b; the substrate form path never emitted it. */

/* ─────────────────────────── review queue (UX-030) ─────────────
 * One-at-a-time review chrome. Reuses .detail-* primitives for
 * the field list; only review-specific shell + progress + nav +
 * action chrome lives here. */

.review-shell {
  max-width: 56rem;  /* ~max-w-4xl */
}

.review-progress {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
}

.review-progress-text {
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.review-progress-bar {
  inline-size: 6rem;
  block-size: 0.25rem;
  border-radius: var(--radius-xs);
}

.review-nav {
  display: flex;
  justify-content: space-between;
  margin-block-end: var(--space-md);
}

.review-actions {
  display: flex;
  gap: var(--space-sm);
  justify-content: flex-end;
}

.review-notes-wrap {
  margin-block-end: var(--space-md);
}

.review-notes-wrap[hidden] {
  display: none;
}

.review-notes-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  margin-block-end: var(--space-xs);
}

.review-notes-required {
  display: inline;
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  color: var(--colour-danger);
  margin-inline-start: 0.25rem;
}

.review-notes-textarea {
  inline-size: 100%;
  min-block-size: 6rem;
  resize: vertical;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  color: var(--colour-text);
  font-size: var(--text-sm);
  transition: box-shadow var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out);
}

.review-notes-textarea::placeholder {
  color: var(--colour-text-muted);
}

.review-notes-textarea:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--colour-brand);
}

.review-empty {
  text-align: center;
  padding-block: var(--space-2xl);
}

.review-empty-icon {
  font-size: 3rem;
  margin-block-end: var(--space-sm);
}

.review-empty-title {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin-block-end: var(--space-xs);
}

.review-empty-body {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  margin-block-end: var(--space-md);
}

/* ─────────────────────────── modal ──────────────────────────────
 * DELETED (Tier F2, 2026-07-06): the dormant .modal* family — its
 * components/modal.html primitive died with the Jinja teardown and no
 * producer emits the classes. Centered modal needs are the HM dialog
 * Hyperpart (dialog.css); edge panels the HM drawer. */

/* ─────────────────────────── confirm dialog — DELETED ───────────
 * (#1546, 2026-07-07): the dormant confirm-dialog family (dialog /
 * backdrop / panel / title / message / actions / spinner rules) died
 * with its Alpine template in the Jinja teardown; the LIVE
 * confirm_action_panel wrapper chrome it accidentally provided moved
 * to the HM confirm-panel Hyperpart (components/confirm-panel.css).
 * The spin keyframes below are LIVE (loading spinners in this
 * file + dashboard.css) and deliberately survive the deletion. */

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ─────────────────────────── island (dormant) ───────────────────
 * Generic dashboard island wrapper — used by components/island.html
 * (UX-059), no production consumer. */

.island {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  padding: var(--space-lg);
}

.island-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-block-end: var(--space-md);
}

.island-title {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  margin: 0;
}

.island-skeleton {
  block-size: 8rem;
  inline-size: 100%;
  border-radius: var(--radius-sm);
}

/* ─────────────────────────── in-app error pages ─────────────────
 * 403 + 404 share centred chrome inside the app shell. Used by
 * app/403.html (#776 / #808) and app/404.html. */

.error-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-block-size: 60vh;
  padding-inline: var(--space-md);
}

.error-code {
  font-size: 3rem;
  font-weight: var(--weight-semibold);
  line-height: 1;
  color: var(--colour-text);
  margin-block-end: var(--space-md);
}

.error-message {
  font-size: var(--text-md);
  color: var(--colour-text-muted);
  margin-block-end: var(--space-md);
  text-align: center;
  max-inline-size: 30rem;
}

.error-suggestions-card,
.error-detail-card {
  inline-size: 100%;
  max-inline-size: 30rem;
  margin-block-end: var(--space-lg);
  padding: var(--space-md);
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
}

.error-suggestions-card {
  background: var(--colour-surface);
}

.error-detail-card {
  background: color-mix(in oklch, var(--colour-bg) 70%, transparent);
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.error-suggestions-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  margin-block-end: var(--space-sm);
}

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

.error-suggestion-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--colour-brand);
  text-decoration: none;
}

.error-suggestion-link:hover {
  text-decoration: underline;
}

.error-suggestion-url {
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

.error-detail-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--space-md);
  row-gap: 0.375rem;
  margin: 0;
}

.error-detail-label {
  color: var(--colour-text-muted);
}

.error-detail-chip {
  display: inline-block;
  margin-inline-end: 0.25rem;
  padding-inline: 0.25rem;
  padding-block: 0.125rem;
  border-radius: var(--radius-xs);
  background: var(--colour-bg);
  border: 1px solid var(--colour-border);
  font-size: var(--text-xs);
  font-family: var(--font-mono, monospace);
}

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

/* ─────────────────────────── workspace card picker (UX-038) ─────
 * Popover above the "Add a card" button, listing widget catalog
 * entries. Used by workspace/_card_picker.html. */

/* #982: visibility driven by `[data-show-picker]` on the ancestor
 * `.workspace[x-data="dzDashboardBuilder()"]` rather than Alpine
 * `x-show` on this element. dzDashboardBuilder mirrors `showPicker`
 * to `data-show-picker="1"` via $watch; CSS reveals the picker only
 * when the flag is set. Default state hidden so there's no flash on
 * initial paint (replaces the prior `x-cloak`-style guard). */

.card-picker {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-block-end: var(--space-sm);
  inline-size: 20rem;
  background: var(--colour-surface);
  color: var(--colour-text);
  box-shadow: var(--shadow-md);
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  padding: var(--space-md);
  z-index: 50;
  max-block-size: 16rem;
  overflow-y: auto;
  display: none;
}

.workspace[data-show-picker="1"] .card-picker {
  display: block;
}

.card-picker-title {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--colour-text-muted);
  margin-block-end: var(--space-sm);
}

.card-picker-entry {
  inline-size: 100%;
  text-align: start;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-radius: var(--radius-sm);
  background: transparent;
  border: 0;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.card-picker-entry:hover {
  background: var(--colour-bg);
}

.card-picker-display-tag {
  padding-inline: 0.375rem;
  padding-block: 0.125rem;
  border-radius: var(--radius-sm);
  background: var(--colour-bg);
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
}

.card-picker-title-text {
  color: var(--colour-text);
}

.card-picker-entity {
  font-size: 0.6875rem;
  color: var(--colour-text-muted);
  margin-inline-start: auto;
}

.card-picker-empty {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
  padding-block: var(--space-sm);
}

/* ─────────────────────────── pipeline_steps region (#890/#911) ──
 * Sequential-stage workflow visualisation. Stack vertically on
 * narrow viewports, row of cards on >=md (40rem) with chevron
 * connectors between cards. Used by workspace/regions/pipeline_steps.html. */

.pipeline-stages {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-block-start: var(--space-sm);
  list-style: none;
  padding: 0;
}

@media (min-width: 48rem) {
  .pipeline-stages {
    flex-direction: row;
  }
}

.pipeline-stage {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-md);
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
}

.pipeline-stage-label {
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--colour-text-muted);
}

.pipeline-stage-value {
  font-size: 1.5rem;
  font-weight: var(--weight-semibold);
  line-height: 1;
  color: var(--colour-text);
}

.pipeline-stage-caption {
  font-size: var(--text-xs);
  line-height: 1.3;
  color: var(--colour-text-muted);
}

.pipeline-stage-progress {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-block-start: var(--space-xs);
}

.pipeline-stage-progress-track {
  position: relative;
  flex: 1;
  block-size: 0.25rem;
  border-radius: 9999px;
  background: var(--colour-bg);
  overflow: hidden;
}

.pipeline-stage-progress-fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  background: var(--colour-brand);
  border-radius: 9999px;
  transition: width 0.2s var(--ease-out);
}

.pipeline-stage-progress-label {
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  color: var(--colour-text-muted);
  font-variant-numeric: tabular-nums;
}

.pipeline-connector {
  position: absolute;
  display: none;
  align-items: center;
  justify-content: center;
  inset-inline-end: -0.875rem;
  inset-block-start: 50%;
  transform: translateY(-50%);
  inline-size: 1.75rem;
  block-size: 1.75rem;
  z-index: 5;
  color: var(--colour-text-muted);
}

@media (min-width: 48rem) {
  .pipeline-connector {
    display: flex;
  }
}

.pipeline-connector-mobile {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-block-start: var(--space-xs);
  color: var(--colour-text-muted);
}

@media (min-width: 48rem) {
  .pipeline-connector-mobile {
    display: none;
  }
}

/* ─────────────────────────── experience shell (UX-072) ──────────
 * Step-progress + inner-surface chrome for experience flows. Used
 * by experience/_content.html. */

.experience {
  max-inline-size: 56rem;
  margin-inline: auto;
}

.experience-title {
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  color: var(--colour-text);
  margin-block-end: var(--space-xl);
}

/* Experience stepper reuses the existing .steps* family from
 * the steps_indicator fragment block above (line ~448). The pattern
 * uses .is-completed on the .steps-circle / .steps-label /
 * .steps-connector children rather than a modifier on the item. */

.experience-form-shell {
  max-inline-size: 42rem;
}

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

.experience-stage-title {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  margin-block-end: var(--space-md);
}

.experience-actions {
  display: flex;
  gap: var(--space-md);
  padding-block-start: var(--space-md);
}

.experience-actions-centered {
  justify-content: center;
}

.experience-ready {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  border-radius: var(--radius-sm);
  background: color-mix(in oklch, var(--colour-brand) 8%, transparent);
  border: 1px solid color-mix(in oklch, var(--colour-brand) 20%, transparent);
  font-size: var(--text-sm);
  color: var(--colour-text);
}

.experience-placeholder {
  border-radius: var(--radius-md);
  background: var(--colour-bg);
  padding: var(--space-2xl);
  text-align: center;
}

.experience-placeholder-title {
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  margin-block-end: var(--space-sm);
}

.experience-placeholder-body {
  color: var(--colour-text-muted);
}

}
/* ── components/dashboard-card.css ── */
@layer components {
/* Internal dashboard chrome (not a standalone gallery Hyperpart — no HYPERPART
 * marker, matching the other migrated chrome components detail/fragments/etc.). */
/* Moved from Dazzle dashboard.css (HMC-007d) — the dashboard card grid +
 * card chrome (wrapper/header/actions/titles/notice/body/skeleton/resize) +
 * add-card CTA. Byte-faithful, HM-tokenised. The pulse keyframe used by the
 * skeleton is HM's (tokens.css); .card base is unique to this file. */

/* ─────────────────────────── card grid ──────────────────────────
 * 1-col on mobile, 12-col on >=md. items-start to collapse to
 * intrinsic content height (#844). */

.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
  align-items: start;
}

@media (min-width: 48rem) {
  .dashboard-grid {
    grid-template-columns: repeat(12, minmax(0, 1fr));
  }
}

/* ─────────────────────────── card wrapper ───────────────────────
 * Layout/positioning element only. Visible card chrome lives on
 * the inner <article>. Wrapper stays square (no rounding) so the
 * focus ring traces a clean rectangle (#794). */

.card-wrapper {
  position: relative;
  outline: none;
}

.card-wrapper:focus {
  box-shadow: 0 0 0 2px var(--colour-surface), 0 0 0 4px var(--colour-brand);
}

/* Smooth transitions on all reposition/resize except while dragging
 * (the wrapper sets style.transform directly during drag). Toggled
 * via .is-animating class (Alpine sets it when not dragging and not
 * mid-drag). */
.card-wrapper.is-animating {
  transition: all var(--duration-base) var(--ease-out);
}

/* ─────────────────────────── card chrome ────────────────────────
 * The visible card surface. Default border + bg + clipping. When
 * resizing, swap to a dashed brand-coloured border. */

.card {
  border-radius: var(--radius-md);
  border: 1px solid var(--colour-border);
  background: var(--colour-surface);
  /* TASTE-2: quiet stacked shadow at rest — elevation reads as a surface,
     not decoration; hover transition already declared below. */
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  transition:
    border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}

.card.is-resizing {
  border-style: dashed;
  border-color: var(--colour-brand);
}

/* Card header — drag handle zone. */
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  cursor: grab;
  min-height: 2.25rem;
}

.card-header:active {
  cursor: grabbing;
}

.card-header.is-dragging {
  cursor: grabbing;
}

.card-titles {
  display: flex;
  flex-direction: column;
}

.card-eyebrow {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--colour-text-muted);
  line-height: 1;
}

.card-title {
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--colour-text);
  user-select: none;
  margin: 0;
}

/* Header actions — fade in on hover/focus-within of the wrapper.
 * Was opacity-0 + group-hover only; #799 set rest opacity to 0.6 so
 * touch + keyboard users can still see them. */
.card-actions {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  opacity: 0.6;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.card-wrapper:hover .card-actions,
.card-wrapper:focus-within .card-actions {
  opacity: 1;
}

.card-action-button {
  width: 1.5rem;
  height: 1.5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--colour-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.card-action-button:hover {
  background: var(--colour-bg);
  color: var(--colour-text);
}

/* Notice band — between header and body. Layout shape here; the tone tints
 * (below) travel with the component (migrated from Dazzle tones.css, HMC-005b
 * 2026-07-09 — the last of the tones tint families). `.notice-band` is the
 * visual carrier the emitter co-emits with `.card-notice`. */
.card-notice {
  margin-inline: var(--space-md);
  margin-block-end: var(--space-sm);
  padding-inline: var(--space-sm);
  padding-block: var(--space-xs);
  border-radius: var(--radius-sm);
  border-inline-start-width: 2px;
  border-inline-start-style: solid;
}

/* Notice-band tone tints — background + left-rail colour per data-notice-tone.
 * Static rules (not Tailwind JIT) so IR-render-time tone class strings still
 * paint; tokens follow any project theme override. */
.notice-band[data-notice-tone="positive"] {
  background: color-mix(in oklab, var(--colour-success) 8%, transparent);
  border-left-color: var(--colour-success);
}
.notice-band[data-notice-tone="warning"] {
  background: color-mix(in oklab, var(--colour-warning) 10%, transparent);
  border-left-color: var(--colour-warning);
}
.notice-band[data-notice-tone="destructive"] {
  background: color-mix(in oklab, var(--colour-danger) 8%, transparent);
  border-left-color: var(--colour-danger);
}
.notice-band[data-notice-tone="accent"] {
  background: color-mix(in oklab, var(--colour-brand) 8%, transparent);
  border-left-color: var(--colour-brand);
}
.notice-band[data-notice-tone="neutral"],
.notice-band:not([data-notice-tone]) {
  background: color-mix(in oklab, var(--colour-surface-muted) 40%, transparent);
  border-left-color: var(--colour-border);
}

.card-notice-title {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  color: var(--colour-text);
}

.card-notice-body {
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--colour-text-muted);
  margin-block-start: 0.125rem;
}

/* Card body — HTMX-loaded region content slot.
 *
 * #962 — `min-height` per display mode reserves space before the htmx
 * region fetch lands so the page doesn't bounce as content arrives.
 * Web Vitals classifies CLS > 0.1 as "needs improvement"; pre-fix
 * surveyed surfaces hit 0.18–0.37. If content exceeds the
 * reservation, only that card grows (one shift, bounded). If content
 * fits, no shift.
 *
 * Selector values match `DisplayMode` (src/dazzle/core/ir/workspaces.py)
 * — lowercased by the template's `| lower` filter. Adding a new
 * display mode? Either add a rule here OR add the value to the
 * fall-through allow-list in test_workspace_cls_reservation.py.
 */
.card-body {
  padding-inline: var(--space-md);
  padding-block-end: var(--space-md);
  /* No default min-height: static / gallery cards (KPI tiles, content) must
   * size to content. Workspace HTMX regions always set data-display and use
   * the reservation rules below for CLS (#962). */
}

/* CLS reservation — only when a workspace DisplayMode is declared. */
.card-body[data-display] {
  min-height: 280px; /* default — matches LIST / TABBED_LIST shape */
}

.card-body[data-display="summary"],
.card-body[data-display="summary_row"] {
  min-height: 96px;
}

.card-body[data-display="metrics"],
.card-body[data-display="metric"] {
  min-height: 140px;
}

/* Queue rows provide their own vertical padding; the generic card body's
 * top padding otherwise creates an empty band below the workspace title. */
.card-body[data-display="queue"] {
  padding-block-start: var(--space-sm);
}

.card-body[data-display="bar_chart"],
.card-body[data-display="chart"],
.card-body[data-display="line_chart"],
.card-body[data-display="bar_track"],
.card-body[data-display="bullet"],
.card-body[data-display="funnel_chart"],
.card-body[data-display="heatmap"],
.card-body[data-display="radar"] {
  min-height: 280px;
}

.card-body[data-display="kanban"] {
  min-height: 480px;
}

.card-body[data-display="diagram"] {
  min-height: 360px;
}

.card-body[data-display="profile_card"] {
  min-height: 200px;
}

.card-body[data-display="action_grid"],
.card-body[data-display="pipeline_steps"],
.card-body[data-display="status_list"] {
  min-height: 200px;
}

.card-body[data-display="pivot_table"],
.card-body[data-display="timeline"] {
  min-height: 320px;
}

/* Skeleton loader (shown until HTMX swap completes) */
.card-skeleton {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

.card-skeleton-line {
  height: 1rem;
  background: var(--colour-bg);
  border-radius: var(--radius-sm);
}

.card-skeleton-line.is-thin {
  height: 0.75rem;
}

.card-skeleton-line.w-3-4 { width: 75%; }
.card-skeleton-line.w-5-6 { width: 83.333%; }

/* @keyframes pulse dropped — HM tokens.css owns it (identical). */

/* Resize handle — right edge. Same fade-on-hover pattern as the
 * action buttons. */
.card-resize {
  position: absolute;
  top: 0;
  inset-inline-end: 0;
  width: 0.375rem;
  height: 100%;
  cursor: col-resize;
  opacity: 0;
  transition:
    opacity var(--duration-fast) var(--ease-out),
    background var(--duration-fast) var(--ease-out);
  border-end-end-radius: var(--radius-md);
  border-start-end-radius: var(--radius-md);
}

.card-wrapper:hover .card-resize,
.card-wrapper:focus-within .card-resize {
  opacity: 1;
}

.card-resize:hover {
  background: color-mix(in oklch, var(--colour-brand) 30%, transparent);
}

/* ─────────────────────────── add-card CTA ───────────────────────
 * Centred dashed-border button below the grid. Picker pops up
 * relative to it (positioning lives in the picker template). */

.add-card-row {
  position: relative;
  display: flex;
  justify-content: center;
  margin-block-start: var(--space-md);
}

.add-card-button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  height: 2rem;
  padding-inline: var(--space-lg);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  border-radius: var(--radius-md);
  border: 2px dashed var(--colour-border);
  color: var(--colour-text-muted);
  background: transparent;
  cursor: pointer;
  transition:
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.add-card-button:hover {
  border-color: var(--colour-brand);
  color: var(--colour-text);
}

}
/* ── components/sitespec.css ── */
@layer components {
/* HaTchi-MaXchi — Sitespec / marketing page sections (hero, features, pricing, FAQ, testimonials, stats, CTA, steps, logos, comparison, cards, trust-bar, team, backgrounds). Ported from Dazzle site-sections.css (HM-sitespec 1B, 2026-07-09) — theme-agnostic structure; aesthetic families reflavor via tokens. Authored with  prefix (build keeps it for Dazzle, strips for standalone). */

/* =============================================================================
   Hero Section
   ============================================================================= */

.section-hero {
  background: linear-gradient(135deg, var(--dz-hero-bg-from) 0%, var(--dz-hero-bg-to) 100%);
  color: white;
  padding: var(--dz-spacing-hero-y, 6rem) 1.5rem;
  text-align: center;
  position: relative;
  overflow: hidden;
}

.section-hero::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: radial-gradient(circle at 30% 70%, oklch(1 0 0 / 0.1) 0%, transparent 50%);
  pointer-events: none;
}

/* Hero with Media Image — flex on the section to create two columns */
.hero-with-media {
  display: flex;
  align-items: center;
  gap: 3rem;
}

.hero-with-media .section-content {
  flex: 1;
  text-align: left;
}

.hero-with-media .cta-group {
  justify-content: flex-start;
}

.hero-media {
  flex: 1;
  max-width: 50%;
}

.hero-image {
  width: 100%;
  height: auto;
  border-radius: var(--dz-radius-lg, 1rem);
  box-shadow: var(--dz-shadow-xl, 0 20px 50px -12px oklch(0 0 0 / 0.3));
}

@media (max-width: 768px) {
  .hero-with-media {
    flex-direction: column;
    text-align: center;
  }

  .hero-with-media .section-content {
    text-align: center;
  }

  .hero-with-media .cta-group {
    justify-content: center;
  }

  .hero-media {
    max-width: 100%;
    order: -1;
  }
}

/* =============================================================================
   Features Section
   ============================================================================= */

.section-features {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
  background: var(--p, oklch(0.99 0 0));
}

/* =============================================================================
   Pricing Section
   ============================================================================= */

.section-pricing {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
  background: var(--dz-section-alt-bg, oklch(0.97 0.01 260));
}

/* =============================================================================
   FAQ Section
   ============================================================================= */

.section-faq {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
}

/* =============================================================================
   Testimonials Section
   ============================================================================= */

.section-testimonials {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
  background: var(--dz-section-alt-bg, oklch(0.97 0.01 260));
}

/* =============================================================================
   Stats Section
   ============================================================================= */

.section-stats {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
  background: linear-gradient(135deg, var(--dz-hero-bg-from) 0%, var(--dz-hero-bg-to) 100%);
  color: white;
}

/* =============================================================================
   CTA Section
   ============================================================================= */

.section-cta {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
  background: linear-gradient(135deg, var(--dz-hero-bg-from) 0%, var(--dz-hero-bg-to) 100%);
  color: white;
  text-align: center;
}

.section-cta h2 {
  font-size: var(--dz-font-size-cta-headline, 3rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: white;
}

.section-cta .button[data-variant="primary"] {
  background: var(--dz-cta-fill);
  color: var(--dz-cta-text);
  border: none;
  font-weight: 700;
  font-size: 1.0625rem;
  padding: 0.875rem 1.75rem;
  border-radius: var(--dz-radius-button, 0.5rem);
  box-shadow: var(--dz-cta-shadow);
  transition: transform var(--dz-transition-fast, 150ms), box-shadow var(--dz-transition-fast, 150ms);
}

.section-cta .button[data-variant="primary"]:hover {
  background: var(--dz-cta-fill-hover);
  box-shadow: var(--dz-cta-shadow-hover);
  transform: translateY(-1px);
}

/* =============================================================================
   Steps Section
   ============================================================================= */

.section-steps {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
}

/* =============================================================================
   Logo Cloud Section
   ============================================================================= */

.section-logo-cloud {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
  background: var(--dz-section-alt-bg, oklch(0.97 0.01 260));
  text-align: center;
}

/* =============================================================================
   Comparison Section
   ============================================================================= */

.section-comparison {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
}

.comparison-wrapper {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.comparison-table {
  width: 100%;
  border-collapse: collapse;
  text-align: center;
}

.comparison-table th,
.comparison-table td {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--dz-border-subtle);
}

.comparison-table thead th {
  font-weight: 600;
  font-size: var(--text-base);
  padding-bottom: 1rem;
}

.comparison-feature {
  text-align: left;
  font-weight: 500;
}

.comparison-highlighted {
  background: var(--dz-feature-icon-bg, oklch(0.92 0.03 260));
}

/* =============================================================================
   Value Highlight Section
   ============================================================================= */

.section-value-highlight {
  padding: var(--dz-spacing-hero-y, 6rem) 1.5rem;
  text-align: center;
  background: var(--dz-section-alt-bg, oklch(0.97 0.01 260));
}

.value-headline {
  font-size: var(--dz-font-size-hero-headline, 3.5rem);
  font-weight: var(--dz-font-weight-hero-headline, 800);
  line-height: var(--dz-line-height-hero-headline, 1.1);
  letter-spacing: var(--dz-letter-spacing-hero-headline, -0.02em);
}

.value-body {
  max-width: 40rem;
  margin: 0 auto 2rem;
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--dz-text-body);
}

/* =============================================================================
   Split Content Section
   ============================================================================= */

.section-split-content {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
}

.split-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
  align-items: center;
}

.split--reversed .split-grid {
  direction: rtl;
}

.split--reversed .split-grid > * {
  direction: ltr;
}

.split-text h2 {
  text-align: left;
}

.split-text p {
  color: var(--dz-text-body);
}

.cta-group--left {
  justify-content: flex-start;
}

.split-media img {
  width: 100%;
  height: auto;
  border-radius: var(--dz-radius-lg, 1rem);
  box-shadow: var(--dz-shadow-card, 0 1px 3px oklch(0 0 0 / 0.1));
}

@media (max-width: 768px) {
  .split-grid {
    grid-template-columns: 1fr;
  }

  .split--reversed .split-grid {
    direction: ltr;
  }

  .split-text h2 {
    text-align: center;
  }

  .cta-group--left {
    justify-content: center;
  }
}

/* =============================================================================
   Card Grid Section
   ============================================================================= */

.section-card-grid {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
  background: var(--dz-section-alt-bg, oklch(0.97 0.01 260));
}

.card-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.5rem;
}

.card-grid > * {
  flex: 0 1 320px;   /* balanced: ~3-up, incomplete rows centre (#1565) */
}

.card-grid:has(> :nth-child(4):last-child) > * {
  flex-basis: calc(50% - 0.75rem);
  max-width: 26rem;
}

.card-item {
  background: var(--dz-surface-card);
  padding: var(--dz-spacing-card-padding, 1.5rem);
  border: var(--dz-card-border);
  border-radius: var(--dz-radius-card, 0.75rem);
  box-shadow: var(--dz-shadow-card);
  display: flex;
  flex-direction: column;
  transition: transform var(--dz-transition-fast, 150ms),
              box-shadow var(--dz-transition-fast, 150ms),
              border-color var(--dz-transition-fast, 150ms);
}

.card-item:hover {
  transform: translateY(-2px);
  box-shadow: var(--dz-card-shadow-hover);
  border-color: color-mix(in oklab, var(--dz-hero-bg-from) 35%, transparent);
}

.card-item h3 {
  font-size: var(--text-base);
  font-weight: 600;
  margin: 0.5rem 0;
}

.card-item p {
  color: var(--dz-text-body);
  margin: 0 0 1rem;
  line-height: var(--dz-line-height-body, 1.6);
  flex: 1;
}

.card-icon {
  width: 3rem;
  height: 3rem;
  background: var(--dz-feature-icon-bg, oklch(0.92 0.03 260));
  border-radius: 0.625rem;
  box-shadow: var(--dz-card-icon-ring);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 1rem;
  color: var(--dz-hero-bg-from);
}

.card-icon svg {
  width: 1.5rem;
  height: 1.5rem;
}

/* =============================================================================
   Trust Bar Section
   ============================================================================= */

.section-trust-bar {
  padding: 2rem 1.5rem;
  background: var(--dz-section-alt-bg, oklch(0.97 0.01 260));
  border-top: 1px solid var(--dz-border-subtle);
  border-bottom: 1px solid var(--dz-border-subtle);
}

.trust-strip {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2rem;
  align-items: center;
}

.trust-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--dz-text-body);
}

.trust-item svg {
  width: 1.25rem;
  height: 1.25rem;
  color: var(--dz-hero-bg-from);
}

/* =============================================================================
   Team Section
   ============================================================================= */

.section-team {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
}

.team-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 2rem;
  max-width: 64rem;
  margin: 0 auto;
}

.team-card {
  text-align: center;
  padding: 2rem 1.5rem;
}

.team-avatar {
  width: 6rem;
  height: 6rem;
  border-radius: 50%;
  margin: 0 auto 1rem;
  overflow: hidden;
  background: linear-gradient(135deg, var(--dz-hero-bg-from) 0%, var(--dz-hero-bg-to) 100%);
  display: flex;
  align-items: center;
  justify-content: center;
}

.team-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.team-initials {
  font-size: var(--text-xl);
  font-weight: 600;
  color: white;
  letter-spacing: 0.05em;
}

.team-name {
  font-size: var(--text-base);
  font-weight: 600;
  margin: 0 0 0.25rem;
}

.team-role {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--dz-hero-bg-from);
  margin: 0 0 0.75rem;
}

.team-bio {
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--dz-text-body);
  margin: 0 0 1rem;
}

.team-links {
  display: flex;
  gap: 0.75rem;
  justify-content: center;
}

.team-link {
  color: var(--dz-text-body);
  transition: color var(--dz-transition-fast, 150ms);
}

.team-link:hover {
  color: var(--dz-hero-bg-from);
}

.team-link svg {
  width: 1.25rem;
  height: 1.25rem;
}

/* =============================================================================
   Dark Mode Adjustments
   ============================================================================= */

/* Site Header — dark mode handled by token overrides in design-system.css */

/* Theme Toggle Button */
.theme-toggle {
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0.5rem;
  border-radius: 0.375rem;
  color: var(--dz-text-body);
  transition: color var(--dz-transition-fast, 150ms), background var(--dz-transition-fast, 150ms);
  display: flex;
  align-items: center;
  justify-content: center;
}

.theme-toggle:hover {
  color: var(--dz-text-foreground);
  background: var(--dz-border-subtle);
}

/* Theme toggle dark mode handled by token overrides in design-system.css */

.theme-toggle__icon {
  width: 1.25rem;
  height: 1.25rem;
}

/* Show sun icon by default (for dark mode), hide moon */
.theme-toggle__sun { display: none; }
.theme-toggle__moon { display: block; }

/* In dark mode, show sun, hide moon */
[data-theme="dark"] .theme-toggle__sun { display: block; }
[data-theme="dark"] .theme-toggle__moon { display: none; }

/* =============================================================================
   Section Background Utilities
   Applied via sitespec background: alt | primary | dark
   ============================================================================= */

.bg-alt {
  background: var(--dz-section-alt-bg, oklch(0.97 0.01 260));
}

.bg-primary {
  background: linear-gradient(135deg, var(--dz-hero-bg-from) 0%, var(--dz-hero-bg-to) 100%);
  color: white;
}

.bg-primary .section-header h2,
.bg-primary .section-header p {
  color: white;
}

.bg-dark {
  background: oklch(0.15 0.01 260);
  color: oklch(0.9 0 0);
}

.bg-dark .section-header h2,
.bg-dark .section-header p {
  color: oklch(0.9 0 0);
}

[data-theme="dark"] .bg-alt {
  background: oklch(0.12 0.01 260);
}

/* Section backgrounds */
[data-theme="dark"] .section-features,
[data-theme="dark"] .section-faq,
[data-theme="dark"] .section-steps {
  background: oklch(0.12 0.01 260);
}

[data-theme="dark"] .section-pricing,
[data-theme="dark"] .section-testimonials,
[data-theme="dark"] .section-logo-cloud,
[data-theme="dark"] .section-cta,
[data-theme="dark"] .section-value-highlight,
[data-theme="dark"] .section-card-grid,
[data-theme="dark"] .section-trust-bar {
  background: oklch(0.08 0.01 260);
}

[data-theme="dark"] .section-comparison,
[data-theme="dark"] .section-split-content {
  background: oklch(0.12 0.01 260);
}

/* Cards and items */
[data-theme="dark"] .feature-item,
[data-theme="dark"] .testimonial-item,
[data-theme="dark"] .card-item,
[data-theme="dark"] .pricing-tier:not(.pricing-highlighted) {
  background: oklch(0.18 0.01 260);
}

/* Comparison table */
[data-theme="dark"] .comparison-table th,
[data-theme="dark"] .comparison-table td {
  border-color: oklch(1 0 0 / 0.1);
}

[data-theme="dark"] .comparison-highlighted {
  background: oklch(0.18 0.03 260);
}

/* Headings */
[data-theme="dark"] .section h2,
[data-theme="dark"] .feature-item h3,
[data-theme="dark"] .step-item h3,
[data-theme="dark"] .faq-item summary,
[data-theme="dark"] .pricing-tier h3 {
  color: oklch(0.95 0 0);
}

/* Body text */
[data-theme="dark"] .feature-item p,
[data-theme="dark"] .step-item p,
[data-theme="dark"] .testimonial-item blockquote,
[data-theme="dark"] .faq-item p {
  color: oklch(0.65 0 0);
}

/* FAQ borders */
[data-theme="dark"] .faq-item {
  border-color: oklch(1 0 0 / 0.1);
}

/* Prose/markdown content */
[data-theme="dark"] .prose {
  color: oklch(0.85 0 0);
}

[data-theme="dark"] .prose h1,
[data-theme="dark"] .prose h2,
[data-theme="dark"] .prose h3 {
  color: oklch(0.95 0 0);
}

/* Logo cloud */
[data-theme="dark"] .logo-item img {
  filter: grayscale(100%) invert(1);
  opacity: 0.5;
}

[data-theme="dark"] .logo-item:hover img {
  filter: grayscale(0%) invert(1);
  opacity: 0.8;
}

/* Buttons - works with both DaisyUI btn and legacy btn classes */
[data-theme="dark"] .btn-secondary,
[data-theme="dark"] .btn-outline {
  color: oklch(0.85 0 0);
  border-color: oklch(0.85 0 0);
}

[data-theme="dark"] .btn-secondary:hover,
[data-theme="dark"] .btn-outline:hover {
  background: oklch(1 0 0 / 0.1);
}

/* =============================================================================
   Responsive Adjustments
   ============================================================================= */

@media (max-width: 768px) {
  .section h1 {
    font-size: var(--text-3xl);
  }

  .section h2 {
    font-size: var(--text-2xl);
  }

  .section-markdown h2,
  .section .prose h2 {
    font-size: var(--text-lg);
  }

  .section-markdown h1,
  .section .prose h1 {
    font-size: var(--text-2xl);
  }

  .section-cta h2 {
    font-size: var(--text-2xl);
  }

  .cta-group {
    flex-direction: column;
    align-items: center;
  }

  .pricing-highlighted {
    transform: none;
  }

  .stat-value {
    font-size: var(--text-2xl);
  }
}

/* =============================================================================
   Site Layout - Header, Footer, Nav
   ============================================================================= */

.site {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* Default body background — overridden per page via {% block body_class %}.
 * Was inline `bg-[hsl(var(--card))]` Tailwind on <body> in site_base.html;
 * v0.62 moves to a semantic class rule. */
.site-bg {
  background: var(--dz-surface-card, var(--colour-surface));
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--dz-header-bg);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--dz-border-subtle);
}

.site-nav {
  /* dazzle#1591: never force the page wider than the viewport */
  max-width: min(72rem, 100%);
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  margin: 0 auto;
  padding: 1rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
}

.site-logo {
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--dz-logo-color);
  text-decoration: none;
  flex-shrink: 0;
  min-width: 0;
}

.nav-items {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  /* dazzle#1591: wrap before horizontal overflow (no app-level overflow-x hack) */
  flex-wrap: wrap;
  min-width: 0;
  max-width: 100%;
}

/* Narrow viewports: stack logo + items so multi-link marketing nav fits. */
@media (max-width: 767px) {
  .site-nav {
    flex-wrap: wrap;
    gap: 0.75rem 1rem;
  }

  .nav-items {
    flex: 1 1 100%;
    width: 100%;
    max-width: 100%;
    gap: 0.4rem 0.75rem;
    justify-content: flex-start;
  }
}

.nav-link {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--dz-text-body);
  text-decoration: none;
  transition: color var(--dz-transition-fast, 150ms);
}

.nav-link:hover {
  color: var(--dz-text-foreground);
}

.nav-cta {
  font-size: var(--text-sm);
  font-weight: 600;
  color: white;
  background: var(--dz-hero-bg-from, oklch(0.50 0.18 270));
  padding: 0.5rem 1rem;
  border-radius: var(--dz-radius-button, 0.375rem);
  text-decoration: none;
  transition: background var(--dz-transition-fast, 150ms);
}

.nav-cta:hover {
  background: var(--dz-hero-bg-to, oklch(0.45 0.18 270));
}

.site-footer {
  margin-top: auto;
  background: var(--dz-footer-bg);
  color: var(--dz-footer-text);
  padding: 3rem 1.5rem 1.5rem;
}

.footer-content {
  max-width: 72rem;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 2rem;
  margin-bottom: 2rem;
}

.footer-col h4 {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--dz-footer-heading);
  margin: 0 0 1rem;
}

.footer-col ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.footer-col li {
  margin-bottom: 0.5rem;
}

.footer-col a {
  font-size: var(--text-sm);
  color: var(--dz-footer-link);
  text-decoration: none;
  transition: color var(--dz-transition-fast, 150ms);
}

.footer-col a:hover {
  color: var(--dz-footer-heading);
}

.footer-bottom {
  max-width: 72rem;
  margin: 0 auto;
  padding-top: 1.5rem;
  border-top: 1px solid var(--dz-footer-border);
  text-align: center;
}

.footer-bottom p {
  font-size: var(--text-xs);
  margin: 0;
}

/* =============================================================================
   Button Styles for Site Pages

   NOTE: Most button styling is now handled by DaisyUI btn classes (btn, btn-primary, etc.)
   These are kept for backwards compatibility and site-specific overrides.
   ============================================================================= */

/* Hero section CTA buttons need special styling to stand out on the gradient.
   Buttons render as `.button[data-variant]` (HM button component); these
   hero-context rules win on specificity to make the primary pop and the
   secondary a legible ghost. */
.section-hero .button[data-variant="primary"] {
  background: var(--dz-cta-fill);
  color: var(--dz-cta-text);
  border: none;
  font-weight: 700;
  font-size: 1.0625rem;
  padding: 0.875rem 1.75rem;
  border-radius: var(--dz-radius-button, 0.5rem);
  box-shadow: var(--dz-cta-shadow);
  transition: transform var(--dz-transition-fast, 150ms), box-shadow var(--dz-transition-fast, 150ms),
              background var(--dz-transition-fast, 150ms);
}

.section-hero .button[data-variant="primary"]:hover {
  background: var(--dz-cta-fill-hover);
  box-shadow: var(--dz-cta-shadow-hover);
  transform: translateY(-1px);
}

.section-hero .button[data-variant="outline"],
.section-hero .button[data-variant="secondary"] {
  background: transparent;
  color: white;
  border: 1.5px solid var(--dz-hero-border, oklch(1 0 0 / 0.5));
  font-weight: 600;
  font-size: 1.0625rem;
  padding: 0.875rem 1.75rem;
  border-radius: var(--dz-radius-button, 0.5rem);
  transition: background var(--dz-transition-fast, 150ms), border-color var(--dz-transition-fast, 150ms);
}

.section-hero .button[data-variant="outline"]:hover,
.section-hero .button[data-variant="secondary"]:hover {
  background: var(--dz-hero-overlay);
  border-color: var(--dz-hero-border-hover);
  color: white;
}

/* =============================================================================
   Site Page Sections (JS-rendered)

   These rules target the HTML structure generated by get_site_js().
   They use design-system tokens so themes propagate correctly.
   ============================================================================= */

.section {
  width: 100%;
}

.section-content {
  max-width: 72rem;
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* --- Section header (renderSectionHeader helper) --- */
.section-header {
  text-align: center;
  max-width: 40rem;
  margin: 0 auto 3rem;
}

.section-media {
  text-align: center;
  margin: 0 auto 2rem;
  max-width: 48rem;
}

.section-media img {
  max-width: 100%;
  height: auto;
  border-radius: 0.5rem;
}

/* --- Generic section typography --- */
.section h1 {
  font-size: var(--dz-font-size-hero-headline, 3.5rem);
  font-weight: var(--dz-font-weight-hero-headline, 800);
  line-height: var(--dz-line-height-hero-headline, 1.1);
  letter-spacing: var(--dz-letter-spacing-hero-headline, -0.02em);
  margin: 0 0 1.5rem;
}

.section h2 {
  font-size: var(--dz-font-size-section-headline, 2.25rem);
  font-weight: var(--dz-font-weight-section-headline, 700);
  line-height: var(--dz-line-height-section-headline, 1.2);
  letter-spacing: var(--dz-letter-spacing-section-headline, -0.01em);
  margin: 0 0 2rem;
  text-align: center;
}

.section h3 {
  font-size: var(--text-base);
  font-weight: 600;
  margin: 0.5rem 0;
}

.section p {
  font-size: var(--dz-font-size-body, 1rem);
  line-height: var(--dz-line-height-body, 1.6);
}

/* --- Article body / markdown typography ---
   .section h1/h2/h3/p above target marketing *section headlines*
   (centred, large). Markdown bodies and any .prose content inside a
   section are article copy and must opt out of the headline treatment
   — otherwise blog-post H2s render centred and oversized (issue #921). */
.section-markdown h1,
.section-markdown h2,
.section-markdown h3,
.section-markdown h4,
.section-markdown p,
.section .prose h1,
.section .prose h2,
.section .prose h3,
.section .prose h4,
.section .prose p {
  text-align: start;
  letter-spacing: normal;
}

.section-markdown h1,
.section .prose h1 {
  font-size: var(--text-2xl);
  font-weight: 700;
  line-height: 1.25;
  margin: 2.5rem 0 1rem;
}

.section-markdown h2,
.section .prose h2 {
  font-size: var(--text-xl);
  font-weight: 600;
  line-height: 1.3;
  margin: 2rem 0 0.75rem;
}

.section-markdown h3,
.section .prose h3 {
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.4;
  margin: 1.5rem 0 0.5rem;
}

.section-markdown h4,
.section .prose h4 {
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.4;
  margin: 1.25rem 0 0.5rem;
}

.section-markdown p,
.section .prose p {
  font-size: var(--dz-font-size-body, 1rem);
  line-height: 1.7;
  margin: 0 0 1em;
}

/* --- 404 page --- */
.404-section {
  text-align: center;
  padding: var(--dz-spacing-hero-y, 6rem) 1.5rem;
}

.404-headline {
  font-size: var(--dz-font-size-hero-headline, 3.5rem);
  font-weight: var(--dz-font-weight-hero-headline, 800);
  line-height: var(--dz-line-height-hero-headline, 1.1);
  margin-bottom: 1rem;
}

.404-cta {
  margin-top: 2rem;
}

/* --- Pricing tier description --- */
.pricing-description {
  color: var(--dz-text-muted);
  margin: 0 0 1.5rem;
  font-size: var(--dz-font-size-body, 1rem);
  line-height: var(--dz-line-height-body, 1.6);
}

/* --- Markdown page section --- */
.section-markdown {
  padding: var(--dz-spacing-section-y, 5rem) 1.5rem;
}

.section-markdown .prose hr {
  border: none;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--dz-border-subtle), transparent);
  margin: 3rem auto;
  max-width: 200px;
}

.subhead {
  font-size: var(--text-lg);
  line-height: 1.6;
  opacity: 0.9;
  margin: 0 0 2rem;
}

.cta-group {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap;
}

/* Balanced feature/testimonial grid — flex-wrap + center so any card count reads
   intentional: max ~3 per row (cards don't grow), and incomplete trailing rows
   CENTER instead of left-orphaning (#1565 — a 5th card no longer hangs alone). */
.features-grid,
.testimonials-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.5rem;
}

.features-grid > *,
.testimonials-grid > * {
  flex: 0 1 320px;   /* fixed basis, no grow → wraps at ~3-up, orphans centre */
}

/* Exactly 4 → a balanced 2×2 rather than a lone centred card. */
.features-grid:has(> :nth-child(4):last-child) > *,
.testimonials-grid:has(> :nth-child(4):last-child) > * {
  flex-basis: calc(50% - 0.75rem);
  max-width: 26rem;
}

.pricing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.5rem;
}

.feature-item,
.testimonial-item {
  background: var(--dz-surface-card);
  padding: var(--dz-spacing-card-padding, 1.5rem);
  border: var(--dz-card-border);
  border-radius: var(--dz-radius-card, 0.75rem);
  box-shadow: var(--dz-shadow-card);
  transition: transform var(--dz-transition-fast, 150ms), box-shadow var(--dz-transition-fast, 150ms),
              border-color var(--dz-transition-fast, 150ms);
}

.feature-item:hover {
  transform: translateY(-2px);
  box-shadow: var(--dz-card-shadow-hover);
  border-color: color-mix(in oklab, var(--dz-hero-bg-from) 35%, transparent);
}

.feature-item h3 {
  font-size: var(--text-base);
  font-weight: 600;
  margin: 0.5rem 0;
}

.feature-item p {
  color: var(--dz-text-body);
  margin: 0;
  line-height: var(--dz-line-height-body, 1.6);
}

.feature-icon {
  width: 3rem;
  height: 3rem;
  background: var(--dz-feature-icon-bg, oklch(0.92 0.03 260));
  border-radius: 0.625rem;
  box-shadow: var(--dz-card-icon-ring);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 1rem;
  color: var(--dz-hero-bg-from);
}

.feature-icon svg {
  width: 1.5rem;
  height: 1.5rem;
}

.faq-list {
  max-width: 48rem;
  margin: 0 auto;
}

.faq-item {
  border-bottom: 1px solid var(--dz-border-default);
  padding: 1rem 0;
}

.faq-item summary {
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}

.faq-item summary::-webkit-details-marker {
  display: none;
}

.faq-item p {
  margin: 0.75rem 0 0;
  color: var(--dz-text-body);
  line-height: var(--dz-line-height-body, 1.6);
}

/* #1113 — replaces DaisyUI's `stats stats-vertical lg:stats-horizontal
 * shadow` grid emitted by `site_section_builder._build_stats_section`.
 * Vertical stack on narrow viewports, horizontal row from --breakpoint-lg
 * upward. Tokens-only, no hardcoded values. */
.stats-grid {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: var(--colour-surface);
  color: var(--colour-text);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  text-align: left;
}

@media (min-width: 64rem) {
  .stats-grid {
    flex-direction: row;
    align-items: stretch;
    justify-content: space-around;
    gap: var(--space-lg);
  }
}

.stat-item {
  padding: var(--space-md);
}

.stat-value {
  font-size: var(--text-2xl);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
  color: var(--colour-text);
  display: block;
  font-variant-numeric: tabular-nums;
}

.stat-label {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* List wrapper for the marketing steps section. Mobile stacks
 * vertically; desktop equal-width columns. Distinct from
 * the framework's `.steps` rule (in components/fragments.css)
 * which is used by the in-app step indicator.
 * dazzle#1590: connector must not be a flex:1 child that steals column width. */
.section-steps-list {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1rem;
  list-style: none;
  padding: 0;
  margin: 0;
  width: 100%;
}

@media (min-width: 1024px) {
  .section-steps-list {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1.5rem;
  }
}

/* Decorative connector — out of flex flow so step columns stay equal. */
.step-connector {
  display: none;
}

/* Mobile: vertical steps — number beside text in a row */
.step-item {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
  min-width: 0;
}

/* Do not give is-not-last different flex growth than siblings (crush risk). */
.step-item.is-not-last {
  flex: initial;
}

.step-number {
  width: 2.5rem;
  height: 2.5rem;
  background: var(--dz-hero-bg-from);
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: var(--text-base);
  flex-shrink: 0;
}

.step-content {
  min-width: 0;
  width: 100%;
}

.step-content h3 {
  font-size: var(--text-base);
  font-weight: 600;
  margin: 0 0 0.25rem;
}

.step-content p {
  color: var(--dz-text-body);
  margin: 0;
  line-height: var(--dz-line-height-body, 1.6);
  font-size: var(--text-sm);
}

/* Desktop: equal-width columns; connector absolutely positioned between steps. */
@media (min-width: 1024px) {
  .step-item,
  .step-item.is-not-last {
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0.75rem;
    position: relative;
  }

  .step-item.is-not-last .step-connector {
    display: block;
    position: absolute;
    /* Center of the number circle → into the gap toward the next column */
    top: 1.5rem;
    left: calc(50% + 1.75rem);
    width: calc(50% + 0.75rem);
    height: 1px;
    margin: 0;
    flex: none;
    background: var(--dz-border-default, var(--colour-border));
    pointer-events: none;
  }

  .step-number {
    width: 3rem;
    height: 3rem;
    font-size: var(--text-lg);
  }

  .step-content {
    text-align: center;
  }

  .step-content h3 {
    font-size: var(--text-lg);
    margin: 0 0 0.5rem;
  }

  .step-content p {
    font-size: var(--text-sm);
  }
}

.logos-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 3rem;
  align-items: center;
}

.logo-item img {
  height: 2rem;
  opacity: 0.6;
  filter: grayscale(100%);
  transition: all var(--duration-fast, 150ms);
}

.logo-item:hover img {
  opacity: 1;
  filter: grayscale(0%);
}

.pricing-tier {
  background: var(--dz-surface-card);
  padding: 2rem;
  border-radius: var(--dz-radius-card, 0.75rem);
  box-shadow: var(--dz-shadow-card, 0 1px 3px oklch(0 0 0 / 0.1));
  display: flex;
  flex-direction: column;
  transition: transform var(--dz-transition-fast, 150ms);
}

.pricing-tier:hover:not(.pricing-highlighted) {
  transform: translateY(-4px);
}

.pricing-tier h3 {
  font-size: var(--text-lg);
  font-weight: 600;
  margin: 0 0 0.5rem;
}

.pricing-price {
  margin: 0 0 1.5rem;
}

.price {
  font-size: var(--text-3xl);
  font-weight: 700;
}

.period {
  font-size: var(--text-sm);
  color: var(--dz-text-muted);
}

.pricing-features {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  flex: 1;
}

.pricing-features li {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--dz-border-subtle);
}

.pricing-highlighted {
  background: var(--dz-pricing-highlight-bg, var(--dz-hero-bg-from));
  color: white;
  transform: scale(1.05);
  box-shadow: var(--dz-shadow-lg);
}

.pricing-highlighted .period,
.pricing-highlighted li {
  color: var(--dz-hero-text-dimmed);
  border-color: var(--dz-hero-border-dim);
}

.testimonial-item:hover {
  transform: translateY(-4px);
  box-shadow: var(--dz-shadow-lg, 0 10px 15px -3px oklch(0 0 0 / 0.1));
}

.testimonial-item blockquote {
  font-style: italic;
  color: var(--dz-text-emphasis);
  margin: 0 0 1rem;
  line-height: var(--dz-line-height-body, 1.6);
}

.testimonial-author {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.testimonial-author strong {
  display: block;
  font-size: var(--text-sm);
}

.testimonial-author span {
  display: block;
  font-size: var(--text-xs);
  color: var(--dz-text-muted);
}

.prose {
  max-width: 48rem;
  margin: 0 auto;
  padding: 3rem 1.5rem;
}

.prose h1, .prose h2, .prose h3 {
  text-align: left;
}

/* --- Legal page styling (terms, privacy) --- */
.page-legal .section-markdown .prose {
  max-width: 45rem;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

.page-legal .section-markdown .prose h1,
.page-legal .section-markdown .prose h2,
.page-legal .section-markdown .prose h3 {
  text-align: left;
}

.page-legal .section-markdown .prose p {
  line-height: 1.7;
}

.page-legal .section-markdown .prose h2 {
  margin-top: 2em;
  margin-bottom: 0.75em;
  font-size: var(--text-lg);
  font-weight: 600;
}

.page-legal .section-markdown .prose h3 {
  margin-top: 1.5em;
  margin-bottom: 0.5em;
  font-size: var(--text-base);
  font-weight: 600;
}

/* --- Task surface container (human task workflow pages) --- */
.task-surface-container {
  max-width: 75rem;
  margin: 0 auto;
  padding: var(--dz-spacing-card-padding, 1.5rem);
}

.loading {
  text-align: center;
  padding: 4rem;
  color: var(--dz-text-muted);
}

.error {
  text-align: center;
  padding: 2rem;
  color: var(--dz-error-text);
}

/* ============================================================= */
/* Consent banner (v0.61.0 Phase 2 — Analytics, Consent & Privacy) */
/* ============================================================= */

.consent-banner {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 1000;
  padding: 1.25rem 1rem;
  background: var(--dz-bg-card, var(--colour-surface));
  border-top: 1px solid var(--dz-border, var(--colour-border));
  box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.08);
}

.consent-panel {
  max-width: 64rem;
  margin: 0 auto;
  color: var(--dz-text-primary, var(--colour-text));
  font-size: var(--text-sm);
  line-height: 1.5;
}

.consent-title {
  margin: 0 0 0.5rem 0;
  font-size: var(--text-base);
  font-weight: 600;
}

.consent-description {
  margin: 0 0 1rem 0;
  color: var(--dz-text-muted, var(--colour-text-muted));
  font-size: var(--text-sm);
}

.consent-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin-top: 1rem;
}

.consent-btn {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  color: inherit;
  padding: 0.5rem 1rem;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  border-radius: 0.375rem;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.consent-btn:focus-visible {
  outline: 2px solid var(--dz-focus-ring, var(--colour-brand));
  outline-offset: 2px;
}

.consent-btn-primary {
  background: var(--dz-primary, var(--colour-brand));
  color: var(--dz-primary-foreground, var(--colour-brand-contrast));
}

.consent-btn-primary:hover {
  filter: brightness(1.05);
}

.consent-btn-secondary {
  border-color: var(--dz-border, var(--colour-border));
  background: var(--dz-bg-card, var(--colour-surface));
}

.consent-btn-secondary:hover {
  background: var(--dz-bg-hover, var(--colour-surface-muted));
}

.consent-btn-link {
  text-decoration: underline;
  text-underline-offset: 2px;
  padding: 0.5rem 0;
  color: var(--dz-text-link, var(--colour-brand));
}

.consent-btn-link:hover {
  text-decoration-thickness: 2px;
}

.consent-customize {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--dz-border, var(--colour-border));
}

.consent-category {
  border: 0;
  padding: 0.5rem 0;
  margin: 0;
}

.consent-category + .consent-category {
  border-top: 1px solid var(--dz-border, var(--colour-border));
}

.consent-category label {
  display: inline-flex;
  gap: 0.5rem;
  align-items: center;
  font-weight: 500;
  cursor: pointer;
}

.consent-category input[type="checkbox"]:disabled + .consent-category-label {
  color: var(--dz-text-muted, var(--colour-text-muted));
  cursor: not-allowed;
}

.consent-category-hint {
  margin: 0.25rem 0 0 1.5rem;
  color: var(--dz-text-muted, var(--colour-text-muted));
  font-size: var(--text-xs);
}

.consent-links {
  margin: 0.75rem 0 0 0;
  font-size: var(--text-xs);
}

.consent-links a {
  color: var(--dz-text-link, var(--colour-brand));
  text-decoration: underline;
  text-underline-offset: 2px;
}

@media (max-width: 640px) {
  .consent-banner {
    padding: 1rem 0.75rem;
  }
  .consent-actions {
    flex-direction: column;
    align-items: stretch;
  }
  .consent-btn {
    width: 100%;
  }
  .consent-btn-link {
    width: auto;
    align-self: center;
  }
}

/* =============================================================================
   #1113 — Per-stat wrapper + native toast. The `.stats-grid` /
   `.stat-value` / `.stat-label` rules above (modernised in the
   same release) cover the section-level layout; `.stat` here is the
   per-item flex column the section-builder emits. `.stat-title` is
   the per-item label kept distinct from `.stat-label` (the latter is
   used by the legacy `stat-item` shape in templated marketing pages).
   Tokens-only — no literal hsl/hex.
   ============================================================================= */

.stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xs);
  padding: var(--space-md) var(--space-lg);
  flex: 1 1 auto;
  min-width: 0;
}

.stat-title {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* Toast unit + stack — owned by HYPERPART: toast (components/toast.css).
 * Do not re-declare .toast* here; dual-lock lives in toast.css. */

}
/* ── components/feedback-widget.css ── */
@layer components {
@layer framework {
/* HaTchi-MaXchi — Feedback widget (floating feedback FAB + panel: categories, severities, form). Ported from Dazzle feedback-widget.css (HM-sitespec 1C, 2026-07-09) — was orphaned (unlinked) on the Dazzle side; now served via the HM dist. Authored with  prefix. */

/* Floating button */
.feedback-btn {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  width: 3rem;
  height: 3rem;
  border-radius: 9999px;
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  border: none;
  cursor: pointer;
  z-index: 9998;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.feedback-btn:hover {
  transform: scale(1.08);
  box-shadow: 0 6px 16px rgb(0 0 0 / 0.2);
}
.feedback-btn svg {
  width: 1.25rem;
  height: 1.25rem;
}

/* Slide-out panel */
.feedback-panel {
  position: fixed;
  top: 0;
  right: 0;
  width: 24rem;
  max-width: 100vw;
  height: 100dvh;
  background: var(--colour-surface);
  border-left: 1px solid var(--colour-border);
  box-shadow: -4px 0 12px rgba(0, 0, 0, 0.1);
  z-index: 9999;
  transform: translateX(100%);
  transition: transform 0.25s ease;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}
.feedback-panel.open {
  transform: translateX(0);
}

/* Panel header */
.feedback-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--colour-border);
}
.feedback-header h3 {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--colour-text);
}
.feedback-close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--colour-text);
  font-size: 1.25rem;
  padding: 0.25rem;
}

/* Panel body */
.feedback-body {
  padding: 1.25rem;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Category buttons */
.feedback-categories {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}
.feedback-cat-btn {
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--colour-border);
  border-radius: 0.375rem;
  background: var(--colour-surface-muted);
  color: var(--colour-text);
  cursor: pointer;
  font-size: 0.8125rem;
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.feedback-cat-btn:hover {
  border-color: var(--colour-brand);
}
.feedback-cat-btn.selected {
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  border-color: var(--colour-brand);
}

/* Severity toggles */
.feedback-severities {
  display: flex;
  gap: 0.5rem;
}
.feedback-sev-btn {
  flex: 1;
  padding: 0.375rem 0.5rem;
  border: 1px solid var(--colour-border);
  border-radius: 0.375rem;
  background: var(--colour-surface-muted);
  color: var(--colour-text);
  cursor: pointer;
  font-size: 0.75rem;
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.feedback-sev-btn.selected[data-severity="blocker"] {
  background: var(--danger-600);
  color: white;
  border-color: var(--danger-600);
}
.feedback-sev-btn.selected[data-severity="annoying"] {
  background: var(--warning-600);
  color: white;
  border-color: var(--warning-600);
}
.feedback-sev-btn.selected[data-severity="minor"] {
  background: var(--success-600);
  color: white;
  border-color: var(--success-600);
}

/* Description textarea */
.feedback-description {
  width: 100%;
  min-height: 6rem;
  padding: 0.75rem;
  border: 1px solid var(--colour-border);
  border-radius: 0.375rem;
  background: var(--colour-surface);
  color: var(--colour-text);
  font-family: inherit;
  font-size: 0.875rem;
  resize: vertical;
}
.feedback-description:focus {
  outline: 2px solid var(--colour-brand);
  outline-offset: -1px;
}

/* Submit button */
.feedback-submit {
  width: 100%;
  padding: 0.625rem 1rem;
  border: none;
  border-radius: 0.375rem;
  background: var(--colour-brand);
  color: var(--colour-brand-contrast);
  font-weight: 600;
  font-size: 0.875rem;
  cursor: pointer;
  transition: opacity 0.15s ease;
}
.feedback-submit:hover {
  opacity: 0.9;
}
.feedback-submit:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Label */
.feedback-label {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--colour-text);
}

/* Toast notification */
.feedback-toast {
  position: fixed;
  bottom: 5rem;
  right: 1.5rem;
  padding: 0.75rem 1.25rem;
  border-radius: 0.375rem;
  background: var(--success-600);
  color: white;
  font-size: 0.875rem;
  z-index: 10000;
  opacity: 0;
  transform: translateY(0.5rem);
  transition: opacity 0.2s ease, transform 0.2s ease;
  pointer-events: none;
}
.feedback-toast.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Backdrop overlay */
.feedback-backdrop {
  position: fixed;
  inset: 0;
  background: rgb(0 0 0 / 0.3);
  z-index: 9998;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}
.feedback-backdrop.visible {
  opacity: 1;
  pointer-events: auto;
}

} /* end @layer framework */

}
