@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,319 @@
1
+ /* ============================================================
2
+ TABS — non-sequential navigation between sibling views
3
+ Underline (default) for primary panel nav · pill (segmented)
4
+ for compact filters/toolbars. Count badges, icons, keyboard
5
+ nav. All values from tokens.
6
+
7
+ Extracted from primitives.css (Nov 2025): Tabs is a stateful
8
+ navigation component, not a headless atom. See tabs.html.
9
+
10
+ Markup contract:
11
+ <div class="tabs" role="tablist" aria-label="…">
12
+ <button class="tab" role="tab" aria-selected="true">
13
+ <span class="tab-label" data-label="All">All</span>
14
+ <span class="tab-count">128</span>
15
+ </button>
16
+ <button class="tab" role="tab" aria-selected="false">…</button>
17
+ </div>
18
+ Pill variant: add .tabs-pill to the container.
19
+ The .tab-label wrapper (with a data-label matching its text)
20
+ reserves the bold width so the active/hover state never reflows.
21
+ ============================================================ */
22
+
23
+ .tabs {
24
+ display: flex;
25
+ gap: var(--space-1);
26
+ border-bottom: 1px solid var(--color-border-default);
27
+ position: relative;
28
+ }
29
+ .tab {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ gap: var(--space-2);
33
+ padding: var(--space-3) var(--space-4);
34
+ background: transparent;
35
+ border: 0;
36
+ cursor: pointer;
37
+ font-family: var(--font-sans);
38
+ font-size: var(--fs-sm);
39
+ font-weight: var(--w-medium);
40
+ color: var(--color-foreground-secondary);
41
+ position: relative;
42
+ white-space: nowrap;
43
+ transition: var(--transition-fast);
44
+ margin-bottom: -1px;
45
+ }
46
+ .tab:hover { color: var(--color-foreground-default); }
47
+ .tab[aria-selected="true"],
48
+ .tab.is-active {
49
+ color: var(--color-brand-accent-700);
50
+ font-weight: var(--w-semibold);
51
+ }
52
+ .tab[aria-selected="true"]::after,
53
+ .tab.is-active::after {
54
+ content: '';
55
+ position: absolute;
56
+ left: var(--space-3);
57
+ right: var(--space-3);
58
+ bottom: 0;
59
+ height: 2px;
60
+ background: var(--color-brand-accent-500);
61
+ border-radius: var(--radius-xs) var(--radius-xs) 0 0;
62
+ }
63
+ .tab:focus-visible {
64
+ outline: none;
65
+ box-shadow: var(--shadow-focus);
66
+ border-radius: var(--radius-sm);
67
+ }
68
+ .tab[disabled] { opacity: 0.45; color: var(--color-foreground-disabled); cursor: not-allowed; }
69
+
70
+ .tab-count {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ min-width: 20px;
75
+ height: 20px;
76
+ padding: 0 var(--space-2);
77
+ font-family: var(--font-mono);
78
+ font-size: var(--fs-xxs);
79
+ font-weight: var(--w-bold);
80
+ background: var(--color-background-subtle);
81
+ color: var(--color-foreground-muted);
82
+ border-radius: var(--radius-pill);
83
+ }
84
+ .tab[aria-selected="true"] .tab-count,
85
+ .tab.is-active .tab-count {
86
+ background: var(--color-brand-accent-50);
87
+ color: var(--color-brand-accent-700);
88
+ }
89
+
90
+ /* Pill tabs — alternative for compact, segmented contexts */
91
+ .tabs-pill {
92
+ border-bottom: 0;
93
+ background: var(--color-background-subtle);
94
+ border: 1px solid var(--color-border-default);
95
+ border-radius: var(--radius-pill);
96
+ padding: var(--space-1);
97
+ gap: 0;
98
+ }
99
+ .tabs-pill .tab {
100
+ padding: var(--space-2) var(--space-4);
101
+ border-radius: var(--radius-pill);
102
+ margin-bottom: 0;
103
+ }
104
+ .tabs-pill .tab::after { display: none; }
105
+ .tabs-pill .tab[aria-selected="true"],
106
+ .tabs-pill .tab.is-active {
107
+ background: var(--color-background-surface);
108
+ color: var(--color-foreground-default);
109
+ box-shadow: var(--shadow-xs);
110
+ }
111
+
112
+ /* ---- Pill layout options ----------------------------------- */
113
+ /* Mobile: the segmented shell fills the row and scrolls internally when
114
+ its segments overflow (B1). The shell itself never wraps; tabs keep
115
+ their size and the user swipes horizontally inside the pill. */
116
+ @media (max-width: 720px) {
117
+ .tabs-pill {
118
+ display: flex;
119
+ width: 100%;
120
+ overflow-x: auto;
121
+ flex-wrap: nowrap;
122
+ scrollbar-width: none;
123
+ -webkit-overflow-scrolling: touch;
124
+ scroll-snap-type: x proximity;
125
+ }
126
+ .tabs-pill::-webkit-scrollbar { display: none; }
127
+ .tabs-pill .tab { flex: 0 0 auto; scroll-snap-align: start; }
128
+ }
129
+
130
+ /* Equal-width segments: every tab the same width (good for 2–4
131
+ short, symmetric options like Day/Week/Month). */
132
+ .tabs-pill.is-equal { display: inline-flex; }
133
+ .tabs-pill.is-equal .tab { flex: 1 1 0; justify-content: center; }
134
+
135
+ /* Full-width: the segmented control fills its container. */
136
+ .tabs-pill.is-block { display: flex; width: 100%; }
137
+ .tabs-pill.is-block .tab { flex: 1 1 0; justify-content: center; }
138
+
139
+ /* Equal-width underline tabs: each item takes the same width with its
140
+ content horizontally centered. Use when tabs should read as a balanced row. */
141
+ .tabs.is-block { display: flex; width: 100%; }
142
+ .tabs.is-block .tab { flex: 1 1 0; justify-content: center; min-width: 0; }
143
+ .tabs.is-block .tab-label { justify-items: center; min-width: 0; }
144
+ .tabs.is-equal { display: flex; }
145
+ .tabs.is-equal .tab { flex: 1 1 0; justify-content: center; min-width: 0; }
146
+ .tabs.is-equal .tab-label { justify-items: center; min-width: 0; }
147
+ /* Equal-width pill segments shouldn't overflow their labels either */
148
+ .tabs-pill.is-equal .tab, .tabs-pill.is-block .tab { min-width: 0; }
149
+
150
+ /* Icon-only pill: square segments, label provided via aria-label. */
151
+ .tabs-pill .tab.is-icon { padding-left: var(--space-2); padding-right: var(--space-2); aspect-ratio: 1; justify-content: center; }
152
+ .tabs-pill .tab.is-icon i, .tabs-pill .tab.is-icon svg { width: 16px; height: 16px; }
153
+
154
+ /* ============================================================
155
+ SIZES — apply on the container; both variants honour them.
156
+ .tabs-sm dense (toolbars, card headers, drawers)
157
+ (default) standard
158
+ .tabs-lg spacious (page-level primary nav)
159
+ ============================================================ */
160
+ .tabs-sm .tab {
161
+ padding: var(--space-2) var(--space-3);
162
+ font-size: var(--fs-xs);
163
+ gap: var(--space-1);
164
+ }
165
+ .tabs-sm .tab-count { min-width: 16px; height: 16px; font-size: 10px; padding: 0 var(--space-1); }
166
+ .tabs-sm .tab i, .tabs-sm .tab svg { width: 13px; height: 13px; }
167
+ .tabs-sm.tabs-pill .tab { padding: var(--space-1) var(--space-3); }
168
+
169
+ .tabs-lg .tab {
170
+ padding: var(--space-4) var(--space-5);
171
+ font-size: var(--fs-md);
172
+ gap: var(--space-3);
173
+ }
174
+ .tabs-lg .tab-count { min-width: 22px; height: 22px; font-size: var(--fs-xs); }
175
+ .tabs-lg .tab i, .tabs-lg .tab svg { width: 17px; height: 17px; }
176
+ .tabs-lg.tabs-pill .tab { padding: var(--space-3) var(--space-5); }
177
+
178
+ /* Mobile: tabs-lg steps down to the default size so page-level nav
179
+ isn't oversized on phones (C). */
180
+ @media (max-width: 720px) {
181
+ .tabs-lg .tab { padding: var(--space-3) var(--space-4); font-size: var(--fs-sm); gap: var(--space-2); }
182
+ .tabs-lg .tab-count { min-width: 18px; height: 18px; font-size: var(--fs-xxs); }
183
+ .tabs-lg .tab i, .tabs-lg .tab svg { width: 15px; height: 15px; }
184
+ .tabs-lg.tabs-pill .tab { padding: var(--space-2) var(--space-4); }
185
+ .tabs-lg .tab[aria-selected="true"]::after,
186
+ .tabs-lg .tab.is-active::after { left: var(--space-3); right: var(--space-3); }
187
+ }
188
+ /* Keep the underline indicator inset consistent at each size */
189
+ .tabs-sm .tab[aria-selected="true"]::after,
190
+ .tabs-sm .tab.is-active::after { left: var(--space-2); right: var(--space-2); }
191
+ .tabs-lg .tab[aria-selected="true"]::after,
192
+ .tabs-lg .tab.is-active::after { left: var(--space-4); right: var(--space-4); }
193
+
194
+ /* Tab icons inherit the size of the type they sit beside */
195
+ .tab i, .tab svg { width: 15px; height: 15px; stroke-width: 2; }
196
+
197
+ /* Label wrapper: reserves the SEMIBOLD width up front so toggling
198
+ weight between states never reflows the row. The hidden ::after
199
+ holds a bold copy of the text in a second (0-height) grid row, so
200
+ the column width = max(regular, bold) = bold. No layout shift. */
201
+ .tab-label {
202
+ display: inline-grid;
203
+ grid-template-columns: max-content;
204
+ }
205
+ .tab-label::after {
206
+ content: attr(data-label);
207
+ font-weight: var(--w-semibold);
208
+ height: 0;
209
+ visibility: hidden;
210
+ overflow: hidden;
211
+ pointer-events: none;
212
+ user-select: none;
213
+ }
214
+
215
+ /* ============================================================
216
+ ANIMATION — opt-in sliding indicator + panel fade
217
+ Add .is-animated to the .tabs container. A single
218
+ <span class="tab-indicator" aria-hidden="true"></span> is
219
+ placed as the LAST child of the tablist; the JS controller
220
+ sets --ind-x (translateX) and --ind-w (width) from the active
221
+ tab, and recomputes on resize. Without the controller the
222
+ indicator stays at 0 width — so .is-animated REQUIRES the JS.
223
+ ============================================================ */
224
+ .tabs.is-animated { position: relative; }
225
+
226
+ /* Underline: suppress the per-tab static bar, show one sliding bar */
227
+ .tabs.is-animated .tab[aria-selected="true"]::after,
228
+ .tabs.is-animated .tab.is-active::after { display: none; }
229
+
230
+ .tab-indicator {
231
+ position: absolute;
232
+ left: 0;
233
+ bottom: 0;
234
+ height: 2px;
235
+ width: var(--ind-w, 0);
236
+ transform: translateX(var(--ind-x, 0));
237
+ background: var(--color-brand-accent-500);
238
+ border-radius: var(--radius-xs) var(--radius-xs) 0 0;
239
+ pointer-events: none;
240
+ transition: transform var(--duration-base) var(--ease-emphasis),
241
+ width var(--duration-base) var(--ease-emphasis);
242
+ }
243
+
244
+ /* Pill: the lifted white surface becomes a sliding element behind tabs */
245
+ .tabs-pill.is-animated .tab { z-index: 1; }
246
+ .tabs-pill.is-animated .tab[aria-selected="true"],
247
+ .tabs-pill.is-animated .tab.is-active { background: transparent; box-shadow: none; }
248
+ .tabs-pill.is-animated .tab-indicator {
249
+ top: var(--space-1);
250
+ bottom: var(--space-1);
251
+ height: auto;
252
+ background: var(--color-background-surface);
253
+ box-shadow: var(--shadow-xs);
254
+ border-radius: var(--radius-pill);
255
+ z-index: 0;
256
+ }
257
+
258
+ /* Panel fade — add .tab-panel-animated to each [role="tabpanel"] */
259
+ @keyframes tab-panel-in {
260
+ from { opacity: 0; transform: translateY(4px); }
261
+ to { opacity: 1; transform: translateY(0); }
262
+ }
263
+ .tab-panel-animated:not([hidden]) {
264
+ animation: tab-panel-in var(--duration-base) var(--ease-standard);
265
+ }
266
+
267
+ @media (prefers-reduced-motion: reduce) {
268
+ .tab { transition: none; }
269
+ .tab-indicator { transition: none; }
270
+ .tab-panel-animated:not([hidden]) { animation: none; }
271
+ }
272
+
273
+ /* ============================================================
274
+ MOBILE — horizontal scroll for overflowing underline tabs
275
+ Underline tabs scroll sideways by DEFAULT below 720px (A) so a
276
+ long row never overflows the page or wraps. `.is-scroll` forces
277
+ the same behaviour at any width. Pill tabs get their own mobile
278
+ scroll rule above (the shell scrolls internally, B1).
279
+ Note: overflow clips the box-shadow focus ring slightly at the
280
+ top edge — acceptable tradeoff; the underline still reads.
281
+ ============================================================ */
282
+ @media (max-width: 720px) {
283
+ .tabs:not(.tabs-pill):not(.is-block):not(.is-equal) {
284
+ overflow-x: auto;
285
+ flex-wrap: nowrap;
286
+ scrollbar-width: none;
287
+ -webkit-overflow-scrolling: touch;
288
+ scroll-snap-type: x proximity;
289
+ }
290
+ .tabs:not(.tabs-pill):not(.is-block):not(.is-equal)::-webkit-scrollbar { display: none; }
291
+ .tabs:not(.tabs-pill):not(.is-block):not(.is-equal) .tab { scroll-snap-align: start; flex: 0 0 auto; }
292
+ }
293
+ .tabs.is-scroll {
294
+ overflow-x: auto;
295
+ flex-wrap: nowrap;
296
+ scrollbar-width: none;
297
+ -webkit-overflow-scrolling: touch;
298
+ scroll-snap-type: x proximity;
299
+ }
300
+ .tabs.is-scroll::-webkit-scrollbar { display: none; }
301
+ .tabs.is-scroll .tab { scroll-snap-align: start; flex: 0 0 auto; }
302
+ /* In a scroll container the box-shadow ring is clipped at the top edge,
303
+ so swap to an inset outline that lives inside the tab. */
304
+ .tabs.is-scroll .tab:focus-visible {
305
+ box-shadow: none;
306
+ outline: 2px solid var(--color-border-focus);
307
+ outline-offset: -3px;
308
+ }
309
+
310
+ /* ============================================================
311
+ TOUCH TARGETS — guarantee ≥44px hit height on touch devices.
312
+ Tabs (esp. .tabs-sm) are visually short; on a coarse pointer
313
+ we pad the tap area up to the platform minimum without
314
+ changing the visual type size.
315
+ ============================================================ */
316
+ @media (pointer: coarse) {
317
+ .tab { min-height: 44px; }
318
+ .tabs-pill .tab { min-height: 40px; }
319
+ }
@@ -0,0 +1,239 @@
1
+ /* ============================================================
2
+ TOAST COMPONENT — ATTRUS Design System
3
+ Ephemeral feedback after an action. Auto-dismiss, non-blocking.
4
+ All values from tokens.
5
+ ============================================================ */
6
+
7
+ .toast {
8
+ display: grid;
9
+ grid-template-columns: auto 1fr auto;
10
+ align-items: flex-start;
11
+ gap: var(--space-3);
12
+ min-width: 320px;
13
+ max-width: 480px;
14
+ padding: var(--space-4) var(--space-5);
15
+ background: var(--glass-white);
16
+ -webkit-backdrop-filter: var(--glass-blur);
17
+ backdrop-filter: var(--glass-blur);
18
+ border: var(--glass-border);
19
+ border-radius: var(--radius-md);
20
+ box-shadow: var(--shadow-glass), var(--shadow-lg);
21
+ font-family: var(--font-sans);
22
+ font-size: var(--fs-sm);
23
+ line-height: var(--lh-normal);
24
+ color: var(--color-foreground-default);
25
+ position: relative;
26
+ overflow: hidden;
27
+ }
28
+
29
+ /* Fallback for browsers without backdrop-filter — go back to a solid
30
+ surface so text never loses contrast. */
31
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
32
+ .toast { background: var(--color-background-surface); box-shadow: var(--shadow-lg); }
33
+ }
34
+
35
+ /* Leading icon container */
36
+ .toast-icon {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ width: var(--icon-md);
41
+ height: var(--icon-md);
42
+ flex-shrink: 0;
43
+ margin-top: 2px;
44
+ }
45
+ .toast-icon i, .toast-icon svg {
46
+ width: var(--icon-md);
47
+ height: var(--icon-md);
48
+ stroke-width: var(--icon-stroke);
49
+ }
50
+
51
+ /* Body — title + description */
52
+ .toast-body {
53
+ min-width: 0;
54
+ }
55
+ .toast-title {
56
+ font-size: var(--fs-sm);
57
+ font-weight: var(--w-semibold);
58
+ color: var(--color-foreground-default);
59
+ margin: 0 0 var(--space-1);
60
+ letter-spacing: var(--ls-tight);
61
+ }
62
+ .toast-desc {
63
+ font-size: var(--fs-sm);
64
+ color: var(--color-foreground-secondary);
65
+ line-height: var(--lh-relaxed);
66
+ margin: 0;
67
+ }
68
+
69
+ /* Inline action link */
70
+ .toast-action {
71
+ display: inline-block;
72
+ margin-top: var(--space-2);
73
+ background: transparent;
74
+ border: 0;
75
+ padding: 0;
76
+ font: inherit;
77
+ font-weight: var(--w-semibold);
78
+ color: var(--color-foreground-accent);
79
+ text-decoration: underline;
80
+ text-underline-offset: 3px;
81
+ cursor: pointer;
82
+ }
83
+ .toast-action:hover { color: var(--color-brand-secondary-700); }
84
+ .toast-action:focus-visible {
85
+ outline: none;
86
+ box-shadow: var(--shadow-focus);
87
+ border-radius: var(--radius-xs);
88
+ }
89
+
90
+ /* Close button */
91
+ .toast-close {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ width: 24px; height: 24px;
96
+ border-radius: var(--radius-xs);
97
+ background: transparent;
98
+ border: 0;
99
+ color: var(--color-foreground-muted);
100
+ cursor: pointer;
101
+ flex-shrink: 0;
102
+ margin-top: -2px;
103
+ transition: var(--transition-fast);
104
+ }
105
+ .toast-close:hover { background: var(--color-background-hover); color: var(--color-foreground-default); }
106
+ .toast-close:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
107
+ .toast-close i, .toast-close svg { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-stroke); }
108
+
109
+ /* Progress (auto-dismiss countdown) — bottom hairline */
110
+ .toast-progress {
111
+ position: absolute;
112
+ bottom: 0; left: 0; right: 0;
113
+ height: 2px;
114
+ background: var(--color-border-subtle);
115
+ overflow: hidden;
116
+ }
117
+ .toast-progress::after {
118
+ content: "";
119
+ position: block;
120
+ display: block;
121
+ height: 100%;
122
+ width: 100%;
123
+ background: var(--color-foreground-muted);
124
+ transform-origin: left;
125
+ animation: toast-progress var(--toast-duration, 5000ms) linear forwards;
126
+ }
127
+
128
+ @keyframes toast-progress {
129
+ from { transform: scaleX(1); }
130
+ to { transform: scaleX(0); }
131
+ }
132
+
133
+ /* ============================================================
134
+ VARIANTS — semantic
135
+ ============================================================ */
136
+
137
+ /* Default (neutral, plain) — uses defaults above */
138
+
139
+ /* Success */
140
+ .toast-success { border-left: 3px solid var(--color-feedback-success-border); }
141
+ .toast-success .toast-icon { color: var(--color-feedback-success-foreground); }
142
+ .toast-success .toast-progress::after { background: var(--color-feedback-success-foreground); }
143
+
144
+ /* Warning */
145
+ .toast-warning { border-left: 3px solid var(--color-feedback-warning-border); }
146
+ .toast-warning .toast-icon { color: var(--color-feedback-warning-foreground); }
147
+ .toast-warning .toast-progress::after { background: var(--color-feedback-warning-foreground); }
148
+
149
+ /* Danger */
150
+ .toast-danger { border-left: 3px solid var(--color-feedback-danger-border); }
151
+ .toast-danger .toast-icon { color: var(--color-feedback-danger-foreground); }
152
+ .toast-danger .toast-progress::after { background: var(--color-feedback-danger-foreground); }
153
+
154
+ /* Info */
155
+ .toast-info { border-left: 3px solid var(--color-feedback-info-border); }
156
+ .toast-info .toast-icon { color: var(--color-feedback-info-foreground); }
157
+ .toast-info .toast-progress::after { background: var(--color-feedback-info-foreground); }
158
+
159
+ /* Brand (announcement / non-feedback messages) */
160
+ .toast-brand { border-left: 3px solid var(--color-brand-secondary-300); }
161
+ .toast-brand .toast-icon { color: var(--color-foreground-accent); }
162
+ .toast-brand .toast-progress::after { background: var(--color-foreground-accent); }
163
+
164
+ /* Inverse (over photo, dark hero, etc.) */
165
+ .toast-inverse {
166
+ background: var(--glass-ink);
167
+ -webkit-backdrop-filter: var(--glass-blur);
168
+ backdrop-filter: var(--glass-blur);
169
+ border: var(--glass-border-dark);
170
+ box-shadow: var(--shadow-glass-dark), var(--shadow-2xl);
171
+ }
172
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
173
+ .toast-inverse { background: var(--color-background-inverse); box-shadow: var(--shadow-2xl); }
174
+ }
175
+ .toast-inverse .toast-title { color: var(--color-foreground-inverse-default); }
176
+ .toast-inverse .toast-desc { color: var(--color-foreground-inverse-muted); }
177
+ .toast-inverse .toast-close { color: var(--color-foreground-inverse-muted); }
178
+ .toast-inverse .toast-close:hover { background: var(--color-background-inverse-hover); color: var(--color-foreground-inverse-default); }
179
+ .toast-inverse .toast-action { color: var(--color-foreground-on-solid); }
180
+ .toast-inverse .toast-action:hover { color: var(--color-brand-secondary-100); }
181
+
182
+ /* ============================================================
183
+ STACK — viewport positioning
184
+ ============================================================ */
185
+
186
+ .toast-stack {
187
+ position: fixed;
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: var(--space-3);
191
+ z-index: 9000;
192
+ pointer-events: none;
193
+ }
194
+ .toast-stack > .toast { pointer-events: auto; }
195
+
196
+ .toast-stack-tr { top: var(--space-6); right: var(--space-6); align-items: flex-end; }
197
+ .toast-stack-tl { top: var(--space-6); left: var(--space-6); align-items: flex-start; }
198
+ .toast-stack-tc { top: var(--space-6); left: 50%; transform: translateX(-50%); align-items: center; }
199
+ .toast-stack-br { bottom: var(--space-6); right: var(--space-6); align-items: flex-end; flex-direction: column-reverse; }
200
+ .toast-stack-bl { bottom: var(--space-6); left: var(--space-6); align-items: flex-start; flex-direction: column-reverse; }
201
+ .toast-stack-bc { bottom: var(--space-6); left: 50%; transform: translateX(-50%); align-items: center; flex-direction: column-reverse; }
202
+
203
+ /* ============================================================
204
+ ENTER / EXIT animations
205
+ ============================================================ */
206
+
207
+ @keyframes toast-enter-right {
208
+ from { opacity: 0; transform: translateX(16px); }
209
+ to { opacity: 1; transform: translateX(0); }
210
+ }
211
+ @keyframes toast-enter-left {
212
+ from { opacity: 0; transform: translateX(-16px); }
213
+ to { opacity: 1; transform: translateX(0); }
214
+ }
215
+ @keyframes toast-enter-down {
216
+ from { opacity: 0; transform: translateY(-12px); }
217
+ to { opacity: 1; transform: translateY(0); }
218
+ }
219
+ @keyframes toast-enter-up {
220
+ from { opacity: 0; transform: translateY(12px); }
221
+ to { opacity: 1; transform: translateY(0); }
222
+ }
223
+
224
+ .toast-stack-tr > .toast, .toast-stack-br > .toast { animation: toast-enter-right var(--duration-base) var(--ease-emphasis) both; }
225
+ .toast-stack-tl > .toast, .toast-stack-bl > .toast { animation: toast-enter-left var(--duration-base) var(--ease-emphasis) both; }
226
+ .toast-stack-tc > .toast { animation: toast-enter-down var(--duration-base) var(--ease-emphasis) both; }
227
+ .toast-stack-bc > .toast { animation: toast-enter-up var(--duration-base) var(--ease-emphasis) both; }
228
+
229
+ /* Exit — applied via .is-leaving class set by JS before removing */
230
+ .toast.is-leaving { animation: toast-exit var(--duration-fast) var(--ease-standard) both; }
231
+ @keyframes toast-exit {
232
+ from { opacity: 1; transform: translateX(0) translateY(0) scale(1); }
233
+ to { opacity: 0; transform: translateY(-4px) scale(0.96); }
234
+ }
235
+
236
+ @media (prefers-reduced-motion: reduce) {
237
+ .toast-stack > .toast { animation: none; }
238
+ .toast-progress::after { animation: none; }
239
+ }