flawed-avatar 0.2.1 → 0.2.2

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/assets/icon.png +0 -0
  4. package/dist/chat-renderer-bundle/chat-index.html +1 -1
  5. package/dist/main/main/device-identity.d.ts +19 -0
  6. package/dist/main/main/device-identity.js +83 -0
  7. package/dist/main/main/gateway-client.d.ts +2 -1
  8. package/dist/main/main/gateway-client.js +50 -12
  9. package/dist/main/main/main.js +5 -7
  10. package/dist/main/main/persistence/types.d.ts +2 -2
  11. package/dist/renderer-bundle/renderer.js +35 -46
  12. package/dist/settings-preload.cjs +153 -0
  13. package/dist/settings-renderer-bundle/settings-index.html +16 -0
  14. package/dist/settings-renderer-bundle/settings-renderer.js +502 -0
  15. package/dist/settings-renderer-bundle/styles/base.css +106 -0
  16. package/dist/settings-renderer-bundle/styles/chat.css +516 -0
  17. package/dist/settings-renderer-bundle/styles/components/button.css +221 -0
  18. package/dist/settings-renderer-bundle/styles/components/indicator.css +216 -0
  19. package/dist/settings-renderer-bundle/styles/components/input.css +139 -0
  20. package/dist/settings-renderer-bundle/styles/components/toast.css +204 -0
  21. package/dist/settings-renderer-bundle/styles/controls.css +279 -0
  22. package/dist/settings-renderer-bundle/styles/settings.css +310 -0
  23. package/dist/settings-renderer-bundle/styles/tokens.css +220 -0
  24. package/dist/settings-renderer-bundle/styles/utilities.css +349 -0
  25. package/index.ts +2 -2
  26. package/package.json +6 -1
  27. package/src/main/device-identity.ts +103 -0
  28. package/src/main/gateway-client.ts +52 -11
  29. package/src/main/main.ts +5 -6
  30. package/src/renderer/audio/index.ts +0 -3
  31. package/src/renderer/audio/kokoro-model-loader.ts +0 -2
  32. package/src/renderer/audio/kokoro-tts-service.ts +0 -2
  33. package/src/renderer/audio/tts-controller.ts +0 -3
  34. package/src/renderer/avatar/ibl-enhancer.ts +1 -1
  35. package/src/renderer/chat-window/chat-index.html +1 -1
  36. package/src/renderer/renderer.ts +0 -1
  37. package/src/renderer/settings-window/settings-index.html +1 -1
  38. package/src/renderer/ui/chat-bubble.ts +0 -39
  39. package/src/service.ts +1 -1
  40. package/src/renderer/audio/tts-service.ts +0 -16
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Chat Component Styles
3
+ * Uses BEM naming convention and imports component styles
4
+ */
5
+
6
+ @import "./tokens.css";
7
+ @import "./base.css";
8
+ @import "./components/button.css";
9
+ @import "./components/input.css";
10
+ @import "./components/indicator.css";
11
+ @import "./components/toast.css";
12
+
13
+ /* === Backdrop fallback === */
14
+ @supports not (backdrop-filter: blur(1px)) {
15
+ .chat__body {
16
+ background: var(--glass-bg-solid) !important;
17
+ }
18
+ }
19
+
20
+ /* === Chat Container === */
21
+ .chat {
22
+ width: 100%;
23
+ height: 100%;
24
+ display: flex;
25
+ flex-direction: column;
26
+ box-sizing: border-box;
27
+ min-height: 0;
28
+ overflow: hidden;
29
+ }
30
+
31
+ /* Legacy ID support - maps to BEM class */
32
+ #chat-container {
33
+ width: 100%;
34
+ height: 100%;
35
+ display: flex;
36
+ flex-direction: column;
37
+ box-sizing: border-box;
38
+ min-height: 0;
39
+ overflow: hidden;
40
+ }
41
+
42
+ /* === Chat Body (glass panel) === */
43
+ .chat__body {
44
+ flex: 1;
45
+ display: flex;
46
+ flex-direction: column;
47
+ background:
48
+ linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, transparent 40%),
49
+ var(--glass-bg);
50
+ backdrop-filter: blur(16px);
51
+ -webkit-backdrop-filter: blur(16px);
52
+ border: 1px solid var(--glass-border);
53
+ border-radius: var(--radius-xl);
54
+ box-shadow:
55
+ var(--shadow-lg),
56
+ inset 0 1px 0 rgba(255, 255, 255, 0.06);
57
+ min-height: 0;
58
+ overflow: hidden;
59
+ }
60
+
61
+ /* Legacy ID support */
62
+ #chat-body {
63
+ flex: 1;
64
+ display: flex;
65
+ flex-direction: column;
66
+ background:
67
+ linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, transparent 40%),
68
+ var(--glass-bg);
69
+ backdrop-filter: blur(16px);
70
+ -webkit-backdrop-filter: blur(16px);
71
+ border: 1px solid var(--glass-border);
72
+ border-radius: var(--radius-xl);
73
+ box-shadow:
74
+ var(--shadow-lg),
75
+ inset 0 1px 0 rgba(255, 255, 255, 0.06);
76
+ min-height: 0;
77
+ overflow: hidden;
78
+ }
79
+
80
+ /* === Chat Tail (speech bubble pointer) === */
81
+ .chat__tail {
82
+ width: var(--chat-tail-width);
83
+ height: var(--chat-tail-height);
84
+ margin: -1px auto 0;
85
+ background: var(--glass-bg);
86
+ clip-path: polygon(0 0, 100% 0, 50% 100%);
87
+ }
88
+
89
+ /* Legacy ID support */
90
+ #chat-tail {
91
+ width: var(--chat-tail-width);
92
+ height: var(--chat-tail-height);
93
+ margin: -1px auto 0;
94
+ background: var(--glass-bg);
95
+ clip-path: polygon(0 0, 100% 0, 50% 100%);
96
+ }
97
+
98
+ /* === Chat Bubble Container === */
99
+ .chat__bubble {
100
+ flex: 1;
101
+ display: flex;
102
+ flex-direction: column;
103
+ min-height: 0;
104
+ overflow: hidden;
105
+ opacity: 1;
106
+ pointer-events: auto;
107
+ transition: opacity var(--transition-slow);
108
+ }
109
+
110
+ /* Legacy ID support */
111
+ #chat-bubble {
112
+ flex: 1;
113
+ display: flex;
114
+ flex-direction: column;
115
+ min-height: 0;
116
+ overflow: hidden;
117
+ opacity: 1;
118
+ pointer-events: auto;
119
+ transition: opacity var(--transition-slow);
120
+ }
121
+
122
+ /* === Messages Area === */
123
+ .chat__messages {
124
+ flex: 1;
125
+ overflow-y: auto;
126
+ overflow-x: hidden;
127
+ padding: var(--space-2);
128
+ font-family: var(--font-sans);
129
+ font-size: var(--font-size-md);
130
+ line-height: var(--line-height);
131
+ color: var(--text-primary);
132
+ scroll-behavior: smooth;
133
+ contain: strict;
134
+ content-visibility: auto;
135
+ contain-intrinsic-size: 0 500px;
136
+ }
137
+
138
+ /* Legacy ID support */
139
+ #chat-messages {
140
+ flex: 1;
141
+ overflow-y: auto;
142
+ overflow-x: hidden;
143
+ padding: var(--space-2);
144
+ font-family: var(--font-sans);
145
+ font-size: var(--font-size-md);
146
+ line-height: var(--line-height);
147
+ color: var(--text-primary);
148
+ scroll-behavior: smooth;
149
+ contain: strict;
150
+ content-visibility: auto;
151
+ contain-intrinsic-size: 0 500px;
152
+ }
153
+
154
+ /* === Message Bubbles === */
155
+ .message {
156
+ padding: var(--space-3) var(--space-4);
157
+ margin-bottom: var(--space-2);
158
+ word-wrap: break-word;
159
+ animation: message-appear var(--duration-slower) var(--ease-out-expo);
160
+ box-shadow: var(--shadow-xs);
161
+ contain: content;
162
+ will-change: transform, opacity;
163
+ }
164
+
165
+ @keyframes message-appear {
166
+ from {
167
+ opacity: 0;
168
+ transform: translateY(8px) scale(0.98);
169
+ }
170
+ to {
171
+ opacity: 1;
172
+ transform: translateY(0) scale(1);
173
+ }
174
+ }
175
+
176
+ /* Assistant message */
177
+ .message--assistant {
178
+ background: linear-gradient(
179
+ 135deg,
180
+ var(--surface-message-assistant) 0%,
181
+ rgba(255, 255, 255, 0.02) 100%
182
+ );
183
+ border: 1px solid rgba(255, 255, 255, 0.05);
184
+ border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm);
185
+ margin-inline-end: var(--chat-message-margin);
186
+ color: var(--text-assistant);
187
+ }
188
+
189
+ /* Legacy class support */
190
+ .chat-assistant-msg {
191
+ background: linear-gradient(
192
+ 135deg,
193
+ var(--surface-message-assistant) 0%,
194
+ rgba(255, 255, 255, 0.02) 100%
195
+ );
196
+ border: 1px solid rgba(255, 255, 255, 0.05);
197
+ border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm);
198
+ margin-inline-end: var(--chat-message-margin);
199
+ color: var(--text-assistant);
200
+ padding: var(--space-3) var(--space-4);
201
+ margin-bottom: var(--space-2);
202
+ word-wrap: break-word;
203
+ animation: message-appear var(--duration-slower) var(--ease-out-expo);
204
+ box-shadow: var(--shadow-xs);
205
+ }
206
+
207
+ /* User message */
208
+ .message--user {
209
+ background: linear-gradient(
210
+ 135deg,
211
+ var(--surface-message-user) 0%,
212
+ rgba(100, 180, 255, 0.08) 100%
213
+ );
214
+ border: 1px solid rgba(100, 180, 255, 0.12);
215
+ border-radius: var(--radius-lg) var(--radius-lg) var(--radius-sm) var(--radius-lg);
216
+ margin-inline-start: var(--chat-message-margin);
217
+ color: var(--text-user);
218
+ text-align: end;
219
+ }
220
+
221
+ /* Legacy class support */
222
+ .chat-user-msg {
223
+ background: linear-gradient(
224
+ 135deg,
225
+ var(--surface-message-user) 0%,
226
+ rgba(100, 180, 255, 0.08) 100%
227
+ );
228
+ border: 1px solid rgba(100, 180, 255, 0.12);
229
+ border-radius: var(--radius-lg) var(--radius-lg) var(--radius-sm) var(--radius-lg);
230
+ margin-inline-start: var(--chat-message-margin);
231
+ color: var(--text-user);
232
+ text-align: end;
233
+ padding: var(--space-3) var(--space-4);
234
+ margin-bottom: var(--space-2);
235
+ word-wrap: break-word;
236
+ animation: message-appear var(--duration-slower) var(--ease-out-expo);
237
+ box-shadow: var(--shadow-xs);
238
+ }
239
+
240
+ /* === Empty State === */
241
+ .chat-empty {
242
+ display: flex;
243
+ flex-direction: column;
244
+ align-items: center;
245
+ justify-content: center;
246
+ padding: var(--space-6);
247
+ text-align: center;
248
+ color: var(--text-secondary);
249
+ gap: var(--space-2);
250
+ flex: 1;
251
+ }
252
+
253
+ .chat-empty__icon {
254
+ width: 48px;
255
+ height: 48px;
256
+ opacity: 0.5;
257
+ font-size: 32px;
258
+ }
259
+
260
+ .chat-empty__title {
261
+ font-size: var(--font-size-md);
262
+ font-weight: var(--font-weight-medium);
263
+ }
264
+
265
+ .chat-empty__message {
266
+ font-size: var(--font-size-sm);
267
+ max-width: 200px;
268
+ line-height: var(--line-height-relaxed);
269
+ }
270
+
271
+ /* === Error State === */
272
+ .chat-error {
273
+ display: flex;
274
+ align-items: center;
275
+ gap: var(--space-2);
276
+ padding: var(--space-3);
277
+ background: var(--color-error-soft);
278
+ border: 1px solid var(--color-error);
279
+ border-radius: var(--radius-md);
280
+ color: var(--color-error);
281
+ font-size: var(--font-size-sm);
282
+ margin: var(--space-2);
283
+ }
284
+
285
+ .chat-error__icon {
286
+ width: 16px;
287
+ height: 16px;
288
+ flex-shrink: 0;
289
+ }
290
+
291
+ .chat-error__message {
292
+ flex: 1;
293
+ }
294
+
295
+ .chat-error__retry {
296
+ padding: var(--space-1) var(--space-2);
297
+ background: transparent;
298
+ border: 1px solid var(--color-error);
299
+ border-radius: var(--radius-sm);
300
+ color: var(--color-error);
301
+ font-size: var(--font-size-xs);
302
+ cursor: pointer;
303
+ transition:
304
+ background var(--duration-fast),
305
+ color var(--duration-fast);
306
+ }
307
+
308
+ .chat-error__retry:hover {
309
+ background: var(--color-error);
310
+ color: var(--text-primary);
311
+ }
312
+
313
+ /* === Loading State === */
314
+ .chat-loading {
315
+ display: flex;
316
+ flex-direction: column;
317
+ gap: var(--space-2);
318
+ padding: var(--space-2);
319
+ }
320
+
321
+ /* === Input Area === */
322
+ .chat__input-row {
323
+ display: flex;
324
+ align-items: center;
325
+ gap: var(--space-sm);
326
+ padding: var(--space-2);
327
+ background: var(--surface-sunken);
328
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
329
+ }
330
+
331
+ /* Legacy ID support */
332
+ #chat-input-row {
333
+ display: flex;
334
+ align-items: center;
335
+ gap: var(--space-sm);
336
+ padding: var(--space-2);
337
+ background: var(--surface-sunken);
338
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
339
+ }
340
+
341
+ /* Chat input - extends .input */
342
+ .chat__input {
343
+ flex: 1;
344
+ }
345
+
346
+ /* Legacy ID support */
347
+ #chat-input {
348
+ flex: 1;
349
+ background: var(--surface-input);
350
+ border: 1px solid var(--glass-border);
351
+ border-radius: var(--radius-full);
352
+ padding: var(--space-2) var(--space-xl);
353
+ color: var(--text-primary);
354
+ font-family: var(--font-sans);
355
+ font-size: var(--font-size-md);
356
+ outline: none;
357
+ transition:
358
+ border-color var(--transition-normal),
359
+ box-shadow var(--transition-normal);
360
+ }
361
+
362
+ #chat-input:focus {
363
+ border-color: rgba(100, 180, 255, 0.5);
364
+ box-shadow: 0 0 0 2px rgba(100, 180, 255, 0.15);
365
+ }
366
+
367
+ #chat-input::placeholder {
368
+ color: var(--text-secondary);
369
+ opacity: 0.6;
370
+ }
371
+
372
+ /* Send button - uses .btn .btn--icon */
373
+ .chat__send {
374
+ /* Additional chat-specific styles if needed */
375
+ }
376
+
377
+ /* Legacy ID support */
378
+ #send-btn {
379
+ width: 32px;
380
+ height: 32px;
381
+ flex-shrink: 0;
382
+ display: flex;
383
+ align-items: center;
384
+ justify-content: center;
385
+ border: none;
386
+ border-radius: 50%;
387
+ background: var(--accent-blue-soft);
388
+ color: var(--accent-blue);
389
+ cursor: pointer;
390
+ transition:
391
+ background var(--transition-normal),
392
+ color var(--transition-normal),
393
+ transform var(--transition-fast),
394
+ opacity var(--transition-normal);
395
+ }
396
+
397
+ #send-btn:hover:not(:disabled) {
398
+ background: rgba(100, 180, 255, 0.35);
399
+ color: var(--text-primary);
400
+ transform: scale(1.05);
401
+ }
402
+
403
+ #send-btn:active:not(:disabled) {
404
+ transform: scale(0.95);
405
+ }
406
+
407
+ #send-btn:disabled {
408
+ opacity: 0.3;
409
+ cursor: not-allowed;
410
+ }
411
+
412
+ #send-btn:focus-visible {
413
+ outline: 2px solid var(--accent-blue);
414
+ outline-offset: 2px;
415
+ }
416
+
417
+ #send-btn svg {
418
+ width: 14px;
419
+ height: 14px;
420
+ }
421
+
422
+ /* === Character Counter === */
423
+ .chat__char-counter {
424
+ font-size: var(--font-size-xs);
425
+ color: var(--text-secondary);
426
+ opacity: 0;
427
+ transition: opacity var(--transition-normal);
428
+ padding-inline-end: var(--space-1);
429
+ }
430
+
431
+ .chat__char-counter.is-visible {
432
+ opacity: 1;
433
+ }
434
+
435
+ .chat__char-counter--warning {
436
+ color: var(--accent-amber);
437
+ }
438
+
439
+ .chat__char-counter--error {
440
+ color: var(--color-error);
441
+ }
442
+
443
+ /* Legacy ID and class support */
444
+ #char-counter {
445
+ font-size: var(--font-size-xs);
446
+ color: var(--text-secondary);
447
+ opacity: 0;
448
+ transition: opacity var(--transition-normal);
449
+ padding-inline-end: var(--space-1);
450
+ }
451
+
452
+ #char-counter.visible {
453
+ opacity: 1;
454
+ }
455
+
456
+ #char-counter.warning {
457
+ color: var(--accent-amber);
458
+ }
459
+
460
+ #char-counter.error {
461
+ color: var(--color-error);
462
+ }
463
+
464
+ /* === Responsive Design === */
465
+ @media (max-width: 480px) {
466
+ .chat__body,
467
+ #chat-body {
468
+ border-radius: var(--radius-md);
469
+ }
470
+
471
+ .message,
472
+ .chat-assistant-msg,
473
+ .chat-user-msg {
474
+ padding: var(--space-2) var(--space-3);
475
+ font-size: var(--font-size-sm);
476
+ }
477
+
478
+ .message--assistant,
479
+ .chat-assistant-msg {
480
+ margin-inline-end: var(--space-3);
481
+ }
482
+
483
+ .message--user,
484
+ .chat-user-msg {
485
+ margin-inline-start: var(--space-3);
486
+ }
487
+
488
+ .chat__input,
489
+ #chat-input {
490
+ font-size: 16px; /* Prevent iOS zoom */
491
+ }
492
+ }
493
+
494
+ /* === Focus Indicators (Accessibility) === */
495
+ :focus {
496
+ outline: none;
497
+ }
498
+
499
+ :focus-visible {
500
+ outline: 2px solid var(--accent-blue);
501
+ outline-offset: 2px;
502
+ box-shadow: 0 0 0 4px var(--surface-focus);
503
+ }
504
+
505
+ /* === Forced Colors Mode === */
506
+ @media (forced-colors: active) {
507
+ .message,
508
+ .chat-assistant-msg,
509
+ .chat-user-msg {
510
+ border: 2px solid currentColor;
511
+ }
512
+
513
+ .chat-error {
514
+ border: 2px solid currentColor;
515
+ }
516
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Button Component
3
+ * Reusable button styles with variants
4
+ */
5
+
6
+ /* === Base Button === */
7
+ .btn {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ border: none;
12
+ cursor: pointer;
13
+ font-family: var(--font-sans);
14
+ font-size: var(--font-size-md);
15
+ transition:
16
+ background var(--duration-fast) var(--ease-out-expo),
17
+ color var(--duration-fast) var(--ease-out-expo),
18
+ transform var(--duration-normal) var(--ease-out-back),
19
+ box-shadow var(--duration-fast) var(--ease-out-expo),
20
+ opacity var(--duration-normal);
21
+ }
22
+
23
+ .btn:hover:not(:disabled) {
24
+ transform: translateY(-1px);
25
+ }
26
+
27
+ .btn:active:not(:disabled) {
28
+ transform: translateY(0) scale(0.96);
29
+ }
30
+
31
+ .btn:disabled {
32
+ opacity: 0.4;
33
+ cursor: not-allowed;
34
+ }
35
+
36
+ /* Focus visible */
37
+ .btn:focus {
38
+ outline: none;
39
+ }
40
+
41
+ .btn:focus-visible {
42
+ outline: 2px solid var(--accent-blue);
43
+ outline-offset: 2px;
44
+ box-shadow: 0 0 0 4px var(--surface-focus);
45
+ }
46
+
47
+ /* === Icon Button Variant === */
48
+ .btn--icon {
49
+ width: 32px;
50
+ height: 32px;
51
+ padding: 0;
52
+ border-radius: 50%;
53
+ background: var(--accent-blue-soft);
54
+ color: var(--accent-blue);
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ .btn--icon:hover:not(:disabled) {
59
+ background: rgba(100, 180, 255, 0.35);
60
+ color: var(--text-primary);
61
+ box-shadow: var(--glow-blue-sm);
62
+ transform: scale(1.05);
63
+ }
64
+
65
+ .btn--icon:active:not(:disabled) {
66
+ transform: scale(0.95);
67
+ }
68
+
69
+ .btn--icon svg {
70
+ width: 14px;
71
+ height: 14px;
72
+ }
73
+
74
+ /* === Ghost Button Variant === */
75
+ .btn--ghost {
76
+ width: 28px;
77
+ height: 28px;
78
+ padding: 0;
79
+ border-radius: var(--radius-md);
80
+ background: var(--surface-button);
81
+ backdrop-filter: blur(8px);
82
+ -webkit-backdrop-filter: blur(8px);
83
+ color: rgba(255, 255, 255, 0.7);
84
+ }
85
+
86
+ .btn--ghost:hover:not(:disabled) {
87
+ background: var(--surface-hover);
88
+ color: var(--text-primary);
89
+ box-shadow: var(--shadow-sm);
90
+ transform: scale(1.05);
91
+ }
92
+
93
+ .btn--ghost:active:not(:disabled) {
94
+ transform: scale(0.95);
95
+ }
96
+
97
+ .btn--ghost svg {
98
+ width: 14px;
99
+ height: 14px;
100
+ }
101
+
102
+ /* === Primary Button Variant === */
103
+ .btn--primary {
104
+ padding: var(--space-2) var(--space-4);
105
+ border-radius: var(--radius-md);
106
+ background: var(--accent-blue);
107
+ color: rgba(0, 0, 0, 0.9);
108
+ font-weight: var(--font-weight-medium);
109
+ }
110
+
111
+ .btn--primary:hover:not(:disabled) {
112
+ background: rgba(120, 190, 255, 1);
113
+ box-shadow: var(--glow-blue-sm);
114
+ }
115
+
116
+ /* === Secondary Button Variant === */
117
+ .btn--secondary {
118
+ padding: var(--space-2) var(--space-4);
119
+ border-radius: var(--radius-md);
120
+ background: var(--surface-button);
121
+ border: 1px solid var(--glass-border);
122
+ color: var(--text-primary);
123
+ }
124
+
125
+ .btn--secondary:hover:not(:disabled) {
126
+ background: var(--surface-hover);
127
+ border-color: var(--glass-border-hover);
128
+ }
129
+
130
+ /* === Size Variants === */
131
+ .btn--sm {
132
+ width: 24px;
133
+ height: 24px;
134
+ }
135
+
136
+ .btn--sm svg {
137
+ width: 12px;
138
+ height: 12px;
139
+ }
140
+
141
+ .btn--lg {
142
+ width: 40px;
143
+ height: 40px;
144
+ }
145
+
146
+ .btn--lg svg {
147
+ width: 18px;
148
+ height: 18px;
149
+ }
150
+
151
+ /* === State Modifiers === */
152
+ .btn--active,
153
+ .btn.is-active {
154
+ background: var(--accent-blue-soft);
155
+ color: var(--accent-blue);
156
+ box-shadow: var(--shadow-glow);
157
+ }
158
+
159
+ .btn--loading {
160
+ position: relative;
161
+ color: transparent;
162
+ pointer-events: none;
163
+ }
164
+
165
+ .btn--loading::after {
166
+ content: "";
167
+ position: absolute;
168
+ width: 14px;
169
+ height: 14px;
170
+ border: 2px solid currentColor;
171
+ border-right-color: transparent;
172
+ border-radius: 50%;
173
+ animation: btn-spin 0.6s linear infinite;
174
+ }
175
+
176
+ @keyframes btn-spin {
177
+ to {
178
+ transform: rotate(360deg);
179
+ }
180
+ }
181
+
182
+ /* === Icon visibility toggle === */
183
+ .btn .icon-on {
184
+ display: none;
185
+ }
186
+
187
+ .btn .icon-off {
188
+ display: block;
189
+ }
190
+
191
+ .btn.is-active .icon-on,
192
+ .btn--active .icon-on {
193
+ display: block;
194
+ }
195
+
196
+ .btn.is-active .icon-off,
197
+ .btn--active .icon-off {
198
+ display: none;
199
+ }
200
+
201
+ /* === Speaking animation === */
202
+ .btn--speaking {
203
+ animation: btn-pulse 1.5s ease-in-out infinite;
204
+ }
205
+
206
+ @keyframes btn-pulse {
207
+ 0%,
208
+ 100% {
209
+ box-shadow: var(--shadow-glow);
210
+ }
211
+ 50% {
212
+ box-shadow: var(--glow-blue-md);
213
+ }
214
+ }
215
+
216
+ /* === Forced colors mode === */
217
+ @media (forced-colors: active) {
218
+ .btn {
219
+ border: 2px solid currentColor;
220
+ }
221
+ }