{
  "groups": [
    { "id": "foundation", "name": "Foundation", "items": ["token-architecture", "typography", "colors", "icons", "size", "radius", "shadow", "opacity", "animation", "layout"] },
    {
      "id": "charts",
      "name": "Charts",
      "items": ["charts", "bar-chart", "bar-list", "dashboard", "donut", "line-chart", "sparkline", "stat"]
    },
    {
      "id": "components",
      "name": "Components",
      "items": [
        "accordion", "avatar", "badge", "banner", "breadcrumb", "button",
        "calendar", "card", "chat", "checkbox", "chip", "code-block", "color-picker", "command-palette", "counter", "datepicker", "divider",
        "dropdown", "empty-state", "file-uploader", "flag", "footer", "forms", "header", "input", "kanban", "link", "list", "media-player", "menu", "modal", "navigation",
        "pagination", "popover", "progress", "radio", "rating", "resizable", "scroll-area", "segmented", "select", "sheet",
        "skeleton", "slider", "spinner", "stepper", "switch", "table", "tabs",
        "time-picker", "timeline", "toast", "tooltip", "tree"
      ]
    },
    {
      "id": "templates",
      "name": "Templates",
      "items": [
        "ai-chat", "app-shell", "auth", "data-table", "dock", "inspector", "library", "playbar"
      ]
    }
  ],
  "items": {
  "token-architecture": {
   "name": "Token Architecture",
   "description": "The reasoning behind the tokens: three tiers (DTCG source \u2192 generated CSS), the naming grammar, role prefixes (`bg` fills, `fg` ink \u2014 text AND icons, `border` boundaries), the property\u2260role principle, sanctioned cross-role exceptions, the value-display roles, the minting checklist, and the Figma round-trip via the letbe plugin.",
      "usage": "Nothing to mount — this page is the contract the other token pages obey. Bind **L2 semantic** roles by default (`var(--lb-bg-accent)`, `var(--lb-fg-muted)`, `var(--lb-border-focus)`); reach for **L3** only in the three concept families `--lb-action-*` / `--lb-field-*` / `--lb-surface-*`, and bind **L1** directly only on the sanctioned flat scale (`--lb-size-*` for spacing/sizing). Name grammar is `--lb-` + optional L3 concept + role axis + variant + state, where the axis says what the pixel *is*: `bg-*` surfaces and fills, `fg-*` ink — text AND icons, since every icon draws with `currentColor` — `border-*` boundaries; state suffixes (`-hover`, `-pressed`, `-disabled`) exist ONLY at L3, and the modifier vocabulary is closed (`subtle` → `muted` → default → `strong` → `bolder`). Three cross-role bindings are sanctioned and nothing else: a filled control's border taking its own fill (`action.border-primary-* → {bg.accent*}`), a cutout ring taking the surface it cuts into (`surface.border-cutout → {bg.default}`), and the role-free categorical palette `data.1..8`. Never edit `tokens/theme.css` — it is generated; edit `tokens/source-tokens.json` (DTCG `$value`/`$type`, aliases written `{path.to.token}`) and run `node scripts/build-tokens.js`, then add the new pairings to `scripts/audit-contrast.js` (rewrites `tokens/CONTRAST.md`, exits non-zero on a failing pair) and hand the names to the letbe Figma plugin, which builds its variables from the same source file."
  },
    "typography": {
      "name": "Typography",
      "description": "Six semantic text roles — display, heading, body, action, label, caption — each with multiple sizes. Use display for hero moments, heading for section titles, body for paragraph content, action for interactive text (buttons, tabs), label for form field labels, caption for helper/metadata text. All styles come from `source-tokens.json` and scale with the theme editor's base size.",
      "usage": "Pure CSS — nothing to init, and there are no `.lb-text-*` / `.lb-type-*` utility classes: consumers bind the five per-property L2 composite vars directly, e.g. `font: var(--lb-t-body-m-font-weight) var(--lb-t-body-m-font-size)/var(--lb-t-body-m-line-height) var(--lb-t-body-m-font-family); letter-spacing: var(--lb-t-body-m-letter-spacing)`. Token shape is `--lb-t-{role}-{size}-{font-family|font-size|font-weight|line-height|letter-spacing}`; the six documented roles are display `xl/l/m/s/xs`, heading `xl/l/m/s/xs/2xs`, body `xl/l/m/s/xs`, action `l/m/s`, label `l/m/s` and caption `m/s`, and `tokens/theme.css` also ships `code` `s/m/l` plus one size-less composite, `--lb-t-overline-*` (the uppercase micro-label voice, bound directly by several components in `components.css`). Family resolves in three hops — composite → role alias `--lb-typography-family-{display|heading|body|action|label|caption|code}` → numbered L1 slot `--lb-font-family-1|2|3`; action points at slot 2 and code at slot 3, every other role at slot 1, so re-pointing a slot re-faces every role bound to it without touching a composite. Responsive: only display (all five) and heading `xl/l/m` shrink, emitted as `@media (max-width: 600px) { html:not([data-typo-preview]) { … } }` — M and L inherit the base values; the theme editor's Viewport switcher forces a mode by setting `data-typo-preview=\"S\"` or `\"M\"` on `<html>` (L removes the attribute, since L is the base state), which is why the S rule is guarded by `:not()`."
    },
    "colors": {
      "name": "Colors",
      "description": "Six L1 color palettes — neutral, violet (brand), blue (info), green (success), yellow (warning), red (danger) — each with 11 steps from 50 (lightest) to 950 (darkest). Primitives are immutable; theming happens by remapping L2 semantic tokens to different L1 palettes. Pick brand color in the theme editor to generate a new `brand.*` palette.",
      "usage": "Nothing to init. Never bind an L1 palette step in component CSS (`--lb-violet-600` is immutable and identical in both themes) — bind the L2 roles `--lb-fg-*` (ink, and icon color via `currentColor`), `--lb-bg-*` (surfaces and fills), `--lb-border-*` (boundaries, dividers, focus rings), the role-free chart palette `--lb-data-1..8`, the syntax palette `--lb-code-1..8`, or the L3 concepts `--lb-action-*` / `--lb-field-*` / `--lb-surface-*`; only L2 and L3 flip between themes. L1 is eight 11-step ramps — `--lb-{neutral|violet|blue|green|yellow|red|orange|cyan}-{50…950}` — with orange and cyan existing to complete the 8-hue rotation the `data` and `code` groups need. Dark mode is `[data-theme=\"dark\"]` on **any** element (not just `<html>`), mirrored by `@media (prefers-color-scheme: dark)`; the L3 aliases are re-declared verbatim inside both dark blocks so section-scoped theming re-substitutes correctly. The theme editor's brand picker generates a full `brand.*` L1 palette from one hex and remaps the L2 accent tokens — including a separately measured `bg.accent-value` chosen against its neutral track — to reference it, leaving L1 violet intact; every shipped pairing is measured in `tokens/CONTRAST.md`."
    },
    "icons": {
      "name": "Icons",
      "description": "254 icons bundled as standalone SVGs in three categories. OUTLINED (Lucide, stroke-based, default) — navigation, actions, status, people, docs, AI-era set (sparkles, bot, brain, audio-lines, mic, circle-stop, loader-circle, paperclip…), dev/data (terminal, braces, git-branch, database…), media/music (shuffle, repeat, disc-3, podcast, mic-vocal, guitar…). FILLED — 13 solid status/rating glyphs. BRAND (Simple Icons + Bootstrap Icons, filled single-path logos, `fill: currentColor`) — the letbe house mark, social (tiktok, x-logo, instagram, facebook, linkedin, whatsapp, telegram, discord, reddit, threads, bluesky, mastodon, snapchat, pinterest, twitch, youtube), music/creator (spotify, apple-music, soundcloud, bandcamp, deezer, tidal, youtube-music, patreon, substack, medium), dev/design (github, gitlab, figma, slack, dribbble, behance, google, apple). Lucide 1.0 removed all brand icons, hence the dual sourcing; brand logos are trademarks — usage subject to each brand's guidelines (see assets/icons/LICENSE.txt). Use `LB.icon('name', element)` to inject by name; stroke width follows `--lb-stroke-icon` (outlined only — brand/filled color via fill). Requires serving over http (not `file://`).",
      "usage": "`<span data-lb-icon=\"search\" aria-hidden=\"true\"></span>` — `LB.init()` runs `initIcons(root)` over every `[data-lb-icon]`, fetches `assets/icons/<name>.svg` and injects it; the sweep is idempotent (marks `el._lbIconDone`) and re-runs on every `LB.init()`, so dynamically added markup hydrates itself. Base path is auto-derived from lb.js's own `<script src>` (one level up from `js/`) and overridable with `LB.setIconBasePath(path)`; the JS API is `LB.icon(name, targetEl?)` (returns the cached SVG string, fires an async load and injects on arrival when cold), `await LB.iconLoad(name)`, `LB.iconPreload([names])` — a failed fetch caches `''` and never overwrites author-provided fallback content. Both circle orderings resolve through an alias map (`check-circle` ↔ `circle-check`, `circle-help` → `help-circle`, `circle-x` → `x-circle`, …), so either guess hits the shipped file. The slot is `1em × 1em` inline-flex at `color: currentColor` (reserved before load, so no layout shift); outlined stroke width comes from the global `svg[stroke-width] { stroke-width: var(--lb-stroke-icon) }`, while brand/filled glyphs are single-path `fill: currentColor`. `LB.icon()` injects no `role` and no accessible name — put `aria-hidden=\"true\"` on decorative slots and `aria-label` on icon-only controls — and icons are fetched at runtime, so the page must be served over http(s), not `file://`."
    },
    "size": {
      "name": "Size",
      "description": "L1 size scale — used across spacing, padding, gap, heights, widths, and (via reference) border-widths. Built on a 4px base grid with a 2px sub-grid for hairline details. Includes the theme-editable `--lb-size-theme` (1.6px default, backs `--lb-border-width-action`, runtime-mutable via the theme editor's Action stroke knob).",
      "usage": "`padding: var(--lb-size-4x)` — L1 binds directly for spacing, padding, gap, heights and widths; this is the one sanctioned flat scale, there is no L2 spacing layer to route through. The scale is `--lb-size-` `0` (0px), `hairline` (1px), `theme` (1.6px), `0-5x` (2), `1x` (4), `1-5x` (6), `2x` (8), `3x` (12), `3-5x` (14), `4x` (16), `5x` (20), `6x` (24), `8x` (32), `10x` (40), `12x` (48), `16x` (64), `20x` (80), `24x` (96) — a 4px base grid with a 2px sub-grid, `hairline` and `theme` being the only two values off both. House convention: `2x` intra-element, `4x` element-to-element, `6x` between sections. Border and stroke widths never take a raw size — go through the L2 aliases `--lb-border-width-none` (→ `size-0`), `--lb-border-width-thin` (→ `size-hairline`), `--lb-border-width-action` (→ `size-theme`), `--lb-border-width-medium` (→ `size-0-5x`), and `--lb-stroke-icon` (→ `size-theme`). `size-theme` is the theme-editable one: the theme editor's Action stroke knob rewrites L1 `size.theme` to a literal px, retuning every control border and every icon stroke together."
    },
    "radius": {
      "name": "Radius",
      "description": "Corner radius primitives (L1) and semantic aliases (L2) for interactive, field, surface, overlay, badge, and full/pill shapes. Theme editor's corner-radius knob remaps the L2 aliases to different L1 values so all components follow one slider.",
      "usage": "`border-radius: var(--lb-radius-surface)` — bind the L2 aliases, not L1: `--lb-radius-interactive` (buttons, chips), `--lb-radius-field` (inputs, selects), `--lb-radius-surface` (cards, panels), `--lb-radius-overlay` (modals, menus, popovers), `--lb-radius-media`, `--lb-radius-badge`, `--lb-radius-full`, `--lb-radius-none`. L1 primitives are `--lb-radius-0|2|4|8|12|16|9999`; shipped mapping is interactive/field → 4, surface/media → 8, overlay → 12, badge/full → 9999, none → 0. The theme editor's radius control is a four-option segmented picker — Square / Soft / Rounded / Pill — that rewrites seven of the eight aliases (everything except `none`) to different L1 primitives in one move (Pill sends interactive/field to 9999 while surface stays at 8), so a component that binds an L1 radius directly opts itself out of the shape system. The direct L1 bindings that remain in `components.css` are small internal details deliberately kept off the shape system: `--lb-radius-2` on focus-ring corners (`.lb-link:focus-visible`, `.lb-chip__remove`, `.lb-banner__close`, `.lb-toast__close`) and a handful of nested swatches, `--lb-radius-4` on `.lb-menu__trigger`, and `--lb-radius-9999` on `.lb-conv__unread` and `.lb-context-budget-bar__track`."
    },
    "shadow": {
      "name": "Shadow",
      "description": "Elevation shadows. L1 shadow.1–4 define progressively deeper drop-shadows; L2 aliases (card, dropdown, modal, tooltip, focus) point at the right primitive for each surface role. The focus shadow (L1 shadow.ring) is available for inset focus indicators; no shipped component uses it — focus states are drawn with outline instead.",
      "usage": "`box-shadow: var(--lb-shadow-card)` — bind the L2 aliases: `--lb-shadow-card` → `shadow-2`, `--lb-shadow-dropdown` → `shadow-3`, `--lb-shadow-tooltip` → `shadow-3`, `--lb-shadow-modal` → `shadow-4`, `--lb-shadow-focus` → `shadow-ring`. L1 `--lb-shadow-1..4` are single drop-shadows built with `color-mix` over `--lb-neutral-900` at 8 / 10 / 12 / 16% (`0 1px 2px` → `0 8px 24px`); they are not re-declared in either dark block, so shadow values are identical in light and dark. `--lb-shadow-ring` / `--lb-shadow-focus` (`0 0 0 3px` blue-500 at 40%) ship but have zero consumers in `components.css`: focus in this system is drawn with `outline`, not shadow — `outline: var(--lb-border-width-medium) solid var(--lb-border-focus); outline-offset: var(--lb-size-0-5x)` on most components, with fields swapping the color for `--lb-field-border-focus` (or `-error` / `-success`) — and that outline pattern, not the focus shadow, is what new components should match."
    },
    "opacity": {
      "name": "Opacity",
      "description": "Opacity scale for transparent overlays and muted elements. L1 provides 0, 0.05, 0.1, 0.2, 0.4, 0.6, 0.8, 1. L2 names them (hidden, faint, subtle, light, medium, heavy, strong, opaque) for semantic use in interactive states.",
      "usage": "`opacity: var(--lb-opacity-60)` — L1 is `--lb-opacity-0|5|10|20|40|60|80|100`, and the L2 aliases map onto them 1:1 in order: `hidden` 0, `faint` 0.05, `subtle` 0.1, `light` 0.2, `medium` 0.4, `heavy` 0.6, `strong` 0.8, `opaque` 1. Consumption is currently near-zero: no L2 opacity alias is bound anywhere in `components.css`, and the single L1 binding in the whole repo is `.lb-rating--disabled { opacity: var(--lb-opacity-60) }`. Disabled and muted states are otherwise expressed with dedicated color tokens (`--lb-fg-disabled`, `--lb-bg-disabled`, `--lb-border-disabled`), and that is the pattern to follow — use this scale for scrims, veils and fade transitions rather than for dimming ink. Note that `scripts/audit-contrast.js` resolves flat token pairs only and reports non-flat values as unmeasurable, so opacity-dimmed foregrounds fall outside the measured coverage in `tokens/CONTRAST.md`."
    },
    "animation": {
      "name": "Animation",
      "description": "Motion tokens — durations (0–500ms) and easing curves. L2 aliases give semantic names like `dur-fast` (100ms, state changes), `dur-normal` (150ms, menus/tooltips), `dur-gentle` (200ms, transitions), `ease-ui` (standard UI), and `ease-bounce` (playful spring). Honors `prefers-reduced-motion`.",
      "usage": "Two L1 primitive families — `--lb-duration-0|50|100|150|200|300|500` and `--lb-ease-linear|standard|decelerate|accelerate|spring` — plus the eleven L2 semantic aliases components actually bind to: `--lb-animation-dur-instant|micro|fast|normal|gentle|slow|collapse` and `--lb-animation-ease-ui|in|out|bounce`. Consume the L2 names together in shorthand — `transition: color var(--lb-animation-dur-fast) var(--lb-animation-ease-ui)` — reserving `ease-in` for entering elements, `ease-out` for leaving, `ease-bounce` for playful/notification motion, and `ease-ui` as the default for everything else. Every L2 alias is a `var()` reference to an L1 primitive, never a raw value, and there is no L3 motion tier: L2 is the binding layer for components. Reduced motion is per-component, not global — each animating rule ships its own `@media (prefers-reduced-motion: reduce)` override (18 such blocks in `components.css`, e.g. the badge dot pulse), so new motion must add its own."
    },
    "layout": {
      "name": "Layout",
      "description": "Four single-purpose layout primitives covering ~80% of the spacing patterns we use: **Stack** (vertical, gap), **Cluster** (horizontal, gap, wraps) with --start/--end/--center/--between alignment modifiers, **Grid** (auto-fit columns with --min and --gap), and **Masonry** (Pinterest-style packed columns for variable-height items). Each accepts CSS custom properties so the same class works for tight toolbars and breathing-room hero sections. Replaces inline `style=\"display: flex; gap: …\"` everywhere — composes freely (Stack of Clusters is the form-row pattern; Grid of Cards is the dashboard pattern).",
      "usage": "`<div class=\"lb-stack\">`, `<div class=\"lb-cluster\">`, `<div class=\"lb-grid\">`, `<div class=\"lb-masonry\">`. Override gap with `style=\"--gap: var(--lb-size-2x)\"`. Grid and Masonry take `--min` for the column min-width that drives the responsive breakpoint. Cluster has `--end`, `--center`, `--between` modifiers for justification. Masonry uses CSS multi-column under the hood — items flow column-by-column (A→D→G then B→E→H), so use it for visual content (photo grids, card walls) where strict left-to-right reading order doesn't matter; for ordered lists use `.lb-grid`."
    },
    "accordion": {
      "name": "Accordion",
      "description": "Collapsible panels for progressively disclosing content. Use when you have grouped information that doesn't need to be visible at once — FAQ lists, settings sections, nested details, filter groups. Avoid for primary navigation; use tabs or nav instead. Each item supports an optional leading icon (`.lb-accordion__icon`) and a right-cluster slot (`.lb-accordion__indicators`) for badges + chevron.",
      "usage": "Set `data-lb-accordion` on the root. Each item needs a `.lb-accordion__trigger` button and matching `.lb-accordion__panel`. Add `<span class=\"lb-accordion__icon\" data-lb-icon=\"…\">` before the label for category-style accordions."
    },
    "auth": {
      "name": "Auth",
      "description": "Composed authentication TEMPLATES (screens, not primitives) — first entry of the Templates group. Six flows: **login**, **signup**, **forgot-password request**, **reset password**, **verify** (OTP code · magic-link-sent variants), **MFA code** (reuses the verify skeleton). Two layouts per screen: **centered card** (default, SaaS apps) and **`--split`** (form + swappable brand panel, marketing-led products; panel hides ≤768px). Pure-CSS scaffolding — every control is an existing primitive (`.lb-card`, `.lb-field`, `.lb-input` incl. `data-lb-password` eye-toggle, `.lb-checkbox`, `.lb-btn`, `.lb-link`, brand icons). The auth ENGINE is the consumer's (Clerk/Supabase/custom) — forms ship unwired, per the pluggable-engines rule. 2026 patterns baked in (verified research): visible labels always (never placeholder-only — placeholder-as-label is a WCAG failure); full autocomplete set (WCAG 1.3.5): `username webauthn` on login email (FIDO passkey-autofill primary pattern), `current-password`/`new-password`, `one-time-code`; OTP entry is a SINGLE input (multi-box breaks SMS autofill + screen readers); passkey button = optional secondary slot; passkey enrollment is post-signup, never the signup credential. Third deliverable of the app-shell/templates program (North Star pillar #3).",
      "usage": "`<div class=\"lb-auth\">` (add `--split` + `.lb-auth__panel` slot + `.lb-auth__main` for the split layout) wrapping a `.lb-card.lb-card--padded.lb-card--elevated.lb-auth__card`. Inside: `.lb-auth__head` (`__logo` + `__title` + `__subtitle`), optional `.lb-auth__social` (`.lb-btn--secondary` + brand icons; `--row` for icon-only; place before or after the divider for leading/trailing), `.lb-auth__divider` (\"or\"), a `.lb-form.lb-form--compact` of standard `.lb-field`s, `.lb-auth__row` (remember-me + forgot link), `.lb-auth__meta` (switch-screen link). OTP screens: one `.lb-field.lb-auth__code` input with `inputmode=\"numeric\" pattern=\"\\d{6}\" maxlength=\"6\" autocomplete=\"one-time-code\"`. Nothing to init — password toggle comes from the existing `data-lb-password` behavior. On a real page give the host `min-height: 100vh`.",
      "status": "app-shell program stage 3"
    },
    "avatar": {
      "name": "Avatar",
      "description": "User or entity representation with image fallback to initials. Three sizes: small, medium, large. Use in user lists, comments, nav user-menus, chat messages. Initials render automatically when the image fails to load. The same page covers **Avatar Group** — stacked overlapping avatars with optional '+N' counter and hover-to-fan-out for teams, assignees, attendees.",
      "usage": "`<div class=\"lb-avatar lb-avatar--medium\" role=\"img\" aria-label=\"Jane Doe\" data-lb-initials=\"JD\"><img class=\"lb-avatar__img\" src=\"…\" alt=\"\"></div>` — always pair base with a size (`--small` 2rem / `--medium` 2.5rem / `--large` 3rem); the bare base carries no dimensions. Groups are pure composition: `<div class=\"lb-avatar-group lb-avatar-group--medium\">` holding matching-size `.lb-avatar` children, an optional tail `<span class=\"lb-avatar lb-avatar--medium lb-avatar-group__count\" role=\"img\" aria-label=\"8 more people\">+8</span>`, and optional `--hover-expand` to fan the stack apart on hover. Auto-inits at `LB.init()` (self-runs on `DOMContentLoaded`): `initAvatars()` sweeps `.lb-avatar[data-lb-initials]` and replaces the element's children with the initials text when the `<img>` has already failed, fails later, or is absent entirely — the outer `role`/`aria-label` survive the swap. No custom events, no `el._lb*` handle, and no method API; `initAvatars` is not individually exported, so re-run `LB.init()` after injecting avatars (the per-element `_lbAvatarBound` guard makes it idempotent)."
    },
    "badge": {
      "name": "Badge",
      "description": "Compact status label, and the read-only counterpart to Chip — use `--default` for neutral non-status descriptors. Four variants: default (brand), success, warning, danger. Two sizes: medium (default, 16px icon slot) and small (12px icon slot). Supports an optional leading dot (`.lb-badge__dot`) or icon (`.lb-badge__icon`) — useful for money-flow and a11y-forward statuses.",
      "usage": "`<span class=\"lb-badge lb-badge--success\">Active</span>` — variants are `--default` (neutral, for non-status descriptors), `--success`, `--warning`, `--danger`. The bare base is already medium sizing, so `--medium` is an intentionally-empty back-compat alias and `--small` is the only real size modifier. Optional leading `<span class=\"lb-badge__dot\"></span>` (add `--pulse` for a 2s ambient pulse, dropped entirely under `prefers-reduced-motion: reduce`) or `<span class=\"lb-badge__icon\" aria-hidden=\"true\" data-lb-icon=\"…\"></span>` (1rem, 0.75rem under `--small`). Pure CSS — nothing to init, no events, no JS handle; the only live part is the icon slot, filled by the `LB.initIcons()` pass inside `LB.init()` (self-runs on `DOMContentLoaded` once `js/lb.js` is on the page), so text- and dot-only badges work from the stylesheets alone. For badges injected after boot call `LB.initIcons(root)` — exported and idempotent, it skips already-filled slots."
    },
    "banner": {
      "name": "Banner",
      "description": "Contextual message spanning the top of a section or page. Four status variants (info, success, warning, danger), three sizes (small, medium, large). Supports optional CTA actions and dismiss. Use for page-level announcements, system status, or contextual help.",
      "usage": "Mark with `role=\"status\"` for info/success, `role=\"alert\"` for warning/danger. Actions go in `.lb-banner__actions`; the close button is optional."
    },
    "charts": {
      "name": "Charts",
      "description": "The data visualization suite — Stat, Sparkline, Bar List, Donut, Bar Chart, and Line Chart. All hand-rolled SVG (or pure CSS), no charting libraries, no dependencies. Built around the `--lb-data-1..8` palette tokens so every chart adopts the active theme automatically. Compose with Card for surface, Grid for KPI strips, Table for tabular adjacency, Resizable for dashboard layouts.",
      "usage": "Each chart ships as its own component with its own demo page — see Stat, Sparkline, Bar List, Donut, Bar Chart, Line Chart. This page is a single-place overview with anatomy and a worked example of each. Charts share a consistent JSON-config-in-script-tag input pattern, automatic theme palette, and ARIA fallback summaries for screen readers."
    },
    "bar-chart": {
      "name": "Bar Chart",
      "description": "Categorical bar chart — vertical (default) or horizontal, single or multi-series, grouped or stacked. Hand-rolled SVG. Computes nice round-number axis ticks (port of d3-array's algorithm in ~30 lines, no dep). Hover tooltip on each bar via the shared chart tooltip pattern. Auto-renders a legend for multi-series. Color picks `--lb-data-1..8` by default.",
      "usage": "`<div data-lb-bar-chart><script type=\"application/json\">{ \"x\": [\"Jan\",\"Feb\",…], \"series\": [{ \"name\": \"Revenue\", \"data\": [120,140,…] }] }</script></div>`. Config keys: `x` (categories), `series` (array of `{ name, data }`), `orientation` (\"vertical\" | \"horizontal\"), `stacked` (bool), `colors` (override per-series colors), `legend` (bool, default true for multi-series). Programmatic: `el._lbBarChart.setData({ … })`."
    },
    "bar-list": {
      "name": "Bar List",
      "description": "Ranked horizontal bar list — top countries, top sources, top pages. Each row is a label + value with a proportional bar layer behind. Pure HTML/CSS — JS only computes the per-row width as a percentage of the largest value. Drops into Card or any container; consumer can pair labels with `<a>` for clickable rows by adding `--interactive`. The bar color picks `--lb-data-1` by default; override per-list via `style=\"--color: …\"`.",
      "usage": "`<ul data-lb-bar-list><li data-lb-bar-list-row data-lb-value=\"384\"><span class=\"lb-bar-list__label\">United States</span><span class=\"lb-bar-list__value\">384</span></li>…</ul>`. Add `lb-bar-list--interactive` to enable hover state and pointer cursor. Set the color via `style=\"--color: var(--lb-data-series-2)\"` on the list."
    },
    "calendar": {
      "name": "Calendar",
      "description": "Event & booking calendar with three views (Month / Week / List) and two intents (browse / book). Sits in the data-viz family alongside Bar Chart and Stat — different from Datepicker (input control). ISO 8601 week numbers in the left column, today-marked with an underline accent, past days dimmed for clear what-has-vs-hasn't-happened cue. Events colour via the L2 categorical palette (`data.1..8`). All values route through DS tokens; no hardcoded colours.",
      "usage": "`<div data-lb-calendar></div>` — auto-init on page load. Mutate via `el._lbCalendar.setEvents([{ id, start, end, title, category }])`, `setView('week')`, `setDate(d)`, `next()`, `prev()`, `today()`. Listens to `lb-calendar-event-click`, `lb-calendar-date-click`, `lb-calendar-view-change`. Toggle week numbers off with `data-lb-no-week-numbers`."
    },
    "breadcrumb": {
      "name": "Breadcrumb",
      "description": "Hierarchical navigation showing the user's location in the app. Use when pages are nested three or more levels deep. The current page gets `aria-current=\"page\"` and is not a link.",
      "usage": "`<nav class=\"lb-breadcrumb\" aria-label=\"Breadcrumb\"><ol class=\"lb-breadcrumb__list\">` wrapping `<li class=\"lb-breadcrumb__item\">` entries, each holding an `<a class=\"lb-breadcrumb__link\">` plus `<span class=\"lb-breadcrumb__separator\" aria-hidden=\"true\">/</span>`; the last item is a `<span class=\"lb-breadcrumb__current\" aria-current=\"page\">` and is deliberately not a link. A crumb that runs an action instead of navigating uses `<button class=\"lb-breadcrumb__link lb-breadcrumb__link--button\">` — the modifier strips native button chrome so it matches the links either side. Pure CSS — nothing to initialise, no controller in `lb.js`, no events, no JS handle. Works at any depth: add as many `__item`s as the IA needs; the list wraps and links take the shared `:focus-visible` ring from `--lb-border-focus`."
    },
    "button": {
      "name": "Button",
      "description": "Primary action trigger. Variants: primary (main action), secondary (alternative action, outlined), subtle/ghost (low emphasis), danger, accent-2/accent-3 (filled actions on the theme’s extra signal slots — dormant by default, rendering identical to primary until the Theme Editor fills the slot). Sizes: medium, small. Supports leading icon (`.lb-btn__icon`), trailing count badge (`.lb-counter.lb-counter--on-btn` — auto-hides when empty), or icon-only (`.lb-icon-btn` with `aria-label`). Includes a Button Group layout wrapper for aligning multiple buttons (start, end, center, justify, stack).",
      "usage": "Use primary for the main path (one per screen/group), secondary for alternatives, subtle for auxiliary actions. Icon-only buttons MUST have `aria-label`. For count badges, leave the inner span empty (no number) to hide the pill — pair with `disabled` on the host button to read as inactive."
    },
    "card": {
      "name": "Card",
      "description": "Surface container for grouped content. Default is bordered with 24px padding. Variants: `--elevated` (shadow), `--interactive` (hover lift + brand border + focus ring), `--stat` (dashboard metric tile with big number + label + delta). Sizes: `--sm` / default / `--lg`. Slots: `__header` (title + actions), `__body`, `__section` (divided subsection), `__footer`, `__media`, `__nav-icon` (trailing chevron / arrow for navigation cards). Media positions: top / left (horizontal) / background (full-bleed overlay, text pinned to white regardless of theme). States: `--selected`, `--disabled`.",
      "usage": "Interactive cards: render as `<button class=\"lb-card lb-card--interactive\">` or `<a class=\"lb-card lb-card--interactive\">` so the whole surface is one click target — don't nest buttons. Picker grids: add `--selected` + `aria-pressed=\"true\"`. Stat tiles: replace __body with `.lb-card__stat-label` + `.lb-card__stat-value` + optional `.lb-card__stat-delta--up/--down`. Navigation cards: append `<span class=\"lb-card__nav-icon\" data-lb-icon=\"chevron-right\">` (use `arrow-up-right` for external links). Disabled: native `disabled` on `<button>` cards, or `aria-disabled=\"true\"` + `tabindex=\"-1\"` on plain `<div>`. Lift animation honors `prefers-reduced-motion`. Media aspect ratio: top-stacked media (`lb-card__media--top`) defaults to 16:9 so it never collapses; override with a ratio preset mirroring the Figma `card/media` variants — `lb-card__media--16x9` / `--4x3` / `--1x1` / `--3x4`. Ratio is inert for `lb-card--media-left` (fixed column) and `lb-card--media-bg` (absolute full-bleed), which size from layout."
    },
    "chat": {
      "name": "Chat",
      "description": "AI Chat component set — Bubble, Thread, Composer, ToolCall, ConvList, ContextBudgetBar, AI Chat template, Artifact panel. letbe-ds ships chrome + layout + state machine; consumers bring the LLM SDK, streaming source, markdown engine, sandboxed previews, and tool implementations. Per the pluggable-engine HARD rule, nothing in lb-chat*.js wraps a model API. Modules live in js/components/lb-chat*.js so pages opt in to only what they use. Slices 1-7 ship the primitives. Slice 8 adds `LB.AI Chat template`. Slice 9 adds `LB.Artifact` (right-rail panel via Tabs + Select + lb-code-block). Slice 10 lands the accessibility + keyboard pass: Thread is `role=log aria-live=polite`, streaming bubbles carry `aria-busy=true` until done; ConvList is `role=listbox` with `role=option` rows + arrow-key roaming + Home/End; kebab menu has full menu-keyboard support (Arrow / Home / End / Enter / Esc / Tab) + focus management + `aria-haspopup` + `aria-expanded`; Composer textarea gets an auto `aria-label`; AI Chat template exposes customisable keyboard shortcuts via `data-lb-chat-shortcuts` JSON or `setShortcuts(map)` — defaults: `/` focus composer, `mod+k` new chat, `mod+r` regenerate, `mod+\\` toggle sidebar; unknown handler names emit a generic `lb-chat-workspace-shortcut {handler}` event for consumer-defined actions. Demo dashboard at /chat/workspace/. Slice 11 ships the composer popover variant + Timeline composition: `LB.openComposerPopover(opts)` builds a compact `LB.Composer` inside an `.lb-popover` surface anchored to a rect. LB.Timeline gains an `lb-timeline-selection` event on drag-end. Slice 12 closes the 12-slice arc with the two remaining workspace layouts (the code-assistant layout and the timeline/generation layout), the workspace dashboard at /chat/workspace/ updated to load all three layouts with `?layout=converse|dev|timeline` deep linking, plus the chat module pattern documented in `js/components/README.md`. A post-arc refinement pass relocated chrome based on user feedback: layout switcher moved from the header into the sidebar above the search row (high-discoverability), context-budget bar moved from the header to the foot of main (just below the composer), the sidebar settings dock was removed in favor of a workspace-wide Settings panel that opens in the rail via the new gear icon in the header (`ws.openSettings() / closeSettings() / toggleSettings()` with `addSettingsSection({id,label,icon,render})` for consumer extensions; sections ship with Appearance/Keyboard shortcuts/About). Rail is now multi-tenant — opening Settings closes any open Artifact panel and vice versa, and either tenant's close button collapses the rail entirely. ConvList group headers gain top borders for clearer section separation. Full arc spec in /letbe-presentation/ai-chat-deep-analysis.html.",
      "usage": "Load `<script src=\"../js/components/lb-chat.js\"></script>` (primitives) and optionally `<script src=\"../js/components/lb-chat-workspace.js\"></script>` (shell + ContextBudgetBar + default Conversational layout) after lb.js. Auto-init selectors: `[data-lb-bubble]`, `[data-lb-thread]`, `[data-lb-composer]`, `[data-lb-conv-list]`, `[data-lb-context-budget-bar]`, `[data-lb-chat-workspace]`. AI Chat template API at `el._lbAI Chat template`: `setTitle(text)`, `setBudget({used,total,label?})`, `registerMode(id,{label,icon,hint})`, `setMode(id)/getMode()`, `setLayout(id)/getLayout()/getLayouts()/setLayouts(ids)`, `setDensity / setWidth / setLineHeight`, `toggleSidebar(force?)`, `openMobileDrawer()/closeMobileDrawer()`, `getSlot(name)`. Authoring layouts: `LB.AI Chat template.registerLayout(id, {label, build(ctx)})`. Workspace events: `lb-chat-workspace-layout-change`, `lb-chat-workspace-mode-change`, `lb-chat-workspace-settings-change`, `lb-chat-workspace-sidebar-toggle`. Primitive APIs (Bubble/Thread/Composer/ConvList) unchanged from earlier slices."
    },
    "checkbox": {
      "name": "Checkbox",
      "description": "Binary or ternary selection control. Supports checked, unchecked, indeterminate, and disabled states. Use for multi-select lists, settings toggles (when a clear on/off isn't implied), and form agreements. For a single boolean with immediate effect, prefer Switch.",
      "usage": "Author only the bare control — `<label class=\"lb-checkbox-wrap\"><input type=\"checkbox\" class=\"lb-checkbox\"><span class=\"lb-checkbox__label\">…</span></label>`; for the disabled state put native `disabled` on the input and `lb-checkbox-wrap--disabled` on the wrap. Auto-inits at `LB.init()` (self-runs on `DOMContentLoaded`): `initCheckboxGlyphs` replaces the input with a `.lb-checkbox-frame` wrapper and appends two `aria-hidden` sibling spans, `.lb-checkbox__glyph--check[data-lb-icon=\"check\"]` and `--minus[data-lb-icon=\"minus\"]`, which CSS sibling combinators reveal on `:checked` / `:indeterminate` — never hand-write the frame or the glyphs. Wiring stays native: listen for `change`, read `input.checked`, and set `input.indeterminate = true` from JS for the ternary state (a DOM property, not an attribute). No custom events and no instance handle; for checkboxes injected after boot run the exported `LB.initCheckboxGlyphs(root)` then `LB.initIcons(root)` on the new subtree — the injected glyphs are empty `data-lb-icon` slots until the icon pass fills them, and both calls are idempotent."
    },
    "chip": {
      "name": "Chip",
      "description": "Compact, toggle-able tag. Unselected state uses the secondary action style; selected uses primary. Click to toggle selection; X icon appears when selected to deselect. Use for filter lists, tag selection, and multi-choice interfaces. Not for primary navigation.",
      "usage": "`<span class=\"lb-chip\">Design</span>` — add `--selected` to pre-select, `--disabled` for an inert chip. Auto-inits at `LB.init()` (self-runs on `DOMContentLoaded`) with no data attribute and no constructor: `initChips()` sweeps every `.lb-chip`, gives non-`<button>` chips `role=\"button\"` + `tabindex=\"0\"` + an Enter/Space keydown, keeps `aria-pressed` in sync, and injects/removes the leading `.lb-chip__check` itself as selection changes (any legacy `.lb-chip__remove` is stripped — `×` is the Multi-Select removal idiom, not Chip's), so never author the check yourself. Every toggle dispatches a bubbling `lb-chip-toggle` CustomEvent with `detail.selected` (boolean) — listen once on a container; there is no instance handle and no method API, so read state from the `lb-chip--selected` class. Chips inside `[data-lb-multi-select]` or `.lb-filter-bar` and `--disabled` chips are deliberately skipped, and `initChips` is not individually exported — re-run `LB.init()` after injecting chips (the per-element `_lbChipInit` guard makes it idempotent)."
    },
    "code-block": {
      "name": "Code Block",
      "description": "Styled code surface for snippets, configs, theme exports. Optional header strip with language label, filename, line numbers, opt-in regex highlighter for js/ts/json/css/html/bash. Lightweight (~150 LOC of regex, no highlighting-library dependency). **Action row** (Slice 6) holds Copy / Save / Open / Apply: Copy is dual-format (writes both text/plain AND text/html with syntax-highlight preservation — paste into a rich-text editor and get formatted code, paste into a terminal and get plain text). Save downloads as a file with smart extension. Open and Apply are opt-in — emit `lb-code-open` / `lb-code-apply` events for consumers building dev-IDE surfaces. All actions are always-visible (no hover-gating, per forum feedback). Inline counterpart `.lb-code` for prose-level snippets.",
      "usage": "`<pre data-lb-code-block><code>npm install letbe-ds</code></pre>`. Add `data-lb-lang=\"js|ts|json|css|html|bash\"` for highlighting, `data-lb-title=\"file.ts\"` for a filename header, `data-lb-numbers` for line numbers. Opt out: `data-lb-no-copy`, `data-lb-no-save`. Opt in: `data-lb-open` for the Open-in-editor button, `data-lb-apply` for the Apply (primary) button. Events: `lb-code-copy {source, format}`, `lb-code-save {source, filename, lang}`, `lb-code-open {source, lang, title}`, `lb-code-apply {source, lang, title}`. Programmatic: `el._lbCodeBlock.setSource('new code', 'js')`, `.copy()`, `.save()`, `.openInEditor()`, `.apply()`. Inline: `<code class=\"lb-code\">npm install</code>`."
    },
    "color-picker": {
      "name": "Color Picker",
      "description": "Saturation × value surface plus hue and (optional) alpha sliders. Below the picker: a HEX / RGB / HSL format toggle that swaps the bottom input row between a single hex field and three numeric component fields. Hand-rolled, no dependencies. Reuses the existing Popover and Input components for the trigger-swatch dropdown and the text fields. Optional preset-swatch row, optional native EyeDropper button (Chrome / Edge — progressive enhancement, hidden when unsupported). Two layouts: inline picker, or popover-triggered swatch button. Full keyboard support — arrows on each surface, Enter on the hex field, native step on numeric fields. Emits `lb-color-change` with `hex`, `rgb`, `rgba`, `hsl`, alpha — pick the format your code wants.",
      "usage": "Inline: `<div data-lb-color-picker data-lb-value=\"#7c3aed\"></div>`. Popover trigger: `<div data-lb-color-picker data-lb-popover data-lb-value=\"#7c3aed\"></div>`. Add `data-lb-alpha` for the alpha slider (and an A column in RGB / HSL modes), `data-lb-presets=\"#000,#fff,#7c3aed,…\"` (CSV) for a preset-swatch row. Listen for `lb-color-change` with `{hex, rgb, rgba, hsl}`. Programmatic: `el._lbColorPicker.getValue()`, `setValue(\"#hex\" | \"rgba(…)\")`."
    },
    "command-palette": {
      "name": "Command Palette",
      "description": "Global ⌘K / Ctrl+K launcher. Jump to any page, action, or setting without leaving the keyboard. Composes the List primitive in filterable mode for the command list, wrapped in a top-anchored modal shell with grouped commands, icons, and kbd-shortcut hints.",
      "usage": "`<div class=\"lb-cmdk-backdrop\" data-lb-cmdk hidden>` containing `.lb-cmdk` shell → `.lb-cmdk__search-wrap` (with `.lb-cmdk__search.lb-list__search` input) → `.lb-cmdk__results` → `<ul class=\"lb-list lb-list--filterable\" data-lb-list>` with `.lb-list__item` commands and optional `.lb-cmdk__group-label` headers. Override hotkey with `data-lb-hotkey=\"/\"`. Dispatches `lb-cmdk-select` on activation."
    },
    "counter": {
      "name": "Counter",
      "description": "Numeric notification pill — the small bubble used by Tabs, Accordion, Calendar cells, and Button. Single component shared by all four hosts so the shape, sizing, and contrast guarantees stay consistent. Default is brand-themed (theme-stable saturated bg + theme-stable white digit) so contrast holds in light and dark themes and follows the user's brand colour. Variants: `--subtle` (dimmed for low-priority states), `--on-btn` (inherits the host button's variant colour pair). Auto-hides when empty.",
      "usage": "`<span class=\"lb-counter\">5</span>` standalone, or `<span class=\"lb-counter lb-counter--on-btn\">5</span>` inside a button. Add `--small` for the 16px size. Blank `textContent` to hide without removing the node."
    },
    "datepicker": {
      "name": "Date Picker",
      "description": "Calendar date selection. Supports single-date and range modes; available inline or in a popover. Month/year navigation via custom selects. Use when the user needs to pick a specific day (booking, scheduling, filtering). For less precise time inputs prefer a standard input with format hint.",
      "usage": "`<div class=\"lb-datepicker--inline\" data-lb-datepicker data-lb-variant=\"inline\" data-lb-mode=\"single\"></div>` — mount it EMPTY; the controller renders every inner node itself. Input variant is `<div class=\"lb-datepicker-field\" data-lb-datepicker data-lb-variant=\"input\" data-lb-mode=\"range\" data-lb-label=\"…\" data-lb-placeholder=\"…\" data-lb-size=\"small|medium|large\">`. Modes: `single`, `range`, `week`, `month`, `time` (time mode is documented on the Time Picker page); further attributes `data-lb-hint`, `data-lb-error`, `data-lb-disabled`, `data-lb-week-start` (`0`=Sun, default `1`=Mon/ISO), `data-lb-time-format`, `data-lb-time-step`. Auto-inits via `data-lb-datepicker` (instance at `el._lbDatePicker`); `minDate`, `maxDate`, `formatDate`, `onChange`, `onRangeChange` have no attribute form and need `new LB.DatePicker(el, options)`. Every mode dispatches the one event `lb-datepicker-change` on the host element (non-bubbling — listen on the element itself) with `detail.mode` + `detail.value`, plus `detail.rangeStart`/`rangeEnd` in range mode and `detail.weekStart`/`weekEnd` in week mode; methods are `setValue(date)`, `setRange(start, end)`, `setTime({ hours, minutes })`, `destroy()`."
    },
    "divider": {
      "name": "Divider",
      "description": "Horizontal or vertical rule separating content. Use between logically distinct sections or between items in a list. Prefer whitespace over dividers when the layout makes groupings clear on its own.",
      "usage": "Always pair base and orientation: `<hr class=\"lb-divider lb-divider--horizontal\">`, or inline `<span class=\"lb-divider lb-divider--vertical\"></span>`. The base class alone only paints `background: var(--lb-border-muted)` and carries no dimensions, so it renders collapsed; `--strong` swaps the fill to `--lb-border-default` when the rule needs more contrast. The horizontal rule ships `margin: 0` — vertical rhythm comes from the parent layout, never the divider; the vertical variant is `1em` tall and `vertical-align: middle`, so it sizes off the surrounding text and sits between inline content with no extra wiring. Pure CSS — nothing to init, no events, no JS handle; the `<hr>` keeps its implicit `separator` role for free and the vertical `<span>` is decorative (contributes nothing to the accessibility tree)."
    },
    "dashboard": {
      "name": "Dashboard",
      "description": "A worked composition putting the whole Charts suite in context. Resizable shell with Tree navigation rail on the left, a stacked main column with a KPI strip (Stat × Sparkline × Card × Grid), a Line Chart panel, a Bar Chart + Donut row, and a Table + Bar List adjacency, plus a right rail with compact Stats and a uptime Donut. No application code — every part is letbe-ds primitives composing. PLUS the Store overview commerce skeleton: commerce and analytics dashboards read best as KPI strip \u2192 big time-series \u2192 breakdowns \u2192 table. Filter row (Popover dropdowns + --pulse live badge), 4 KPI Stat cards with sparklines, revenue Line Chart with the DASHED comparison series (previous period as context), channel Donut \u00b7 top-products Bar List \u00b7 goal ring (Donut center slot), horizontal + vertical axis Bar Chart cards, recent-customers Table.",
      "usage": "Reference page only. Read the markup as a recipe for assembling your own dashboards. The same layout pattern (Resizable shell + KPI strip + chart panel + tabular adjacency) carries most admin overviews."
    },
    "donut": {
      "name": "Donut",
      "description": "Proportions chart. Hand-rolled SVG using same-radius circles with `stroke-dasharray` arcs and per-segment rotation. Configurable thickness (set thickness to 50 for a pie). Optional center slot for value + label. Pairs with the Donut Legend list for a side-by-side breakdown. Color picks from `--lb-data-1..8` by default; override via the `colors` array in the JSON config.",
      "usage": "`<div data-lb-donut><script type=\"application/json\">{\"data\":[{\"label\":\"Mobile\",\"value\":60},{\"label\":\"Desktop\",\"value\":32}]}</script></div>`. Optional center slot: add a `<div class=\"lb-donut__center\">` child with `__value` + `__label` spans. Config keys: `data`, `colors` (CSS color array), `thickness` (0–50, default 14), `gap` (default 1). Programmatic: `el._lbDonut.setData([…])`."
    },
    "dock": {
      "name": "Composer Dock",
      "description": "Workspace input module (S1 of the workspace-modules program) — mounts the existing `.lb-composer` (chips/attach/mic/send/model slots, all `LB.Composer` behavior) at the bottom of a canvas. **Docked by default** (sticky at the container bottom — the verified convention; floating bars overlapping the last message are the documented top mobile UX bug), `--floating` opt-in for media/canvas contexts (overlays; host needs `position: relative`). Content caps at `--lb-dock-max` (48rem — a readable measure for a chat stream). Optional `__suggestions` chip row (real buttons, before the input in DOM order) and `__hint` line. **Parameter tray** = consumed `LB.Popover` (`--top` + `--tray` sizing modifier) on any footer toggle — aria-haspopup/expanded/controls, Esc, outside-click for free; tray rows: `__tray-row` + `__tray-label` + `.lb-segmented`. Zero dock JavaScript. a11y: dock first in its region's tab order (composer → message actions → header actions); safe-area padding baked in; on-screen keyboards shrink the visual viewport and the sticky dock rides along natively (documented, not fought).",
      "usage": "`<div class=\"lb-dock\">` (add `--floating` for canvas overlay) → `.lb-dock__inner` holding optional `.lb-dock__suggestions` (`.lb-chip` buttons), the canonical `.lb-composer[data-lb-composer]` block, and optional `p.lb-dock__hint`. Tray: wrap a footer trigger in `.lb-popover-host[data-lb-popover]` with `.lb-popover.lb-popover--top.lb-popover--tray`. Load `js/components/lb-chat.js` for composer behavior. Docked mode: place as LAST CHILD of the scroll container. Width override: `--lb-dock-max`.",
      "status": "workspace-modules program S1"
    },
    "dropdown": {
      "name": "Dropdown",
      "description": "Combobox input — type to filter, pick from a list. Use when the option set is large (>10 items) and the user knows roughly what they want. For small option sets use Select instead. For free-form text, use a regular Input.",
      "usage": "`<div class=\"lb-dropdown-field\" data-lb-dropdown data-lb-options='[{\"value\":\"apple\",\"label\":\"Apple\",\"disabled\":true}]'>` wrapping `<label class=\"lb-dropdown-field__label\">`, `<div class=\"lb-dropdown-wrap\">` containing `<input class=\"lb-dropdown lb-dropdown--medium\" autocomplete=\"off\">` (or `--small`) + `<span class=\"lb-dropdown-wrap__chevron\" data-lb-icon=\"chevron-down\">`, and an optional `.lb-dropdown-field__hint`. Auto-inits via `data-lb-dropdown`; instance at `el._lbDropdown`. Dispatches `lb-dropdown-change` on the field with `detail` = the committed option object (`{ value: \"\", label: \"\" }` when the clear button fires). Methods: `setOptions(array)`, `setValue(v)` — use it instead of `input.value` so clear-button visibility stays in sync — and `destroy()`. Power options are constructor-only: `new LB.Dropdown(el, { onChange, filterFn, onRender, chunkSize, clearable })` for custom matching, row decoration and lazy chunked rendering of long lists; `clearable` is also settable as `data-lb-clearable=\"true\"`. The popup is built as `<ul class=\"lb-dropdown-list lb-list\" role=\"listbox\">` with `.lb-list__item` rows."
    },
    "empty-state": {
      "name": "Empty State",
      "description": "Reusable 'no data yet' placeholder for list views, search results, filtered tables, dashboards, inbox-zero screens, 404s, and error states. Slots: icon (48px default), title, description, action buttons. Three sizes: `--sm` (inline), default, `--lg` (full-page). Optional `--danger` modifier for error states (failed to load, permission denied) — tints the icon red while keeping the rest of the state neutral. Nine demo patterns ship: inbox-zero, no-results, 404, no-icon, title-only, in-card, onboarding (\"create your first\"), filtered-empty, error-state.",
      "usage": "`<div class=\"lb-empty-state\">` wraps `__icon`, `__title` (<h3>), `__description` (<p>), `__actions` (button row). Any slot is optional. Add `--danger` for error variants."
    },
    "file-uploader": {
      "name": "File Uploader",
      "description": "Drag-drop zone + click-to-browse native input + uploaded-files list with progress bars and remove controls. Constraints: `data-lb-accept` (MIME/extension list), `data-lb-max-size` (bytes), `data-lb-max-files`. Single-file mode by default unless `data-lb-multiple` or the inner input has `multiple`. Compact variant for settings rows.",
      "usage": "`<div class=\"lb-uploader\" data-lb-uploader>` wrapping `.lb-uploader__dropzone` (with hidden `.lb-uploader__input`) and `<ul class=\"lb-uploader__files\">`. Consumer drives uploads — call `el._lbUploader.setProgress(id, pct)`, `markDone(id)`, `markError(id, msg)` from your XHR/fetch progress handlers. Events: `lb-file-added`, `lb-file-removed`, `lb-file-error`."
    },
    "flag": {
      "name": "Flag",
      "description": "Country flag with optional code or name label. Uses the same vendored flag set as the Phone input (173 ISO-coded SVGs at `/assets/flags/`). Three sizes (small / medium / large). Default circular shape matches Phone; `--rect` flips to a rectangular thumbnail for table rows where a non-circular icon reads more naturally. Pairs with Bar List, Bar Chart, Table, and Stat for compact country representation in dashboards and analytics views.",
      "usage": "`<span class=\"lb-flag\"><img class=\"lb-flag__img\" src=\"/assets/flags/us.svg\" alt=\"\"><span class=\"lb-flag__code\">US</span></span>`. Use `.lb-flag__label` instead of `.lb-flag__code` for a full country name. Modifiers: `.lb-flag--small`, `.lb-flag--large`, `.lb-flag--rect` (rectangular flag for tables). Always set `alt=\"\"` on the img — the code or label span carries the accessible name."
    },
    "footer": {
      "name": "Footer",
      "description": "Site/app footer — pure CSS, no JS. Two flavors from one component: **marketing** (default: brand zone with logo + tagline + social row, 3–5 link columns, optional newsletter, legal row) and **app** (`--app`, one thin row: copyright + links + optional status badge). Optional `__giant` decorative oversized brand wordmark (aria-hidden). Consumes `.lb-btn`, `.lb-input-wrap` (newsletter), `.lb-badge` (status), `.lb-icon-btn` (social links), and the brand icon set. Newsletter placement is a variant: inline beside the columns, or `--row` as its own full-width band. Mobile collapse is plain stacking — no accordion (a11y cost, no verified benefit). a11y (verified, W3C/MDN): must be the page's single body-scoped `<footer>` rendered OUTSIDE `<main>` (nested it silently loses the contentinfo landmark); each link column is a `<nav aria-labelledby>` pointing at its real heading; icon-only social links carry `aria-label`. Second deliverable of the app-shell/templates program (North Star pillar #3).",
      "usage": "`<footer class=\"lb-footer\">` (add `--app` for the thin flavor) with `.lb-footer__inner` holding: `.lb-footer__top` (`.lb-footer__brandcol` = `.lb-footer__brand` + `.lb-footer__tagline` + `.lb-footer__social` icon-btn row; `.lb-footer__cols` = 3–5 `nav.lb-footer__col` each with `.lb-footer__col-title` heading + `.lb-footer__list` of `.lb-footer__link`; optional `.lb-footer__newsletter` inline), optional `.lb-footer__newsletter.lb-footer__newsletter--row` full-width band, optional `p.lb-footer__giant[aria-hidden]`, and `.lb-footer__legal` (copyright + `.lb-footer__legal-links` + optional `.lb-badge` status). Newsletter form ships unwired — consumer brings the submit engine. IMPORTANT: place as a sibling of `<main>`, never inside it. Nothing to init — icons injected by the global `LB.init()` sweep.",
      "status": "app-shell program stage 2"
    },
    "forms": {
      "name": "Forms",
      "description": "Layout primitives for building forms: `.lb-field` (single field wrapper, vertical default + `--horizontal` for label-left), `.lb-field-group` (semantic `<fieldset>` with legend for related fields), `.lb-form` (parent shell with consistent vertical rhythm), `.lb-form__actions` (submit/cancel row), `.lb-field-row` (two fields side-by-side). Works with every form primitive in the DS — Input, Select, Textarea, Checkbox, Radio, Switch, Date Picker, Phone, Dropdown, MultiSelect.",
      "usage": "Wrap each input in `<div class=\"lb-field\">` with `.lb-label`, optional `.lb-field__hint` / `.lb-field__error` / `.lb-field__success`. Group related fields in `<fieldset class=\"lb-field-group\">`. Parent with `<form class=\"lb-form\">`; put buttons in `<div class=\"lb-form__actions\">`."
    },
    "header": {
      "name": "Header",
      "description": "Site/app navigation header (`LB.Header`) — one component covering three base compositions: **contained bar** (default, app-shell), **mega-menu** (categorized full-width panels, marketing), and **floating pill** (`--floating`, centered rounded island, landing pages). Logo / nav items / CTA are slots (Figma will model them as INSTANCE_SWAP). Reuses `.lb-btn` for CTAs. Responsive: nav collapses to a modal drawer at ≤768px. Scroll behaviors share one rAF-throttled listener: `--sticky`, `--transparent` (→ solid on scroll), `--shrink`, and opt-in `--glass` (translucency + blur, only with a solid fallback — never default, since translucent headers fail WCAG 1.4.3 contrast over variable backgrounds). a11y built in (per W3C ARIA APG): mega-menus are disclosure widgets (`button` + `aria-expanded`, NOT `role=menu` — preserves link semantics); Esc closes a panel/drawer and returns focus; the mobile drawer is a real modal (`role=dialog` + `aria-modal` + focus trap + scroll-lock); a visually-hidden skip-to-content link leads the header. First deliverable of the app-shell/templates program (North Star pillar #3).",
      "usage": "`<header class=\"lb-header lb-header--sticky\" data-lb-header>` with a `.lb-header__inner` holding `.lb-header__brand` (logo slot), `.lb-header__nav` (`.lb-header__link` items; wrap a mega item in `.lb-header__item` with a `button.lb-header__link[data-lb-header-mega]` + `.lb-header__mega[data-lb-header-mega-panel]`), `.lb-header__actions` (`.lb-btn` CTAs), and a hamburger `button.lb-icon-btn.lb-icon-btn--ghost.lb-icon-btn--medium.lb-header__toggle[data-lb-header-toggle]` (consumes `.lb-icon-btn`; the header class only handles responsive show/hide). Add a `.lb-header__drawer[data-lb-header-drawer]` for mobile. Composition modifiers: `--floating`; scroll modifiers: `--sticky` / `--transparent` / `--shrink` / `--glass`. IMPORTANT: a `--sticky` header must be a direct child of the scroll root (never inside a `min-height:100vh` wrapper). API at `el._lbHeader`: `openDrawer/closeDrawer/toggleDrawer`, `openMega(btn)/closeMega`. Events: `lb-header-drawer-open/-close`, `lb-header-mega-open/-close`. Threshold override: `data-lb-scroll-threshold`.",
      "status": "app-shell program stage 1"
    },
    "input": {
      "name": "Input",
      "description": "Single-line text entry. States: default, focus, hover, disabled, error, success. Variants: password (with eye toggle), clearable (with X), search (with search icon), and phone number (with country selector). Always pair with a `.lb-label`; add `.lb-field__hint` or `.lb-field__error` for helper text. The same page covers **Number Input** — numeric variant with stepper buttons, min/max clamping, decimal step, unit prefix/suffix.",
      "usage": "`<div class=\"lb-field\"><label class=\"lb-label\" for=\"x\">Label</label><div class=\"lb-input-wrap\"><input class=\"lb-input lb-input--medium\" id=\"x\"></div><span class=\"lb-field__hint\">…</span></div>` — sizes `--medium` (default) / `--small`, validation `.lb-input--error` / `--success` paired with `.lb-field__error` / `.lb-field__success`, and the native `disabled` attribute; `.lb-label--required` appends the asterisk. Multiline is a native `<textarea class=\"lb-input lb-input--multiline\">` (needs no `.lb-input-wrap`); a leading icon is `<span class=\"lb-input-wrap__icon lb-input-wrap__icon--start\" data-lb-icon=\"search\">` plus `.lb-input--has-start` on the input. Behavior attributes go on `.lb-field` or directly on the `.lb-input-wrap`: `data-lb-clearable` injects the X button and fires `lb-input-clear` (bubbling), `data-lb-password` injects the eye toggle and fires `lb-password-toggle` with `{ visible }`. Phone is its own wrap — `<div class=\"lb-phone\" data-lb-phone>` (states `.lb-phone--error` / `--disabled` on the wrap, not the input) around `<input class=\"lb-phone__input\" type=\"tel\">`; it builds the country combobox, fires `lb-phone-change` with `{ country, number, fullNumber }`, and exposes `el._lbPhone.getValue()` / `.setCountry(iso)`. Number: `<div class=\"lb-number-wrap lb-number-wrap--medium\" data-lb-number data-lb-min=\"0\" data-lb-max=\"10\" data-lb-step=\"1\">` (or `--sm`) containing `<button class=\"lb-number__step lb-number__step--dec\">`, `<input type=\"number\" class=\"lb-number\">`, `<button class=\"lb-number__step lb-number__step--inc\">`, plus optional `.lb-number__prefix` / `.lb-number__suffix`; dispatches `lb-number-change` with `{ value }`, read/write via `wrap._lbNumber.getValue()` / `.setValue(v)`."
    },
    "list": {
      "name": "List",
      "description": "Shared primitive for option lists. Used internally by Select, Dropdown (combobox), MultiSelect, Menu, the Phone input country picker, and Datepicker month/year selects. Provides keyboard navigation (arrows, Home/End, Enter), optional type-to-filter, and single/multi/menu selection modes. Slots: leading icon, label, subtext, trailing hint or check. Direct use is rare — consumers mount it internally — but the API is documented so you can reuse the primitive in new components.",
      "usage": "Root `.lb-list` on a `<ul>`. Items are `.lb-list__item` (li, button, or a). Opt-in behaviour with `data-lb-list` and `data-lb-list-mode=\"single|multi|menu\"`; add `data-lb-list-filterable` for a pinned search input. Dispatches `lb-list-select` with `{ value, item, mode, selected }`."
    },
    "line-chart": {
      "name": "Line Chart",
      "description": "Time-series chart — single or multi-series, line or area fill. Hand-rolled SVG. Reuses the same niceTicks helper as Bar Chart for round-number axis ticks. Per-point dot markers act as hover targets driving a unified multi-series tooltip (all series at the hovered x position). Per-series `\"dashed\": true` renders the comparison-overlay idiom (dashed stroke for a de-emphasized previous-period series; pair with a muted color). Auto-renders a legend for multi-series. Color picks `--lb-data-1..8` by default.",
      "usage": "`<div data-lb-line-chart><script type=\"application/json\">{ \"x\": [\"Jan\",\"Feb\",…], \"series\": [{ \"name\": \"Revenue\", \"data\": [120,140,…] }] }</script></div>`. Config keys: `x` (categories or time labels), `series`, `area` (bool — adds a low-opacity fill below each line), `colors`, `legend`. Programmatic: `el._lbLineChart.setData({ … })`."
    },
    "library": {
      "name": "Library view",
      "description": "Media-library TEMPLATE (workspace-modules S3) — three modules on one canvas. **Toolbar Row** (`.lb-toolbar`): search (`.lb-input--has-start`) \u00b7 filter button with count badge \u00b7 sort \u00b7 segmented view switcher \u00b7 pagination, plus a **bulk bar** (`__bulk`) that replaces the default row while a selection is active (select-all with native indeterminate minus, live count, actions, clear \u2715). **Media Card** (`.lb-media-card`): grid + `--row` list variants; media area consumes the `.lb-card__media--*` aspect presets; overlay slots \u2014 24px selection check in a 32px hit area (WCAG 2.5.8 AA floor, spaced clear of the top-right hover action cluster), duration badge, caption + kebab. **Selection system** (`LB.Selection`, js/components/lb-selection.js): hover/focus reveals checks, FIRST selection escalates checks onto every card (progressive disclosure), long-press enters selection on touch, item-body click toggles while active, Esc/deselect-all/clear() exits \u2014 selection PERSISTS after bulk actions until deliberately cleared. Selected state: accent ring + scrim + persistent check.",
      "usage": "Container `data-lb-selection` (+ `data-lb-selection-toolbar=\"#id\"`), items `data-lb-selectable data-lb-id` each holding `.lb-media-card__check` with a `.lb-checkbox`. Toolbar hooks: `data-lb-selection-all` / `-count` / `-clear`. API `el._lbSelection`: getSelected/selectAll/clear/toggle/count/isActive. Event `lb-selection-change {ids, count, total}`. Load js/components/lb-selection.js.",
      "status": "workspace-modules program S3"
    },
    "inspector": {
      "name": "Inspector Panel",
      "description": "Right-rail detail/settings CONTENT pattern (workspace-modules S4) \u2014 song detail, object properties, issue panels, agent settings. Pure CSS anatomy: `__head` (optional back \u00b7 `__label`+`__title` \u00b7 close) \u2192 scrollable `__body` (`__hero`, badges/stats, CTA, `__section`s of key-value `__row`s with copy-to-clipboard icon-btns) \u2192 pinned `__foot` actions. Two mounts: IN-FLOW inside `aside.lb-shell__panel` (nonmodal complementary landmark \u2014 the verified default: keeps the canvas usable for reference/copy-paste) or OVERLAY inside `.lb-sheet--right` for transient tasks. Dynamic opens: move focus into the panel, return to launcher on close (IBM SidePanel model). Includes the `.lb-choice` row (radio + title + description \u2014 the settings-form pattern) for settings panels with segmented defaults + Save foot.",
      "usage": "`<div class=\"lb-inspector\">` with `__head` / `__body` / `__foot`. Rows: `.lb-inspector__row` + `__row-label` + `__row-value` + a ghost-small copy icon-btn. Settings flavor: `.lb-choice` labels wrapping `.lb-radio`, `.lb-dock__tray-row` + `.lb-segmented` for defaults. Mount in the shell aside or a right sheet.",
      "status": "workspace-modules program S4"
    },
    "link": {
      "name": "Link",
      "description": "Inline text link. Use for navigation between pages, external references, or in-paragraph actions that take the user somewhere. For actions that mutate state (submit, delete), use Button. Three sizes (small, medium, large), subtle variant for low-emphasis. Optional `.lb-link__icon` slot for trailing icons that signal destination type — `external-link` (opens new tab), `arrow-right` (continue/read more), `download` (file action). Icon scales with link size via 1em sizing. For external links, pair with `target=\"_blank\"` + `rel=\"noopener noreferrer\"`.",
      "usage": "Copy the full combo — base plus size: `<a class=\"lb-link lb-link--medium\" href=\"#\">`. `--small` / `--large` swap the action type ramp (a bare `.lb-link` falls back to the medium scale) and `--subtle` stacks on any size, changing colour only. The optional trailing icon is an empty `<span class=\"lb-link__icon\" aria-hidden=\"true\" data-lb-icon=\"external-link|arrow-right|download\"></span>` — the base is `inline-flex` with a built-in gap so it needs no margin, and the slot is sized in `1em` so it tracks the size variant. `.lb-link` is styling only: no controller binds to it, no events, no per-element JS handle; load `js/lb.js` only when you use the icon slot, which `LB.initIcons()` hydrates inside `LB.init()` at `DOMContentLoaded` (call `LB.initIcons(root)` again for links injected later — idempotent, hydrated spans are skipped). The underline is permanent and kept by `--subtle`, so a link is never signalled by colour alone; pair external links with `target=\"_blank\" rel=\"noopener noreferrer\"` and, if the surrounding text doesn't say so, an inner `<span class=\"lb-visually-hidden\">(opens in new tab)</span>` rather than an `aria-label`."
    },
    "media-player": {
      "name": "Media Player",
      "description": "Controls skin over a native `<audio>` / `<video>` element — letbe-ds owns the controls + layout, the playback engine stays native (attach hls.js / shaka-player for HLS/DASH/DRM; we never bundle a streaming engine). Opt in with `data-lb-media` on a wrapper containing the media element; `LB.Media` wires play/pause, seek with buffered region, time, volume, mute, keyboard a11y, an optional click-to-play playlist (auto-advances on end), and an optional now-playing block (cover art + title/artist that update per track). Long titles truncate and slide to reveal on hover (now-playing + playlist rows). Layout variants: default inline; `--card` for a focal now-playing layout (large cover on top, centred title/artist, current·play·total row); narrow players auto-reflow the scrubber to its own line below 320px. Video chrome (fullscreen, scrim, idle-hide), speed menu, skip ±10s, and VTT captions land in later slices. Reuses icon-btn (transport) and the slider rail/thumb pattern (scrubber + volume). All colours from existing tokens — no media-specific tokens. Playlist repeat-all: opt-in via `data-lb-media-loop` on the root (or `setLoop(true)`) — when the last track ends, playback wraps to track 1 instead of stopping; Next on the last track wraps too.",
      "usage": "`<div class=\"lb-media\" data-lb-media>` wrapping a native `<audio src=\"…\" preload=\"metadata\">` plus a `.lb-media__controls` row. Controls hook via data attributes: `data-lb-media-playpause`, `data-lb-media-seek` (containing a `.lb-media__range` + `.lb-media__buffered` + `.lb-media__fill`), `data-lb-media-current` / `-duration`, `data-lb-media-mute`, `data-lb-media-volume` (with `data-lb-media-volume-fill`). Optional playlist: a `[data-lb-media-playlist]` list of `[data-lb-media-track]` buttons each carrying `data-src` (+ optional `data-title`); clicking loads + plays and marks `.lb-media__track--active`. Optional transport: skip buttons (`data-lb-media-skip=\"-10\"` / `\"10\"`, seconds relative) and a speed pill (`data-lb-media-speed`, cycles 0.5×–2×). Public API on `el._lbMedia`: `play()`, `pause()`, `toggle()`, `seekTo(sec)`, `setRate(r)`, `loadTrack(i, autoplay)`, `setLoop(bool)` / `getLoop()` (repeat-all, mirrors `data-lb-media-loop`). Time formatting via `LB.fmtTime(seconds)`."
    },
    "menu": {
      "name": "Menu",
      "description": "Contextual popup list attached to a trigger button. Six patterns demonstrated: text+chevron trigger, kebab/icon-only trigger, user-account menu (avatar + name/email header), sectioned with group labels, checkable toggles (multi), and radio items (single pick). Items reuse the shared List primitive (`.lb-list__item`, `.lb-list__separator`, `.lb-list__group-label`, `.lb-list__check`) so a Menu's items stay visually consistent with Dropdown, Multi-Select, and Command Palette. For in-form selection use Select or Dropdown; for navigation use Nav.",
      "usage": "`<div class=\"lb-menu-wrapper\" data-lb-menu>` holding `<button class=\"lb-menu__trigger lb-btn lb-btn--secondary lb-btn--medium\">` (kebab form: `lb-menu__trigger lb-icon-btn lb-icon-btn--ghost lb-icon-btn--medium` with its own `aria-label`) followed by `<ul class=\"lb-menu lb-list\">` — `.lb-menu` is the popup surface, `.lb-list` the item layout. Rows are `<li><button class=\"lb-list__item\" data-lb-id=\"edit\">` with `.lb-list__icon` / `.lb-list__label` / `.lb-list__hint` (and `.lb-list__check` for stateful rows); modifiers `--danger`, `--disabled`, `--checked`; structural rows are `<li class=\"lb-list__separator\" role=\"separator\">`, `<li class=\"lb-list__group-label\">` and `<li class=\"lb-menu__header\">` (avatar + `.lb-menu__header-text` > `.lb-menu__header-name` / `-meta`). Auto-inits via `data-lb-menu` (instance `el._lbMenu`), and the controller hides the panel itself at init — no inline `display:none` to author. `data-lb-menu-mode=\"checkable\"` / `\"radio\"` on the wrapper upgrades rows to `role=\"menuitemcheckbox\"` / `\"menuitemradio\"` and mirrors `aria-checked` from `.lb-list__item--checked`; flipping that class is the consumer's job. Activating a row dispatches `lb-menu-select` on the wrapper with `detail.item` and then closes; the callback form is `new LB.Menu(el, { onSelect })`, which receives `data-lb-id` (falling back to the row's trimmed text). `destroy()` drops the outside-click listener."
    },
    "modal": {
      "name": "Modal",
      "description": "Overlay dialog that blocks the page until dismissed. Three sizes (small / medium / large). Includes the **Alert Dialog** variant on the same page — focused confirmation for destructive or irreversible actions (ARIA `alertdialog` role, non-dismissable backdrop, programmatic `LB.alert()` short-form with `Promise<boolean>` return).",
      "usage": "Standard dialog: `.lb-modal-backdrop` + `role=\"dialog\"` + `data-lb-modal`. Alert dialog: add `.lb-modal--alert` + use `role=\"alertdialog\"` + `.lb-btn--danger` for the confirm button. Programmatic: `LB.alert({ title, message, danger: true })`."
    },
    "navigation": {
      "name": "Navigation",
      "description": "App-level navigation. Two variants — horizontal (top app bar with underline-marked active item) and vertical (sidebar with left-bar active marker, sectioned groups). Items support leading icons, trailing notification badges (`.lb-nav__item-badge` — auto-hides when empty), disabled state, and aria-current. Vertical variant offers `--collapsed` for icon-only mode. Group items in sidebars with `.lb-nav__group` + `.lb-nav__group-label`. Four demos: horizontal, vertical-sectioned, collapsed, full app-shell (top + side combined).",
      "usage": "`<nav class=\"lb-nav lb-nav--horizontal\" aria-label=\"…\">` or `<nav class=\"lb-nav lb-nav--vertical\" aria-label=\"…\">`; add `lb-nav--collapsed` to the vertical one for the icon-only rail. Inside: `.lb-nav__brand` (with `.lb-nav__brand-mark` image and optional `.lb-nav__brand-text`), `<ul class=\"lb-nav__list\">` of `<a class=\"lb-nav__item\">` carrying `.lb-nav__item-icon` / `.lb-nav__item-label` / `.lb-nav__item-badge`, and a trailing `.lb-nav__actions` slot; vertical sidebars group items with `<li class=\"lb-nav__group\">` + `.lb-nav__group-label` (a plain `<span>` heading — nothing in the markup ties it to the items beneath). Pure CSS — nothing to init, no `data-lb-*` hook, no controller in `lb.js`, no events; the consumer moves `.lb-nav__item--active` + `aria-current=\"page\"` itself. Render an unavailable entry as `<span class=\"lb-nav__item lb-nav__item--disabled\" aria-disabled=\"true\">` instead of `<a>` so it leaves the tab order. Under `--collapsed` the item labels and `.lb-nav__brand-text` are clipped with the visually-hidden pattern (`position: absolute` + `clip-path: inset(50%)`), so they stay in the accessibility tree and keep naming each item — a `title` there is a hover hint, not the accessible name; `.lb-nav__item-badge` and `.lb-nav__group-label` are `display: none` and drop out of the tree entirely, so never put state a user needs in a badge alone."
    },
    "pagination": {
      "name": "Pagination",
      "description": "Page-range selector for long lists. Use when you have more than 10–12 items that users need to browse rather than search. For open-ended feeds (social, news), prefer infinite scroll instead. Active page uses primary action styling.",
      "usage": "`<nav class=\"lb-pagination\" data-lb-pagination data-lb-total-pages=\"12\" data-lb-current-page=\"3\" aria-label=\"Pagination\"></nav>` — ship it EMPTY; the controller owns the contents and renders `.lb-pagination__btn` prev/page/next buttons (`--prev`, `--next`, `--active`) plus `.lb-pagination__ellipsis` gaps from those two attributes. Auto-inits via `data-lb-pagination`; instance at `el._lbPagination`. `siblingCount` (default 1) and `onChange` have no attribute form — pass them to `new LB.Pagination(el, { totalPages, currentPage, siblingCount, onChange })`. Every accepted change re-renders and dispatches `lb-page-change` with `detail.page`; out-of-range and same-page requests are silently ignored, and the ellipsis collapse only kicks in above 7 total pages. The component tracks the page number only — fetching and redrawing the list is the consumer's job, driven by that event or `onChange(page)`; jump programmatically with `el._lbPagination.goTo(page)`."
    },
    "playbar": {
      "name": "Playbar",
      "description": "Persistent bottom now-playing/status dock (workspace-modules S5) \u2014 three zones: `__now` (thumb + title/artist) \u00b7 `__transport` (icon-btn buttons + scrubber consuming the canonical `.lb-slider-track-wrap` structure (keyboard-seekable via the real range input) + tabular timestamps) \u00b7 `__utils` (hidden \u2264768px). Layout + skin ONLY per the pluggable-engines rule \u2014 wire transport to LB.Media or your own engine. Safe-area aware. Mounts as the app frame's last child (below the shell `__main`).",
      "usage": "`<div class=\"lb-playbar\">` \u2192 `__now` (`__thumb` img + `__titles`) \u00b7 `__transport` (`__buttons` icon-btns, play = `--primary --medium`; `__scrub` with the canonical `.lb-slider-track-wrap` (relative wrapper for the absolute track) holding `.lb-slider-track`/`__fill` + a real `.lb-slider` range input + timestamps) \u00b7 `__utils`. Update `__fill` width from your engine's timeupdate; swap the play icon on state.",
      "status": "workspace-modules program S5"
    },
    "popover": {
      "name": "Popover",
      "description": "Non-modal floating panel anchored to a trigger. Four positions: top, bottom, left, right. Use for quick supplementary info, tool panels, or simple actions that don't warrant a full modal. Dismisses on outside click or Escape.",
      "usage": "`<div class=\"lb-popover-host\" data-lb-popover>` containing `<button class=\"lb-popover-trigger lb-btn lb-btn--secondary lb-btn--medium\">` followed by its panel `<div class=\"lb-popover lb-popover--bottom\" style=\"display:none;\">` with one position modifier (`--top` / `--bottom` / `--left` / `--right`) wrapping `.lb-popover__header` + `.lb-popover__body`. Author the panel with that inline `display:none` — the stylesheet never hides it and the controller's own hiding only lands at `DOMContentLoaded`. Auto-inits via `data-lb-popover` (instance at `el._lbPopover`), wiring trigger click to toggle, `Esc` while focus is anywhere inside the host, and outside mousedown to close; no ids to manage. It dispatches no CustomEvents and exposes no public open/close — the only JS hook is the `onOpenChange(open)` callback, so pre-assign `host._lbPopover = new LB.Popover(host, { onOpenChange })` before `DOMContentLoaded` and the auto-init sweep will skip that host. `destroy()` removes the document-level outside-click listener."
    },
    "progress": {
      "name": "Progress",
      "description": "Linear progress indicator. Sizes: small, medium, large. Variants for success/warning/danger. Use for determinate progress (uploads, multi-step form progress). For indeterminate waits use Spinner.",
      "usage": "`<progress class=\"lb-progress lb-progress--medium\" value=\"65\" max=\"100\"></progress>` — a native `<progress>` element, so there is no controller, no init and no events: drive it by setting `value` against `max` and the `::-webkit-progress-value` width transition animates the fill. The size modifier sets track height (`--small` 0.25rem / `--medium` 0.5rem / `--large` 0.75rem) and an optional status modifier stacks on top (`--success` / `--warning` / `--danger`), repainting the fill only — so the state must also be readable from adjacent text. Labelled form: `.lb-progress-wrap` → `.lb-progress-label-row` (`.lb-progress-label` + `.lb-progress-value`) above the `<progress>`; the value readout is plain text you update in the same step. In-cell form: `<span class=\"lb-progress-inline\">` wrapping a `.lb-progress-value` + a BARE `.lb-progress` — omit the size modifier there, the wrapper forces its own 1.8rem × `--lb-size-2x` mini track. A `<progress>` takes no accessible name from a neighbouring `<span>`: give it `aria-label`, or point `aria-labelledby` at the label row."
    },
    "radio": {
      "name": "Radio",
      "description": "Single-choice selection from a group. Horizontal or vertical layout. Use when the user must pick exactly one option and you want all options visible at once (≤5–7). For larger sets prefer Select or Dropdown.",
      "usage": "`<div class=\"lb-radio-group\" role=\"radiogroup\" aria-label=\"…\">` wrapping one `<label class=\"lb-radio-wrap\"><input type=\"radio\" name=\"…\" class=\"lb-radio\" value=\"…\"><span class=\"lb-radio__label\">…</span></label>` per option — the label wrapper is what makes the visible text the accessible name and the whole row a hit target. Pure CSS — nothing to init, no `js/lb.js` dependency, no instance handle and no custom events; rows added after load need no re-sweep because there is nothing to hydrate. Arrow-key roving, the single tab stop per group, and form submission are the browser's own, from the shared `name`. Add `.lb-radio-group--horizontal` for a wrapping row (`flex-direction: row` + `--lb-size-4x` gap); disable a row with native `disabled` on the input plus `.lb-radio-wrap--disabled` on its label. Read state natively: `change` bubbles, so one listener on `.lb-radio-group` hears every selection, and the value is `document.querySelector('input[name=\"…\"]:checked').value` or `FormData` — checked is drawn as a thicker `border-width` (0.375rem), not colour alone."
    },
    "rating": {
      "name": "Rating",
      "description": "Native radiogroup of icon items. Two modes: **cumulative** (default) — star-style 1→N fill for ordinal scales like product ratings; **select** — pick-one with per-item icons for thumbs up/down feedback or emoji satisfaction surveys. Filled state swaps to a paired `-filled` variant — solid for shapes that close cleanly (`star`, `heart`, `thumbs-up`, `thumbs-down`), or a soft tinted circle for face glyphs (`angry`, `frown`, `meh`, `smile`, `laugh`) so eyes and mouth stay readable. Hover preview, sizes, read-only display, disabled.",
      "usage": "Cumulative: `<div data-lb-rating data-lb-value=\"3\"></div>`. Select: `<div data-lb-rating data-lb-mode=\"select\" data-lb-icons=\"thumbs-down,thumbs-up\"></div>` or `data-lb-icons=\"angry,frown,meh,smile,laugh\"`. Like-toggle: `<div data-lb-rating data-lb-count=\"1\" data-lb-icon=\"heart\" data-lb-clearable></div>` — clicking the same item again clears the value. Configure with `data-lb-count`, `data-lb-icon`, `data-lb-size`, `data-lb-readonly`, `data-lb-disabled`. Listen for `lb-rating-change` with `{value}` (1-based index). `data-lb-name` sets the radio group name for form submission."
    },
    "resizable": {
      "name": "Resizable",
      "description": "Splittable panel container — flex layout with auto-injected drag handles between adjacent panels. Horizontal (sidebar + main) or vertical (top + bottom). Pointer events cover mouse, trackpad and touch. Each handle is a `role=\"separator\"` with `aria-orientation`, `aria-valuenow/min/max`, full keyboard support (arrows ±5%, PageUp/Down ±10%, Home/End to floor/ceiling). Min sizes per panel, fluid sizing in %, nestable for IDE-style three-pane layouts.",
      "usage": "`<div data-lb-resizable><div>Sidebar</div><div>Main</div></div>`. Set `data-lb-direction=\"vertical\"` for stacked layout. Per-panel `data-lb-size=\"30\"` (%) for initial sizing — undeclared panels share the remainder. `data-lb-min=\"15\"` enforces a per-panel floor (default 5%). Listen for `lb-resizable-change` with `{sizes}`. Programmatic: `el._lbResizable.setSizes([20, 60, 20])`."
    },
    "scroll-area": {
      "name": "Scroll Area",
      "description": "Documented primitive for 'scrollable content block' — constrains height (or width via `--horizontal`) and hands rendering to the global thin-scrollbar styles. Use for arbitrary scrollable consumer content like long descriptions, sidebar widgets, log views, code previews. `--fade` adds CSS-only scroll-shadow indicators at the start/end edges that appear only when more content exists in that direction (no JS). Component-specific scroll regions in Modal, Sheet, Dropdown keep their own implementations.",
      "usage": "`<div class=\"lb-scroll-area\">…</div>`. Override max-height per instance with `style=\"--max-height: 24rem\"`. Add `--fade` for scroll-shadow indicators; add `--horizontal` for horizontal scroll (use `--max-width` to constrain). The fade gradient uses theme tokens via `color-mix` so light/dark mode flip cleanly without override."
    },
    "segmented": {
      "name": "Segmented",
      "description": "Pill-style selector for mutually-exclusive choices in one visual unit — view modes, time ranges, sort orders, density presets. Visually and semantically distinct from Tabs (which own content panels). Default: radio mode (one active). Opt in `data-lb-mode=\"toggle\"` for multi-select (text-format B/I/U pattern). Sizes: `--sm`, `--medium` (default). Optional `--full-width` to stretch.",
      "usage": "`<div class=\"lb-segmented\" data-lb-segmented role=\"radiogroup\">` with `<button class=\"lb-segmented__item\" aria-checked=\"true|false\" data-lb-value=\"x\">`. For toggle mode: `data-lb-mode=\"toggle\"` + `aria-pressed`. Dispatches `lb-segmented-change` with `{ value, index, item, mode }`. Public API: `el._lbSegmented.getValue()` / `.setValue(v)`."
    },
    "select": {
      "name": "Select",
      "description": "Dropdown for choosing one option from a predefined list. Use when you have a moderate number of options (5–30) and no need for filtering. For filtering, use Dropdown (combobox). For free text, use Input. Sizes: small, medium. The same page also covers **MultiSelect** — multi-value variant with removable pills, optional filter + selected-first sort.",
      "usage": "`<div class=\"lb-select-field\" data-lb-select data-lb-options='[{\"value\":\"1\",\"label\":\"Option One\",\"disabled\":true}]' data-lb-placeholder=\"Choose option\">` holding a `<label class=\"lb-label\">` and an EMPTY `<div class=\"lb-select-wrap\"></div>` — the controller builds the `button.lb-select.lb-select--medium` trigger into that wrap (`data-lb-size=\"small\"` for the compact one, `data-lb-value` for the initial pick). Auto-inits via `data-lb-select` (instance `el._lbSelect`); dispatches `lb-select-change` on the field with `detail` = the option object, and exposes `setOptions(array)`, a `.value` getter/setter (assigning a known value selects it and emits), and `destroy()`. MultiSelect is the multi-value sibling: `<div class=\"lb-multi-select-field\" data-lb-multi-select data-lb-options='[…]' data-lb-placeholder=\"…\">` + empty `<div class=\"lb-multi-select-wrap\"></div>`, with optional `data-lb-filterable` (adds the search input), `data-lb-sort-selected` (floats selected items to the top on open), `data-lb-size=\"small\"` and `data-lb-value=\"de,fr,it\"` for the initial set. It dispatches `lb-multi-select-change` with `{ values, toggled }` and exposes `el._lbMultiSelect.getValue()` / `.setValue(array)` / `.setOptions(array)`. Both render their popup as `.lb-dropdown-list.lb-list` (MultiSelect adds `.lb-list--multi-select`, plus `.lb-list--filterable` when searchable)."
    },
    "sheet": {
      "name": "Sheet",
      "description": "Side panel that slides in from an edge. Positions: right, left, top, bottom. Use for transient forms, filters, or detail views. Less dismissive than Modal — the page stays partially visible.",
      "usage": "`<div class=\"lb-sheet-root\" data-lb-sheet style=\"display:none\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"…\">` holding a `.lb-sheet-overlay[aria-hidden]` scrim plus a `<div class=\"lb-sheet lb-sheet--right\">` panel (`__header` with `__title` + `__close`, then `__body`) — the edge is one modifier, `--right` / `--left` / `--top` / `--bottom` (sides are 400px wide, top/bottom 50vh). Auto-inits via `data-lb-sheet` in the `LB.init()` sweep at DOMContentLoaded; the instance lands on `el._lbSheet` with `.open()` / `.close()`, or construct directly as `new LB.Sheet(el, { onClose })` (`LB.Sheet` is exported). `open()` records `document.activeElement`, sets `display: flex`, sets `body.style.overflow = 'hidden'` and focuses `.lb-sheet__close`; `close()` reverses all three and returns focus to the recorded element, then calls `onClose`. It closes on a `.lb-sheet__close` click, an overlay click, or `Escape` — the keydown listener is bound to the root, so Esc only fires while focus is inside the panel; focus is moved and restored, never trapped (`LB.Modal` is the component that adds a trap). Every transition dispatches `lb-sheet-open` / `lb-sheet-close` on the `.lb-sheet-root` element."
    },
    "ai-chat": {
      "name": "AI Chat",
      "description": "AI-chat workspace TEMPLATE (Stage 5 opener; successor to the AI Chat template dashboard chrome) — pure composition, zero new components: `LB.Shell` frame (collapse/peek/drag-resize/drawer) · `LB.ConvList` sidebar (auto Pinned + date-grouped buckets, search, kebab actions; consumer owns data via `setConvs()`) · `.lb-thread` capped at 48rem (a readable measure for a message stream) · the Composer Dock module docked at the bottom (model/effort parameter tray via Popover) · `ContextBudgetBar` in the topbar · optional artifacts `aside` rail (transient by convention). Verified conventions baked in: assistant messages FLAT/document-style (bubble shapes signal casual messaging — our bubble default; only `--user` keeps the tint); streaming in the Thread's aria-live/role=log region, focus never jumps on completion; tab order composer → message actions → header actions; per-response 👍/👎 in bubble footers. The AI engine is the consumer's — wire composer/thread events to your provider.",
      "usage": "Compose: `LB.Shell` with ConvList filling the sidebar nav zone (`padding: 0` on `.lb-shell__nav`), `.lb-shell__content` as a flex column (scrollable thread wrapper + `.lb-dock` last), thread inside a 48rem centered container, budget bar in `__topbar-actions`, optional `aside.lb-shell__panel[aria-label=\"Artifacts\"]`. Load js/components/lb-chat.js + lb-chat-workspace.js (ContextBudgetBar) + lb-shell.js. Feed conversations via `el._lbConvList.setConvs([...])`, budget via `el._lbContextBudgetBar.setBudget({used, total})`.",
      "status": "workspace-modules program S2"
    },
    "data-table": {
      "name": "Data Table",
      "description": "The composed data-table VIEW (Stage-5 item #1; designed, token-driven, and framework-free). An admin screen, not a component: `LB.Shell` frame + toolbar (search \u00b7 Filters popover with applied-filter chips in `.lb-filter-bar` \u00b7 Columns visibility popover \u00b7 density segmented \u00b7 live result count \u00b7 `LB.Pagination`) with the `.lb-toolbar--selecting` bulk flip, a sticky split `.lb-table` with `data-lb-table-resize` column resize (drag \u00b7 arrow keys \u00b7 dblclick reset per WCAG 2.5.7) and delegated selection, plus the in-flow `.lb-shell__panel` inspector rail as the row side peek (nonmodal \u2014 table stays interactive). Client-side filter/slice/render loop calls `Table.refresh()` every pass; APG partial-DOM a11y (`aria-rowcount`/`aria-rowindex`, polite live-region count). Deliberately NOT bundled: windowed virtualization (bring TanStack Virtual/Clusterize per the pluggable-engines rule), column reorder (owner-deferred), grouping/aggregation/pivot (enterprise territory), inline cell editing (the spreadsheet pole \u2014 this is a list, native table role, deliberately not role=grid).",
      "usage": "Compose: `LB.Shell` (icon collapse) \u2192 `.lb-shell__content` holds page head (title + primary action), `.lb-toolbar` (default row: search/filters/columns/density/count/pagination; bulk row: count + actions + clear), `.lb-filter-bar` chips, then `.lb-table-wrap--sticky[data-lb-table-resize]` with header table (select-all + column heads) and body table (`data-lb-table`). Split-header select-all needs the small bridge adapter (see page source \u2014 the header checkbox lives outside the body table's LB.Table scan). Row click populates `aside.lb-shell__panel`. Load js/lb.js + js/components/lb-shell.js.",
      "status": "data-table program S10"
    },
    "kanban": {
      "name": "Kanban",
      "description": "Kanban board vocabulary (`LB.Board`, Stage-5 item #2; the verified gap: no mainstream free design system ships one \u2014 it is a paid-tier item everywhere). The PROJECTION model: columns are values of a field, the board owns zero data \u2014 the DOM is the state and every mutation dispatches `lb-board-move` {card, from, to, index} for the consumer to persist. Anatomy: `.lb-board` horizontal rail \u00b7 `.lb-board__column` (bg-strong surface, 17rem column width) with header (title + self-maintaining count badge + kebab) \u00b7 scrollable `__column-body` \u00b7 `__column-footer` add-card slot. Card = `.lb-card--sm.lb-card--interactive` composition with FIXED slots (NN/g scannability): `__card-row` (title + hover-reveal kebab) \u2192 `__card-meta` (badges/labels) in the body \u2192 avatar + `__card-due` in the CANONICAL `.lb-card__footer--spread` (its built-in hairline divides the footer zone). MENU-MOVE FIRST: every card kebab gets auto-built \"Move to <column>\" items \u2014 the single-pointer non-drag path WCAG 2.5.7 requires; moves are announced via a polite live region (no ARIA DnD vocabulary exists \u2014 authored announcements are the pattern). Pointer drag is progressive enhancement (see page). Card PROPERTY VOCABULARY (2026-08-04 benchmark, 10/10 verified): cover (`.lb-card__media` + ratios), plain-mono `__card-id`, priority badge (flag icon), checklist `.lb-progress-inline`, quiet `__card-stats` icon+count pairs, `.lb-avatar-group`, due-date states `__card-due--overdue/-soon/-done` (never red on done), blocked warning badge; advisory WIP via `data-lb-board-wip` on a column (count renders n/limit, warning tone only while over - advisory only, never blocks). Quiet by design: neutral cards, accent only for state.",
      "usage": "`<div class=\"lb-board\" data-lb-board>` containing `.lb-board__column[data-lb-board-column=\"Name\"]` \u2192 `__column-header` (`__column-title` + `.lb-badge--small[data-lb-board-count]` + optional column kebab) + `__column-body` with `.lb-card.lb-card--sm.lb-card--interactive.lb-board__card[data-lb-board-card]` cards + `__column-footer` (ghost add button). Load js/components/lb-board.js. API at `el._lbBoard`: `moveCard(card, column, index?)`, `refresh()` (re-sync counts + menus after external changes), `announce(msg)`. Event: `lb-board-move`. Empty column: `.lb-empty-state--sm` in the body.",
      "status": "kanban program K1-K2"
    },
    "app-shell": {
      "name": "App Shell",
      "description": "The dashboard skeleton (`LB.Shell`) — the 2026 \"inverted L\": full-height sidebar + content column whose topbar lives INSIDE the column (a nested header never maps to the banner landmark — W3C-intentional). Variant axes: **collapse** (`data-lb-shell-collapse=\"icon | offcanvas | none\"`; icon-rail = 3rem, expanded = 16rem, per verified convention) × **side** (`--right`) × **style** (standard · `--floating` sidebar-as-card · `--inset` content-as-card) × **right rail** (`.lb-shell__panel` aside slot). Opt-in **drag-resize** (`data-lb-shell-resizable`): drag the sidebar edge (clamps 14–22rem), drag-to-edge collapses, arrow keys resize, Enter/Space toggles — separator semantics consistent with `.lb-resizable__handle` (Resizable's %-engine doesn't fit a fixed-width sidebar, so the shell reuses its handle skin + a11y, not its engine). Opt-in **hover-peek** (`--peek`): the collapsed icon rail expands as a temporary OVERLAY on hover or `:focus-within` — content never shifts (pure CSS, negative-margin overlay; keyboard parity built in; standard style + desktop only; start on the rail by adding `--collapsed` in markup). State (collapsed + width) persists in localStorage under `data-lb-shell-key`. Toggle: `button[aria-expanded][aria-controls]` + Ctrl/Cmd+B. Mobile ≤768px: sidebar becomes a modal drawer (focus trap, scroll-lock, Esc, scrim, return-focus — the LB.Header drawer spec) or, with `--mobile-bar` + `.lb-shell__bottombar`, a bottom nav bar (one-handed reach on mobile, safe-area aware). a11y (verified research): sidebar nav = `<nav aria-label>` (NOT aside); content = `<main tabindex=\"-1\">` skip-link target; rail mode clip-hides labels so accessible names survive; frame = 100dvh; reduced-motion disables collapse animation. Fourth deliverable of the app-shell/templates program (North Star pillar #3).",
      "usage": "`<div class=\"lb-shell\" data-lb-shell data-lb-shell-collapse=\"icon\" data-lb-shell-resizable data-lb-shell-key=\"my-app\">` containing: `.lb-shell__skip` link → `#main`, `.lb-shell__sidebar` (zones: `__sidebar-head` with `__brand`+`__logo`, `nav.lb-shell__nav[aria-label]` with `__group`/`__group-label`/`__item` (+`__item-icon`/`__item-label`/`__item-chevron`; one nested level via `button[data-lb-shell-sub]` + `.lb-shell__sub`), `__sidebar-foot` user zone with `.lb-avatar`), and `.lb-shell__main` (`.lb-shell__topbar` with a `data-lb-shell-toggle` icon-btn + `.lb-breadcrumb` + `__topbar-actions`, then `main.lb-shell__content[tabindex=-1]` with optional `__page-head`/`__page-title`/`__page-actions`). Optional `aside.lb-shell__panel[aria-label]` inspector rail after `__main`. Optional `nav.lb-shell__bottombar` (+ `--mobile-bar` on the root) with `__bottombar-item`s. API at `el._lbShell`: `toggle()`, `setCollapsed(bool)`, `isCollapsed()`, `openDrawer()/closeDrawer()`. Events: `lb-shell-collapse` `{collapsed}`, `lb-shell-drawer-open/-close`. On a real page: shell is body's only child at 100dvh.",
      "status": "app-shell program stage 4"
    },
    "skeleton": {
      "name": "Skeleton",
      "description": "Placeholder shimmer for content loading states. Variants: text line, circular (avatar), rectangular (image/card). Compose to mirror the final layout. Use when content will arrive in under 1 second; longer waits need explicit progress feedback.",
      "usage": "`<span class=\"lb-skeleton lb-skeleton--text\">` — full combo, base plus one shape modifier: `--text` (100% wide × `1em` tall), `--circular` (fixed 2.5rem square), or `--rectangular` (100% wide × 8rem, surface radius). Pure CSS — nothing to init, no controller, no events: the shimmer is a self-running `::after` gradient (`lb-skeleton-shimmer`, 1.4s ease-in-out infinite), so you mount skeletons while loading and swap in the real content yourself; nothing tears them down for you. Width for `--text` / `--rectangular` comes from the container or a per-element inline `width` (the demo shortens lines to 80% / 60% that way). Wrap stacked text lines in `<div class=\"lb-skeleton-lines\">` for block display and consistent `--lb-size-2x` gaps. They are decoration, not content: mark them `aria-hidden=\"true\"` — one attribute on the `.lb-skeleton-lines` wrapper covers a whole group — and announce the wait on the region that will receive the content."
    },
    "slider": {
      "name": "Slider",
      "description": "Numeric value selection via drag handle. Sizes: small, medium. Use when values are continuous and the specific number matters less than the relative position (volume, price range, brightness). For exact input use a number input.",
      "usage": "`<div class=\"lb-slider-field\" data-lb-slider>` → optional `.lb-slider-field__header` (`__label` + `__value`), then `<div class=\"lb-slider-track-wrap lb-slider-track-wrap--medium\">` holding `.lb-slider-track` > `.lb-slider-track__fill` and a native `<input type=\"range\" class=\"lb-slider\" min max value>` — the size modifier lives on the track wrap (`--medium` / `--small`), never on the input, and the track/fill are `pointer-events: none` decoration under the real range input so keyboard operation stays the browser's. Auto-inits via `data-lb-slider` in the `LB.init()` sweep at DOMContentLoaded; instance on `el._lbSlider`, or `new LB.Slider(el)` for markup added later. On every native `input` the controller recomputes `.lb-slider-track__fill` width as a percentage of `min`/`max`, writes the number into `.lb-slider-field__value`, and mirrors it into `aria-valuenow`. Each sync dispatches `lb-slider-change` with `detail.value` (a number) on the `[data-lb-slider]` field element — it does NOT bubble, so listen on that element itself, and expect one fired at init. Read or set from code via the `el._lbSlider.value` getter/setter; the setter re-runs the full sync."
    },
    "sparkline": {
      "name": "Sparkline",
      "description": "Inline mini chart — line, area, or bar — with no axes, no legend, no chrome. Hand-rolled SVG with a 100-unit viewBox so it scales fluidly to its host's CSS size; no resize observer needed. Default color picks `--lb-data-1`; override per-instance via `data-lb-color` or `style=\"--color: …\"`. Optional end-of-series dot for the metric-card pattern. ARIA: rendered as `role=\"img\"` with a generated label summarising point count and trend direction.",
      "usage": "`<div data-lb-sparkline data-lb-data=\"10,15,12,18,22,28\"></div>`. Variants via `data-lb-variant=\"line|area|bar\"` (default line). `data-lb-dot` adds an end-of-series circle. `data-lb-color=\"#hex\"` overrides the series color. Programmatic: `el._lbSparkline.setData([…])` swaps data live."
    },
    "spinner": {
      "name": "Spinner",
      "description": "Indeterminate loading indicator. Three sizes: small, medium, large. Use for waits of unknown duration under ~5 seconds. For longer waits, pair with a message or use Progress if determinate. Don't stack multiple spinners.",
      "usage": "`<span class=\"lb-spinner lb-spinner--medium\" role=\"status\" aria-label=\"Loading\"></span>` — an empty element, and the full combo is required: width, height and ring thickness all live on the size modifier (`--small` 1rem / `--medium` 1.5rem / `--large` 2.5rem), so the bare base renders collapsed. Pure CSS — nothing to init, no start/stop API, no events: it spins from the moment it is in the DOM (`lb-spin` keyframes, 0.65s linear infinite), so mount it when the wait begins and remove it, or swap in the loaded content, when the wait ends. `flex-shrink: 0` is built in, so it can sit in a flex row beside a message without being squeezed. Under `prefers-reduced-motion: reduce` the rotation slows to 2.4s rather than freezing — a stopped ring reads as a hung control. Keep the `role=\"status\"` + `aria-label` pair: the ring holds no text of its own and would otherwise announce as an empty box."
    },
    "stat": {
      "name": "Stat",
      "description": "KPI primitive — label, big number, optional delta indicator, optional caption. Optional leading `.lb-stat__icon` in the label row (opt-in markup, aria-hidden, always fg-muted \u2014 the delta badge stays the only semantically-colored glyph; default is icon-free; icons are scan anchors for LARGE mixed-metric grids only, and it is all tiles in a grid or none). Pure HTML/CSS, no JS. Pairs with Sparkline for the metric-card pattern; stack several inside a Grid for a KPI strip. Owns no surface — drop inside Card or any container. The delta is just a Badge (small variant) inside `.lb-stat__sub`: success tone with `trending-up` icon for gains, danger tone with `trending-down` for losses, default tone with `minus` for flat — reusing the system's status palette and icons rather than bespoke chip styling.",
      "usage": "`<div class=\"lb-stat\"><div class=\"lb-stat__label\">Label</div><div class=\"lb-stat__value\">12,486</div><div class=\"lb-stat__sub\"><span class=\"lb-badge lb-badge--small lb-badge--success\"><span class=\"lb-badge__icon\" data-lb-icon=\"trending-up\"></span>4.2%</span><span class=\"lb-stat__caption\">vs last week</span></div></div>`. Modifiers: `--compact` (smaller value), `--row` (horizontal layout)."
    },
    "stepper": {
      "name": "Stepper",
      "description": "Multi-step progress indicator for signup flows, checkouts, onboarding wizards, multi-part forms. Horizontal (default) or vertical (`--vertical`) layout. States per step: `--complete`, `--active`, default (upcoming), `--error`. Optional clickable variant via `.lb-stepper__trigger` wrapper button. Pure declarative — consumer owns state.",
      "usage": "`<ol class=\"lb-stepper\">` (add `--vertical` for stacked layout). Each `<li class=\"lb-stepper__step\">` holds `.lb-stepper__indicator` (number / check / x SVG) and `.lb-stepper__label`. Vertical adds `.lb-stepper__content` wrapping label + `.lb-stepper__description`. Mark current step with `aria-current=\"step\"`."
    },
    "switch": {
      "name": "Switch",
      "description": "Binary on/off toggle with immediate effect. Use for settings where the change applies instantly (notifications on/off, dark mode). For multi-step forms where the value is submitted later, prefer Checkbox.",
      "usage": "`<label class=\"lb-switch-wrap\"><input type=\"checkbox\" class=\"lb-switch\" role=\"switch\"><span class=\"lb-switch__label\">…</span></label>` — the wrapping `<label>` supplies the accessible name and the row hit target. Pure CSS — nothing to init, no `js/lb.js` dependency, no instance handle, no custom events, and (unlike Checkbox, which the `LB.init()` sweep wraps in a `.lb-checkbox-frame` and injects glyph spans into) NO injected DOM: track and knob are painted straight onto the input via `appearance: none` + `::after`, so the semantics stay native. Pair a `disabled` input with `.lb-switch-wrap--disabled` on the label so the whole row dims. Wire the immediate effect to the native `change` event and read `input.checked`; switches added after load need no re-sweep. `:checked` moves the knob with `transform` in addition to recolouring the track, and focus draws a `--lb-border-focus` outline through `:focus-visible`."
    },
    "table": {
      "name": "Table",
      "description": "Semantic tabular layout for data-dense views — user lists, invoices, reports, admin panels. Cells are slots: drop any letbe-ds component (badge, avatar, button, input, switch, chip) inside. Three sizes (sm/md/lg), alignment per cell (start/center/end), opt-in zebra striping and sticky header, row selection with select-all + indeterminate, sortable columns dispatching a `lb-table-sort` event.",
      "usage": "Put `data-lb-table` on `<table>`. Selection checkboxes use `data-lb-table-select-all` (in thead) and `data-lb-table-select` (one per body row). Sortable headers use `<button data-lb-sort=\"key\">`. Add `data-lb-sort-auto` for built-in DOM sorting. For sticky-header mode, split into TWO tables — one inside `.lb-table-wrap__header` carrying only `<thead>`, another inside `.lb-table-wrap__body` carrying only `<tbody>`. Both get `.lb-table--sticky` (which applies `table-layout: fixed` so columns align). This keeps the vertical scrollbar confined to the body region only."
    },
    "tabs": {
      "name": "Tabs",
      "description": "Switch between related views of the same data in a shared context. Supports label-only, label + count badge (for filters), label + notification dot, icon + label, icon-only, and icon + label + badge. Use for view filters, settings groups, or tabbed content blocks.",
      "usage": "Root gets `data-lb-tabs`. Each tab uses `aria-selected=\"true\"` on the active button. Icon-only tabs need `aria-label` on the button."
    },
    "time-picker": {
      "name": "Time Picker",
      "description": "Hours and minutes selector — implemented as a `data-lb-mode=\"time\"` mode of the Date Picker so it shares dialog chrome, accessibility, keyboard handling, and trigger styling. Inline and input-with-popover variants. Two formats: 24h (default, 00:00–23:59) and 12h with AM/PM column (`data-lb-time-format=\"12h\"`). Minute granularity tunable via `data-lb-time-step` (1, 5, 10, 15, 30 typical). Trigger swaps the Date Picker's calendar icon for a clock so the affordance reads at-a-glance. For datetime input, place a Date Picker beside a Time Picker — each owns its concern.",
      "usage": "`<div class=\"lb-datepicker-field\" data-lb-datepicker data-lb-variant=\"input\" data-lb-mode=\"time\">` (or `data-lb-variant=\"inline\"`). Optional: `data-lb-time-format=\"12h\"`, `data-lb-time-step=\"15\"`, `data-lb-label`, `data-lb-hint`, `data-lb-error`, `data-lb-disabled`. Listens for `lb-datepicker-change` with `{ mode: 'time', value: { hours, minutes } }`. Public API: `field._lbDatepicker.setTime({ hours, minutes })`."
    },
    "timeline": {
      "name": "Timeline",
      "description": "Standalone trim / playhead / ruler primitive — set an in/out window over a duration, with an auto-scaled time-axis ruler (M:SS.mmm under a minute, MM:SS under an hour, HH:MM:SS otherwise). Composes with Media Player but doesn't require one — useful for any 'select a time range over a duration' UI (audio editor, video trimmer, transcript scrubber). Per the letbe-ds pluggable-engine rule, we ship the chrome + state machine; bring your own waveform renderer (WaveSurfer / Peaks) or frame-accurate scrub engine. Slice 1 ships the visible chrome + public API; pointer drag, keyboard nudge, and zoom land in later slices.",
      "usage": "`<div data-lb-timeline data-lb-duration=\"124.5\" data-lb-in=\"12\" data-lb-out=\"98\" data-lb-playhead=\"42\"></div>`. All values in seconds; omit any of `data-lb-in` / `data-lb-out` / `data-lb-playhead` to default to 0 / duration / 0. Public API: `el._lbTimeline.getRange()` / `getPlayhead()` / `getDuration()`, `setRange({inSec,outSec})`, `setPlayhead(sec)`, `setDuration(sec)`. Listen for `lb-timeline-change` with `{inSec, outSec, playheadSec, duration}` on any state change. Call `setDuration` from a consumer hook (e.g. `<audio>` `loadedmetadata`) to populate an empty timeline."
    },
    "toast": {
      "name": "Toast",
      "description": "Brief non-blocking notification appearing bottom-right. Four status variants (info, success, warning, danger) plus `neutral` — the actionable/undo flavor: an elevated-surface floating pill for neutral events that carry one action. Status toasts auto-dismiss after 5 seconds; a toast carrying an action gets 10. Use for transient confirmations (saved, copied) or low-priority errors. For critical errors use Banner or Modal.",
      "usage": "Instantiate once: `const mgr = new LB.ToastManager(); mgr.show({ variant, title, message })` — the `.lb-toast-region` is created on first use; `show()` returns an id for `mgr.dismiss(id)`. Variants: `info` | `success` | `warning` | `danger` | `neutral`. Add `action: { label, onClick }` for the undo pattern (pair with `variant: 'neutral'`); exactly ONE action is supported by design — two or more choices belong in a Modal. Passing `action` doubles the dismiss time to 10s (timers pause on hover/focus) and forces `role=\"status\"` instead of `alert`; the toast closes after the action fires unless `dismissOnAction: false`. For the strictest WCAG 2.2.1 reading pass `duration: 0`, or `new LB.ToastManager({ autoDismissWithAction: 0 })` globally. Other options: `duration` (ms, `0` = persist), `icon` (neutral renders none by default), `title`; constructor takes `{ autoDismiss }` for the global default. Timers pause on hover and focus; Esc dismisses while focus is inside."
    },
    "tooltip": {
      "name": "Tooltip",
      "description": "Short hint that appears on hover or focus. Four positions (top, bottom, left, right) with directional arrow carets. Default 300ms delay on mouse hover (override via `data-lb-tooltip-delay`); keyboard focus shows instantly. Optional `--small` variant for compact toolbars, `.lb-tooltip__kbd` for keyboard shortcuts. Hidden from touch users (no hover events) — never put essential information here. Pair icon-only buttons with `aria-label` so assistive tech still reads the affordance even when the tooltip never fires.",
      "usage": "`<span class=\"lb-tooltip-wrap\" data-lb-tooltip>` wrapping the trigger FIRST, then `<span class=\"lb-tooltip lb-tooltip--top\">…</span>` — the controller takes `el.firstElementChild` as the trigger, so that ordering is load-bearing. Auto-inits via `data-lb-tooltip` in the `LB.init()` sweep at DOMContentLoaded (handle at `el._lbTooltip`, or `new LB.Tooltip(el, { delay })`); on init it stamps the bubble with a generated `id` + `role=\"tooltip\"` and sets `aria-describedby` on the trigger, making the hint a description and never the accessible name — icon-only triggers still need their own `aria-label`. `mouseenter` shows after a 300ms timer while keyboard `focus` shows instantly; `mouseleave` and `blur` hide. Override the hover delay per instance with `data-lb-tooltip-delay=\"600\"` (`0` for instant). Position modifier picks placement and arrow direction — `--top` / `--bottom` / `--left` / `--right` — plus `--small` for tighter padding and a `.lb-tooltip__kbd` span after the label for a shortcut chip; content wraps at a 15rem `max-width`. No events and no public methods: visibility is purely the `.lb-tooltip--open` class the controller toggles (there is no CSS `:hover` rule, so without the JS it never opens), and the surface is `pointer-events: none` so it never intercepts a click meant for the trigger."
    },
    "tree": {
      "name": "Tree",
      "description": "Hierarchical list with expand / collapse, single-select, and full keyboard navigation. Markup-driven: `<ul data-lb-tree>` of `<li data-lb-node>` elements that may contain a nested `<ul>` of children. Each row gets a chevron, optional icon, and label. Native `role=\"tree\"` / `role=\"treeitem\"` with `aria-expanded`, `aria-selected`, `aria-level`. Keyboard: arrows to navigate / expand / collapse, Enter to select, Space to toggle, Home/End for first/last visible.",
      "usage": "Wrap nodes in `<ul data-lb-tree>`. Each `<li data-lb-node>` carries the label as text and may nest a `<ul>` of children. `data-lb-icon=\"folder|file|...\"` adds a glyph; `data-lb-expanded` opens a node initially; `data-lb-value=\"path\"` overrides the selection key (defaults to label text). Listen for `lb-tree-select` `{value, label}` and `lb-tree-expand` `{value, expanded}`. Programmatic: `el._lbTree.expandAll()`, `collapseAll()`, `getSelected()`, `setSelected(value)`."
    }
  }
}
