anentrypoint-design 0.0.322 → 0.0.324

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 (45) hide show
  1. package/README.md +22 -2
  2. package/app-shell.css +24 -4782
  3. package/colors_and_type.css +43 -0
  4. package/dist/247420.css +74 -3
  5. package/dist/247420.js +16 -16
  6. package/package.json +14 -8
  7. package/src/bootstrap.js +10 -1
  8. package/src/components/chat-message-parts.js +355 -0
  9. package/src/components/chat.js +23 -320
  10. package/src/components/data-density.js +15 -10
  11. package/src/components/files-modals.js +14 -2
  12. package/src/components/files.js +5 -1
  13. package/src/components/freddie/helpers.js +46 -8
  14. package/src/css/app-shell/base.css +157 -0
  15. package/src/css/app-shell/catalog-theme.css +144 -0
  16. package/src/css/app-shell/chat-basic.css +12 -0
  17. package/src/css/app-shell/chat-polish.css +459 -0
  18. package/src/css/app-shell/data-density.css +150 -0
  19. package/src/css/app-shell/files.css +709 -0
  20. package/src/css/app-shell/hero-content.css +407 -0
  21. package/src/css/app-shell/kits-appended.css +361 -0
  22. package/src/css/app-shell/loading-alerts.css +120 -0
  23. package/src/css/app-shell/panel-row.css +177 -0
  24. package/src/css/app-shell/primitives.css +197 -0
  25. package/src/css/app-shell/responsive.css +266 -0
  26. package/src/css/app-shell/responsive2-workspace.css +465 -0
  27. package/src/css/app-shell/row-print.css +75 -0
  28. package/src/css/app-shell/sidebar-misc.css +79 -0
  29. package/src/css/app-shell/states-interactions.css +696 -0
  30. package/src/css/app-shell/topbar.css +319 -0
  31. package/src/deck-stage.js +6 -4
  32. package/src/highlight.js +20 -4
  33. package/src/index.js +15 -5
  34. package/src/kits/os/freddie/pages-chat.js +1 -1
  35. package/src/kits/os/freddie/pages-core.js +1 -1
  36. package/src/kits/os/freddie/pages-os.js +1 -1
  37. package/src/kits/os/freddie/pages-tools.js +1 -1
  38. package/src/kits/os/wm.js +38 -0
  39. package/src/kits/slides/deck-stage-overlay.js +68 -0
  40. package/src/kits/slides/deck-stage-state.js +81 -0
  41. package/src/kits/slides/deck-stage-style.js +117 -0
  42. package/src/kits/slides/deck-stage.js +167 -0
  43. package/src/kits/slides/index.js +5 -0
  44. package/src/markdown.js +89 -11
  45. package/src/kits/os/freddie/helpers.js +0 -33
@@ -0,0 +1,696 @@
1
+ /* ============================================================
2
+ Enhanced UX: Component States, Drag-and-Drop, Context Menus
3
+ Comprehensive improvements for perfect UX across all surfaces
4
+ ============================================================ */
5
+
6
+ /* ------------------------------------------------------------
7
+ Component States — Disabled, Loading, Error, Success
8
+ -------------------------------------------------------------- */
9
+
10
+ /* Disabled state for all interactive elements. The `.is-disabled` class
11
+ variants cover Btn()'s anchor-rendered case (shell.js) — an <a> has no
12
+ :disabled pseudo-class, so a disabled link-styled button relies entirely
13
+ on this class selector for its visual treatment (aria-disabled + tabindex
14
+ are set in JS; this is the paired CSS half). */
15
+ .btn:disabled, .btn-primary:disabled, .btn-ghost:disabled,
16
+ .btn.is-disabled, .btn-primary.is-disabled, .btn-ghost.is-disabled,
17
+ button:disabled, [disabled] {
18
+ opacity: 0.6;
19
+ cursor: not-allowed;
20
+ pointer-events: none;
21
+ }
22
+
23
+ /* Loading state — spinning indicator */
24
+ @keyframes spinner-rotate {
25
+ from { transform: rotate(0deg); }
26
+ to { transform: rotate(360deg); }
27
+ }
28
+ /* Reusable spin utility: rotates the element (e.g. a refresh Icon used as a
29
+ busy indicator) via the shared spinner-rotate keyframe. Respects reduced
30
+ motion. Use instead of a decorative rotation glyph. */
31
+ .ds-spin { animation: spinner-rotate 0.8s linear infinite; transform-origin: center; }
32
+ @media (prefers-reduced-motion: reduce) { .ds-spin { animation: none; } }
33
+
34
+ .loading { position: relative; }
35
+ .loading::after {
36
+ content: '';
37
+ position: absolute; top: 50%; right: 12px;
38
+ width: 16px; height: 16px;
39
+ border: 2px solid var(--bg-3);
40
+ border-top-color: var(--accent);
41
+ border-radius: 50%;
42
+ animation: spinner-rotate 0.8s linear infinite;
43
+ }
44
+
45
+ /* Error state */
46
+ .error, [data-state="error"],
47
+ .row.error, input.error,
48
+ textarea.error {
49
+ border-color: var(--warn) !important;
50
+ background-color: color-mix(in oklab, var(--warn) 6%, var(--bg));
51
+ }
52
+ .error::placeholder { color: color-mix(in oklab, var(--warn) 60%, var(--fg-3)); }
53
+
54
+ /* Success state */
55
+ .success, [data-state="success"],
56
+ .row.success { background-color: color-mix(in oklab, var(--green-2) 6%, var(--bg)); }
57
+
58
+ /* Readonly state */
59
+ [readonly], .readonly {
60
+ background: var(--bg-2);
61
+ cursor: default;
62
+ opacity: 0.8;
63
+ }
64
+
65
+ /* ------------------------------------------------------------
66
+ Enhanced Button Micro-Interactions
67
+ -------------------------------------------------------------- */
68
+
69
+ /* Button scale feedback on active */
70
+ .btn:active:not(:disabled),
71
+ .btn-primary:active:not(:disabled),
72
+ .btn-ghost:active:not(:disabled),
73
+ button:active:not(:disabled) {
74
+ transform: scale(0.98);
75
+ }
76
+
77
+ /* Enhanced hover transitions — transition only the properties that actually
78
+ change (not `all`, which animates layout and is a perf trap), and drop the
79
+ blanket `will-change` (it forces a permanent compositor layer per element). */
80
+ .btn, .btn-primary, .btn-ghost,
81
+ button, a.row, .row,
82
+ .chip, [role="button"],
83
+ input[type="checkbox"], input[type="radio"] {
84
+ transition: background-color var(--dur-snap) var(--ease),
85
+ color var(--dur-snap) var(--ease),
86
+ box-shadow var(--dur-snap) var(--ease),
87
+ transform var(--dur-snap) var(--ease);
88
+ }
89
+
90
+ /* Native checkbox/radio tint — browser default is UA blue, which fights the
91
+ single lime lead accent wherever a raw control lands outside a bespoke
92
+ toggle (e.g. signin's "remember me"). accent-color re-tints the native
93
+ control box without hand-drawing a replacement. */
94
+ input[type="checkbox"], input[type="radio"] {
95
+ accent-color: var(--accent-ink);
96
+ }
97
+
98
+ /* Prevent double-tap zoom on buttons (iOS) */
99
+ .btn, .btn-primary, .btn-ghost, button {
100
+ -webkit-user-select: none;
101
+ -webkit-tap-highlight-color: transparent;
102
+ -webkit-touch-callout: none;
103
+ }
104
+
105
+ /* ------------------------------------------------------------
106
+ Drag-and-Drop Styles
107
+ -------------------------------------------------------------- */
108
+
109
+ /* Draggable element */
110
+ [draggable="true"], .draggable {
111
+ cursor: grab;
112
+ user-select: none;
113
+ }
114
+ [draggable="true"]:active { cursor: grabbing; }
115
+
116
+ /* Drag-over state */
117
+ .drag-over {
118
+ border: 2px dashed var(--accent);
119
+ }
120
+
121
+ /* Drag-ghost (semi-transparent dragging element) */
122
+ .drag-ghost {
123
+ opacity: 0.5;
124
+ transform: rotate(2deg) scale(0.95);
125
+ }
126
+
127
+ /* Drop-zone indicator */
128
+ .drop-zone {
129
+ border: 2px dashed var(--rule-strong);
130
+ border-radius: var(--r-2);
131
+ padding: var(--space-4);
132
+ background: color-mix(in oklab, var(--accent) 4%, var(--bg));
133
+ min-height: 120px;
134
+ display: flex; align-items: center; justify-content: center;
135
+ text-align: center; color: var(--fg-3);
136
+ transition: all var(--dur-snap) var(--ease);
137
+ }
138
+ .drop-zone.active {
139
+ border-color: var(--accent);
140
+ background: color-mix(in oklab, var(--accent) 10%, var(--bg));
141
+ color: var(--accent-ink);
142
+ }
143
+
144
+ /* List item reorder indicator */
145
+ .list-item.dragging { opacity: 0.5; }
146
+ .list-item.drag-before::before,
147
+ .list-item.drag-after::after {
148
+ content: '';
149
+ position: absolute;
150
+ left: 0; right: 0; height: 2px;
151
+ background: var(--accent);
152
+ }
153
+ .list-item.drag-before::before { top: -2px; }
154
+ .list-item.drag-after::after { bottom: -2px; }
155
+
156
+ @keyframes pulse-line {
157
+ 0%, 100% { opacity: 0.3; }
158
+ 50% { opacity: 1; }
159
+ }
160
+ @media (prefers-reduced-motion: no-preference) {
161
+ .list-item.drag-before::before,
162
+ .list-item.drag-after::after { animation: pulse-line 0.6s ease-in-out infinite; }
163
+ }
164
+
165
+ /* ------------------------------------------------------------
166
+ Context Menu Styles
167
+ -------------------------------------------------------------- */
168
+
169
+ .ds-context-menu {
170
+ position: absolute;
171
+ background: var(--bg-2);
172
+ border: 1px solid var(--rule);
173
+ border-radius: var(--r-2);
174
+ box-shadow: var(--shadow-overlay);
175
+ min-width: 160px;
176
+ z-index: calc(var(--z-overlay, 1000) + 1);
177
+ padding: 4px 0;
178
+ }
179
+
180
+ @keyframes context-menu-in {
181
+ from {
182
+ opacity: 0;
183
+ transform: scale(0.95) translateY(-4px);
184
+ }
185
+ to {
186
+ opacity: 1;
187
+ transform: scale(1) translateY(0);
188
+ }
189
+ }
190
+ @media (prefers-reduced-motion: no-preference) {
191
+ .ds-context-menu { animation: context-menu-in 0.15s ease-out; }
192
+ }
193
+
194
+ .ds-context-menu-item {
195
+ display: flex; align-items: center; gap: 10px;
196
+ padding: 10px 14px;
197
+ cursor: pointer;
198
+ color: var(--fg);
199
+ font-size: var(--fs-sm);
200
+ transition: background var(--dur-snap) var(--ease);
201
+ }
202
+ .ds-context-menu-item:hover {
203
+ background: var(--bg-3);
204
+ color: var(--accent-ink);
205
+ }
206
+ .ds-context-menu-item:active {
207
+ background: color-mix(in oklab, var(--accent) 20%, var(--bg-3));
208
+ }
209
+ .ds-context-menu-item.danger {
210
+ color: var(--warn);
211
+ }
212
+ .ds-context-menu-item.danger:hover {
213
+ background: color-mix(in oklab, var(--warn) 12%, var(--bg));
214
+ }
215
+ .ds-context-menu-divider {
216
+ height: 1px;
217
+ background: var(--rule);
218
+ margin: 4px 0;
219
+ }
220
+ .ds-context-menu-icon {
221
+ width: 16px; height: 16px;
222
+ flex-shrink: 0;
223
+ opacity: 0.7;
224
+ }
225
+
226
+ /* Context menu trigger indicator */
227
+ [data-has-context-menu] {
228
+ cursor: context-menu;
229
+ }
230
+
231
+ /* ------------------------------------------------------------
232
+ Enhanced Form Inputs
233
+ -------------------------------------------------------------- */
234
+
235
+ /* Base chrome for any BARE native control (outside .ds-field/.row-form/.input):
236
+ the same editorial tonal-fill + printed-baseline-rule as .ds-field, so a raw
237
+ <input> is never a browser-default white box in dark mode. The guards are
238
+ wrapped in :where() so they add ZERO specificity — the whole selector stays
239
+ at (0,1,1), exactly like `.ds-field input`, so the later .ds-field / .ds-field--sm
240
+ / .row-form descendant rules win by source order instead of being out-specified
241
+ (a bare :not(.input) counts as a class and would beat every .ds-field rule,
242
+ which clobbered the size variants). */
243
+ input[type="text"]:where(:not(.input):not(.ds-search-input)),
244
+ input[type="email"]:where(:not(.input):not(.ds-search-input)),
245
+ input[type="password"]:where(:not(.input):not(.ds-search-input)),
246
+ input[type="number"]:where(:not(.input):not(.ds-search-input)),
247
+ input[type="search"]:where(:not(.input):not(.ds-search-input)),
248
+ input[type="url"]:where(:not(.input):not(.ds-search-input)),
249
+ input[type="tel"]:where(:not(.input):not(.ds-search-input)),
250
+ textarea:where(:not(.input)),
251
+ select:where(:not(.ds-select)) {
252
+ font: inherit; color: var(--fg);
253
+ background: var(--bg-2);
254
+ border: 0;
255
+ box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
256
+ inset 0 calc(-1 * var(--bw-rule)) 0 var(--rule-strong);
257
+ border-radius: var(--r-1);
258
+ padding: 0 var(--space-3);
259
+ transition: background var(--dur-snap) var(--ease),
260
+ border-color var(--dur-snap) var(--ease),
261
+ box-shadow var(--dur-snap) var(--ease);
262
+ }
263
+ /* Single-line bare controls snap to the field height; textarea keeps auto
264
+ height with symmetric padding (its content decides the box). */
265
+ input[type="text"]:where(:not(.input):not(.ds-search-input)),
266
+ input[type="email"]:where(:not(.input):not(.ds-search-input)),
267
+ input[type="password"]:where(:not(.input):not(.ds-search-input)),
268
+ input[type="number"]:where(:not(.input):not(.ds-search-input)),
269
+ input[type="search"]:where(:not(.input):not(.ds-search-input)),
270
+ input[type="url"]:where(:not(.input):not(.ds-search-input)),
271
+ input[type="tel"]:where(:not(.input):not(.ds-search-input)),
272
+ select:where(:not(.ds-select)) {
273
+ height: var(--field-height);
274
+ }
275
+ textarea:where(:not(.input)) { padding: var(--space-2) var(--space-3); min-height: calc(4 * 1.5em); }
276
+ input[type="text"]:where(:not(.input):not(.ds-search-input)):focus-visible,
277
+ input[type="email"]:where(:not(.input):not(.ds-search-input)):focus-visible,
278
+ input[type="password"]:where(:not(.input):not(.ds-search-input)):focus-visible,
279
+ input[type="number"]:where(:not(.input):not(.ds-search-input)):focus-visible,
280
+ input[type="search"]:where(:not(.input):not(.ds-search-input)):focus-visible,
281
+ input[type="url"]:where(:not(.input):not(.ds-search-input)):focus-visible,
282
+ input[type="tel"]:where(:not(.input):not(.ds-search-input)):focus-visible,
283
+ textarea:where(:not(.input)):focus-visible,
284
+ select:where(:not(.ds-select)):focus-visible {
285
+ outline: none;
286
+ background: var(--bg);
287
+ box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
288
+ inset 0 calc(-1 * var(--bw-chunk)) 0 var(--accent);
289
+ }
290
+
291
+ input[type="text"]::placeholder,
292
+ input[type="email"]::placeholder,
293
+ input[type="password"]::placeholder,
294
+ input[type="number"]::placeholder,
295
+ input[type="search"]::placeholder,
296
+ input[type="url"]::placeholder,
297
+ textarea::placeholder {
298
+ color: var(--fg-3);
299
+ }
300
+
301
+ /* Long-form reading voice — quiet sans, generous line-height, capped to a
302
+ readable measure so a wide viewport never stretches body copy edge to
303
+ edge. Links use --accent-ink (never the bare fill, which is unreadable
304
+ as text). */
305
+ .prose {
306
+ max-width: var(--measure);
307
+ font-family: var(--ff-body); font-size: var(--fs-body); line-height: 1.6;
308
+ color: var(--fg);
309
+ }
310
+ .prose p { margin: 0 0 var(--space-4) 0; }
311
+ .prose p:last-child { margin-bottom: 0; }
312
+ .prose a { color: var(--accent-ink); text-decoration-color: var(--rule-strong); text-underline-offset: 0.15em; }
313
+ .prose a:hover { text-decoration-color: currentColor; }
314
+
315
+ /* Rubber-stamp motif — a bordered, uppercase, letter-spaced badge with a
316
+ slight built-in rotation (variants may override via inline transform for
317
+ a stronger tilt). Max 1 per page per the component's own caption. */
318
+ .stamp {
319
+ display: inline-block;
320
+ font-family: var(--ff-mono); font-size: var(--fs-xs); font-weight: 700;
321
+ text-transform: uppercase; letter-spacing: 0.08em;
322
+ padding: var(--space-1-5, var(--space-2)) var(--space-3);
323
+ border: 2px solid var(--fg);
324
+ border-radius: var(--r-hair);
325
+ color: var(--fg);
326
+ transform: rotate(-2deg);
327
+ white-space: nowrap;
328
+ }
329
+ .stamp.ink { background: var(--ink); border-color: var(--ink); color: var(--paper); }
330
+ .stamp.acid { background: var(--accent); border-color: var(--accent-ink); color: var(--accent-ink); }
331
+ .stamp.purple { border-color: var(--purple-2); color: var(--purple-2); }
332
+ .stamp.mascot { border-color: var(--mascot); color: var(--mascot-deep); }
333
+
334
+ /* Same rubber-stamp face, but a real interactive <button> — resets UA button
335
+ chrome so it isn't blue link text in a browser default box. */
336
+ .btn-stamp {
337
+ display: inline-block; cursor: pointer;
338
+ font-family: var(--ff-mono); font-size: var(--fs-xs); font-weight: 700;
339
+ text-transform: uppercase; letter-spacing: 0.08em;
340
+ padding: var(--space-1-5, var(--space-2)) var(--space-3);
341
+ border: 2px solid var(--fg);
342
+ border-radius: var(--r-hair);
343
+ background: transparent; color: var(--fg);
344
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
345
+ }
346
+ .btn-stamp:hover { background: var(--panel-hover); }
347
+ .btn-stamp.acid { border-color: var(--accent-ink); color: var(--accent-ink); }
348
+ .btn-stamp.acid:hover { background: var(--accent-tint); }
349
+ .btn-stamp.purple { border-color: var(--purple-2); color: var(--purple-2); }
350
+ .btn-stamp.mascot { border-color: var(--mascot); color: var(--mascot-deep); }
351
+
352
+ /* Structural hairlines — "no boxes, no cards, structure comes from rules."
353
+ Three weights: .rule (default separator), .rule-double (major section
354
+ break), .rule-dotted (optional/skippable). Applied to a bare <hr>, which
355
+ otherwise falls back to the browser's default inset groove line. */
356
+ hr.rule, hr.rule-double, hr.rule-dotted {
357
+ width: 100%; height: 0; border: 0;
358
+ }
359
+ hr.rule { border-top: 1px solid var(--rule); }
360
+ hr.rule-double { border-top: 3px double var(--rule-strong); }
361
+ hr.rule-dotted { border-top: 1px dotted var(--rule-strong); }
362
+
363
+ /* Hairline-only text input — no box, no fill. A single bottom rule that
364
+ thickens to 2px on focus, matching the row/rule zero-border aesthetic
365
+ instead of a browser-default boxed field. */
366
+ .input {
367
+ display: block; width: 100%; box-sizing: border-box;
368
+ border: 0; border-bottom: 1px solid var(--rule-strong);
369
+ border-radius: var(--r-0);
370
+ background: transparent;
371
+ padding: var(--space-2) var(--space-1);
372
+ font: inherit; color: var(--fg);
373
+ }
374
+ .input::placeholder { color: var(--fg-3); }
375
+ .input:focus-visible {
376
+ outline: none;
377
+ border-bottom-width: 2px;
378
+ border-bottom-color: var(--accent-ink);
379
+ }
380
+
381
+ /* -- Field controls: themed base for TextField / Select / SearchInput --
382
+ Root fix: previously only `transition` was set, so themed apps got
383
+ browser-default white boxes in dark mode and labels collided with inputs
384
+ because `.ds-field` had no layout. */
385
+ .ds-field {
386
+ display: flex;
387
+ flex-direction: column;
388
+ gap: var(--space-1, 6px);
389
+ align-items: stretch;
390
+ width: 100%;
391
+ }
392
+ .ds-field-label {
393
+ font-size: var(--fs-sm, 14px);
394
+ color: var(--fg-2);
395
+ line-height: 1.3;
396
+ }
397
+ .ds-field-hint {
398
+ font-size: var(--fs-tiny, 13px);
399
+ color: var(--fg-3);
400
+ line-height: 1.35;
401
+ }
402
+ .ds-field-count {
403
+ font-size: var(--fs-tiny, 13px);
404
+ color: var(--fg-3);
405
+ align-self: flex-end;
406
+ }
407
+
408
+ .ds-field input,
409
+ .ds-field textarea,
410
+ .ds-field .ds-select,
411
+ .ds-search-input {
412
+ width: 100%;
413
+ box-sizing: border-box;
414
+ font: inherit;
415
+ color: var(--fg);
416
+ background: var(--bg-2);
417
+ border: 0;
418
+ /* Editorial field — tonal fill with a printed baseline rule that thickens to
419
+ the electric lead on focus, instead of a uniform hairline box. */
420
+ box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
421
+ inset 0 calc(-1 * var(--bw-rule)) 0 var(--rule-strong);
422
+ /* Tighter corner than the old --r-2 (14px): a dense pro control, not a soft
423
+ consumer box. Single-line controls snap to --field-height with h-only
424
+ padding so they align pixel-exact with buttons on the same --ctl-md rung. */
425
+ border-radius: var(--r-1);
426
+ height: var(--field-height);
427
+ padding: 0 var(--space-3);
428
+ transition: box-shadow var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
429
+ }
430
+ /* textarea is multi-line: height is auto (min-height instead), keep symmetric
431
+ vertical padding so text doesn't sit flush to the top edge. */
432
+ .ds-field textarea { height: auto; min-height: calc(4 * 1.5em); padding: var(--space-2) var(--space-3); resize: vertical; }
433
+
434
+ /* Field size ladder (size prop on TextField/Select/SearchInput). md == base.
435
+ sm/lg retune height + h-padding + font off the --ctl-* ladder, matching the
436
+ button ladder so a form of mixed controls stays on one rhythm. */
437
+ .ds-field--sm input, .ds-field--sm .ds-select, .ds-field--sm .ds-search-input {
438
+ height: var(--ctl-sm); padding: 0 var(--space-2-5); font-size: var(--fs-tiny);
439
+ }
440
+ .ds-field--sm textarea { height: auto; padding: var(--space-1-75) var(--space-2-5); font-size: var(--fs-tiny); }
441
+ .ds-field--lg input, .ds-field--lg .ds-select, .ds-field--lg .ds-search-input {
442
+ height: var(--ctl-lg); padding: 0 var(--space-4); font-size: var(--fs-body);
443
+ }
444
+ .ds-field--lg textarea { height: auto; padding: var(--space-2-5) var(--space-4); font-size: var(--fs-body); }
445
+ .ds-search-input::placeholder { color: var(--fg-3); }
446
+ /* Only present when SearchInput is passed resultCount or has a value (clear
447
+ button) - transparent to any flex/grid layout the bare input previously
448
+ sat in directly. */
449
+ .ds-search-input-wrap { display: contents; }
450
+
451
+ /* Visible clear (X) button for SearchInput - mirrors .ds-alert-dismiss's
452
+ ghost-icon treatment but sized to the kit's 44px touch-target floor
453
+ (app-shell.css already establishes this convention for .ds-file-check /
454
+ .ds-density-btn / .ds-upload-act etc). Positioned by the consumer's own
455
+ flex/grid context (ds-search-input-wrap is display:contents), so it simply
456
+ sits after the input in DOM order. */
457
+ .ds-search-clear {
458
+ display: inline-flex;
459
+ align-items: center; justify-content: center;
460
+ min-width: 44px; min-height: 44px;
461
+ flex-shrink: 0;
462
+ cursor: pointer;
463
+ color: var(--fg-3);
464
+ background: transparent;
465
+ border: 0;
466
+ padding: 0;
467
+ border-radius: var(--r-1);
468
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
469
+ }
470
+ .ds-search-clear:hover { background: var(--bg-2); color: var(--fg); }
471
+ .ds-search-clear:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
472
+
473
+ .ds-field input:focus-visible,
474
+ .ds-field textarea:focus-visible,
475
+ .ds-field .ds-select:focus-visible,
476
+ .ds-search-input:focus-visible {
477
+ outline: none;
478
+ background: var(--bg);
479
+ /* Focus thickens the baseline to the lead accent — the printed-rule tell. */
480
+ box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
481
+ inset 0 calc(-1 * var(--bw-chunk)) 0 var(--accent);
482
+ }
483
+ .ds-field input:disabled,
484
+ .ds-field textarea:disabled,
485
+ .ds-field .ds-select:disabled {
486
+ opacity: .55;
487
+ cursor: not-allowed;
488
+ }
489
+ .ds-field input[aria-invalid="true"],
490
+ .ds-field textarea[aria-invalid="true"] {
491
+ box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--flame);
492
+ }
493
+
494
+ /* Clear button for text inputs */
495
+ input[type="text"]:not(:placeholder-shown) + .input-clear,
496
+ input[type="email"]:not(:placeholder-shown) + .input-clear,
497
+ input[type="password"]:not(:placeholder-shown) + .input-clear {
498
+ display: inline-flex;
499
+ }
500
+
501
+ .input-clear {
502
+ display: none;
503
+ align-items: center; justify-content: center;
504
+ width: 32px; height: 32px;
505
+ cursor: pointer;
506
+ color: var(--fg-3);
507
+ background: transparent;
508
+ border: 0;
509
+ padding: 0;
510
+ margin-right: 8px;
511
+ border-radius: var(--r-1);
512
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
513
+ }
514
+ .input-clear:hover {
515
+ background: var(--bg-2);
516
+ color: var(--fg);
517
+ }
518
+
519
+ /* ------------------------------------------------------------
520
+ Accessibility Enhancements
521
+ -------------------------------------------------------------- */
522
+
523
+ /* Base focus-visible: 2px ring + 2px offset clears dark bg-3 surfaces (WCAG 1.4.11) */
524
+ :focus-visible {
525
+ outline: var(--focus-w) solid var(--focus-color);
526
+ outline-offset: var(--focus-offset);
527
+ }
528
+
529
+ /* Enhanced focus-visible for all interactive elements */
530
+ [role="button"]:focus-visible,
531
+ [role="link"]:focus-visible,
532
+ [role="tab"]:focus-visible,
533
+ .row:focus-visible,
534
+ .row[tabindex]:focus-visible {
535
+ outline: var(--focus-w) solid var(--focus-color);
536
+ outline-offset: var(--focus-offset);
537
+ }
538
+
539
+ /* Skip to main content link */
540
+ .skip-to-main {
541
+ position: absolute; top: -40px; left: 0;
542
+ background: var(--fg); color: var(--bg);
543
+ padding: 8px 16px; border-radius: var(--r-1);
544
+ text-decoration: none; z-index: var(--z-overlay, 1000);
545
+ }
546
+ .skip-to-main:focus { top: 10px; }
547
+
548
+ /* Reduced motion preferences — scoped to the DS surface so it doesn't reach
549
+ into and neutralize the host document's own motion. */
550
+ @media (prefers-reduced-motion: reduce) {
551
+ .ds-247420 *, .app * {
552
+ animation-duration: 0.01ms !important;
553
+ animation-iteration-count: 1 !important;
554
+ transition-duration: 0.01ms !important;
555
+ }
556
+ /* Kill the residual paint frame of looping skeleton/shimmer placeholders and
557
+ give reduced-motion users a steady placeholder fill instead. */
558
+ .skeleton, .ds-skeleton, .ds-session-row-skeleton .ds-skel {
559
+ animation: none !important;
560
+ background: var(--bg-2) !important;
561
+ }
562
+ }
563
+
564
+ /* ------------------------------------------------------------
565
+ Performance
566
+ -------------------------------------------------------------- */
567
+
568
+ /* Limit layout/style containment to self-contained components. `paint` is
569
+ intentionally omitted — it clips focus rings, tooltips and dropdowns that
570
+ emanate from a contained .row/.btn. */
571
+ .panel, .row {
572
+ contain: layout style;
573
+ }
574
+
575
+ /* ------------------------------------------------------------
576
+ Mobile Touch Optimizations
577
+ -------------------------------------------------------------- */
578
+
579
+ @media (hover: none) and (pointer: coarse) {
580
+ /* Mobile devices: larger touch targets, faster responses */
581
+ .btn, .btn-primary, .btn-ghost,
582
+ button, a.row, [role="button"],
583
+ .ds-icon-btn, .composer-btn {
584
+ min-height: 48px;
585
+ min-width: 48px;
586
+ padding: 12px 20px;
587
+ }
588
+ /* Icon buttons stay square — the padding bump above would distort them. */
589
+ .ds-icon-btn, .composer-btn { padding: 0; }
590
+
591
+ /* Remove hover effects on touch devices (use active instead) */
592
+ .btn:hover, .btn-primary:hover, .btn-ghost:hover {
593
+ background-color: inherit;
594
+ }
595
+
596
+ /* Momentum scrolling for iOS */
597
+ .app-side, .panel, .row {
598
+ -webkit-overflow-scrolling: touch;
599
+ }
600
+
601
+ /* Long-press feedback for drag-enabled items */
602
+ [draggable="true"], .draggable {
603
+ -webkit-user-select: none;
604
+ -webkit-touch-callout: none;
605
+ }
606
+ }
607
+
608
+ /* Touch floor also for coarse-pointer devices that report hover (hybrid
609
+ laptops/tablets with mice) — the block above requires hover:none too. */
610
+ @media (pointer: coarse) {
611
+ .ds-icon-btn, .ds-icon-btn-xs, .ds-icon-btn-sm, .ds-icon-btn-base {
612
+ min-width: 44px; min-height: 44px;
613
+ }
614
+ .btn, .btn-primary, .btn-ghost,
615
+ .app-topbar nav a, .app-side a { min-height: 44px; }
616
+ }
617
+
618
+ /* ------------------------------------------------------------
619
+ Theme Transition Smoothness
620
+ -------------------------------------------------------------- */
621
+
622
+ /* Scoped to the DS wrapper so the design system never reaches out and
623
+ animates the host document's html/body. */
624
+ .ds-247420, .app {
625
+ transition: background-color var(--dur-base) var(--ease),
626
+ color var(--dur-base) var(--ease);
627
+ }
628
+
629
+ /* ------------------------------------------------------------
630
+ Empty State & Loading States
631
+ -------------------------------------------------------------- */
632
+
633
+ /* Skeleton loading state */
634
+ .skeleton {
635
+ background: linear-gradient(
636
+ 90deg,
637
+ var(--bg-2) 0%,
638
+ var(--bg-3) 50%,
639
+ var(--bg-2) 100%
640
+ );
641
+ background-size: 200% 100%;
642
+ animation: skeleton-load 1.5s infinite;
643
+ border-radius: var(--r-2);
644
+ }
645
+
646
+ @keyframes skeleton-load {
647
+ 0% { background-position: 200% 0; }
648
+ 100% { background-position: -200% 0; }
649
+ }
650
+
651
+ .skeleton-line { height: 12px; margin-bottom: 12px; }
652
+ .skeleton-title { height: 24px; margin-bottom: 16px; width: 60%; }
653
+
654
+ /* ------------------------------------------------------------
655
+ Toast Notification Styles
656
+ -------------------------------------------------------------- */
657
+
658
+ .toast {
659
+ position: fixed;
660
+ bottom: 24px; right: 24px;
661
+ background: var(--bg-2);
662
+ border: 1px solid var(--rule);
663
+ border-radius: var(--r-2);
664
+ padding: var(--space-3) var(--space-4);
665
+ max-width: 320px;
666
+ box-shadow: var(--shadow-overlay);
667
+ z-index: calc(var(--z-overlay, 1000) + 10);
668
+ }
669
+
670
+ @keyframes toast-slide-in {
671
+ from {
672
+ opacity: 0;
673
+ transform: translateX(400px);
674
+ }
675
+ to {
676
+ opacity: 1;
677
+ transform: translateX(0);
678
+ }
679
+ }
680
+ @media (prefers-reduced-motion: no-preference) {
681
+ .toast { animation: toast-slide-in 0.3s ease-out; }
682
+ }
683
+
684
+ .toast.error { border-color: var(--warn); }
685
+ .toast.success { border-color: var(--green-2); }
686
+ /* --sun measures well under 3:1 against every app background (1.1-1.5:1) -
687
+ a border effectively invisible in light theme; --amber is the tuned sibling
688
+ warning tone already used for this exact severity elsewhere. */
689
+ .toast.warning { border-color: var(--amber); }
690
+
691
+ @media (max-width: 480px) {
692
+ .toast {
693
+ bottom: 16px; right: 16px; left: 16px;
694
+ max-width: none;
695
+ }
696
+ }