@teamofsilicons/silicon-ui 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +4 -3
  2. package/check.mjs +14 -5
  3. package/components/activity-heatmap/activity-heatmap.module.css +4 -4
  4. package/components/alert/alert.module.css +1 -1
  5. package/components/animated-counter/animated-counter.js +1 -1
  6. package/components/announcement-bar/announcement-bar.module.css +6 -6
  7. package/components/avatar/avatar.d.ts +2 -2
  8. package/components/avatar/avatar.js +1 -1
  9. package/components/avatar/avatar.module.css +5 -2
  10. package/components/avatar-group/avatar-group.module.css +4 -4
  11. package/components/badge/badge.module.css +4 -4
  12. package/components/bar-chart/bar-chart.js +1 -1
  13. package/components/bar-chart/bar-chart.module.css +4 -4
  14. package/components/billing-toggle/billing-toggle.js +2 -2
  15. package/components/billing-toggle/billing-toggle.module.css +10 -10
  16. package/components/bottom-sheet/bottom-sheet.js +1 -1
  17. package/components/bottom-sheet/bottom-sheet.module.css +4 -5
  18. package/components/breadcrumb/breadcrumb.d.ts +8 -2
  19. package/components/breadcrumb/breadcrumb.js +21 -9
  20. package/components/breadcrumb/breadcrumb.module.css +7 -3
  21. package/components/brush-chart/brush-chart.module.css +9 -9
  22. package/components/button/button.d.ts +7 -2
  23. package/components/button/button.js +3 -1
  24. package/components/button/button.module.css +10 -1
  25. package/components/button-group/button-group.module.css +7 -7
  26. package/components/calendar/calendar.js +15 -5
  27. package/components/calendar/calendar.module.css +13 -10
  28. package/components/card/card.js +4 -4
  29. package/components/card/card.module.css +2 -2
  30. package/components/card-stack/card-stack.js +2 -2
  31. package/components/card-stack/card-stack.module.css +2 -3
  32. package/components/carousel/carousel.module.css +3 -3
  33. package/components/chat-thread/chat-thread.module.css +41 -41
  34. package/components/checkbox/checkbox.module.css +1 -1
  35. package/components/chip-group/chip-group.js +1 -1
  36. package/components/chip-group/chip-group.module.css +2 -2
  37. package/components/code-block/code-block.module.css +7 -14
  38. package/components/color-picker/color-picker.module.css +20 -21
  39. package/components/combobox/combobox.js +5 -1
  40. package/components/combobox/combobox.module.css +2 -2
  41. package/components/command-palette/command-palette.js +18 -11
  42. package/components/command-palette/command-palette.module.css +18 -18
  43. package/components/comment-thread/comment-thread.module.css +35 -35
  44. package/components/confirm-morph/confirm-morph.js +5 -5
  45. package/components/confirm-morph/confirm-morph.module.css +9 -9
  46. package/components/context-menu/context-menu.js +10 -28
  47. package/components/context-menu/context-menu.module.css +3 -3
  48. package/components/copy-button/copy-button.js +4 -4
  49. package/components/countdown/countdown.module.css +4 -4
  50. package/components/date-picker/date-picker.js +3 -3
  51. package/components/date-picker/date-picker.module.css +7 -7
  52. package/components/date-range-picker/date-range-picker.js +13 -10
  53. package/components/date-range-picker/date-range-picker.module.css +23 -22
  54. package/components/dialog/dialog.module.css +2 -2
  55. package/components/donut-chart/donut-chart.module.css +5 -5
  56. package/components/drawer/drawer.module.css +7 -7
  57. package/components/dropdown-menu/dropdown-menu.d.ts +71 -1
  58. package/components/dropdown-menu/dropdown-menu.js +100 -6
  59. package/components/dropdown-menu/dropdown-menu.module.css +15 -5
  60. package/components/elastic-slider/elastic-slider.js +2 -2
  61. package/components/elastic-slider/elastic-slider.module.css +4 -4
  62. package/components/expandable-card/expandable-card.js +1 -1
  63. package/components/expandable-card/expandable-card.module.css +1 -1
  64. package/components/expanding-button-group/expanding-button-group.module.css +3 -3
  65. package/components/expanding-search/expanding-search.js +3 -3
  66. package/components/expanding-search/expanding-search.module.css +9 -9
  67. package/components/file-dropzone/file-dropzone.js +3 -3
  68. package/components/file-dropzone/file-dropzone.module.css +21 -21
  69. package/components/file-upload/file-upload.module.css +7 -7
  70. package/components/filter-toolbar/filter-toolbar.js +7 -3
  71. package/components/filter-toolbar/filter-toolbar.module.css +9 -9
  72. package/components/floating-button-group/floating-button-group.module.css +7 -7
  73. package/components/gauge/gauge.js +1 -1
  74. package/components/gauge/gauge.module.css +2 -2
  75. package/components/hover-card/hover-card.js +2 -2
  76. package/components/hover-card/hover-card.module.css +2 -2
  77. package/components/image-compare/image-compare.module.css +4 -4
  78. package/components/inline-edit/inline-edit.module.css +12 -12
  79. package/components/json-viewer/json-viewer.module.css +12 -12
  80. package/components/line-chart/line-chart.js +1 -1
  81. package/components/line-chart/line-chart.module.css +8 -8
  82. package/components/mention-input/mention-input.js +4 -2
  83. package/components/mention-input/mention-input.module.css +8 -8
  84. package/components/metric-card/metric-card.module.css +1 -1
  85. package/components/money-input/money-input.js +6 -3
  86. package/components/money-input/money-input.module.css +10 -10
  87. package/components/morph-select/morph-select.js +7 -5
  88. package/components/morph-select/morph-select.module.css +13 -13
  89. package/components/multi-select/multi-select.js +4 -2
  90. package/components/multi-select/multi-select.module.css +6 -6
  91. package/components/nav-list/nav-list.d.ts +64 -0
  92. package/components/nav-list/nav-list.js +96 -0
  93. package/components/nav-list/nav-list.module.css +25 -0
  94. package/components/notification-center/notification-center.module.css +27 -27
  95. package/components/number-field/number-field.js +3 -3
  96. package/components/number-field/number-field.module.css +3 -3
  97. package/components/otp-input/otp-input.module.css +4 -4
  98. package/components/pagination/pagination.module.css +1 -1
  99. package/components/password-field/password-field.module.css +2 -2
  100. package/components/password-strength/password-strength.module.css +8 -8
  101. package/components/phone-input/phone-input.js +9 -7
  102. package/components/phone-input/phone-input.module.css +19 -19
  103. package/components/popover/popover.module.css +2 -2
  104. package/components/progress/progress.module.css +1 -1
  105. package/components/radio-cards/radio-cards.module.css +8 -8
  106. package/components/radio-group/radio-group.js +4 -1
  107. package/components/radio-group/radio-group.module.css +4 -3
  108. package/components/resizable-panels/resizable-panels.module.css +5 -5
  109. package/components/rich-text-editor/rich-text-editor.js +3 -3
  110. package/components/rich-text-editor/rich-text-editor.module.css +12 -12
  111. package/components/ridgeline/ridgeline.module.css +7 -7
  112. package/components/scroll-area/scroll-area.module.css +8 -8
  113. package/components/segmented-control/segmented-control.d.ts +3 -1
  114. package/components/segmented-control/segmented-control.js +5 -2
  115. package/components/segmented-control/segmented-control.module.css +9 -4
  116. package/components/select/select.module.css +5 -5
  117. package/components/shortcut-recorder/shortcut-recorder.js +5 -5
  118. package/components/shortcut-recorder/shortcut-recorder.module.css +16 -16
  119. package/components/signature-pad/signature-pad.module.css +11 -11
  120. package/components/slider/slider.module.css +4 -4
  121. package/components/slope-chart/slope-chart.module.css +7 -7
  122. package/components/slot-text/slot-text.js +2 -1
  123. package/components/sortable-data-table/sortable-data-table.module.css +4 -4
  124. package/components/sparkline/sparkline.js +1 -1
  125. package/components/split-button/split-button.module.css +6 -6
  126. package/components/stepper/stepper.module.css +3 -3
  127. package/components/streamgraph/streamgraph.module.css +7 -7
  128. package/components/swipe-actions/swipe-actions.js +2 -2
  129. package/components/swipe-actions/swipe-actions.module.css +5 -5
  130. package/components/switch/switch.js +2 -2
  131. package/components/switch/switch.module.css +1 -1
  132. package/components/tabs/tabs.d.ts +9 -2
  133. package/components/tabs/tabs.js +11 -5
  134. package/components/tabs/tabs.module.css +10 -3
  135. package/components/tag-input/tag-input.module.css +4 -4
  136. package/components/text-reveal/text-reveal.js +1 -1
  137. package/components/time-picker/time-picker.js +4 -2
  138. package/components/time-picker/time-picker.module.css +3 -3
  139. package/components/timeline/timeline.js +4 -4
  140. package/components/timeline/timeline.module.css +3 -3
  141. package/components/toast/toast.js +1 -1
  142. package/components/toast/toast.module.css +5 -5
  143. package/components/toast-stack/toast-stack.js +1 -1
  144. package/components/toast-stack/toast-stack.module.css +5 -5
  145. package/components/tooltip/tooltip.module.css +1 -1
  146. package/components/tree-view/tree-view.js +16 -12
  147. package/components/tree-view/tree-view.module.css +10 -8
  148. package/components/treemap/treemap.module.css +13 -13
  149. package/components/usage-meter/usage-meter.js +1 -1
  150. package/components/usage-meter/usage-meter.module.css +5 -5
  151. package/components/user-menu/user-menu.js +16 -14
  152. package/components/user-menu/user-menu.module.css +22 -22
  153. package/components/waffle-chart/waffle-chart.module.css +7 -7
  154. package/components.json +416 -1
  155. package/eslint.js +35 -3
  156. package/lib/fluid-hover.d.ts +83 -0
  157. package/lib/fluid-hover.js +184 -0
  158. package/lib/menu-highlight.d.ts +24 -11
  159. package/lib/menu-highlight.js +29 -28
  160. package/lib/motion-tokens.d.ts +76 -18
  161. package/lib/motion-tokens.js +45 -10
  162. package/package.json +1 -1
  163. package/styles.css +50 -7
@@ -4,6 +4,7 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
4
4
  import { ChevronDown, X } from "lucide-react";
5
5
  import { useEffect, useId, useMemo, useRef, useState } from "react";
6
6
  import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import { useNearestItem } from "../../lib/fluid-hover.js";
7
8
  import styles from "./multi-select.module.css";
8
9
  const enter = motionTokens.ease.enter;
9
10
  const standard = motionTokens.ease.standard;
@@ -41,6 +42,8 @@ export function MultiSelect({ label, options, value, defaultValue = [], onValueC
41
42
  const [internal, setInternal] = useState(defaultValue);
42
43
  const [open, setOpen] = useState(false);
43
44
  const [activeIndex, setActiveIndex] = useState(-1);
45
+ // Fluid hover: the mouse makes the nearest option active, gaps and padding included, and a click there toggles it.
46
+ const nearest = useNearestItem({ items: '[role="option"]', onPick: item => setActiveIndex(Number(item.dataset.index)) });
44
47
  const selected = value ?? internal;
45
48
  const selectedSet = useMemo(() => new Set(selected), [selected]);
46
49
  const labelFor = (item) => options.find((option) => option.value === item)?.label ?? item;
@@ -100,6 +103,5 @@ export function MultiSelect({ label, options, value, defaultValue = [], onValueC
100
103
  }
101
104
  };
102
105
  const reduce = useReducedMotion();
103
- return _jsxs("div", { ref: rootRef, className: [styles.field, className].filter(Boolean).join(" "), children: [_jsx("span", { id: labelId, className: styles.label, children: label }), _jsxs("div", { className: styles.control, children: [_jsxs("button", { type: "button", className: `${styles.trigger} ${selected.length ? styles.hasClear : ""}`, disabled: disabled, "aria-labelledby": `${labelId} ${valueId}`, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": `${id}-listbox`, onClick: () => setOpen((current) => !current), onKeyDown: onKeyDown, children: [_jsx("span", { id: valueId, className: styles.srOnly, children: selected.length ? selected.map(labelFor).join(", ") : placeholder }), _jsx("span", { className: styles.value, "aria-hidden": "true", children: _jsxs(AnimatePresence, { initial: false, children: [visible.map((item) => _jsx(motion.span, { className: styles.slot, variants: reduce ? slotFade : slot, initial: "hidden", animate: "shown", exit: "gone", children: _jsx(motion.span, { className: styles.chip, variants: reduce ? chipStill : chip, children: item.label }) }, `chip-${item.value}`)), remaining > 0 && _jsx(motion.span, { className: styles.slot, variants: reduce ? slotFade : slot, initial: "hidden", animate: "shown", exit: "gone", children: _jsxs(motion.span, { className: styles.more, variants: reduce ? chipStill : chip, children: ["+", _jsx("span", { className: styles.count, children: _jsx(AnimatePresence, { initial: false, custom: countDirection, children: _jsx(motion.span, { custom: countDirection, variants: reduce ? fade : roll, initial: "hidden", animate: "shown", exit: "gone", children: remaining }, remaining) }) })] }) }, "more"), !selected.length && _jsx(motion.span, { className: styles.placeholder, variants: fade, initial: "hidden", animate: "shown", exit: "gone", children: placeholder }, "placeholder")] }) }), _jsx(ChevronDown, { className: styles.chevron, size: 16, "aria-hidden": "true" })] }), _jsx(AnimatePresence, { initial: false, children: selected.length > 0 && !disabled && _jsx(motion.button, { type: "button", "aria-label": "Clear selections", className: styles.clear, onClick: clear, initial: reduce ? { opacity: 0 } : { opacity: 0, scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, scale: 1, filter: "blur(0px)", transition: reduce ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast } } }, exit: { opacity: 0, ...(reduce ? {} : { scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` }), transition: { duration: motionTokens.duration.instant, ease: standard } }, whileTap: { scale: reduce ? 1 : 0.96, transition: { duration: motionTokens.duration.instant, ease: standard } }, children: _jsx(X, { size: 14, "aria-hidden": "true" }) }) }), _jsx(AnimatePresence, { initial: false, children: open && _jsx(motion.div, { id: `${id}-listbox`, className: styles.menu, role: "listbox", "aria-label": label, "aria-multiselectable": "true", initial: reduce ? { opacity: 0 } : { opacity: 0, y: -6, scale: .97 }, animate: { opacity: 1, y: 0, scale: 1, transition: reduce ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: enter } } }, exit: { opacity: 0, ...(reduce ? {} : { y: -4, scale: .98 }), transition: { duration: motionTokens.duration.instant, ease: standard } }, children: options.map((option, index) => _jsxs("button", { type: "button", role: "option", "aria-selected": selectedSet.has(option.value), "aria-disabled": option.disabled || undefined, className: styles.option, "data-active": activeIndex === index, disabled: option.disabled, onPointerMove: () => { if (activeIndex !== index)
104
- setActiveIndex(index); }, onClick: () => toggle(option), children: [_jsx("span", { children: option.label }), _jsx(AnimatePresence, { initial: false, children: selectedSet.has(option.value) && _jsx(CheckMark, { reduce: reduce }, "check") })] }, option.value)) }) })] }), description && _jsx("span", { className: styles.description, children: description })] });
106
+ return _jsxs("div", { ref: rootRef, className: [styles.field, className].filter(Boolean).join(" "), children: [_jsx("span", { id: labelId, className: styles.label, children: label }), _jsxs("div", { className: styles.control, children: [_jsxs("button", { type: "button", className: `${styles.trigger} ${selected.length ? styles.hasClear : ""}`, disabled: disabled, "aria-labelledby": `${labelId} ${valueId}`, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": `${id}-listbox`, onClick: () => setOpen((current) => !current), onKeyDown: onKeyDown, children: [_jsx("span", { id: valueId, className: styles.srOnly, children: selected.length ? selected.map(labelFor).join(", ") : placeholder }), _jsx("span", { className: styles.value, "aria-hidden": "true", children: _jsxs(AnimatePresence, { initial: false, children: [visible.map((item) => _jsx(motion.span, { className: styles.slot, variants: reduce ? slotFade : slot, initial: "hidden", animate: "shown", exit: "gone", children: _jsx(motion.span, { className: styles.chip, variants: reduce ? chipStill : chip, children: item.label }) }, `chip-${item.value}`)), remaining > 0 && _jsx(motion.span, { className: styles.slot, variants: reduce ? slotFade : slot, initial: "hidden", animate: "shown", exit: "gone", children: _jsxs(motion.span, { className: styles.more, variants: reduce ? chipStill : chip, children: ["+", _jsx("span", { className: styles.count, children: _jsx(AnimatePresence, { initial: false, custom: countDirection, children: _jsx(motion.span, { custom: countDirection, variants: reduce ? fade : roll, initial: "hidden", animate: "shown", exit: "gone", children: remaining }, remaining) }) })] }) }, "more"), !selected.length && _jsx(motion.span, { className: styles.placeholder, variants: fade, initial: "hidden", animate: "shown", exit: "gone", children: placeholder }, "placeholder")] }) }), _jsx(ChevronDown, { className: styles.chevron, size: 16, "aria-hidden": "true" })] }), _jsx(AnimatePresence, { initial: false, children: selected.length > 0 && !disabled && _jsx(motion.button, { type: "button", "aria-label": "Clear selections", className: styles.clear, onClick: clear, initial: reduce ? { opacity: 0 } : { opacity: 0, scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, scale: 1, filter: "blur(0px)", transition: reduce ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast } } }, exit: { opacity: 0, ...(reduce ? {} : { scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` }), transition: { duration: motionTokens.duration.instant, ease: standard } }, whileTap: { scale: reduce ? 1 : 0.96, transition: { duration: motionTokens.duration.instant, ease: standard } }, children: _jsx(X, { size: 14, "aria-hidden": "true" }) }) }), _jsx(AnimatePresence, { initial: false, children: open && _jsx(motion.div, { id: `${id}-listbox`, className: styles.menu, role: "listbox", "aria-label": label, "aria-multiselectable": "true", ...nearest, initial: reduce ? { opacity: 0 } : { opacity: 0, y: -6, scale: .97 }, animate: { opacity: 1, y: 0, scale: 1, transition: reduce ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: enter } } }, exit: { opacity: 0, ...(reduce ? {} : { y: -4, scale: .98 }), transition: { duration: motionTokens.duration.instant, ease: standard } }, children: options.map((option, index) => _jsxs("button", { type: "button", role: "option", "aria-selected": selectedSet.has(option.value), "aria-disabled": option.disabled || undefined, className: styles.option, "data-active": activeIndex === index, disabled: option.disabled, "data-index": index, onClick: () => toggle(option), children: [_jsx("span", { children: option.label }), _jsx(AnimatePresence, { initial: false, children: selectedSet.has(option.value) && _jsx(CheckMark, { reduce: reduce }, "check") })] }, option.value)) }) })] }), description && _jsx("span", { className: styles.description, children: description })] });
105
107
  }
@@ -2,12 +2,12 @@
2
2
  .control { position: relative; min-width: 0; }
3
3
  .label { font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
4
4
  /* The trigger anchors the menu, so press feedback stays in color; it never scales. */
5
- .trigger { position: relative; display: flex; width: 100%; min-height: var(--control-height-md); align-items: center; gap: 8px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: 5px 11px 5px 12px; background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
5
+ .trigger { position: relative; display: flex; width: 100%; min-height: var(--control-height-md); align-items: center; gap: var(--space-related); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: var(--space-tight) var(--space-group) var(--space-tight) var(--space-group); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
6
6
  @media (hover: hover) and (pointer: fine) { .trigger:hover:not(:disabled) { border-color: var(--border-strong); background: var(--surface-muted); } }
7
7
  .trigger:active:not(:disabled), .trigger[aria-expanded="true"] { border-color: var(--border-strong); background: var(--surface-muted); }
8
8
  .trigger:disabled { opacity: .5; cursor: not-allowed; }
9
9
  /* The chevron stays pinned to the trailing edge; a selection only reserves room for the clear button beside it, so nothing jumps. */
10
- .hasClear .chevron { margin-left: 26px; }
10
+ .hasClear .chevron { margin-left: var(--space-section); }
11
11
  .value { position: relative; display: flex; min-width: 0; min-height: 26px; flex: 1; align-items: center; overflow: hidden; }
12
12
  .placeholder, .description { color: var(--text-muted); }
13
13
  /* The placeholder sits outside the flow so the first chip can open its slot from the leading edge. */
@@ -15,21 +15,21 @@
15
15
  /* The slot clips while its width springs open or closed; the chip keeps its natural width so the label never re-truncates mid-flight. */
16
16
  .slot { display: flex; flex: 0 1 auto; min-width: 0; overflow: hidden; }
17
17
  .slot:has(.more) { flex-shrink: 0; }
18
- .chip, .more { box-sizing: border-box; flex: 0 0 auto; max-width: 9rem; overflow: hidden; margin-right: 5px; border-radius: var(--radius-full); corner-shape: round; padding: 4px 8px; background: var(--surface-muted); box-shadow: inset 0 0 0 1px var(--border-subtle); color: var(--foreground); text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); transition: background-color var(--duration-fast) var(--ease-standard); }
18
+ .chip, .more { box-sizing: border-box; flex: 0 0 auto; max-width: 9rem; overflow: hidden; margin-right: var(--space-tight); border-radius: var(--radius-full); corner-shape: round; padding: var(--space-tight) var(--space-related); background: var(--surface-muted); box-shadow: inset 0 0 0 1px var(--border-subtle); color: var(--foreground); text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); transition: background-color var(--duration-fast) var(--ease-standard); }
19
19
  .trigger[aria-expanded="true"] .chip, .trigger[aria-expanded="true"] .more { background: var(--surface); }
20
20
  @media (hover: hover) and (pointer: fine) { .trigger:hover:not(:disabled) .chip, .trigger:hover:not(:disabled) .more { background: var(--surface); } }
21
21
  .more { display: inline-flex; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
22
22
  .count { display: inline-grid; }
23
23
  .count > span { grid-area: 1 / 1; }
24
24
  .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
25
- .clear { position: absolute; top: 0; bottom: 0; right: 36px; display: grid; width: 22px; height: 22px; margin: auto 0; place-items: center; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
25
+ .clear { position: absolute; top: 0; bottom: 0; right: var(--space-block); display: grid; width: 22px; height: 22px; margin: auto 0; place-items: center; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
26
26
  @media (hover: hover) and (pointer: fine) { .clear:hover { background: var(--surface); color: var(--foreground); } }
27
27
  .chevron { flex: 0 0 auto; color: var(--text-muted); transition: transform var(--duration-spring) var(--ease-spring); }
28
28
  .trigger[aria-expanded="true"] .chevron { transform: rotate(180deg); }
29
29
  .description { font-size: var(--text-xs); }
30
- .menu { position: absolute; z-index: 80; top: calc(100% + 8px); right: 0; left: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); padding: 5px; background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: top center; will-change: transform, opacity; }
30
+ .menu { position: absolute; z-index: var(--layer-dropdown); top: calc(100% + var(--space-related)); right: 0; left: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); padding: var(--space-tight); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: top center; will-change: transform, opacity; }
31
31
  /* Arrow keys move the highlight often, so it changes almost instantly. Selection is shown by the check, not by weight, so labels never reflow. */
32
- .option { display: flex; width: 100%; min-height: 38px; align-items: center; justify-content: space-between; gap: 12px; border: 0; border-radius: calc(var(--radius-container) - 5px); corner-shape: var(--corner-shape); padding: 0 11px; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: background-color 80ms var(--ease-standard), color 80ms var(--ease-standard); }
32
+ .option { display: flex; width: 100%; min-height: 38px; align-items: center; justify-content: space-between; gap: var(--space-group); border: 0; border-radius: calc(var(--radius-container) - var(--space-tight)); corner-shape: var(--corner-shape); padding: 0 var(--space-group); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: background-color 80ms var(--ease-standard), color 80ms var(--ease-standard); }
33
33
  .option > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34
34
  .option[data-active="true"] { background: var(--surface-muted); }
35
35
  .option:disabled { opacity: .42; cursor: not-allowed; }
@@ -0,0 +1,64 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ /** What NavList's onNavigate receives: the chosen row's href, or none for an action row. */
3
+ export interface NavChoice {
4
+ href?: string;
5
+ }
6
+ export interface NavListProps {
7
+ /** Names the navigation for screen readers, such as "Main" or "Settings". */
8
+ label: string;
9
+ /** NavItem and NavSection elements, in order. */
10
+ children: ReactNode;
11
+ /** Placement only (width, margins, grid area); the rows' look comes from the component. */
12
+ className?: string;
13
+ /** The `<nav>` element. */
14
+ ref?: Ref<HTMLElement>;
15
+ /**
16
+ * Called when any row is chosen (click, Enter or Space; link and action rows), before it navigates or acts. A notification,
17
+ * such as closing a phone sheet: it cannot cancel. Locked rows and modified clicks (a new tab or window) do not call it.
18
+ */
19
+ onNavigate?: (choice: NavChoice) => void;
20
+ }
21
+ /**
22
+ * A sidebar's list of destinations: a `<nav>` with a list of NavItem rows, optionally grouped by NavSection. The row nearest
23
+ * the pointer is lit (fluid hover), and the current page's marker glides when the page changes.
24
+ */
25
+ export declare function NavList({ label, children, className, ref, onNavigate }: NavListProps): import("react").JSX.Element;
26
+ export interface NavSectionProps {
27
+ /** The section's heading, such as "Workspace". */
28
+ label: string;
29
+ /** The section's NavItem rows. */
30
+ children: ReactNode;
31
+ }
32
+ /** A labelled group of NavItem rows inside a NavList. */
33
+ export declare function NavSection({ label, children }: NavSectionProps): import("react").JSX.Element;
34
+ interface NavItemBase {
35
+ /** The row's label. */
36
+ children: ReactNode;
37
+ /** A 16px icon before the label. */
38
+ icon?: ReactNode;
39
+ /** The current page: sets aria-current="page" and the active marker. */
40
+ active?: boolean;
41
+ /** Turns the row off. It stays focusable (aria-disabled) so keyboard users can reach it and hear why. */
42
+ disabled?: boolean;
43
+ /** Why the row is off, such as "Pro". Shown as a badge and read as the row's description. Only shown when disabled. */
44
+ reason?: string;
45
+ /** Content at the row's end, such as a count or an avatar. Hidden while a reason shows. */
46
+ trailing?: ReactNode;
47
+ /**
48
+ * Called each time the pointer enters the row or it receives focus, before it is chosen: start loading the destination
49
+ * (prefetch). It can fire more than once, so keep it idempotent. Not called while disabled.
50
+ */
51
+ onIntent?: () => void;
52
+ /** Placement only; the row's look comes from the component. */
53
+ className?: string;
54
+ }
55
+ export type NavItemProps = NavItemBase & ({
56
+ href: string;
57
+ onSelect?: never;
58
+ } | {
59
+ href?: never; /** An action, such as opening search. */
60
+ onSelect: () => void;
61
+ });
62
+ /** One row of a NavList: a link by `href`, or an action by `onSelect`. */
63
+ export declare function NavItem({ children, icon, active, disabled, reason, trailing, onIntent, className, href, onSelect }: NavItemProps): import("react").JSX.Element;
64
+ export default NavList;
@@ -0,0 +1,96 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { FluidHoverHighlight, useFluidHover } from "../../lib/fluid-hover.js";
5
+ import { MenuHighlight } from "../../lib/menu-highlight.js";
6
+ import { SiliconLink } from "../../lib/silicon-provider.js";
7
+ import { Badge } from "../badge/badge.js";
8
+ import styles from "./nav-list.module.css";
9
+ const ITEM = "[data-nav-item]";
10
+ const boxOf = (item) => ({ top: item.offsetTop, height: item.offsetHeight, danger: false });
11
+ /** Lets NavItem reach its list without prop drilling: the active marker's position, and reporting a choice. */
12
+ const NavContext = createContext(null);
13
+ /**
14
+ * A sidebar's list of destinations: a `<nav>` with a list of NavItem rows, optionally grouped by NavSection. The row nearest
15
+ * the pointer is lit (fluid hover), and the current page's marker glides when the page changes.
16
+ */
17
+ export function NavList({ label, children, className, ref, onNavigate }) {
18
+ const root = useRef(null);
19
+ const [active, setActive] = useState(null);
20
+ // The row nearest the pointer is lit, gaps included, and a click in a gap follows the row (lib/fluid-hover).
21
+ const hover = useFluidHover(root, { items: ITEM });
22
+ // The latest handler, read at choice time, so a new function each render never changes the context.
23
+ const navigateHandler = useRef(onNavigate);
24
+ useEffect(() => { navigateHandler.current = onNavigate; });
25
+ const setRoot = useCallback((node) => {
26
+ root.current = node;
27
+ if (typeof ref === "function")
28
+ return ref(node);
29
+ if (ref)
30
+ ref.current = node;
31
+ }, [ref]);
32
+ // The active marker follows the item with aria-current. It glides only when the page changes, never on first paint.
33
+ const measureActive = useCallback(() => {
34
+ const item = root.current?.querySelector(`${ITEM}[aria-current="page"]`);
35
+ setActive(current => {
36
+ if (!item)
37
+ return null;
38
+ const next = boxOf(item);
39
+ return current && current.top === next.top && current.height === next.height ? current : { ...next, glide: current !== null };
40
+ });
41
+ }, []);
42
+ useLayoutEffect(() => {
43
+ measureActive();
44
+ const node = root.current;
45
+ if (!node || typeof ResizeObserver === "undefined")
46
+ return;
47
+ // A resize (a late font, a collapsing section) moves the marker at once; only a page change glides.
48
+ const observer = new ResizeObserver(() => {
49
+ const item = node.querySelector(`${ITEM}[aria-current="page"]`);
50
+ setActive(current => {
51
+ if (!item)
52
+ return null;
53
+ const next = boxOf(item);
54
+ return current && current.top === next.top && current.height === next.height ? current : { ...next, glide: false };
55
+ });
56
+ });
57
+ observer.observe(node);
58
+ return () => observer.disconnect();
59
+ }, [measureActive]);
60
+ const context = useMemo(() => ({ activeChanged: measureActive, navigate: (choice) => navigateHandler.current?.(choice) }), [measureActive]);
61
+ return (_jsx(NavContext.Provider, { value: context, children: _jsxs("nav", { ref: setRoot, "aria-label": label, className: [styles.list, className].filter(Boolean).join(" "), ...hover.handlers, children: [_jsx(FluidHoverHighlight, { hover: hover, className: styles.highlight }), _jsx(MenuHighlight, { state: active, className: styles.active }), _jsx("ul", { className: styles.items, children: children })] }) }));
62
+ }
63
+ /** A labelled group of NavItem rows inside a NavList. */
64
+ export function NavSection({ label, children }) {
65
+ const id = useId();
66
+ return (_jsxs("li", { className: styles.section, children: [_jsx("div", { id: id, className: styles.sectionLabel, children: label }), _jsx("ul", { className: styles.items, "aria-labelledby": id, children: children })] }));
67
+ }
68
+ /** One row of a NavList: a link by `href`, or an action by `onSelect`. */
69
+ export function NavItem({ children, icon, active = false, disabled = false, reason, trailing, onIntent, className, href, onSelect }) {
70
+ const nav = useContext(NavContext);
71
+ const reasonId = useId();
72
+ const showReason = disabled && reason;
73
+ useLayoutEffect(() => { nav?.activeChanged(); }, [active, nav]);
74
+ const content = _jsxs(_Fragment, { children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }) : null, _jsx("span", { className: styles.label, children: children }), showReason ? _jsx(Badge, { id: reasonId, size: "sm", className: styles.reason, "aria-hidden": "true", children: reason })
75
+ : trailing ? _jsx("span", { className: styles.trailing, children: trailing }) : null] });
76
+ const shared = {
77
+ className: [styles.item, className].filter(Boolean).join(" "),
78
+ "data-nav-item": "",
79
+ "aria-current": active ? "page" : undefined,
80
+ "aria-disabled": disabled || undefined,
81
+ "aria-describedby": showReason ? reasonId : undefined,
82
+ };
83
+ if (disabled) {
84
+ // No href and no action: it can be focused and read, but not followed.
85
+ return _jsx("li", { children: _jsx("span", { ...shared, role: href ? "link" : "button", tabIndex: 0, onClick: (event) => event.preventDefault(), children: content }) });
86
+ }
87
+ const intent = onIntent ? { onPointerEnter: onIntent, onFocus: onIntent } : {};
88
+ if (href !== undefined) {
89
+ // A modified click opens a new tab or window, so this page stays where it is: no choice to report.
90
+ const onClick = (event) => { if (event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey)
91
+ nav?.navigate({ href }); };
92
+ return _jsx("li", { children: _jsx(SiliconLink, { ...shared, ...intent, href: href, onClick: onClick, children: content }) });
93
+ }
94
+ return _jsx("li", { children: _jsx("button", { ...shared, ...intent, type: "button", onClick: () => { nav?.navigate({}); onSelect?.(); }, children: content }) });
95
+ }
96
+ export default NavList;
@@ -0,0 +1,25 @@
1
+ /* The list is the positioning box for both markers, so rows measure their offsetTop against it. Place it on --background or
2
+ --surface; on a --surface-muted panel the hover fill has nothing to show against. */
3
+ .list { position: relative; display: block; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
4
+ .items { display: grid; gap: var(--space-hair); margin: 0; padding: 0; list-style: none; }
5
+ .section + .section, .items > li + .section { margin-top: var(--space-group); }
6
+ .sectionLabel { padding: var(--space-tight) var(--space-related); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); }
7
+
8
+ /* Two markers under the rows: the current page (stronger), and the pointer's hover, which glides between rows. */
9
+ .active, .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); pointer-events: none; }
10
+ .active { background: var(--surface-pressed); }
11
+ .highlight { background: var(--surface-muted); }
12
+
13
+ .item { position: relative; display: flex; box-sizing: border-box; width: 100%; min-height: 2rem; align-items: center; gap: var(--space-related); margin: 0; padding: 0 var(--space-related); border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); text-align: left; text-decoration: none; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
14
+ .item:hover, .item:focus-visible, .item[aria-current="page"] { color: var(--foreground); }
15
+ .item[aria-current="page"] { font-weight: 500; }
16
+ .item[aria-disabled="true"] { color: var(--text-muted); cursor: not-allowed; }
17
+ .icon { display: inline-flex; flex: none; color: var(--text-secondary); }
18
+ .icon svg { width: 16px; height: 16px; }
19
+ .item[aria-current="page"] .icon, .item:hover .icon { color: var(--foreground); }
20
+ .item[aria-disabled="true"] .icon { color: var(--text-muted); }
21
+ .label { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
22
+ .trailing { display: inline-flex; flex: none; align-items: center; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
23
+ .reason { flex: none; }
24
+
25
+ @media (prefers-reduced-motion: reduce) { .item { transition: none; } }
@@ -2,8 +2,8 @@
2
2
  .trigger[data-state="open"], .trigger:active { border-color: var(--border-strong); background: var(--surface-muted); }
3
3
  @media (hover: hover) and (pointer: fine) { .trigger:hover { border-color: var(--border-strong); background: var(--surface-muted); } }
4
4
  .trigger > span:first-child { display: grid; place-items: center; }
5
- .badge { position: absolute; top: -6px; right: -6px; display: grid; min-width: 20px; height: 20px; padding: 0 5px; place-items: center; overflow: hidden; border: 2px solid var(--surface); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
6
- .panel { z-index: 90; display: flex; flex-direction: column; width: min(424px, calc(100vw - 24px)); max-height: min(590px, calc(100vh - 24px)); overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); font-family: var(--font-body); letter-spacing: var(--tracking-body); transform-origin: var(--radix-popover-content-transform-origin); }
5
+ .badge { position: absolute; top: calc(var(--space-related) * -1); right: calc(var(--space-related) * -1); display: grid; min-width: 20px; height: 20px; padding: 0 var(--space-tight); place-items: center; overflow: hidden; border: 2px solid var(--surface); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
6
+ .panel { z-index: var(--layer-popover); display: flex; flex-direction: column; width: min(424px, calc(100vw - 24px)); max-height: min(590px, calc(100vh - 24px)); overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); font-family: var(--font-body); letter-spacing: var(--tracking-body); transform-origin: var(--radix-popover-content-transform-origin); }
7
7
  /* The panel grows out of the bell: it starts a few pixels toward the trigger on whichever side Radix placed it, and leaves faster than it arrives. */
8
8
  .panel { --panel-x: 0px; --panel-y: -6px; }
9
9
  .panel[data-side="top"] { --panel-y: 6px; }
@@ -11,61 +11,61 @@
11
11
  .panel[data-side="right"] { --panel-x: -6px; --panel-y: 0px; }
12
12
  .panel[data-state="open"] { animation: panel-open var(--duration-standard) var(--ease-enter) both; }
13
13
  .panel[data-state="closed"] { animation: panel-close var(--duration-instant) var(--ease-standard) both; }
14
- .header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 23px 24px 18px; }
14
+ .header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-group); padding: var(--space-section) var(--space-section) var(--space-comfort); }
15
15
  .heading { display: flex; align-items: center; gap: var(--space-related); }
16
16
  .heading h2 { margin: 0; font-size: var(--text-xl); font-weight: 500; letter-spacing: var(--tracking-body); line-height: 1.2; }
17
- .count { display: grid; min-width: 21px; height: 21px; padding: 0 5px; place-items: center; overflow: hidden; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); line-height: 1; font-variant-numeric: tabular-nums; }
18
- .header p { position: relative; margin: 4px 0 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
17
+ .count { display: grid; min-width: 21px; height: 21px; padding: 0 var(--space-tight); place-items: center; overflow: hidden; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); line-height: 1; font-variant-numeric: tabular-nums; }
18
+ .header p { position: relative; margin: var(--space-tight) 0 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
19
19
  .close { display: grid; width: 30px; height: 30px; flex: none; place-items: center; border: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--text-muted); cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard); }
20
20
  .close:active { transform: scale(.96); }
21
- .toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 52px; padding: 0 17px 13px 20px; border-bottom: 1px solid var(--border-subtle); }
22
- .viewSwitch { display: inline-flex; align-items: center; gap: 3px; }
23
- .viewSwitch button { position: relative; isolation: isolate; min-width: 61px; height: 31px; padding: 0 10px; border: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }
21
+ .toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-group); min-height: 52px; padding: 0 var(--space-inset) var(--space-group) var(--space-comfort); border-bottom: 1px solid var(--border-subtle); }
22
+ .viewSwitch { display: inline-flex; align-items: center; gap: var(--space-tight); }
23
+ .viewSwitch button { position: relative; isolation: isolate; min-width: 61px; height: 31px; padding: 0 var(--space-group); border: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }
24
24
  .viewSwitch .viewActive { color: var(--foreground); }
25
25
  .viewSwitch button > span:last-child { position: relative; z-index: 1; }
26
26
  .viewHighlight { position: absolute; z-index: -1; inset: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-muted); }
27
- .markAll { display: inline-flex; align-items: center; gap: 6px; padding: 6px 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); white-space: nowrap; cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
27
+ .markAll { display: inline-flex; align-items: center; gap: var(--space-related); padding: var(--space-related) 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); white-space: nowrap; cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
28
28
  .markAll:active { transform: scale(.97); }
29
- .list { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 9px 11px 10px; scrollbar-width: none; }
29
+ .list { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-related) var(--space-group) var(--space-group); scrollbar-width: none; }
30
30
  .list::-webkit-scrollbar { display: none; }
31
31
  .row { overflow: hidden; }
32
32
  /* Rows settle in with a short cascade when the panel opens or a view adds them. */
33
- .item { position: relative; padding: 10px; border-radius: calc(var(--radius-container) - 12px); corner-shape: var(--corner-shape); transition: background var(--duration-fast) var(--ease-standard); animation: item-in var(--duration-standard) var(--ease-enter) calc(var(--index, 0) * 35ms) both; }
33
+ .item { position: relative; padding: var(--space-group); border-radius: calc(var(--radius-container) - var(--space-group)); corner-shape: var(--corner-shape); transition: background var(--duration-fast) var(--ease-standard); animation: item-in var(--duration-standard) var(--ease-enter) calc(var(--index, 0) * 35ms) both; }
34
34
  .itemExpanded { background: var(--surface-muted); }
35
35
  .itemRead .itemTitle strong { color: var(--text-secondary); }
36
- .itemMain { display: flex; align-items: flex-start; gap: 11px; min-width: 0; }
36
+ .itemMain { display: flex; align-items: flex-start; gap: var(--space-group); min-width: 0; }
37
37
  .eventIcon { display: grid; width: 36px; height: 36px; flex: none; place-items: center; color: var(--accent); }
38
38
  .eventIcon.success { color: var(--success); }
39
39
  .eventIcon.warning { color: var(--warning); }
40
- .itemToggle { display: grid; min-width: 0; flex: 1; gap: 4px; padding: 2px 0 0; border: 0; background: transparent; color: var(--foreground); text-align: left; font: inherit; cursor: pointer; }
41
- .itemTitle { display: flex; align-items: center; gap: 6px; min-width: 0; min-height: 20px; }
40
+ .itemToggle { display: grid; min-width: 0; flex: 1; gap: var(--space-tight); padding: var(--space-hair) 0 0; border: 0; background: transparent; color: var(--foreground); text-align: left; font: inherit; cursor: pointer; }
41
+ .itemTitle { display: flex; align-items: center; gap: var(--space-related); min-width: 0; min-height: 20px; }
42
42
  .itemTitle strong { overflow: hidden; font-size: var(--text-sm); font-weight: 500; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; }
43
43
  .unreadDot { display: block; width: 6px; height: 6px; flex: none; border-radius: 50%; corner-shape: round; background: var(--foreground); }
44
44
  .itemPreview { overflow: hidden; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
45
- .time { flex: none; padding-top: 4px; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; white-space: nowrap; }
46
- .details { overflow: hidden; padding-left: 47px; }
47
- .details p { margin: 12px 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
48
- .itemActions { display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 5px; }
49
- .itemActions button { display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 10px; overflow: clip; border: 1px solid var(--border); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-xs); line-height: 1; white-space: nowrap; cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), border-color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard); }
45
+ .time { flex: none; padding-top: var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; white-space: nowrap; }
46
+ .details { overflow: hidden; padding-left: var(--space-page); }
47
+ .details p { margin: var(--space-group) 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
48
+ .itemActions { display: flex; flex-wrap: wrap; gap: var(--space-related); padding-bottom: var(--space-tight); }
49
+ .itemActions button { display: inline-flex; align-items: center; gap: var(--space-tight); height: 30px; padding: 0 var(--space-group); overflow: clip; border: 1px solid var(--border); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-xs); line-height: 1; white-space: nowrap; cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), border-color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard); }
50
50
  .itemActions button:active { transform: scale(.97); }
51
51
  /* Quick press, spring release: the resting transition above carries the spring back. */
52
52
  .close:active, .markAll:active, .itemActions button:active, .footer button:active { transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
53
53
  .morph { display: inline-flex; align-items: center; }
54
- .morphContent { position: relative; display: inline-flex; width: max-content; align-items: center; gap: 5px; }
54
+ .morphContent { position: relative; display: inline-flex; width: max-content; align-items: center; gap: var(--space-tight); }
55
55
  .actionIcon, .actionGlyph { position: relative; display: grid; place-items: center; }
56
56
  .swap { position: relative; display: inline-block; max-width: 100%; vertical-align: top; }
57
57
  .swapLine { display: block; }
58
58
  .roll { position: relative; display: inline-grid; vertical-align: top; }
59
59
  .rollValue { display: block; }
60
60
  .emptyFrame { overflow: hidden; }
61
- .empty { display: grid; justify-items: center; align-content: center; min-height: 190px; padding: 22px 12px; text-align: center; }
62
- .empty > svg { margin-bottom: 13px; color: var(--text-muted); }
61
+ .empty { display: grid; justify-items: center; align-content: center; min-height: 190px; padding: var(--space-section) var(--space-group); text-align: center; }
62
+ .empty > svg { margin-bottom: var(--space-group); color: var(--text-muted); }
63
63
  .empty strong { font-size: var(--text-sm); font-weight: 500; }
64
- .empty p { margin: 4px 0 0; color: var(--text-secondary); font-size: var(--text-xs); }
65
- .empty button { margin-top: 16px; padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-xs); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
64
+ .empty p { margin: var(--space-tight) 0 0; color: var(--text-secondary); font-size: var(--text-xs); }
65
+ .empty button { margin-top: var(--space-inset); padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-xs); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
66
66
  .footerFrame { flex: none; overflow: hidden; }
67
- .footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 46px; padding: 8px 22px; border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: var(--text-xs); }
68
- .footer button { padding: 4px 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
67
+ .footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-group); min-height: 46px; padding: var(--space-related) var(--space-section); border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: var(--text-xs); }
68
+ .footer button { padding: var(--space-tight) 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
69
69
  .footer button:active { transform: scale(.97); }
70
70
  @media (hover: hover) and (pointer: fine) {
71
71
  .close:hover, .item:hover { background: var(--surface-muted); }
@@ -75,5 +75,5 @@
75
75
  @keyframes panel-open { from { opacity: 0; transform: translate(var(--panel-x), var(--panel-y)) scale(.97); } }
76
76
  @keyframes panel-close { to { opacity: 0; transform: translate(calc(var(--panel-x) / 2), calc(var(--panel-y) / 2)) scale(.98); } }
77
77
  @keyframes item-in { from { opacity: 0; transform: translateY(4px); } }
78
- @media (max-width: 380px) { .header { padding: 20px 16px 16px; } .heading h2 { font-size: var(--text-lg); } .toolbar { align-items: flex-start; padding: 0 12px 12px; } .list { padding: 8px 5px; } .item { padding-inline: 7px; } .itemMain { gap: 7px; } .details { padding-left: 43px; } .itemActions { gap: 5px; } .itemActions button { padding-inline: 7px; } .markAll span { display: none; } .footer { padding-inline: 16px; } }
78
+ @media (max-width: 380px) { .header { padding: var(--space-comfort) var(--space-inset) var(--space-inset); } .heading h2 { font-size: var(--text-lg); } .toolbar { align-items: flex-start; padding: 0 var(--space-group) var(--space-group); } .list { padding: var(--space-related) var(--space-tight); } .item { padding-inline: var(--space-related); } .itemMain { gap: var(--space-related); } .details { padding-left: var(--space-page); } .itemActions { gap: var(--space-tight); } .itemActions button { padding-inline: var(--space-related); } .markAll span { display: none; } .footer { padding-inline: var(--space-inset); } }
79
79
  @media (prefers-reduced-motion: reduce) { .panel[data-state], .trigger, .close, .viewSwitch button, .markAll, .item, .itemActions button, .footer button { animation: none; transition-duration: 0ms; } .close:active, .markAll:active, .itemActions button:active, .footer button:active { transform: none; } }
@@ -42,7 +42,7 @@ function partsOf(value, format) {
42
42
  Scale and opacity ride the same spring as width, so a digit grows with its slot instead of landing on its neighbours. */
43
43
  const slot = {
44
44
  enter: (direction) => ({ width: 0, scale: .6, opacity: 0, y: `${direction * .3}em`, filter: `blur(${blur.soft}px)` }),
45
- center: { width: "auto", scale: 1, opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { width: spring.morph, scale: spring.morph, opacity: spring.morph, y: spring.snappy, filter: { duration: .3, ease: exitEase } } },
45
+ center: { width: "auto", scale: 1, opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { width: spring.morph, scale: spring.morph, opacity: spring.morph, y: spring.snappy, filter: { duration: motionTokens.duration.standard, ease: exitEase } } },
46
46
  exit: (direction) => ({ width: 0, scale: .6, opacity: 0, y: `${direction * -.3}em`, filter: `blur(${blur.subtle}px)`, transition: { width: spring.smooth, scale: spring.smooth, y: { duration: duration.fast, ease: exitEase }, opacity: { duration: duration.instant }, filter: { duration: duration.instant } } }),
47
47
  };
48
48
  /* Reduced motion keeps a short fade and no travel. While typing, columns follow the draft at once so they never lag the caret.
@@ -99,8 +99,8 @@ const directionsBetween = (from, to) => { const before = from.split(" "); return
99
99
  const wordRise = {
100
100
  enter: (direction) => ({ opacity: 0, y: `${direction * .3}em`, filter: `blur(${blur.soft}px)` }),
101
101
  // Opacity follows the slot's width spring, so a longer word never shows before its room has opened.
102
- center: { opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { duration: .22, ease: enterEase, opacity: spring.morph } },
103
- exit: (direction) => ({ opacity: 0, y: `${direction * -.3}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: .14, ease: exitEase } }),
102
+ center: { opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { duration: motionTokens.duration.exit, ease: enterEase, opacity: spring.morph } },
103
+ exit: (direction) => ({ opacity: 0, y: `${direction * -.3}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: exitEase } }),
104
104
  };
105
105
  const wordFade = { enter: { opacity: 0, y: 0, filter: "none" }, center: { opacity: 1, y: 0, filter: "none", transition: { duration: duration.instant } }, exit: { opacity: 0, transition: { duration: 0 } } };
106
106
  /** A word's width follows its new copy on a spring, so a price gaining a digit eases the rest of the line along instead of shoving it. */
@@ -12,7 +12,7 @@
12
12
  .label[data-scrub] { cursor: ew-resize; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
13
13
  /* A hairline shell. Focus darkens the border only (the library draws no rings); a typed value past a limit tints it with the warning
14
14
  role instead, calm rather than alarming. Set --number-field-width to resize the shell. */
15
- .control { display: flex; align-items: center; width: min(100%, var(--nf-width)); min-height: var(--nf-height); gap: 2px; border: 1px solid var(--border); border-radius: var(--radius-control); padding: var(--nf-inset); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
15
+ .control { display: flex; align-items: center; width: min(100%, var(--nf-width)); min-height: var(--nf-height); gap: var(--space-hair); border: 1px solid var(--border); border-radius: var(--radius-control); padding: var(--nf-inset); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
16
16
  @media (hover: hover) and (pointer: fine) { .control:hover, .field:has(.label[data-scrub]:hover) .control { border-color: var(--border-strong); } }
17
17
  .control[data-scrubbing] { border-color: var(--foreground); }
18
18
  .control:has(input:focus) { border-color: var(--foreground); }
@@ -20,13 +20,13 @@
20
20
  .control[data-warn]:has(input:focus) { border-color: color-mix(in oklab, var(--warning) 80%, var(--border)); }
21
21
  .control[data-disabled] { opacity: .5; background: var(--surface-muted); }
22
22
  /* Radius steps in by the shell's inset, so the pressed fill sits concentric with the shell. */
23
- .step { display: grid; width: var(--nf-step); height: var(--nf-step); flex: 0 0 var(--nf-step); place-items: center; border: 0; border-radius: calc(var(--radius-control) - var(--nf-inset) - 1px); padding: 0; background: transparent; color: var(--text-secondary); cursor: pointer; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-standard); }
23
+ .step { display: grid; width: var(--nf-step); height: var(--nf-step); flex: 0 0 var(--nf-step); place-items: center; border: 0; border-radius: calc(var(--radius-control) - var(--nf-inset) - var(--space-hair)); padding: 0; background: transparent; color: var(--text-secondary); cursor: pointer; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-standard); }
24
24
  .icon { display: grid; place-items: center; }
25
25
  .icon svg { transition: transform var(--duration-spring) var(--ease-spring); }
26
26
  @media (hover: hover) and (pointer: fine) { .step:hover:not(:disabled, [aria-disabled="true"]) { background: var(--surface-muted); color: var(--foreground); } }
27
27
  /* Quick press, spring release: the fill deepens and the glyph sinks. A held button stays pressed for as long as it repeats. */
28
28
  .step:is(:active, [data-pressed]):not(:disabled, [aria-disabled="true"]) { background: color-mix(in oklab, var(--foreground) 10%, var(--surface)); color: var(--foreground); transition: background-color 60ms linear, color 60ms linear; }
29
- .step:is(:active, [data-pressed]):not(:disabled, [aria-disabled="true"]) svg { transform: scale(.82); transition: transform 90ms var(--ease-standard); }
29
+ .step:is(:active, [data-pressed]):not(:disabled, [aria-disabled="true"]) svg { transform: scale(.82); transition: transform var(--duration-fast) var(--ease-standard); }
30
30
  .step:focus-visible { outline: none; background: var(--surface-muted); color: var(--foreground); }
31
31
  /* At a limit the button dims and refuses, but stays in the tab order; pressing it strains the value toward the limit and it shakes once. */
32
32
  .step[aria-disabled="true"] { opacity: .32; cursor: not-allowed; }
@@ -1,7 +1,7 @@
1
1
  /* No row gap: grid tracks clamp a negative margin at zero, so a closed message row would still pay the gap. Each row carries its own space instead. */
2
2
  .field { display: grid; min-width: 0; }
3
- .label { margin-bottom: 8px; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
4
- .inputs { position: relative; display: flex; width: fit-content; max-width: 100%; gap: 8px; }
3
+ .label { margin-bottom: var(--space-related); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
4
+ .inputs { position: relative; display: flex; width: fit-content; max-width: 100%; gap: var(--space-related); }
5
5
  .slot { position: relative; display: grid; width: 42px; height: 48px; min-width: 0; flex: 0 1 42px; }
6
6
  /* The native text is transparent (caret stays visible); .glyph draws the digit so it can rise in. */
7
7
  .input { width: 100%; height: 100%; min-width: 0; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface); color: transparent; caret-color: var(--accent); font: inherit; font-size: var(--text-lg); font-variant-numeric: tabular-nums; text-align: center; outline: none; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
@@ -17,12 +17,12 @@
17
17
  .inputs[data-invalid="true"] .ring { border-color: var(--danger); }
18
18
  /* The row's space is padding inside the clipped slot, so the height animates from a true zero. */
19
19
  .messageSlot { display: block; overflow: hidden; }
20
- .description, .error { display: block; padding-top: 8px; font-size: var(--text-xs); line-height: var(--leading-body); }
20
+ .description, .error { display: block; padding-top: var(--space-related); font-size: var(--text-xs); line-height: var(--leading-body); }
21
21
  .description { color: var(--text-muted); }.error { color: var(--danger); }
22
22
  /* Words are measured against this box when they pop out to leave, so it must be the positioned parent. */
23
23
  .words { position: relative; display: block; }
24
24
  .word { display: inline-block; white-space: pre; }
25
25
  .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
26
- @media (max-width: 360px) { .inputs { gap: 6px; }.slot { height: 44px; } }
26
+ @media (max-width: 360px) { .inputs { gap: var(--space-related); }.slot { height: 44px; } }
27
27
  @media (forced-colors: active) { .ring { display: none; } .input:focus-visible { outline: 2px solid CanvasText; outline-offset: 2px; } .input { color: CanvasText; } .glyph { display: none; } }
28
28
  @media (prefers-reduced-motion: reduce) { .input, .ring { transition: none; } }
@@ -7,7 +7,7 @@
7
7
  .nav:not([data-mark-ready]) button[aria-current="page"] { border-color: var(--border); background: var(--surface); box-shadow: var(--shadow-resting); }
8
8
  /* Only the arrows press in. Page numbers move with Motion, so they keep CSS off transform and opacity. */
9
9
  .nav .step { transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
10
- .nav .step:active:not(:disabled) { transform: scale(.96); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-fast), 100ms; transition-timing-function: var(--ease-standard), var(--ease-standard), var(--ease-standard), ease-out; }
10
+ .nav .step:active:not(:disabled) { transform: scale(.96); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-fast), var(--duration-fast); transition-timing-function: var(--ease-standard), var(--ease-standard), var(--ease-standard), var(--ease-standard); }
11
11
  .nav button:disabled { opacity: .4; cursor: not-allowed; }
12
12
  @media (hover: hover) and (pointer: fine) {
13
13
  .nav button:hover:not(:disabled) { background: var(--surface-muted); color: var(--foreground); }
@@ -2,7 +2,7 @@
2
2
  .field { display: grid; min-width: 0; }
3
3
  .field label { margin-bottom: var(--space-related); font-size: var(--text-sm); font-weight: 500; }
4
4
  /* Focus shows as a darker border only; the shell never changes size. The toggle sits with an even inset on top, bottom and right. */
5
- .shell { display: flex; min-height: var(--control-height-md); align-items: center; border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 0 calc((var(--control-height-md) - 2px - 30px) / 2) 0 var(--space-group); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
5
+ .shell { display: flex; min-height: var(--control-height-md); align-items: center; border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 0 calc((var(--control-height-md) - var(--space-hair) - var(--space-block)) / 2) 0 var(--space-group); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
6
6
  @media (hover: hover) and (pointer: fine) { .shell:hover:not(:focus-within) { border-color: var(--foreground); } }
7
7
  .shell:focus-within { border-color: var(--foreground); }
8
8
  .shell:has(.input[aria-invalid="true"]) { border-color: var(--danger); }
@@ -15,7 +15,7 @@
15
15
  @keyframes resolve-hidden { from { opacity: .35; filter: blur(2px); } }
16
16
  .shell button { display: grid; width: 30px; height: 30px; flex: 0 0 auto; place-items: center; border: 0; border-radius: var(--radius-control); background: transparent; color: var(--text-muted); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
17
17
  @media (hover: hover) and (pointer: fine) { .shell button:hover { background: var(--surface-muted); color: var(--foreground); } }
18
- .shell button:active { transform: scale(.96); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform 100ms var(--ease-standard); }
18
+ .shell button:active { transform: scale(.96); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard); }
19
19
  .shell button[aria-pressed="true"] { color: var(--foreground); }
20
20
  /* The row's space is padding inside the clipped slot, so the height animates from a true zero. */
21
21
  .messageSlot { display: block; overflow: hidden; }
@@ -5,7 +5,7 @@
5
5
  .field[data-level="3"], .field[data-level="4"] { --tone: var(--success); }
6
6
  .label { margin-bottom: var(--space-related); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
7
7
  /* The shell only transitions color; its sideways shake is a spring on transform, so the two never fight. */
8
- .shell { display: flex; min-height: var(--control-height-md); align-items: center; gap: var(--space-tight); padding: 0 5px 0 var(--space-inset); border: 1px solid var(--border-strong); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
8
+ .shell { display: flex; min-height: var(--control-height-md); align-items: center; gap: var(--space-tight); padding: 0 var(--space-tight) 0 var(--space-inset); border: 1px solid var(--border-strong); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
9
9
  @media (hover: hover) and (pointer: fine) { .shell:hover:not(:focus-within) { border-color: var(--text-muted); } }
10
10
  .shell:focus-within { border-color: var(--foreground); }
11
11
  .shell:has(.input[aria-invalid="true"]) { border-color: var(--danger); }
@@ -18,31 +18,31 @@
18
18
  .input[data-reveal="hidden"] { animation: resolve-hidden var(--duration-standard) var(--ease-enter); }
19
19
  @keyframes resolve-shown { from { opacity: .35; filter: blur(2px); } }
20
20
  @keyframes resolve-hidden { from { opacity: .35; filter: blur(2px); } }
21
- .reveal { display: grid; width: 34px; height: 34px; flex: 0 0 auto; place-items: center; padding: 0; border: 0; border-radius: calc(var(--radius-control) - 5px); corner-shape: var(--corner-shape); background: transparent; color: var(--text-muted); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
21
+ .reveal { display: grid; width: 34px; height: 34px; flex: 0 0 auto; place-items: center; padding: 0; border: 0; border-radius: calc(var(--radius-control) - var(--space-tight)); corner-shape: var(--corner-shape); background: transparent; color: var(--text-muted); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
22
22
  @media (hover: hover) and (pointer: fine) { .reveal:hover { background: var(--surface-muted); color: var(--foreground); } }
23
- .reveal:active { transform: scale(.94); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform 100ms var(--ease-standard); }
23
+ .reveal:active { transform: scale(.94); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard); }
24
24
  .reveal[aria-pressed="true"] { color: var(--foreground); }
25
25
  /* The row's space is padding inside the clipped slot, so its height opens from a true zero. */
26
26
  .errorSlot { display: block; overflow: hidden; }
27
27
  .error { position: relative; display: block; padding-top: var(--space-related); color: var(--danger); font-size: var(--text-xs); line-height: var(--leading-body); }
28
28
  .errorText { display: block; }
29
29
  .meterRow { display: flex; align-items: center; gap: var(--space-group); margin-top: var(--space-group); }
30
- .meter { display: grid; flex: 1 1 auto; min-width: 0; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 5px; }
30
+ .meter { display: grid; flex: 1 1 auto; min-width: 0; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-tight); }
31
31
  /* Each fill slides in from the left inside its clipped track, so the rounded ends keep their shape at every frame. */
32
32
  .segment { position: relative; height: 4px; overflow: hidden; isolation: isolate; border-radius: var(--radius-full); corner-shape: round; background: var(--border); }
33
33
  .fill { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; background: var(--tone); transition: background-color var(--duration-standard) var(--ease-standard); }
34
34
  /* A fixed slot: the meter never resizes when the word changes, and the word stays flush with the field's edge. */
35
35
  .word { position: relative; display: block; flex: 0 0 4.5rem; height: 1.4em; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); text-align: right; white-space: nowrap; }
36
36
  .wordText { display: inline-block; }
37
- .rules { display: grid; gap: 6px; margin: var(--space-inset) 0 0; padding: 0; list-style: none; }
38
- .rule { display: flex; min-height: 1.25rem; align-items: center; gap: 10px; color: var(--text-muted); font-size: var(--text-sm); line-height: var(--leading-body); transition: color var(--duration-standard) var(--ease-standard); }
37
+ .rules { display: grid; gap: var(--space-related); margin: var(--space-inset) 0 0; padding: 0; list-style: none; }
38
+ .rule { display: flex; min-height: 1.25rem; align-items: center; gap: var(--space-group); color: var(--text-muted); font-size: var(--text-sm); line-height: var(--leading-body); transition: color var(--duration-standard) var(--ease-standard); }
39
39
  .rule[data-met] { color: var(--foreground); }
40
40
  .ruleLabel { min-width: 0; }
41
41
  /* The outline hands over to a faint tint while the tick draws on top; most of the color sits on the tick. */
42
42
  .mark { position: relative; display: block; width: 16px; height: 16px; flex: 0 0 16px; border: 1.25px solid var(--border-strong); border-radius: var(--radius-full); corner-shape: round; color: var(--success); transition: border-color var(--duration-fast) var(--ease-standard); }
43
43
  .rule[data-met] .mark { border-color: transparent; }
44
- .markFill { position: absolute; inset: -1.25px; border-radius: inherit; corner-shape: inherit; background: color-mix(in oklab, var(--success) 16%, transparent); }
45
- .tick { position: absolute; inset: -1.25px; width: 16px; height: 16px; }
44
+ .markFill { position: absolute; inset: calc(var(--space-hair) * -1); border-radius: inherit; corner-shape: inherit; background: color-mix(in oklab, var(--success) 16%, transparent); }
45
+ .tick { position: absolute; inset: calc(var(--space-hair) * -1); width: 16px; height: 16px; }
46
46
  .remaining { position: relative; display: block; margin-left: auto; padding-left: var(--space-related); color: var(--text-muted); font-size: var(--text-xs); white-space: nowrap; }
47
47
  .remainingText { display: inline-flex; align-items: baseline; gap: .28em; }
48
48
  .number { display: inline-flex; font-variant-numeric: tabular-nums; }