@attrus-ds/core 0.1.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 (54) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +123 -0
  3. package/dist/assets/flags/Brazil-rounded.svg +16 -0
  4. package/dist/assets/flags/Brazil-square.svg +17 -0
  5. package/dist/assets/flags/Chile-rounded.svg +11 -0
  6. package/dist/assets/flags/Chile-square.svg +11 -0
  7. package/dist/assets/flags/Colombia-rounded.svg +10 -0
  8. package/dist/assets/flags/Colombia-square.svg +10 -0
  9. package/dist/assets/flags/EUA-rounded.svg +11 -0
  10. package/dist/assets/flags/EUA-square.svg +11 -0
  11. package/dist/assets/flags/EUR-rounded.svg +9 -0
  12. package/dist/assets/flags/EUR-square.svg +9 -0
  13. package/dist/assets/flags/Mexico-rounded.svg +19 -0
  14. package/dist/assets/flags/Mexico-square.svg +19 -0
  15. package/dist/assets/icon-attrus-dark.svg +3 -0
  16. package/dist/assets/icon-attrus-light.svg +3 -0
  17. package/dist/assets/logo-attrus-dark.svg +8 -0
  18. package/dist/assets/logo-attrus-light.svg +8 -0
  19. package/dist/css/components/buttons.css +301 -0
  20. package/dist/css/components/callout.css +195 -0
  21. package/dist/css/components/cards.css +272 -0
  22. package/dist/css/components/combobox.css +303 -0
  23. package/dist/css/components/empty-states.css +61 -0
  24. package/dist/css/components/inputs.css +1027 -0
  25. package/dist/css/components/number-input.css +138 -0
  26. package/dist/css/components/overlays.css +405 -0
  27. package/dist/css/components/primitives.css +305 -0
  28. package/dist/css/components/sidebar.css +179 -0
  29. package/dist/css/components/status-pills.css +207 -0
  30. package/dist/css/components/stepper.css +309 -0
  31. package/dist/css/components/table.css +600 -0
  32. package/dist/css/components/tabs.css +319 -0
  33. package/dist/css/components/toast.css +239 -0
  34. package/dist/css/components/tooltip.css +226 -0
  35. package/dist/css/components/topbar.css +148 -0
  36. package/dist/css/fonts.css +91 -0
  37. package/dist/css/index.css +23 -0
  38. package/dist/css/tokens.css +691 -0
  39. package/dist/fonts/LICENSE-JetBrainsMono.txt +93 -0
  40. package/dist/fonts/LICENSE-Lato.txt +93 -0
  41. package/dist/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  42. package/dist/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  43. package/dist/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
  44. package/dist/fonts/lato-latin-100-normal.woff2 +0 -0
  45. package/dist/fonts/lato-latin-300-italic.woff2 +0 -0
  46. package/dist/fonts/lato-latin-300-normal.woff2 +0 -0
  47. package/dist/fonts/lato-latin-400-italic.woff2 +0 -0
  48. package/dist/fonts/lato-latin-400-normal.woff2 +0 -0
  49. package/dist/fonts/lato-latin-700-italic.woff2 +0 -0
  50. package/dist/fonts/lato-latin-700-normal.woff2 +0 -0
  51. package/dist/fonts/lato-latin-900-normal.woff2 +0 -0
  52. package/dist/tokens/attrus-tokens.flat.json +1235 -0
  53. package/dist/tokens/attrus-tokens.json +1090 -0
  54. package/package.json +46 -0
@@ -0,0 +1,138 @@
1
+ /* ============================================================
2
+ NUMBER INPUT — numeric field with − / + accelerator buttons
3
+ For bounded quantities: counts, percentages, days, seats.
4
+ All values from tokens.
5
+
6
+ Renamed from .stepper (Nov 2025) to free the "Stepper" name
7
+ for the multi-step flow indicator component. See stepper.css.
8
+ ============================================================ */
9
+
10
+ .number-input {
11
+ display: inline-flex;
12
+ align-items: stretch;
13
+ height: 40px;
14
+ background: var(--color-background-surface);
15
+ border: 1px solid var(--color-border-default);
16
+ border-radius: var(--radius-sm);
17
+ font-family: var(--font-sans);
18
+ transition: var(--transition-fast);
19
+ overflow: hidden;
20
+ }
21
+ .number-input:hover { border-color: var(--color-border-strong); }
22
+ .number-input:focus-within {
23
+ border-color: var(--color-foreground-accent);
24
+ box-shadow: var(--shadow-focus);
25
+ }
26
+
27
+ .number-input-btn {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ width: 36px;
32
+ background: transparent;
33
+ border: none;
34
+ color: var(--color-foreground-secondary);
35
+ cursor: pointer;
36
+ flex-shrink: 0;
37
+ transition: var(--transition-fast);
38
+ }
39
+ .number-input-btn:hover {
40
+ background: var(--color-background-hover);
41
+ color: var(--color-foreground-default);
42
+ }
43
+ .number-input-btn:focus-visible { outline: none; background: var(--color-background-hover); }
44
+ .number-input-btn:disabled,
45
+ .number-input-btn[aria-disabled="true"] {
46
+ color: var(--color-foreground-muted);
47
+ cursor: not-allowed;
48
+ background: transparent;
49
+ }
50
+ .number-input-btn i, .number-input-btn svg {
51
+ width: 14px; height: 14px;
52
+ stroke-width: 2.4;
53
+ }
54
+
55
+ .number-input-field {
56
+ flex: 1;
57
+ min-width: 0;
58
+ width: 80px;
59
+ padding: 0 var(--space-2);
60
+ background: transparent;
61
+ border: none;
62
+ border-left: 1px solid var(--color-border-subtle);
63
+ border-right: 1px solid var(--color-border-subtle);
64
+ font-family: var(--font-mono);
65
+ font-size: var(--fs-sm);
66
+ font-weight: var(--w-medium);
67
+ color: var(--color-foreground-default);
68
+ text-align: center;
69
+ -moz-appearance: textfield;
70
+ }
71
+ .number-input-field::-webkit-outer-spin-button,
72
+ .number-input-field::-webkit-inner-spin-button {
73
+ -webkit-appearance: none; margin: 0;
74
+ }
75
+ .number-input-field:focus { outline: none; }
76
+
77
+ /* Sizes */
78
+ .number-input-sm { height: 32px; }
79
+ .number-input-sm .number-input-btn { width: 28px; }
80
+ .number-input-sm .number-input-btn i, .number-input-sm .number-input-btn svg { width: 12px; height: 12px; }
81
+ .number-input-sm .number-input-field { font-size: var(--fs-xs); width: 56px; }
82
+
83
+ .number-input-lg { height: 48px; }
84
+ .number-input-lg .number-input-btn { width: 44px; }
85
+ .number-input-lg .number-input-btn i, .number-input-lg .number-input-btn svg { width: 16px; height: 16px; }
86
+ .number-input-lg .number-input-field { font-size: var(--fs-md); width: 96px; }
87
+
88
+ /* With suffix (currency, unit) */
89
+ .number-input-suffix {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ padding: 0 var(--space-3);
93
+ font-family: var(--font-mono);
94
+ font-size: var(--fs-xs);
95
+ font-weight: var(--w-semibold);
96
+ letter-spacing: var(--ls-caps);
97
+ text-transform: uppercase;
98
+ color: var(--color-foreground-muted);
99
+ border-left: 1px solid var(--color-border-subtle);
100
+ background: var(--color-background-subtle);
101
+ }
102
+
103
+ .number-input-prefix {
104
+ display: inline-flex;
105
+ align-items: center;
106
+ padding: 0 var(--space-3);
107
+ font-family: var(--font-mono);
108
+ font-size: var(--fs-sm);
109
+ font-weight: var(--w-semibold);
110
+ color: var(--color-foreground-muted);
111
+ border-right: 1px solid var(--color-border-subtle);
112
+ background: var(--color-background-subtle);
113
+ }
114
+
115
+ /* States */
116
+ .number-input.is-disabled,
117
+ .number-input:has(.number-input-field:disabled) {
118
+ background: var(--color-background-subtle);
119
+ border-color: var(--color-border-subtle);
120
+ cursor: not-allowed;
121
+ }
122
+ .number-input.is-disabled .number-input-field,
123
+ .number-input-field:disabled {
124
+ color: var(--color-foreground-muted);
125
+ cursor: not-allowed;
126
+ }
127
+
128
+ .number-input.is-invalid {
129
+ border-color: var(--color-feedback-danger-border);
130
+ }
131
+ .number-input.is-invalid:focus-within {
132
+ box-shadow: 0 0 0 3px var(--color-feedback-danger-background);
133
+ }
134
+
135
+ /* Reduced motion */
136
+ @media (prefers-reduced-motion: reduce) {
137
+ .number-input, .number-input-btn { transition: none; }
138
+ }
@@ -0,0 +1,405 @@
1
+ /* ============================================================
2
+ OVERLAY SYSTEM — Modal + Drawer
3
+ Both share scrim, entry/exit motion, focus management.
4
+ All values from tokens.
5
+ ============================================================ */
6
+
7
+ /* =================== SCRIM =================== */
8
+ .scrim {
9
+ position: fixed;
10
+ inset: 0;
11
+ background: var(--color-scrim);
12
+ backdrop-filter: blur(var(--scrim-blur));
13
+ -webkit-backdrop-filter: blur(var(--scrim-blur));
14
+ z-index: 9000;
15
+ opacity: 0;
16
+ pointer-events: none;
17
+ transition: opacity var(--duration-base) var(--ease-standard);
18
+ }
19
+ .scrim.is-open {
20
+ opacity: 1;
21
+ pointer-events: auto;
22
+ }
23
+
24
+ /* Fallback for browsers without backdrop-filter — strengthen the tint to compensate. */
25
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
26
+ .scrim { background: rgba(10, 15, 31, 0.55); }
27
+ }
28
+
29
+ /* =================== MODAL =================== */
30
+ .modal {
31
+ position: fixed;
32
+ z-index: 9100;
33
+ top: 50%; left: 50%;
34
+ transform: translate(-50%, calc(-50% + 32px)) scale(0.96);
35
+ width: min(560px, calc(100vw - var(--space-8)));
36
+ max-height: calc(100vh - var(--space-12));
37
+ display: flex;
38
+ flex-direction: column;
39
+ background: var(--color-background-surface);
40
+ border-radius: var(--radius-lg);
41
+ box-shadow: var(--shadow-2xl);
42
+ opacity: 0;
43
+ pointer-events: none;
44
+ will-change: transform, opacity;
45
+ /* Exit (default state) — quick fade + small slide */
46
+ transition:
47
+ opacity var(--duration-base) var(--ease-standard),
48
+ transform var(--duration-base) var(--ease-standard);
49
+ }
50
+ .modal.is-open {
51
+ opacity: 1;
52
+ pointer-events: auto;
53
+ transform: translate(-50%, -50%) scale(1);
54
+ /* Entry — page-level pace, emphasized for the "land" feel */
55
+ transition:
56
+ opacity var(--duration-slower) var(--ease-standard),
57
+ transform var(--duration-deliberate) var(--ease-emphasis);
58
+ }
59
+
60
+ /* Sizes */
61
+ .modal-sm { width: min(420px, calc(100vw - var(--space-8))); }
62
+ .modal-md { width: min(560px, calc(100vw - var(--space-8))); }
63
+ .modal-lg { width: min(720px, calc(100vw - var(--space-8))); }
64
+ .modal-xl { width: min(960px, calc(100vw - var(--space-8))); }
65
+
66
+ /* Header */
67
+ .modal-header {
68
+ display: flex;
69
+ align-items: flex-start;
70
+ justify-content: space-between;
71
+ gap: var(--space-4);
72
+ padding: var(--space-6) var(--space-6) var(--space-4);
73
+ }
74
+ .modal-header-text { flex: 1; min-width: 0; }
75
+ .modal-title {
76
+ font-family: var(--font-sans);
77
+ font-size: var(--fs-xl);
78
+ font-weight: var(--w-bold);
79
+ letter-spacing: var(--ls-title);
80
+ line-height: var(--lh-tight);
81
+ color: var(--color-foreground-default);
82
+ margin: 0 0 var(--space-1);
83
+ }
84
+ .modal-subtitle {
85
+ font-size: var(--fs-sm);
86
+ color: var(--color-foreground-secondary);
87
+ line-height: var(--lh-normal);
88
+ margin: 0;
89
+ }
90
+ .modal-close {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ width: 32px; height: 32px;
95
+ background: transparent;
96
+ border: none;
97
+ border-radius: var(--radius-sm);
98
+ color: var(--color-foreground-muted);
99
+ cursor: pointer;
100
+ flex-shrink: 0;
101
+ transition: var(--transition-fast);
102
+ }
103
+ .modal-close:hover {
104
+ background: var(--color-background-hover);
105
+ color: var(--color-foreground-default);
106
+ }
107
+ .modal-close:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
108
+ .modal-close i, .modal-close svg { width: 18px; height: 18px; stroke-width: var(--icon-stroke); }
109
+
110
+ /* Body */
111
+ .modal-body {
112
+ padding: var(--space-2) var(--space-6) var(--space-6);
113
+ overflow-y: auto;
114
+ font-size: var(--fs-sm);
115
+ color: var(--color-foreground-default);
116
+ line-height: var(--lh-relaxed);
117
+ flex: 1;
118
+ }
119
+ .modal-body p { margin: 0 0 var(--space-3); }
120
+ .modal-body p:last-child { margin-bottom: 0; }
121
+
122
+ /* Footer */
123
+ .modal-footer {
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: flex-end;
127
+ gap: var(--space-3);
128
+ padding: var(--space-4) var(--space-6) var(--space-6);
129
+ border-top: 1px solid var(--color-border-subtle);
130
+ margin-top: auto;
131
+ flex-shrink: 0;
132
+ }
133
+ .modal-footer-spread { justify-content: space-between; }
134
+ .modal-footer-aux {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: var(--space-2);
138
+ font-size: var(--fs-xs);
139
+ color: var(--color-foreground-muted);
140
+ }
141
+
142
+ /* Variant: destructive — top accent strip */
143
+ .modal-destructive .modal-header {
144
+ border-top: 3px solid var(--color-feedback-danger-foreground);
145
+ padding-top: calc(var(--space-6) - 3px);
146
+ border-top-left-radius: var(--radius-lg);
147
+ border-top-right-radius: var(--radius-lg);
148
+ }
149
+ .modal-destructive .modal-icon {
150
+ display: inline-flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ width: 40px; height: 40px;
154
+ background: var(--color-feedback-danger-background);
155
+ color: var(--color-feedback-danger-foreground);
156
+ border-radius: var(--radius-pill);
157
+ margin-bottom: var(--space-3);
158
+ }
159
+ .modal-destructive .modal-icon i, .modal-destructive .modal-icon svg { width: 20px; height: 20px; stroke-width: var(--icon-stroke); }
160
+
161
+
162
+ /* =================== DRAWER =================== */
163
+ .drawer {
164
+ position: fixed;
165
+ z-index: 9100;
166
+ background: var(--color-background-surface);
167
+ display: flex;
168
+ flex-direction: column;
169
+ overflow-y: auto;
170
+ overflow-x: hidden;
171
+ box-shadow: var(--shadow-2xl);
172
+ opacity: 0;
173
+ pointer-events: none;
174
+ will-change: transform, opacity;
175
+ /* Exit — slide back out + fade */
176
+ transition:
177
+ transform var(--duration-slow) var(--ease-standard),
178
+ opacity var(--duration-base) var(--ease-standard);
179
+ }
180
+ .drawer.is-open {
181
+ opacity: 1;
182
+ pointer-events: auto;
183
+ /* Entry — page-level pace, emphasized */
184
+ transition:
185
+ transform var(--duration-deliberate) var(--ease-emphasis),
186
+ opacity var(--duration-slower) var(--ease-standard);
187
+ }
188
+
189
+ /* Right (default).
190
+ --drawer-w drives both the width AND stack offsets so size variants
191
+ automatically scale the stacking math. --drawer-offset shifts the panel
192
+ inward when stacked behind another drawer. */
193
+ .drawer-right {
194
+ --drawer-w: min(440px, 90vw);
195
+ --drawer-offset: 0px;
196
+ top: 0; right: 0; bottom: 0;
197
+ width: var(--drawer-w);
198
+ border-top-left-radius: var(--radius-xs);
199
+ border-bottom-left-radius: var(--radius-xs);
200
+ transform: translateX(100%);
201
+ }
202
+ .drawer-right.is-open { transform: translateX(calc(-1 * var(--drawer-offset))); }
203
+
204
+ /* Left */
205
+ .drawer-left {
206
+ --drawer-w: min(320px, 85vw);
207
+ --drawer-offset: 0px;
208
+ top: 0; left: 0; bottom: 0;
209
+ width: var(--drawer-w);
210
+ border-top-right-radius: var(--radius-xs);
211
+ border-bottom-right-radius: var(--radius-xs);
212
+ transform: translateX(-100%);
213
+ }
214
+ .drawer-left.is-open { transform: translateX(var(--drawer-offset)); }
215
+
216
+ /* =================== STACKED DRAWERS ===================
217
+ Open a second drawer alongside the first instead of replacing it.
218
+ --drawer-stack = how many slots inward to shift; 0 = frontmost panel.
219
+ Slot width derived from --drawer-w so size variants stack correctly.
220
+ Cap at 3 stacked panels for screens ≥1280px; 2 below.
221
+ Usage: <aside class="drawer drawer-right drawer-md" data-stack="1"> */
222
+ .drawer-right[data-stack="1"], .drawer-left[data-stack="1"] { --drawer-offset: var(--drawer-w); }
223
+ .drawer-right[data-stack="2"], .drawer-left[data-stack="2"] { --drawer-offset: calc(var(--drawer-w) * 2); }
224
+ .drawer-right[data-stack="3"], .drawer-left[data-stack="3"] { --drawer-offset: calc(var(--drawer-w) * 3); }
225
+
226
+ /* Stacked panels lose their inner radius — they butt up against a sibling.
227
+ Separator is placed on the edge that TOUCHES the adjacent panel.
228
+ `:has(~)` only matches FOLLOWING siblings, so we apply rules from the
229
+ panel that comes EARLIER in the DOM (the back panel, which is rendered
230
+ before the front panel in modal-drawer.html). */
231
+
232
+ /* Back panel (stack ≥ 1): when followed by an open front panel,
233
+ only flatten the INNER edge (right side) and add separator there.
234
+ Outer edge (left side) keeps the rounded corners. */
235
+ .drawer-right[data-stack]:not([data-stack="0"]).is-open:has(~ .drawer-right[data-stack="0"].is-open) {
236
+ border-top-right-radius: 0;
237
+ border-bottom-right-radius: 0;
238
+ border-right: 2px solid var(--color-border-default);
239
+ }
240
+ .drawer-left[data-stack]:not([data-stack="0"]).is-open:has(~ .drawer-left[data-stack="0"].is-open) {
241
+ border-top-left-radius: 0;
242
+ border-bottom-left-radius: 0;
243
+ border-left: 2px solid var(--color-border-default);
244
+ }
245
+
246
+ /* Front panel (stack=0): when PRECEDED by an open back panel, flatten
247
+ the inner-side radius and add the matching separator. Use the parent's
248
+ `:has` to detect the configuration, then target the front. */
249
+ :has(> .drawer-right[data-stack]:not([data-stack="0"]).is-open) > .drawer-right[data-stack="0"].is-open {
250
+ border-top-left-radius: 0;
251
+ border-bottom-left-radius: 0;
252
+ border-left: 2px solid var(--color-border-default);
253
+ }
254
+ :has(> .drawer-left[data-stack]:not([data-stack="0"]).is-open) > .drawer-left[data-stack="0"].is-open {
255
+ border-top-right-radius: 0;
256
+ border-bottom-right-radius: 0;
257
+ border-right: 2px solid var(--color-border-default);
258
+ }
259
+
260
+ /* Bottom */
261
+ .drawer-bottom {
262
+ left: 0; right: 0; bottom: 0;
263
+ max-height: 80vh;
264
+ border-top-left-radius: var(--radius-sm);
265
+ border-top-right-radius: var(--radius-sm);
266
+ transform: translateY(100%);
267
+ }
268
+ .drawer-bottom.is-open { transform: translateY(0); }
269
+
270
+ /* Drawer sizes (right/left) — feed --drawer-w so stacking math scales */
271
+ .drawer-sm { --drawer-w: min(360px, 90vw); width: var(--drawer-w); }
272
+ .drawer-md { --drawer-w: min(440px, 90vw); width: var(--drawer-w); }
273
+ .drawer-lg { --drawer-w: min(560px, 92vw); width: var(--drawer-w); }
274
+ .drawer-xl { --drawer-w: min(720px, 94vw); width: var(--drawer-w); }
275
+
276
+ /* Drawer parts (mirror modal vocabulary).
277
+ Header and footer are STICKY with a liquid-glass background: body
278
+ content scrolls underneath them with a frosted blur. The drawer
279
+ itself is the scroll container (see .drawer overflow-y above). */
280
+ .drawer-header {
281
+ position: sticky;
282
+ top: 0;
283
+ z-index: 2;
284
+ display: flex;
285
+ align-items: flex-start;
286
+ justify-content: space-between;
287
+ gap: var(--space-4);
288
+ padding: var(--space-6);
289
+ background: var(--glass-white);
290
+ -webkit-backdrop-filter: var(--glass-blur);
291
+ backdrop-filter: var(--glass-blur);
292
+ border-bottom: 1px solid var(--color-border-subtle);
293
+ flex-shrink: 0;
294
+ }
295
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
296
+ .drawer-header, .drawer-footer { background: var(--color-background-surface); }
297
+ }
298
+ .drawer-header-text { flex: 1; min-width: 0; }
299
+ .drawer-title {
300
+ font-family: var(--font-sans);
301
+ font-size: var(--fs-lg);
302
+ font-weight: var(--w-bold);
303
+ letter-spacing: var(--ls-title);
304
+ line-height: var(--lh-tight);
305
+ color: var(--color-foreground-default);
306
+ margin: 0 0 var(--space-1);
307
+ }
308
+ .drawer-subtitle {
309
+ font-size: var(--fs-sm);
310
+ color: var(--color-foreground-secondary);
311
+ line-height: var(--lh-normal);
312
+ margin: 0;
313
+ }
314
+ .drawer-close {
315
+ display: inline-flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ align-self: flex-start;
319
+ width: 32px; height: 32px;
320
+ background: transparent;
321
+ border: none;
322
+ border-radius: var(--radius-sm);
323
+ color: var(--color-foreground-muted);
324
+ cursor: pointer;
325
+ flex-shrink: 0;
326
+ transition: var(--transition-fast);
327
+ }
328
+ .drawer-close:hover { background: var(--color-background-hover); color: var(--color-foreground-default); }
329
+ .drawer-close:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
330
+ .drawer-close i, .drawer-close svg { width: 18px; height: 18px; stroke-width: var(--icon-stroke); }
331
+
332
+ /* Header lead tile — standardized icon container on the LEFT of the header.
333
+ 40×40, top-aligned, consistent radius; caller sets bg/color per context. */
334
+ .drawer-lead {
335
+ width: 40px; height: 40px;
336
+ flex-shrink: 0;
337
+ align-self: flex-start;
338
+ display: inline-flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ border-radius: var(--radius-md);
342
+ }
343
+ .drawer-lead i, .drawer-lead svg { width: 20px; height: 20px; stroke-width: var(--icon-stroke); }
344
+
345
+ .drawer-body {
346
+ padding: var(--space-6);
347
+ flex: 1 0 auto;
348
+ font-size: var(--fs-sm);
349
+ color: var(--color-foreground-default);
350
+ line-height: var(--lh-relaxed);
351
+ }
352
+
353
+ .drawer-footer {
354
+ position: sticky;
355
+ bottom: 0;
356
+ z-index: 2;
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: flex-end;
360
+ gap: var(--space-3);
361
+ padding: var(--space-5) var(--space-6);
362
+ background: var(--glass-white);
363
+ -webkit-backdrop-filter: var(--glass-blur);
364
+ backdrop-filter: var(--glass-blur);
365
+ border-top: 1px solid var(--color-border-subtle);
366
+ flex-shrink: 0;
367
+ }
368
+
369
+ /* Drawer-bottom drag handle (mobile-style) */
370
+ .drawer-handle {
371
+ position: sticky;
372
+ top: 0;
373
+ z-index: 3;
374
+ display: flex;
375
+ justify-content: center;
376
+ padding: var(--space-3) 0 var(--space-1);
377
+ background: var(--glass-white);
378
+ -webkit-backdrop-filter: var(--glass-blur);
379
+ backdrop-filter: var(--glass-blur);
380
+ flex-shrink: 0;
381
+ }
382
+ /* When a handle precedes the header (bottom drawer), the header no longer
383
+ needs its own glass top — they read as one frosted bar. */
384
+ .drawer-handle + .drawer-header { border-top: 0; }
385
+ .drawer-handle::before {
386
+ content: "";
387
+ width: 36px;
388
+ height: 4px;
389
+ background: var(--color-border-strong);
390
+ border-radius: var(--radius-pill);
391
+ }
392
+
393
+ /* Reduced motion */
394
+ @media (prefers-reduced-motion: reduce) {
395
+ .scrim, .modal, .drawer, .modal.is-open, .drawer.is-open {
396
+ transition: opacity var(--duration-instant) linear !important;
397
+ }
398
+ .modal.is-open { transform: translate(-50%, -50%); }
399
+ .drawer-right.is-open,
400
+ .drawer-left.is-open,
401
+ .drawer-bottom.is-open { transform: none; }
402
+ .drawer-right { transform: none; }
403
+ .drawer-left { transform: none; }
404
+ .drawer-bottom { transform: none; }
405
+ }