@chai-ui/vue 0.0.0-stage → 0.7.1

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 (88) hide show
  1. package/DESIGN.md +235 -0
  2. package/LICENSE +21 -0
  3. package/README.md +122 -2
  4. package/dist/ChaiProvider.vue.d.ts +24 -0
  5. package/dist/ChaiProvider.vue.d.ts.map +1 -0
  6. package/dist/Composer.vue.d.ts +94 -0
  7. package/dist/Composer.vue.d.ts.map +1 -0
  8. package/dist/EditCard.vue.d.ts +38 -0
  9. package/dist/EditCard.vue.d.ts.map +1 -0
  10. package/dist/MediaAnalyzer.vue.d.ts +54 -0
  11. package/dist/MediaAnalyzer.vue.d.ts.map +1 -0
  12. package/dist/PageSlide.vue.d.ts +23 -0
  13. package/dist/PageSlide.vue.d.ts.map +1 -0
  14. package/dist/ResultCard.vue.d.ts +29 -0
  15. package/dist/ResultCard.vue.d.ts.map +1 -0
  16. package/dist/composer/AttachMenu.vue.d.ts +17 -0
  17. package/dist/composer/AttachMenu.vue.d.ts.map +1 -0
  18. package/dist/composer/Chip.vue.d.ts +14 -0
  19. package/dist/composer/Chip.vue.d.ts.map +1 -0
  20. package/dist/dismiss.d.ts +4 -0
  21. package/dist/dismiss.d.ts.map +1 -0
  22. package/dist/edit-card/ExpandedImage.vue.d.ts +12 -0
  23. package/dist/edit-card/ExpandedImage.vue.d.ts.map +1 -0
  24. package/dist/edit-card/RegionShape.vue.d.ts +20 -0
  25. package/dist/edit-card/RegionShape.vue.d.ts.map +1 -0
  26. package/dist/icons.d.ts +43 -0
  27. package/dist/icons.d.ts.map +1 -0
  28. package/dist/index.d.ts +16 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +2521 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/page-slide.d.ts +31 -0
  33. package/dist/page-slide.d.ts.map +1 -0
  34. package/dist/primitives/Pagination.vue.d.ts +26 -0
  35. package/dist/primitives/Pagination.vue.d.ts.map +1 -0
  36. package/dist/primitives/SearchMenu.vue.d.ts +51 -0
  37. package/dist/primitives/SearchMenu.vue.d.ts.map +1 -0
  38. package/dist/primitives/Toggle.vue.d.ts +16 -0
  39. package/dist/primitives/Toggle.vue.d.ts.map +1 -0
  40. package/dist/primitives/search-menu.d.ts +19 -0
  41. package/dist/primitives/search-menu.d.ts.map +1 -0
  42. package/dist/reasoning.d.ts +6 -0
  43. package/dist/reasoning.d.ts.map +1 -0
  44. package/dist/result-card/IconButton.vue.d.ts +21 -0
  45. package/dist/result-card/IconButton.vue.d.ts.map +1 -0
  46. package/dist/result-card/MoreMenu.vue.d.ts +13 -0
  47. package/dist/result-card/MoreMenu.vue.d.ts.map +1 -0
  48. package/dist/result-card/ResultMedia.vue.d.ts +28 -0
  49. package/dist/result-card/ResultMedia.vue.d.ts.map +1 -0
  50. package/dist/result-card/ResultText.vue.d.ts +8 -0
  51. package/dist/result-card/ResultText.vue.d.ts.map +1 -0
  52. package/dist/result-card/VideoPlayer.vue.d.ts +18 -0
  53. package/dist/result-card/VideoPlayer.vue.d.ts.map +1 -0
  54. package/dist/result-card/types.d.ts +9 -0
  55. package/dist/result-card/types.d.ts.map +1 -0
  56. package/dist/style.css +2378 -0
  57. package/dist/useComposer.d.ts +22 -0
  58. package/dist/useComposer.d.ts.map +1 -0
  59. package/dist/useMediaAnalyzer.d.ts +17 -0
  60. package/dist/useMediaAnalyzer.d.ts.map +1 -0
  61. package/package.json +79 -3
  62. package/src/ChaiProvider.vue +26 -0
  63. package/src/Composer.vue +372 -0
  64. package/src/EditCard.vue +520 -0
  65. package/src/MediaAnalyzer.vue +216 -0
  66. package/src/PageSlide.vue +36 -0
  67. package/src/ResultCard.vue +297 -0
  68. package/src/composer/AttachMenu.vue +66 -0
  69. package/src/composer/Chip.vue +26 -0
  70. package/src/dismiss.ts +22 -0
  71. package/src/edit-card/ExpandedImage.vue +39 -0
  72. package/src/edit-card/RegionShape.vue +39 -0
  73. package/src/icons.ts +55 -0
  74. package/src/index.ts +77 -0
  75. package/src/page-slide.ts +32 -0
  76. package/src/primitives/Pagination.vue +76 -0
  77. package/src/primitives/SearchMenu.vue +167 -0
  78. package/src/primitives/Toggle.vue +23 -0
  79. package/src/primitives/search-menu.ts +20 -0
  80. package/src/reasoning.ts +10 -0
  81. package/src/result-card/IconButton.vue +18 -0
  82. package/src/result-card/MoreMenu.vue +35 -0
  83. package/src/result-card/ResultMedia.vue +77 -0
  84. package/src/result-card/ResultText.vue +37 -0
  85. package/src/result-card/VideoPlayer.vue +90 -0
  86. package/src/result-card/types.ts +10 -0
  87. package/src/useComposer.ts +38 -0
  88. package/src/useMediaAnalyzer.ts +33 -0
package/dist/style.css ADDED
@@ -0,0 +1,2378 @@
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
+ /* On a narrow phone, never wider than the screen less a margin. */
201
+ max-width: calc(100vw - 32px);
202
+ padding: var(--chai-space-3);
203
+ border-radius: var(--chai-radius-lg);
204
+ /* White per Figma (reference: every dropdown menu, Model + Generator). */
205
+ background: var(--chai-color-semantic-control-surface-raised);
206
+ box-shadow: var(--chai-shadow-menu);
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: var(--chai-space-2);
210
+ }
211
+
212
+ .chai-search-menu__search {
213
+ width: 100%;
214
+ }
215
+
216
+ .chai-search-menu__search-icon {
217
+ display: flex;
218
+ }
219
+
220
+ .chai-search-menu__search svg {
221
+ width: 20px;
222
+ height: 20px;
223
+ }
224
+
225
+ .chai-search-menu__clear {
226
+ padding: 0;
227
+ border: none;
228
+ background: transparent;
229
+ color: inherit;
230
+ cursor: pointer;
231
+ display: flex;
232
+ }
233
+
234
+ .chai-search-menu__list {
235
+ list-style: none;
236
+ margin: 0;
237
+ padding: 0;
238
+ max-height: 240px;
239
+ overflow-y: auto;
240
+ }
241
+
242
+ .chai-search-menu__option {
243
+ width: 100%;
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: space-between;
247
+ gap: 8px;
248
+ padding: 10px 12px;
249
+ border: none;
250
+ border-radius: var(--chai-radius-sm);
251
+ background: transparent;
252
+ color: var(--chai-color-semantic-control-text-strong);
253
+ font-family: var(--chai-type-family-material);
254
+ font-size: var(--chai-type-size-body-large);
255
+ text-align: left;
256
+ cursor: pointer;
257
+ }
258
+
259
+ .chai-search-menu__option:hover:not(:disabled) {
260
+ background: var(--chai-color-semantic-control-surface-hover);
261
+ }
262
+
263
+ .chai-search-menu__option--selected {
264
+ color: var(--chai-color-semantic-accent-text);
265
+ font-weight: var(--chai-type-weight-material-medium);
266
+ }
267
+
268
+ /* Grayed + inert while a `toggleHeader` is on (reference: the account
269
+ holder's own "Auto-select model" mock) — the row that matches `value`
270
+ keeps its checkmark regardless, so the underlying manual pick stays
271
+ visible even though it can't be changed until the toggle goes off. */
272
+ .chai-search-menu__option:disabled {
273
+ /* MD3's disabled-content spec (on-surface at 38%) — the muted text token
274
+ alone read almost the same as an enabled row. */
275
+ color: color-mix(in srgb, var(--chai-color-semantic-control-text-strong) 38%, transparent);
276
+ cursor: not-allowed;
277
+ }
278
+
279
+ .chai-search-menu__option:disabled.chai-search-menu__option--selected {
280
+ color: color-mix(in srgb, var(--chai-color-semantic-control-text-strong) 38%, transparent);
281
+ font-weight: var(--chai-type-weight-material-regular);
282
+ }
283
+
284
+ /* Long option labels (a full model name, at this menu's default width)
285
+ ellipsize instead of wrapping or pushing the checkmark out of view. */
286
+ .chai-search-menu__option-label {
287
+ flex: 1;
288
+ min-width: 0;
289
+ overflow: hidden;
290
+ text-overflow: ellipsis;
291
+ white-space: nowrap;
292
+ }
293
+
294
+ /* Multi-select's leading checkbox: display only, so clicks land on the
295
+ row button that owns selection. */
296
+ .chai-search-menu__checkbox {
297
+ flex: none;
298
+ pointer-events: none;
299
+ }
300
+
301
+ .chai-search-menu__option svg {
302
+ width: 18px;
303
+ height: 18px;
304
+ flex: none;
305
+ }
306
+
307
+ /*
308
+ * Toggle header (reference: the design note "Auto-select model"
309
+ * mock) — an MD3 outlined-field look built by hand (the notched-outline
310
+ * trick: an absolutely-positioned label with the panel's own background
311
+ * masks the border behind it) rather than a real `<md-outlined-text-field>`,
312
+ * since this needs a switch inside it, not text input.
313
+ */
314
+ .chai-search-menu__toggle-header {
315
+ position: relative;
316
+ margin-bottom: var(--chai-space-2);
317
+ padding: var(--chai-space-3);
318
+ border: 1px solid var(--chai-color-semantic-control-border);
319
+ border-radius: var(--chai-radius-sm);
320
+ }
321
+
322
+ .chai-search-menu__toggle-header-label {
323
+ position: absolute;
324
+ top: 0;
325
+ left: 10px;
326
+ transform: translateY(-50%);
327
+ padding: 0 4px;
328
+ background: var(--chai-color-semantic-control-surface-raised);
329
+ color: var(--chai-color-semantic-accent-text);
330
+ font-family: var(--chai-type-family-material);
331
+ font-size: var(--chai-type-size-label-small);
332
+ letter-spacing: var(--chai-type-tracking-label-small);
333
+ }
334
+
335
+ .chai-search-menu__toggle-header-row {
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: space-between;
339
+ gap: var(--chai-space-3);
340
+ }
341
+
342
+ .chai-search-menu__toggle-header-text {
343
+ font-family: var(--chai-type-family-material);
344
+ font-size: var(--chai-type-size-body-large);
345
+ font-weight: var(--chai-type-weight-material-medium);
346
+ color: var(--chai-color-semantic-control-text-strong);
347
+ }
348
+
349
+ .chai-search-menu__empty {
350
+ padding: 10px 12px;
351
+ color: var(--chai-color-semantic-control-text);
352
+ font-size: var(--chai-type-size-sm);
353
+ }
354
+
355
+ /* Grouped sections (reference: Media Analyzer's "Select model" menu —
356
+ Image/Video/Audio) — a plain heading + divider, not a new visual
357
+ language: `&__option`'s own rules still apply inside `&__section-list`. */
358
+ .chai-search-menu__section-list {
359
+ list-style: none;
360
+ margin: 0;
361
+ padding: 0;
362
+ }
363
+
364
+ .chai-search-menu__section-label {
365
+ margin: 0;
366
+ padding: 8px 12px 4px;
367
+ color: var(--chai-color-semantic-control-text);
368
+ font-family: var(--chai-type-family-material);
369
+ font-size: var(--chai-type-size-label-small);
370
+ font-weight: var(--chai-type-weight-material-medium);
371
+ letter-spacing: var(--chai-type-tracking-label-small);
372
+ text-transform: uppercase;
373
+ }
374
+
375
+ .chai-search-menu__divider {
376
+ margin: var(--chai-space-2) 0;
377
+ border: none;
378
+ border-top: 1px solid var(--chai-color-semantic-control-divider);
379
+ }
380
+
381
+ @keyframes chai-spin {
382
+ to {
383
+ transform: rotate(360deg);
384
+ }
385
+ }
386
+
387
+ /*
388
+ * --- Pagination (primitives/Pagination.tsx) ---
389
+ * Progress-style pager (reference: the Figma "Pagination" asset) — a pill
390
+ * that fills from page 1 up to whichever page is current, not independent
391
+ * per-page dots. Matches the mockup's own dot graphic, which is a
392
+ * rounded-rect fill growing under plain circles, not a row of
393
+ * self-contained badges.
394
+ */
395
+ .chai-pagination {
396
+ position: relative;
397
+ display: inline-flex;
398
+ align-items: center;
399
+ align-self: center;
400
+ border-radius: var(--chai-radius-full);
401
+ cursor: pointer;
402
+ }
403
+
404
+ .chai-pagination--disabled {
405
+ cursor: not-allowed;
406
+ }
407
+
408
+ .chai-pagination:focus-visible {
409
+ outline: 2px solid var(--chai-color-semantic-accent);
410
+ outline-offset: 2px;
411
+ }
412
+
413
+ /* The pill is 16px tall, centred on the 24px-tall row. */
414
+ .chai-pagination__track {
415
+ position: absolute;
416
+ top: 4px;
417
+ bottom: 4px;
418
+ left: 0;
419
+ width: var(--chai-pagination-fill, 0%);
420
+ /* Chai UI's actual primary green — the same token `.chai-btn--primary`
421
+ and the selected Accept segment fill with — not `accent-strong`, which
422
+ is the darker shade reserved for text-on-light-bg contexts. */
423
+ background: var(--chai-color-semantic-accent);
424
+ border-radius: var(--chai-radius-full);
425
+ transition: width var(--chai-motion-duration-base) var(--chai-motion-easing-standard);
426
+ }
427
+
428
+ /* Each dot is a 16px cell drawing a 6px dot at its centre, so the dots sit
429
+ 16px apart. The whole row is one control, 24px tall, so it meets WCAG
430
+ 2.2's target size (2.5.8) without each dot having to. */
431
+ .chai-pagination__dot {
432
+ position: relative;
433
+ width: 16px;
434
+ height: 24px;
435
+ }
436
+
437
+ .chai-pagination__dot::before {
438
+ content: "";
439
+ position: absolute;
440
+ top: 9px;
441
+ left: 5px;
442
+ width: 6px;
443
+ height: 6px;
444
+ border-radius: 50%;
445
+ background: var(--chai-color-semantic-control-border-faint);
446
+ }
447
+
448
+ .chai-pagination__dot--filled::before {
449
+ background: var(--chai-color-semantic-text-on-accent);
450
+ }
451
+
452
+ /*
453
+ * --- Composer (bar layout) ---
454
+ * Reference: the "Compose Component" design. Its flat cool gray comes from the control-* semantic tokens, and
455
+ * the --chai-composer-* properties below are the per-component hooks for
456
+ * restyling it.
457
+ */
458
+ .chai-composer {
459
+ --chai-composer-bg: var(--chai-color-semantic-control-surface);
460
+ --chai-composer-border: var(--chai-color-semantic-control-border);
461
+ --chai-composer-placeholder: var(--chai-color-semantic-control-text-muted);
462
+ --chai-composer-text: var(--chai-color-semantic-control-text);
463
+ /* Every hover/open state in this component. */
464
+ --chai-composer-hover: var(--chai-color-semantic-control-surface-hover);
465
+
466
+ display: flex;
467
+ flex-direction: column;
468
+ gap: var(--chai-space-3);
469
+ padding: var(--chai-space-4) var(--chai-space-5);
470
+ /* Bespoke pill radius, doesn't match the --chai-radius-* scale (max lg
471
+ is 16px) — this container is meant to read rounder than any existing
472
+ card. */
473
+ border-radius: 22px;
474
+ border: 1px solid var(--chai-composer-border);
475
+ background: var(--chai-composer-bg);
476
+ font-family: var(--chai-type-family-material);
477
+ }
478
+
479
+ .chai-composer__thumbnails {
480
+ display: flex;
481
+ flex-wrap: wrap;
482
+ gap: var(--chai-space-3);
483
+ }
484
+
485
+ .chai-composer__thumb {
486
+ position: relative;
487
+ width: 88px;
488
+ height: 88px;
489
+ flex: none;
490
+ border-radius: var(--chai-radius-sm);
491
+ border: 1px solid var(--chai-color-semantic-control-divider);
492
+ background: var(--chai-composer-bg);
493
+ overflow: hidden;
494
+ display: flex;
495
+ align-items: center;
496
+ justify-content: center;
497
+ }
498
+
499
+ .chai-composer__thumb-img {
500
+ width: 100%;
501
+ height: 100%;
502
+ object-fit: cover;
503
+ }
504
+
505
+ .chai-composer__thumb-icon {
506
+ width: 28px;
507
+ height: 28px;
508
+ color: var(--chai-composer-placeholder);
509
+ }
510
+
511
+ .chai-composer__thumb-icon svg {
512
+ width: 100%;
513
+ height: 100%;
514
+ }
515
+
516
+ /* Hidden until the thumbnail is hovered/focused, same treatment as the
517
+ use-case chip's own remove button. */
518
+ .chai-composer__thumb-remove {
519
+ position: absolute;
520
+ top: 4px;
521
+ right: 4px;
522
+ width: 20px;
523
+ height: 20px;
524
+ display: flex;
525
+ align-items: center;
526
+ justify-content: center;
527
+ border: none;
528
+ border-radius: var(--chai-radius-full);
529
+ background: var(--chai-color-semantic-overlay);
530
+ color: var(--chai-color-semantic-on-overlay);
531
+ cursor: pointer;
532
+ opacity: 0;
533
+ transition: opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
534
+ }
535
+
536
+ .chai-composer__thumb:hover .chai-composer__thumb-remove,
537
+ .chai-composer__thumb:focus-within .chai-composer__thumb-remove {
538
+ opacity: 1;
539
+ }
540
+
541
+ /* Touch screens can't hover, so the remove button always shows. */
542
+ @media (hover: none) {
543
+ .chai-composer__thumb-remove {
544
+ opacity: 1;
545
+ }
546
+ }
547
+
548
+ .chai-composer__thumb-remove svg {
549
+ width: 14px;
550
+ height: 14px;
551
+ }
552
+
553
+ .chai-composer__field {
554
+ position: relative;
555
+ display: flex;
556
+ }
557
+
558
+ /* Shared by the real `<textarea>` and its reveal-overlay `<div>` (below) —
559
+ one source for the font/padding rules that must match exactly for the
560
+ overlay's text to sit pixel-for-pixel on top of the real thing, rather
561
+ than two copies that could quietly drift apart. */
562
+ .chai-composer__type {
563
+ padding: 0;
564
+ /* Clears the optimize button's full 40px hit box (see its own comment)
565
+ plus real breathing room — at the old 36px, typed text ran right up
566
+ against the icon before wrapping. */
567
+ padding-right: 56px;
568
+ overflow-wrap: break-word;
569
+ font-family: var(--chai-type-family-material);
570
+ /* Body Large, not the mock's own literal Title Large spec (22px) — reads
571
+ noticeably heavy/large once actually rendered live, a direct design correction over the original Figma value. */
572
+ font-size: var(--chai-type-size-body-large);
573
+ font-weight: var(--chai-type-weight-material-regular);
574
+ line-height: var(--chai-type-leading-body-large);
575
+ letter-spacing: var(--chai-type-tracking-body-large);
576
+ }
577
+
578
+ .chai-composer__input {
579
+ flex: 1;
580
+ resize: none;
581
+ overflow: hidden;
582
+ border: none;
583
+ background: transparent;
584
+ outline: none;
585
+ color: var(--chai-composer-text);
586
+ }
587
+
588
+ .chai-composer__input::placeholder {
589
+ color: var(--chai-composer-placeholder);
590
+ }
591
+
592
+ .chai-composer__input:disabled {
593
+ opacity: 0.6;
594
+ }
595
+
596
+ /* The optimize/undo reveal (Composer.tsx's own doc comment on the overlay
597
+ for the full mechanics) — the real textarea's text goes fully
598
+ transparent so only the overlay's progressive, gradient-lit copy shows
599
+ underneath it, not both at once. */
600
+ .chai-composer__input--revealing {
601
+ color: transparent;
602
+ }
603
+
604
+ .chai-composer__reveal {
605
+ position: absolute;
606
+ inset: 0;
607
+ white-space: pre-wrap;
608
+ pointer-events: none;
609
+ }
610
+
611
+ .chai-composer__reveal-text {
612
+ background: var(--chai-gradient-ai);
613
+ background-size: 300% 100%;
614
+ -webkit-background-clip: text;
615
+ background-clip: text;
616
+ color: transparent;
617
+ animation: chai-composer-reveal-shimmer 1.4s linear infinite;
618
+ }
619
+
620
+ @keyframes chai-composer-reveal-shimmer {
621
+ from {
622
+ background-position: 0% 50%;
623
+ }
624
+ to {
625
+ background-position: 300% 50%;
626
+ }
627
+ }
628
+
629
+ @media (prefers-reduced-motion: reduce) {
630
+ .chai-composer__reveal-text {
631
+ animation: none;
632
+ }
633
+ }
634
+
635
+ /* Width matches `<md-fab size="small">`'s own container width (MD3 spec:
636
+ 40px) so the icon centers over the submit FAB below it, not over this
637
+ button's own (much narrower) hit target — same right edge, different
638
+ widths, was reading visibly off-center from the FAB's icon. */
639
+ .chai-composer__optimize {
640
+ position: absolute;
641
+ top: 0;
642
+ right: 0;
643
+ display: flex;
644
+ align-items: center;
645
+ justify-content: center;
646
+ width: 40px;
647
+ height: 24px;
648
+ padding: 0;
649
+ border: none;
650
+ background: transparent;
651
+ color: var(--chai-composer-text);
652
+ cursor: pointer;
653
+ }
654
+
655
+ .chai-composer__optimize svg {
656
+ width: 24px;
657
+ height: 24px;
658
+ }
659
+
660
+ .chai-composer__optimize:disabled {
661
+ cursor: not-allowed;
662
+ opacity: 0.5;
663
+ }
664
+
665
+ .chai-composer__optimize:hover:not(:disabled),
666
+ .chai-composer__optimize--active {
667
+ /* The traced Optimize icon's "active" fill: the brand green. */
668
+ color: var(--chai-color-semantic-accent);
669
+ }
670
+
671
+ .chai-composer__controls {
672
+ display: flex;
673
+ align-items: center;
674
+ justify-content: space-between;
675
+ gap: var(--chai-space-3);
676
+ }
677
+
678
+ .chai-composer__controls-start {
679
+ display: flex;
680
+ align-items: center;
681
+ gap: var(--chai-space-3);
682
+ flex-wrap: wrap;
683
+ }
684
+
685
+ /* Neutral trigger text, not `--md-sys-color-primary` green (account
686
+ holder correction: "don't make dropdown select menu buttons green when
687
+ they don't need to be"). */
688
+ .chai-composer .chai-search-menu__trigger {
689
+ color: var(--chai-composer-text);
690
+ }
691
+
692
+ /* The shared `--trigger--open` rule fills with `--md-sys-color-secondary-
693
+ container` (brand green) — right for Model/Actions menus elsewhere, but
694
+ this component is deliberately neutral throughout (see the container
695
+ comment at the top of this section), so the open state here stays a
696
+ plain rounded-rectangle highlight instead. */
697
+ .chai-composer .chai-search-menu__trigger--open {
698
+ background: var(--chai-composer-hover);
699
+ border-color: transparent;
700
+ color: var(--chai-composer-text);
701
+ }
702
+
703
+ .chai-composer .chai-search-menu__trigger--text:hover:not(:disabled) {
704
+ background: var(--chai-composer-hover);
705
+ }
706
+
707
+ .chai-composer__attach {
708
+ position: relative;
709
+ }
710
+
711
+ .chai-composer__attach-toggle {
712
+ display: flex;
713
+ align-items: center;
714
+ justify-content: center;
715
+ width: 24px;
716
+ height: 24px;
717
+ padding: 0;
718
+ border: none;
719
+ border-radius: var(--chai-radius-full);
720
+ background: transparent;
721
+ color: var(--chai-composer-text);
722
+ cursor: pointer;
723
+ }
724
+
725
+ .chai-composer__attach-toggle svg {
726
+ width: 24px;
727
+ height: 24px;
728
+ }
729
+
730
+ .chai-composer__attach-toggle--open {
731
+ background: var(--chai-composer-hover);
732
+ }
733
+
734
+ .chai-composer__attach-toggle:disabled {
735
+ cursor: not-allowed;
736
+ opacity: 0.5;
737
+ }
738
+
739
+ .chai-composer__attach-menu {
740
+ position: absolute;
741
+ z-index: 10;
742
+ bottom: calc(100% + 8px);
743
+ left: 0;
744
+ min-width: 180px;
745
+ margin: 0;
746
+ padding: var(--chai-space-2) 0;
747
+ list-style: none;
748
+ border-radius: var(--chai-radius-sm);
749
+ background: var(--chai-color-semantic-control-surface-raised);
750
+ box-shadow: var(--chai-shadow-menu);
751
+ }
752
+
753
+ .chai-composer__attach-menu-item {
754
+ display: flex;
755
+ align-items: center;
756
+ gap: var(--chai-space-3);
757
+ width: 100%;
758
+ padding: var(--chai-space-2) var(--chai-space-4);
759
+ border: none;
760
+ background: transparent;
761
+ color: var(--chai-color-semantic-control-text-strong);
762
+ font-family: var(--chai-type-family-material);
763
+ font-size: var(--chai-type-size-label-large);
764
+ font-weight: var(--chai-type-weight-material-medium);
765
+ line-height: var(--chai-type-leading-label-large);
766
+ letter-spacing: var(--chai-type-tracking-label-large);
767
+ text-align: left;
768
+ cursor: pointer;
769
+ }
770
+
771
+ .chai-composer__attach-menu-item:hover {
772
+ background: var(--chai-composer-hover);
773
+ }
774
+
775
+ .chai-composer__attach-menu-icon {
776
+ width: 20px;
777
+ height: 20px;
778
+ flex: none;
779
+ color: var(--chai-color-semantic-control-text);
780
+ }
781
+
782
+ .chai-composer__attach-menu-icon svg {
783
+ width: 100%;
784
+ height: 100%;
785
+ }
786
+
787
+ /* Reference: "Add media" (`MenuListItem1`) is the only item with a divider
788
+ under it in the source (`showDivider: true`), separating the real
789
+ attach action from the three generation-mode actions below it. */
790
+ .chai-composer__attach-menu-divider {
791
+ margin: var(--chai-space-2) 0;
792
+ border: none;
793
+ border-top: 1px solid var(--chai-color-semantic-control-divider);
794
+ }
795
+
796
+ /* Hand-rolled use-case chip (see `UseCaseChip`'s own comment for why this
797
+ isn't `<md-input-chip>`) — rounded square, not MD3 input chip's stadium
798
+ default, no leading icon (that's the attach-menu's own affordance, not
799
+ the chip's — see `UseCaseChip`). No `gap` here (unlike other rows in
800
+ this component) — the remove button's own width/margin do that job
801
+ instead, collapsed to 0 at rest so the chip is genuinely narrower by
802
+ default, not just hiding an already-reserved slot; hovering (or
803
+ focusing) the chip expands it to reveal the button, rather than fading
804
+ an icon in place. */
805
+ .chai-composer__chip {
806
+ position: relative;
807
+ display: inline-flex;
808
+ align-items: center;
809
+ padding: 6px 10px;
810
+ border-radius: var(--chai-radius-sm);
811
+ border: 1px solid var(--chai-composer-border);
812
+ background: var(--chai-composer-bg);
813
+ color: var(--chai-composer-text);
814
+ font-family: var(--chai-type-family-material);
815
+ font-size: var(--chai-type-size-label-large);
816
+ font-weight: var(--chai-type-weight-material-medium);
817
+ line-height: var(--chai-type-leading-label-large);
818
+ letter-spacing: var(--chai-type-tracking-label-large);
819
+ white-space: nowrap;
820
+ transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
821
+ }
822
+
823
+ .chai-composer__chip:hover {
824
+ background: var(--chai-composer-hover);
825
+ }
826
+
827
+ .chai-composer__chip-remove {
828
+ display: flex;
829
+ align-items: center;
830
+ justify-content: center;
831
+ width: 0;
832
+ height: 16px;
833
+ margin-left: 0;
834
+ padding: 0;
835
+ border: none;
836
+ border-radius: var(--chai-radius-full);
837
+ background: transparent;
838
+ color: var(--chai-composer-text);
839
+ cursor: pointer;
840
+ opacity: 0;
841
+ overflow: hidden;
842
+ flex: none;
843
+ transition:
844
+ width var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
845
+ margin-left var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
846
+ opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
847
+ }
848
+
849
+ .chai-composer__chip-remove svg {
850
+ width: 16px;
851
+ height: 16px;
852
+ flex: none;
853
+ }
854
+
855
+ .chai-composer__chip:hover .chai-composer__chip-remove,
856
+ .chai-composer__chip:focus-within .chai-composer__chip-remove {
857
+ width: 16px;
858
+ margin-left: 6px;
859
+ opacity: 1;
860
+ }
861
+
862
+ .chai-composer__chip-remove:disabled {
863
+ cursor: not-allowed;
864
+ }
865
+
866
+ /* Edit regions, above the prompt field: each chip in its region's color
867
+ (set inline as --chai-region-color), with the numbered badge that marks
868
+ the same region on the image. */
869
+ .chai-composer__regions {
870
+ display: flex;
871
+ flex-wrap: wrap;
872
+ gap: var(--chai-space-2);
873
+ margin: 0;
874
+ padding: 0;
875
+ list-style: none;
876
+ }
877
+
878
+ .chai-composer__chip--region {
879
+ gap: 6px;
880
+ padding-left: 6px;
881
+ border-color: var(--chai-region-color);
882
+ }
883
+
884
+ .chai-composer__region-badge {
885
+ display: inline-flex;
886
+ align-items: center;
887
+ justify-content: center;
888
+ min-width: 18px;
889
+ height: 18px;
890
+ padding: 0 4px;
891
+ border-radius: var(--chai-radius-full);
892
+ background: var(--chai-region-color);
893
+ color: var(--chai-color-semantic-text-on-accent);
894
+ font-size: var(--chai-type-size-label-small);
895
+ line-height: 1;
896
+ }
897
+
898
+ .chai-composer__submit {
899
+ flex: none;
900
+ /* Flat always, both states — the mock has no FAB shadow at all. */
901
+ --md-fab-container-elevation: 0;
902
+ --md-fab-hover-container-elevation: 0;
903
+ --md-fab-pressed-container-elevation: 0;
904
+ --md-fab-focus-container-elevation: 0;
905
+ }
906
+
907
+ /* Unfilled rest state — icon only, no visible container, until there's a
908
+ valid prompt. Not M3's "surface" variant: a green-seeded theme's own
909
+ surface containers still pick up a faint tint from dynamic color, so
910
+ overriding the secondary-variant custom properties directly was the only
911
+ way to get a genuinely flat rest state. MD3's own hover state layer
912
+ (unaffected by these overrides) still gives the "slight color change on
913
+ hover" the mock wants, with no extra rule needed. Once `[data-ready]` is
914
+ set, neither override applies and `<md-fab>`'s real `variant="secondary"`
915
+ colors (the brand-green bridge) show through on their own. */
916
+ .chai-composer__submit:not([data-ready]) {
917
+ --md-fab-secondary-container-color: transparent;
918
+ --md-fab-secondary-icon-color: var(--chai-composer-text);
919
+ }
920
+
921
+ /* Retry state — real `--chai-color-semantic-danger*` tokens, not this
922
+ component's own literal-hex palette: unlike the neutral chrome, an
923
+ error should look the same "error" everywhere in the app, not just in
924
+ Composer. `[data-error]` can co-occur with `[data-ready]` (retry isn't
925
+ gated on `hasContent`, see `submitError`'s doc comment), so this rule
926
+ is scoped only to `[data-error]` and always wins when it's set. */
927
+ .chai-composer__submit[data-error] {
928
+ --md-fab-secondary-container-color: var(--chai-color-semantic-danger-subtle);
929
+ --md-fab-secondary-icon-color: var(--chai-color-semantic-danger);
930
+ }
931
+
932
+ /* Stop state — real `--chai-color-semantic-warning*` tokens (a light
933
+ orange), same reasoning as `[data-error]` above: reuses an existing
934
+ system color rather than inventing a one-off literal hex for this one
935
+ moment. Declared before `[data-error]` so error still wins if both were
936
+ ever true (they aren't, in practice — a request is either in flight or
937
+ already failed, never both), matching that rule's own documented
938
+ precedence. */
939
+ .chai-composer__submit[data-stop] {
940
+ --md-fab-secondary-container-color: var(--chai-color-semantic-warning-subtle);
941
+ --md-fab-secondary-icon-color: var(--chai-color-semantic-warning);
942
+ }
943
+
944
+ .chai-composer__error {
945
+ display: flex;
946
+ align-items: center;
947
+ gap: var(--chai-space-2);
948
+ margin: 0;
949
+ padding: 2px 2px 0;
950
+ color: var(--chai-color-semantic-danger);
951
+ font-family: var(--chai-type-family-material);
952
+ font-size: var(--chai-type-size-label-large);
953
+ font-weight: var(--chai-type-weight-material-medium);
954
+ line-height: var(--chai-type-leading-label-large);
955
+ letter-spacing: var(--chai-type-tracking-label-large);
956
+ }
957
+
958
+ .chai-composer__error svg {
959
+ width: 16px;
960
+ height: 16px;
961
+ flex: none;
962
+ }
963
+
964
+ /*
965
+ * --- Media Analyzer (reference: the "Composer from Media Analysis" design)
966
+ * — same treatment as `.chai-composer` above, with the
967
+ * exact same container/border/radius values, since the mock reuses them.
968
+ */
969
+ .chai-media-analyzer {
970
+ --chai-media-analyzer-bg: var(--chai-color-semantic-control-surface);
971
+ --chai-media-analyzer-border: var(--chai-color-semantic-control-border);
972
+ --chai-media-analyzer-text: var(--chai-color-semantic-control-text);
973
+ --chai-media-analyzer-hover: var(--chai-color-semantic-control-surface-hover);
974
+
975
+ display: flex;
976
+ flex-direction: column;
977
+ gap: var(--chai-space-3);
978
+ padding: var(--chai-space-4) var(--chai-space-5);
979
+ border-radius: 22px;
980
+ border: 1px solid var(--chai-media-analyzer-border);
981
+ background: var(--chai-media-analyzer-bg);
982
+ font-family: var(--chai-type-family-material);
983
+ }
984
+
985
+ .chai-media-analyzer__dropzone {
986
+ display: flex;
987
+ flex-direction: column;
988
+ align-items: center;
989
+ justify-content: center;
990
+ gap: var(--chai-space-2);
991
+ min-height: 140px;
992
+ padding: var(--chai-space-4);
993
+ border-radius: var(--chai-radius-md);
994
+ border: 1.5px dashed var(--chai-color-semantic-control-text-strong);
995
+ /* Brand-green wash, same token `.chai-drop-zone` reads for the same
996
+ reason (see that rule's own comment) — this dashed box is this
997
+ component's equivalent surface. */
998
+ background: var(--chai-color-semantic-accent-subtle);
999
+ text-align: center;
1000
+ cursor: pointer;
1001
+ transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
1002
+ border-color var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
1003
+ }
1004
+
1005
+ /* At cap for the attached kind (reference: "Analyze Media Composer -
1006
+ Disabled - Maximum Media Attached") — flat gray, not a dimmed green
1007
+ (mixing "disabled" with the brand wash read as broken, not intentional).
1008
+ Still a real drop target for a *different* kind, which always replaces
1009
+ what's here — see MediaAnalyzer.tsx's `nextMediaAnalyzerAttachments` for
1010
+ why this never fully disables the zone itself, only the same-kind add. */
1011
+ .chai-media-analyzer__dropzone--disabled {
1012
+ background: var(--chai-color-semantic-control-surface-hover);
1013
+ cursor: default;
1014
+ }
1015
+
1016
+ /* A file is being dragged over the zone but not dropped yet — there was no
1017
+ feedback at all for this before (the box looked identical whether or not
1018
+ a drag was even over it). Solid border + a visibly darker green than the
1019
+ dashed zone's own resting wash, so "this is a live drop target right
1020
+ now" reads clearly regardless of whether the zone is also at cap (a
1021
+ drag-over can still be a valid cross-kind replace even then, so this
1022
+ wins over `--disabled`'s gray — see the design note on
1023
+ the drop zone always accepting a different kind). */
1024
+ .chai-media-analyzer__dropzone--drag-active,
1025
+ .chai-media-analyzer__dropzone--disabled.chai-media-analyzer__dropzone--drag-active {
1026
+ border-style: solid;
1027
+ border-color: var(--chai-color-semantic-accent);
1028
+ background: var(--chai-color-semantic-accent-soft);
1029
+ }
1030
+
1031
+ .chai-media-analyzer__dropzone-icon {
1032
+ width: 32px;
1033
+ height: 32px;
1034
+ color: var(--chai-color-semantic-control-text);
1035
+ }
1036
+
1037
+ .chai-media-analyzer__dropzone-icon svg {
1038
+ width: 100%;
1039
+ height: 100%;
1040
+ }
1041
+
1042
+ .chai-media-analyzer__dropzone-hint {
1043
+ margin: 0;
1044
+ font-size: var(--chai-type-size-body-medium);
1045
+ font-weight: var(--chai-type-weight-material-regular);
1046
+ color: var(--chai-media-analyzer-text);
1047
+ }
1048
+
1049
+ .chai-media-analyzer__thumbnails {
1050
+ display: flex;
1051
+ flex-wrap: wrap;
1052
+ gap: var(--chai-space-2);
1053
+ }
1054
+
1055
+ /* Same shape/behavior as `.chai-composer__thumb`, sized down (56px, not
1056
+ 88px) so all 5 of an image analysis's maximum fit in one row under the
1057
+ drag box at typical card widths (reference: the design
1058
+ spec — "slightly smaller to fit a total of five image thumbs"). */
1059
+ .chai-media-analyzer__thumb {
1060
+ position: relative;
1061
+ width: 56px;
1062
+ height: 56px;
1063
+ flex: none;
1064
+ border-radius: var(--chai-radius-sm);
1065
+ border: 1px solid var(--chai-color-semantic-control-divider);
1066
+ background: var(--chai-media-analyzer-bg);
1067
+ overflow: hidden;
1068
+ display: flex;
1069
+ align-items: center;
1070
+ justify-content: center;
1071
+ }
1072
+
1073
+ .chai-media-analyzer__thumb-img {
1074
+ width: 100%;
1075
+ height: 100%;
1076
+ object-fit: cover;
1077
+ }
1078
+
1079
+ .chai-media-analyzer__thumb-icon {
1080
+ width: 20px;
1081
+ height: 20px;
1082
+ color: var(--chai-media-analyzer-text);
1083
+ }
1084
+
1085
+ .chai-media-analyzer__thumb-icon svg {
1086
+ width: 100%;
1087
+ height: 100%;
1088
+ }
1089
+
1090
+ .chai-media-analyzer__thumb-remove {
1091
+ position: absolute;
1092
+ top: 2px;
1093
+ right: 2px;
1094
+ width: 20px;
1095
+ height: 20px;
1096
+ display: flex;
1097
+ align-items: center;
1098
+ justify-content: center;
1099
+ border: none;
1100
+ border-radius: var(--chai-radius-full);
1101
+ /* Darker + fully opaque (was translucent black) — against a busy photo
1102
+ thumbnail a semi-transparent badge let the image show through and
1103
+ washed out the X on top of it. A solid, near-black circle is what
1104
+ actually gives the white X reliable contrast in every case. */
1105
+ background: var(--chai-color-semantic-overlay);
1106
+ color: var(--chai-color-semantic-on-overlay);
1107
+ cursor: pointer;
1108
+ opacity: 0;
1109
+ transition: opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
1110
+ }
1111
+
1112
+ .chai-media-analyzer__thumb:hover .chai-media-analyzer__thumb-remove,
1113
+ .chai-media-analyzer__thumb:focus-within .chai-media-analyzer__thumb-remove {
1114
+ opacity: 1;
1115
+ }
1116
+
1117
+ @media (hover: none) {
1118
+ .chai-media-analyzer__thumb-remove {
1119
+ opacity: 1;
1120
+ }
1121
+ }
1122
+
1123
+ /* Still tiny in absolute terms (a corner badge on a 56px thumbnail can only
1124
+ be so big), but the X itself should read as a solid mark, not a hairline
1125
+ — reference: the design note "more legible" example, a chunky
1126
+ X that fills most of the circle rather than a thin one centered in a lot
1127
+ of empty padding. Both the icon's own size *and* its `strokeWidth`
1128
+ (RemoveIcon, MediaAnalyzer.tsx) went up together — bumping only one or
1129
+ the other still read as too delicate at this size. */
1130
+ .chai-media-analyzer__thumb-remove svg {
1131
+ width: 14px;
1132
+ height: 14px;
1133
+ }
1134
+
1135
+ .chai-media-analyzer__controls {
1136
+ display: flex;
1137
+ align-items: center;
1138
+ justify-content: space-between;
1139
+ gap: var(--chai-space-3);
1140
+ }
1141
+
1142
+ .chai-media-analyzer__controls-start {
1143
+ display: flex;
1144
+ align-items: center;
1145
+ gap: var(--chai-space-2);
1146
+ flex-wrap: wrap;
1147
+ }
1148
+
1149
+ /* Neutral trigger text/open-state, not the shared rule's brand-green fill —
1150
+ same correction `.chai-composer` already applies to its own SearchMenu
1151
+ triggers, for the same reason (see that rule's own comment). */
1152
+ .chai-media-analyzer .chai-search-menu__trigger {
1153
+ color: var(--chai-media-analyzer-text);
1154
+ }
1155
+
1156
+ .chai-media-analyzer .chai-search-menu__trigger--open {
1157
+ background: var(--chai-media-analyzer-hover);
1158
+ border-color: transparent;
1159
+ color: var(--chai-media-analyzer-text);
1160
+ }
1161
+
1162
+ .chai-media-analyzer .chai-search-menu__trigger--text:hover:not(:disabled) {
1163
+ background: var(--chai-media-analyzer-hover);
1164
+ }
1165
+
1166
+ .chai-media-analyzer__submit {
1167
+ flex: none;
1168
+ --md-fab-container-elevation: 0;
1169
+ --md-fab-hover-container-elevation: 0;
1170
+ --md-fab-pressed-container-elevation: 0;
1171
+ --md-fab-focus-container-elevation: 0;
1172
+ }
1173
+
1174
+ /* Unfilled until there's at least one attachment to submit — same reasoning
1175
+ as `.chai-composer__submit:not([data-ready])`. */
1176
+ .chai-media-analyzer__submit:not([data-ready]) {
1177
+ --md-fab-secondary-container-color: transparent;
1178
+ --md-fab-secondary-icon-color: var(--chai-media-analyzer-text);
1179
+ }
1180
+
1181
+ /* While analyzing, with `onAbort`: submit becomes stop, same as the Composer's.
1182
+ Before `[data-error]`, so an error still wins. */
1183
+ .chai-media-analyzer__submit[data-stop] {
1184
+ --md-fab-secondary-container-color: var(--chai-color-semantic-warning-subtle);
1185
+ --md-fab-secondary-icon-color: var(--chai-color-semantic-warning);
1186
+ }
1187
+
1188
+ .chai-media-analyzer__submit[data-error] {
1189
+ --md-fab-secondary-container-color: var(--chai-color-semantic-danger-subtle);
1190
+ --md-fab-secondary-icon-color: var(--chai-color-semantic-danger);
1191
+ }
1192
+
1193
+ /* What the last attach replaced (one kind per analysis). */
1194
+ .chai-media-analyzer__note {
1195
+ margin: 0;
1196
+ padding: 2px 2px 0;
1197
+ color: var(--chai-media-analyzer-text);
1198
+ font-size: var(--chai-type-size-label-large);
1199
+ }
1200
+
1201
+ .chai-media-analyzer__error {
1202
+ margin: 0;
1203
+ padding: 2px 2px 0;
1204
+ color: var(--chai-color-semantic-danger);
1205
+ font-size: var(--chai-type-size-label-large);
1206
+ font-weight: var(--chai-type-weight-material-medium);
1207
+ }
1208
+
1209
+ /*
1210
+ * --- Result Card (reference: Figma "08 Result from Composer Bar", node
1211
+ * 194:1695) — Composer bar's compliment. Literal hex again (not
1212
+ * --chai- or --md-sys-color- tokens) for the card chrome itself, same
1213
+ * "green-seeded theme tints everything neutral" reason `.chai-composer`'s
1214
+ * own container comment documents; chips/menus underneath still read real
1215
+ * MD3 tokens since those aren't large flat fills.
1216
+ */
1217
+
1218
+ /* A fixed default width (375px, matching the Figma mock), not sized off
1219
+ the source media's own pixel dimensions — design
1220
+ correction: letting the card's *width* track a real generated image's
1221
+ actual resolution would make the card's footprint unpredictable
1222
+ (anywhere from a couple hundred px to several thousand) and break the
1223
+ layout of whatever app it's embedded in. The *height* still isn't
1224
+ fixed: the media inside renders at `width: 100%; height: auto` (its own
1225
+ comment below), so at this one fixed width, height naturally follows
1226
+ whatever the real aspect ratio is — a 4:3 image lands close to the
1227
+ Figma mock's own ~335px total card height, 1:1 taller, 16:9 shorter,
1228
+ with no special-casing needed per ratio. This is the actual "thumbnail"
1229
+ behavior (scaled down, never cropped) — the "Expand" button is what
1230
+ shows the real, full-size media when someone needs it. A `width` prop
1231
+ overrides the 375px default for a builder who wants a different fixed
1232
+ footprint. */
1233
+ .chai-result-card {
1234
+ width: var(--chai-result-card-width, 375px);
1235
+ max-width: 100%;
1236
+ perspective: 1800px;
1237
+ font-family: var(--chai-type-family-material);
1238
+ }
1239
+
1240
+ /* The *front* (the media) is what establishes the card's height — normal
1241
+ block flow, no special positioning, so `.inner`'s own auto height is
1242
+ just "whatever `.front`'s natural content needs." The *back* (info
1243
+ side) is `position: absolute; inset: 0` and simply conforms to
1244
+ whatever height that resolves to, scrolling its own prompt box
1245
+ internally if its content needs more room than the media provided.
1246
+ Deliberately not symmetric ("taller of the two, either stretches to
1247
+ match") — a design correction: a media-driven card
1248
+ flipping to a shorter or differently-proportioned info side read as
1249
+ visually inconsistent, and media should stay the one source of truth
1250
+ for the card's height in both states. `.back` being absolute doesn't
1251
+ reintroduce the old circular-sizing problem (`position: absolute` on
1252
+ *both* faces once tried and reverted) because only one side of that
1253
+ circle exists now — `.front` is never absolute, so it always has a
1254
+ real, independently-computed height for `.back` to match. */
1255
+ .chai-result-card__inner {
1256
+ position: relative;
1257
+ width: 100%;
1258
+ transition: transform var(--chai-motion-duration-slow) var(--chai-motion-easing-standard);
1259
+ transform-style: preserve-3d;
1260
+ }
1261
+
1262
+ .chai-result-card--flipped .chai-result-card__inner {
1263
+ transform: rotateY(180deg);
1264
+ }
1265
+
1266
+ .chai-result-card__face {
1267
+ width: 100%;
1268
+ display: flex;
1269
+ flex-direction: column;
1270
+ backface-visibility: hidden;
1271
+ border-radius: 29px;
1272
+ border: 1px solid var(--chai-color-semantic-control-border-soft);
1273
+ overflow: hidden;
1274
+ }
1275
+
1276
+ /* The media fills the face, with the controls floating over its bottom
1277
+ (see "Floating card controls"). `--chai-card-bottom-clear` is how much of
1278
+ the bottom they cover, for content that must stay clear of them. */
1279
+ .chai-result-card__front {
1280
+ --chai-card-bottom-clear: 56px;
1281
+ position: relative;
1282
+ background: var(--chai-color-semantic-control-surface);
1283
+ /* `.back` is later in DOM order and stacks above `.front` regardless of
1284
+ which face is currently rotated toward the viewer (sibling stacking
1285
+ order doesn't reorder itself just because `backface-visibility` hides
1286
+ paint) — so without this, `.back`'s own hidden-but-still-present box
1287
+ can swallow clicks meant for `.front` in some engines. Pointer-events
1288
+ are switched explicitly per face instead of trusting
1289
+ `backface-visibility: hidden` to also gate hit-testing. */
1290
+ pointer-events: auto;
1291
+ }
1292
+
1293
+ .chai-result-card--flipped .chai-result-card__front {
1294
+ pointer-events: none;
1295
+ }
1296
+
1297
+ .chai-result-card__back {
1298
+ position: absolute;
1299
+ inset: 0;
1300
+ background: var(--chai-color-semantic-control-surface-raised);
1301
+ transform: rotateY(180deg);
1302
+ padding: var(--chai-space-5) var(--chai-space-4) var(--chai-space-4);
1303
+ gap: var(--chai-space-3);
1304
+ pointer-events: none;
1305
+ }
1306
+
1307
+ .chai-result-card--flipped .chai-result-card__back {
1308
+ pointer-events: auto;
1309
+ }
1310
+
1311
+ /* --- Media (image/video/audio/text, by status) ---
1312
+ * `flex: 1` here is only a graceful-overflow behavior, not a fixed size:
1313
+ * with no explicit height anywhere in this chain, the media area's own
1314
+ * natural size already comes from its image/video content below (`width:
1315
+ * 100%; height: auto`, no `object-fit` cropping/letterboxing needed) — it
1316
+ * only actually stretches on the rarer occasion the *other* face (the
1317
+ * flipped info side) is naturally taller, since both faces share one grid
1318
+ * track sized to the taller one (`.chai-result-card__inner`'s own
1319
+ * comment). When that happens the extra height lands here as breathing
1320
+ * room around a still-centered, still-unstretched image, not a fixed gray
1321
+ * box the image sits inside by default.
1322
+ */
1323
+ /* Holds the paging slide: clips the page sliding out, and passes the face's
1324
+ column layout through so the media still sizes as before. */
1325
+ .chai-result-card__media-frame,
1326
+ .chai-result-card__media-page {
1327
+ position: relative;
1328
+ flex: 1;
1329
+ display: flex;
1330
+ flex-direction: column;
1331
+ }
1332
+
1333
+ .chai-result-card__media-frame {
1334
+ overflow: hidden;
1335
+ }
1336
+
1337
+ .chai-result-card__media {
1338
+ position: relative;
1339
+ flex: 1;
1340
+ display: flex;
1341
+ align-items: center;
1342
+ justify-content: center;
1343
+ background: var(--chai-color-semantic-control-surface);
1344
+ overflow: hidden;
1345
+ }
1346
+
1347
+ /* Queued/running/error/audio/text have no image of their own to derive a
1348
+ natural size from, so they get one fixed reasonable height instead of
1349
+ collapsing to near-zero. */
1350
+ .chai-result-card__media--fixed-height,
1351
+ .chai-result-card__media--pending,
1352
+ .chai-result-card__media--error,
1353
+ .chai-result-card__media--cancelled {
1354
+ min-height: 220px;
1355
+ padding: var(--chai-space-5);
1356
+ /* Text, audio and status messages end above the floating controls. */
1357
+ padding-bottom: var(--chai-card-bottom-clear);
1358
+ }
1359
+
1360
+ /* The floating votes and details sit above the pager bar. */
1361
+ .chai-result-card__front--paginated .chai-card-bottom {
1362
+ bottom: calc(var(--chai-card-pager-bar) + 12px);
1363
+ }
1364
+
1365
+ /* A sibling of `.chai-result-card` itself, not a descendant — see the
1366
+ overlay's own comment in ResultCard.tsx (`perspective` on the card, set
1367
+ for the flip animation, would otherwise trap `position: fixed` inside
1368
+ the card's own box instead of the real viewport). */
1369
+ .chai-result-card__expand-overlay {
1370
+ position: fixed;
1371
+ inset: 0;
1372
+ z-index: 50;
1373
+ display: flex;
1374
+ align-items: center;
1375
+ justify-content: center;
1376
+ }
1377
+
1378
+ .chai-result-card__expand-scrim {
1379
+ position: absolute;
1380
+ inset: 0;
1381
+ z-index: 0;
1382
+ background: var(--chai-color-semantic-scrim);
1383
+ }
1384
+
1385
+ .chai-result-card__expand-frame {
1386
+ position: relative;
1387
+ z-index: 1;
1388
+ width: min(90vw, 960px);
1389
+ height: min(80vh, 540px);
1390
+ border-radius: var(--chai-radius-lg);
1391
+ overflow: hidden;
1392
+ background: var(--chai-color-semantic-media-backdrop);
1393
+ }
1394
+
1395
+ .chai-result-card__expand-frame .chai-result-card__video,
1396
+ .chai-result-card__expand-frame .chai-result-card__image {
1397
+ /* The frame itself is a fixed box (unlike the inline card, which sizes
1398
+ to the media) — `object-fit: contain` here is what keeps a portrait
1399
+ or unusually-shaped image/video from overflowing it, height: 100%
1400
+ instead of auto since auto would size off the media's own aspect
1401
+ ratio again and could exceed the frame's fixed height. */
1402
+ height: 100%;
1403
+ object-fit: contain;
1404
+ aspect-ratio: auto;
1405
+ border-radius: 0;
1406
+ }
1407
+
1408
+ /* No `object-fit`/`max-width`/`max-height` — the whole point of sizing the
1409
+ card itself to the media (see `.chai-result-card`'s own comment) is that
1410
+ the image never needs cropping or letterboxing to begin with: it just
1411
+ renders at its own natural aspect ratio, full width, corners rounded by
1412
+ `.chai-result-card__face`'s own `overflow: hidden` around it. */
1413
+ .chai-result-card__image {
1414
+ position: relative;
1415
+ z-index: 1;
1416
+ display: block;
1417
+ width: 100%;
1418
+ height: auto;
1419
+ }
1420
+
1421
+ .chai-result-card__video {
1422
+ position: relative;
1423
+ z-index: 1;
1424
+ display: block;
1425
+ width: 100%;
1426
+ height: auto;
1427
+ /* A reasonable guess before real `loadedmetadata` dimensions are known
1428
+ (avoids a 0-height flash) — once they load, intrinsic sizing from
1429
+ `width: 100%; height: auto` takes over and this stops applying. */
1430
+ aspect-ratio: 16 / 9;
1431
+ background: var(--chai-color-semantic-media-backdrop);
1432
+ cursor: pointer;
1433
+ }
1434
+
1435
+ .chai-result-card__audio {
1436
+ position: relative;
1437
+ z-index: 1;
1438
+ width: 90%;
1439
+ }
1440
+
1441
+ /* Model output is often long and structured, so line breaks are kept and
1442
+ the text scrolls past a max height instead of growing the card. Builders
1443
+ can change the height with --chai-result-card-text-max-height. */
1444
+ .chai-result-card__text-output {
1445
+ position: relative;
1446
+ z-index: 1;
1447
+ /* Starts below the download/share buttons, which sit at the top right:
1448
+ text scrolling underneath them was hard to read. */
1449
+ margin: var(--chai-space-4) 0 0;
1450
+ padding: var(--chai-space-4);
1451
+ min-height: 160px;
1452
+ max-height: var(--chai-result-card-text-max-height, 400px);
1453
+ overflow-y: auto;
1454
+ white-space: pre-wrap;
1455
+ overflow-wrap: anywhere;
1456
+ font-size: var(--chai-type-size-body-medium);
1457
+ line-height: var(--chai-type-leading-body-medium);
1458
+ color: var(--chai-color-semantic-control-text-strong);
1459
+ }
1460
+
1461
+ /* An always-visible scrollbar, so it's clear more text exists. macOS hides
1462
+ overlay scrollbars until you scroll, which made long answers look cut
1463
+ off. WebKit/Blink use the pseudo-elements; Firefox the standard props. */
1464
+ .chai-result-card__text-output::-webkit-scrollbar {
1465
+ width: 8px;
1466
+ }
1467
+
1468
+ .chai-result-card__text-output::-webkit-scrollbar-track {
1469
+ background: transparent;
1470
+ }
1471
+
1472
+ .chai-result-card__text-output::-webkit-scrollbar-thumb {
1473
+ border-radius: var(--chai-radius-full);
1474
+ background: var(--chai-color-semantic-control-border-soft);
1475
+ }
1476
+
1477
+ @supports not selector(::-webkit-scrollbar) {
1478
+ .chai-result-card__text-output {
1479
+ scrollbar-width: thin;
1480
+ scrollbar-color: var(--chai-color-semantic-control-border-soft) transparent;
1481
+ }
1482
+ }
1483
+
1484
+ /* Streaming caret — a block cursor trailing the partial text while a text
1485
+ result is still arriving. */
1486
+ .chai-result-card__text-output--streaming::after {
1487
+ content: "";
1488
+ display: inline-block;
1489
+ width: 0.5em;
1490
+ height: 1em;
1491
+ margin-left: 2px;
1492
+ vertical-align: text-bottom;
1493
+ background: var(--chai-color-semantic-control-text);
1494
+ animation: chai-caret-blink calc(var(--chai-motion-duration-steep) * 1.2) steps(1) infinite;
1495
+ }
1496
+
1497
+ @keyframes chai-caret-blink {
1498
+ 50% {
1499
+ opacity: 0;
1500
+ }
1501
+ }
1502
+
1503
+ @media (prefers-reduced-motion: reduce) {
1504
+ .chai-result-card__text-output--streaming::after {
1505
+ animation: none;
1506
+ }
1507
+ }
1508
+
1509
+ .chai-result-card__spinner {
1510
+ width: 32px;
1511
+ height: 32px;
1512
+ border-radius: 50%;
1513
+ border: 3px solid var(--chai-color-semantic-control-border-faint);
1514
+ border-top-color: var(--chai-color-semantic-accent);
1515
+ animation: chai-spin var(--chai-motion-duration-steep) linear infinite;
1516
+ }
1517
+
1518
+ .chai-result-card__cancelled-text {
1519
+ margin: 0;
1520
+ font-size: var(--chai-type-size-body-medium);
1521
+ color: var(--chai-color-semantic-control-text-muted);
1522
+ }
1523
+
1524
+ .chai-result-card__error-text {
1525
+ margin: 0;
1526
+ padding: 0 var(--chai-space-4);
1527
+ text-align: center;
1528
+ font-size: var(--chai-type-size-body-medium);
1529
+ color: var(--chai-color-semantic-danger);
1530
+ }
1531
+
1532
+ /* Play/scrubber sit relative to their nearest positioned ancestor —
1533
+ `.chai-result-card__media` inline, `.chai-result-card__expand-frame`
1534
+ when expanded — not the `<video>` element itself. Both are exactly the
1535
+ video's own rendered box (`object-fit: contain` fills either one), so
1536
+ the same rule works for both without measuring the video's actual
1537
+ painted rect. On an aspect ratio very different from that box, the
1538
+ scrubber can sit in the letterboxed margin rather than snug against the
1539
+ visible frame — accepted trade-off against the complexity of measuring
1540
+ real video bounds for what's a slim progress bar, not a hit-critical
1541
+ control. */
1542
+ .chai-result-card__play {
1543
+ position: absolute;
1544
+ z-index: 1;
1545
+ top: 50%;
1546
+ left: 50%;
1547
+ transform: translate(-50%, -50%);
1548
+ width: 56px;
1549
+ height: 56px;
1550
+ border-radius: 50%;
1551
+ border: none;
1552
+ background: var(--chai-color-semantic-overlay);
1553
+ color: var(--chai-color-semantic-accent);
1554
+ display: flex;
1555
+ align-items: center;
1556
+ justify-content: center;
1557
+ cursor: pointer;
1558
+ }
1559
+
1560
+ .chai-result-card__play svg {
1561
+ width: 28px;
1562
+ height: 28px;
1563
+ }
1564
+
1565
+ /* The scrubber is a 24px-tall slider (WCAG 2.2 target size) drawing the
1566
+ same thin track, centred where the track has always sat. */
1567
+ .chai-result-card__scrubber {
1568
+ --chai-scrubber-track: 6px;
1569
+ position: absolute;
1570
+ z-index: 1;
1571
+ left: 12px;
1572
+ right: 12px;
1573
+ /* Above the floating controls in the card; near the bottom when expanded. */
1574
+ bottom: calc(var(--chai-card-bottom-clear, 10px) - (24px - var(--chai-scrubber-track)) / 2);
1575
+ height: 24px;
1576
+ border-radius: 6px;
1577
+ cursor: pointer;
1578
+ }
1579
+
1580
+ .chai-result-card__scrubber::before,
1581
+ .chai-result-card__scrubber-fill {
1582
+ position: absolute;
1583
+ top: calc((24px - var(--chai-scrubber-track)) / 2);
1584
+ height: var(--chai-scrubber-track);
1585
+ border-radius: var(--chai-scrubber-track);
1586
+ }
1587
+
1588
+ .chai-result-card__scrubber::before {
1589
+ content: "";
1590
+ left: 0;
1591
+ right: 0;
1592
+ background: var(--chai-color-semantic-control-track);
1593
+ }
1594
+
1595
+ .chai-result-card__scrubber--expanded {
1596
+ --chai-scrubber-track: 8px;
1597
+ left: 16px;
1598
+ right: 16px;
1599
+ bottom: calc(16px - (24px - var(--chai-scrubber-track)) / 2);
1600
+ }
1601
+
1602
+ .chai-result-card__scrubber-fill {
1603
+ left: 0;
1604
+ background: var(--chai-color-semantic-accent);
1605
+ }
1606
+
1607
+ /* Transparent, covering the video: the play/pause control. */
1608
+ .chai-result-card__video-toggle {
1609
+ position: absolute;
1610
+ inset: 0;
1611
+ padding: 0;
1612
+ border: none;
1613
+ background: transparent;
1614
+ cursor: pointer;
1615
+ }
1616
+
1617
+ .chai-result-card__collapse {
1618
+ position: fixed;
1619
+ z-index: 2;
1620
+ top: 20px;
1621
+ right: 20px;
1622
+ width: 36px;
1623
+ height: 36px;
1624
+ border-radius: 50%;
1625
+ border: none;
1626
+ background: var(--chai-color-semantic-overlay);
1627
+ color: var(--chai-color-semantic-on-overlay);
1628
+ display: flex;
1629
+ align-items: center;
1630
+ justify-content: center;
1631
+ cursor: pointer;
1632
+ }
1633
+
1634
+ .chai-result-card__collapse svg {
1635
+ width: 14px;
1636
+ height: 14px;
1637
+ }
1638
+
1639
+ /* --- More menu (reference: Figma "Result Card - More menu expanded") --- */
1640
+
1641
+ .chai-result-card__more {
1642
+ position: relative;
1643
+ }
1644
+
1645
+ /* Opens upward, not down like the Figma mock's own isolated rendering of
1646
+ it — the "more" trigger sits right at the card's bottom edge, and
1647
+ `.chai-result-card__face` needs `overflow: hidden` for its rounded
1648
+ corners and the flip's `backface-visibility`, which would clip a
1649
+ downward menu (confirmed via a real screenshot during this build — the
1650
+ trigger showed pressed/active but no menu was visible at all). Opening
1651
+ upward keeps it inside the still-visible media area above the bar. */
1652
+ .chai-result-card__more-menu {
1653
+ position: absolute;
1654
+ z-index: 10;
1655
+ bottom: calc(100% + 8px);
1656
+ left: 0;
1657
+ width: 180px;
1658
+ margin: 0;
1659
+ padding: var(--chai-space-2);
1660
+ list-style: none;
1661
+ border-radius: var(--chai-radius-lg);
1662
+ background: var(--chai-color-semantic-control-surface-raised);
1663
+ box-shadow: var(--chai-shadow-menu);
1664
+ }
1665
+
1666
+ .chai-result-card__more-menu-item {
1667
+ display: flex;
1668
+ align-items: center;
1669
+ gap: var(--chai-space-2);
1670
+ width: 100%;
1671
+ padding: 10px 12px;
1672
+ border: none;
1673
+ border-radius: var(--chai-radius-sm);
1674
+ background: transparent;
1675
+ color: var(--chai-color-semantic-control-text-strong);
1676
+ font-family: var(--chai-type-family-material);
1677
+ font-size: var(--chai-type-size-body-large);
1678
+ text-align: left;
1679
+ cursor: pointer;
1680
+ }
1681
+
1682
+ .chai-result-card__more-menu-item:hover {
1683
+ background: var(--chai-color-semantic-control-surface-hover);
1684
+ }
1685
+
1686
+ .chai-result-card__more-menu-icon {
1687
+ display: flex;
1688
+ width: 18px;
1689
+ height: 18px;
1690
+ flex: none;
1691
+ color: var(--chai-color-semantic-control-text);
1692
+ }
1693
+
1694
+ /* --- Info flip (back face) --- */
1695
+
1696
+ .chai-result-card__close {
1697
+ position: absolute;
1698
+ z-index: 1;
1699
+ top: 12px;
1700
+ right: 12px;
1701
+ width: 24px;
1702
+ height: 24px;
1703
+ padding: 0;
1704
+ border: none;
1705
+ border-radius: 50%;
1706
+ background: transparent;
1707
+ color: var(--chai-color-semantic-control-text-muted);
1708
+ display: flex;
1709
+ align-items: center;
1710
+ justify-content: center;
1711
+ cursor: pointer;
1712
+ }
1713
+
1714
+ .chai-result-card__close:hover {
1715
+ color: var(--chai-color-semantic-control-text-strong);
1716
+ background: var(--chai-color-semantic-control-surface-hover);
1717
+ }
1718
+
1719
+ .chai-result-card__close svg {
1720
+ width: 14px;
1721
+ height: 14px;
1722
+ }
1723
+
1724
+ .chai-result-card__prompt-box {
1725
+ position: relative;
1726
+ flex: 1;
1727
+ min-height: 0;
1728
+ margin-top: var(--chai-space-4);
1729
+ padding: var(--chai-space-3) var(--chai-space-4);
1730
+ border-radius: 14px;
1731
+ background: var(--chai-color-semantic-control-surface);
1732
+ overflow-y: auto;
1733
+ }
1734
+
1735
+ .chai-result-card__prompt-text {
1736
+ margin: 0;
1737
+ padding-right: 20px;
1738
+ font-size: var(--chai-type-size-body-medium);
1739
+ line-height: var(--chai-type-leading-body-medium);
1740
+ color: var(--chai-color-semantic-control-text-strong);
1741
+ user-select: text;
1742
+ white-space: pre-wrap;
1743
+ }
1744
+
1745
+ .chai-result-card__copy {
1746
+ position: absolute;
1747
+ top: 9px;
1748
+ right: 9px;
1749
+ width: 22px;
1750
+ height: 22px;
1751
+ padding: 0;
1752
+ border: none;
1753
+ border-radius: 50%;
1754
+ background: transparent;
1755
+ color: var(--chai-color-semantic-control-text-muted);
1756
+ display: flex;
1757
+ align-items: center;
1758
+ justify-content: center;
1759
+ cursor: pointer;
1760
+ }
1761
+
1762
+ .chai-result-card__copy:hover {
1763
+ color: var(--chai-color-semantic-control-text-strong);
1764
+ background: var(--chai-color-semantic-control-surface-hover);
1765
+ }
1766
+
1767
+ .chai-result-card__copy svg {
1768
+ width: 10px;
1769
+ height: 13px;
1770
+ }
1771
+
1772
+ .chai-result-card__chips {
1773
+ display: flex;
1774
+ flex-wrap: wrap;
1775
+ gap: var(--chai-space-2);
1776
+ flex: none;
1777
+ }
1778
+
1779
+ .chai-result-card__chip {
1780
+ padding: 6px 12px;
1781
+ border-radius: var(--chai-radius-sm);
1782
+ border: 1px solid var(--chai-color-semantic-control-border);
1783
+ font-size: var(--chai-type-size-label-large);
1784
+ color: var(--chai-color-semantic-control-text);
1785
+ white-space: nowrap;
1786
+ }
1787
+
1788
+ .chai-result-card__chip--evaluation {
1789
+ display: inline-flex;
1790
+ align-items: center;
1791
+ gap: 4px;
1792
+ }
1793
+
1794
+ .chai-result-card__chip--evaluation svg {
1795
+ width: 14px;
1796
+ height: 14px;
1797
+ }
1798
+
1799
+ /* Reference: the mock's checked, filled green "Evaluation" chip. */
1800
+ .chai-result-card__chip--passed {
1801
+ border-color: var(--chai-color-semantic-accent);
1802
+ color: var(--chai-color-semantic-accent-text);
1803
+ background: var(--chai-color-semantic-accent-subtle);
1804
+ }
1805
+
1806
+ /* The failed counterpart, in the danger tokens. Outlined, not filled:
1807
+ danger text on danger-subtle is 2.96:1, under WCAG AA's 4.5:1, while on
1808
+ the card surface it's 4.62:1. The cross icon carries the distinction the
1809
+ fill would have. */
1810
+ .chai-result-card__chip--failed {
1811
+ border-color: var(--chai-color-semantic-danger);
1812
+ color: var(--chai-color-semantic-danger);
1813
+ }
1814
+
1815
+ /* The cross glyph fills its whole viewBox where the check has padding —
1816
+ sized down so both icons read at the same visual weight. */
1817
+ .chai-result-card__chip--failed svg {
1818
+ width: 10px;
1819
+ height: 10px;
1820
+ margin: 0 2px;
1821
+ }
1822
+
1823
+ /* Announced, not shown — carries "passed"/"failed" for screen readers. */
1824
+ .chai-visually-hidden {
1825
+ position: absolute;
1826
+ width: 1px;
1827
+ height: 1px;
1828
+ margin: -1px;
1829
+ padding: 0;
1830
+ overflow: hidden;
1831
+ clip: rect(0 0 0 0);
1832
+ white-space: nowrap;
1833
+ border: 0;
1834
+ }
1835
+
1836
+ /* --- Edit card ------------------------------------------------------------
1837
+ The result card's frame (rounded outline, white action bar) around an
1838
+ image the person marks regions on. The viewport clips the image to the
1839
+ card's corners at every zoom; the layer inside it holds the image and its
1840
+ regions, so both move and scale together. */
1841
+
1842
+ .chai-edit-card {
1843
+ width: var(--chai-edit-card-width, 630px);
1844
+ max-width: 100%;
1845
+ display: flex;
1846
+ flex-direction: column;
1847
+ border-radius: 29px;
1848
+ border: 1px solid var(--chai-color-semantic-control-border-soft);
1849
+ background: var(--chai-color-semantic-control-surface-raised);
1850
+ overflow: hidden;
1851
+ font-family: var(--chai-type-family-material);
1852
+ }
1853
+
1854
+ .chai-edit-card__viewport {
1855
+ position: relative;
1856
+ overflow: hidden;
1857
+ background: var(--chai-color-semantic-control-surface);
1858
+ touch-action: none;
1859
+ user-select: none;
1860
+ }
1861
+
1862
+ .chai-edit-card__viewport--zoomed {
1863
+ cursor: grab;
1864
+ }
1865
+
1866
+ .chai-edit-card__viewport--zoomed:active {
1867
+ cursor: grabbing;
1868
+ }
1869
+
1870
+ .chai-edit-card__layer {
1871
+ position: relative;
1872
+ transform-origin: 0 0;
1873
+ }
1874
+
1875
+ .chai-edit-card__image {
1876
+ display: block;
1877
+ width: 100%;
1878
+ height: auto;
1879
+ pointer-events: none;
1880
+ }
1881
+
1882
+ .chai-edit-card__placeholder {
1883
+ min-height: 220px;
1884
+ }
1885
+
1886
+ /* Region boxes. Borders, badges and handles divide by the zoom so they stay
1887
+ the same size on screen while the image scales. A faint dark outline keeps
1888
+ each colored border visible on light and dark images alike. */
1889
+ .chai-edit-card__region {
1890
+ --chai-edit-line: calc(2px / var(--chai-edit-zoom, 1));
1891
+ position: absolute;
1892
+ box-sizing: border-box;
1893
+ border: var(--chai-edit-line) solid var(--chai-region-color);
1894
+ box-shadow: 0 0 0 calc(1px / var(--chai-edit-zoom, 1)) var(--chai-color-semantic-overlay);
1895
+ }
1896
+
1897
+ .chai-edit-card__region--selected,
1898
+ .chai-edit-card__region--draft {
1899
+ border-style: dashed;
1900
+ }
1901
+
1902
+ /* Regions a version was made with: shown for reference, without a number
1903
+ badge and not selectable, so they don't read as regions to send. */
1904
+ .chai-edit-card__region--applied {
1905
+ pointer-events: none;
1906
+ }
1907
+
1908
+ .chai-edit-card__region-body {
1909
+ position: absolute;
1910
+ inset: 0;
1911
+ width: 100%;
1912
+ height: 100%;
1913
+ padding: 0;
1914
+ border: none;
1915
+ background: transparent;
1916
+ cursor: move;
1917
+ }
1918
+
1919
+ .chai-edit-card__region-body:disabled {
1920
+ cursor: default;
1921
+ }
1922
+
1923
+ .chai-edit-card__badge {
1924
+ position: absolute;
1925
+ top: 0;
1926
+ left: 0;
1927
+ display: flex;
1928
+ align-items: center;
1929
+ justify-content: center;
1930
+ min-width: 16px;
1931
+ height: 16px;
1932
+ padding: 0 3px;
1933
+ box-sizing: border-box;
1934
+ border-radius: var(--chai-radius-full);
1935
+ background: var(--chai-region-color);
1936
+ color: var(--chai-color-semantic-text-on-accent);
1937
+ font-size: var(--chai-type-size-label-small);
1938
+ font-weight: var(--chai-type-weight-material-medium);
1939
+ line-height: 1;
1940
+ transform: translate(-50%, -50%) scale(calc(1 / var(--chai-edit-zoom, 1)));
1941
+ pointer-events: none;
1942
+ }
1943
+
1944
+ .chai-edit-card__handle {
1945
+ position: absolute;
1946
+ width: 12px;
1947
+ height: 12px;
1948
+ box-sizing: border-box;
1949
+ border: 1px solid var(--chai-region-color);
1950
+ background: var(--chai-color-semantic-on-overlay);
1951
+ transform: translate(-50%, -50%) scale(calc(1 / var(--chai-edit-zoom, 1)));
1952
+ touch-action: none;
1953
+ }
1954
+
1955
+ .chai-edit-card__handle--nw { top: 0; left: 0; cursor: nwse-resize; }
1956
+ .chai-edit-card__handle--ne { top: 0; left: 100%; cursor: nesw-resize; }
1957
+ .chai-edit-card__handle--sw { top: 100%; left: 0; cursor: nesw-resize; }
1958
+ .chai-edit-card__handle--se { top: 100%; left: 100%; cursor: nwse-resize; }
1959
+
1960
+ /* An edit in progress (or stopped, or failed) over the image it came from. */
1961
+ .chai-edit-card__status {
1962
+ position: absolute;
1963
+ inset: 0;
1964
+ z-index: 2;
1965
+ display: flex;
1966
+ align-items: center;
1967
+ justify-content: center;
1968
+ padding: var(--chai-space-5);
1969
+ background: var(--chai-color-semantic-overlay);
1970
+ }
1971
+
1972
+ .chai-edit-card__spinner {
1973
+ width: 32px;
1974
+ height: 32px;
1975
+ border-radius: 50%;
1976
+ border: 3px solid var(--chai-color-semantic-control-border-faint);
1977
+ border-top-color: var(--chai-color-semantic-accent);
1978
+ animation: chai-spin var(--chai-motion-duration-steep) linear infinite;
1979
+ }
1980
+
1981
+ .chai-edit-card__status-text {
1982
+ margin: 0;
1983
+ text-align: center;
1984
+ font-size: var(--chai-type-size-body-medium);
1985
+ color: var(--chai-color-semantic-on-overlay);
1986
+ }
1987
+
1988
+ .chai-edit-card__zoom-level {
1989
+ min-width: 34px;
1990
+ text-align: center;
1991
+ font-size: var(--chai-type-size-label-small);
1992
+ font-weight: var(--chai-type-weight-material-medium);
1993
+ letter-spacing: var(--chai-type-tracking-label-small);
1994
+ color: var(--chai-color-semantic-on-overlay);
1995
+ }
1996
+
1997
+ /* The region's instruction field. A fixed width, so the text wraps after a
1998
+ few words instead of running past the card's edge. */
1999
+ .chai-edit-card__popover {
2000
+ position: absolute;
2001
+ z-index: 4;
2002
+ width: 220px;
2003
+ box-sizing: border-box;
2004
+ display: flex;
2005
+ flex-direction: column;
2006
+ gap: 6px;
2007
+ padding: 6px 8px 8px;
2008
+ border-radius: var(--chai-radius-sm);
2009
+ background: var(--chai-color-semantic-scrim);
2010
+ cursor: default;
2011
+ }
2012
+
2013
+ .chai-edit-card__popover--above {
2014
+ transform: translateY(-100%);
2015
+ }
2016
+
2017
+ .chai-edit-card__popover-header {
2018
+ display: flex;
2019
+ align-items: center;
2020
+ gap: 2px;
2021
+ }
2022
+
2023
+ .chai-edit-card__popover-title {
2024
+ flex: 1;
2025
+ font-size: var(--chai-type-size-label-small);
2026
+ font-weight: var(--chai-type-weight-material-medium);
2027
+ letter-spacing: var(--chai-type-tracking-label-small);
2028
+ color: var(--chai-color-semantic-on-overlay);
2029
+ }
2030
+
2031
+ .chai-edit-card__field {
2032
+ width: 100%;
2033
+ box-sizing: border-box;
2034
+ max-height: 120px;
2035
+ padding: 4px 8px;
2036
+ border: 1px solid var(--chai-color-semantic-on-overlay);
2037
+ border-radius: var(--chai-radius-sm);
2038
+ background: transparent;
2039
+ color: var(--chai-color-semantic-on-overlay);
2040
+ font-family: inherit;
2041
+ font-size: var(--chai-type-size-label-small);
2042
+ line-height: var(--chai-type-leading-label-small);
2043
+ resize: none;
2044
+ overflow-y: auto;
2045
+ }
2046
+
2047
+ .chai-edit-card__field::placeholder {
2048
+ color: var(--chai-color-semantic-on-overlay);
2049
+ opacity: 0.75;
2050
+ }
2051
+
2052
+ @media (prefers-reduced-motion: reduce) {
2053
+ .chai-edit-card__spinner {
2054
+ animation-duration: calc(var(--chai-motion-duration-steep) * 3);
2055
+ }
2056
+ }
2057
+
2058
+ /* --- Floating card controls -------------------------------------------------
2059
+ Shared by the image cards (result card, edit card), whose media fills the
2060
+ card. Controls along the top sit in dark translucent groups; controls
2061
+ along the bottom sit on light translucent surfaces: actions on the left,
2062
+ one round button on the right, and the page dots centered above them.
2063
+ Each reads on any image without sampling its colors. */
2064
+
2065
+ .chai-card-top {
2066
+ position: absolute;
2067
+ z-index: 3;
2068
+ top: 16px;
2069
+ display: flex;
2070
+ align-items: center;
2071
+ gap: 2px;
2072
+ padding: 2px;
2073
+ border-radius: var(--chai-radius-sm);
2074
+ background: var(--chai-color-semantic-overlay);
2075
+ }
2076
+
2077
+ .chai-card-top--start {
2078
+ left: 16px;
2079
+ }
2080
+
2081
+ .chai-card-top--end {
2082
+ right: 16px;
2083
+ }
2084
+
2085
+ .chai-card-top-btn {
2086
+ width: 24px;
2087
+ height: 24px;
2088
+ padding: 0;
2089
+ border: none;
2090
+ border-radius: var(--chai-radius-sm);
2091
+ background: transparent;
2092
+ color: var(--chai-color-semantic-on-overlay);
2093
+ display: flex;
2094
+ align-items: center;
2095
+ justify-content: center;
2096
+ cursor: pointer;
2097
+ }
2098
+
2099
+ .chai-card-top-btn svg {
2100
+ width: 18px;
2101
+ height: 18px;
2102
+ }
2103
+
2104
+ .chai-card-top-btn:hover:not(:disabled) {
2105
+ background: var(--chai-color-semantic-overlay);
2106
+ }
2107
+
2108
+ .chai-card-top-btn:disabled {
2109
+ opacity: 0.4;
2110
+ cursor: not-allowed;
2111
+ }
2112
+
2113
+ /* The bottom: the actions, floating over the media. */
2114
+ .chai-card-bottom {
2115
+ position: absolute;
2116
+ z-index: 3;
2117
+ left: 16px;
2118
+ right: 16px;
2119
+ bottom: 12px;
2120
+ display: flex;
2121
+ flex-direction: column;
2122
+ align-items: center;
2123
+ gap: var(--chai-space-2);
2124
+ }
2125
+
2126
+ /* --- Pager bar ---
2127
+ * With several pages (results, or edit versions), the page dots are the
2128
+ * card's last row, centred, in a bar that makes the card that much taller,
2129
+ * with an optional label on the left (the result card's model). Under an
2130
+ * image or video the bar is dark and the media fades into it, so the label
2131
+ * and dots read over any picture. */
2132
+ .chai-result-card,
2133
+ .chai-edit-card {
2134
+ --chai-card-pager-bar: 40px;
2135
+ }
2136
+
2137
+ .chai-card-pager-bar {
2138
+ flex: none;
2139
+ display: grid;
2140
+ grid-template-columns: 1fr auto 1fr;
2141
+ align-items: center;
2142
+ height: var(--chai-card-pager-bar);
2143
+ background: var(--chai-color-semantic-control-surface);
2144
+ }
2145
+
2146
+ .chai-card-pager-bar > .chai-pagination {
2147
+ grid-column: 2;
2148
+ }
2149
+
2150
+ .chai-card-pager-bar--over-media {
2151
+ background: var(--chai-color-semantic-media-backdrop);
2152
+ }
2153
+
2154
+ /* An MD3 assist chip with its fill and outline off, in Label Small. */
2155
+ .chai-card-pager-label {
2156
+ justify-self: start;
2157
+ min-width: 0;
2158
+ max-width: 100%;
2159
+ display: inline-flex;
2160
+ align-items: center;
2161
+ height: 32px;
2162
+ padding: 0 var(--chai-space-4);
2163
+ box-sizing: border-box;
2164
+ color: var(--chai-color-semantic-control-text-muted);
2165
+ font-size: var(--chai-type-size-label-small);
2166
+ font-weight: var(--chai-type-weight-material-medium);
2167
+ line-height: var(--chai-type-leading-label-small);
2168
+ letter-spacing: var(--chai-type-tracking-label-small);
2169
+ white-space: nowrap;
2170
+ overflow: hidden;
2171
+ text-overflow: ellipsis;
2172
+ }
2173
+
2174
+ .chai-card-pager-bar--over-media .chai-card-pager-label {
2175
+ color: var(--chai-color-semantic-on-media-fade);
2176
+ }
2177
+
2178
+ /* The fade at the bottom of the media, into the dark bar. At most half the
2179
+ media, so on short media it never reaches the buttons along the top. */
2180
+ .chai-card-fade {
2181
+ position: absolute;
2182
+ z-index: 2;
2183
+ left: 0;
2184
+ right: 0;
2185
+ bottom: 0;
2186
+ height: min(120px, 50%);
2187
+ background: var(--chai-gradient-media-fade);
2188
+ pointer-events: none;
2189
+ }
2190
+
2191
+ .chai-card-actions {
2192
+ align-self: stretch;
2193
+ display: flex;
2194
+ align-items: center;
2195
+ justify-content: space-between;
2196
+ gap: var(--chai-space-2);
2197
+ }
2198
+
2199
+ .chai-card-chips {
2200
+ display: flex;
2201
+ flex-wrap: wrap;
2202
+ gap: var(--chai-space-2);
2203
+ }
2204
+
2205
+ /* A labeled action (the edit card's region chips). */
2206
+ .chai-card-chip {
2207
+ display: inline-flex;
2208
+ align-items: center;
2209
+ gap: var(--chai-space-2);
2210
+ height: 32px;
2211
+ padding: 0 12px 0 8px;
2212
+ border: none;
2213
+ border-radius: var(--chai-radius-sm);
2214
+ background: var(--chai-color-semantic-overlay-light);
2215
+ color: var(--chai-color-semantic-control-text-strong);
2216
+ font-family: inherit;
2217
+ font-size: var(--chai-type-size-label-large);
2218
+ font-weight: var(--chai-type-weight-material-medium);
2219
+ letter-spacing: var(--chai-type-tracking-label-large);
2220
+ white-space: nowrap;
2221
+ cursor: pointer;
2222
+ }
2223
+
2224
+ /* Icon-only actions in one light pill (the result card's votes and retry). */
2225
+ .chai-card-pill {
2226
+ display: flex;
2227
+ align-items: center;
2228
+ height: 32px;
2229
+ padding: 0 2px;
2230
+ border-radius: var(--chai-radius-full);
2231
+ background: var(--chai-color-semantic-overlay-light);
2232
+ }
2233
+
2234
+ .chai-card-pill-btn,
2235
+ .chai-card-round {
2236
+ flex: none;
2237
+ width: 32px;
2238
+ height: 32px;
2239
+ padding: 0;
2240
+ border: none;
2241
+ border-radius: 50%;
2242
+ background: transparent;
2243
+ color: var(--chai-color-semantic-control-text-strong);
2244
+ display: flex;
2245
+ align-items: center;
2246
+ justify-content: center;
2247
+ cursor: pointer;
2248
+ }
2249
+
2250
+ /* The one round button on the right (show/hide regions, details). */
2251
+ .chai-card-round {
2252
+ background: var(--chai-color-semantic-overlay-light);
2253
+ }
2254
+
2255
+ .chai-card-chip svg,
2256
+ .chai-card-pill-btn svg,
2257
+ .chai-card-round svg {
2258
+ width: 18px;
2259
+ height: 18px;
2260
+ }
2261
+
2262
+ .chai-card-chip:hover:not(:disabled),
2263
+ .chai-card-pill-btn:hover:not(:disabled),
2264
+ .chai-card-round:hover:not(:disabled) {
2265
+ background: var(--chai-color-semantic-control-surface-raised);
2266
+ }
2267
+
2268
+ /* A vote that's on: green, on a solid surface. */
2269
+ .chai-card-pill-btn--active {
2270
+ color: var(--chai-color-semantic-accent-text);
2271
+ background: var(--chai-color-semantic-control-surface-raised);
2272
+ }
2273
+
2274
+ .chai-card-chip:disabled,
2275
+ .chai-card-pill-btn:disabled,
2276
+ .chai-card-round:disabled {
2277
+ color: var(--chai-color-semantic-control-text-muted);
2278
+ cursor: not-allowed;
2279
+ }
2280
+
2281
+ /* --- Page slide ------------------------------------------------------------
2282
+ Shared by the paginated cards: the new page slides in from the side it was
2283
+ paged from while the last image slides out the other way. Skipped (in
2284
+ page-slide.tsx) for people who ask for reduced motion. */
2285
+ .chai-slide-in--left {
2286
+ animation: chai-slide-in-left var(--chai-motion-duration-slow) var(--chai-motion-easing-standard);
2287
+ }
2288
+
2289
+ .chai-slide-in--right {
2290
+ animation: chai-slide-in-right var(--chai-motion-duration-slow) var(--chai-motion-easing-standard);
2291
+ }
2292
+
2293
+ /* The outgoing image stays in the layout, holding the card's height while
2294
+ the new page (laid over it) loads. */
2295
+ .chai-slide-out {
2296
+ display: block;
2297
+ width: 100%;
2298
+ height: auto;
2299
+ pointer-events: none;
2300
+ }
2301
+
2302
+ .chai-slide-in--over {
2303
+ position: absolute;
2304
+ inset: 0;
2305
+ }
2306
+
2307
+ .chai-slide-out--left {
2308
+ animation: chai-slide-out-left var(--chai-motion-duration-slow) var(--chai-motion-easing-standard) forwards;
2309
+ }
2310
+
2311
+ .chai-slide-out--right {
2312
+ animation: chai-slide-out-right var(--chai-motion-duration-slow) var(--chai-motion-easing-standard) forwards;
2313
+ }
2314
+
2315
+ @keyframes chai-slide-in-left {
2316
+ from { transform: translateX(100%); }
2317
+ to { transform: translateX(0); }
2318
+ }
2319
+
2320
+ @keyframes chai-slide-in-right {
2321
+ from { transform: translateX(-100%); }
2322
+ to { transform: translateX(0); }
2323
+ }
2324
+
2325
+ @keyframes chai-slide-out-left {
2326
+ from { transform: translateX(0); }
2327
+ to { transform: translateX(-100%); }
2328
+ }
2329
+
2330
+ @keyframes chai-slide-out-right {
2331
+ from { transform: translateX(0); }
2332
+ to { transform: translateX(100%); }
2333
+ }
2334
+
2335
+ /* --- Focus rings ------------------------------------------------------------
2336
+ On by default: every CHAI control shows a visible keyboard-focus ring,
2337
+ which WCAG 2.4.7 Focus Visible requires. Buttons and menu items
2338
+ keep the browser's own ring; MD3 elements (the submit FAB, the switch)
2339
+ draw `<md-focus-ring>`. The Composer's textarea is the one control that
2340
+ had none — its ring goes on the whole pill, matching MD3's ring width
2341
+ and color so every ring in a component reads as the same system. */
2342
+ .chai-composer:has(.chai-composer__input:focus-visible) {
2343
+ outline: 3px solid var(--chai-color-semantic-accent);
2344
+ outline-offset: 2px;
2345
+ }
2346
+
2347
+ /* Off switch: `data-chai-focus-ring="hidden"` on any ancestor hides every
2348
+ CHAI focus ring inside it — for a showcase or preview surface, not a
2349
+ production app. Hiding them removes the only indicator keyboard users
2350
+ have of where they are, failing WCAG 2.4.7 Focus Visible.
2351
+ Scoped to CHAI's own roots so a builder's other controls under the same
2352
+ ancestor keep theirs. */
2353
+ [data-chai-focus-ring="hidden"] :is(
2354
+ .chai-composer,
2355
+ .chai-media-analyzer,
2356
+ .chai-result-card,
2357
+ .chai-result-card__expand-overlay,
2358
+ .chai-edit-card,
2359
+ .chai-search-menu,
2360
+ .chai-pagination,
2361
+ .chai-switch
2362
+ ) {
2363
+ --md-focus-ring-width: 0px;
2364
+ --md-focus-ring-active-width: 0px;
2365
+ }
2366
+
2367
+ [data-chai-focus-ring="hidden"] .chai-composer:has(.chai-composer__input:focus-visible),
2368
+ [data-chai-focus-ring="hidden"] :is(
2369
+ .chai-composer,
2370
+ .chai-media-analyzer,
2371
+ .chai-result-card,
2372
+ .chai-result-card__expand-overlay,
2373
+ .chai-edit-card,
2374
+ .chai-search-menu,
2375
+ .chai-pagination
2376
+ ) :focus-visible {
2377
+ outline: none;
2378
+ }