@teamofsilicons/silicon-ui 0.2.0 → 0.3.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 (112) 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/announcement-bar/announcement-bar.module.css +6 -6
  6. package/components/avatar/avatar.d.ts +2 -2
  7. package/components/avatar/avatar.js +1 -1
  8. package/components/avatar/avatar.module.css +5 -2
  9. package/components/avatar-group/avatar-group.module.css +4 -4
  10. package/components/badge/badge.module.css +4 -4
  11. package/components/bar-chart/bar-chart.module.css +4 -4
  12. package/components/billing-toggle/billing-toggle.module.css +10 -10
  13. package/components/bottom-sheet/bottom-sheet.module.css +4 -5
  14. package/components/breadcrumb/breadcrumb.d.ts +8 -2
  15. package/components/breadcrumb/breadcrumb.js +21 -9
  16. package/components/breadcrumb/breadcrumb.module.css +7 -3
  17. package/components/brush-chart/brush-chart.module.css +9 -9
  18. package/components/button/button.d.ts +7 -2
  19. package/components/button/button.js +3 -1
  20. package/components/button/button.module.css +10 -1
  21. package/components/button-group/button-group.module.css +6 -6
  22. package/components/calendar/calendar.module.css +7 -7
  23. package/components/card/card.module.css +2 -2
  24. package/components/card-stack/card-stack.module.css +2 -3
  25. package/components/carousel/carousel.module.css +2 -2
  26. package/components/chat-thread/chat-thread.module.css +41 -41
  27. package/components/checkbox/checkbox.module.css +1 -1
  28. package/components/chip-group/chip-group.module.css +2 -2
  29. package/components/code-block/code-block.module.css +7 -14
  30. package/components/color-picker/color-picker.module.css +20 -21
  31. package/components/combobox/combobox.module.css +2 -2
  32. package/components/command-palette/command-palette.module.css +18 -18
  33. package/components/comment-thread/comment-thread.module.css +35 -35
  34. package/components/confirm-morph/confirm-morph.module.css +9 -9
  35. package/components/context-menu/context-menu.module.css +3 -3
  36. package/components/countdown/countdown.module.css +4 -4
  37. package/components/date-picker/date-picker.module.css +6 -6
  38. package/components/date-range-picker/date-range-picker.module.css +21 -21
  39. package/components/dialog/dialog.module.css +2 -2
  40. package/components/donut-chart/donut-chart.module.css +4 -4
  41. package/components/drawer/drawer.module.css +5 -5
  42. package/components/dropdown-menu/dropdown-menu.d.ts +71 -1
  43. package/components/dropdown-menu/dropdown-menu.js +100 -6
  44. package/components/dropdown-menu/dropdown-menu.module.css +15 -5
  45. package/components/elastic-slider/elastic-slider.module.css +4 -4
  46. package/components/expandable-card/expandable-card.module.css +1 -1
  47. package/components/expanding-button-group/expanding-button-group.module.css +1 -1
  48. package/components/expanding-search/expanding-search.module.css +9 -9
  49. package/components/file-dropzone/file-dropzone.module.css +21 -21
  50. package/components/file-upload/file-upload.module.css +7 -7
  51. package/components/filter-toolbar/filter-toolbar.module.css +9 -9
  52. package/components/floating-button-group/floating-button-group.module.css +6 -6
  53. package/components/gauge/gauge.module.css +2 -2
  54. package/components/hover-card/hover-card.module.css +2 -2
  55. package/components/image-compare/image-compare.module.css +4 -4
  56. package/components/inline-edit/inline-edit.module.css +12 -12
  57. package/components/json-viewer/json-viewer.module.css +12 -12
  58. package/components/line-chart/line-chart.module.css +8 -8
  59. package/components/mention-input/mention-input.module.css +8 -8
  60. package/components/metric-card/metric-card.module.css +1 -1
  61. package/components/money-input/money-input.module.css +9 -9
  62. package/components/morph-select/morph-select.module.css +13 -13
  63. package/components/multi-select/multi-select.module.css +6 -6
  64. package/components/nav-list/nav-list.d.ts +64 -0
  65. package/components/nav-list/nav-list.js +103 -0
  66. package/components/nav-list/nav-list.module.css +25 -0
  67. package/components/notification-center/notification-center.module.css +27 -27
  68. package/components/number-field/number-field.module.css +2 -2
  69. package/components/otp-input/otp-input.module.css +4 -4
  70. package/components/password-field/password-field.module.css +1 -1
  71. package/components/password-strength/password-strength.module.css +7 -7
  72. package/components/phone-input/phone-input.module.css +19 -19
  73. package/components/popover/popover.module.css +2 -2
  74. package/components/progress/progress.module.css +1 -1
  75. package/components/radio-cards/radio-cards.module.css +8 -8
  76. package/components/radio-group/radio-group.module.css +1 -1
  77. package/components/resizable-panels/resizable-panels.module.css +5 -5
  78. package/components/rich-text-editor/rich-text-editor.module.css +12 -12
  79. package/components/ridgeline/ridgeline.module.css +7 -7
  80. package/components/scroll-area/scroll-area.module.css +8 -8
  81. package/components/segmented-control/segmented-control.d.ts +3 -1
  82. package/components/segmented-control/segmented-control.js +2 -2
  83. package/components/segmented-control/segmented-control.module.css +7 -4
  84. package/components/select/select.module.css +5 -5
  85. package/components/shortcut-recorder/shortcut-recorder.module.css +15 -15
  86. package/components/signature-pad/signature-pad.module.css +11 -11
  87. package/components/slider/slider.module.css +4 -4
  88. package/components/slope-chart/slope-chart.module.css +7 -7
  89. package/components/sortable-data-table/sortable-data-table.module.css +4 -4
  90. package/components/split-button/split-button.module.css +5 -5
  91. package/components/stepper/stepper.module.css +3 -3
  92. package/components/streamgraph/streamgraph.module.css +7 -7
  93. package/components/swipe-actions/swipe-actions.module.css +5 -5
  94. package/components/switch/switch.module.css +1 -1
  95. package/components/tabs/tabs.d.ts +9 -2
  96. package/components/tabs/tabs.js +4 -2
  97. package/components/tabs/tabs.module.css +7 -2
  98. package/components/tag-input/tag-input.module.css +4 -4
  99. package/components/time-picker/time-picker.module.css +3 -3
  100. package/components/timeline/timeline.module.css +3 -3
  101. package/components/toast/toast.module.css +5 -5
  102. package/components/toast-stack/toast-stack.module.css +5 -5
  103. package/components/tooltip/tooltip.module.css +1 -1
  104. package/components/tree-view/tree-view.module.css +7 -7
  105. package/components/treemap/treemap.module.css +13 -13
  106. package/components/usage-meter/usage-meter.module.css +5 -5
  107. package/components/user-menu/user-menu.module.css +22 -22
  108. package/components/waffle-chart/waffle-chart.module.css +7 -7
  109. package/components.json +416 -1
  110. package/eslint.js +35 -3
  111. package/package.json +1 -1
  112. package/styles.css +44 -4
@@ -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. One hover highlight
23
+ * glides between rows for the pointer, 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,103 @@
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 { MenuHighlight } from "../../lib/menu-highlight.js";
5
+ import { SiliconLink } from "../../lib/silicon-provider.js";
6
+ import { Badge } from "../badge/badge.js";
7
+ import styles from "./nav-list.module.css";
8
+ const ITEM = "[data-nav-item]";
9
+ const boxOf = (item) => ({ top: item.offsetTop, height: item.offsetHeight, danger: false });
10
+ /** Lets NavItem reach its list without prop drilling: the active marker's position, and reporting a choice. */
11
+ const NavContext = createContext(null);
12
+ /**
13
+ * A sidebar's list of destinations: a `<nav>` with a list of NavItem rows, optionally grouped by NavSection. One hover highlight
14
+ * glides between rows for the pointer, and the current page's marker glides when the page changes.
15
+ */
16
+ export function NavList({ label, children, className, ref, onNavigate }) {
17
+ const root = useRef(null);
18
+ const [hover, setHover] = useState(null);
19
+ const [active, setActive] = useState(null);
20
+ const pointer = useRef(false);
21
+ // The latest handler, read at choice time, so a new function each render never changes the context.
22
+ const navigateHandler = useRef(onNavigate);
23
+ useEffect(() => { navigateHandler.current = onNavigate; });
24
+ const setRoot = useCallback((node) => {
25
+ root.current = node;
26
+ if (typeof ref === "function")
27
+ return ref(node);
28
+ if (ref)
29
+ ref.current = node;
30
+ }, [ref]);
31
+ // The active marker follows the item with aria-current. It glides only when the page changes, never on first paint.
32
+ const measureActive = useCallback(() => {
33
+ const item = root.current?.querySelector(`${ITEM}[aria-current="page"]`);
34
+ setActive(current => {
35
+ if (!item)
36
+ return null;
37
+ const next = boxOf(item);
38
+ return current && current.top === next.top && current.height === next.height ? current : { ...next, glide: current !== null };
39
+ });
40
+ }, []);
41
+ useLayoutEffect(() => {
42
+ measureActive();
43
+ const node = root.current;
44
+ if (!node || typeof ResizeObserver === "undefined")
45
+ return;
46
+ // A resize (a late font, a collapsing section) moves the marker at once; only a page change glides.
47
+ const observer = new ResizeObserver(() => {
48
+ const item = node.querySelector(`${ITEM}[aria-current="page"]`);
49
+ setActive(current => {
50
+ if (!item)
51
+ return null;
52
+ const next = boxOf(item);
53
+ return current && current.top === next.top && current.height === next.height ? current : { ...next, glide: false };
54
+ });
55
+ });
56
+ observer.observe(node);
57
+ return () => observer.disconnect();
58
+ }, [measureActive]);
59
+ function show(target, glide) {
60
+ const item = target instanceof HTMLElement ? target.closest(ITEM) : null;
61
+ if (!item || item.getAttribute("aria-disabled") === "true")
62
+ return setHover(null);
63
+ setHover(current => ({ ...boxOf(item), glide: glide && current !== null }));
64
+ }
65
+ const context = useMemo(() => ({ activeChanged: measureActive, navigate: (choice) => navigateHandler.current?.(choice) }), [measureActive]);
66
+ return (_jsx(NavContext.Provider, { value: context, children: _jsxs("nav", { ref: setRoot, "aria-label": label, className: [styles.list, className].filter(Boolean).join(" "), onPointerOver: (event) => { pointer.current = true; show(event.target, true); }, onPointerLeave: () => setHover(null), onKeyDownCapture: () => { pointer.current = false; }, onFocus: (event) => { if (!pointer.current && event.target.matches(":focus-visible"))
67
+ show(event.target, false); }, onBlur: (event) => { if (!pointer.current && !event.currentTarget.contains(event.relatedTarget))
68
+ setHover(null); }, children: [_jsx(MenuHighlight, { state: hover, className: styles.highlight }), _jsx(MenuHighlight, { state: active, className: styles.active }), _jsx("ul", { className: styles.items, children: children })] }) }));
69
+ }
70
+ /** A labelled group of NavItem rows inside a NavList. */
71
+ export function NavSection({ label, children }) {
72
+ const id = useId();
73
+ 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 })] }));
74
+ }
75
+ /** One row of a NavList: a link by `href`, or an action by `onSelect`. */
76
+ export function NavItem({ children, icon, active = false, disabled = false, reason, trailing, onIntent, className, href, onSelect }) {
77
+ const nav = useContext(NavContext);
78
+ const reasonId = useId();
79
+ const showReason = disabled && reason;
80
+ useLayoutEffect(() => { nav?.activeChanged(); }, [active, nav]);
81
+ 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 })
82
+ : trailing ? _jsx("span", { className: styles.trailing, children: trailing }) : null] });
83
+ const shared = {
84
+ className: [styles.item, className].filter(Boolean).join(" "),
85
+ "data-nav-item": "",
86
+ "aria-current": active ? "page" : undefined,
87
+ "aria-disabled": disabled || undefined,
88
+ "aria-describedby": showReason ? reasonId : undefined,
89
+ };
90
+ if (disabled) {
91
+ // No href and no action: it can be focused and read, but not followed.
92
+ return _jsx("li", { children: _jsx("span", { ...shared, role: href ? "link" : "button", tabIndex: 0, onClick: (event) => event.preventDefault(), children: content }) });
93
+ }
94
+ const intent = onIntent ? { onPointerEnter: onIntent, onFocus: onIntent } : {};
95
+ if (href !== undefined) {
96
+ // A modified click opens a new tab or window, so this page stays where it is: no choice to report.
97
+ const onClick = (event) => { if (event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey)
98
+ nav?.navigate({ href }); };
99
+ return _jsx("li", { children: _jsx(SiliconLink, { ...shared, ...intent, href: href, onClick: onClick, children: content }) });
100
+ }
101
+ return _jsx("li", { children: _jsx("button", { ...shared, ...intent, type: "button", onClick: () => { nav?.navigate({}); onSelect?.(); }, children: content }) });
102
+ }
103
+ 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; } }
@@ -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,7 +20,7 @@
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); } }
@@ -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; } }
@@ -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); }
@@ -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,7 +18,7 @@
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
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); }
24
24
  .reveal[aria-pressed="true"] { color: var(--foreground); }
@@ -27,22 +27,22 @@
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; }
@@ -1,6 +1,6 @@
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 { --pi-h: var(--control-height-md); --pi-panel-w: 320px; position: relative; display: grid; min-width: 0; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
3
- .field[data-open] { z-index: 30; }
3
+ .field[data-open] { z-index: var(--layer-dropdown); }
4
4
  .label { justify-self: start; margin-bottom: var(--space-related); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
5
5
 
6
6
  /* The shell matches the labeled input: one border, one radius, and a quiet border change on hover and focus. */
@@ -12,7 +12,7 @@
12
12
 
13
13
  /* The anchor holds the closed button's place and springs to a new width when the calling code changes length. */
14
14
  .anchor { position: relative; flex: none; height: 100%; }
15
- .anchor::after { content: ""; position: absolute; top: 10px; right: 0; bottom: 10px; width: 1px; background: var(--border); transition: opacity var(--duration-fast) var(--ease-standard); }
15
+ .anchor::after { content: ""; position: absolute; top: var(--space-group); right: 0; bottom: var(--space-group); width: 1px; background: var(--border); transition: opacity var(--duration-fast) var(--ease-standard); }
16
16
  .field[data-open] .anchor::after { opacity: 0; }
17
17
 
18
18
  /* One material for both states. Open, it steps out over the shell's own border so the two edges never double up. */
@@ -24,8 +24,8 @@
24
24
 
25
25
  .lid { position: absolute; z-index: 1; top: 0; right: 0; left: 0; height: calc(var(--pi-h) - 2px); }
26
26
 
27
- .trigger { box-sizing: border-box; display: flex; height: calc(var(--pi-h) - 2px); align-items: center; padding: 0 36px 0 12px; font-size: var(--text-sm); font-weight: 500; white-space: nowrap; }
28
- .lid .trigger { position: absolute; inset: 3px auto 3px 3px; width: calc(100% - 6px); height: auto; margin: 0; padding: 0 33px 0 9px; border: 0; border-radius: calc(var(--radius-control) - 4px); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); cursor: pointer; font-family: inherit; letter-spacing: inherit; text-align: left; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: background-color var(--duration-fast) var(--ease-standard); }
27
+ .trigger { box-sizing: border-box; display: flex; height: calc(var(--pi-h) - 2px); align-items: center; padding: 0 var(--space-block) 0 var(--space-group); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; }
28
+ .lid .trigger { position: absolute; inset: var(--space-tight) auto var(--space-tight) var(--space-tight); width: calc(100% - 6px); height: auto; margin: 0; padding: 0 var(--space-block) 0 var(--space-related); border: 0; border-radius: calc(var(--radius-control) - var(--space-tight)); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); cursor: pointer; font-family: inherit; letter-spacing: inherit; text-align: left; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: background-color var(--duration-fast) var(--ease-standard); }
29
29
  @media (hover: hover) and (pointer: fine) { .field:not([data-open], [data-disabled]) .lid .trigger:hover { background: var(--surface-muted); } }
30
30
  .lid .trigger:active:not(:disabled) { background: var(--surface-muted); }
31
31
  .lid .trigger:disabled { cursor: not-allowed; }
@@ -37,14 +37,14 @@
37
37
 
38
38
  .slot { position: relative; display: block; min-width: 0; height: 100%; flex: 1; }
39
39
  .slot[data-hidden] { opacity: 0; transition: opacity var(--duration-instant) var(--ease-standard); }
40
- .layer { position: absolute; inset: 0; display: flex; align-items: center; gap: 6px; }
41
- .flag { display: inline-grid; width: 20px; flex: none; place-items: center; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", var(--font-body); font-size: 17px; font-weight: 400; line-height: 1; }
40
+ .layer { position: absolute; inset: 0; display: flex; align-items: center; gap: var(--space-related); }
41
+ .flag { display: inline-grid; width: 20px; flex: none; place-items: center; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", var(--font-body); font-size: var(--text-lg); font-weight: 400; line-height: 1; }
42
42
  .dial { font-variant-numeric: tabular-nums; }
43
43
 
44
- .chevron { position: absolute; top: 50%; right: 12px; display: grid; width: 16px; height: 16px; margin-top: -8px; place-items: center; color: var(--text-muted); pointer-events: none; }
44
+ .chevron { position: absolute; top: 50%; right: var(--space-group); display: grid; width: 16px; height: 16px; margin-top: calc(var(--space-related) * -1); place-items: center; color: var(--text-muted); pointer-events: none; }
45
45
 
46
46
  /* Search takes the lid's place while the list is open. */
47
- .searchRow { position: absolute; inset: 0; display: flex; align-items: center; gap: var(--space-related); padding: 0 2px 0 13px; }
47
+ .searchRow { position: absolute; inset: 0; display: flex; align-items: center; gap: var(--space-related); padding: 0 var(--space-hair) 0 var(--space-group); }
48
48
  .searchRow[inert] { pointer-events: none; }
49
49
  .searchIcon { flex: none; color: var(--text-muted); }
50
50
  .search { min-width: 0; height: 100%; flex: 1; padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); outline: none; }
@@ -58,31 +58,31 @@
58
58
  }
59
59
 
60
60
  /* The list hangs under the lid at its final width, so rows never reflow while the surface springs open. */
61
- .listFace { position: absolute; top: calc(var(--pi-h) - 2px); left: 0; box-sizing: border-box; width: var(--pi-panel-w); padding: 0 6px 6px; }
62
- .listFace::before { content: ""; position: absolute; top: 0; right: 14px; left: 14px; height: 1px; background: var(--border-subtle); }
61
+ .listFace { position: absolute; top: calc(var(--pi-h) - var(--space-hair)); left: 0; box-sizing: border-box; width: var(--pi-panel-w); padding: 0 var(--space-related) var(--space-related); }
62
+ .listFace::before { content: ""; position: absolute; top: 0; right: var(--space-inset); left: var(--space-inset); height: 1px; background: var(--border-subtle); }
63
63
  .listFace[inert] { pointer-events: none; }
64
- .scroll { max-height: 280px; overflow-y: auto; overscroll-behavior: contain; padding-top: 6px; scrollbar-width: thin; }
65
- .options { position: relative; display: grid; gap: 2px; }
64
+ .scroll { max-height: 280px; overflow-y: auto; overscroll-behavior: contain; padding-top: var(--space-related); scrollbar-width: thin; }
65
+ .options { position: relative; display: grid; gap: var(--space-hair); }
66
66
  .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }
67
- .group { display: grid; gap: 2px; }
68
- .group + .group { margin-top: 4px; }
69
- .groupLabel { padding: 8px 10px 4px; color: var(--text-muted); font-size: var(--text-xs); }
70
- .option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: 10px; padding: 0 10px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; font-size: var(--text-sm); user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
67
+ .group { display: grid; gap: var(--space-hair); }
68
+ .group + .group { margin-top: var(--space-tight); }
69
+ .groupLabel { padding: var(--space-related) var(--space-group) var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); }
70
+ .option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: var(--space-group); padding: 0 var(--space-group); border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; font-size: var(--text-sm); user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
71
71
  .option[data-active], .option[aria-selected="true"] { color: var(--foreground); }
72
72
  .name { min-width: 0; flex: 1; overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
73
73
  .meta { flex: none; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
74
74
  .check { display: grid; width: 16px; height: 16px; flex: none; place-items: center; color: var(--foreground); opacity: 0; transform: scale(.6); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
75
75
  .check[data-on] { opacity: 1; transform: none; }
76
- .empty { margin: 0; padding: 12px 10px 14px; color: var(--text-muted); font-size: var(--text-sm); }
76
+ .empty { margin: 0; padding: var(--space-group) var(--space-group) var(--space-inset); color: var(--text-muted); font-size: var(--text-sm); }
77
77
 
78
78
  /* The number and its guide share one box. The guide paints the typed part invisibly, then the rest of the example faintly after it. */
79
79
  .numberWrap { position: relative; display: flex; min-width: 0; flex: 1; align-items: center; }
80
- .number, .guide { box-sizing: border-box; padding: 0 36px 0 12px; font-family: inherit; font-size: var(--text-sm); font-variant-numeric: tabular-nums; letter-spacing: var(--tracking-body); line-height: var(--leading-body); }
80
+ .number, .guide { box-sizing: border-box; padding: 0 var(--space-block) 0 var(--space-group); font-family: inherit; font-size: var(--text-sm); font-variant-numeric: tabular-nums; letter-spacing: var(--tracking-body); line-height: var(--leading-body); }
81
81
  .number { position: relative; z-index: 1; width: 100%; height: 100%; min-width: 0; margin: 0; border: 0; border-radius: 0 var(--radius-control) var(--radius-control) 0; corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); outline: none; }
82
82
  .number:disabled { cursor: not-allowed; }
83
83
  .guide { position: absolute; inset: 0; display: flex; align-items: center; overflow: hidden; color: color-mix(in oklab, var(--text-muted) 72%, transparent); pointer-events: none; white-space: pre; }
84
84
  .guideTyped { visibility: hidden; }
85
- .valid { position: absolute; z-index: 1; top: 50%; right: 12px; display: grid; width: 18px; height: 18px; margin-top: -9px; place-items: center; color: var(--success); pointer-events: none; }
85
+ .valid { position: absolute; z-index: 1; top: 50%; right: var(--space-group); display: grid; width: 18px; height: 18px; margin-top: calc(var(--space-related) * -1); place-items: center; color: var(--success); pointer-events: none; }
86
86
 
87
87
  /* The row's space is padding inside the clipped slot, so the height animates from a true zero. */
88
88
  .messageSlot { position: relative; display: block; overflow: hidden; }
@@ -3,10 +3,10 @@
3
3
  .content {
4
4
  --popover-x: 0px;
5
5
  --popover-y: -5px;
6
- z-index: 80;
6
+ z-index: var(--layer-popover);
7
7
  min-width: 12rem;
8
8
  max-width: min(22rem, calc(100vw - 20px));
9
- padding: 1rem;
9
+ padding: var(--space-inset);
10
10
  border: 1px solid var(--border);
11
11
  border-radius: var(--radius-container);
12
12
  background: var(--surface-raised);
@@ -1 +1 @@
1
- .progress { width: 100%; color: var(--text-secondary); font-size: var(--text-sm); } .meta { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 8px; line-height: var(--leading-body); } .label { position: relative; min-width: 0; } .line { display: block; } .value { display: inline-flex; flex: none; align-items: center; gap: 4px; color: var(--text-muted); font-variant-numeric: tabular-nums; } .count { display: inline-grid; justify-items: end; } /* A hidden "100%" row reserves the widest count, so the check never shifts as the number gains a digit. */ .count::before { content: "100%"; height: 0; overflow: hidden; visibility: hidden; } .done { display: grid; place-items: center; color: var(--success); } .track { height: 7px; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-muted); } .fill { display: block; width: 100%; height: 100%; border-radius: inherit; corner-shape: inherit; background: var(--accent); transition: background-color var(--duration-standard) var(--ease-standard); } .progress[data-complete] .fill { background: var(--success); transition-delay: 200ms; } @media (prefers-reduced-motion: reduce) { .fill, .progress[data-complete] .fill { transition: none; } }
1
+ .progress { width: 100%; color: var(--text-secondary); font-size: var(--text-sm); } .meta { display: flex; justify-content: space-between; gap: var(--space-group); margin-bottom: var(--space-related); line-height: var(--leading-body); } .label { position: relative; min-width: 0; } .line { display: block; } .value { display: inline-flex; flex: none; align-items: center; gap: var(--space-tight); color: var(--text-muted); font-variant-numeric: tabular-nums; } .count { display: inline-grid; justify-items: end; } /* A hidden "100%" row reserves the widest count, so the check never shifts as the number gains a digit. */ .count::before { content: "100%"; height: 0; overflow: hidden; visibility: hidden; } .done { display: grid; place-items: center; color: var(--success); } .track { height: 7px; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-muted); } .fill { display: block; width: 100%; height: 100%; border-radius: inherit; corner-shape: inherit; background: var(--accent); transition: background-color var(--duration-standard) var(--ease-standard); } .progress[data-complete] .fill { background: var(--success); transition-delay: 200ms; } @media (prefers-reduced-motion: reduce) { .fill, .progress[data-complete] .fill { transition: none; } }
@@ -1,7 +1,7 @@
1
1
  .root {
2
2
  position: relative;
3
3
  display: grid;
4
- gap: 10px;
4
+ gap: var(--space-group);
5
5
  color: var(--foreground);
6
6
  font-family: var(--font-body);
7
7
  font-size: var(--text-sm);
@@ -10,7 +10,7 @@
10
10
  }
11
11
  .root *, .root *::before, .root *::after { box-sizing: border-box; }
12
12
  .root[data-layout="grid"] { grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--min-column, 180px)), 1fr)); }
13
- .root[data-layout="list"] { grid-template-columns: minmax(0, 1fr); gap: 8px; }
13
+ .root[data-layout="list"] { grid-template-columns: minmax(0, 1fr); gap: var(--space-related); }
14
14
 
15
15
  /* The one moving part: a ring that springs from card to card. It sits over the card border so the card itself never changes size. */
16
16
  .ring {
@@ -28,8 +28,8 @@
28
28
  display: grid;
29
29
  min-width: 0;
30
30
  align-items: start;
31
- gap: 4px 12px;
32
- padding: 14px 16px;
31
+ gap: var(--space-tight) var(--space-group);
32
+ padding: var(--space-inset) var(--space-inset);
33
33
  border: 1px solid var(--border);
34
34
  border-radius: var(--radius-control); corner-shape: var(--corner-shape);
35
35
  background: var(--surface);
@@ -40,7 +40,7 @@
40
40
  }
41
41
  /* The body row takes any spare height, so prices sit on one baseline across a row even when a description wraps. */
42
42
  .root[data-layout="grid"] .card { grid-template-areas: "body indicator" "meta meta"; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: minmax(0, 1fr) auto; }
43
- .root[data-layout="list"] .card { grid-template-areas: "indicator body meta"; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; padding: 13px 16px 13px 14px; }
43
+ .root[data-layout="list"] .card { grid-template-areas: "indicator body meta"; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; padding: var(--space-group) var(--space-inset) var(--space-group) var(--space-inset); }
44
44
  .card[data-checked] { background: color-mix(in oklab, var(--control-on) 4%, var(--surface)); }
45
45
  .card[aria-disabled="true"] { background: var(--surface-muted); cursor: not-allowed; }
46
46
  .card[aria-disabled="true"] :is(.label, .meta) { color: var(--text-muted); }
@@ -61,8 +61,8 @@
61
61
  .indicatorDot { width: 6px; height: 6px; border-radius: 50%; corner-shape: round; background: var(--control-glyph); }
62
62
  .card[aria-disabled="true"] .indicator { border-color: var(--border); }
63
63
 
64
- .body { display: grid; min-width: 0; gap: 2px; grid-area: body; }
65
- .label { display: flex; min-width: 0; align-items: center; gap: 8px; font-weight: 500; }
64
+ .body { display: grid; min-width: 0; gap: var(--space-hair); grid-area: body; }
65
+ .label { display: flex; min-width: 0; align-items: center; gap: var(--space-related); font-weight: 500; }
66
66
  .labelText { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
67
67
  .icon { display: grid; width: 18px; height: 18px; flex: none; place-items: center; color: var(--text-secondary); }
68
68
  .icon > svg { width: 18px; height: 18px; }
@@ -70,7 +70,7 @@
70
70
  .description { color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); text-wrap: pretty; }
71
71
 
72
72
  .meta { grid-area: meta; color: var(--foreground); font-variant-numeric: tabular-nums; white-space: nowrap; }
73
- .root[data-layout="grid"] .meta { margin-top: 10px; font-size: var(--text-lg); }
73
+ .root[data-layout="grid"] .meta { margin-top: var(--space-group); font-size: var(--text-lg); }
74
74
  .root[data-layout="list"] .meta { font-weight: 500; }
75
75
 
76
76
  @media (hover: hover) and (pointer: fine) {