{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"filter-menu-clean","type":"registry:ui","title":"Filter Menu (Clean)","description":"Grouped filters with removable chips, collapsible checkbox groups, Clear all and full keyboard control","dependencies":[],"files":[{"path":"registry/filter-menu-clean/filter-menu-clean.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useId, useLayoutEffect, useRef, useState, type KeyboardEvent, type RefObject } from \"react\";\n\n/*\n * Filter Menu (Clean) – grouped filters with chips, in a single self-contained file.\n * Clean style: no tray – one white card with hairline shadows, gray tab-like chips on one line (\"+N\" for the rest),\n * quiet rows with a subtle hover, blue checkboxes and the keyboard hints as the card's last row.\n * Active filters sit on top as removable chips; below, collapsible groups list their options as checkboxes\n * with a count of what's picked. Arrow keys move through groups and options, Enter or Space picks,\n * ← and → fold a group. Heights glide when groups open or chips come and go.\n * The styles are injected below, so no extra files are needed.\n */\n\nexport type FilterOption = { id: string; label: string };\n\nexport type FilterGroup = {\n  id: string;\n  label: string;\n  options: FilterOption[];\n  /** Put in front of the option on its chip, e.g. \"Score: \" → \"Score: 80–90\" */\n  chipPrefix?: string;\n};\n\n/** Picked option ids per group id */\nexport type FilterValue = Record<string, string[]>;\n\nexport type FilterMenuProps = {\n  groups: FilterGroup[];\n  /** Controlled value */\n  value?: FilterValue;\n  /** Starting value when uncontrolled */\n  defaultValue?: FilterValue;\n  onChange?: (value: FilterValue) => void;\n  /** Groups that start open – by default the first one */\n  defaultOpen?: string[];\n  title?: string;\n  /** Show the keyboard hints as the last row of the card */\n  hints?: boolean;\n  className?: string;\n};\n\ntype Row = { kind: \"group\"; group: FilterGroup; gi: number } | { kind: \"option\"; group: FilterGroup; option: FilterOption; gi: number; oi: number };\n\n/** Gap between chips, in px – matches the CSS */\nconst CHIP_GAP = 4;\n\nexport function FilterMenu({\n  groups: rawGroups,\n  value,\n  defaultValue = {},\n  onChange,\n  defaultOpen,\n  title = \"Filters\",\n  hints = true,\n  className,\n}: FilterMenuProps) {\n  // Gaps, groups without an options list and options without a label are skipped; ids stay as given\n  const groups = (Array.isArray(rawGroups) ? rawGroups : [])\n    .filter((g): g is FilterGroup => !!g && typeof g === \"object\")\n    .map((g) => ({\n      ...g,\n      label: String(g.label ?? \"\"),\n      options: (Array.isArray(g.options) ? g.options : []).filter((o): o is FilterOption => !!o && typeof o === \"object\").map((o) => ({ ...o, label: String(o.label ?? \"\") })),\n    }));\n  const uid = useId();\n  const [inner, setInner] = useState<FilterValue>(defaultValue);\n  const picked = value ?? inner;\n  const [open, setOpen] = useState<Set<string>>(() => new Set(defaultOpen ?? groups.slice(0, 1).map((g) => g.id)));\n  const [focus, setFocus] = useState<string | null>(null);\n  const listRef = useRef<HTMLDivElement>(null);\n  const chipsInner = useRef<HTMLDivElement>(null);\n  const chipsOuter = useSmoothHeight(chipsInner);\n  const measure = useRef<HTMLDivElement>(null);\n  const [fit, setFit] = useState(Infinity);\n\n  const commit = (next: FilterValue) => {\n    if (value === undefined) setInner(next);\n    onChange?.(next);\n  };\n  // Only lists count – a string or a stray value in `value` picks nothing\n  const pickedIn = (g: string) => (Array.isArray(picked[g]) ? picked[g] : []);\n  const isOn = (g: string, o: string) => pickedIn(g).includes(o);\n  const toggle = (g: string, o: string) => {\n    const cur = pickedIn(g);\n    commit({ ...picked, [g]: cur.includes(o) ? cur.filter((x) => x !== o) : [...cur, o] });\n  };\n  const toggleGroup = (g: string) =>\n    setOpen((s) => {\n      const next = new Set(s);\n      if (next.has(g)) next.delete(g);\n      else next.add(g);\n      return next;\n    });\n\n  // Chips in the order of the groups and their options – one per picked id, even if an id repeats\n  const seen = new Set<string>();\n  const chips = groups.flatMap((g) =>\n    g.options\n      .filter((o) => isOn(g.id, o.id) && !seen.has(`${g.id}\\u0000${o.id}`) && !!seen.add(`${g.id}\\u0000${o.id}`))\n      .map((o) => ({ group: g, option: o, label: `${g.chipPrefix ?? \"\"}${o.label}` })),\n  );\n  const chipKey = chips.map((c) => `${c.group.id}:${c.option.id}:${c.label}`).join(\"|\");\n\n  // One line only: as many chips as fit, the rest hide behind \"+N\".\n  // A hidden copy of the chips (plus a \"+99\" sample) is measured to know their widths before they are placed.\n  useLayoutEffect(() => {\n    const row = chipsInner.current;\n    const list = measure.current;\n    if (!row || !list) return;\n    const place = () => {\n      const widths = [...list.children].map((el) => (el as HTMLElement).offsetWidth + CHIP_GAP);\n      const more = widths.pop() ?? 0;\n      const style = getComputedStyle(row);\n      const room = row.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) + CHIP_GAP;\n      let used = 0;\n      let n = 0;\n      for (; n < widths.length; n++) {\n        const rest = n + 1 < widths.length ? more : 0;\n        if (used + widths[n] + rest > room) break;\n        used += widths[n];\n      }\n      setFit(n);\n    };\n    place();\n    const ro = new ResizeObserver(place);\n    ro.observe(row);\n    return () => ro.disconnect();\n  }, [chipKey]);\n\n  const shown = chips.slice(0, fit);\n  const hidden = chips.slice(fit);\n\n  // Everything the arrow keys can reach, top to bottom\n  // Keys come from positions, so groups or options that share an id stay reachable\n  const rows: Row[] = groups.flatMap((g, gi) => [\n    { kind: \"group\", group: g, gi } as Row,\n    ...(open.has(g.id) ? g.options.map((o, oi) => ({ kind: \"option\", group: g, option: o, gi, oi }) as Row) : []),\n  ]);\n  const keyOf = (r: Row) => (r.kind === \"group\" ? `g:${r.gi}` : `o:${r.gi}:${r.oi}`);\n\n  const moveTo = (key: string) => {\n    setFocus(key);\n    listRef.current?.querySelector<HTMLElement>(`[data-key=\"${CSS.escape(key)}\"]`)?.focus();\n  };\n\n  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    const i = rows.findIndex((r) => keyOf(r) === focus);\n    const row = rows[i];\n    if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      const next = rows[Math.max(0, Math.min(rows.length - 1, i + (e.key === \"ArrowDown\" ? 1 : -1)))] ?? rows[0];\n      moveTo(keyOf(next));\n    } else if (e.key === \"Home\" || e.key === \"End\") {\n      e.preventDefault();\n      moveTo(keyOf(rows[e.key === \"Home\" ? 0 : rows.length - 1]));\n    } else if (row && (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\")) {\n      e.preventDefault();\n      const g = row.group.id;\n      if (e.key === \"ArrowRight\" && !open.has(g)) toggleGroup(g);\n      if (e.key === \"ArrowLeft\") {\n        if (open.has(g)) toggleGroup(g);\n        moveTo(`g:${row.gi}`);\n      }\n    } else if (row && (e.key === \"Enter\" || e.key === \" \")) {\n      e.preventDefault();\n      if (row.kind === \"group\") toggleGroup(row.group.id);\n      else toggle(row.group.id, row.option.id);\n    }\n  };\n\n  return (\n    <div className={[\"ui-fmc\", className].filter(Boolean).join(\" \")}>\n      {/* React 19 hoists this into <head> once, no matter how many menus render */}\n      <style href=\"ui-filter-menu-clean\" precedence=\"default\">\n        {css}\n      </style>\n\n      <div className=\"ui-fmc__head\">\n        <span className=\"ui-fmc__title\" dir=\"auto\" title={title}>\n          {title}\n        </span>\n        <button type=\"button\" className=\"ui-fmc__clear\" disabled={chips.length === 0} onClick={() => commit({})}>\n          Clear all\n        </button>\n      </div>\n\n      {/* Active filters on one line – the box glides to its new height as the line appears or goes */}\n      <div className=\"ui-fmc__chips-outer\" ref={chipsOuter}>\n        <div className=\"ui-fmc__chips\" ref={chipsInner} data-empty={chips.length === 0 || undefined} aria-live=\"polite\">\n          {/* Hidden copy, only to measure how many chips fit on the line */}\n          <div className=\"ui-fmc__measure\" ref={measure} aria-hidden=\"true\">\n            {chips.map((c, i) => (\n              <span key={i} className=\"ui-fmc__chip\">\n                <span className=\"ui-fmc__chip-label\">{c.label}</span>\n                <span className=\"ui-fmc__chip-x\" />\n              </span>\n            ))}\n            <span className=\"ui-fmc__chip ui-fmc__more\">+99</span>\n          </div>\n          {shown.map((c, i) => (\n            <span key={i} className=\"ui-fmc__chip\">\n              <span className=\"ui-fmc__chip-label\" dir=\"auto\" title={c.label}>\n                {c.label}\n              </span>\n              <button type=\"button\" className=\"ui-fmc__chip-x\" aria-label={`Remove ${c.label}`} onClick={() => toggle(c.group.id, c.option.id)}>\n                <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n                  <path d=\"M4.5 4.5l7 7M11.5 4.5l-7 7\" />\n                </svg>\n              </button>\n            </span>\n          ))}\n          {hidden.length > 0 && (\n            <span\n              className=\"ui-fmc__chip ui-fmc__more\"\n              title={hidden.map((c) => c.label).join(\", \")}\n              aria-label={`${hidden.length} more: ${hidden.map((c) => c.label).join(\", \")}`}\n            >\n              <span key={hidden.length} className=\"ui-fmc__roll\">\n                +{hidden.length}\n              </span>\n            </span>\n          )}\n        </div>\n      </div>\n\n      <div className=\"ui-fmc__groups\" ref={listRef} onKeyDown={onKeyDown}>\n        {groups.map((g, gi) => {\n          const isOpen = open.has(g.id);\n          const count = new Set(pickedIn(g.id).filter((id) => g.options.some((o) => o.id === id))).size;\n          const gKey = `g:${gi}`;\n          return (\n            <section key={`${g.id}#${gi}`} className=\"ui-fmc__group\" data-open={isOpen ? \"\" : undefined}>\n              <button\n                type=\"button\"\n                className=\"ui-fmc__group-head\"\n                data-key={gKey}\n                tabIndex={focus === gKey || (focus === null && gi === 0) ? 0 : -1}\n                aria-expanded={isOpen}\n                aria-controls={`${uid}-${gi}`}\n                onFocus={() => setFocus(gKey)}\n                onClick={() => toggleGroup(g.id)}\n              >\n                <svg className=\"ui-fmc__chevron\" viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n                  <path d=\"M4.5 6.25 8 9.75l3.5-3.5\" />\n                </svg>\n                <span className=\"ui-fmc__group-label\" dir=\"auto\" title={g.label}>\n                  {g.label}\n                </span>\n                <span className=\"ui-fmc__group-count\">{g.options.length.toLocaleString(\"en-US\")}</span>\n                <span className=\"ui-fmc__group-picked\" aria-live=\"polite\">\n                  {count > 0 && (\n                    <span key={count} className=\"ui-fmc__roll\">\n                      {`${count} filter${count === 1 ? \"\" : \"s\"}`}\n                    </span>\n                  )}\n                </span>\n              </button>\n              {/* Opens and closes by animating its grid row from 0 to the content height */}\n              <div className=\"ui-fmc__panel\" id={`${uid}-${gi}`} role=\"group\" aria-label={g.label}>\n                <div className=\"ui-fmc__panel-inner\">\n                  {g.options.map((o, oi) => {\n                    const key = `o:${gi}:${oi}`;\n                    const on = isOn(g.id, o.id);\n                    return (\n                      <button\n                        key={`${o.id}#${oi}`}\n                        type=\"button\"\n                        role=\"checkbox\"\n                        aria-checked={on}\n                        className=\"ui-fmc__option\"\n                        data-key={key}\n                        tabIndex={isOpen && focus === key ? 0 : -1}\n                        onFocus={() => setFocus(key)}\n                        onClick={() => toggle(g.id, o.id)}\n                      >\n                        <span className=\"ui-fmc__box\" aria-hidden=\"true\">\n                          <svg viewBox=\"0 0 16 16\">\n                            <path d=\"m4.25 8.25 2.5 2.5 5-5.5\" />\n                          </svg>\n                        </span>\n                        <span className=\"ui-fmc__option-label\" dir=\"auto\" title={o.label}>\n                          {o.label}\n                        </span>\n                      </button>\n                    );\n                  })}\n                </div>\n              </div>\n            </section>\n          );\n        })}\n      </div>\n\n      {/* The last row of the card, under a hairline */}\n      {hints && (\n        <div className=\"ui-fmc__hints\" aria-hidden=\"true\">\n          <span className=\"ui-fmc__hint\">\n            <kbd>↑</kbd>\n            <kbd>↓</kbd>\n            <span>to navigate</span>\n          </span>\n          <span className=\"ui-fmc__hint\">\n            <kbd>↵</kbd>\n            <span>to select</span>\n          </span>\n        </div>\n      )}\n    </div>\n  );\n}\n\n/** The outer box follows the inner box's height, with a transition after the first frame */\nfunction useSmoothHeight(inner: RefObject<HTMLElement | null>) {\n  const outer = useRef<HTMLDivElement>(null);\n  useLayoutEffect(() => {\n    const o = outer.current;\n    const i = inner.current;\n    if (!o || !i) return;\n    const sync = () => {\n      o.style.height = `${i.offsetHeight}px`;\n    };\n    sync();\n    const ro = new ResizeObserver(sync);\n    ro.observe(i);\n    const frame = requestAnimationFrame(() => o.setAttribute(\"data-smooth\", \"\"));\n    return () => {\n      ro.disconnect();\n      cancelAnimationFrame(frame);\n    };\n  }, [inner]);\n  return outer;\n}\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n/* Colors and motion as variables – override them on .ui-fmc */\n.ui-fmc {\n  --ui-fmc-card: #ffffff;\n  --ui-fmc-ink: var(--aui-ink, rgba(0, 0, 0, 0.88));\n  --ui-fmc-strong: var(--aui-ink, #000000);\n  --ui-fmc-text: var(--aui-muted, rgba(0, 0, 0, 0.61));\n  --ui-fmc-muted: var(--aui-muted, rgba(0, 0, 0, 0.45));\n  --ui-fmc-faint: var(--aui-placeholder, rgba(0, 0, 0, 0.27));\n  --ui-fmc-icon: var(--aui-muted, rgba(0, 0, 0, 0.447));\n  --ui-fmc-line: var(--aui-border, rgba(0, 0, 0, 0.06));\n  --ui-fmc-fill: rgba(0, 0, 0, 0.06);\n  --ui-fmc-fill-strong: rgba(0, 0, 0, 0.1);\n  --ui-fmc-wash: var(--aui-subtle, rgba(0, 0, 0, 0.03));\n  --ui-fmc-box-ring: var(--aui-border-strong, rgba(0, 0, 0, 0.22));\n  --ui-fmc-accent: rgba(0, 96, 226, 0.765);\n  --ui-fmc-check: #ffffff;\n  --ui-fmc-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  /* Strokes are shadows: a 0.5px hairline plus a soft lift */\n  --ui-fmc-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.06), 0 1px 1px -1px rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n  --ui-fmc-ease: var(--aui-ease, cubic-bezier(0.22, 1, 0.36, 1));\n  --ui-fmc-spring: cubic-bezier(0.34, 1.56, 0.64, 1);\n  box-sizing: border-box;\n  width: 100%;\n  max-width: 400px;\n  container-type: inline-size;\n  overflow: hidden;\n  border-radius: var(--aui-radius-lg, 10px);\n  background: var(--aui-surface, var(--ui-fmc-card));\n  box-shadow: var(--aui-shadow-card, var(--ui-fmc-shadow));\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 13px;\n  line-height: 20px;\n  font-weight: 400;\n  color: var(--ui-fmc-ink);\n  -webkit-font-smoothing: antialiased;\n}\n.ui-fmc *, .ui-fmc *::before, .ui-fmc *::after { box-sizing: border-box; }\n:where(.ui-fmc) button { margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; }\n:where(.ui-fmc) svg { display: block; flex: none; }\n.ui-fmc button:focus-visible { outline: 2px solid var(--ui-fmc-focus); outline-offset: 2px; }\n\n/* ---------- Header ---------- */\n\n.ui-fmc__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 10px 8px 16px; }\n.ui-fmc__title { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-align: start; font-size: 15px; line-height: 24px; font-weight: 500; color: var(--ui-fmc-strong); }\n/* Clear all: a quiet ghost button */\n.ui-fmc__clear {\n  flex: none;\n  height: 28px;\n  padding: 0 8px !important;\n  border-radius: calc(var(--aui-radius-sm, 5px) + 1px);\n  font-size: 13px;\n  font-weight: 500;\n  color: var(--ui-fmc-text);\n  transition: color 160ms ease, background-color 160ms ease;\n}\n.ui-fmc__clear:hover:not(:disabled) { background: var(--ui-fmc-wash); color: var(--ui-fmc-ink); }\n.ui-fmc__clear:disabled { color: var(--ui-fmc-faint); cursor: default; }\n\n/* ---------- Chips: one line, the rest go into \"+N\" ---------- */\n\n.ui-fmc__chips-outer { overflow: hidden; }\n.ui-fmc__chips-outer[data-smooth] { transition: height 320ms var(--ui-fmc-ease); }\n.ui-fmc__chips { position: relative; display: flex; flex-wrap: nowrap; gap: 4px; padding: 0 16px 12px; overflow: hidden; }\n.ui-fmc__chips[data-empty] { padding-bottom: 0; }\n.ui-fmc__measure { position: absolute; top: 0; left: 0; display: flex; gap: 4px; width: max-content; visibility: hidden; pointer-events: none; }\n.ui-fmc__chip {\n  display: inline-flex;\n  flex: none;\n  align-items: center;\n  gap: 2px;\n  height: 24px;\n  padding: 0 3px 0 8px;\n  border-radius: calc(var(--aui-radius-sm, 5px) + 1px);\n  background: var(--aui-subtle, var(--ui-fmc-fill));\n  font-size: 12px;\n  line-height: 16px;\n  font-weight: 500;\n  color: var(--ui-fmc-ink);\n  white-space: nowrap;\n  animation: ui-fmc-chip-in 260ms var(--ui-fmc-ease) both;\n}\n.ui-fmc__chip-label { min-width: 0; max-width: 180px; overflow: hidden; text-overflow: ellipsis; }\n.ui-fmc__chip-x {\n  display: grid;\n  place-items: center;\n  flex: none;\n  width: 18px;\n  height: 18px;\n  border-radius: calc(var(--aui-radius-sm, 5px) - 1px);\n  color: var(--ui-fmc-icon);\n  transition: background-color 140ms ease, color 140ms ease;\n}\n.ui-fmc__chip-x:hover { background: var(--ui-fmc-fill); color: var(--ui-fmc-ink); }\n.ui-fmc__chip-x svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; }\n.ui-fmc__more { padding: 0 8px; color: var(--ui-fmc-text); font-variant-numeric: tabular-nums; cursor: default; }\n.ui-fmc__roll { display: inline-block; animation: ui-fmc-roll 300ms var(--ui-fmc-ease) both; }\n\n/* ---------- Groups ---------- */\n\n.ui-fmc__groups { display: grid; grid-template-columns: minmax(0, 1fr); padding: 4px; border-top: 0.5px solid var(--ui-fmc-line); }\n.ui-fmc__group-head {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  width: 100%;\n  height: 32px;\n  padding: 0 8px !important;\n  border-radius: calc(var(--aui-radius-sm, 5px) + 1px);\n  /* Hover comes in crisp and fades out when the pointer moves on */\n  transition: background-color 140ms ease;\n}\n.ui-fmc__group-head:hover { background: var(--ui-fmc-wash); transition-duration: 90ms; }\n.ui-fmc__group-head:focus-visible, .ui-fmc__option:focus-visible { outline-offset: -2px; }\n.ui-fmc__chevron {\n  width: 16px;\n  height: 16px;\n  fill: none;\n  stroke: var(--ui-fmc-icon);\n  stroke-width: 1.1;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n  transform: rotate(-90deg);\n  transition: transform 260ms var(--ui-fmc-ease);\n}\n.ui-fmc__group[data-open] .ui-fmc__chevron { transform: none; }\n.ui-fmc__group-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: start; font-weight: 500; color: var(--ui-fmc-ink); white-space: nowrap; }\n.ui-fmc__group-count { flex: none; font-size: 12px; line-height: 16px; color: var(--ui-fmc-faint); font-variant-numeric: tabular-nums; }\n.ui-fmc__group-picked { display: inline-flex; flex: none; margin-left: auto; overflow: hidden; font-size: 12px; line-height: 16px; color: var(--ui-fmc-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }\n\n.ui-fmc__panel {\n  display: grid;\n  grid-template-rows: 0fr;\n  transition: grid-template-rows 320ms var(--ui-fmc-ease);\n}\n.ui-fmc__group[data-open] .ui-fmc__panel { grid-template-rows: 1fr; }\n.ui-fmc__panel-inner {\n  display: grid;\n  grid-template-columns: minmax(0, 1fr);\n  min-height: 0;\n  overflow: hidden;\n  padding: 0;\n  opacity: 0;\n  transition: opacity 220ms ease, padding 320ms var(--ui-fmc-ease);\n}\n.ui-fmc__group[data-open] .ui-fmc__panel-inner { opacity: 1; padding: 0 0 4px; }\n/* A long list scrolls inside the open group, so the card keeps a sane height */\n.ui-fmc__group[data-open] .ui-fmc__panel-inner { max-height: 320px; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; }\n\n/* Options sit under the group's label, the checkbox lined up with its text */\n.ui-fmc__option {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  width: 100%;\n  height: 32px;\n  padding: 0 8px 0 32px !important;\n  border-radius: calc(var(--aui-radius-sm, 5px) + 1px);\n  color: var(--ui-fmc-ink);\n  transition: background-color 140ms ease;\n}\n.ui-fmc__option:hover { background: var(--ui-fmc-wash); transition-duration: 90ms; }\n.ui-fmc__option-label { min-width: 0; text-align: start; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n/* Checkbox: 14px with a hairline, blue when picked; the check draws itself in */\n.ui-fmc__box {\n  display: grid;\n  place-items: center;\n  flex: none;\n  width: 14px;\n  height: 14px;\n  border-radius: 4px;\n  background: var(--ui-fmc-card);\n  box-shadow: inset 0 0 0 0.5px var(--ui-fmc-box-ring), 0 1px 1px 0 rgba(0, 0, 0, 0.04);\n  transition: background-color 160ms ease, box-shadow 160ms ease;\n}\n.ui-fmc__box svg {\n  width: 12px;\n  height: 12px;\n  fill: none;\n  stroke: var(--ui-fmc-check);\n  stroke-width: 1.6;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n  stroke-dasharray: 12;\n  stroke-dashoffset: 12;\n  transition: stroke-dashoffset 240ms var(--ui-fmc-ease);\n}\n.ui-fmc__option[aria-checked=\"true\"] .ui-fmc__box { background: var(--ui-fmc-accent); box-shadow: none; }\n.ui-fmc__option[aria-checked=\"true\"] .ui-fmc__box svg { stroke-dashoffset: 0; transition-delay: 40ms; }\n\n/* ---------- Keyboard hints: the last row of the card ---------- */\n\n.ui-fmc__hints {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  height: 32px;\n  padding: 0 12px;\n  border-top: 0.5px solid var(--ui-fmc-line);\n  font-size: 12px;\n  line-height: 16px;\n  color: var(--ui-fmc-muted);\n}\n.ui-fmc__hint { display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; }\n.ui-fmc__hint > span { margin-left: 4px; }\n.ui-fmc kbd {\n  display: inline-grid;\n  place-items: center;\n  min-width: 16px;\n  height: 16px;\n  padding: 0 4px;\n  border-radius: calc(var(--aui-radius-sm, 5px) - 1px);\n  background: var(--ui-fmc-card);\n  box-shadow: var(--ui-fmc-shadow);\n  font-family: inherit;\n  font-size: 10px;\n  line-height: 16px;\n  font-weight: 500;\n  color: var(--ui-fmc-text);\n}\n\n@keyframes ui-fmc-chip-in { from { opacity: 0; transform: scale(0.9); } }\n@keyframes ui-fmc-roll { from { opacity: 0; transform: translateY(60%); } to { opacity: 1; transform: none; } }\n\n/* Narrow: a bit less side padding, shorter chips */\n@container (max-width: 320px) {\n  .ui-fmc__head { padding-left: 14px; }\n  .ui-fmc__chips { padding-left: 14px; padding-right: 14px; }\n  .ui-fmc__chip-label { max-width: 132px; }\n}\n/* Touch screens: taller rows and buttons */\n@media (hover: none) and (pointer: coarse) {\n  .ui-fmc__clear { height: 36px; }\n  .ui-fmc__chip { height: 32px; }\n  .ui-fmc__chip-x { width: 26px; height: 26px; }\n  .ui-fmc__group-head { height: 44px; }\n  .ui-fmc__option { height: 44px; }\n  .ui-fmc__box { width: 18px; height: 18px; border-radius: 5px; }\n  .ui-fmc__box svg { width: 14px; height: 14px; }\n  .ui-fmc__hints { display: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .ui-fmc *, .ui-fmc *::before, .ui-fmc *::after { animation: none !important; transition: none !important; }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-fmc {\n  --ui-fmc-card: #1c1c1e;\n  --ui-fmc-ink: var(--aui-ink, rgba(255, 255, 255, 0.9));\n  --ui-fmc-strong: var(--aui-ink, #ffffff);\n  --ui-fmc-text: var(--aui-muted, rgba(255, 255, 255, 0.62));\n  --ui-fmc-muted: var(--aui-muted, rgba(255, 255, 255, 0.46));\n  --ui-fmc-faint: var(--aui-placeholder, rgba(255, 255, 255, 0.3));\n  --ui-fmc-icon: var(--aui-muted, rgba(255, 255, 255, 0.5));\n  --ui-fmc-line: var(--aui-border, rgba(255, 255, 255, 0.08));\n  --ui-fmc-fill: rgba(255, 255, 255, 0.08);\n  --ui-fmc-fill-strong: rgba(255, 255, 255, 0.13);\n  --ui-fmc-wash: var(--aui-subtle, rgba(255, 255, 255, 0.04));\n  --ui-fmc-box-ring: var(--aui-border-strong, rgba(255, 255, 255, 0.28));\n  --ui-fmc-accent: var(--aui-blue, #4fb0fe);\n  --ui-fmc-check: #ffffff;\n  --ui-fmc-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n  --ui-fmc-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.4);\n}\n`;\n"}]}