@echodial/deck 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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1327 -0
  3. package/bin/deck.mjs +219 -0
  4. package/build.mjs +338 -0
  5. package/dist/api-buckets.json +10486 -0
  6. package/dist/api.json +57896 -0
  7. package/dist/brand/deck-apple-touch-icon.png +0 -0
  8. package/dist/brand/deck-logo-dark.svg +1 -0
  9. package/dist/brand/deck-logo-light.svg +1 -0
  10. package/dist/brand/deck-logo-stacked.svg +1 -0
  11. package/dist/brand/deck-logo.svg +1 -0
  12. package/dist/brand/deck-mark.svg +7 -0
  13. package/dist/brand/deck-og.png +0 -0
  14. package/dist/collisions.json +223 -0
  15. package/dist/deck-adapters.js +482 -0
  16. package/dist/deck-adapters.min.js +23 -0
  17. package/dist/deck-extras.js +823 -0
  18. package/dist/deck-extras.min.js +5 -0
  19. package/dist/deck-icons.svg +184 -0
  20. package/dist/deck.bundle.js +2638 -0
  21. package/dist/deck.bundle.min.js +86 -0
  22. package/dist/deck.css +7904 -0
  23. package/dist/deck.esm.js +2644 -0
  24. package/dist/deck.js +1331 -0
  25. package/dist/deck.min.css +32 -0
  26. package/dist/deck.min.js +60 -0
  27. package/dist/layers/anchor.css +192 -0
  28. package/dist/layers/buttons.css +210 -0
  29. package/dist/layers/charts.css +337 -0
  30. package/dist/layers/combobox.css +248 -0
  31. package/dist/layers/components.css +868 -0
  32. package/dist/layers/container.css +199 -0
  33. package/dist/layers/datagrid.css +335 -0
  34. package/dist/layers/datepicker.css +305 -0
  35. package/dist/layers/forms.css +401 -0
  36. package/dist/layers/gradients.css +302 -0
  37. package/dist/layers/inputs.css +522 -0
  38. package/dist/layers/layers.css +87 -0
  39. package/dist/layers/layout.css +235 -0
  40. package/dist/layers/logical.css +226 -0
  41. package/dist/layers/media.css +400 -0
  42. package/dist/layers/mobile.css +245 -0
  43. package/dist/layers/motion.css +379 -0
  44. package/dist/layers/nav.css +771 -0
  45. package/dist/layers/perf.css +234 -0
  46. package/dist/layers/print.css +198 -0
  47. package/dist/layers/reset.css +103 -0
  48. package/dist/layers/space3d.css +280 -0
  49. package/dist/layers/toasts.css +201 -0
  50. package/dist/layers/tokens.css +229 -0
  51. package/dist/layers/type.css +114 -0
  52. package/dist/layers/utilities.css +241 -0
  53. package/dist/sizes.json +52 -0
  54. package/dist/usage.json +6340 -0
  55. package/package.json +94 -0
  56. package/src/00-layers.css +87 -0
  57. package/src/01-tokens.css +229 -0
  58. package/src/02-reset.css +103 -0
  59. package/src/03-type.css +114 -0
  60. package/src/04-layout.css +235 -0
  61. package/src/05-buttons.css +210 -0
  62. package/src/06-forms.css +401 -0
  63. package/src/07-components.css +868 -0
  64. package/src/08-mobile.css +245 -0
  65. package/src/09-utilities.css +241 -0
  66. package/src/10-datepicker.css +305 -0
  67. package/src/11-combobox.css +248 -0
  68. package/src/12-datagrid.css +335 -0
  69. package/src/13-toasts.css +201 -0
  70. package/src/14-charts.css +337 -0
  71. package/src/16-motion.css +379 -0
  72. package/src/18-container.css +199 -0
  73. package/src/19-logical.css +226 -0
  74. package/src/20-gradients.css +302 -0
  75. package/src/21-space3d.css +280 -0
  76. package/src/22-nav.css +771 -0
  77. package/src/23-inputs.css +522 -0
  78. package/src/24-media.css +400 -0
  79. package/src/25-anchor.css +192 -0
  80. package/src/26-perf.css +234 -0
  81. package/src/99-print.css +198 -0
  82. package/src/brand/deck-apple-touch-icon.png +0 -0
  83. package/src/brand/deck-logo-dark.svg +1 -0
  84. package/src/brand/deck-logo-light.svg +1 -0
  85. package/src/brand/deck-logo-stacked.svg +1 -0
  86. package/src/brand/deck-logo.svg +1 -0
  87. package/src/brand/deck-mark.svg +7 -0
  88. package/src/brand/deck-og.png +0 -0
  89. package/src/brand/sources.json +7 -0
  90. package/src/deck-icons.svg +184 -0
  91. package/src/js/deck-adapters.js +482 -0
  92. package/src/js/deck-extras.js +823 -0
  93. package/src/js/deck.js +1331 -0
@@ -0,0 +1,305 @@
1
+ /* ==========================================================================
2
+ Deck — 10 Date picker
3
+ Popover calendar on desktop, bottom sheet on a phone. Single date, range,
4
+ and month/year jump. Behaviour lives in deck.js; this file is the skin.
5
+ ========================================================================== */
6
+
7
+ @layer deck.components {
8
+
9
+ /* --- Trigger field ------------------------------------------------------ */
10
+ /* The trigger for a date picker: an input with a calendar icon inside its
11
+ starting edge and a clear button at the end. The input is readonly and
12
+ cursor: pointer, because the value comes from the calendar — which also
13
+ means a reader who knows the date cannot type it. */
14
+ .datefield { position: relative; display: flex; align-items: center; }
15
+ .datefield > .icon {
16
+ position: absolute;
17
+ inset-inline-start: var(--space-3);
18
+ color: var(--text-faint);
19
+ pointer-events: none;
20
+ }
21
+ .datefield > .input { padding-inline-start: var(--space-10); cursor: pointer; }
22
+ /* Clears the field. Hidden until .has-value is set by deck.js. */
23
+ .datefield > .datefield-clear {
24
+ position: absolute;
25
+ inset-inline-end: var(--space-2);
26
+ inline-size: 28px;
27
+ block-size: 28px;
28
+ display: grid;
29
+ place-items: center;
30
+ border-radius: var(--r-full);
31
+ color: var(--text-faint);
32
+ cursor: pointer;
33
+ }
34
+ .datefield > .datefield-clear:hover { background: var(--surface-hover); color: var(--text); }
35
+ .datefield:not(.has-value) .datefield-clear { display: none; }
36
+
37
+ /* --- Panel -------------------------------------------------------------- */
38
+ /* The calendar panel: a native popover, anchored to its field by
39
+ 25-anchor.css with the same fallback chain as a menu. Where there is no
40
+ room below it opens upward, and position-try-order: most-block-size picks
41
+ the direction with the most room rather than the first that fits — which
42
+ matters more for a calendar than for a menu. */
43
+ .datepicker {
44
+ position: absolute;
45
+ margin: 0;
46
+ inset: auto;
47
+ padding: 0;
48
+ inline-size: max-content;
49
+ max-inline-size: calc(100vw - var(--space-6));
50
+ background: var(--surface);
51
+ border: 1px solid var(--line);
52
+ border-radius: var(--r-md);
53
+ box-shadow: var(--shadow-4);
54
+ color: var(--text);
55
+ opacity: 0;
56
+ translate: 0 -6px;
57
+ transition: opacity var(--dur-2) var(--ease-out),
58
+ translate var(--dur-2) var(--ease-out),
59
+ overlay var(--dur-2) allow-discrete,
60
+ display var(--dur-2) allow-discrete;
61
+ }
62
+ .datepicker:popover-open { opacity: 1; translate: 0; }
63
+ @starting-style { .datepicker:popover-open { opacity: 0; translate: 0 -6px; } }
64
+ .datepicker::backdrop { background: transparent; }
65
+
66
+ .datepicker-layout { display: flex; flex-direction: column; }
67
+ @media (min-width: 40rem) {
68
+ .datepicker-layout:has(.datepicker-presets) { flex-direction: row; }
69
+ }
70
+
71
+ /* --- Preset rail --------------------------------------------------------- */
72
+ /* The shortcut rail — "Last 7 days", "This month". A horizontal scroller
73
+ below 40rem and a column beside the calendar above it. */
74
+ .datepicker-presets {
75
+ display: flex;
76
+ gap: var(--space-1);
77
+ padding: var(--space-2);
78
+ overflow-x: auto;
79
+ scrollbar-width: none;
80
+ border-block-end: 1px solid var(--line);
81
+ }
82
+ .datepicker-presets::-webkit-scrollbar { display: none; }
83
+ @media (min-width: 40rem) {
84
+ .datepicker-presets {
85
+ flex-direction: column;
86
+ inline-size: 10.5rem;
87
+ overflow-x: visible;
88
+ border-block-end: 0;
89
+ border-inline-end: 1px solid var(--line);
90
+ background: var(--surface-2);
91
+ }
92
+ }
93
+ .datepicker-preset {
94
+ padding: var(--space-2) var(--space-3);
95
+ min-block-size: 36px;
96
+ border-radius: var(--r-xs);
97
+ font-size: var(--text-sm);
98
+ font-weight: 520;
99
+ color: var(--text-muted);
100
+ text-align: start;
101
+ white-space: nowrap;
102
+ cursor: pointer;
103
+ transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
104
+ }
105
+ .datepicker-preset:hover { background: var(--surface-hover); color: var(--text); }
106
+ .datepicker-preset[aria-pressed="true"] { background: var(--brand-soft); color: var(--brand-soft-text); font-weight: 600; }
107
+
108
+ /* --- Calendar ----------------------------------------------------------- */
109
+ .datepicker-main { padding: var(--space-3); }
110
+
111
+ .datepicker-head {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: var(--space-1);
115
+ padding-block-end: var(--space-2);
116
+ }
117
+ .datepicker-title {
118
+ flex: 1 1 auto;
119
+ display: inline-flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ gap: var(--space-2);
123
+ min-block-size: 36px;
124
+ padding-inline: var(--space-3);
125
+ border-radius: var(--r-sm);
126
+ font-weight: 620;
127
+ font-size: var(--text-sm);
128
+ cursor: pointer;
129
+ letter-spacing: -.01em;
130
+ }
131
+ .datepicker-title:hover { background: var(--surface-hover); }
132
+ .datepicker-nav {
133
+ inline-size: 36px;
134
+ block-size: 36px;
135
+ display: grid;
136
+ place-items: center;
137
+ border-radius: var(--r-sm);
138
+ color: var(--text-muted);
139
+ cursor: pointer;
140
+ }
141
+ .datepicker-nav:hover { background: var(--surface-hover); color: var(--text); }
142
+ .datepicker-nav[disabled] { opacity: .35; pointer-events: none; }
143
+
144
+ .datepicker-months { display: flex; gap: var(--space-5); }
145
+ .datepicker-month { display: flex; flex-direction: column; }
146
+
147
+ .datepicker-grid {
148
+ display: grid;
149
+ grid-template-columns: repeat(7, 1fr);
150
+ gap: 1px;
151
+ }
152
+ .datepicker-dow {
153
+ text-align: center;
154
+ font-size: var(--text-2xs);
155
+ font-weight: 620;
156
+ color: var(--text-faint);
157
+ padding-block: var(--space-2);
158
+ }
159
+
160
+ /* One day cell. 38px, raised to 42px on a coarse pointer so it approaches
161
+ the touch target. Eight is- states compose on it, and the range band is
162
+ built from is-in-range plus logical corner radii on the two ends. */
163
+ .datepicker-day {
164
+ position: relative;
165
+ inline-size: 38px;
166
+ block-size: 38px;
167
+ display: grid;
168
+ place-items: center;
169
+ border-radius: var(--r-sm);
170
+ font-size: var(--text-sm);
171
+ font-variant-numeric: tabular-nums;
172
+ color: var(--text);
173
+ cursor: pointer;
174
+ -webkit-tap-highlight-color: transparent;
175
+ transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
176
+ }
177
+ @media (pointer: coarse) { .datepicker-day { inline-size: 42px; block-size: 42px; } }
178
+
179
+ .datepicker-day:hover { background: var(--surface-hover); }
180
+ .datepicker-day.is-outside { color: var(--text-faint); opacity: .55; }
181
+ .datepicker-day[disabled], .datepicker-day.is-blocked {
182
+ color: var(--text-faint);
183
+ opacity: .4;
184
+ pointer-events: none;
185
+ text-decoration: line-through;
186
+ }
187
+
188
+ /* Today gets a marker, not a fill, so it never competes with the selection */
189
+ .datepicker-day.is-today::after {
190
+ content: "";
191
+ position: absolute;
192
+ inset-block-end: 5px;
193
+ inline-size: 4px;
194
+ block-size: 4px;
195
+ border-radius: var(--r-full);
196
+ background: var(--brand);
197
+ }
198
+
199
+ .datepicker-day.is-selected {
200
+ background: var(--brand-600);
201
+ color: var(--text-on-brand);
202
+ font-weight: 620;
203
+ }
204
+ .datepicker-day.is-selected::after { background: var(--text-on-brand); }
205
+
206
+ /* Range: the middle days get a continuous band, the ends get the caps */
207
+ .datepicker-day.is-in-range {
208
+ background: var(--brand-soft);
209
+ color: var(--brand-soft-text);
210
+ border-radius: 0;
211
+ }
212
+ .datepicker-day.is-range-start {
213
+ border-start-end-radius: 0;
214
+ border-end-end-radius: 0;
215
+ }
216
+ .datepicker-day.is-range-end {
217
+ border-start-start-radius: 0;
218
+ border-end-start-radius: 0;
219
+ }
220
+ .datepicker-day.is-range-start.is-range-end { border-radius: var(--r-sm); }
221
+ .datepicker-grid:has(.is-in-range) .datepicker-day.is-in-range:first-child {
222
+ border-start-start-radius: var(--r-sm); border-end-start-radius: var(--r-sm);
223
+ }
224
+ .datepicker-grid:has(.is-in-range) .datepicker-day.is-in-range:nth-child(7n) {
225
+ border-start-end-radius: var(--r-sm); border-end-end-radius: var(--r-sm);
226
+ }
227
+ .datepicker-day.is-preview { background: color-mix(in oklab, var(--brand-soft) 60%, transparent); }
228
+ .datepicker-day:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; z-index: 1; }
229
+
230
+ /* --- Month and year jump grid ------------------------------------------- */
231
+ /* Opens the month and year grid. A picker without one makes a reader press
232
+ the next-month arrow twenty-four times to reach a date two years out. */
233
+ .datepicker-jump {
234
+ display: grid;
235
+ grid-template-columns: repeat(3, 1fr);
236
+ gap: var(--space-1);
237
+ padding-block: var(--space-2);
238
+ }
239
+ .datepicker-jump > button {
240
+ min-block-size: 40px;
241
+ padding-inline: var(--space-2);
242
+ border-radius: var(--r-sm);
243
+ font-size: var(--text-sm);
244
+ font-variant-numeric: tabular-nums;
245
+ color: var(--text-muted);
246
+ cursor: pointer;
247
+ }
248
+ .datepicker-jump > button:hover { background: var(--surface-hover); color: var(--text); }
249
+ .datepicker-jump > button[aria-selected="true"] {
250
+ background: var(--brand-600);
251
+ color: var(--text-on-brand);
252
+ font-weight: 620;
253
+ }
254
+
255
+ /* --- Time row and footer ------------------------------------------------- */
256
+ .datepicker-time {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: var(--space-2);
260
+ padding: var(--space-3);
261
+ border-block-start: 1px solid var(--line);
262
+ }
263
+ .datepicker-time .input { inline-size: auto; min-inline-size: 7.5rem; }
264
+
265
+ .datepicker-foot {
266
+ display: flex;
267
+ align-items: center;
268
+ gap: var(--space-2);
269
+ padding: var(--space-3);
270
+ border-block-start: 1px solid var(--line);
271
+ background: var(--surface-2);
272
+ }
273
+ .datepicker-readout {
274
+ flex: 1 1 auto;
275
+ font-size: var(--text-sm);
276
+ color: var(--text-muted);
277
+ font-variant-numeric: tabular-nums;
278
+ min-inline-size: 0;
279
+ }
280
+
281
+ /* --- Phone: the picker becomes a sheet ---------------------------------- */
282
+ @media (max-width: 30rem) {
283
+ .datepicker {
284
+ position: fixed;
285
+ inset: auto 0 0 0;
286
+ inline-size: 100%;
287
+ max-inline-size: 100%;
288
+ border-radius: var(--r-lg) var(--r-lg) 0 0;
289
+ border-inline: 0;
290
+ border-block-end: 0;
291
+ translate: 0 100%;
292
+ padding-block-end: env(safe-area-inset-bottom);
293
+ transition: translate var(--dur-3) var(--ease-out),
294
+ overlay var(--dur-3) allow-discrete,
295
+ display var(--dur-3) allow-discrete;
296
+ opacity: 1;
297
+ }
298
+ .datepicker:popover-open { translate: 0; }
299
+ @starting-style { .datepicker:popover-open { translate: 0 100%; } }
300
+ .datepicker::backdrop { background: oklch(18% .02 var(--hue-neutral) / .45); }
301
+ .datepicker-months { flex-direction: column; }
302
+ .datepicker-day { inline-size: 100%; }
303
+ .datepicker-main { padding-inline: var(--space-4); }
304
+ }
305
+ }
@@ -0,0 +1,401 @@
1
+ /* ==========================================================================
2
+ Deck — 06 Forms
3
+ Inputs are 16px on touch devices so iOS never zooms on focus.
4
+ ========================================================================== */
5
+
6
+ @layer deck.components {
7
+
8
+ /* One form control and everything attached to it: label, input, help
9
+ text, error. The vertical gap between them comes from here, so a field
10
+ never needs margins. */
11
+ .field {
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--space-2);
15
+ min-inline-size: 0;
16
+ }
17
+
18
+ /* The label for a control. A flex row, so a "required" or "optional"
19
+ marker sits beside the text without extra markup. */
20
+ .label {
21
+ font-size: var(--text-sm);
22
+ font-weight: 580;
23
+ color: var(--text);
24
+ display: flex;
25
+ align-items: center;
26
+ gap: var(--space-2);
27
+ }
28
+ /* The marker for an optional field. Where most fields are required this
29
+ reads better than an asterisk on nearly every label. */
30
+ .label .optional { color: var(--text-faint); font-weight: 400; }
31
+ /* The marker beside a required field's label. Visual only — a screen
32
+ reader announces the required attribute, not the asterisk. */
33
+ .label .required { color: var(--bad-500); }
34
+
35
+ /* A hint below a control. Needs aria-describedby on the control to reach
36
+ a screen reader — sitting next to it is not an association. */
37
+ .help { font-size: var(--text-sm); color: var(--text-muted); }
38
+ /* A validation message below a control. Lightened for dark mode with
39
+ light-dark(), and aligned to flex-start so an icon stays on the first
40
+ line of a two-line message. */
41
+ .error {
42
+ font-size: var(--text-sm);
43
+ color: light-dark(var(--bad-700), oklch(76% .13 var(--hue-bad)));
44
+ display: flex;
45
+ gap: var(--space-2);
46
+ align-items: flex-start;
47
+ }
48
+
49
+ .input, .textarea, .select {
50
+ inline-size: 100%;
51
+ min-block-size: var(--control-h);
52
+ padding: var(--space-2) var(--space-3);
53
+ background: var(--surface);
54
+ color: var(--text);
55
+ border: 1px solid var(--line-strong);
56
+ border-radius: var(--r-sm);
57
+ box-shadow: var(--shadow-1);
58
+ font-size: var(--text-base);
59
+ line-height: 1.45;
60
+ transition: border-color var(--dur-1) var(--ease-out),
61
+ box-shadow var(--dur-1) var(--ease-out),
62
+ background-color var(--dur-1) var(--ease-out);
63
+ }
64
+
65
+ @media (pointer: coarse) {
66
+ .input, .textarea, .select { font-size: 16px; }
67
+ }
68
+
69
+ .input::placeholder, .textarea::placeholder { color: var(--text-faint); }
70
+
71
+ .input:hover, .textarea:hover, .select:hover { border-color: var(--ink-400); }
72
+
73
+ .input:focus, .textarea:focus, .select:focus {
74
+ outline: none;
75
+ border-color: var(--focus);
76
+ box-shadow: var(--ring);
77
+ }
78
+
79
+ .input[disabled], .textarea[disabled], .select[disabled] {
80
+ background: var(--bg-sunken);
81
+ color: var(--text-faint);
82
+ cursor: not-allowed;
83
+ box-shadow: none;
84
+ }
85
+ .input[readonly] { background: var(--surface-2); }
86
+
87
+ /* Grows with its content instead of scrolling in a 4-row box */
88
+ .textarea {
89
+ min-block-size: calc(var(--control-h) * 2);
90
+ resize: vertical;
91
+ field-sizing: content;
92
+ max-block-size: 22rem;
93
+ }
94
+
95
+ .select {
96
+ appearance: none;
97
+ padding-inline-end: var(--space-10);
98
+ background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
99
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
100
+ background-position: calc(100% - 19px) calc(50% + 1px), calc(100% - 14px) calc(50% + 1px);
101
+ background-size: 5px 5px, 5px 5px;
102
+ background-repeat: no-repeat;
103
+ cursor: pointer;
104
+ }
105
+
106
+ /* Validation only after the user has actually interacted */
107
+ /* Marks a field invalid from your own validation, producing the same
108
+ border and ring as :user-invalid without needing the browser to agree.
109
+ One of the few states an author writes by hand. */
110
+ .input:user-invalid, .textarea:user-invalid, .select:user-invalid,
111
+ .field.is-invalid :is(.input, .textarea, .select) {
112
+ border-color: var(--bad-500);
113
+ }
114
+ .input:user-invalid:focus, .field.is-invalid :is(.input, .textarea, .select):focus {
115
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--bad-500) 26%, transparent);
116
+ }
117
+
118
+ /* --- Input group: prefix / suffix / attached button --------------------- */
119
+ /* A prefix, suffix or attached button joined to a control. The group
120
+ takes over the border, radius and shadow, and the input inside strips
121
+ its own, so the seam does not double up. :focus-within puts the ring
122
+ around the whole assembly. */
123
+ .input-group {
124
+ display: flex;
125
+ align-items: stretch;
126
+ inline-size: 100%;
127
+ border: 1px solid var(--line-strong);
128
+ border-radius: var(--r-sm);
129
+ background: var(--surface);
130
+ box-shadow: var(--shadow-1);
131
+ overflow: hidden;
132
+ }
133
+ .input-group:focus-within { border-color: var(--focus); box-shadow: var(--ring); }
134
+ .input-group > .input {
135
+ border: 0;
136
+ border-radius: 0;
137
+ box-shadow: none;
138
+ background: transparent;
139
+ }
140
+ .input-group > .input:focus { box-shadow: none; }
141
+ /* A prefix or suffix attached to a control inside .input-group. Its
142
+ divider is border-inline-end normally and border-inline-start on
143
+ :last-child, so a leading and a trailing addon each get their rule on
144
+ the correct side in both writing directions. */
145
+ .input-group > .addon {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ gap: var(--space-2);
149
+ padding-inline: var(--space-3);
150
+ background: var(--surface-2);
151
+ color: var(--text-muted);
152
+ font-size: var(--text-sm);
153
+ white-space: nowrap;
154
+ border-inline-end: 1px solid var(--line);
155
+ }
156
+ .input-group > .addon:last-child { border-inline-start: 1px solid var(--line); border-inline-end: 0; }
157
+ .input-group > .btn { border-radius: 0; border-block: 0; border-inline-end: 0; box-shadow: none; }
158
+
159
+ /* --- Search field ------------------------------------------------------- */
160
+ /* A control with an icon inside its starting edge. The icon is
161
+ pointer-events: none, so clicking it focuses the field underneath
162
+ rather than doing nothing. */
163
+ .search { position: relative; display: flex; align-items: center; }
164
+ .search > .icon {
165
+ position: absolute;
166
+ inset-inline-start: var(--space-3);
167
+ color: var(--text-faint);
168
+ pointer-events: none;
169
+ }
170
+ .search > .input { padding-inline-start: var(--space-10); border-radius: var(--r-full); }
171
+
172
+ /* --- Checkbox and radio ------------------------------------------------- */
173
+ /* A checkbox or radio and its text, laid out as a row that clears the
174
+ 44px touch target. Wrap the text in .check-text to add a second line. */
175
+ .check {
176
+ display: flex;
177
+ align-items: flex-start;
178
+ gap: var(--space-3);
179
+ min-block-size: var(--tap);
180
+ padding-block: var(--space-2);
181
+ cursor: pointer;
182
+ line-height: 1.4;
183
+ }
184
+ .check input[type="checkbox"], .check input[type="radio"] {
185
+ appearance: none;
186
+ flex: 0 0 auto;
187
+ inline-size: 20px;
188
+ block-size: 20px;
189
+ margin-block-start: 1px;
190
+ border: 1.5px solid var(--line-strong);
191
+ background: var(--surface);
192
+ border-radius: var(--r-xs);
193
+ display: grid;
194
+ place-content: center;
195
+ cursor: pointer;
196
+ transition: background-color var(--dur-1) var(--ease-out),
197
+ border-color var(--dur-1) var(--ease-out);
198
+ }
199
+ .check input[type="radio"] { border-radius: var(--r-full); }
200
+ .check input:hover { border-color: var(--brand-400); }
201
+ .check input:checked { background: var(--brand-600); border-color: var(--brand-600); }
202
+ .check input[type="checkbox"]::before {
203
+ content: "";
204
+ inline-size: 11px;
205
+ block-size: 11px;
206
+ scale: 0;
207
+ transition: scale var(--dur-2) var(--ease-spring);
208
+ box-shadow: inset 1em 1em var(--text-on-brand);
209
+ clip-path: polygon(14% 44%, 0 65%, 41% 100%, 100% 16%, 84% 0, 39% 68%);
210
+ }
211
+ .check input[type="radio"]::before {
212
+ content: "";
213
+ inline-size: 9px;
214
+ block-size: 9px;
215
+ border-radius: var(--r-full);
216
+ background: var(--text-on-brand);
217
+ scale: 0;
218
+ transition: scale var(--dur-2) var(--ease-spring);
219
+ }
220
+ .check input:checked::before { scale: 1; }
221
+ .check input:indeterminate { background: var(--brand-600); border-color: var(--brand-600); }
222
+ .check input:indeterminate::before {
223
+ scale: 1;
224
+ clip-path: polygon(10% 42%, 90% 42%, 90% 58%, 10% 58%);
225
+ }
226
+ .check input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
227
+ /* The text beside a checkbox or radio. A column, so a .check-note can sit
228
+ under the label as a second line. */
229
+ /* The text beside a checkbox, as a column, so a .check-note can sit
230
+ beneath the label as a second line. */
231
+ .check .check-text { display: flex; flex-direction: column; gap: 2px; }
232
+ /* A muted second line inside .check-text. It is inside the label, so it
233
+ becomes part of the accessible name — move it out and use
234
+ aria-describedby if that name gets too long. */
235
+ .check .check-note { font-size: var(--text-sm); color: var(--text-muted); }
236
+
237
+ /* Card-style choice, good for pricing and plan pickers */
238
+ .check-card {
239
+ border: 1px solid var(--line);
240
+ border-radius: var(--r-md);
241
+ padding: var(--space-4);
242
+ background: var(--surface);
243
+ transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
244
+ }
245
+ .check-card:has(input:checked) {
246
+ border-color: var(--brand-500);
247
+ background: var(--brand-soft);
248
+ }
249
+
250
+ /* --- Switch ------------------------------------------------------------- */
251
+ /* An on/off control that takes effect immediately, drawn over a real
252
+ checkbox. If the reader has to press Save afterwards, use .check
253
+ instead. Add role="switch" when "on"/"off" reads better than
254
+ "checked"/"unchecked". */
255
+ .switch { display: flex; align-items: center; gap: var(--space-3); min-block-size: var(--tap); cursor: pointer; }
256
+ .switch input {
257
+ appearance: none;
258
+ flex: 0 0 auto;
259
+ inline-size: 46px;
260
+ block-size: 28px;
261
+ border-radius: var(--r-full);
262
+ background: var(--ink-300);
263
+ position: relative;
264
+ cursor: pointer;
265
+ transition: background-color var(--dur-2) var(--ease-out);
266
+ }
267
+ .switch input::after {
268
+ content: "";
269
+ position: absolute;
270
+ inset-block-start: 3px;
271
+ inset-inline-start: 3px;
272
+ inline-size: 22px;
273
+ block-size: 22px;
274
+ border-radius: var(--r-full);
275
+ background: var(--ink-0);
276
+ box-shadow: var(--shadow-2);
277
+ transition: translate var(--dur-2) var(--ease-spring);
278
+ }
279
+ .switch input:checked { background: var(--brand-600); }
280
+ .switch input:checked::after { translate: 18px 0; }
281
+ .switch input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
282
+
283
+ /* --- Range -------------------------------------------------------------- */
284
+ /* A slider. The track and thumb are vendor pseudo-elements with
285
+ different names and different box models per engine, so every
286
+ declaration is written twice — WebKit needs a negative block-start
287
+ margin to centre the thumb and Firefox does not. */
288
+ .range {
289
+ inline-size: 100%;
290
+ appearance: none;
291
+ background: transparent;
292
+ block-size: var(--tap);
293
+ }
294
+ .range::-webkit-slider-runnable-track {
295
+ block-size: 6px;
296
+ border-radius: var(--r-full);
297
+ background: var(--ink-200);
298
+ }
299
+ .range::-moz-range-track {
300
+ block-size: 6px;
301
+ border-radius: var(--r-full);
302
+ background: var(--ink-200);
303
+ }
304
+ .range::-webkit-slider-thumb {
305
+ appearance: none;
306
+ margin-block-start: -8px;
307
+ inline-size: 22px;
308
+ block-size: 22px;
309
+ border-radius: var(--r-full);
310
+ background: var(--brand-600);
311
+ border: 3px solid var(--ink-0);
312
+ box-shadow: var(--shadow-2);
313
+ cursor: grab;
314
+ }
315
+ .range::-moz-range-thumb {
316
+ inline-size: 22px;
317
+ block-size: 22px;
318
+ border-radius: var(--r-full);
319
+ background: var(--brand-600);
320
+ border: 3px solid var(--ink-0);
321
+ box-shadow: var(--shadow-2);
322
+ cursor: grab;
323
+ }
324
+ .range:active::-webkit-slider-thumb { cursor: grabbing; scale: 1.08; }
325
+
326
+ /* --- File --------------------------------------------------------------- */
327
+ /* A dashed drop zone wrapping a real file input. The input is 1px and
328
+ transparent rather than display: none, so it keeps its place in the
329
+ tab order, and :focus-within is what makes that focus visible. Deck
330
+ styles the zone; dropping needs a handler of your own. */
331
+ .file {
332
+ display: grid;
333
+ place-items: center;
334
+ gap: var(--space-2);
335
+ padding: var(--space-6);
336
+ border: 1.5px dashed var(--line-strong);
337
+ border-radius: var(--r-md);
338
+ background: var(--surface-2);
339
+ text-align: center;
340
+ color: var(--text-muted);
341
+ cursor: pointer;
342
+ transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
343
+ }
344
+ .file:hover, .file:focus-within { border-color: var(--brand-500); background: var(--brand-soft); }
345
+ .file input[type="file"] { position: absolute; opacity: 0; inline-size: 1px; block-size: 1px; }
346
+
347
+ /* --- Fieldset ----------------------------------------------------------- */
348
+ /* A bordered group of controls that answer one question. Its legend is
349
+ the group's accessible name, which is what a radio group needs and
350
+ what a .label above one does not provide. */
351
+ .fieldset {
352
+ border: 1px solid var(--line);
353
+ border-radius: var(--r-md);
354
+ padding: var(--space-5);
355
+ display: flex;
356
+ flex-direction: column;
357
+ gap: var(--space-4);
358
+ min-inline-size: 0;
359
+ }
360
+ .fieldset > legend {
361
+ padding-inline: var(--space-2);
362
+ font-size: var(--text-sm);
363
+ font-weight: 600;
364
+ color: var(--text-muted);
365
+ }
366
+
367
+ /* Two fields side by side on anything wider than a phone */
368
+ .field-row {
369
+ display: grid;
370
+ gap: var(--space-4);
371
+ grid-template-columns: 1fr;
372
+ }
373
+ @media (min-width: 40rem) {
374
+ .field-row { grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); }
375
+ }
376
+
377
+ /* Sticky save bar for long forms on mobile */
378
+ .form-actions {
379
+ display: flex;
380
+ gap: var(--space-3);
381
+ flex-wrap: wrap;
382
+ }
383
+ /* Pins the action row to the bottom of the viewport with a blur behind
384
+ it and a safe-area inset for the home indicator. Add matching bottom
385
+ padding to the form, or the last field sits underneath it. */
386
+ .form-actions-sticky {
387
+ position: sticky;
388
+ inset-block-end: 0;
389
+ z-index: var(--z-sticky);
390
+ background: color-mix(in oklab, var(--bg) 88%, transparent);
391
+ backdrop-filter: blur(12px);
392
+ -webkit-backdrop-filter: blur(12px);
393
+ border-block-start: 1px solid var(--line);
394
+ padding-block: var(--space-3);
395
+ padding-block-end: calc(var(--space-3) + env(safe-area-inset-bottom));
396
+ }
397
+ .form-actions-sticky > .btn { flex: 1 1 auto; }
398
+ @media (min-width: 48rem) {
399
+ .form-actions-sticky > .btn { flex: 0 0 auto; }
400
+ }
401
+ }