/* brand-supabase-dark.css — a Lattice BRAND (token swap).
 *
 * Loaded BEFORE the library (library.css). It sets the whole variable contract the
 * library reads (SiteForge names) to a Supabase-style dark palette, so every atom and
 * molecule renders dark + green with zero component changes. This is the theme model:
 * same components, different token file.
 */

:root {
  /* ---- Fonts ---- */
  --font-display: 'Inter', system-ui, -apple-system, sans-serif;
  --font-body:    'Inter', system-ui, -apple-system, sans-serif;
  --font-mono:    'JetBrains Mono', ui-monospace, monospace;

  /* ---- Type scale ---- */
  --text-xs: .75rem; --text-sm: .8125rem; --text-base: .9375rem; --text-lg: 1.0625rem;
  --text-xl: 1.25rem; --text-2xl: 1.5rem; --text-3xl: 1.875rem; --text-display: 2.25rem; --text-hero: 3rem;

  /* ---- Spacing ---- */
  --space-xs: .25rem; --space-sm: .5rem; --space-md: 1rem; --space-lg: 1.5rem;
  --space-xl: 2rem; --space-2xl: 3rem; --space-3xl: 4rem;

  /* ---- Radius ---- */
  --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px; --radius-full: 9999px;

  /* ---- Motion ---- */
  --dur-fast: 150ms; --dur-normal: 200ms; --ease-out: cubic-bezier(0,0,.2,1);

  /* ---- Colour: brand + semantics (Supabase dark) ---- */
  --color-bg:          #1c1c1c;
  --color-surface:     #1f1f1f;
  --color-text:        #ededed;
  --color-text-muted:  #8b8b8b;
  --color-primary:     #3ecf8e;
  --color-primary-text:#08130d;   /* dark text on the light-green primary (readable) */
  --color-accent:      #3ecf8e;

  --primary-400: #5ed99f; --primary-500: #3ecf8e; --primary-600: #24b47e; --primary-700: #24b47e;

  /* ---- Neutral scale mapped to dark (by how the library uses each step) ----
     100 = subtle/hover bg + table head; 200/700 = borders + dividers;
     800 = surfaces/code bg; 900/950 = darkest (tooltip, code). */
  --neutral-50:  #fafafa;
  --neutral-100: #242424;
  --neutral-200: #2e2e2e;
  --neutral-300: #3a3a3a;
  --neutral-400: #8b8b8b;
  --neutral-500: #a0a0a0;
  --neutral-600: #b4b4b4;
  --neutral-700: #2e2e2e;
  --neutral-800: #1f1f1f;
  --neutral-900: #181818;
  --neutral-950: #121212;

  /* ---- Phone-baseline / effect tokens the library falls back on ---- */
  --tap-min: 40px;
  --shadow-md: 0 4px 12px rgba(0,0,0,.35);
  --shadow-lg: 0 12px 32px rgba(0,0,0,.45);
  --focus-ring: 0 0 0 2px rgba(62,207,142,.5);
}
/* atoms.css — canonical ATOM layer for SiteForge (the web implementation of the
 * Galvan design-system spec). Implements the atoms in
 * ~/projects/design-system/catalog.json using SiteForge's existing CSS-variable
 * contract (so rebrand-over-wire keeps working) plus phone-baseline fallbacks
 * (--tap-min etc.) so every atom is touch-correct even where the harness doesn't
 * inject them.
 *
 * RULE: sections compose THESE classes. A section must never re-declare an atom's
 * colour/radius/typography — only layout. See ATOMIZATION.md.
 *
 * Var contract: --color-primary/accent/surface/text/text-muted, --primary-*,
 * --neutral-*, --font-display/body, --text-*, --space-*, --radius-*.
 * Baseline fallbacks: --tap-min(44px) --dur-fast(150ms) --ease-out.
 */

/* ==================== Muted text floor ====================
 * Muted text is derived from the palette's own ink and ground rather than shipped as a
 * fixed grey, so it follows a bound site palette, follows the light/dark ground, and
 * still clears WCAG AA on the tinted panels sections actually put it on.
 *
 * Measured (WCAG 2.x, sRGB). The greys this replaces, against the worst light ground
 * in the library, --neutral-200 (#e5e7eb):
 *   #64748b, the value the preview :root ships   3.84:1  FAIL
 *   #6b7280 (--neutral-500)                      3.90:1  FAIL
 * On --neutral-100 (#f3f4f6) those are 4.32 and 4.39, also under the 4.5:1 bar. That
 * one grey is the whole of the low-contrast worklist on nine sections.
 *
 * 70% ink over ground resolves to #575d6a on a white page and #b2b6bd on a #0f172a
 * page. Worst case each way:
 *   #575d6a on #e5e7eb  5.34:1     #b2b6bd on #374151  5.06:1
 *   #575d6a on #f3f4f6  6.00:1     #b2b6bd on #1e293b  7.19:1
 * A flat --neutral-600 also clears the light side (6.10:1 on #e5e7eb) but it is a
 * light-ground value: on a dark page it lands at 1.9:1 inside a --color-surface card.
 * Deriving it is what keeps both grounds correct from one declaration.
 * Dark SECTIONS on a light page are handled by .on-dark just below. */
:root {
  --color-text-muted: color-mix(in srgb, var(--color-text, #0f172a) 70%, var(--color-background, #ffffff));
}

/* ==================== Tinted chip ground ====================
 * SHARED CAUSE, fixed here rather than in the 24 sections that tripped over it.
 * A badge, chip, tag, avatar, kbd, menu hover row or table head is a small tinted
 * patch of the PAGE, and every one of them printed --color-text (ground-relative) on
 * --neutral-100 / --neutral-200 (absolute, always near-white). On a light page that
 * reads; on a dark page it is #f8fafc on #e5e7eb, measured at 1.2:1. --neutral-* never
 * flips, so the ink and its ground were resolving against different theories of where
 * the component sits.
 *
 * Derived the same way --color-text-muted above is: a percentage of the page's own ink
 * over the ground the chip actually sits on, so the tint follows whichever ground it
 * lands on and the ink stays correct by construction.
 *
 * Mixed over --color-SURFACE, not --color-background, and that distinction was found by
 * looking at the rendered dark preview rather than at the numbers. Mixing over the page
 * ground passes the contrast gate, because the ink is still correct, but a chip inside a
 * card then resolves to almost exactly the card's own colour: the price pills in
 * app-store-grid, the role badge in card-user and the key caps in search-command all
 * lost their pill and read as bare text on dark. --color-surface is the raised ground a
 * chip is normally on, and it is one step off the page either way, so the chip stays
 * visible whether it sits on a card or directly on the page.
 *   light  7% #0f172a over #f8fafc -> #e7eaed   10% -> #e0e3e7 (was #f3f4f6 / #e5e7eb)
 *   dark   7% #f8fafc over #1e293b -> #2d3748   10% -> #343e4e
 * Contrast on the dark step: ink #f8fafc 12.2:1, muted #b2b6bd 6.0:1.
 * 7 and not 5, because two separate reads of the dark previews said the same thing: at
 * 5% the price pills and the role badge were legible but their pill had almost no fill
 * against the card. The same delta reads weaker light-on-dark than dark-on-light, so
 * the number that looked right on the light ground was not enough on the dark one.
 * .on-dark needs no special case: it sets --color-surface itself, so the same
 * declaration resolves against --neutral-800 there. */
:root {
  --color-chip: color-mix(in srgb, var(--color-text, #0f172a) 7%, var(--color-surface, var(--color-background, #ffffff)));
  --color-chip-strong: color-mix(in srgb, var(--color-text, #0f172a) 10%, var(--color-surface, var(--color-background, #ffffff)));
}

/* ==================== Brand-tinted patch ====================
 * --color-chip one step further in: the patch a section tints with the BRAND rather
 * than with its own ink. A promo band, an unread row, a featured plan, a quote block.
 * Every one of them painted --primary-50, which is the brand mixed into WHITE and so
 * is absolute in exactly the way --neutral-100 was, while the ink on it stayed
 * ground-relative. Measured on the dark preview: #f8fafc on #eef3fd is 1.1:1 and
 * #b2b6bd on #eef3fd is 1.8:1.
 *
 * Deriving the tint over --color-surface fixes the ink. It also introduces a second
 * failure, which is why this is a token and not a per-section edit: on the dark ground
 * --color-surface is already a slate (#1e293b, chroma 29) and 10% of the brand lifts it
 * to chroma 46, past the detector's 40 threshold, so the patch counts as a COLOURED
 * background and the page's grey muted step (#b2b6bd, chroma 11) fires gray-on-color.
 * That was a REGRESSION introduced with the ground-relative tint, not an old bug:
 * dropdown-notification and modal-pricing measured clean while the tint was absolute.
 *
 * So the patch carries its own muted step instead of borrowing the page's. Mixing ink
 * with the BRAND rather than with the ground keeps the patch's hue in the text, which
 * is what the rule actually asks for ("use a darker shade of the background colour"),
 * and it still clears contrast comfortably because it is 70% ink either way:
 *   blue  light  #162e64 on #e3ebfa 11.1:1     dark  #b9cdf7 on #1f2f4d  8.5:1
 *   lime  light  #445c1d on #f3fae3  7.0:1     dark  #e8fbb0 on #2e3e35 10.2:1
 * The muted step measures chroma 62 to 78, so it is never read as grey. Full
 * --color-text needs no swap: gray-on-color only looks at text between 0.05 and 0.85
 * relative luminance, and ink sits outside that on both grounds (0.009 and 0.954).
 *
 * --color-tint is exposed separately so a section that already publishes its own
 * background hook can keep it and still opt into the text context via the class. */
:root {
  --color-tint: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface, var(--color-background, #ffffff)));
}
.surface-tint {
  background: var(--color-tint);
  --color-text-muted: color-mix(in srgb, var(--color-text, #0f172a) 70%, var(--color-primary));
}

/* ==================== Surface context (local token swap) ====================
 * Put on any dark section so every atom inside inverts automatically — no
 * per-section text-colour overrides. This is theme-swap applied locally. */
.on-dark, .surface-dark {
  --color-text: #fff;
  --color-text-muted: rgba(255,255,255,.72);
  --color-surface: var(--neutral-800, #1f2937);
  color: var(--color-text);
  /* The derived tints have to be RE-declared here, not just inherited. A custom
     property containing var() is substituted on the element that declares it, so the
     --color-chip on :root resolves against the PAGE's ink and surface and descendants
     inherit that finished colour. Overriding --color-text and --color-surface below it
     does nothing to a token already resolved above it.
     The previous pass assumed the opposite ("`.on-dark` needs no special case: it sets
     --color-surface itself"), and the gate found it the moment a plain .badge landed
     inside a dark player: chip #e8eaed under #ffffff text, 1.2:1, which is the same
     absolute-patch-under-relative-ink defect one level further in. Re-declaring makes
     the tints resolve against this element's swapped values instead. */
  --color-chip: color-mix(in srgb, var(--color-text, #0f172a) 7%, var(--color-surface, var(--color-background, #ffffff)));
  --color-chip-strong: color-mix(in srgb, var(--color-text, #0f172a) 10%, var(--color-surface, var(--color-background, #ffffff)));
  --color-tint: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface, var(--color-background, #ffffff)));
}

/* Brand colour used as TEXT steps to --primary-400 on a dark ground, which is what the
   library's own dark/light usage table already specifies for tags and labels. A mid
   brand colour on near-black does not clear body contrast: the default #2563eb
   measures 3.43:1 on --neutral-900 (#111827), its --primary-400 step (#6b95f1)
   measures 6.08:1. Fills and buttons keep the base primary.
   Carried on a token rather than a `.on-dark .text--accent` descendant selector, so the
   atoms keep their single-class weight and a section's own overrides still win. */
.on-dark, .surface-dark { --color-accent-text: var(--primary-400, var(--color-primary)); }

/* The fallback when NOTHING resolves the token is INK, not the raw brand.
   A section that paints no ground of its own carries neither .on-dark nor a dark
   surface, so --color-accent-text is unset on BOTH grounds and the old
   `var(--color-primary)` fallback wrote the raw brand as text. Measured:
   #2563eb on #0f172a is 3.5:1 and #c1fe00 on #ffffff is 1.2:1, against a 4.5:1 bar.
   --color-text follows the ground (#0f172a light, #f8fafc dark), so ink clears it
   either way: 16.9:1 and 16.4:1.

   The fallback has to be a LITERAL colour. Every derived CSS colour (color-mix,
   oklch, rgb(from ...)) serialises in the detector's Chrome 151 as `color(srgb ...)`
   or `oklch(...)`, and detect-antipatterns-browser.js parseRgb matches `rgb()` only.
   So --primary-900 here would clear the gate by making the text UNMEASURABLE rather
   than by making it readable, which is a false pass.

   Ink is the fallback, not the answer. The answer is a resolved --color-accent-text:
   rivet-server injects design.accent_text_on (the brand darkened by the least amount
   that clears 4.5:1) and this declaration then never applies. See EDITORIAL-STANDARD
   rule 22. SiteForge's own preview :root does not emit the token yet, which is why
   the standalone preview renders accent text as ink. */

/* ==================== Motion vocabulary ====================
 * Measured off linear.app (LINEAR-DECONSTRUCTION.md, "Motion vocabulary"). The
 * headline finding is what is ABSENT: across 180 downloaded JS chunks, framer-motion
 * 0, gsap 0, three 0, WebGL 0, `.animate(` 0 (no WAAPI), getAnimations 0,
 * startViewTransition 0, useInView 0; and in 351KB of CSS, animation-timeline 0,
 * view-timeline 0, scroll-timeline 0. The whole motion system is CSS transitions on
 * state plus CSS keyframe loops, which means almost all of it is reproducible in a
 * static section with scoped CSS and no JS.
 *
 * The 18 curves are a complete Penner set, copied verbatim. Counted in their CSS,
 * --ease-out-quad dominates by an order of magnitude; in-out-quint, in-out-expo,
 * in-out-circ, in-circ and in-expo are declared and never used on those pages. The
 * house style is EASE-OUT, ALWAYS, and --ease-out-quad is the house curve. The set
 * ships whole anyway because it is 18 inert declarations and it stops the next
 * author inventing a nineteenth cubic-bezier by hand.
 *
 * NOT a second vocabulary: ~/projects/design-system/tokens/tokens-motion.css already
 * defines --ease-standard / --ease-in / --ease-out / --ease-emphasized /
 * --ease-linear, --dur-instant/fast/normal/slow/signal and --press-scale. Those names
 * are left alone and are still what the atoms below consume; these are 18 NEW names
 * that do not exist upstream. */
:root {
  --ease-in-quad:      cubic-bezier(.55,.085,.68,.53);
  --ease-out-quad:     cubic-bezier(.25,.46,.45,.94);
  --ease-in-cubic:     cubic-bezier(.55,.055,.675,.19);
  --ease-out-cubic:    cubic-bezier(.215,.61,.355,1);
  --ease-in-quart:     cubic-bezier(.895,.03,.685,.22);
  --ease-out-quart:    cubic-bezier(.165,.84,.44,1);
  --ease-in-quint:     cubic-bezier(.755,.05,.855,.06);
  --ease-out-quint:    cubic-bezier(.23,1,.32,1);
  --ease-in-expo:      cubic-bezier(.95,.05,.795,.035);
  --ease-out-expo:     cubic-bezier(.19,1,.22,1);
  --ease-in-circ:      cubic-bezier(.6,.04,.98,.335);
  --ease-out-circ:     cubic-bezier(.075,.82,.165,1);
  --ease-in-out-quad:  cubic-bezier(.455,.03,.515,.955);
  --ease-in-out-cubic: cubic-bezier(.645,.045,.355,1);
  --ease-in-out-quart: cubic-bezier(.77,0,.175,1);
  --ease-in-out-quint: cubic-bezier(.86,0,.07,1);
  --ease-in-out-expo:  cubic-bezier(1,0,0,1);
  --ease-in-out-circ:  cubic-bezier(.785,.135,.15,.86);
}

/* ==================== The house hover ====================
 * 0.16s with --ease-out-quad is their default interaction transition: of 79
 * transition declarations, 26 are 0.16s, more than twice the next duration. The
 * canonical pattern, repeated verbatim across their components:
 *
 *     transition: filter .16s var(--ease-out-quad), transform .16s var(--ease-out-quad);
 *     :hover  { filter: brightness(1.4); }
 *     :active { transform: scale(.97); }
 *
 * They change BRIGHTNESS, not colour. There is no hover colour swap anywhere in the
 * sampled CSS, which means one declaration replaces a per-variant hover colour for
 * every brand and both grounds.
 *
 * Applied here to the FILLED atoms only, and that is a deliberate deviation with a
 * reason. Brightening a fill lifts it on any ground. Brightening dark ink on paper
 * does not: it washes the text toward the page and REMOVES the affordance instead of
 * adding one. linear.app is an all-dark site, so the question never came up there.
 * The transparent variants (.btn--secondary, .btn--ghost) therefore keep their
 * background tint on hover, which is the same "no hover colour on the ink" idea
 * expressed the way a light ground needs.
 *
 * The :active scale sits inside prefers-reduced-motion: no-preference, per the
 * inversion below.
 *
 * KNOWN, and verified by hovering the real button rather than reasoning about it:
 * brightness multiplies and then CLAMPS per channel, so a brand that already sits at
 * a channel ceiling shifts hue instead of just lifting. Galvan's lime #c1fe00 has
 * green at 254, so x1.4 lands on (255,255,0), pure yellow. It reads unmistakably as
 * "lit" and the dark button text stays at 18.9:1, but it is a hue shift, not a
 * brightening. linear.app's indigo #5e6ad2 has no channel near the ceiling and never
 * shows this. If the yellow is unwanted, the document also measures 1.3 and 110% in
 * their CSS; at 110% the lime lifts to (212,255,0) and stays lime. 1.4 is the
 * canonical value of the 26 uses, so it is what ships until someone decides
 * otherwise. */
.hover-bright {
  transition: filter .16s var(--ease-out-quad), transform .16s var(--ease-out-quad);
}
.hover-bright:hover { filter: brightness(1.4); }
@media (prefers-reduced-motion: no-preference) {
  .hover-bright:active { transform: scale(var(--press-scale, .97)); }
}

/* ============================ Button ============================ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-sm, .5rem);
  min-height: var(--tap-min, 44px); padding: var(--space-sm, .5rem) calc(var(--space-lg, 1.5rem) + .25rem);
  font-family: var(--font-body); font-size: var(--text-base, 1rem); font-weight: 600;
  line-height: 1.2; border: 1px solid transparent; border-radius: var(--radius-md, 12px);
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background var(--dur-fast, 150ms) var(--ease-out, ease),
              border-color var(--dur-fast, 150ms) var(--ease-out, ease),
              transform var(--dur-instant, 100ms) var(--ease-out, ease);
}
.btn:active { transform: scale(var(--press-scale, .97)); }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 40%, transparent); }
/* The house hover: brightness, not a colour swap. --primary-700 was a second brand
   step maintained purely so a filled button had somewhere to go on hover. */
.btn--primary {
  background: var(--color-primary); color: var(--color-primary-text, #fff);
  box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,.05));
  transition: filter .16s var(--ease-out-quad), transform var(--dur-instant, 100ms) var(--ease-out, ease);
}
.btn--primary:hover { filter: brightness(1.4); }
/* The RING takes the resolved accent too. A saturated 1px brand outline on paper is
   the neon look: lime at 1.15:1 against the ground is not a visible edge, it is a
   glow. The fill keeps the true brand; an outline is closer to text than to a fill. */
.btn--secondary { background: transparent; color: var(--color-accent-text, var(--color-primary)); border-color: var(--color-accent-text, var(--color-primary)); }
.btn--secondary:hover { background: color-mix(in srgb, var(--color-primary) 8%, transparent); }
.btn--ghost { background: transparent; color: var(--color-text); }
.btn--ghost:hover { background: var(--neutral-100, rgba(0,0,0,.05)); }
.btn--destructive { background: #dc2626; color: #fff; }
.btn--destructive:hover { background: #b91c1c; }
.btn--icon { padding: var(--space-sm, .5rem); min-width: var(--tap-min, 44px); border-radius: var(--radius-full, 9999px); }
.btn--sm { min-height: 36px; padding: var(--space-xs, .25rem) var(--space-md, 1rem); font-size: var(--text-sm, .875rem); }
.btn--lg { min-height: var(--tap-primary, 56px); padding: var(--space-md, 1rem) var(--space-xl, 2rem); font-size: var(--text-lg, 1.125rem); }
.btn--block { display: flex; width: 100%; }

/* ======================== Icon button ========================= */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap-min, 44px); height: var(--tap-min, 44px);
  border: none; background: transparent; color: var(--color-text);
  border-radius: var(--radius-full, 9999px); cursor: pointer;
  transition: background var(--dur-fast, 150ms) var(--ease-out, ease);
}
.icon-btn:hover { background: var(--neutral-100, rgba(0,0,0,.05)); }
.icon-btn--danger { color: #dc2626; }

/* ========================== Inputs =========================== */
.input, .textarea, .select {
  width: 100%; min-height: var(--tap-min, 44px);
  padding: var(--space-sm, .5rem) var(--space-md, 1rem);
  font-family: var(--font-body); font-size: var(--text-base, 1rem); color: var(--color-text);
  background: var(--color-surface, #fff); border: 1px solid var(--neutral-200, #e5e7eb);
  border-radius: var(--radius-md, 8px);
  transition: border-color var(--dur-fast, 150ms) var(--ease-out, ease);
}
.input::placeholder, .textarea::placeholder { color: var(--color-text-muted); }
.input:focus, .textarea:focus, .select:focus {
  outline: none; border-color: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}
.textarea { min-height: calc(var(--tap-min, 44px) * 2); resize: vertical; line-height: 1.5; }
.select { appearance: none; padding-right: var(--space-xl, 2rem);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 1rem center, right .75rem center; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; }
.input[aria-invalid="true"], .textarea[aria-invalid="true"] { border-color: #dc2626; }

/* ==================== Checkbox / radio / switch ==================== */
.checkbox, .radio { width: 20px; height: 20px; min-width: 20px; accent-color: var(--color-primary); cursor: pointer; }
.switch { position: relative; display: inline-flex; align-items: center; width: 44px; height: 26px; cursor: pointer; }
.switch input { position: absolute; opacity: 0; }
.switch .switch__track { width: 44px; height: 26px; background: var(--neutral-200, #e5e7eb); border-radius: var(--radius-full, 9999px); transition: background var(--dur-fast, 150ms) var(--ease-out, ease); }
.switch .switch__thumb { position: absolute; left: 3px; width: 20px; height: 20px; background: #fff; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,.2); transition: transform var(--dur-fast, 150ms) var(--ease-out, ease); }
.switch input:checked + .switch__track { background: var(--color-primary); }
.switch input:checked ~ .switch__thumb { transform: translateX(18px); }

/* ========================== Badge / chip / tag ========================== */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-xs, .25rem);
  padding: 2px var(--space-sm, .5rem); font-size: var(--text-xs, .75rem); font-weight: 600;
  line-height: 1.4; border-radius: var(--radius-full, 9999px);
  background: var(--color-chip, var(--neutral-100, #f3f4f6)); color: var(--color-text);
}
.badge--primary { background: color-mix(in srgb, var(--color-primary) 15%, transparent); color: var(--color-accent-text, var(--color-text, #0f172a)); }
.badge--success { background: #dcfce7; color: #166534; }
.badge--warning { background: #fef3c7; color: #92400e; }
.badge--danger  { background: #fee2e2; color: #991b1b; }
.chip {
  display: inline-flex; align-items: center; gap: var(--space-xs, .25rem);
  min-height: 32px; padding: var(--space-xs, .25rem) var(--space-md, 1rem);
  font-size: var(--text-sm, .875rem); border-radius: var(--radius-full, 9999px);
  background: var(--color-chip, var(--neutral-100, #f3f4f6)); color: var(--color-text); cursor: pointer;
  border: 1px solid transparent; transition: all var(--dur-fast, 150ms) var(--ease-out, ease);
}
/* The other brand FILL in the atom layer, so it takes the same house hover. */
.chip--selected { background: var(--color-primary); color: var(--color-primary-text, #0b0b0b); transition: filter .16s var(--ease-out-quad); }
.chip--selected:hover { filter: brightness(1.4); }
.chip__remove { display: inline-flex; cursor: pointer; opacity: .7; }
.tag { display: inline-block; padding: 1px var(--space-sm, .5rem); font-size: var(--text-xs, .75rem); border-radius: var(--radius-sm, 4px); background: var(--color-chip, var(--neutral-100, #f3f4f6)); color: var(--color-text-muted); }

/* ========================== Avatar ========================== */
.avatar { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: var(--radius-full, 9999px); overflow: hidden; background: var(--color-chip-strong, var(--neutral-200, #e5e7eb)); color: var(--color-text); font-size: var(--text-sm, .875rem); font-weight: 600; flex-shrink: 0; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar--sm { width: 28px; height: 28px; font-size: var(--text-xs, .75rem); }
.avatar--lg { width: 56px; height: 56px; font-size: var(--text-lg, 1.125rem); }

/* ==================== Tracking as a function of SIZE ====================
 * Letter-spacing is a computed function of font size, not a per-element guess.
 * Measured off linear.app and verified against RENDERED values, not read off their
 * token list (see LINEAR-DECONSTRUCTION.md, "Type scale"):
 *   72px renders -1.584px (72 x -0.022)   48px -1.056px   32px -0.704px
 *   24px renders -0.288px (24 x -0.012)
 *   15px renders -0.165px (-0.011)   14px -0.182px (-0.013)   13px -0.13px (-0.010)
 * Shape of the rule:
 *   >= 32px        -0.022em
 *   20px to 24px   -0.012em
 *   13px to 15px   -0.010em to -0.013em
 *   12px and 17px   0
 * So tracking tightens as size climbs and relaxes back to zero at the smallest UI
 * sizes. The old declaration ran a flat -0.02em on .heading with -0.03em on h1 and
 * -0.025em on h2, so a 20px .heading--h4 (1058 uses, the most-used heading in the
 * library) inherited DISPLAY tracking: -0.4px per gap on a size Linear sets at
 * -0.24px, and nothing anywhere on their site is tighter than -0.022em. -0.03em is
 * off the top of the measured range entirely.
 * Exposed as tokens so a section that sizes its own heading can pick the band that
 * matches the size it actually rendered, instead of guessing a new number. */
:root {
  --track-display: -0.022em;  /* >= 32px */
  --track-title:   -0.012em;  /* 20px to 24px */
  --track-body:    -0.011em;  /* 13px to 16px; measured at 15px */
}

/* ========================== Typography atoms ========================== */
.text { font-family: var(--font-body); font-size: var(--text-base, 1rem); color: var(--color-text); line-height: 1.6; }
.text--muted { color: var(--color-text-muted); }
.text--lead { font-size: var(--text-lg, 1.125rem); line-height: 1.6; }
.text--caption { font-size: var(--text-sm, .875rem); color: var(--color-text-muted); }
/* Premium type: tight, confident headings (negative tracking + tight leading).
   The BASE carries the body band, not the display band, because a bare `.heading`
   with no size modifier is whatever size the section gave it and in this library that
   is 18px. Every size modifier below restates its own band at equal specificity and
   later in the file, so the modifier always wins. */
.heading { font-family: var(--font-display); color: var(--color-text); line-height: 1.1; letter-spacing: var(--track-body); font-weight: 700; }
/* Leading tightens as size grows: the measured reference build runs 0.94 on its
   150px hero, Klim runs 1.0 on 218px. At 1.05 a large heading reads loose and timid. */
.heading--h1 { font-size: var(--text-display, 3rem); line-height: 0.95; letter-spacing: var(--track-display); font-weight: 800; }
.heading--h2 { font-size: var(--text-3xl, 2rem); line-height: 1.02; letter-spacing: var(--track-display); }
.heading--h3 { font-size: var(--text-2xl, 1.5rem); font-weight: 700; letter-spacing: var(--track-title); }
.heading--h4 { font-size: var(--text-xl, 1.25rem); font-weight: 700; letter-spacing: var(--track-title); }
.link { color: var(--color-accent-text, var(--color-primary)); text-decoration: none; }
.link:hover { text-decoration: underline; }
.link--muted { color: var(--color-text-muted); }

/* ========================== Divider ========================== */
.divider { border: none; border-top: 1px solid var(--neutral-200, #e5e7eb); margin: var(--space-lg, 1.5rem) 0; }
.divider--vertical { border-top: none; border-left: 1px solid var(--neutral-200, #e5e7eb); margin: 0 var(--space-lg, 1.5rem); align-self: stretch; }

/* ==================== Spinner / progress / skeleton ==================== */
.spinner { display: inline-block; width: 24px; height: 24px; border: 2px solid var(--neutral-200, #e5e7eb); border-top-color: var(--color-primary); border-radius: 50%; }
@keyframes sf-spin { to { transform: rotate(360deg); } }
/* Reduced motion by INVERSION. Every keyframe on linear.app sits inside
   prefers-reduced-motion: no-preference rather than being switched off inside
   `reduce`, so the DEFAULT is the end state and reduced motion needs no override at
   all. That is the whole trick: a rule that has to be remembered somewhere else is a
   rule that gets forgotten, and the failure is silent. The ring keeps its brand-
   coloured top border either way, so it still reads as "working" when it is still. */
@media (prefers-reduced-motion: no-preference) {
  .spinner { animation: sf-spin .8s linear infinite; }
}
.progress { width: 100%; height: 8px; background: var(--neutral-200, #e5e7eb); border-radius: var(--radius-full, 9999px); overflow: hidden; }
/* No transition on width: the library ships no JS and fills are server-rendered,
   so it never animated anything, while animating width causes layout thrash (the
   impeccable layout-transition rule flagged it on 100% of the library). A consumer
   that does animate a fill should do it with transform: scaleX() instead. */
.progress__fill { height: 100%; background: var(--color-primary); border-radius: inherit; }
.skeleton { position: relative; overflow: hidden; background: var(--neutral-100, #f3f4f6); border-radius: var(--radius-md, 8px); }
.skeleton--text { height: 1em; border-radius: var(--radius-sm, 4px); }
.skeleton--circle { border-radius: 50%; }

/* ========================== Tooltip ========================== */
.tooltip { position: relative; display: inline-flex; }
.tooltip__bubble { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); padding: var(--space-xs, .25rem) var(--space-sm, .5rem); font-size: var(--text-xs, .75rem); color: #fff; background: var(--neutral-900, #111827); border-radius: var(--radius-sm, 4px); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--dur-fast, 150ms) var(--ease-out, ease); }
.tooltip:hover .tooltip__bubble, .tooltip:focus-within .tooltip__bubble { opacity: 1; }

/* ========================== Overline / eyebrow ========================== */
/* Uppercase letter-spaced label above titles. Add .text--accent for brand colour. */
.text--overline { font-size: var(--text-xs, .75rem); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--color-text-muted); }
.text--accent { color: var(--color-accent-text, var(--color-text, #0f172a)); }
.text--unit { font-size: .7em; font-weight: 500; color: var(--color-text-muted); }

/* ========================== Blockquote (large light quote) ========================== */
/* --font-accent, not --font-display: a pull-quote IS the "rare editorial statement"
   the accent face exists for. --font-accent falls back to --font-display, so a site
   that sets no accent face is unchanged. */
.blockquote { font-family: var(--font-accent, var(--font-display)); font-size: var(--text-2xl, 1.5rem); font-weight: 400; font-style: italic; line-height: 1.4; color: var(--color-text); }

/* ========================== Extra atom variants ========================== */
.chip--outline { background: transparent; border-color: var(--color-chip-strong, var(--neutral-200, #e5e7eb)); }
.chip--outline.chip--selected { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-text, #0b0b0b); }
.avatar--square { border-radius: var(--radius-md, 8px); }

/* ========================== More atoms (backlog consolidation) ========================== */
.text--strong { font-weight: 600; }
.heading--h5 { font-size: var(--text-base, 1rem); font-weight: 700; line-height: 1.3; letter-spacing: var(--track-body); }
.text--overline-lg { font-size: var(--text-sm, .875rem); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--color-text-muted); }
.kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 1.6em; padding: 1px var(--space-xs, .25rem); font-family: var(--font-mono, monospace); font-size: var(--text-xs, .75rem); color: var(--color-text); background: var(--color-chip, var(--neutral-100, #f3f4f6)); border: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); border-bottom-width: 2px; border-radius: var(--radius-sm, 4px); }
/* Range / slider */
.range { -webkit-appearance: none; appearance: none; width: 100%; height: 6px; border-radius: var(--radius-full, 9999px); background: var(--neutral-200, #e5e7eb); outline: none; }
.range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border: none; border-radius: 50%; background: var(--color-primary); cursor: pointer; }
.range::-moz-range-thumb { width: 18px; height: 18px; border: none; border-radius: 50%; background: var(--color-primary); cursor: pointer; }
/* Status glyph (check/cross in a tinted round holder) */
.status-dot { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0; }
.status-dot > svg { width: 60%; height: 60%; }
.status-dot--success { background: #dcfce7; color: #166534; }
.status-dot--danger  { background: #fee2e2; color: #991b1b; }
.status-dot--muted   { background: var(--color-chip, var(--neutral-100, #f3f4f6)); color: var(--color-text-muted); }
/* Circular / radial progress (drive with --value: 0..100) */
.progress-ring { --value: 0; display: inline-grid; place-items: center; width: 64px; aspect-ratio: 1; border-radius: 50%; background: conic-gradient(var(--color-primary) calc(var(--value) * 1%), var(--neutral-200, #e5e7eb) 0); }
.progress-ring::before { content: ""; grid-area: 1 / 1; width: 76%; aspect-ratio: 1; border-radius: 50%; background: var(--color-surface, #fff); }
.progress-ring__label { grid-area: 1 / 1; font-weight: 700; font-size: var(--text-sm, .875rem); }

/* ========================== More variants (admin gaps) ========================== */
.badge--info { background: #dbeafe; color: #1e40af; }
.link--danger { color: #ef4444; }
.link--danger:hover { color: #dc2626; }
.text--warning { color: #f59e0b; }
.text--danger { color: #ef4444; }
/* molecules.css — canonical MOLECULE layer. Composes atoms; adds layout + minimal glue
 * only, never restyles the atoms inside it. Implements the molecules in
 * ~/projects/design-system/catalog.json. Load after atoms.css. See ATOMIZATION.md.
 */

/* ========================== Field ========================== */
.field { display: flex; flex-direction: column; gap: var(--space-xs, .25rem); }
.field__label { font-size: var(--text-sm, .875rem); font-weight: 600; color: var(--color-text); }
.field__label .req { color: #dc2626; }
.field__help { font-size: var(--text-xs, .75rem); color: var(--color-text-muted); }
.field__error { font-size: var(--text-xs, .75rem); color: #dc2626; }

/* ========================== Card ========================== */
.card { display: flex; flex-direction: column; background: var(--color-surface, #fff); border: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); border-radius: var(--radius-lg, 18px); box-shadow: var(--shadow-md, 0 1px 2px rgba(0,0,0,.04), 0 8px 24px -6px rgba(0,0,0,.10)); overflow: hidden; }
.card__media { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.card__body { display: flex; flex-direction: column; gap: var(--space-sm, .5rem); padding: var(--space-lg, 1.5rem); }
.card__actions { display: flex; gap: var(--space-sm, .5rem); padding: 0 var(--space-lg, 1.5rem) var(--space-lg, 1.5rem); margin-top: auto; }
.card--feature { text-align: center; align-items: center; }
.card--horizontal { flex-direction: row; }
.card--horizontal .card__media { width: 40%; aspect-ratio: auto; }
.card--overlay { position: relative; color: #fff; }
.card--overlay .card__body { position: absolute; inset: auto 0 0 0; background: linear-gradient(transparent, rgba(0,0,0,.75)); }
/* Clickable card: whole card is an affordance (pair with an <a> wrap or onclick) */
.card--link { cursor: pointer; text-decoration: none; color: inherit; transition: border-color var(--dur-fast, 150ms) var(--ease-out, ease), transform var(--dur-fast, 150ms) var(--ease-out, ease); }
.card--link:hover { border-color: var(--color-primary); }
.card--link:active { transform: scale(.995); }

/* ========================== List item ========================== */
.list-item { display: flex; align-items: center; gap: var(--space-md, 1rem); min-height: var(--tap-min, 44px); padding: var(--space-sm, .5rem) var(--space-md, 1rem); }
.list-item__leading { flex-shrink: 0; }
.list-item__content { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.list-item__title { font-weight: 600; color: var(--color-text); }
.list-item__subtitle { font-size: var(--text-sm, .875rem); color: var(--color-text-muted); }
.list-item__trailing { flex-shrink: 0; margin-left: auto; color: var(--color-text-muted); }

/* ========================== Menu item ========================== */
/* Atoms and molecules that are legitimately applied to a <button> (the accessible
   element for a tab, a chip filter or a menu row) must neutralise the UA box, or
   the browser paints its own border and shrink-to-fit width through them. Measured
   usage: .tab on a button in 9 components, .chip in 12, .menu-item in 1. */
button.menu-item, button.tab, button.chip, button.accordion-item__header {
  appearance: none; -webkit-appearance: none; background: none; border: 0;
  font: inherit; color: inherit;
}

.menu-item { display: flex; align-items: center; gap: var(--space-sm, .5rem); min-height: var(--tap-min, 44px); padding: var(--space-sm, .5rem) var(--space-md, 1rem); color: var(--color-text); cursor: pointer; border-radius: var(--radius-md, 8px); }
.menu-item:hover, .menu-item--active { background: var(--color-chip, var(--neutral-100, rgba(0,0,0,.05))); }
.menu-item--destructive { color: #dc2626; }
.menu-item__shortcut { margin-left: auto; font-size: var(--text-xs, .75rem); color: var(--color-text-muted); }

/* ========================== Tabs ========================== */
.tabs { display: flex; gap: var(--space-xs, .25rem); border-bottom: 1px solid var(--neutral-200, #e5e7eb); }
.tab { display: inline-flex; align-items: center; gap: var(--space-xs, .25rem); min-height: var(--tap-min, 44px); padding: var(--space-sm, .5rem) var(--space-md, 1rem); color: var(--color-text-muted); cursor: pointer; border-bottom: 2px solid transparent; }
.tab--active { color: var(--color-accent-text, var(--color-primary)); border-bottom-color: var(--color-primary); }

/* ========================== Segmented control ========================== */
.segmented { display: inline-flex; padding: 3px; background: var(--color-chip, var(--neutral-100, #f3f4f6)); border-radius: var(--radius-md, 8px); }
.segmented__item { min-height: 36px; padding: var(--space-xs, .25rem) var(--space-md, 1rem); border-radius: calc(var(--radius-md, 8px) - 2px); cursor: pointer; color: var(--color-text-muted); }
.segmented__item--active { background: var(--color-surface, #fff); color: var(--color-text); box-shadow: 0 1px 2px rgba(0,0,0,.1); }

/* ========================== Alert ========================== */
.alert { display: flex; gap: var(--space-md, 1rem); padding: var(--space-md, 1rem); border-radius: var(--radius-md, 8px); border: 1px solid; }
.alert__body { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.alert__title { font-weight: 600; }
.alert--info    { background: #eff6ff; border-color: #bfdbfe; color: #1e40af; }
.alert--success { background: #f0fdf4; border-color: #bbf7d0; color: #166534; }
.alert--warning { background: #fffbeb; border-color: #fde68a; color: #92400e; }
.alert--danger  { background: #fef2f2; border-color: #fecaca; color: #991b1b; }

/* ========================== Toast ========================== */
.toast { display: flex; align-items: center; gap: var(--space-md, 1rem); padding: var(--space-sm, .5rem) var(--space-md, 1rem); background: var(--neutral-900, #111827); color: #fff; border-radius: var(--radius-md, 8px); box-shadow: 0 4px 12px rgba(0,0,0,.2); }

/* ========================== Stat ========================== */
.stat { display: flex; flex-direction: column; gap: var(--space-xs, .25rem); }
.stat__value { font-family: var(--font-display); font-size: var(--text-3xl, 2rem); font-weight: 800; color: var(--color-text); line-height: 1; }
.stat__label { font-size: var(--text-sm, .875rem); color: var(--color-text-muted); }
.stat__delta--up { color: #16a34a; } .stat__delta--down { color: #dc2626; }

/* ========================== Search input ========================== */
.search { display: flex; align-items: center; gap: var(--space-sm, .5rem); min-height: var(--tap-min, 44px); padding: 0 var(--space-md, 1rem); background: var(--color-surface, #fff); border: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); border-radius: var(--radius-full, 9999px); }
.search input { flex: 1; border: none; background: transparent; outline: none; font-size: var(--text-base, 1rem); color: var(--color-text); }

/* ========================== Accordion item ========================== */
.accordion-item { border-bottom: 1px solid var(--neutral-200, #e5e7eb); }
/* A <button> is the correct, accessible element for an accordion trigger, and 3
   components use one. Without a UA reset the browser paints its own
   `border: 2px outset` and shrink-to-fit width straight through, so the trigger
   renders as a native button box sized to its text. Neither gate catches this:
   audit.py sees a defined class, and the detector has no rule for "looks like an
   unstyled button". Reset here so the molecule is safe on the RIGHT element. */
.accordion-item__header { appearance: none; -webkit-appearance: none;
  background: none; border: 0; width: 100%; text-align: left; font: inherit;
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-md, 1rem); min-height: var(--tap-min, 44px); padding: var(--space-md, 1rem) 0; cursor: pointer; font-weight: 600; color: var(--color-text); }
.accordion-item__chevron { flex-shrink: 0; color: var(--color-text-muted); transition: transform var(--dur-normal, 200ms) var(--ease-out, ease); }
.accordion-item--open .accordion-item__chevron { transform: rotate(180deg); }
/* No height transition here: animating max-height thrashes layout, and the library
   ships no JS so nothing toggles the open class anyway. A consumer that wires the
   accordion should animate grid-template-rows on a wrapper instead. */
.accordion-item__panel { max-height: 0; overflow: hidden; color: var(--color-text-muted); }
.accordion-item--open .accordion-item__panel { max-height: 400px; padding-bottom: var(--space-md, 1rem); }

/* ========================== Breadcrumb / pagination ========================== */
.breadcrumb { display: flex; align-items: center; gap: var(--space-xs, .25rem); font-size: var(--text-sm, .875rem); color: var(--color-text-muted); }
.breadcrumb__sep { opacity: .5; }
.pagination { display: flex; gap: var(--space-xs, .25rem); }
.pagination__item { display: inline-flex; align-items: center; justify-content: center; min-width: var(--tap-min, 44px); min-height: var(--tap-min, 44px); border-radius: var(--radius-md, 8px); color: var(--color-text); cursor: pointer; }
.pagination__item--active { background: var(--color-primary); color: var(--color-primary-text, #0b0b0b); }
.pagination__item--disabled { opacity: .4; pointer-events: none; }

/* ========================== Dropdown / popover ========================== */
.dropdown { position: relative; display: inline-flex; }
.dropdown__menu, .popover__panel { position: absolute; top: calc(100% + 4px); min-width: 180px; padding: var(--space-xs, .25rem); background: var(--color-surface, #fff); border: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); border-radius: var(--radius-md, 8px); box-shadow: 0 4px 12px rgba(0,0,0,.12); z-index: 20; }

/* ========================== Stepper ========================== */
.stepper { display: flex; align-items: center; gap: var(--space-sm, .5rem); }
.stepper-step { display: flex; align-items: center; gap: var(--space-sm, .5rem); }
.stepper-step__marker { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; background: var(--color-chip-strong, var(--neutral-200, #e5e7eb)); color: var(--color-text-muted); font-size: var(--text-sm, .875rem); }
.stepper-step--current .stepper-step__marker { background: var(--color-primary); color: var(--color-primary-text, #0b0b0b); }
.stepper-step--done .stepper-step__marker { background: #16a34a; color: #fff; }
.stepper-step__connector { width: 32px; height: 2px; background: var(--neutral-200, #e5e7eb); }

/* ========================== Avatar group ========================== */
.avatar-group { display: inline-flex; }
.avatar-group > .avatar { margin-left: -8px; border: 2px solid var(--color-surface, #fff); }
.avatar-group > .avatar:first-child { margin-left: 0; }

/* ========================== Code block / embed ========================== */
.code-block { border-radius: var(--radius-md, 8px); overflow: hidden; background: var(--neutral-900, #111827); }
.code-block__header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-xs, .25rem) var(--space-md, 1rem); color: #cbd5e1; font-size: var(--text-xs, .75rem); }
.code-block pre { padding: var(--space-md, 1rem); overflow-x: auto; color: #e2e8f0; font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--text-sm, .875rem); }
.embed { position: relative; aspect-ratio: 16 / 9; border-radius: var(--radius-md, 8px); overflow: hidden; }
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: none; }

/* ========================== Icon tile (rounded-square glyph container) ========================== */
.icon-tile { display: inline-flex; align-items: center; justify-content: center; width: 48px; height: 48px; border-radius: var(--radius-md, 8px); background: color-mix(in srgb, var(--color-primary) 12%, transparent); color: var(--color-accent-text, var(--color-primary)); flex-shrink: 0; }
.icon-tile--sm { width: 36px; height: 36px; border-radius: var(--radius-sm, 4px); }
.icon-tile--lg { width: 64px; height: 64px; border-radius: var(--radius-lg, 16px); }
.icon-tile--round { border-radius: var(--radius-full, 9999px); }
.icon-tile > svg { width: 55%; height: 55%; }

/* ========================== Rating (stars) ========================== */
.rating { display: inline-flex; align-items: center; gap: 2px; color: #f59e0b; }
.rating__star { width: 1em; height: 1em; }
.rating__star--empty { color: var(--neutral-200, #e5e7eb); }
.rating__value { margin-left: var(--space-xs, .25rem); font-size: var(--text-sm, .875rem); color: var(--color-text-muted); }

/* ========================== Play overlay (media affordance) ========================== */
.play-overlay { position: relative; display: block; }
.play-overlay__btn { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: inline-flex; align-items: center; justify-content: center; width: 72px; height: 72px; border-radius: var(--radius-full, 9999px); background: var(--color-primary); color: var(--color-primary-text, #0b0b0b); border: none; cursor: pointer; box-shadow: 0 4px 20px rgba(0,0,0,.3); }
.play-overlay__btn > svg { width: 40%; height: 40%; }

/* ========================== Toast severity variants ========================== */
.toast--success { background: #166534; }
.toast--info    { background: #1e40af; }
.toast--warning { background: #92400e; }
.toast--danger  { background: #991b1b; }
.toast--light   { background: var(--color-surface, #fff); color: var(--color-text); border: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); }

/* ============ Native <details> accordion (JS-free) ============ */
details.accordion-item > summary { list-style: none; cursor: pointer; }
details.accordion-item > summary::-webkit-details-marker { display: none; }
details.accordion-item[open] .accordion-item__chevron { transform: rotate(180deg); }
details.accordion-item:not([open]) .accordion-item__panel { max-height: 0; padding-bottom: 0; }
details.accordion-item[open] .accordion-item__panel { max-height: 400px; padding-bottom: var(--space-md, 1rem); }

/* ========================== Check list (feature ticks) ========================== */
.check-list { display: flex; flex-direction: column; gap: var(--space-sm, .5rem); }
.check-item { display: flex; align-items: flex-start; gap: var(--space-sm, .5rem); }
.check-item__mark { flex-shrink: 0; color: var(--color-accent-text, var(--color-primary)); margin-top: 2px; }
.check-item--muted .check-item__mark { color: var(--color-text-muted); }
.check-item--no .check-item__mark { color: #dc2626; }

/* ========================== Input affix (prefix / suffix adornment) ========================== */
.input-affix { display: inline-flex; align-items: center; width: 100%; background: var(--color-surface, #fff); border: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); border-radius: var(--radius-md, 8px); }
.input-affix:focus-within { border-color: var(--color-primary); }
.input-affix .input { border: none; background: transparent; flex: 1; min-width: 0; }
.input-affix .input:focus { box-shadow: none; }
.input-affix__prefix, .input-affix__suffix { padding: 0 var(--space-sm, .5rem); color: var(--color-text-muted); white-space: nowrap; }

/* ========================== Labeled divider (line — label — line) ========================== */
.divider--labeled { display: flex; align-items: center; gap: var(--space-md, 1rem); border: none; color: var(--color-text-muted); font-size: var(--text-sm, .875rem); }
.divider--labeled::before, .divider--labeled::after { content: ""; flex: 1; height: 1px; background: var(--color-chip-strong, var(--neutral-200, #e5e7eb)); }

/* ========================== Search flush variant (top-bar, no pill) ========================== */
.search--flush { border: none; border-bottom: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); border-radius: 0; background: transparent; }

/* ========================== Data table (admin grid) ========================== */
.data-table { width: 100%; background: var(--color-surface, #fff); border: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); border-radius: var(--radius-lg, 12px); overflow: hidden; }
.data-table__scroll { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: var(--text-sm, .875rem); }
.table thead th { text-align: left; padding: var(--space-sm, .5rem) var(--space-lg, 1.5rem); font-size: var(--text-xs, .75rem); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--color-text-muted); background: var(--color-chip, var(--neutral-100, #f9fafb)); border-bottom: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); white-space: nowrap; }
.table thead th a { color: inherit; }
.table td { padding: var(--space-sm, .5rem) var(--space-lg, 1.5rem); border-bottom: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); color: var(--color-text); vertical-align: middle; }
.table tbody tr { transition: background var(--dur-fast, 150ms) var(--ease-out, ease); }
.table tbody tr:hover { background: var(--color-chip, var(--neutral-100, #f9fafb)); }
.table tbody tr:last-child td { border-bottom: none; }
.table--clickable tbody tr { cursor: pointer; }
.data-table__foot { display: flex; align-items: center; justify-content: space-between; padding: var(--space-sm, .5rem) var(--space-lg, 1.5rem); border-top: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); font-size: var(--text-sm, .875rem); color: var(--color-text-muted); }

/* ========================== Modal / dialog ========================== */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; padding: var(--space-lg, 1.5rem); z-index: 50; }
.modal { width: 100%; max-width: 440px; background: var(--color-surface, #fff); border: 1px solid var(--color-chip-strong, var(--neutral-200, #e5e7eb)); border-radius: var(--radius-lg, 12px); box-shadow: var(--shadow-lg, 0 12px 32px rgba(0,0,0,.16)); overflow: hidden; }
.modal__body { padding: var(--space-lg, 1.5rem); }
.modal__actions { display: flex; justify-content: flex-end; gap: var(--space-sm, .5rem); padding: var(--space-md, 1rem) var(--space-lg, 1.5rem); border-top: 1px solid var(--neutral-200, #e5e7eb); }
/* interactions.css — the BEHAVIOUR layer: reusable micro-interactions & effects.
 * Implements the `interaction` entries in ~/projects/design-system/catalog.json,
 * grounded in design-knowledge-base/synthesis/interaction.md. Load after atoms/molecules.
 * Motion tokens have baseline fallbacks so effects work even without tokens-motion.css.
 */

/* Motion token defaults (mirrors design-system/tokens/tokens-motion.css). */
:root {
  --dur-instant: 100ms; --dur-fast: 150ms; --dur-normal: 200ms; --dur-slow: 320ms; --dur-signal: 800ms;
  --delay-secondary: 300ms;
  --ease-standard: cubic-bezier(.2,0,0,1); --ease-in: cubic-bezier(.4,0,1,1);
  /* --ease-emphasized previously used an overshoot curve (a control point above 1),
     which DESIGN-PRINCIPLES bans and the impeccable detector flagged across the
     whole library. It is now an ease-out-quint: still emphatic, decelerates like a
     real object, no bounce. Do NOT quote the old value here; the detector scans
     stylesheet text, so a banned literal in a comment reports as a real finding. */
  --ease-out: cubic-bezier(0,0,.2,1); --ease-emphasized: cubic-bezier(.22,1,.36,1);
  --press-scale: .97; --shimmer-dur: 1.2s;
}

/* press-feedback / hover-raise / focus-ring — utility hooks any element can adopt */
.fx-press:active { transform: scale(var(--press-scale)); }
.fx-hover-raise { transition: transform var(--dur-instant) var(--ease-out), box-shadow var(--dur-instant) var(--ease-out); }
@media (hover: hover) { .fx-hover-raise:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,.12); } }
.fx-focus-ring:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 40%, transparent); }

/* ripple */
.fx-ripple { position: relative; overflow: hidden; }
.fx-ripple::after { content: ""; position: absolute; inset: 0; background: radial-gradient(circle, currentColor 10%, transparent 10.01%) center/15000%; opacity: 0; transition: opacity var(--dur-slow) var(--ease-out); }
.fx-ripple:active::after { background-size: 100%; opacity: .18; transition: background-size 0s; }

/* skeleton-shimmer */
.skeleton::before { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,.6), transparent); transform: translateX(-100%); animation: sf-shimmer var(--shimmer-dur) infinite; }
@keyframes sf-shimmer { 100% { transform: translateX(100%); } }

/* enter transitions: toast / sheet / drawer / modal */
.fx-toast-enter   { animation: sf-toast-in var(--dur-normal) var(--ease-out); }
.fx-sheet-enter   { animation: sf-sheet-up var(--dur-slow) var(--ease-out); }
.fx-drawer-enter  { animation: sf-drawer-in var(--dur-slow) var(--ease-out); }
.fx-modal-enter   { animation: sf-modal-in var(--dur-normal) var(--ease-out); }
@keyframes sf-toast-in  { from { opacity: 0; transform: translateY(12px); } }
@keyframes sf-sheet-up  { from { transform: translateY(100%); } }
@keyframes sf-drawer-in { from { transform: translateX(-100%); } }
@keyframes sf-modal-in  { from { opacity: 0; transform: scale(.96); } }

/* list-stagger: children enter in sequence to direct attention */
.fx-stagger > * { opacity: 0; animation: sf-fade-up var(--dur-fast) var(--ease-out) forwards; }
.fx-stagger > *:nth-child(1){animation-delay:0ms} .fx-stagger > *:nth-child(2){animation-delay:60ms}
.fx-stagger > *:nth-child(3){animation-delay:120ms} .fx-stagger > *:nth-child(4){animation-delay:180ms}
.fx-stagger > *:nth-child(5){animation-delay:240ms} .fx-stagger > *:nth-child(n+6){animation-delay:300ms}
@keyframes sf-fade-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* success-flourish */
.fx-flourish { animation: sf-pop var(--dur-signal) var(--ease-emphasized); }
@keyframes sf-pop { 0%{transform:scale(1)} 30%{transform:scale(1.08)} 100%{transform:scale(1)} }

/* Accessibility: honour reduced-motion — strip movement, keep feedback. Non-negotiable. */
@media (prefers-reduced-motion: reduce) {
  .fx-hover-raise:hover, .fx-press:active, .fx-flourish { transform: none; }
  .skeleton::before, .fx-stagger > *, .fx-toast-enter, .fx-sheet-enter, .fx-drawer-enter, .fx-modal-enter { animation: none; }
  .fx-stagger > * { opacity: 1; }
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
/* ===== Admin layout + contrast layer (Lattice-token-driven; appended after the library) =====
   Source for the admin shell layout and a few contrast overrides. Concatenated into
   assets/lattice.css by build_lattice_css.sh (after the brand tokens + component library). */
* { box-sizing: border-box; }
body.bp-admin { margin:0; background:var(--color-bg); color:var(--color-text); font-family:var(--font-body); display:flex; min-height:100vh; -webkit-font-smoothing:antialiased; font-size:var(--text-base); }
a { color:inherit; text-decoration:none; }
::-webkit-scrollbar{width:10px;height:10px} ::-webkit-scrollbar-thumb{background:var(--neutral-200);border-radius:6px;border:2px solid var(--color-bg)} ::-webkit-scrollbar-thumb:hover{background:var(--neutral-300)}

.bp-sidebar { width:240px; background:var(--neutral-900); border-right:1px solid var(--neutral-200); position:fixed; inset:0 auto 0 0; display:flex; flex-direction:column; }
.bp-brand { display:flex; align-items:center; gap:.5rem; height:56px; padding:0 var(--space-lg); border-bottom:1px solid var(--neutral-200); font-weight:600; }
.bp-logo { width:26px;height:26px;border-radius:7px;background:var(--color-primary);color:var(--color-primary-text,#0d0d0d);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.9rem;flex-shrink:0; }
.bp-nav { flex:1; overflow-y:auto; padding:var(--space-md) var(--space-sm); }
/* Collapsible groups. An app with 60 collections rendered every group open at once,
   so the sidebar was one long scroll and the headings were too quiet to scan against.
   <details>/<summary> gives native collapse, keyboard support and correct semantics
   with no JS; the script only remembers which groups you left open.
   Headings are larger and brighter than the old .6875rem muted uppercase, because they
   are now the primary way you navigate rather than passive labels. */
.bp-nav__group { border:0; }
.bp-nav__section { padding:var(--space-md) var(--space-md) var(--space-xs); font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--color-text); }
summary.bp-nav__section { display:flex; align-items:center; gap:.4rem; cursor:pointer; list-style:none; border-radius:var(--radius-md); user-select:none; }
summary.bp-nav__section::-webkit-details-marker { display:none; }
summary.bp-nav__section:hover { background:var(--neutral-100); }
summary.bp-nav__section:focus-visible { outline:2px solid var(--color-primary); outline-offset:2px; }
/* Disclosure triangle, rotated by the open state so the control reads as a control. */
.bp-nav__caret { width:.55rem; height:.55rem; flex-shrink:0; border-right:2px solid currentColor; border-bottom:2px solid currentColor; transform:rotate(-45deg); transition:transform .15s ease; opacity:.7; }
.bp-nav__group[open] > summary .bp-nav__caret { transform:rotate(45deg); }
.bp-nav__count { margin-left:auto; font-size:.7rem; font-weight:500; color:var(--color-text-muted); }
@media (prefers-reduced-motion: reduce) { .bp-nav__caret { transition:none; } }
.bp-nav__item { display:flex; align-items:center; gap:.5rem; padding:.4rem var(--space-md); border-radius:var(--radius-md); color:var(--neutral-500); font-size:.875rem; font-weight:500; transition:all .12s ease; }
.bp-nav__item:hover { background:var(--neutral-100); color:var(--color-text); }
.bp-nav__item--active { background:var(--neutral-100); color:var(--color-text); }
.bp-nav__ext { margin-left:auto; font-size:.7rem; color:var(--color-text-muted); }
.bp-main { flex:1; margin-left:240px; min-width:0; }
.bp-header { height:56px; position:sticky; top:0; z-index:10; display:flex; align-items:center; justify-content:space-between; gap:var(--space-md); padding:0 var(--space-xl); background:color-mix(in srgb, var(--color-bg) 90%, transparent); backdrop-filter:blur(8px); border-bottom:1px solid var(--neutral-200); }
.bp-header h1 { font-size:.95rem; font-weight:600; margin:0; }
.bp-content { padding:var(--space-xl); max-width:1400px; }
.bp-user { display:flex; align-items:center; gap:.6rem; padding:var(--space-md); border-top:1px solid var(--neutral-200); }
.bp-avatar { width:28px;height:28px;border-radius:50%;background:var(--neutral-200);display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:600;flex-shrink:0; }
.bp-row { display:flex; align-items:center; gap:var(--space-sm); }
.bp-between { display:flex; align-items:center; justify-content:space-between; gap:var(--space-md); }
.bp-stack { display:flex; flex-direction:column; gap:var(--space-md); }
.bp-muted { color:var(--color-text-muted); }
.bp-mono { font-family:var(--font-mono); }

/* ---- Form-control contrast: fields read clearly against panels AND the page ---- */
.input, textarea.input, textarea, select,
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=search], input[type=url], input[type=tel], input[type=date], input[type=datetime-local] {
  background:#242424; color:var(--color-text); border:1px solid #454545; border-radius:var(--radius-md);
  padding:.5rem .75rem; font-family:inherit; font-size:var(--text-sm);
  /* Unify control heights so inputs, selects, and buttons line up in filter rows */
  min-height:36px; line-height:1.25; box-sizing:border-box;
}
textarea, textarea.input { min-height:96px; }
/* Buttons share the 36px control height so they baseline-align with fields */
.btn--sm { min-height:36px; }
/* A filter/toolbar row: fields + buttons aligned at the bottom */
.bp-filter-row { display:flex; flex-wrap:wrap; gap:var(--space-md); align-items:flex-end; }
.bp-filter-row .field { margin:0; }
.input::placeholder, textarea::placeholder { color:#8f8f8f; }
select option { background:var(--neutral-800); color:var(--color-text); }
.input:focus, textarea:focus, select:focus, input:focus { outline:none; border-color:var(--color-primary); box-shadow:0 0 0 2px rgba(62,207,142,.28); }
.field__label { color:var(--color-text); font-weight:600; }
.checkbox, .radio { accent-color:var(--color-primary); width:16px; height:16px; }

/* ===== Prose: rendered markdown (docs pages) styled with Lattice tokens ===== */
.bp-prose { color: var(--color-text); font-size: var(--text-base); line-height: 1.7; max-width: 800px; }
.bp-prose > :first-child { margin-top: 0; }
.bp-prose h1 { font-size: 1.6rem; font-weight: 700; line-height: 1.2; margin: 0 0 1rem; padding-bottom: .5rem; border-bottom: 1px solid var(--neutral-200); }
.bp-prose h2 { font-size: 1.25rem; font-weight: 600; margin: 2rem 0 .75rem; padding-bottom: .35rem; border-bottom: 1px solid var(--neutral-200); }
.bp-prose h3 { font-size: 1.05rem; font-weight: 600; margin: 1.5rem 0 .5rem; }
.bp-prose h4 { font-size: .95rem; font-weight: 600; margin: 1.25rem 0 .4rem; color: var(--color-text); }
.bp-prose p { margin: 0 0 1rem; }
.bp-prose a { color: var(--color-primary); text-decoration: none; }
.bp-prose a:hover { text-decoration: underline; }
.bp-prose strong { font-weight: 600; color: var(--color-text); }
.bp-prose ul, .bp-prose ol { margin: 0 0 1rem; padding-left: 1.4rem; }
.bp-prose li { margin: .3rem 0; }
.bp-prose li::marker { color: var(--color-text-muted); }
.bp-prose code { font-family: var(--font-mono); font-size: .85em; background: var(--neutral-100); color: var(--color-text); padding: .12em .4em; border-radius: 5px; border: 1px solid var(--neutral-200); }
.bp-prose pre { background: var(--neutral-950, #121212); border: 1px solid var(--neutral-200); border-radius: var(--radius-lg); padding: 1rem 1.15rem; overflow-x: auto; margin: 0 0 1.25rem; }
.bp-prose pre code { background: none; border: none; padding: 0; font-size: .82rem; color: #e6e6e6; line-height: 1.6; }
.bp-prose blockquote { margin: 0 0 1rem; padding: .1rem 0 .1rem 1rem; border-left: 3px solid var(--color-primary); color: var(--color-text-muted); }
.bp-prose hr { border: none; border-top: 1px solid var(--neutral-200); margin: 2rem 0; }
.bp-prose img { max-width: 100%; border-radius: var(--radius-md); }
.bp-prose table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); margin: 0 0 1.25rem; }
.bp-prose th { text-align: left; font-weight: 600; padding: .5rem .75rem; background: var(--neutral-100); border: 1px solid var(--neutral-200); }
.bp-prose td { padding: .5rem .75rem; border: 1px solid var(--neutral-200); }
.bp-prose kbd { font-family: var(--font-mono); font-size: .8em; background: var(--neutral-100); border: 1px solid var(--neutral-200); border-bottom-width: 2px; border-radius: 4px; padding: .1em .4em; }

/* A select whose options failed to load. Without a visible state the field just
   said "Loading..." forever and the only trace was a console line. */
select.is-error { border-color:var(--color-danger, #e5484d); color:var(--color-danger, #e5484d); }

/* The save button sat at the bottom of the form, so a 40-field record hid it
   below a screen of scrolling and every edit ended with a hunt for it. */
.bp-form-actions { position:sticky; bottom:0; z-index:5; padding:var(--space-md) 0;
  background:linear-gradient(to top, var(--color-bg) 70%, transparent);
  backdrop-filter:blur(4px); }

/* ---- Admin list: toolbar + table ------------------------------------------
   The toolbar was a tall card whose search input and per-page select stacked,
   because the form had no flex. Everything below is spacing and alignment; no
   colours are introduced that are not already brand tokens. */
.bp-toolbar { margin-bottom: var(--space-lg); }
.bp-toolbar .card__body { padding: var(--space-sm) var(--space-lg); }
.bp-toolbar__row { display:flex; align-items:center; justify-content:space-between;
  gap: var(--space-lg); flex-wrap:wrap; min-height:48px; }
.bp-toolbar__meta { display:flex; align-items:baseline; gap:.4rem; }
.bp-toolbar__controls { display:flex; align-items:center; gap:var(--space-sm); flex-wrap:wrap; }
.bp-count { font-size:1.05rem; font-weight:700; color:var(--color-text); font-variant-numeric:tabular-nums; }
.bp-toolbar__controls > * { flex:0 0 auto; }
.bp-toolbar__controls .input { width:auto; }
.bp-search { width:clamp(200px, 24vw, 320px); }
.bp-perpage { width:auto; }
.btn.is-active { background:var(--neutral-100); color:var(--color-text); }

/* Table: give rows room, keep the header visible while scrolling a long list,
   and make the hovered row obvious since the whole row is clickable. */
.bp-table thead th { position:sticky; top:0; z-index:2; background:var(--neutral-900);
  backdrop-filter:blur(6px); }
.bp-table th, .bp-table td { padding:calc(var(--space-sm) + 2px) var(--space-md); }
.bp-table tbody tr { transition:background .12s ease; cursor:pointer; }
.bp-table tbody tr:hover { background:var(--neutral-100); }
.bp-table tbody tr + tr { border-top:1px solid var(--neutral-200); }
