@chai-ui/react 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 (66) hide show
  1. package/DESIGN.md +207 -0
  2. package/LICENSE +21 -0
  3. package/README.md +265 -0
  4. package/dist/ChaiProvider.d.ts +49 -0
  5. package/dist/ChaiProvider.d.ts.map +1 -0
  6. package/dist/ChaiProvider.js +54 -0
  7. package/dist/ChaiProvider.js.map +1 -0
  8. package/dist/Composer.d.ts +169 -0
  9. package/dist/Composer.d.ts.map +1 -0
  10. package/dist/Composer.js +389 -0
  11. package/dist/Composer.js.map +1 -0
  12. package/dist/MediaAnalyzer.d.ts +102 -0
  13. package/dist/MediaAnalyzer.d.ts.map +1 -0
  14. package/dist/MediaAnalyzer.js +250 -0
  15. package/dist/MediaAnalyzer.js.map +1 -0
  16. package/dist/ResultCard.d.ts +55 -0
  17. package/dist/ResultCard.d.ts.map +1 -0
  18. package/dist/ResultCard.js +409 -0
  19. package/dist/ResultCard.js.map +1 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +11 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/primitives/Pagination.d.ts +19 -0
  25. package/dist/primitives/Pagination.d.ts.map +1 -0
  26. package/dist/primitives/Pagination.js +13 -0
  27. package/dist/primitives/Pagination.js.map +1 -0
  28. package/dist/primitives/SearchMenu.d.ts +69 -0
  29. package/dist/primitives/SearchMenu.d.ts.map +1 -0
  30. package/dist/primitives/SearchMenu.js +88 -0
  31. package/dist/primitives/SearchMenu.js.map +1 -0
  32. package/dist/primitives/Toggle.d.ts +9 -0
  33. package/dist/primitives/Toggle.d.ts.map +1 -0
  34. package/dist/primitives/Toggle.js +28 -0
  35. package/dist/primitives/Toggle.js.map +1 -0
  36. package/dist/primitives/index.d.ts +4 -0
  37. package/dist/primitives/index.d.ts.map +1 -0
  38. package/dist/primitives/index.js +4 -0
  39. package/dist/primitives/index.js.map +1 -0
  40. package/dist/style.css +1860 -0
  41. package/dist/use-latest.d.ts +7 -0
  42. package/dist/use-latest.d.ts.map +1 -0
  43. package/dist/use-latest.js +17 -0
  44. package/dist/use-latest.js.map +1 -0
  45. package/dist/useComposer.d.ts +75 -0
  46. package/dist/useComposer.d.ts.map +1 -0
  47. package/dist/useComposer.js +191 -0
  48. package/dist/useComposer.js.map +1 -0
  49. package/dist/useMediaAnalyzer.d.ts +37 -0
  50. package/dist/useMediaAnalyzer.d.ts.map +1 -0
  51. package/dist/useMediaAnalyzer.js +114 -0
  52. package/dist/useMediaAnalyzer.js.map +1 -0
  53. package/package.json +88 -0
  54. package/src/ChaiProvider.tsx +88 -0
  55. package/src/Composer.tsx +961 -0
  56. package/src/MediaAnalyzer.tsx +527 -0
  57. package/src/ResultCard.tsx +980 -0
  58. package/src/index.ts +36 -0
  59. package/src/primitives/Pagination.tsx +41 -0
  60. package/src/primitives/SearchMenu.tsx +284 -0
  61. package/src/primitives/Toggle.tsx +43 -0
  62. package/src/primitives/index.ts +3 -0
  63. package/src/style.css +1860 -0
  64. package/src/use-latest.ts +18 -0
  65. package/src/useComposer.ts +289 -0
  66. package/src/useMediaAnalyzer.ts +157 -0
package/dist/style.css ADDED
@@ -0,0 +1,1860 @@
1
+ /*
2
+ * @chai-ui/react default styling.
3
+ *
4
+ * Requires @chai-ui/tokens/css to be loaded first (it defines every
5
+ * --chai-* variable referenced below) — see the package README.
6
+ *
7
+ * Class names use a `chai-` prefix with BEM-style `__element` and
8
+ * `--modifier` suffixes, one block per component or primitive.
9
+ */
10
+
11
+ @import "@fontsource/roboto/400.css";
12
+ @import "@fontsource/roboto/500.css";
13
+
14
+ /*
15
+ * Every switch in the app, locked once here rather than per call site: the
16
+ * "on" track is the brand green, and the "off" state is plain gray rather
17
+ * than Material's green-tinted neutrals. All values are Chai tokens.
18
+ */
19
+ .chai-switch {
20
+ --md-switch-selected-track-color: var(--chai-color-semantic-accent);
21
+ --md-switch-selected-hover-track-color: var(--chai-color-semantic-accent);
22
+ --md-switch-selected-focus-track-color: var(--chai-color-semantic-accent);
23
+ --md-switch-selected-pressed-track-color: var(--chai-color-semantic-accent);
24
+ --md-switch-track-color: var(--chai-color-semantic-control-track);
25
+ --md-switch-hover-track-color: var(--chai-color-semantic-control-track);
26
+ --md-switch-focus-track-color: var(--chai-color-semantic-control-track);
27
+ --md-switch-pressed-track-color: var(--chai-color-semantic-control-track);
28
+ --md-switch-track-outline-color: var(--chai-color-semantic-control-border-soft);
29
+ --md-switch-hover-track-outline-color: var(--chai-color-semantic-control-border-soft);
30
+ --md-switch-focus-track-outline-color: var(--chai-color-semantic-control-border-soft);
31
+ --md-switch-pressed-track-outline-color: var(--chai-color-semantic-control-border-soft);
32
+ --md-switch-handle-color: var(--chai-color-semantic-control-text-muted);
33
+ --md-switch-hover-handle-color: var(--chai-color-semantic-control-text-muted);
34
+ --md-switch-focus-handle-color: var(--chai-color-semantic-control-text-muted);
35
+ --md-switch-pressed-handle-color: var(--chai-color-semantic-control-text-muted);
36
+ }
37
+
38
+ /* --- Buttons --- */
39
+
40
+ .chai-btn {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ gap: 6px;
45
+ padding: 8px 14px;
46
+ /* MD3 buttons are fully-rounded (stadium) by default (m3.material.io/components/buttons). */
47
+ border-radius: var(--chai-radius-full);
48
+ border: 1px solid var(--chai-color-semantic-control-border-faint);
49
+ background: var(--chai-color-semantic-control-surface-raised);
50
+ color: var(--chai-color-semantic-control-text-strong);
51
+ font-family: var(--chai-type-family-material);
52
+ font-size: var(--chai-type-size-label-large);
53
+ font-weight: var(--chai-type-weight-material-medium);
54
+ line-height: var(--chai-type-leading-label-large);
55
+ letter-spacing: var(--chai-type-tracking-label-large);
56
+ cursor: pointer;
57
+ transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
58
+ }
59
+
60
+ .chai-btn:hover:not(:disabled) {
61
+ background: var(--chai-color-semantic-control-surface-hover);
62
+ }
63
+
64
+ .chai-btn:disabled {
65
+ opacity: 0.5;
66
+ cursor: not-allowed;
67
+ }
68
+
69
+ /*
70
+ * MD3's outlined button (m3.material.io/components/buttons#outlined-button)
71
+ * — for actions that matter (they're real, bordered buttons, not links) but
72
+ * shouldn't compete with the primary action's visual weight. Reads MD3's
73
+ * own roles rather than CHAI's --chai-color-semantic-* ones, so it stays
74
+ * visually consistent with the other MD3-native pieces around it.
75
+ */
76
+ .chai-btn--outlined {
77
+ background: transparent;
78
+ border-color: var(--chai-color-semantic-control-border);
79
+ color: var(--chai-color-semantic-accent-text);
80
+ }
81
+
82
+ .chai-btn--outlined:hover:not(:disabled) {
83
+ background: var(--chai-color-semantic-control-surface-hover);
84
+ }
85
+
86
+ /*
87
+ * MD3 assist chip (m3.material.io/components/chips#assist-chip) — a
88
+ * rounded-rectangle, not the stadium shape every `.chai-btn` uses, for
89
+ * "Select files" specifically (reference: Media Analyze - Default).
90
+ */
91
+ .chai-chip-button {
92
+ padding: 6px 14px;
93
+ border-radius: var(--chai-radius-sm);
94
+ border: 1px solid var(--chai-color-semantic-control-border);
95
+ background: var(--chai-color-semantic-control-surface-raised);
96
+ color: var(--chai-color-semantic-control-text);
97
+ font-family: var(--chai-type-family-material);
98
+ font-size: var(--chai-type-size-label-large);
99
+ font-weight: var(--chai-type-weight-material-medium);
100
+ line-height: var(--chai-type-leading-label-large);
101
+ letter-spacing: var(--chai-type-tracking-label-large);
102
+ cursor: pointer;
103
+ }
104
+
105
+ .chai-chip-button:hover:not(:disabled) {
106
+ background: var(--chai-color-semantic-control-surface-hover);
107
+ }
108
+
109
+ .chai-chip-button:disabled {
110
+ opacity: 0.5;
111
+ cursor: not-allowed;
112
+ }
113
+
114
+ /*
115
+ * --- Search menu (reference: Media Analyze - Model Menu) — the shared
116
+ * searchable-menu-button pattern behind ModelMenu and every other option
117
+ * menu (see SearchMenu.tsx). A trigger styled like the outlined
118
+ * "Show options" button, opening a self-positioned panel (not `<md-menu>`'s
119
+ * own anchor system) with a real `<md-outlined-text-field>` search field
120
+ * over a hand-rolled option list, same split as SegmentedButton: real MD3
121
+ * web component where one exists and fits, hand-rolled where the composite
122
+ * doesn't map onto a single primitive.
123
+ */
124
+ .chai-search-menu {
125
+ position: relative;
126
+ }
127
+
128
+ .chai-search-menu__trigger {
129
+ display: inline-flex;
130
+ align-items: center;
131
+ gap: 6px;
132
+ max-width: 220px;
133
+ min-width: 0;
134
+ }
135
+
136
+ /* "text" trigger variant (reference: Composer's Model/Aspect ratio triggers
137
+ — outline hidden) — label + trailing chevron only, no border/fill at
138
+ rest. Generous padding (not the base rule's tight `max-width` alone) so
139
+ the label's last character has real breathing room before the chevron,
140
+ and a real border-radius so the hover/open state below reads as a
141
+ rounded rectangle, not a hard-cornered box. */
142
+ .chai-search-menu__trigger--text {
143
+ padding: 6px 10px;
144
+ border: none;
145
+ border-radius: var(--chai-radius-sm);
146
+ background: transparent;
147
+ color: var(--chai-color-semantic-control-text-strong);
148
+ font-family: var(--chai-type-family-material);
149
+ font-size: var(--chai-type-size-label-large);
150
+ font-weight: var(--chai-type-weight-material-medium);
151
+ line-height: var(--chai-type-leading-label-large);
152
+ letter-spacing: var(--chai-type-tracking-label-large);
153
+ cursor: pointer;
154
+ transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
155
+ }
156
+
157
+ .chai-search-menu__trigger--text:hover:not(:disabled) {
158
+ background: var(--chai-color-semantic-control-surface-hover);
159
+ }
160
+
161
+ .chai-search-menu__trigger--text:disabled {
162
+ opacity: 0.5;
163
+ cursor: not-allowed;
164
+ }
165
+
166
+ /* A selected model's label (e.g. a long OpenRouter model name) is capped and
167
+ ellipsized here rather than left to grow the trigger — otherwise it can
168
+ push wide enough to overlap the card's right-aligned "Show options" /
169
+ "Generate" actions instead of wrapping under `chai-component-card__actions`'
170
+ flex-wrap. */
171
+ .chai-search-menu__trigger-label {
172
+ overflow: hidden;
173
+ text-overflow: ellipsis;
174
+ white-space: nowrap;
175
+ min-width: 0;
176
+ }
177
+
178
+ .chai-search-menu__trigger--open {
179
+ background: var(--chai-color-semantic-accent-subtle);
180
+ border-color: var(--chai-color-semantic-accent-subtle);
181
+ color: var(--chai-color-semantic-accent-text);
182
+ }
183
+
184
+ .chai-search-menu__chevron {
185
+ width: 14px;
186
+ height: 14px;
187
+ transition: transform var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
188
+ }
189
+
190
+ .chai-search-menu__chevron--open {
191
+ transform: rotate(180deg);
192
+ }
193
+
194
+ .chai-search-menu__panel {
195
+ position: absolute;
196
+ z-index: 10;
197
+ top: calc(100% + 8px);
198
+ left: 0;
199
+ width: 280px;
200
+ padding: var(--chai-space-3);
201
+ border-radius: var(--chai-radius-lg);
202
+ /* White per Figma (reference: every dropdown menu, Model + Generator). */
203
+ background: var(--chai-color-semantic-control-surface-raised);
204
+ box-shadow: var(--chai-shadow-menu);
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: var(--chai-space-2);
208
+ }
209
+
210
+ .chai-search-menu__search {
211
+ width: 100%;
212
+ }
213
+
214
+ .chai-search-menu__search-icon {
215
+ display: flex;
216
+ }
217
+
218
+ .chai-search-menu__search svg {
219
+ width: 20px;
220
+ height: 20px;
221
+ }
222
+
223
+ .chai-search-menu__clear {
224
+ padding: 0;
225
+ border: none;
226
+ background: transparent;
227
+ color: inherit;
228
+ cursor: pointer;
229
+ display: flex;
230
+ }
231
+
232
+ .chai-search-menu__list {
233
+ list-style: none;
234
+ margin: 0;
235
+ padding: 0;
236
+ max-height: 240px;
237
+ overflow-y: auto;
238
+ }
239
+
240
+ .chai-search-menu__option {
241
+ width: 100%;
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: space-between;
245
+ gap: 8px;
246
+ padding: 10px 12px;
247
+ border: none;
248
+ border-radius: var(--chai-radius-sm);
249
+ background: transparent;
250
+ color: var(--chai-color-semantic-control-text-strong);
251
+ font-family: var(--chai-type-family-material);
252
+ font-size: var(--chai-type-size-body-large);
253
+ text-align: left;
254
+ cursor: pointer;
255
+ }
256
+
257
+ .chai-search-menu__option:hover:not(:disabled) {
258
+ background: var(--chai-color-semantic-control-surface-hover);
259
+ }
260
+
261
+ .chai-search-menu__option--selected {
262
+ color: var(--chai-color-semantic-accent-text);
263
+ font-weight: var(--chai-type-weight-material-medium);
264
+ }
265
+
266
+ /* Grayed + inert while a `toggleHeader` is on (reference: the account
267
+ holder's own "Auto-select model" mock) — the row that matches `value`
268
+ keeps its checkmark regardless, so the underlying manual pick stays
269
+ visible even though it can't be changed until the toggle goes off. */
270
+ .chai-search-menu__option:disabled {
271
+ /* MD3's disabled-content spec (on-surface at 38%) — the muted text token
272
+ alone read almost the same as an enabled row. */
273
+ color: color-mix(in srgb, var(--chai-color-semantic-control-text-strong) 38%, transparent);
274
+ cursor: not-allowed;
275
+ }
276
+
277
+ .chai-search-menu__option:disabled.chai-search-menu__option--selected {
278
+ color: color-mix(in srgb, var(--chai-color-semantic-control-text-strong) 38%, transparent);
279
+ font-weight: var(--chai-type-weight-material-regular);
280
+ }
281
+
282
+ /* Long option labels (a full model name, at this menu's default width)
283
+ ellipsize instead of wrapping or pushing the checkmark out of view. */
284
+ .chai-search-menu__option-label {
285
+ flex: 1;
286
+ min-width: 0;
287
+ overflow: hidden;
288
+ text-overflow: ellipsis;
289
+ white-space: nowrap;
290
+ }
291
+
292
+ /* Multi-select's leading checkbox: display only, so clicks land on the
293
+ row button that owns selection. */
294
+ .chai-search-menu__checkbox {
295
+ flex: none;
296
+ pointer-events: none;
297
+ }
298
+
299
+ .chai-search-menu__option svg {
300
+ width: 18px;
301
+ height: 18px;
302
+ flex: none;
303
+ }
304
+
305
+ /*
306
+ * Toggle header (reference: the design note "Auto-select model"
307
+ * mock) — an MD3 outlined-field look built by hand (the notched-outline
308
+ * trick: an absolutely-positioned label with the panel's own background
309
+ * masks the border behind it) rather than a real `<md-outlined-text-field>`,
310
+ * since this needs a switch inside it, not text input.
311
+ */
312
+ .chai-search-menu__toggle-header {
313
+ position: relative;
314
+ margin-bottom: var(--chai-space-2);
315
+ padding: var(--chai-space-3);
316
+ border: 1px solid var(--chai-color-semantic-control-border);
317
+ border-radius: var(--chai-radius-sm);
318
+ }
319
+
320
+ .chai-search-menu__toggle-header-label {
321
+ position: absolute;
322
+ top: 0;
323
+ left: 10px;
324
+ transform: translateY(-50%);
325
+ padding: 0 4px;
326
+ background: var(--chai-color-semantic-control-surface-raised);
327
+ color: var(--chai-color-semantic-accent-text);
328
+ font-family: var(--chai-type-family-material);
329
+ font-size: var(--chai-type-size-label-small);
330
+ letter-spacing: var(--chai-type-tracking-label-small);
331
+ }
332
+
333
+ .chai-search-menu__toggle-header-row {
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: space-between;
337
+ gap: var(--chai-space-3);
338
+ }
339
+
340
+ .chai-search-menu__toggle-header-text {
341
+ font-family: var(--chai-type-family-material);
342
+ font-size: var(--chai-type-size-body-large);
343
+ font-weight: var(--chai-type-weight-material-medium);
344
+ color: var(--chai-color-semantic-control-text-strong);
345
+ }
346
+
347
+ .chai-search-menu__empty {
348
+ padding: 10px 12px;
349
+ color: var(--chai-color-semantic-control-text);
350
+ font-size: var(--chai-type-size-sm);
351
+ }
352
+
353
+ /* Grouped sections (reference: Media Analyzer's "Select model" menu —
354
+ Image/Video/Audio) — a plain heading + divider, not a new visual
355
+ language: `&__option`'s own rules still apply inside `&__section-list`. */
356
+ .chai-search-menu__section-list {
357
+ list-style: none;
358
+ margin: 0;
359
+ padding: 0;
360
+ }
361
+
362
+ .chai-search-menu__section-label {
363
+ margin: 0;
364
+ padding: 8px 12px 4px;
365
+ color: var(--chai-color-semantic-control-text);
366
+ font-family: var(--chai-type-family-material);
367
+ font-size: var(--chai-type-size-label-small);
368
+ font-weight: var(--chai-type-weight-material-medium);
369
+ letter-spacing: var(--chai-type-tracking-label-small);
370
+ text-transform: uppercase;
371
+ }
372
+
373
+ .chai-search-menu__divider {
374
+ margin: var(--chai-space-2) 0;
375
+ border: none;
376
+ border-top: 1px solid var(--chai-color-semantic-control-divider);
377
+ }
378
+
379
+ @keyframes chai-spin {
380
+ to {
381
+ transform: rotate(360deg);
382
+ }
383
+ }
384
+
385
+ /*
386
+ * --- Pagination (primitives/Pagination.tsx) ---
387
+ * Progress-style pager (reference: the Figma "Pagination" asset) — a pill
388
+ * that fills from page 1 up to whichever page is current, not independent
389
+ * per-page dots. Matches the mockup's own dot graphic, which is a
390
+ * rounded-rect fill growing under plain circles, not a row of
391
+ * self-contained badges.
392
+ */
393
+ .chai-pagination {
394
+ position: relative;
395
+ display: inline-flex;
396
+ align-items: center;
397
+ align-self: center;
398
+ }
399
+
400
+ /* The pill is 16px tall, centred on the 24px dot buttons. */
401
+ .chai-pagination__track {
402
+ position: absolute;
403
+ top: 4px;
404
+ bottom: 4px;
405
+ left: 0;
406
+ width: var(--chai-pagination-fill, 0%);
407
+ /* Chai UI's actual primary green — the same token `.chai-btn--primary`
408
+ and the selected Accept segment fill with — not `accent-strong`, which
409
+ is the darker shade reserved for text-on-light-bg contexts. */
410
+ background: var(--chai-color-semantic-accent);
411
+ border-radius: var(--chai-radius-full);
412
+ transition: width var(--chai-motion-duration-base) var(--chai-motion-easing-standard);
413
+ }
414
+
415
+ /* Each dot is a 24px button (WCAG 2.2 target size, 2.5.8) drawing a 6px
416
+ dot at its centre, so the dots sit 24px apart. */
417
+ .chai-pagination__dot {
418
+ position: relative;
419
+ width: 24px;
420
+ height: 24px;
421
+ padding: 0;
422
+ border: none;
423
+ border-radius: 50%;
424
+ background: transparent;
425
+ cursor: pointer;
426
+ }
427
+
428
+ .chai-pagination__dot::before {
429
+ content: "";
430
+ position: absolute;
431
+ inset: 9px;
432
+ border-radius: 50%;
433
+ background: var(--chai-color-semantic-control-border-faint);
434
+ }
435
+
436
+ .chai-pagination__dot--filled::before {
437
+ background: var(--chai-color-semantic-text-on-accent);
438
+ }
439
+
440
+ /*
441
+ * --- Composer (bar layout) ---
442
+ * Reference: the "Compose Component" design. Its flat cool gray comes from the control-* semantic tokens, and
443
+ * the --chai-composer-* properties below are the per-component hooks for
444
+ * restyling it.
445
+ */
446
+ .chai-composer {
447
+ --chai-composer-bg: var(--chai-color-semantic-control-surface);
448
+ --chai-composer-border: var(--chai-color-semantic-control-border);
449
+ --chai-composer-placeholder: var(--chai-color-semantic-control-text-muted);
450
+ --chai-composer-text: var(--chai-color-semantic-control-text);
451
+ /* Every hover/open state in this component. */
452
+ --chai-composer-hover: var(--chai-color-semantic-control-surface-hover);
453
+
454
+ display: flex;
455
+ flex-direction: column;
456
+ gap: var(--chai-space-3);
457
+ padding: var(--chai-space-4) var(--chai-space-5);
458
+ /* Bespoke pill radius, doesn't match the --chai-radius-* scale (max lg
459
+ is 16px) — this container is meant to read rounder than any existing
460
+ card. */
461
+ border-radius: 22px;
462
+ border: 1px solid var(--chai-composer-border);
463
+ background: var(--chai-composer-bg);
464
+ font-family: var(--chai-type-family-material);
465
+ }
466
+
467
+ .chai-composer__thumbnails {
468
+ display: flex;
469
+ flex-wrap: wrap;
470
+ gap: var(--chai-space-3);
471
+ }
472
+
473
+ .chai-composer__thumb {
474
+ position: relative;
475
+ width: 88px;
476
+ height: 88px;
477
+ flex: none;
478
+ border-radius: var(--chai-radius-sm);
479
+ border: 1px solid var(--chai-color-semantic-control-divider);
480
+ background: var(--chai-composer-bg);
481
+ overflow: hidden;
482
+ display: flex;
483
+ align-items: center;
484
+ justify-content: center;
485
+ }
486
+
487
+ .chai-composer__thumb-img {
488
+ width: 100%;
489
+ height: 100%;
490
+ object-fit: cover;
491
+ }
492
+
493
+ .chai-composer__thumb-icon {
494
+ width: 28px;
495
+ height: 28px;
496
+ color: var(--chai-composer-placeholder);
497
+ }
498
+
499
+ .chai-composer__thumb-icon svg {
500
+ width: 100%;
501
+ height: 100%;
502
+ }
503
+
504
+ /* Hidden until the thumbnail is hovered/focused, same treatment as the
505
+ use-case chip's own remove button. */
506
+ .chai-composer__thumb-remove {
507
+ position: absolute;
508
+ top: 4px;
509
+ right: 4px;
510
+ width: 20px;
511
+ height: 20px;
512
+ display: flex;
513
+ align-items: center;
514
+ justify-content: center;
515
+ border: none;
516
+ border-radius: var(--chai-radius-full);
517
+ background: var(--chai-color-semantic-overlay);
518
+ color: var(--chai-color-semantic-on-overlay);
519
+ cursor: pointer;
520
+ opacity: 0;
521
+ transition: opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
522
+ }
523
+
524
+ .chai-composer__thumb:hover .chai-composer__thumb-remove,
525
+ .chai-composer__thumb:focus-within .chai-composer__thumb-remove {
526
+ opacity: 1;
527
+ }
528
+
529
+ .chai-composer__thumb-remove svg {
530
+ width: 14px;
531
+ height: 14px;
532
+ }
533
+
534
+ .chai-composer__field {
535
+ position: relative;
536
+ display: flex;
537
+ }
538
+
539
+ /* Shared by the real `<textarea>` and its reveal-overlay `<div>` (below) —
540
+ one source for the font/padding rules that must match exactly for the
541
+ overlay's text to sit pixel-for-pixel on top of the real thing, rather
542
+ than two copies that could quietly drift apart. */
543
+ .chai-composer__type {
544
+ padding: 0;
545
+ /* Clears the optimize button's full 40px hit box (see its own comment)
546
+ plus real breathing room — at the old 36px, typed text ran right up
547
+ against the icon before wrapping. */
548
+ padding-right: 56px;
549
+ overflow-wrap: break-word;
550
+ font-family: var(--chai-type-family-material);
551
+ /* Body Large, not the mock's own literal Title Large spec (22px) — reads
552
+ noticeably heavy/large once actually rendered live, a direct design correction over the original Figma value. */
553
+ font-size: var(--chai-type-size-body-large);
554
+ font-weight: var(--chai-type-weight-material-regular);
555
+ line-height: var(--chai-type-leading-body-large);
556
+ letter-spacing: var(--chai-type-tracking-body-large);
557
+ }
558
+
559
+ .chai-composer__input {
560
+ flex: 1;
561
+ resize: none;
562
+ overflow: hidden;
563
+ border: none;
564
+ background: transparent;
565
+ outline: none;
566
+ color: var(--chai-composer-text);
567
+ }
568
+
569
+ .chai-composer__input::placeholder {
570
+ color: var(--chai-composer-placeholder);
571
+ }
572
+
573
+ .chai-composer__input:disabled {
574
+ opacity: 0.6;
575
+ }
576
+
577
+ /* The optimize/undo reveal (Composer.tsx's own doc comment on the overlay
578
+ for the full mechanics) — the real textarea's text goes fully
579
+ transparent so only the overlay's progressive, gradient-lit copy shows
580
+ underneath it, not both at once. */
581
+ .chai-composer__input--revealing {
582
+ color: transparent;
583
+ }
584
+
585
+ .chai-composer__reveal {
586
+ position: absolute;
587
+ inset: 0;
588
+ white-space: pre-wrap;
589
+ pointer-events: none;
590
+ }
591
+
592
+ .chai-composer__reveal-text {
593
+ background: var(--chai-gradient-ai);
594
+ background-size: 300% 100%;
595
+ -webkit-background-clip: text;
596
+ background-clip: text;
597
+ color: transparent;
598
+ animation: chai-composer-reveal-shimmer 1.4s linear infinite;
599
+ }
600
+
601
+ @keyframes chai-composer-reveal-shimmer {
602
+ from {
603
+ background-position: 0% 50%;
604
+ }
605
+ to {
606
+ background-position: 300% 50%;
607
+ }
608
+ }
609
+
610
+ @media (prefers-reduced-motion: reduce) {
611
+ .chai-composer__reveal-text {
612
+ animation: none;
613
+ }
614
+ }
615
+
616
+ /* Width matches `<md-fab size="small">`'s own container width (MD3 spec:
617
+ 40px) so the icon centers over the submit FAB below it, not over this
618
+ button's own (much narrower) hit target — same right edge, different
619
+ widths, was reading visibly off-center from the FAB's icon. */
620
+ .chai-composer__optimize {
621
+ position: absolute;
622
+ top: 0;
623
+ right: 0;
624
+ display: flex;
625
+ align-items: center;
626
+ justify-content: center;
627
+ width: 40px;
628
+ height: 24px;
629
+ padding: 0;
630
+ border: none;
631
+ background: transparent;
632
+ color: var(--chai-composer-text);
633
+ cursor: pointer;
634
+ }
635
+
636
+ .chai-composer__optimize svg {
637
+ width: 24px;
638
+ height: 24px;
639
+ }
640
+
641
+ .chai-composer__optimize:disabled {
642
+ cursor: not-allowed;
643
+ opacity: 0.5;
644
+ }
645
+
646
+ .chai-composer__optimize:hover:not(:disabled),
647
+ .chai-composer__optimize--active {
648
+ /* The traced Optimize icon's "active" fill: the brand green. */
649
+ color: var(--chai-color-semantic-accent);
650
+ }
651
+
652
+ .chai-composer__controls {
653
+ display: flex;
654
+ align-items: center;
655
+ justify-content: space-between;
656
+ gap: var(--chai-space-3);
657
+ }
658
+
659
+ .chai-composer__controls-start {
660
+ display: flex;
661
+ align-items: center;
662
+ gap: var(--chai-space-3);
663
+ flex-wrap: wrap;
664
+ }
665
+
666
+ /* Neutral trigger text, not `--md-sys-color-primary` green (account
667
+ holder correction: "don't make dropdown select menu buttons green when
668
+ they don't need to be"). */
669
+ .chai-composer .chai-search-menu__trigger {
670
+ color: var(--chai-composer-text);
671
+ }
672
+
673
+ /* The shared `--trigger--open` rule fills with `--md-sys-color-secondary-
674
+ container` (brand green) — right for Model/Actions menus elsewhere, but
675
+ this component is deliberately neutral throughout (see the container
676
+ comment at the top of this section), so the open state here stays a
677
+ plain rounded-rectangle highlight instead. */
678
+ .chai-composer .chai-search-menu__trigger--open {
679
+ background: var(--chai-composer-hover);
680
+ border-color: transparent;
681
+ color: var(--chai-composer-text);
682
+ }
683
+
684
+ .chai-composer .chai-search-menu__trigger--text:hover:not(:disabled) {
685
+ background: var(--chai-composer-hover);
686
+ }
687
+
688
+ .chai-composer__attach {
689
+ position: relative;
690
+ }
691
+
692
+ .chai-composer__attach-toggle {
693
+ display: flex;
694
+ align-items: center;
695
+ justify-content: center;
696
+ width: 24px;
697
+ height: 24px;
698
+ padding: 0;
699
+ border: none;
700
+ border-radius: var(--chai-radius-full);
701
+ background: transparent;
702
+ color: var(--chai-composer-text);
703
+ cursor: pointer;
704
+ }
705
+
706
+ .chai-composer__attach-toggle svg {
707
+ width: 24px;
708
+ height: 24px;
709
+ }
710
+
711
+ .chai-composer__attach-toggle--open {
712
+ background: var(--chai-composer-hover);
713
+ }
714
+
715
+ .chai-composer__attach-toggle:disabled {
716
+ cursor: not-allowed;
717
+ opacity: 0.5;
718
+ }
719
+
720
+ .chai-composer__attach-menu {
721
+ position: absolute;
722
+ z-index: 10;
723
+ bottom: calc(100% + 8px);
724
+ left: 0;
725
+ min-width: 180px;
726
+ margin: 0;
727
+ padding: var(--chai-space-2) 0;
728
+ list-style: none;
729
+ border-radius: var(--chai-radius-sm);
730
+ background: var(--chai-color-semantic-control-surface-raised);
731
+ box-shadow: var(--chai-shadow-menu);
732
+ }
733
+
734
+ .chai-composer__attach-menu-item {
735
+ display: flex;
736
+ align-items: center;
737
+ gap: var(--chai-space-3);
738
+ width: 100%;
739
+ padding: var(--chai-space-2) var(--chai-space-4);
740
+ border: none;
741
+ background: transparent;
742
+ color: var(--chai-color-semantic-control-text-strong);
743
+ font-family: var(--chai-type-family-material);
744
+ font-size: var(--chai-type-size-label-large);
745
+ font-weight: var(--chai-type-weight-material-medium);
746
+ line-height: var(--chai-type-leading-label-large);
747
+ letter-spacing: var(--chai-type-tracking-label-large);
748
+ text-align: left;
749
+ cursor: pointer;
750
+ }
751
+
752
+ .chai-composer__attach-menu-item:hover {
753
+ background: var(--chai-composer-hover);
754
+ }
755
+
756
+ .chai-composer__attach-menu-icon {
757
+ width: 20px;
758
+ height: 20px;
759
+ flex: none;
760
+ color: var(--chai-color-semantic-control-text);
761
+ }
762
+
763
+ .chai-composer__attach-menu-icon svg {
764
+ width: 100%;
765
+ height: 100%;
766
+ }
767
+
768
+ /* Reference: "Add media" (`MenuListItem1`) is the only item with a divider
769
+ under it in the source (`showDivider: true`), separating the real
770
+ attach action from the three generation-mode actions below it. */
771
+ .chai-composer__attach-menu-divider {
772
+ margin: var(--chai-space-2) 0;
773
+ border: none;
774
+ border-top: 1px solid var(--chai-color-semantic-control-divider);
775
+ }
776
+
777
+ /* Hand-rolled use-case chip (see `UseCaseChip`'s own comment for why this
778
+ isn't `<md-input-chip>`) — rounded square, not MD3 input chip's stadium
779
+ default, no leading icon (that's the attach-menu's own affordance, not
780
+ the chip's — see `UseCaseChip`). No `gap` here (unlike other rows in
781
+ this component) — the remove button's own width/margin do that job
782
+ instead, collapsed to 0 at rest so the chip is genuinely narrower by
783
+ default, not just hiding an already-reserved slot; hovering (or
784
+ focusing) the chip expands it to reveal the button, rather than fading
785
+ an icon in place. */
786
+ .chai-composer__chip {
787
+ position: relative;
788
+ display: inline-flex;
789
+ align-items: center;
790
+ padding: 6px 10px;
791
+ border-radius: var(--chai-radius-sm);
792
+ border: 1px solid var(--chai-composer-border);
793
+ background: var(--chai-composer-bg);
794
+ color: var(--chai-composer-text);
795
+ font-family: var(--chai-type-family-material);
796
+ font-size: var(--chai-type-size-label-large);
797
+ font-weight: var(--chai-type-weight-material-medium);
798
+ line-height: var(--chai-type-leading-label-large);
799
+ letter-spacing: var(--chai-type-tracking-label-large);
800
+ white-space: nowrap;
801
+ transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
802
+ }
803
+
804
+ .chai-composer__chip:hover {
805
+ background: var(--chai-composer-hover);
806
+ }
807
+
808
+ .chai-composer__chip-remove {
809
+ display: flex;
810
+ align-items: center;
811
+ justify-content: center;
812
+ width: 0;
813
+ height: 16px;
814
+ margin-left: 0;
815
+ padding: 0;
816
+ border: none;
817
+ border-radius: var(--chai-radius-full);
818
+ background: transparent;
819
+ color: var(--chai-composer-text);
820
+ cursor: pointer;
821
+ opacity: 0;
822
+ overflow: hidden;
823
+ flex: none;
824
+ transition:
825
+ width var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
826
+ margin-left var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
827
+ opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
828
+ }
829
+
830
+ .chai-composer__chip-remove svg {
831
+ width: 16px;
832
+ height: 16px;
833
+ flex: none;
834
+ }
835
+
836
+ .chai-composer__chip:hover .chai-composer__chip-remove,
837
+ .chai-composer__chip:focus-within .chai-composer__chip-remove {
838
+ width: 16px;
839
+ margin-left: 6px;
840
+ opacity: 1;
841
+ }
842
+
843
+ .chai-composer__chip-remove:disabled {
844
+ cursor: not-allowed;
845
+ }
846
+
847
+ .chai-composer__submit {
848
+ flex: none;
849
+ /* Flat always, both states — the mock has no FAB shadow at all. */
850
+ --md-fab-container-elevation: 0;
851
+ --md-fab-hover-container-elevation: 0;
852
+ --md-fab-pressed-container-elevation: 0;
853
+ --md-fab-focus-container-elevation: 0;
854
+ }
855
+
856
+ /* Unfilled rest state — icon only, no visible container, until there's a
857
+ valid prompt. Not M3's "surface" variant: a green-seeded theme's own
858
+ surface containers still pick up a faint tint from dynamic color, so
859
+ overriding the secondary-variant custom properties directly was the only
860
+ way to get a genuinely flat rest state. MD3's own hover state layer
861
+ (unaffected by these overrides) still gives the "slight color change on
862
+ hover" the mock wants, with no extra rule needed. Once `[data-ready]` is
863
+ set, neither override applies and `<md-fab>`'s real `variant="secondary"`
864
+ colors (the brand-green bridge) show through on their own. */
865
+ .chai-composer__submit:not([data-ready]) {
866
+ --md-fab-secondary-container-color: transparent;
867
+ --md-fab-secondary-icon-color: var(--chai-composer-text);
868
+ }
869
+
870
+ /* Retry state — real `--chai-color-semantic-danger*` tokens, not this
871
+ component's own literal-hex palette: unlike the neutral chrome, an
872
+ error should look the same "error" everywhere in the app, not just in
873
+ Composer. `[data-error]` can co-occur with `[data-ready]` (retry isn't
874
+ gated on `hasContent`, see `submitError`'s doc comment), so this rule
875
+ is scoped only to `[data-error]` and always wins when it's set. */
876
+ .chai-composer__submit[data-error] {
877
+ --md-fab-secondary-container-color: var(--chai-color-semantic-danger-subtle);
878
+ --md-fab-secondary-icon-color: var(--chai-color-semantic-danger);
879
+ }
880
+
881
+ /* Stop state — real `--chai-color-semantic-warning*` tokens (a light
882
+ orange), same reasoning as `[data-error]` above: reuses an existing
883
+ system color rather than inventing a one-off literal hex for this one
884
+ moment. Declared before `[data-error]` so error still wins if both were
885
+ ever true (they aren't, in practice — a request is either in flight or
886
+ already failed, never both), matching that rule's own documented
887
+ precedence. */
888
+ .chai-composer__submit[data-stop] {
889
+ --md-fab-secondary-container-color: var(--chai-color-semantic-warning-subtle);
890
+ --md-fab-secondary-icon-color: var(--chai-color-semantic-warning);
891
+ }
892
+
893
+ .chai-composer__error {
894
+ display: flex;
895
+ align-items: center;
896
+ gap: var(--chai-space-2);
897
+ margin: 0;
898
+ padding: 2px 2px 0;
899
+ color: var(--chai-color-semantic-danger);
900
+ font-family: var(--chai-type-family-material);
901
+ font-size: var(--chai-type-size-label-large);
902
+ font-weight: var(--chai-type-weight-material-medium);
903
+ line-height: var(--chai-type-leading-label-large);
904
+ letter-spacing: var(--chai-type-tracking-label-large);
905
+ }
906
+
907
+ .chai-composer__error svg {
908
+ width: 16px;
909
+ height: 16px;
910
+ flex: none;
911
+ }
912
+
913
+ /*
914
+ * --- Media Analyzer (reference: the "Composer from Media Analysis" design)
915
+ * — same treatment as `.chai-composer` above, with the
916
+ * exact same container/border/radius values, since the mock reuses them.
917
+ */
918
+ .chai-media-analyzer {
919
+ --chai-media-analyzer-bg: var(--chai-color-semantic-control-surface);
920
+ --chai-media-analyzer-border: var(--chai-color-semantic-control-border);
921
+ --chai-media-analyzer-text: var(--chai-color-semantic-control-text);
922
+ --chai-media-analyzer-hover: var(--chai-color-semantic-control-surface-hover);
923
+
924
+ display: flex;
925
+ flex-direction: column;
926
+ gap: var(--chai-space-3);
927
+ padding: var(--chai-space-4) var(--chai-space-5);
928
+ border-radius: 22px;
929
+ border: 1px solid var(--chai-media-analyzer-border);
930
+ background: var(--chai-media-analyzer-bg);
931
+ font-family: var(--chai-type-family-material);
932
+ }
933
+
934
+ .chai-media-analyzer__dropzone {
935
+ display: flex;
936
+ flex-direction: column;
937
+ align-items: center;
938
+ justify-content: center;
939
+ gap: var(--chai-space-2);
940
+ min-height: 140px;
941
+ padding: var(--chai-space-4);
942
+ border-radius: var(--chai-radius-md);
943
+ border: 1.5px dashed var(--chai-color-semantic-control-text-strong);
944
+ /* Brand-green wash, same token `.chai-drop-zone` reads for the same
945
+ reason (see that rule's own comment) — this dashed box is this
946
+ component's equivalent surface. */
947
+ background: var(--chai-color-semantic-accent-subtle);
948
+ text-align: center;
949
+ cursor: pointer;
950
+ transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
951
+ border-color var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
952
+ }
953
+
954
+ /* At cap for the attached kind (reference: "Analyze Media Composer -
955
+ Disabled - Maximum Media Attached") — flat gray, not a dimmed green
956
+ (mixing "disabled" with the brand wash read as broken, not intentional).
957
+ Still a real drop target for a *different* kind, which always replaces
958
+ what's here — see MediaAnalyzer.tsx's `nextMediaAnalyzerAttachments` for
959
+ why this never fully disables the zone itself, only the same-kind add. */
960
+ .chai-media-analyzer__dropzone--disabled {
961
+ background: var(--chai-color-semantic-control-surface-hover);
962
+ cursor: default;
963
+ }
964
+
965
+ /* A file is being dragged over the zone but not dropped yet — there was no
966
+ feedback at all for this before (the box looked identical whether or not
967
+ a drag was even over it). Solid border + a visibly darker green than the
968
+ dashed zone's own resting wash, so "this is a live drop target right
969
+ now" reads clearly regardless of whether the zone is also at cap (a
970
+ drag-over can still be a valid cross-kind replace even then, so this
971
+ wins over `--disabled`'s gray — see the design note on
972
+ the drop zone always accepting a different kind). */
973
+ .chai-media-analyzer__dropzone--drag-active,
974
+ .chai-media-analyzer__dropzone--disabled.chai-media-analyzer__dropzone--drag-active {
975
+ border-style: solid;
976
+ border-color: var(--chai-color-semantic-accent);
977
+ background: var(--chai-color-semantic-accent-soft);
978
+ }
979
+
980
+ .chai-media-analyzer__dropzone-icon {
981
+ width: 32px;
982
+ height: 32px;
983
+ color: var(--chai-color-semantic-control-text);
984
+ }
985
+
986
+ .chai-media-analyzer__dropzone-icon svg {
987
+ width: 100%;
988
+ height: 100%;
989
+ }
990
+
991
+ .chai-media-analyzer__dropzone-hint {
992
+ margin: 0;
993
+ font-size: var(--chai-type-size-body-medium);
994
+ font-weight: var(--chai-type-weight-material-regular);
995
+ color: var(--chai-media-analyzer-text);
996
+ }
997
+
998
+ .chai-media-analyzer__thumbnails {
999
+ display: flex;
1000
+ flex-wrap: wrap;
1001
+ gap: var(--chai-space-2);
1002
+ }
1003
+
1004
+ /* Same shape/behavior as `.chai-composer__thumb`, sized down (56px, not
1005
+ 88px) so all 5 of an image analysis's maximum fit in one row under the
1006
+ drag box at typical card widths (reference: the design
1007
+ spec — "slightly smaller to fit a total of five image thumbs"). */
1008
+ .chai-media-analyzer__thumb {
1009
+ position: relative;
1010
+ width: 56px;
1011
+ height: 56px;
1012
+ flex: none;
1013
+ border-radius: var(--chai-radius-sm);
1014
+ border: 1px solid var(--chai-color-semantic-control-divider);
1015
+ background: var(--chai-media-analyzer-bg);
1016
+ overflow: hidden;
1017
+ display: flex;
1018
+ align-items: center;
1019
+ justify-content: center;
1020
+ }
1021
+
1022
+ .chai-media-analyzer__thumb-img {
1023
+ width: 100%;
1024
+ height: 100%;
1025
+ object-fit: cover;
1026
+ }
1027
+
1028
+ .chai-media-analyzer__thumb-icon {
1029
+ width: 20px;
1030
+ height: 20px;
1031
+ color: var(--chai-media-analyzer-text);
1032
+ }
1033
+
1034
+ .chai-media-analyzer__thumb-icon svg {
1035
+ width: 100%;
1036
+ height: 100%;
1037
+ }
1038
+
1039
+ .chai-media-analyzer__thumb-remove {
1040
+ position: absolute;
1041
+ top: 2px;
1042
+ right: 2px;
1043
+ width: 20px;
1044
+ height: 20px;
1045
+ display: flex;
1046
+ align-items: center;
1047
+ justify-content: center;
1048
+ border: none;
1049
+ border-radius: var(--chai-radius-full);
1050
+ /* Darker + fully opaque (was translucent black) — against a busy photo
1051
+ thumbnail a semi-transparent badge let the image show through and
1052
+ washed out the X on top of it. A solid, near-black circle is what
1053
+ actually gives the white X reliable contrast in every case. */
1054
+ background: var(--chai-color-semantic-overlay);
1055
+ color: var(--chai-color-semantic-on-overlay);
1056
+ cursor: pointer;
1057
+ opacity: 0;
1058
+ transition: opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
1059
+ }
1060
+
1061
+ .chai-media-analyzer__thumb:hover .chai-media-analyzer__thumb-remove,
1062
+ .chai-media-analyzer__thumb:focus-within .chai-media-analyzer__thumb-remove {
1063
+ opacity: 1;
1064
+ }
1065
+
1066
+ /* Still tiny in absolute terms (a corner badge on a 56px thumbnail can only
1067
+ be so big), but the X itself should read as a solid mark, not a hairline
1068
+ — reference: the design note "more legible" example, a chunky
1069
+ X that fills most of the circle rather than a thin one centered in a lot
1070
+ of empty padding. Both the icon's own size *and* its `strokeWidth`
1071
+ (RemoveIcon, MediaAnalyzer.tsx) went up together — bumping only one or
1072
+ the other still read as too delicate at this size. */
1073
+ .chai-media-analyzer__thumb-remove svg {
1074
+ width: 14px;
1075
+ height: 14px;
1076
+ }
1077
+
1078
+ .chai-media-analyzer__controls {
1079
+ display: flex;
1080
+ align-items: center;
1081
+ justify-content: space-between;
1082
+ gap: var(--chai-space-3);
1083
+ }
1084
+
1085
+ .chai-media-analyzer__controls-start {
1086
+ display: flex;
1087
+ align-items: center;
1088
+ gap: var(--chai-space-2);
1089
+ flex-wrap: wrap;
1090
+ }
1091
+
1092
+ /* Neutral trigger text/open-state, not the shared rule's brand-green fill —
1093
+ same correction `.chai-composer` already applies to its own SearchMenu
1094
+ triggers, for the same reason (see that rule's own comment). */
1095
+ .chai-media-analyzer .chai-search-menu__trigger {
1096
+ color: var(--chai-media-analyzer-text);
1097
+ }
1098
+
1099
+ .chai-media-analyzer .chai-search-menu__trigger--open {
1100
+ background: var(--chai-media-analyzer-hover);
1101
+ border-color: transparent;
1102
+ color: var(--chai-media-analyzer-text);
1103
+ }
1104
+
1105
+ .chai-media-analyzer .chai-search-menu__trigger--text:hover:not(:disabled) {
1106
+ background: var(--chai-media-analyzer-hover);
1107
+ }
1108
+
1109
+ .chai-media-analyzer__submit {
1110
+ flex: none;
1111
+ --md-fab-container-elevation: 0;
1112
+ --md-fab-hover-container-elevation: 0;
1113
+ --md-fab-pressed-container-elevation: 0;
1114
+ --md-fab-focus-container-elevation: 0;
1115
+ }
1116
+
1117
+ /* Unfilled until there's at least one attachment to submit — same reasoning
1118
+ as `.chai-composer__submit:not([data-ready])`. */
1119
+ .chai-media-analyzer__submit:not([data-ready]) {
1120
+ --md-fab-secondary-container-color: transparent;
1121
+ --md-fab-secondary-icon-color: var(--chai-media-analyzer-text);
1122
+ }
1123
+
1124
+ .chai-media-analyzer__submit[data-error] {
1125
+ --md-fab-secondary-container-color: var(--chai-color-semantic-danger-subtle);
1126
+ --md-fab-secondary-icon-color: var(--chai-color-semantic-danger);
1127
+ }
1128
+
1129
+ .chai-media-analyzer__error {
1130
+ margin: 0;
1131
+ padding: 2px 2px 0;
1132
+ color: var(--chai-color-semantic-danger);
1133
+ font-size: var(--chai-type-size-label-large);
1134
+ font-weight: var(--chai-type-weight-material-medium);
1135
+ }
1136
+
1137
+ /*
1138
+ * --- Result Card (reference: Figma "08 Result from Composer Bar", node
1139
+ * 194:1695) — Composer bar's compliment. Literal hex again (not
1140
+ * --chai- or --md-sys-color- tokens) for the card chrome itself, same
1141
+ * "green-seeded theme tints everything neutral" reason `.chai-composer`'s
1142
+ * own container comment documents; chips/menus underneath still read real
1143
+ * MD3 tokens since those aren't large flat fills.
1144
+ */
1145
+
1146
+ /* A fixed default width (375px, matching the Figma mock), not sized off
1147
+ the source media's own pixel dimensions — design
1148
+ correction: letting the card's *width* track a real generated image's
1149
+ actual resolution would make the card's footprint unpredictable
1150
+ (anywhere from a couple hundred px to several thousand) and break the
1151
+ layout of whatever app it's embedded in. The *height* still isn't
1152
+ fixed: the media inside renders at `width: 100%; height: auto` (its own
1153
+ comment below), so at this one fixed width, height naturally follows
1154
+ whatever the real aspect ratio is — a 4:3 image lands close to the
1155
+ Figma mock's own ~335px total card height, 1:1 taller, 16:9 shorter,
1156
+ with no special-casing needed per ratio. This is the actual "thumbnail"
1157
+ behavior (scaled down, never cropped) — the "Expand" button is what
1158
+ shows the real, full-size media when someone needs it. A `width` prop
1159
+ overrides the 375px default for a builder who wants a different fixed
1160
+ footprint. */
1161
+ .chai-result-card {
1162
+ width: var(--chai-result-card-width, 375px);
1163
+ max-width: 100%;
1164
+ perspective: 1800px;
1165
+ font-family: var(--chai-type-family-material);
1166
+ }
1167
+
1168
+ /* The *front* (the media) is what establishes the card's height — normal
1169
+ block flow, no special positioning, so `.inner`'s own auto height is
1170
+ just "whatever `.front`'s natural content needs." The *back* (info
1171
+ side) is `position: absolute; inset: 0` and simply conforms to
1172
+ whatever height that resolves to, scrolling its own prompt box
1173
+ internally if its content needs more room than the media provided.
1174
+ Deliberately not symmetric ("taller of the two, either stretches to
1175
+ match") — a design correction: a media-driven card
1176
+ flipping to a shorter or differently-proportioned info side read as
1177
+ visually inconsistent, and media should stay the one source of truth
1178
+ for the card's height in both states. `.back` being absolute doesn't
1179
+ reintroduce the old circular-sizing problem (`position: absolute` on
1180
+ *both* faces once tried and reverted) because only one side of that
1181
+ circle exists now — `.front` is never absolute, so it always has a
1182
+ real, independently-computed height for `.back` to match. */
1183
+ .chai-result-card__inner {
1184
+ position: relative;
1185
+ width: 100%;
1186
+ transition: transform var(--chai-motion-duration-slow) var(--chai-motion-easing-standard);
1187
+ transform-style: preserve-3d;
1188
+ }
1189
+
1190
+ .chai-result-card--flipped .chai-result-card__inner {
1191
+ transform: rotateY(180deg);
1192
+ }
1193
+
1194
+ .chai-result-card__face {
1195
+ width: 100%;
1196
+ display: flex;
1197
+ flex-direction: column;
1198
+ backface-visibility: hidden;
1199
+ border-radius: 29px;
1200
+ border: 1px solid var(--chai-color-semantic-control-border-soft);
1201
+ overflow: hidden;
1202
+ }
1203
+
1204
+ .chai-result-card__front {
1205
+ position: relative;
1206
+ background: var(--chai-color-semantic-control-surface);
1207
+ /* `.back` is later in DOM order and stacks above `.front` regardless of
1208
+ which face is currently rotated toward the viewer (sibling stacking
1209
+ order doesn't reorder itself just because `backface-visibility` hides
1210
+ paint) — so without this, `.back`'s own hidden-but-still-present box
1211
+ can swallow clicks meant for `.front` in some engines. Pointer-events
1212
+ are switched explicitly per face instead of trusting
1213
+ `backface-visibility: hidden` to also gate hit-testing. */
1214
+ pointer-events: auto;
1215
+ }
1216
+
1217
+ .chai-result-card--flipped .chai-result-card__front {
1218
+ pointer-events: none;
1219
+ }
1220
+
1221
+ .chai-result-card__back {
1222
+ position: absolute;
1223
+ inset: 0;
1224
+ background: var(--chai-color-semantic-control-surface-raised);
1225
+ transform: rotateY(180deg);
1226
+ padding: var(--chai-space-5) var(--chai-space-4) var(--chai-space-4);
1227
+ gap: var(--chai-space-3);
1228
+ pointer-events: none;
1229
+ }
1230
+
1231
+ .chai-result-card--flipped .chai-result-card__back {
1232
+ pointer-events: auto;
1233
+ }
1234
+
1235
+ /* --- Media (image/video/audio/text, by status) ---
1236
+ * `flex: 1` here is only a graceful-overflow behavior, not a fixed size:
1237
+ * with no explicit height anywhere in this chain, the media area's own
1238
+ * natural size already comes from its image/video content below (`width:
1239
+ * 100%; height: auto`, no `object-fit` cropping/letterboxing needed) — it
1240
+ * only actually stretches on the rarer occasion the *other* face (the
1241
+ * flipped info side) is naturally taller, since both faces share one grid
1242
+ * track sized to the taller one (`.chai-result-card__inner`'s own
1243
+ * comment). When that happens the extra height lands here as breathing
1244
+ * room around a still-centered, still-unstretched image, not a fixed gray
1245
+ * box the image sits inside by default.
1246
+ */
1247
+ .chai-result-card__media {
1248
+ position: relative;
1249
+ flex: 1;
1250
+ display: flex;
1251
+ align-items: center;
1252
+ justify-content: center;
1253
+ background: var(--chai-color-semantic-control-surface);
1254
+ overflow: hidden;
1255
+ }
1256
+
1257
+ /* Queued/running/error/audio/text have no image of their own to derive a
1258
+ natural size from, so they get one fixed reasonable height instead of
1259
+ collapsing to near-zero. */
1260
+ .chai-result-card__media--fixed-height,
1261
+ .chai-result-card__media--pending,
1262
+ .chai-result-card__media--error,
1263
+ .chai-result-card__media--cancelled {
1264
+ min-height: 220px;
1265
+ padding: var(--chai-space-5);
1266
+ }
1267
+
1268
+ /* A sibling of `.chai-result-card` itself, not a descendant — see the
1269
+ overlay's own comment in ResultCard.tsx (`perspective` on the card, set
1270
+ for the flip animation, would otherwise trap `position: fixed` inside
1271
+ the card's own box instead of the real viewport). */
1272
+ .chai-result-card__expand-overlay {
1273
+ position: fixed;
1274
+ inset: 0;
1275
+ z-index: 50;
1276
+ display: flex;
1277
+ align-items: center;
1278
+ justify-content: center;
1279
+ }
1280
+
1281
+ .chai-result-card__expand-scrim {
1282
+ position: absolute;
1283
+ inset: 0;
1284
+ z-index: 0;
1285
+ background: var(--chai-color-semantic-scrim);
1286
+ }
1287
+
1288
+ .chai-result-card__expand-frame {
1289
+ position: relative;
1290
+ z-index: 1;
1291
+ width: min(90vw, 960px);
1292
+ height: min(80vh, 540px);
1293
+ border-radius: var(--chai-radius-lg);
1294
+ overflow: hidden;
1295
+ background: var(--chai-color-semantic-media-backdrop);
1296
+ }
1297
+
1298
+ .chai-result-card__expand-frame .chai-result-card__video,
1299
+ .chai-result-card__expand-frame .chai-result-card__image {
1300
+ /* The frame itself is a fixed box (unlike the inline card, which sizes
1301
+ to the media) — `object-fit: contain` here is what keeps a portrait
1302
+ or unusually-shaped image/video from overflowing it, height: 100%
1303
+ instead of auto since auto would size off the media's own aspect
1304
+ ratio again and could exceed the frame's fixed height. */
1305
+ height: 100%;
1306
+ object-fit: contain;
1307
+ aspect-ratio: auto;
1308
+ border-radius: 0;
1309
+ }
1310
+
1311
+ /* No `object-fit`/`max-width`/`max-height` — the whole point of sizing the
1312
+ card itself to the media (see `.chai-result-card`'s own comment) is that
1313
+ the image never needs cropping or letterboxing to begin with: it just
1314
+ renders at its own natural aspect ratio, full width, corners rounded by
1315
+ `.chai-result-card__face`'s own `overflow: hidden` around it. */
1316
+ .chai-result-card__image {
1317
+ position: relative;
1318
+ z-index: 1;
1319
+ display: block;
1320
+ width: 100%;
1321
+ height: auto;
1322
+ }
1323
+
1324
+ .chai-result-card__video {
1325
+ position: relative;
1326
+ z-index: 1;
1327
+ display: block;
1328
+ width: 100%;
1329
+ height: auto;
1330
+ /* A reasonable guess before real `loadedmetadata` dimensions are known
1331
+ (avoids a 0-height flash) — once they load, intrinsic sizing from
1332
+ `width: 100%; height: auto` takes over and this stops applying. */
1333
+ aspect-ratio: 16 / 9;
1334
+ background: var(--chai-color-semantic-media-backdrop);
1335
+ cursor: pointer;
1336
+ }
1337
+
1338
+ .chai-result-card__audio {
1339
+ position: relative;
1340
+ z-index: 1;
1341
+ width: 90%;
1342
+ }
1343
+
1344
+ /* Model output is often long and structured, so line breaks are kept and
1345
+ the text scrolls past a max height instead of growing the card. Builders
1346
+ can change the height with --chai-result-card-text-max-height. */
1347
+ .chai-result-card__text-output {
1348
+ position: relative;
1349
+ z-index: 1;
1350
+ /* Starts below the download/share buttons, which sit at the top right:
1351
+ text scrolling underneath them was hard to read. */
1352
+ margin: var(--chai-space-4) 0 0;
1353
+ padding: var(--chai-space-4);
1354
+ min-height: 160px;
1355
+ max-height: var(--chai-result-card-text-max-height, 400px);
1356
+ overflow-y: auto;
1357
+ white-space: pre-wrap;
1358
+ overflow-wrap: anywhere;
1359
+ font-size: var(--chai-type-size-body-medium);
1360
+ line-height: var(--chai-type-leading-body-medium);
1361
+ color: var(--chai-color-semantic-control-text-strong);
1362
+ }
1363
+
1364
+ /* An always-visible scrollbar, so it's clear more text exists. macOS hides
1365
+ overlay scrollbars until you scroll, which made long answers look cut
1366
+ off. WebKit/Blink use the pseudo-elements; Firefox the standard props. */
1367
+ .chai-result-card__text-output::-webkit-scrollbar {
1368
+ width: 8px;
1369
+ }
1370
+
1371
+ .chai-result-card__text-output::-webkit-scrollbar-track {
1372
+ background: transparent;
1373
+ }
1374
+
1375
+ .chai-result-card__text-output::-webkit-scrollbar-thumb {
1376
+ border-radius: var(--chai-radius-full);
1377
+ background: var(--chai-color-semantic-control-border-soft);
1378
+ }
1379
+
1380
+ @supports not selector(::-webkit-scrollbar) {
1381
+ .chai-result-card__text-output {
1382
+ scrollbar-width: thin;
1383
+ scrollbar-color: var(--chai-color-semantic-control-border-soft) transparent;
1384
+ }
1385
+ }
1386
+
1387
+ /* Streaming caret — a block cursor trailing the partial text while a text
1388
+ result is still arriving. */
1389
+ .chai-result-card__text-output--streaming::after {
1390
+ content: "";
1391
+ display: inline-block;
1392
+ width: 0.5em;
1393
+ height: 1em;
1394
+ margin-left: 2px;
1395
+ vertical-align: text-bottom;
1396
+ background: var(--chai-color-semantic-control-text);
1397
+ animation: chai-caret-blink calc(var(--chai-motion-duration-steep) * 1.2) steps(1) infinite;
1398
+ }
1399
+
1400
+ @keyframes chai-caret-blink {
1401
+ 50% {
1402
+ opacity: 0;
1403
+ }
1404
+ }
1405
+
1406
+ @media (prefers-reduced-motion: reduce) {
1407
+ .chai-result-card__text-output--streaming::after {
1408
+ animation: none;
1409
+ }
1410
+ }
1411
+
1412
+ .chai-result-card__spinner {
1413
+ width: 32px;
1414
+ height: 32px;
1415
+ border-radius: 50%;
1416
+ border: 3px solid var(--chai-color-semantic-control-border-faint);
1417
+ border-top-color: var(--chai-color-semantic-accent);
1418
+ animation: chai-spin var(--chai-motion-duration-steep) linear infinite;
1419
+ }
1420
+
1421
+ .chai-result-card__cancelled-text {
1422
+ margin: 0;
1423
+ font-size: var(--chai-type-size-body-medium);
1424
+ color: var(--chai-color-semantic-control-text-muted);
1425
+ }
1426
+
1427
+ .chai-result-card__error-text {
1428
+ margin: 0;
1429
+ padding: 0 var(--chai-space-4);
1430
+ text-align: center;
1431
+ font-size: var(--chai-type-size-body-medium);
1432
+ color: var(--chai-color-semantic-danger);
1433
+ }
1434
+
1435
+ .chai-result-card__media-actions {
1436
+ position: absolute;
1437
+ z-index: 1;
1438
+ top: 10px;
1439
+ right: 10px;
1440
+ display: flex;
1441
+ gap: 2px;
1442
+ }
1443
+
1444
+ /* Play/scrubber sit relative to their nearest positioned ancestor —
1445
+ `.chai-result-card__media` inline, `.chai-result-card__expand-frame`
1446
+ when expanded — not the `<video>` element itself. Both are exactly the
1447
+ video's own rendered box (`object-fit: contain` fills either one), so
1448
+ the same rule works for both without measuring the video's actual
1449
+ painted rect. On an aspect ratio very different from that box, the
1450
+ scrubber can sit in the letterboxed margin rather than snug against the
1451
+ visible frame — accepted trade-off against the complexity of measuring
1452
+ real video bounds for what's a slim progress bar, not a hit-critical
1453
+ control. */
1454
+ .chai-result-card__play {
1455
+ position: absolute;
1456
+ z-index: 1;
1457
+ top: 50%;
1458
+ left: 50%;
1459
+ transform: translate(-50%, -50%);
1460
+ width: 56px;
1461
+ height: 56px;
1462
+ border-radius: 50%;
1463
+ border: none;
1464
+ background: var(--chai-color-semantic-overlay);
1465
+ color: var(--chai-color-semantic-accent);
1466
+ display: flex;
1467
+ align-items: center;
1468
+ justify-content: center;
1469
+ cursor: pointer;
1470
+ }
1471
+
1472
+ .chai-result-card__play svg {
1473
+ width: 28px;
1474
+ height: 28px;
1475
+ }
1476
+
1477
+ /* The scrubber is a 24px-tall slider (WCAG 2.2 target size) drawing the
1478
+ same thin track, centred where the track has always sat. */
1479
+ .chai-result-card__scrubber {
1480
+ --chai-scrubber-track: 6px;
1481
+ position: absolute;
1482
+ z-index: 1;
1483
+ left: 12px;
1484
+ right: 12px;
1485
+ bottom: calc(10px - (24px - var(--chai-scrubber-track)) / 2);
1486
+ height: 24px;
1487
+ border-radius: 6px;
1488
+ cursor: pointer;
1489
+ }
1490
+
1491
+ .chai-result-card__scrubber::before,
1492
+ .chai-result-card__scrubber-fill {
1493
+ position: absolute;
1494
+ top: calc((24px - var(--chai-scrubber-track)) / 2);
1495
+ height: var(--chai-scrubber-track);
1496
+ border-radius: var(--chai-scrubber-track);
1497
+ }
1498
+
1499
+ .chai-result-card__scrubber::before {
1500
+ content: "";
1501
+ left: 0;
1502
+ right: 0;
1503
+ background: var(--chai-color-semantic-control-track);
1504
+ }
1505
+
1506
+ .chai-result-card__scrubber--expanded {
1507
+ --chai-scrubber-track: 8px;
1508
+ left: 16px;
1509
+ right: 16px;
1510
+ bottom: calc(16px - (24px - var(--chai-scrubber-track)) / 2);
1511
+ }
1512
+
1513
+ .chai-result-card__scrubber-fill {
1514
+ left: 0;
1515
+ background: var(--chai-color-semantic-accent);
1516
+ }
1517
+
1518
+ /* Transparent, covering the video: the play/pause control. */
1519
+ .chai-result-card__video-toggle {
1520
+ position: absolute;
1521
+ inset: 0;
1522
+ padding: 0;
1523
+ border: none;
1524
+ background: transparent;
1525
+ cursor: pointer;
1526
+ }
1527
+
1528
+ .chai-result-card__collapse {
1529
+ position: fixed;
1530
+ z-index: 2;
1531
+ top: 20px;
1532
+ right: 20px;
1533
+ width: 36px;
1534
+ height: 36px;
1535
+ border-radius: 50%;
1536
+ border: none;
1537
+ background: var(--chai-color-semantic-overlay);
1538
+ color: var(--chai-color-semantic-on-overlay);
1539
+ display: flex;
1540
+ align-items: center;
1541
+ justify-content: center;
1542
+ cursor: pointer;
1543
+ }
1544
+
1545
+ .chai-result-card__collapse svg {
1546
+ width: 14px;
1547
+ height: 14px;
1548
+ }
1549
+
1550
+ /* --- Bottom bar --- */
1551
+
1552
+ /* Reference: Figma "Result Card - Image - Paginated" — an
1553
+ 11px inset around the icon row, and when there's more than one result the
1554
+ pager drops to its own centered row beneath it (8px from the card's
1555
+ bottom edge) rather than squeezing between the icon groups. Without a
1556
+ pager the icon row is simply centered in a comparably roomy bar. */
1557
+ .chai-result-card__bar {
1558
+ display: flex;
1559
+ flex-direction: column;
1560
+ align-items: center;
1561
+ padding: 16px 11px;
1562
+ background: var(--chai-color-semantic-control-surface-raised);
1563
+ }
1564
+
1565
+ .chai-result-card__bar:has(.chai-pagination) {
1566
+ padding: 11px 11px 8px;
1567
+ }
1568
+
1569
+ .chai-result-card__bar-row {
1570
+ align-self: stretch;
1571
+ display: flex;
1572
+ align-items: center;
1573
+ justify-content: space-between;
1574
+ gap: var(--chai-space-2);
1575
+ }
1576
+
1577
+ .chai-result-card__bar-start {
1578
+ display: flex;
1579
+ align-items: center;
1580
+ gap: 2px;
1581
+ }
1582
+
1583
+ /* The shared hover-circle treatment — reference: the design
1584
+ note that the "like" button's hover state in the mock is how every
1585
+ small icon button on the card should behave, not just that one. `
1586
+ --active` (a persisted, not just hovered, tint) is this component's own
1587
+ extension for like/dislike/more, since the mock only documents hover. */
1588
+ .chai-result-card__icon-btn {
1589
+ position: relative;
1590
+ width: 24px;
1591
+ height: 24px;
1592
+ padding: 0;
1593
+ border: none;
1594
+ border-radius: 50%;
1595
+ background: transparent;
1596
+ color: var(--chai-color-semantic-control-text-muted);
1597
+ display: flex;
1598
+ align-items: center;
1599
+ justify-content: center;
1600
+ cursor: pointer;
1601
+ }
1602
+
1603
+ .chai-result-card__icon-btn svg {
1604
+ width: 18px;
1605
+ height: 18px;
1606
+ }
1607
+
1608
+ .chai-result-card__icon-btn:hover:not(:disabled) {
1609
+ background: var(--chai-color-semantic-control-surface-hover);
1610
+ }
1611
+
1612
+ .chai-result-card__icon-btn--active {
1613
+ color: var(--chai-color-semantic-accent-text);
1614
+ background: var(--chai-color-semantic-control-surface-hover);
1615
+ }
1616
+
1617
+ .chai-result-card__icon-btn:disabled {
1618
+ opacity: 0.4;
1619
+ cursor: not-allowed;
1620
+ }
1621
+
1622
+ /* --- More menu (reference: Figma "Result Card - More menu expanded") --- */
1623
+
1624
+ .chai-result-card__more {
1625
+ position: relative;
1626
+ }
1627
+
1628
+ /* Opens upward, not down like the Figma mock's own isolated rendering of
1629
+ it — the "more" trigger sits right at the card's bottom edge, and
1630
+ `.chai-result-card__face` needs `overflow: hidden` for its rounded
1631
+ corners and the flip's `backface-visibility`, which would clip a
1632
+ downward menu (confirmed via a real screenshot during this build — the
1633
+ trigger showed pressed/active but no menu was visible at all). Opening
1634
+ upward keeps it inside the still-visible media area above the bar. */
1635
+ .chai-result-card__more-menu {
1636
+ position: absolute;
1637
+ z-index: 10;
1638
+ bottom: calc(100% + 8px);
1639
+ left: 0;
1640
+ width: 180px;
1641
+ margin: 0;
1642
+ padding: var(--chai-space-2);
1643
+ list-style: none;
1644
+ border-radius: var(--chai-radius-lg);
1645
+ background: var(--chai-color-semantic-control-surface-raised);
1646
+ box-shadow: var(--chai-shadow-menu);
1647
+ }
1648
+
1649
+ .chai-result-card__more-menu-item {
1650
+ display: flex;
1651
+ align-items: center;
1652
+ gap: var(--chai-space-2);
1653
+ width: 100%;
1654
+ padding: 10px 12px;
1655
+ border: none;
1656
+ border-radius: var(--chai-radius-sm);
1657
+ background: transparent;
1658
+ color: var(--chai-color-semantic-control-text-strong);
1659
+ font-family: var(--chai-type-family-material);
1660
+ font-size: var(--chai-type-size-body-large);
1661
+ text-align: left;
1662
+ cursor: pointer;
1663
+ }
1664
+
1665
+ .chai-result-card__more-menu-item:hover {
1666
+ background: var(--chai-color-semantic-control-surface-hover);
1667
+ }
1668
+
1669
+ .chai-result-card__more-menu-icon {
1670
+ display: flex;
1671
+ width: 18px;
1672
+ height: 18px;
1673
+ flex: none;
1674
+ color: var(--chai-color-semantic-control-text);
1675
+ }
1676
+
1677
+ /* --- Info flip (back face) --- */
1678
+
1679
+ .chai-result-card__close {
1680
+ position: absolute;
1681
+ z-index: 1;
1682
+ top: 12px;
1683
+ right: 12px;
1684
+ width: 24px;
1685
+ height: 24px;
1686
+ padding: 0;
1687
+ border: none;
1688
+ border-radius: 50%;
1689
+ background: transparent;
1690
+ color: var(--chai-color-semantic-control-text-muted);
1691
+ display: flex;
1692
+ align-items: center;
1693
+ justify-content: center;
1694
+ cursor: pointer;
1695
+ }
1696
+
1697
+ .chai-result-card__close:hover {
1698
+ color: var(--chai-color-semantic-control-text-strong);
1699
+ background: var(--chai-color-semantic-control-surface-hover);
1700
+ }
1701
+
1702
+ .chai-result-card__close svg {
1703
+ width: 14px;
1704
+ height: 14px;
1705
+ }
1706
+
1707
+ .chai-result-card__prompt-box {
1708
+ position: relative;
1709
+ flex: 1;
1710
+ min-height: 0;
1711
+ margin-top: var(--chai-space-4);
1712
+ padding: var(--chai-space-3) var(--chai-space-4);
1713
+ border-radius: 14px;
1714
+ background: var(--chai-color-semantic-control-surface);
1715
+ overflow-y: auto;
1716
+ }
1717
+
1718
+ .chai-result-card__prompt-text {
1719
+ margin: 0;
1720
+ padding-right: 20px;
1721
+ font-size: var(--chai-type-size-body-medium);
1722
+ line-height: var(--chai-type-leading-body-medium);
1723
+ color: var(--chai-color-semantic-control-text-strong);
1724
+ user-select: text;
1725
+ white-space: pre-wrap;
1726
+ }
1727
+
1728
+ .chai-result-card__copy {
1729
+ position: absolute;
1730
+ top: 9px;
1731
+ right: 9px;
1732
+ width: 22px;
1733
+ height: 22px;
1734
+ padding: 0;
1735
+ border: none;
1736
+ border-radius: 50%;
1737
+ background: transparent;
1738
+ color: var(--chai-color-semantic-control-text-muted);
1739
+ display: flex;
1740
+ align-items: center;
1741
+ justify-content: center;
1742
+ cursor: pointer;
1743
+ }
1744
+
1745
+ .chai-result-card__copy:hover {
1746
+ color: var(--chai-color-semantic-control-text-strong);
1747
+ background: var(--chai-color-semantic-control-surface-hover);
1748
+ }
1749
+
1750
+ .chai-result-card__copy svg {
1751
+ width: 10px;
1752
+ height: 13px;
1753
+ }
1754
+
1755
+ .chai-result-card__chips {
1756
+ display: flex;
1757
+ flex-wrap: wrap;
1758
+ gap: var(--chai-space-2);
1759
+ flex: none;
1760
+ }
1761
+
1762
+ .chai-result-card__chip {
1763
+ padding: 6px 12px;
1764
+ border-radius: var(--chai-radius-sm);
1765
+ border: 1px solid var(--chai-color-semantic-control-border);
1766
+ font-size: var(--chai-type-size-label-large);
1767
+ color: var(--chai-color-semantic-control-text);
1768
+ white-space: nowrap;
1769
+ }
1770
+
1771
+ .chai-result-card__chip--evaluation {
1772
+ display: inline-flex;
1773
+ align-items: center;
1774
+ gap: 4px;
1775
+ }
1776
+
1777
+ .chai-result-card__chip--evaluation svg {
1778
+ width: 14px;
1779
+ height: 14px;
1780
+ }
1781
+
1782
+ /* Reference: the mock's checked, filled green "Evaluation" chip. */
1783
+ .chai-result-card__chip--passed {
1784
+ border-color: var(--chai-color-semantic-accent);
1785
+ color: var(--chai-color-semantic-accent-text);
1786
+ background: var(--chai-color-semantic-accent-subtle);
1787
+ }
1788
+
1789
+ /* The failed counterpart, in the danger tokens. Outlined, not filled:
1790
+ danger text on danger-subtle is 2.96:1, under WCAG AA's 4.5:1, while on
1791
+ the card surface it's 4.62:1. The cross icon carries the distinction the
1792
+ fill would have. */
1793
+ .chai-result-card__chip--failed {
1794
+ border-color: var(--chai-color-semantic-danger);
1795
+ color: var(--chai-color-semantic-danger);
1796
+ }
1797
+
1798
+ /* The cross glyph fills its whole viewBox where the check has padding —
1799
+ sized down so both icons read at the same visual weight. */
1800
+ .chai-result-card__chip--failed svg {
1801
+ width: 10px;
1802
+ height: 10px;
1803
+ margin: 0 2px;
1804
+ }
1805
+
1806
+ /* Announced, not shown — carries "passed"/"failed" for screen readers. */
1807
+ .chai-visually-hidden {
1808
+ position: absolute;
1809
+ width: 1px;
1810
+ height: 1px;
1811
+ margin: -1px;
1812
+ padding: 0;
1813
+ overflow: hidden;
1814
+ clip: rect(0 0 0 0);
1815
+ white-space: nowrap;
1816
+ border: 0;
1817
+ }
1818
+
1819
+ /* --- Focus rings ------------------------------------------------------------
1820
+ On by default: every CHAI control shows a visible keyboard-focus ring,
1821
+ which WCAG 2.4.7 Focus Visible requires. Buttons and menu items
1822
+ keep the browser's own ring; MD3 elements (the submit FAB, the switch)
1823
+ draw `<md-focus-ring>`. The Composer's textarea is the one control that
1824
+ had none — its ring goes on the whole pill, matching MD3's ring width
1825
+ and color so every ring in a component reads as the same system. */
1826
+ .chai-composer:has(.chai-composer__input:focus-visible) {
1827
+ outline: 3px solid var(--chai-color-semantic-accent);
1828
+ outline-offset: 2px;
1829
+ }
1830
+
1831
+ /* Off switch: `data-chai-focus-ring="hidden"` on any ancestor hides every
1832
+ CHAI focus ring inside it — for a showcase or preview surface, not a
1833
+ production app. Hiding them removes the only indicator keyboard users
1834
+ have of where they are, failing WCAG 2.4.7 Focus Visible.
1835
+ Scoped to CHAI's own roots so a builder's other controls under the same
1836
+ ancestor keep theirs. */
1837
+ [data-chai-focus-ring="hidden"] :is(
1838
+ .chai-composer,
1839
+ .chai-media-analyzer,
1840
+ .chai-result-card,
1841
+ .chai-result-card__expand-overlay,
1842
+ .chai-search-menu,
1843
+ .chai-pagination,
1844
+ .chai-switch
1845
+ ) {
1846
+ --md-focus-ring-width: 0px;
1847
+ --md-focus-ring-active-width: 0px;
1848
+ }
1849
+
1850
+ [data-chai-focus-ring="hidden"] .chai-composer:has(.chai-composer__input:focus-visible),
1851
+ [data-chai-focus-ring="hidden"] :is(
1852
+ .chai-composer,
1853
+ .chai-media-analyzer,
1854
+ .chai-result-card,
1855
+ .chai-result-card__expand-overlay,
1856
+ .chai-search-menu,
1857
+ .chai-pagination
1858
+ ) :focus-visible {
1859
+ outline: none;
1860
+ }