@coventia/softphone 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/3x2-2HQTJDKZ.js +6583 -0
  2. package/dist/3x2-2HQTJDKZ.js.map +1 -0
  3. package/dist/3x2-INKWPNLA.js +6585 -0
  4. package/dist/3x2-INKWPNLA.js.map +1 -0
  5. package/dist/call-BTYlT2SU.d.cts +680 -0
  6. package/dist/call-BTYlT2SU.d.ts +680 -0
  7. package/dist/chunk-2ZSOGGPO.js +169 -0
  8. package/dist/chunk-2ZSOGGPO.js.map +1 -0
  9. package/dist/chunk-3V2L6UTK.js +129 -0
  10. package/dist/chunk-3V2L6UTK.js.map +1 -0
  11. package/dist/chunk-5BGEHMPP.js +945 -0
  12. package/dist/chunk-5BGEHMPP.js.map +1 -0
  13. package/dist/chunk-5WRI5ZAA.js +29 -0
  14. package/dist/chunk-5WRI5ZAA.js.map +1 -0
  15. package/dist/chunk-KAXEMJ7E.js +11 -0
  16. package/dist/chunk-KAXEMJ7E.js.map +1 -0
  17. package/dist/chunk-MHOJHG5E.js +39 -0
  18. package/dist/chunk-MHOJHG5E.js.map +1 -0
  19. package/dist/chunk-MP7CSA24.js +299 -0
  20. package/dist/chunk-MP7CSA24.js.map +1 -0
  21. package/dist/chunk-N2N2EQQJ.js +29 -0
  22. package/dist/chunk-N2N2EQQJ.js.map +1 -0
  23. package/dist/element/index.d.ts +853 -0
  24. package/dist/element/index.js +9635 -0
  25. package/dist/element/index.js.map +1 -0
  26. package/dist/engine-D9nbeY08.d.cts +115 -0
  27. package/dist/engine-Doa6Brkm.d.ts +115 -0
  28. package/dist/index.cjs +2133 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +62 -0
  31. package/dist/index.d.ts +62 -0
  32. package/dist/index.js +630 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/min-64GIMWWL.js +5456 -0
  35. package/dist/min-64GIMWWL.js.map +1 -0
  36. package/dist/min-ATXYR23R.js +5458 -0
  37. package/dist/min-ATXYR23R.js.map +1 -0
  38. package/dist/react/index.cjs +138 -0
  39. package/dist/react/index.cjs.map +1 -0
  40. package/dist/react/index.d.cts +68 -0
  41. package/dist/react/index.d.ts +68 -0
  42. package/dist/react/index.js +4 -0
  43. package/dist/react/index.js.map +1 -0
  44. package/dist/softphone-DHmhotbf.d.ts +128 -0
  45. package/dist/softphone-DhuhcKCy.d.cts +128 -0
  46. package/dist/softphone.css +1923 -0
  47. package/dist/sources-N4BOEDRL.js +226 -0
  48. package/dist/sources-N4BOEDRL.js.map +1 -0
  49. package/dist/testing/index.cjs +345 -0
  50. package/dist/testing/index.cjs.map +1 -0
  51. package/dist/testing/index.d.cts +66 -0
  52. package/dist/testing/index.d.ts +66 -0
  53. package/dist/testing/index.js +186 -0
  54. package/dist/testing/index.js.map +1 -0
  55. package/dist/ui/index.cjs +15428 -0
  56. package/dist/ui/index.cjs.map +1 -0
  57. package/dist/ui/index.d.cts +188 -0
  58. package/dist/ui/index.d.ts +188 -0
  59. package/dist/ui/index.js +2256 -0
  60. package/dist/ui/index.js.map +1 -0
  61. package/dist/zadarma/index.cjs +1144 -0
  62. package/dist/zadarma/index.cjs.map +1 -0
  63. package/dist/zadarma/index.d.cts +86 -0
  64. package/dist/zadarma/index.d.ts +86 -0
  65. package/dist/zadarma/index.js +5 -0
  66. package/dist/zadarma/index.js.map +1 -0
  67. package/dist/zadarma-engine-DN2SZSQ3.js +5 -0
  68. package/dist/zadarma-engine-DN2SZSQ3.js.map +1 -0
  69. package/package.json +123 -0
@@ -0,0 +1,1923 @@
1
+ /**
2
+ * @coventia/softphone/styles.css
3
+ *
4
+ * The theming contract (SPEC §2.6) + all component styles. A host restyles the softphone by
5
+ * overriding the `--cv-*` custom properties — which are INHERITED, so they cross the shadow boundary
6
+ * of `<coventia-softphone>` by design (the host's own CSS *selectors* do not, which is what stops a
7
+ * stray `button { width:100% }` from destroying the dialpad).
8
+ *
9
+ * Defaults match the Coventia product: light surface, emerald accent, soft rounded key cards.
10
+ *
11
+ * Reseller: coventia-softphone { --cv-accent:#7c3aed; --cv-bg:#0b0b0f; }
12
+ * CRM (shadcn bridge, ~15 lines, dark mode included):
13
+ * .cv-softphone { --cv-bg:hsl(var(--background)); --cv-text:hsl(var(--foreground)); … }
14
+ *
15
+ * TYPOGRAPHY NOTE: `--cv-font` is a system stack on purpose. An embedded widget should inherit its
16
+ * host's type, not force a webfont into someone else's app; hosts override the token if they want.
17
+ *
18
+ * Strings are Spanish-only (SPEC §2.4). No i18n.
19
+ */
20
+
21
+ /*
22
+ * The custom element's own box.
23
+ *
24
+ * A custom element is `display: inline` by default and has NO size — and the widget inside now fills
25
+ * its host rather than asserting 340×520, so without this the element would render to ZERO. The
26
+ * drop-in promise is "one tag and it works", which means the TAG must carry a box.
27
+ *
28
+ * These are defaults, not decisions: they sit on `:host`, so any host stylesheet outranks them —
29
+ *
30
+ * coventia-softphone { width: 100%; height: 100%; } ← a sidebar
31
+ * coventia-softphone { height: 600px; } ← taller
32
+ *
33
+ * React consumers never see this: they import `/ui` and their own wrapper states the box (the CRM's
34
+ * PhoneDialer does exactly that).
35
+ */
36
+ :host {
37
+ display: block;
38
+ width: 340px;
39
+ height: 520px;
40
+ }
41
+
42
+ /* React's container inside the shadow root — element path only; inert everywhere else.
43
+ *
44
+ * It must pass the host's height straight through. `.cv-softphone` below is `height: 100%`, and a
45
+ * percentage height resolves against the PARENT: leave this div at its default `auto` and that 100%
46
+ * silently becomes `auto` too, so the widget collapses to its content height and the host's `height`
47
+ * is ignored. What you see is a short card stranded in a taller box — which reads as a broken widget,
48
+ * not as a broken wrapper. */
49
+ .cv-mount {
50
+ height: 100%;
51
+ }
52
+
53
+ :host,
54
+ .cv-softphone {
55
+ /* Surface */
56
+ --cv-bg: #ffffff;
57
+ --cv-surface: #f9fafb;
58
+ --cv-surface-2: #f3f4f6;
59
+ --cv-border: #f3f4f6;
60
+ --cv-border-strong: #e5e7eb;
61
+
62
+ /* Text — three tiers, split by CONTRAST DUTY, not by taste:
63
+ `text` informational primary 17.7:1
64
+ `muted` informational secondary 4.8:1 (AA — phone numbers, timestamps, tab labels)
65
+ `faint` DECORATIVE only 2.5:1 (keypad letters, placeholder — never information)
66
+ The legacy used gray-400 for all secondary text, which fails AA at 2.54:1. Splitting the tier
67
+ keeps the look while making the informational text legible. */
68
+ --cv-text: #111827;
69
+ --cv-muted: #6b7280;
70
+ --cv-faint: #9ca3af;
71
+
72
+ /* Intent — emerald. Split by CONTRAST DUTY, because one "brand green" cannot do all three jobs:
73
+ `accent` FILL that carries a white glyph → emerald-600, white 3.77:1 ✓
74
+ (emerald-500 is the prettier brand green but white on it is 2.54:1 — the
75
+ call button's phone icon would fail WCAG non-text contrast. Measured, not guessed.)
76
+ `accent-strong` emerald AS TEXT on a surface → emerald-700, 5.48:1 ✓ for the 10px tab label
77
+ `accent-light` DECORATIVE only (the ring halo) — never carries text, so it stays vivid */
78
+ --cv-accent: #059669;
79
+ --cv-accent-strong: #047857;
80
+ --cv-accent-light: #10b981;
81
+ --cv-accent-soft: #a7f3d0;
82
+ --cv-accent-contrast: #ffffff;
83
+ --cv-danger: #ef4444;
84
+ /* `--cv-danger` ITSELF fails 3:1 non-text contrast against `--cv-danger-soft` (2.6:1, measured) —
85
+ the same AS-TEXT/on-a-tinted-surface split `accent` already has, `danger` was just missing until
86
+ the Historial tab's status badges needed it. Darker on light (mirrors `accent-strong`), LIGHTER on
87
+ dark (red-400) — same inversion `accent-strong` documents above, same reason: emphasis on a dark
88
+ surface means more light, not less.
89
+ red-700 (#b91c1c) was the first pick here and overshot both minimums (6.47:1 text, 4.47:1 badge)
90
+ enough to read as a dark, brownish red rather than red — red-600 clears both with real margin
91
+ still (4.83:1 / 3.34:1, measured) and looks like red. */
92
+ --cv-danger-strong: #dc2626;
93
+ --cv-danger-soft: #fecaca;
94
+ /* A third status hue, sky — for the Historial row status chip specifically: an outgoing call that
95
+ WAS answered gets its own colour rather than sharing `accent`, matching the legacy CRM's own
96
+ three-way split (emerald/sky/rose) that this file's earlier restrained accent-only-for-"fine"
97
+ version had collapsed away. Same strong/soft split, same inversion, as `accent`/`danger` above —
98
+ 4.47:1 light / 4.42:1 dark against `-soft`, measured. */
99
+ --cv-info: #0284c7;
100
+ --cv-info-strong: #0369a1;
101
+ --cv-info-soft: #bae6fd;
102
+ --cv-hold: #6366f1;
103
+
104
+ /* Shape & type */
105
+ --cv-radius: 16px;
106
+ /* Strong ease-out. The built-in CSS easings are too weak to read as intentional. */
107
+ --cv-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
108
+ --cv-radius-key: 16px;
109
+ --cv-font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
110
+ --cv-font-size: 14px;
111
+ --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 1px 3px rgb(0 0 0 / 0.06);
112
+
113
+ color: var(--cv-text);
114
+ font-family: var(--cv-font);
115
+ font-size: var(--cv-font-size);
116
+ }
117
+
118
+ /**
119
+ * Dark theme — matched to the Coventia product's own dark tokens, not invented.
120
+ *
121
+ * The CRM defines `--background: 240 10% 10%` (#17171c) with surfaces at 15.9% (#27272a) and borders
122
+ * at 25.9% (#404044) — the zinc/240-hue family. An earlier draft here used a near-black
123
+ * (#0f0f13, ~half the luminance of the product's background) with 2%-luminance borders that were
124
+ * effectively invisible. That is the "black tonality" that read wrong: right hue, wrong lightness.
125
+ *
126
+ * Note the accent INVERTS: on light, `accent-strong` is darker than `accent` (emerald-700); on dark
127
+ * it must be LIGHTER (emerald-400), because emphasis on a dark surface means more light, not less.
128
+ * Same token, opposite direction — miss this and the timer/active tab sink into the background.
129
+ *
130
+ * ── HOW THE THEME IS CHOSEN ──────────────────────────────────────────────────
131
+ * By default it follows the OS (`prefers-color-scheme`). But a host must be able to PIN it: the
132
+ * Coventia CRM toggles dark with a class (next-themes), not the OS setting — so a user with a dark
133
+ * OS but the CRM in light mode would otherwise get a dark widget inside a light app.
134
+ *
135
+ * <SoftphoneWidget theme="light" /> → data-cv-theme="light" — always light
136
+ * <SoftphoneWidget theme="dark" /> → data-cv-theme="dark" — always dark
137
+ * <SoftphoneWidget /> → follows the OS
138
+ *
139
+ * To follow the HOST's theme rather than the OS, pass the host's current theme through, e.g.
140
+ * next-themes: `const { resolvedTheme } = useTheme(); <SoftphoneWidget theme={resolvedTheme} />`.
141
+ *
142
+ * The dark block is duplicated below (OS-driven vs. pinned) on purpose: plain CSS has no way to
143
+ * share a declaration block across a media query and a selector, and the honest duplication beats a
144
+ * clever custom-property toggle nobody can debug.
145
+ */
146
+ @media (prefers-color-scheme: dark) {
147
+ :host(:not([data-cv-theme='light'])),
148
+ .cv-softphone:not([data-cv-theme='light']) {
149
+ --cv-bg: #17171c; /* CRM --background 240 10% 10% */
150
+ --cv-surface: #202027; /* between bg and secondary */
151
+ --cv-surface-2: #27272a; /* CRM --secondary/--muted 240 3.7% 15.9% */
152
+ --cv-border: #2e2e33;
153
+ --cv-border-strong: #404044; /* CRM --border 240 3.7% 25.9% */
154
+ --cv-text: #fafafa; /* CRM --foreground 0 0% 98% */
155
+ --cv-muted: #a1a1aa; /* CRM --muted-foreground 240 5% 64.9% → 6.97:1 AA */
156
+ --cv-faint: #71717a; /* decorative only */
157
+ --cv-accent-strong: #34d399; /* emerald-400 — 9.29:1 on #17171c */
158
+ --cv-accent-light: #34d399; /* the halo reads brighter on dark */
159
+ --cv-accent-soft: #065f46; /* glows must darken on dark, not glare */
160
+ --cv-danger-strong: #f87171; /* red-400 — LIGHTER than --cv-danger, same inversion as accent-strong */
161
+ --cv-danger-soft: #7f1d1d;
162
+ --cv-info-strong: #38bdf8; /* sky-400 — same inversion, same reason */
163
+ --cv-info-soft: #0c4a6e;
164
+ --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.4);
165
+ }
166
+ }
167
+
168
+ /* Pinned dark — wins regardless of the OS setting. */
169
+ :host([data-cv-theme='dark']),
170
+ .cv-softphone[data-cv-theme='dark'] {
171
+ --cv-bg: #17171c;
172
+ --cv-surface: #202027;
173
+ --cv-surface-2: #27272a;
174
+ --cv-border: #2e2e33;
175
+ --cv-border-strong: #404044;
176
+ --cv-text: #fafafa;
177
+ --cv-muted: #a1a1aa;
178
+ --cv-faint: #71717a;
179
+ --cv-accent-strong: #34d399;
180
+ --cv-accent-light: #34d399;
181
+ --cv-accent-soft: #065f46;
182
+ --cv-danger-strong: #f87171;
183
+ --cv-danger-soft: #7f1d1d;
184
+ --cv-info-strong: #38bdf8;
185
+ --cv-info-soft: #0c4a6e;
186
+ --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.4);
187
+ }
188
+
189
+ .cv-softphone,
190
+ .cv-softphone *,
191
+ .cv-softphone *::before,
192
+ .cv-softphone *::after {
193
+ box-sizing: border-box;
194
+ }
195
+
196
+ /* ── Shell ──────────────────────────────────────────────────────────────────── */
197
+ .cv-softphone {
198
+ display: flex;
199
+ flex-direction: column;
200
+
201
+ /* FILL the box the host gives us; do not assert one.
202
+ This was `width: 340px; height: 520px`, which is a component telling its host how big its
203
+ layout is. The CRM's panel is 310 wide, so the third keypad column was simply cut off — and no
204
+ amount of theming could have fixed it, because the widget was not asking. A component in
205
+ someone else's layout accepts a size. */
206
+ width: 100%;
207
+ height: 100%;
208
+
209
+ /* …but a dialpad is not a spreadsheet. Given 900px it should not become a 900px dialpad with
210
+ 300px keys; it should be itself, centred. Cap and centre rather than stretch. */
211
+ max-width: var(--cv-max-width, 420px);
212
+ margin-inline: auto;
213
+
214
+ /* Adapt to THIS box, not to the viewport. The widget can be 310px wide inside a 1920px screen —
215
+ a media query would ask the wrong question and get a confident wrong answer. Container queries
216
+ are the only ones that know the truth, and a shadow root is the ideal place for them. */
217
+ container-type: inline-size;
218
+ container-name: cv;
219
+
220
+ background: var(--cv-bg);
221
+ overflow: hidden;
222
+
223
+ /* The frame is the HOST's call. The CRM's panel already draws `border rounded-xl shadow-2xl`, so
224
+ drawing our own put a card inside a card — nested chrome, which is always wrong. Defaulting to
225
+ a frame still suits a bare drop-in on a blank page; a host that has its own sets these to
226
+ `none` / `0` and the double edge disappears. */
227
+ border: var(--cv-shell-border, 1px solid var(--cv-border-strong));
228
+ border-radius: var(--cv-shell-radius, var(--cv-radius));
229
+ box-shadow: var(--cv-shell-shadow, 0 4px 24px rgb(0 0 0 / 0.06));
230
+ }
231
+
232
+ /* Narrow hosts — a sidebar, a phone-width column. Reclaim the horizontal padding first: at 260px
233
+ the gutters cost more than they give, and a cramped keypad is worse than a tight one. */
234
+ @container cv (max-width: 300px) {
235
+ .cv-keypad { padding: 0 0.6rem; gap: 6px; }
236
+ .cv-dial-prefix { padding: 0.7rem 0.6rem 0; }
237
+ .cv-dial-head { padding: 0.3rem 0.6rem 0.6rem; }
238
+ .cv-display { font-size: 1.5rem; }
239
+ .cv-controls-row { gap: 1.1rem; }
240
+
241
+ }
242
+
243
+ /* Short hosts. The flex chain already lets the keypad absorb what is left, but below this the keys
244
+ stop being tappable — so buy the height back from the chrome instead of from the keys. */
245
+ @container cv (max-height: 440px) {
246
+ .cv-dial-prefix { padding-top: 0.5rem; }
247
+ .cv-dial-head { padding-top: 0.2rem; padding-bottom: 0.4rem; }
248
+ .cv-dial-foot { padding-bottom: 0.5rem; }
249
+ .cv-controls { gap: 0.4rem; padding-bottom: 0.6rem; }
250
+ }
251
+
252
+ /* The flex chain that makes the layout work: body flexes, views fill it, only the
253
+ keypad/list grows. Without `min-height:0` a grid child refuses to shrink and overflows. */
254
+ .cv-body {
255
+ flex: 1;
256
+ min-height: 0;
257
+ display: flex;
258
+ flex-direction: column;
259
+ }
260
+ .cv-view {
261
+ flex: 1;
262
+ min-height: 0;
263
+ display: flex;
264
+ flex-direction: column;
265
+ /* Containing block for `.cv-detail` below — an absolute overlay must cover exactly THIS tab's
266
+ content, not escape to whatever the next positioned ancestor up the tree happens to be. */
267
+ position: relative;
268
+ }
269
+
270
+ /* ── Tab bar ─────────────────────────────────────────────────────────────────── */
271
+ .cv-tabs {
272
+ display: flex;
273
+ border-top: 1px solid var(--cv-border);
274
+ background: var(--cv-bg);
275
+ flex-shrink: 0;
276
+ }
277
+ .cv-tab {
278
+ flex: 1;
279
+ display: flex;
280
+ flex-direction: column;
281
+ align-items: center;
282
+ gap: 3px;
283
+ padding: 9px 0 8px;
284
+ background: none;
285
+ border: none;
286
+ color: var(--cv-muted);
287
+ cursor: pointer;
288
+ font: inherit;
289
+ transition: color 0.15s ease, background 0.15s ease;
290
+ }
291
+ .cv-tab:hover:not(:disabled) { background: var(--cv-surface); }
292
+ .cv-tab[aria-selected='true'] { color: var(--cv-accent-strong); }
293
+ .cv-tab:disabled { opacity: 0.35; cursor: not-allowed; }
294
+ .cv-tab span { font-size: 10px; font-weight: 600; line-height: 1; letter-spacing: 0.01em; }
295
+
296
+ /* ── Buttons ─────────────────────────────────────────────────────────────────── */
297
+ .cv-btn {
298
+ font: 600 0.875rem var(--cv-font);
299
+ cursor: pointer;
300
+ padding: 0.5rem 0.9rem;
301
+ border-radius: 10px;
302
+ border: 1px solid var(--cv-border-strong);
303
+ background: var(--cv-bg);
304
+ color: var(--cv-text);
305
+ transition: background 0.15s ease, transform 0.06s ease;
306
+ }
307
+ .cv-btn:hover:not(:disabled) { background: var(--cv-surface); }
308
+ .cv-btn:active:not(:disabled) { transform: scale(0.97); }
309
+ .cv-btn:disabled { opacity: 0.45; cursor: not-allowed; }
310
+ .cv-btn--accent {
311
+ background: var(--cv-accent);
312
+ border-color: var(--cv-accent);
313
+ color: var(--cv-accent-contrast);
314
+ }
315
+ .cv-btn--accent:hover:not(:disabled) { background: var(--cv-accent-strong); }
316
+
317
+ /* Round action buttons (answer / hangup / call) */
318
+ .cv-round {
319
+ width: 64px;
320
+ height: 64px;
321
+ border-radius: 999px;
322
+ border: none;
323
+ cursor: pointer;
324
+ display: inline-flex;
325
+ align-items: center;
326
+ justify-content: center;
327
+ color: #fff;
328
+ transition: background 0.15s ease, transform 0.08s ease, box-shadow 0.15s ease;
329
+ }
330
+ .cv-round:active:not(:disabled) { transform: scale(0.94); }
331
+ .cv-round--answer {
332
+ background: var(--cv-accent);
333
+ box-shadow: 0 8px 20px -4px var(--cv-accent-soft);
334
+ }
335
+ .cv-round--answer:hover:not(:disabled) { background: var(--cv-accent-strong); }
336
+ .cv-round--answer:disabled {
337
+ background: var(--cv-surface-2);
338
+ color: var(--cv-faint);
339
+ box-shadow: none;
340
+ cursor: not-allowed;
341
+ }
342
+ .cv-round--hangup {
343
+ background: var(--cv-danger);
344
+ box-shadow: 0 8px 20px -4px var(--cv-danger-soft);
345
+ }
346
+ .cv-round--hangup:hover:not(:disabled) { filter: brightness(0.94); }
347
+
348
+ /* ── Dialpad ─────────────────────────────────────────────────────────────────── */
349
+ /* The prefix chip, on its own row above the number.
350
+ Sharing a line with the number starves it: chip + erase button eat the middle, and a formatted
351
+ international number needs every pixel. The number is the content; the prefix is a setting. */
352
+ .cv-dial-prefix {
353
+ flex-shrink: 0;
354
+ display: flex;
355
+ justify-content: center;
356
+ padding: 1rem 1rem 0;
357
+ }
358
+ .cv-dial-head {
359
+ flex-shrink: 0;
360
+ display: flex;
361
+ align-items: center;
362
+ padding: 0.35rem 1rem 0.75rem;
363
+ gap: 0.25rem;
364
+ }
365
+ /*
366
+ * The outcome of the last attempt, under the number.
367
+ *
368
+ * ALWAYS in the layout, visible or not. The keypad is `flex: 1` and absorbs whatever height is left,
369
+ * so a line that appears and disappears would resize every key and shift the whole grid under the
370
+ * user's finger — mid-dial, which is exactly when this text arrives. Reserving the row costs one line
371
+ * of height and buys a keypad that never moves.
372
+ */
373
+ .cv-dial-outcome {
374
+ flex-shrink: 0;
375
+ margin: 0;
376
+ min-height: 1.15rem;
377
+ padding: 0 1rem;
378
+ text-align: center;
379
+ font-size: 0.78rem;
380
+ line-height: 1.15rem;
381
+ color: var(--cv-muted);
382
+ /*
383
+ One line, always. `error-text.test.ts` caps every outcome at 48 characters so nothing should ever
384
+ reach this — but a cap depends on whoever writes the next string remembering it, and the failure
385
+ it prevents is not cosmetic: the keypad below is `flex: 1`, so a second line resizes all twelve
386
+ keys and shifts the grid under a finger that is already moving. Truncating is visibly wrong and
387
+ harmless; reflowing the dialpad mid-dial is invisibly wrong and makes the user misdial.
388
+ */
389
+ white-space: nowrap;
390
+ overflow: hidden;
391
+ text-overflow: ellipsis;
392
+ /* Muted, not danger: the phone still works. A red line here would read as a broken phone. */
393
+ opacity: 0;
394
+ transition: opacity 150ms ease-out;
395
+ }
396
+ .cv-dial-outcome[data-shown='true'] {
397
+ opacity: 1;
398
+ }
399
+ @media (prefers-reduced-motion: reduce) {
400
+ .cv-dial-outcome {
401
+ transition: none;
402
+ }
403
+ }
404
+
405
+ .cv-display {
406
+ flex: 1;
407
+ min-width: 0;
408
+ font: 600 1.75rem var(--cv-font);
409
+ font-variant-numeric: tabular-nums;
410
+ letter-spacing: -0.02em;
411
+ text-align: center;
412
+ color: var(--cv-text);
413
+ border: none;
414
+ background: none;
415
+ padding: 0;
416
+ caret-color: var(--cv-accent);
417
+ }
418
+ .cv-display::placeholder { color: var(--cv-faint); font-weight: 400; }
419
+ .cv-display:focus { outline: none; }
420
+
421
+ /* The spacer/erase pair keeps the number optically centred. */
422
+ .cv-dial-side {
423
+ width: 36px;
424
+ height: 36px;
425
+ flex-shrink: 0;
426
+ display: inline-flex;
427
+ align-items: center;
428
+ justify-content: center;
429
+ border: none;
430
+ background: none;
431
+ color: var(--cv-muted);
432
+ cursor: pointer;
433
+ border-radius: 999px;
434
+ transition: color 0.15s ease, opacity 0.15s ease, transform 0.06s ease;
435
+ }
436
+ .cv-dial-side:hover { color: var(--cv-text); }
437
+ .cv-dial-side:active { transform: scale(0.9); }
438
+ .cv-dial-side[data-hidden='true'] { opacity: 0; pointer-events: none; }
439
+
440
+ /* THE layout fix: the keypad flexes to fill, and its rows share the space equally.
441
+ Keys must NOT use aspect-ratio — that overflows the shell and shoves the call button
442
+ under the tab bar. */
443
+ .cv-keypad {
444
+ flex: 1;
445
+ min-height: 0;
446
+ display: grid;
447
+ grid-template-columns: repeat(3, 1fr);
448
+ grid-template-rows: repeat(4, 1fr);
449
+ gap: 8px;
450
+ padding: 0 1rem;
451
+ }
452
+ .cv-key {
453
+ display: flex;
454
+ flex-direction: column;
455
+ align-items: center;
456
+ justify-content: center;
457
+ gap: 1px;
458
+ border-radius: var(--cv-radius-key);
459
+ border: 1px solid var(--cv-border);
460
+ background: var(--cv-bg);
461
+ box-shadow: var(--cv-shadow);
462
+ color: var(--cv-text);
463
+ cursor: pointer;
464
+ user-select: none;
465
+ transition: background 0.12s ease, transform 0.06s ease;
466
+ }
467
+ .cv-key:hover { background: var(--cv-surface); }
468
+ .cv-key:active { transform: scale(0.95); background: var(--cv-surface-2); }
469
+ .cv-key b { font: 500 1.25rem/1 var(--cv-font); }
470
+ .cv-key small {
471
+ font-size: 9px;
472
+ font-weight: 600;
473
+ letter-spacing: 0.14em;
474
+ /* Decorative: the letters carry no information the digit doesn't. `faint`, not `muted`. */
475
+ color: var(--cv-faint);
476
+ line-height: 1;
477
+ }
478
+
479
+ .cv-dial-foot {
480
+ flex-shrink: 0;
481
+ display: flex;
482
+ justify-content: center;
483
+ padding: 0.75rem 0 1rem;
484
+ }
485
+
486
+ /* ── Active call ─────────────────────────────────────────────────────────────── */
487
+ .cv-call {
488
+ flex: 1;
489
+ min-height: 0;
490
+ display: flex;
491
+ flex-direction: column;
492
+ align-items: center;
493
+ justify-content: center;
494
+ gap: 0.15rem;
495
+ text-align: center;
496
+ padding: 1rem;
497
+ }
498
+
499
+ .cv-avatar-wrap { position: relative; margin-bottom: 0.85rem; }
500
+ .cv-avatar {
501
+ position: relative;
502
+ width: 68px;
503
+ height: 68px;
504
+ border-radius: 999px;
505
+ /* accent → accent-strong (emerald 600→700): white initials clear 3.77:1 at EVERY stop. Starting
506
+ the ramp at emerald-500 would look punchier but drop the light end to 2.54:1. */
507
+ background: linear-gradient(135deg, var(--cv-accent) 0%, var(--cv-accent-strong) 100%);
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: center;
511
+ color: #fff;
512
+ font: 700 1.3rem var(--cv-font);
513
+ letter-spacing: -0.01em;
514
+ box-shadow: 0 8px 20px -6px var(--cv-accent-soft);
515
+ }
516
+ /* A soft halo while ringing — the one moment worth animating. */
517
+ .cv-avatar-ping {
518
+ position: absolute;
519
+ inset: 0;
520
+ border-radius: 999px;
521
+ /* Decorative — nothing sits on it, so it uses the vivid emerald. */
522
+ background: var(--cv-accent-light);
523
+ opacity: 0.16;
524
+ animation: cv-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite;
525
+ pointer-events: none;
526
+ }
527
+ @keyframes cv-ping {
528
+ 0% { transform: scale(1); opacity: 0.22; }
529
+ 80%, 100% { transform: scale(1.7); opacity: 0; }
530
+ }
531
+ .cv-avatar-dot {
532
+ position: absolute;
533
+ bottom: 1px;
534
+ right: 1px;
535
+ width: 14px;
536
+ height: 14px;
537
+ border-radius: 999px;
538
+ background: var(--cv-accent);
539
+ border: 2px solid var(--cv-bg);
540
+ }
541
+
542
+ .cv-call-name { font-size: 1rem; font-weight: 600; line-height: 1.2; }
543
+ .cv-call-number { font-size: 0.75rem; color: var(--cv-muted); margin-top: 2px; }
544
+
545
+ .cv-timer {
546
+ display: flex;
547
+ align-items: baseline;
548
+ justify-content: center;
549
+ gap: 1px;
550
+ margin-top: 0.75rem;
551
+ color: var(--cv-accent-strong);
552
+ }
553
+ .cv-timer b {
554
+ font: 700 2.25rem/1 var(--cv-font);
555
+ font-variant-numeric: tabular-nums;
556
+ letter-spacing: -0.03em;
557
+ }
558
+ .cv-timer i {
559
+ font: 300 1.75rem/1 var(--cv-font);
560
+ font-style: normal;
561
+ color: var(--cv-accent);
562
+ animation: cv-blink 1.6s ease-in-out infinite;
563
+ margin: 0 1px;
564
+ }
565
+ @keyframes cv-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
566
+
567
+ .cv-call-status {
568
+ margin-top: 0.6rem;
569
+ font-size: 0.78rem;
570
+ color: var(--cv-muted);
571
+ letter-spacing: 0.04em;
572
+ }
573
+ .cv-badge-hold { color: var(--cv-hold); font-weight: 600; }
574
+
575
+ /* ── Pre-connect device check (ringing / dialing) ──────────────────────────────
576
+ Before a call connects there is only ONE other control on screen (Colgar, or
577
+ Rechazar/Contestar) — putting Dispositivos in that same neighbourhood, even
578
+ spaced out, keeps it a mis-tap away from hanging up or answering. The entry
579
+ chip lives beside the call status instead, removing it from that neighbourhood
580
+ rather than just adding distance within it — same resting-pill SHAPE as `.cv-cp`
581
+ (CountryPicker), reused rather than invented fresh.
582
+
583
+ Opening it does NOT pop a panel into that same tight space, though — this
584
+ SDK's real production embed (a 310×520 host panel with its own header eating
585
+ further into that) leaves too little free room around a trigger sitting in
586
+ vertically-centred content for a floating popover to open into without either
587
+ overlapping Colgar/Rechazar/Contestar or clamping itself down to something
588
+ cramped and scrolly for just two rows. `.cv-call-devices-inline` REPLACES the
589
+ avatar/name/status block in `.cv-call` instead (see that JSX's own comment) —
590
+ an inline swap, not an overlay, so it gets the full box, not a leftover sliver.
591
+ ───────────────────────────────────────────────────────────────────────────── */
592
+ .cv-call-devices-chip {
593
+ display: inline-flex;
594
+ align-items: center;
595
+ gap: 0.35rem;
596
+ height: 26px;
597
+ padding: 0 0.65rem;
598
+ border: 1px solid var(--cv-border-strong);
599
+ border-radius: 999px;
600
+ background: var(--cv-bg);
601
+ color: var(--cv-muted);
602
+ font: 600 0.7rem var(--cv-font);
603
+ cursor: pointer;
604
+ transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
605
+ }
606
+ .cv-call-devices-chip:hover { background: var(--cv-surface); }
607
+ .cv-call-devices-chip:active { transform: scale(0.95); }
608
+
609
+ /*
610
+ The entry-point row — ONE chip today (Dispositivos), up to two once a host configures
611
+ `incomingCallAction` (that one first: who is calling outranks a phone-utility setting). `width:
612
+ 100%` + `flex-wrap` on purpose: a host's action label is arbitrary text this component cannot
613
+ assume is short, so two chips sit side by side when they fit and wrap onto their own centred line
614
+ when they do not, rather than overflowing `.cv-call`'s fixed width.
615
+ */
616
+ .cv-call-chips {
617
+ width: 100%;
618
+ display: flex;
619
+ flex-wrap: wrap;
620
+ justify-content: center;
621
+ gap: 0.5rem;
622
+ margin-top: 0.65rem;
623
+ }
624
+
625
+ .cv-call-devices-inline {
626
+ width: 100%;
627
+ display: flex;
628
+ flex-direction: column;
629
+ gap: 0.9rem;
630
+ /* `.cv-call` centres and centre-aligns its normal (avatar) content — rows read left-aligned
631
+ everywhere else in the widget (Ajustes, the in-call popover), so this opts back out of both. */
632
+ text-align: left;
633
+ animation: cv-call-devices-in 0.18s cubic-bezier(0.2, 0, 0.2, 1);
634
+ }
635
+ @keyframes cv-call-devices-in {
636
+ from { opacity: 0; transform: translateY(6px); }
637
+ to { opacity: 1; transform: none; }
638
+ }
639
+ @media (prefers-reduced-motion: reduce) {
640
+ .cv-call-devices-inline { animation: none; }
641
+ }
642
+ .cv-call-devices-header { display: flex; align-items: center; gap: 0.5rem; }
643
+ .cv-call-devices-title { font-size: 0.95rem; font-weight: 600; color: var(--cv-text); }
644
+ /* `--cv-muted` on `--cv-bg` — the same already-verified pairing `.cv-settings-note` uses
645
+ (4.83:1 light / 6.97:1 dark), not a new one. */
646
+ .cv-call-devices-context {
647
+ margin: 0;
648
+ font-size: 0.78rem;
649
+ color: var(--cv-muted);
650
+ }
651
+
652
+ /* ── Call controls ───────────────────────────────────────────────────────────
653
+ TWO TIERS. Hang up is the one action that must never be hunted for, so it sits
654
+ alone, centred, and largest — the anchor. The secondaries flank above it. They
655
+ used to share a single row, where hang up was just the last button and carried
656
+ the same weight as "transfer"; mid-call, that is the wrong hierarchy.
657
+ ─────────────────────────────────────────────────────────────────────────── */
658
+ .cv-controls {
659
+ flex-shrink: 0;
660
+ display: flex;
661
+ flex-direction: column;
662
+ align-items: center;
663
+ gap: 0.7rem;
664
+ padding: 0.35rem 1rem 1.1rem;
665
+ /* The containing block for `.cv-device-popover` — see that rule's own comment for why it is
666
+ anchored here rather than to the trigger button. */
667
+ position: relative;
668
+ }
669
+ .cv-controls-row {
670
+ display: flex;
671
+ justify-content: center;
672
+ gap: 1.5rem;
673
+ }
674
+ .cv-controls-primary {
675
+ display: flex;
676
+ justify-content: center;
677
+ gap: 2.75rem;
678
+ }
679
+ /* Button + its word. The column keeps the label from widening the hit target. */
680
+ .cv-ctrl-item {
681
+ display: flex;
682
+ flex-direction: column;
683
+ align-items: center;
684
+ gap: 0.3rem;
685
+ }
686
+ /* 10px, muted: present when you look for it, silent when you don't. An icon-only
687
+ control is a quiz, and mid-call is the worst time to sit one. */
688
+ .cv-ctrl-label {
689
+ font-size: 0.625rem;
690
+ line-height: 1;
691
+ color: var(--cv-muted);
692
+ white-space: nowrap;
693
+ user-select: none;
694
+ }
695
+ .cv-ctrl {
696
+ width: 48px;
697
+ height: 48px;
698
+ border-radius: 999px;
699
+ border: 1px solid var(--cv-border-strong);
700
+ background: var(--cv-bg);
701
+ color: var(--cv-text);
702
+ cursor: pointer;
703
+ display: inline-flex;
704
+ align-items: center;
705
+ justify-content: center;
706
+ transition: background 0.15s ease, color 0.15s ease, transform 0.06s ease;
707
+ }
708
+ .cv-ctrl:hover { background: var(--cv-surface); }
709
+ .cv-ctrl:active { transform: scale(0.94); }
710
+
711
+ /* The ON states say what they MEAN, in the widget's own tokens: muted is danger,
712
+ held is hold, transfer-open is accent. Colour is the state, not decoration. */
713
+ .cv-ctrl[data-tone='danger'] {
714
+ background: var(--cv-danger);
715
+ border-color: var(--cv-danger);
716
+ color: #fff;
717
+ }
718
+ .cv-ctrl[data-tone='hold'] {
719
+ background: var(--cv-hold);
720
+ border-color: var(--cv-hold);
721
+ color: #fff;
722
+ }
723
+ .cv-ctrl[data-tone='accent'] {
724
+ background: var(--cv-accent-soft);
725
+ border-color: transparent;
726
+ color: var(--cv-accent-strong);
727
+ }
728
+ .cv-ctrl[data-tone]:hover { filter: brightness(0.95); }
729
+
730
+ /* Muted is the one state worth animating: it is invisible, it is easy to forget,
731
+ and the cost of forgetting is talking to nobody. Same halo the avatar rings
732
+ with — one language, not two. */
733
+ .cv-ctrl[data-tone='danger'] {
734
+ position: relative;
735
+ }
736
+ .cv-ctrl[data-tone='danger']::after {
737
+ content: '';
738
+ position: absolute;
739
+ inset: 0;
740
+ border-radius: 999px;
741
+ background: var(--cv-danger);
742
+ animation: cv-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite;
743
+ pointer-events: none;
744
+ z-index: -1;
745
+ }
746
+ @media (prefers-reduced-motion: reduce) {
747
+ .cv-ctrl[data-tone='danger']::after { animation: none; opacity: 0; }
748
+ }
749
+ .cv-ctrl[aria-pressed='true'] {
750
+ background: var(--cv-text);
751
+ color: var(--cv-bg);
752
+ border-color: var(--cv-text);
753
+ }
754
+
755
+ .cv-transfer {
756
+ display: flex;
757
+ gap: 0.5rem;
758
+ width: 100%;
759
+ padding: 0.85rem 0.25rem 0;
760
+ }
761
+
762
+ /*
763
+ The in-call device quick-switch (SPEC §7 Phase 2 — device management). Same rows as the Ajustes tab
764
+ (`.cv-settings-row`, via the shared `DevicePickers`), in a popover instead of a page — the call
765
+ screen already takes over the whole body, so there is no tab bar to reach Ajustes from mid-call.
766
+
767
+ Only the in-call state still uses a floating popover — before a call connects, Dispositivos is
768
+ inline content instead (`.cv-call-devices-inline`, above), not an overlay. Opens UPWARD (`bottom:
769
+ 100%`): the trigger sits at the bottom of the call screen, and there is no room to open downward.
770
+ Anchored to `.cv-controls`'s own horizontal padding — a container whose width is bounded by ITS
771
+ parent, never the narrow trigger itself — the same fix CountryPicker's own popover needed (see its
772
+ comment).
773
+ */
774
+ .cv-device-popover {
775
+ position: absolute;
776
+ z-index: 20;
777
+ display: flex;
778
+ flex-direction: column;
779
+ gap: 0.6rem;
780
+ padding: 0.7rem;
781
+ background: var(--cv-surface);
782
+ border: 1px solid var(--cv-border);
783
+ border-radius: var(--cv-radius);
784
+ box-shadow: var(--cv-shadow);
785
+ }
786
+ .cv-device-popover--above {
787
+ bottom: 100%;
788
+ left: 1rem;
789
+ right: 1rem;
790
+ margin-bottom: 0.6rem;
791
+ transform-origin: bottom center;
792
+ animation: cv-device-pop-in-up 0.14s cubic-bezier(0.2, 0, 0.2, 1);
793
+ }
794
+ @keyframes cv-device-pop-in-up {
795
+ from { opacity: 0; transform: translateY(4px) scale(0.97); }
796
+ to { opacity: 1; transform: none; }
797
+ }
798
+ @media (prefers-reduced-motion: reduce) {
799
+ .cv-device-popover--above { animation: none; }
800
+ }
801
+ /* No per-row override needed here: `.cv-settings-row`/`.cv-settings-note` carry no border,
802
+ background or padding of their own (see the Settings tab block) — this popover's own `gap` and
803
+ `padding` are the only spacing in play, which is what keeps it lighter than the full Ajustes
804
+ page's bordered section for what is meant to be a quick, transient panel. */
805
+
806
+ /* ── Lists (contacts / history) ──────────────────────────────────────────────── */
807
+ .cv-list-head { flex-shrink: 0; padding: 0.85rem 1rem 0.5rem; }
808
+ .cv-search {
809
+ width: 100%;
810
+ padding: 0.5rem 0.75rem;
811
+ border-radius: 10px;
812
+ border: 1px solid var(--cv-border-strong);
813
+ background: var(--cv-surface);
814
+ color: var(--cv-text);
815
+ font: inherit;
816
+ transition: border-color 0.15s ease, background 0.15s ease;
817
+ }
818
+ .cv-search::placeholder { color: var(--cv-muted); }
819
+ .cv-search:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }
820
+
821
+ /*
822
+ A wrapper around `.cv-search`, not a change to it — that class is also the transfer-target input
823
+ in CallScreen, which wants neither an icon nor a clear button. `.cv-search--icon` is an ADDITIVE
824
+ modifier on the input itself (padding only), so the base class is byte-for-byte what it was for
825
+ every other consumer.
826
+ */
827
+ .cv-search-field { position: relative; display: flex; align-items: center; }
828
+ .cv-search--icon { padding-left: 2.15rem; padding-right: 1.85rem; }
829
+ .cv-search-icon {
830
+ position: absolute;
831
+ left: 0.75rem;
832
+ color: var(--cv-muted);
833
+ pointer-events: none;
834
+ }
835
+ .cv-search-clear {
836
+ position: absolute;
837
+ right: 0.5rem;
838
+ width: 22px;
839
+ height: 22px;
840
+ display: inline-flex;
841
+ align-items: center;
842
+ justify-content: center;
843
+ border: none;
844
+ border-radius: 999px;
845
+ background: none;
846
+ color: var(--cv-muted);
847
+ cursor: pointer;
848
+ transition: color 0.12s ease, transform 0.1s var(--cv-ease-out);
849
+ }
850
+ .cv-search-clear:hover { color: var(--cv-text); }
851
+ .cv-search-clear:active { transform: scale(0.88); }
852
+
853
+ /* ── Historial filters: status (Todas/Perdidas) + person ─────────────────────── */
854
+ .cv-segmented {
855
+ display: flex;
856
+ gap: 2px;
857
+ padding: 2px;
858
+ border-radius: 10px;
859
+ background: var(--cv-surface-2);
860
+ margin-bottom: 0.4rem;
861
+ }
862
+ .cv-segmented-btn {
863
+ flex: 1;
864
+ padding: 0.35rem 0;
865
+ border: none;
866
+ border-radius: 8px;
867
+ background: none;
868
+ color: var(--cv-muted);
869
+ font: 600 0.75rem var(--cv-font);
870
+ cursor: pointer;
871
+ transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
872
+ }
873
+ /* The active segment is stated by its own fill + shadow, not colour alone — the same reasoning
874
+ `.cv-item-action[data-tone='accent']`'s own comment gives for resting-state colour: a state that
875
+ only differs by hue must still read on a screen that renders neither. */
876
+ .cv-segmented-btn[aria-selected='true'] {
877
+ background: var(--cv-bg);
878
+ color: var(--cv-text);
879
+ box-shadow: var(--cv-shadow);
880
+ }
881
+ @media (prefers-reduced-motion: reduce) {
882
+ .cv-segmented-btn { transition: none; }
883
+ }
884
+
885
+ /* The person filter — same combobox SHAPE as `.cv-cp-*` (CountryPicker): a chip opening a positioned
886
+ popover list with a checkmark on the active row. Not the same CLASSES: the two pickers have nothing
887
+ to do with each other and sharing a namespace would make a future change to one silently reach the
888
+ other. */
889
+ .cv-person-filter { position: relative; width: 100%; }
890
+ .cv-person-filter-chip {
891
+ display: flex;
892
+ align-items: center;
893
+ gap: 0.4rem;
894
+ width: 100%;
895
+ padding: 0.4rem 0.65rem;
896
+ border: 1px solid var(--cv-border-strong);
897
+ border-radius: 10px;
898
+ background: var(--cv-surface);
899
+ color: var(--cv-muted);
900
+ font: 500 0.78rem var(--cv-font);
901
+ cursor: pointer;
902
+ transition: background 0.15s ease, border-color 0.15s ease;
903
+ }
904
+ .cv-person-filter-chip:hover { background: var(--cv-surface-2); }
905
+ /*
906
+ Border only, no fill — the popover opening and the caret rotating already say "this is open";
907
+ painting the whole width of a wide text pill solid green on top of that was the same over-strong
908
+ green the selected row had (see its own comment below). A coloured border is the same restrained
909
+ signal `.cv-search:focus` already uses for "active" elsewhere in this file.
910
+ */
911
+ .cv-person-filter-chip[aria-expanded='true'] {
912
+ border-color: var(--cv-accent);
913
+ }
914
+ .cv-person-filter-label {
915
+ flex: 1;
916
+ min-width: 0;
917
+ overflow: hidden;
918
+ text-overflow: ellipsis;
919
+ white-space: nowrap;
920
+ text-align: left;
921
+ color: var(--cv-text);
922
+ }
923
+ .cv-person-filter-caret {
924
+ flex-shrink: 0;
925
+ transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1);
926
+ }
927
+ .cv-person-filter-caret[data-open='true'] { transform: rotate(180deg); }
928
+ .cv-person-filter-pop {
929
+ position: absolute;
930
+ top: calc(100% + 6px);
931
+ left: 0;
932
+ right: 0;
933
+ z-index: 20;
934
+ max-height: 220px;
935
+ overflow-y: auto;
936
+ padding: 0.25rem;
937
+ background: var(--cv-surface);
938
+ border: 1px solid var(--cv-border);
939
+ border-radius: var(--cv-radius);
940
+ box-shadow: var(--cv-shadow);
941
+ transform-origin: top center;
942
+ animation: cv-cp-in 0.14s cubic-bezier(0.2, 0, 0.2, 1);
943
+ }
944
+ @media (prefers-reduced-motion: reduce) {
945
+ .cv-person-filter-pop { animation: none; }
946
+ .cv-person-filter-caret { transition: none; }
947
+ }
948
+ .cv-person-filter-item {
949
+ width: 100%;
950
+ display: flex;
951
+ align-items: center;
952
+ gap: 0.5rem;
953
+ padding: 0.45rem 0.5rem;
954
+ border: none;
955
+ border-radius: calc(var(--cv-radius) - 4px);
956
+ background: none;
957
+ color: var(--cv-text);
958
+ font: 400 0.8rem var(--cv-font);
959
+ text-align: left;
960
+ cursor: pointer;
961
+ }
962
+ .cv-person-filter-item:hover { background: var(--cv-surface-2); }
963
+ /*
964
+ NOT a solid `--cv-accent-soft` fill — that read as a wall of saturated green across a wide row,
965
+ much louder than the legacy CRM's own treatment for this exact list (`bg-emerald-50` — a barely-
966
+ there wash — plus coloured text, never a strong fill). Coloured, bold text + the checkmark carry
967
+ "selected" instead; `--cv-accent-strong` is already verified elsewhere in this file as AA against
968
+ `--cv-surface`, which is what this popover sits on.
969
+ */
970
+ .cv-person-filter-item[aria-selected='true'] .cv-person-filter-name {
971
+ font-weight: 600;
972
+ color: var(--cv-accent-strong);
973
+ }
974
+ .cv-person-filter-name {
975
+ flex: 1;
976
+ min-width: 0;
977
+ overflow: hidden;
978
+ text-overflow: ellipsis;
979
+ white-space: nowrap;
980
+ }
981
+ .cv-person-filter-check { color: var(--cv-accent-strong); flex-shrink: 0; }
982
+ .cv-person-filter-check[data-on='false'] { opacity: 0; }
983
+
984
+ .cv-list {
985
+ flex: 1;
986
+ min-height: 0;
987
+ overflow-y: auto;
988
+ padding: 0 0.5rem 0.5rem;
989
+ }
990
+ /*
991
+ Historial's day group header. Sticky within `.cv-list` (its nearest scrolling ancestor — a plain
992
+ wrapper div between the two does not break that), OPAQUE `--cv-bg` rather than a translucent
993
+ `/85 + backdrop-blur` treatment: it still needs to fully occlude rows scrolling underneath it, and
994
+ a flat match to the list's own background does that with no new CSS feature this file doesn't
995
+ already use elsewhere.
996
+ */
997
+ .cv-history-day {
998
+ position: sticky;
999
+ top: 0;
1000
+ z-index: 1;
1001
+ padding: 0.5rem 0.6rem 0.35rem;
1002
+ background: var(--cv-bg);
1003
+ color: var(--cv-muted);
1004
+ font-size: 0.68rem;
1005
+ font-weight: 600;
1006
+ text-transform: uppercase;
1007
+ letter-spacing: 0.04em;
1008
+ }
1009
+ .cv-item {
1010
+ display: flex;
1011
+ align-items: center;
1012
+ gap: 0.7rem;
1013
+ padding: 0.5rem;
1014
+ border-radius: 12px;
1015
+ /*
1016
+ NOT `cursor: pointer` — this div has no click handler of its own; only its children
1017
+ (`.cv-item-main`, `.cv-item-action`) do, and each already sets its own pointer cursor. Putting
1018
+ it here too used to make the WHOLE row's box promise a click — its own padding, and the flex
1019
+ `gap` between avatar/name/button — none of which does anything when clicked. Most noticeable
1020
+ right where one row's padding meets the next, which reads as "there's something clickable
1021
+ between these two contacts" when there is not.
1022
+ */
1023
+ transition: background 0.12s ease;
1024
+ text-align: left;
1025
+ width: 100%;
1026
+ border: none;
1027
+ background: none;
1028
+ font: inherit;
1029
+ color: inherit;
1030
+ }
1031
+ .cv-item:hover { background: var(--cv-surface); }
1032
+ .cv-item-avatar {
1033
+ width: 40px;
1034
+ height: 40px;
1035
+ border-radius: 999px;
1036
+ /*
1037
+ RADIAL, not diagonal — deliberately. The initials sit centered, so a radial gradient puts them
1038
+ at its lightest point (pure `--cv-surface`) no matter what, while the edges still darken toward
1039
+ `--cv-surface-2` for the same cheap depth a diagonal gradient gave. A diagonal version measured
1040
+ 4.39:1 for `--cv-muted` text at its far corner — under the 4.5:1 text needs — because centered
1041
+ text's exact position against a DIAGONAL gradient depends on glyph width, not just circle size.
1042
+ Centering the gradient on the centered text removes that dependency entirely, structurally,
1043
+ rather than nudging a token and hoping the margin holds. Both stops stay tokens, so this is
1044
+ still theme-correct for free — a hardcoded gray gradient would not adapt to dark mode.
1045
+ */
1046
+ background: radial-gradient(circle, var(--cv-surface), var(--cv-surface-2));
1047
+ /*
1048
+ Without this, the circle's own boundary can vanish: `.cv-item:hover` sets the ROW background to
1049
+ `--cv-surface` — the exact color the gradient above STARTS at (1.00 contrast ratio, i.e.
1050
+ identical) in both themes. `--cv-border-strong` is the same token `.cv-search`'s border already
1051
+ uses against this same surface a few rules up — a pairing already proven visible, not a new one.
1052
+ */
1053
+ border: 1px solid var(--cv-border-strong);
1054
+ display: flex;
1055
+ align-items: center;
1056
+ justify-content: center;
1057
+ color: var(--cv-muted);
1058
+ font-size: 0.78rem;
1059
+ font-weight: 700;
1060
+ flex-shrink: 0;
1061
+ }
1062
+ /*
1063
+ The Historial row's status chip — WHY a call needs colour at all: direction is already the icon's
1064
+ shape (an arrow in/out), but "did this connect" is not shape-able, and a flat grey circle made every
1065
+ row look identical at a glance. Small badge, not a row-wide fill (that mistake already got made once
1066
+ on the person filter — see its own comment): a 40px circle reads as a status glyph, the same way a
1067
+ colored icon-badge does anywhere else; a wide fill reads as decoration competing with the text next
1068
+ to it. `--danger-strong`/`--accent-strong` (not the plain base tokens) because the base tokens
1069
+ measure BELOW 3:1 non-text contrast against their own `-soft` fill — `--cv-danger` on
1070
+ `--cv-danger-soft` is 2.6:1; `--cv-danger-strong` is 4.47:1 light / 3.62:1 dark, measured.
1071
+ */
1072
+ .cv-item-avatar[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }
1073
+ .cv-item-avatar[data-tone='info'] { background: var(--cv-info-soft); color: var(--cv-info-strong); }
1074
+ .cv-item-avatar[data-tone='danger'] { background: var(--cv-danger-soft); color: var(--cv-danger-strong); }
1075
+ /* A button, reset to look like plain content — the row is a container, not a control. */
1076
+ .cv-item-main {
1077
+ flex: 1;
1078
+ min-width: 0;
1079
+ display: block;
1080
+ border: none;
1081
+ background: none;
1082
+ padding: 0;
1083
+ font: inherit;
1084
+ color: inherit;
1085
+ text-align: left;
1086
+ cursor: pointer;
1087
+ transition: transform 0.1s var(--cv-ease-out);
1088
+ }
1089
+ .cv-item-main:disabled { cursor: default; }
1090
+ .cv-item-main:not(:disabled):active { transform: scale(0.985); }
1091
+ .cv-item-name {
1092
+ display: block;
1093
+ font-weight: 600;
1094
+ font-size: 0.875rem;
1095
+ white-space: nowrap;
1096
+ overflow: hidden;
1097
+ text-overflow: ellipsis;
1098
+ }
1099
+ /*
1100
+ 14px bold does not clear WCAG's large-text bar (18.66px bold) — this text needs the full 4.5:1, not
1101
+ the 3:1 large-text minimum. `--cv-danger-strong`: 6.47:1 light / 6.46:1 dark against `--cv-bg`,
1102
+ measured; the plain `--cv-danger` this used to be was only 3.76:1 light — a real (if marginal) fail.
1103
+ */
1104
+ .cv-item-name[data-tone='danger'] { color: var(--cv-danger-strong); }
1105
+ .cv-item-sub {
1106
+ display: block;
1107
+ font-size: 0.72rem;
1108
+ color: var(--cv-muted);
1109
+ margin-top: 1px;
1110
+ font-variant-numeric: tabular-nums;
1111
+ }
1112
+ .cv-item-sub-outcome { color: var(--cv-danger-strong); }
1113
+ /*
1114
+ Decorative reinforcement, not the sole carrier — the row's own play button is what says "this call
1115
+ has a recording" (it only renders when one exists); this is a small hint IN the metadata line that
1116
+ the button is worth pressing, so `--cv-faint` (2.5:1, documented above as decorative-only) is the
1117
+ right tier here, same reasoning `.cv-empty-icon`/`.cv-gate-mark` already use for icons next to real
1118
+ text rather than standing alone.
1119
+
1120
+ `display: inline-flex` is load-bearing, not decoration — every OTHER inline icon in this file gets
1121
+ an explicit `display` for the same reason: `/react` + `/ui` render straight into the host's light
1122
+ DOM (no shadow root), so a host reset like Tailwind Preflight's `svg { display: block }` reaches
1123
+ this element directly. Without an explicit override here, that reset knocks the icon onto its own
1124
+ line instead of trailing the duration text it is meant to sit beside.
1125
+ */
1126
+ .cv-item-sub-rec {
1127
+ display: inline-flex;
1128
+ color: var(--cv-faint);
1129
+ margin-left: 0.3rem;
1130
+ vertical-align: -1px;
1131
+ }
1132
+ .cv-item-action {
1133
+ width: 34px;
1134
+ height: 34px;
1135
+ border-radius: 999px;
1136
+ border: none;
1137
+ background: none;
1138
+ color: var(--cv-muted);
1139
+ cursor: pointer;
1140
+ display: inline-flex;
1141
+ align-items: center;
1142
+ justify-content: center;
1143
+ flex-shrink: 0;
1144
+ transition: background 0.12s ease, color 0.12s ease, transform 0.1s var(--cv-ease-out);
1145
+ }
1146
+ .cv-item-action:hover { background: var(--cv-accent); color: #fff; }
1147
+ .cv-item-action:active { transform: scale(0.9); }
1148
+ /*
1149
+ The call action specifically (ContactsTab) — NOT History's "play recording", which shares this
1150
+ same class and must stay neutral; a play button has no reason to be accent-colored. Scoped with
1151
+ `data-tone`, the same modifier convention `.cv-ctrl[data-tone]` already uses elsewhere in this file.
1152
+
1153
+ Coloured at REST, not just on hover: a hover-only reveal never fires on a touchscreen, so the one
1154
+ affordance that most needs to read as "you can tap this" would be invisible on exactly the devices
1155
+ a phone widget is likeliest to run on. `accent-strong` is already verified elsewhere in this file
1156
+ as AA on a surface; `accent-soft` is the same hover tint `.cv-ctrl[data-tone='accent']` uses, just
1157
+ transparent at rest so fifty rows of solid fill never turns the list into a wall of green.
1158
+ */
1159
+ .cv-item-action[data-tone='accent'] { color: var(--cv-accent-strong); }
1160
+ .cv-item-action[data-tone='accent']:hover { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }
1161
+
1162
+ @media (prefers-reduced-motion: reduce) {
1163
+ .cv-item-main, .cv-item-action, .cv-search-clear { transition: none; }
1164
+ }
1165
+
1166
+ .cv-empty {
1167
+ display: flex;
1168
+ flex-direction: column;
1169
+ align-items: center;
1170
+ justify-content: center;
1171
+ height: 100%;
1172
+ color: var(--cv-muted);
1173
+ text-align: center;
1174
+ padding: 2.5rem 1.25rem;
1175
+ font-size: 0.82rem;
1176
+ }
1177
+ .cv-empty-icon {
1178
+ display: flex;
1179
+ align-items: center;
1180
+ justify-content: center;
1181
+ width: 48px;
1182
+ height: 48px;
1183
+ border-radius: 999px;
1184
+ background: var(--cv-surface);
1185
+ color: var(--cv-faint);
1186
+ margin-bottom: 0.75rem;
1187
+ }
1188
+ .cv-empty-title { margin: 0; font-size: 0.85rem; font-weight: 600; color: var(--cv-muted); }
1189
+ /*
1190
+ `--cv-muted`, NOT `--cv-faint` — this file documents faint as "decorative only... never
1191
+ information" (2.5:1, below AA), and this line IS information: it is the reason the list is
1192
+ empty, sometimes echoing the user's own search back to them. `--cv-faint` here measured 2.54:1
1193
+ light / 3.70:1 dark against a 4.5:1 bar — a real failure, not a rounding difference.
1194
+ */
1195
+ .cv-empty-sub { margin: 0.2rem 0 0; font-size: 0.75rem; color: var(--cv-muted); }
1196
+ .cv-empty-spinner {
1197
+ margin-bottom: 0.75rem;
1198
+ animation: cv-spin 0.8s linear infinite;
1199
+ /* A loading spinner's rotation IS the information, with no static equivalent — unlike this
1200
+ file's other motion, it stays exempt from reduced-motion rather than freezing into a shape
1201
+ that would just look stuck. Colour comes from `.cv-empty`'s own `color: var(--cv-muted)` —
1202
+ the spinner no longer sets its own, now that it sits next to real text instead of standing
1203
+ alone: the icon is decorative reinforcement, same as `.cv-empty-icon`, not the sole carrier. */
1204
+ }
1205
+ @keyframes cv-spin {
1206
+ to { transform: rotate(360deg); }
1207
+ }
1208
+
1209
+ /* The scroll-triggered "load more" marker — see `useInfiniteList`. Never visible itself; a small
1210
+ height so it has a real box for the `IntersectionObserver` to track. */
1211
+ .cv-list-sentinel {
1212
+ height: 1px;
1213
+ }
1214
+ .cv-list-loading-more {
1215
+ display: flex;
1216
+ align-items: center;
1217
+ justify-content: center;
1218
+ padding: 0.75rem 0 0.25rem;
1219
+ color: var(--cv-muted);
1220
+ }
1221
+ /* Exempt from reduced-motion for the same reason `.cv-empty-spinner` is — see its own comment. */
1222
+ .cv-list-loading-more-spinner {
1223
+ animation: cv-spin 0.8s linear infinite;
1224
+ }
1225
+
1226
+ /* ── Contact detail (overlays the list) ──────────────────────────────────────── */
1227
+ .cv-detail {
1228
+ position: absolute;
1229
+ inset: 0;
1230
+ /* Above the list's own sticky day-group header (`.cv-history-day`, z-index: 1) — otherwise
1231
+ that header's z-index wins the stacking fight wherever its unscrolled flow position happens
1232
+ to land on top of this overlay, and it paints through despite both having an opaque bg. */
1233
+ z-index: 2;
1234
+ display: flex;
1235
+ flex-direction: column;
1236
+ background: var(--cv-bg);
1237
+ animation: cv-detail-in 220ms var(--cv-ease-out) both;
1238
+ }
1239
+ @keyframes cv-detail-in {
1240
+ from { opacity: 0; transform: translateX(10px); }
1241
+ to { opacity: 1; transform: none; }
1242
+ }
1243
+ @media (prefers-reduced-motion: reduce) {
1244
+ .cv-detail { animation: none; }
1245
+ }
1246
+
1247
+ .cv-detail-head { flex-shrink: 0; padding: 0.6rem 0.5rem 0; }
1248
+ .cv-detail-back {
1249
+ display: inline-flex;
1250
+ align-items: center;
1251
+ gap: 0.3rem;
1252
+ padding: 0.4rem 0.5rem;
1253
+ border: none;
1254
+ border-radius: 8px;
1255
+ background: none;
1256
+ color: var(--cv-muted);
1257
+ font: 600 0.82rem var(--cv-font);
1258
+ cursor: pointer;
1259
+ transition: color 0.12s ease, transform 0.1s var(--cv-ease-out);
1260
+ }
1261
+ .cv-detail-back:hover { color: var(--cv-text); }
1262
+ .cv-detail-back:active { transform: scale(0.97); }
1263
+
1264
+ .cv-detail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 1.25rem; }
1265
+ .cv-detail-identity {
1266
+ display: flex;
1267
+ flex-direction: column;
1268
+ align-items: center;
1269
+ text-align: center;
1270
+ padding: 0.75rem 0 1.5rem;
1271
+ }
1272
+ .cv-detail-avatar {
1273
+ width: 72px;
1274
+ height: 72px;
1275
+ border-radius: 999px;
1276
+ /* Same radial + border treatment as `.cv-item-avatar` (see its own comment), just larger. Neutral
1277
+ by default — a contact has no "status" to colour. `HistoryDetail` overrides both with `[data-tone]`
1278
+ below: a call DOES have one, the same one the list row already shows, so the hero avatar staying
1279
+ grey there would be information the list already gave up on repeating here. */
1280
+ background: radial-gradient(circle, var(--cv-surface), var(--cv-surface-2));
1281
+ border: 1px solid var(--cv-border-strong);
1282
+ display: flex;
1283
+ align-items: center;
1284
+ justify-content: center;
1285
+ color: var(--cv-muted);
1286
+ font-size: 1.35rem;
1287
+ font-weight: 700;
1288
+ margin-bottom: 0.85rem;
1289
+ }
1290
+ /* Same token pairs already verified for `.cv-item-avatar[data-tone]` — size doesn't change a contrast
1291
+ ratio, so no new numbers to measure here. */
1292
+ .cv-detail-avatar[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); border-color: transparent; }
1293
+ .cv-detail-avatar[data-tone='info'] { background: var(--cv-info-soft); color: var(--cv-info-strong); border-color: transparent; }
1294
+ .cv-detail-avatar[data-tone='danger'] { background: var(--cv-danger-soft); color: var(--cv-danger-strong); border-color: transparent; }
1295
+ .cv-detail-name {
1296
+ margin: 0;
1297
+ font-size: 1.1rem;
1298
+ font-weight: 700;
1299
+ color: var(--cv-text);
1300
+ letter-spacing: -0.01em;
1301
+ }
1302
+ .cv-detail-name[data-tone='danger'] { color: var(--cv-danger-strong); }
1303
+ .cv-detail-meta {
1304
+ margin: 0.2rem 0 0;
1305
+ font-size: 0.78rem;
1306
+ color: var(--cv-muted);
1307
+ }
1308
+
1309
+ /* ── Call detail: recording ───────────────────────────────────────────────── */
1310
+ .cv-detail-recording { margin-bottom: 1rem; }
1311
+ .cv-detail-recording-label {
1312
+ margin: 0 0 0.4rem;
1313
+ padding: 0 0.15rem;
1314
+ font-size: 0.68rem;
1315
+ font-weight: 600;
1316
+ text-transform: uppercase;
1317
+ letter-spacing: 0.03em;
1318
+ color: var(--cv-muted);
1319
+ }
1320
+ .cv-detail-audio { width: 100%; display: block; }
1321
+ .cv-detail-audio + .cv-detail-audio { margin-top: 0.5rem; }
1322
+ .cv-detail-recording-status {
1323
+ display: flex;
1324
+ align-items: center;
1325
+ gap: 0.5rem;
1326
+ margin: 0 0 1rem;
1327
+ padding: 0.65rem 0.75rem;
1328
+ border-radius: 12px;
1329
+ background: var(--cv-surface);
1330
+ color: var(--cv-muted);
1331
+ font-size: 0.78rem;
1332
+ }
1333
+ /*
1334
+ Text colour only, same neutral `--cv-surface` background the other states use — NOT a
1335
+ `--cv-danger-soft` fill. Measured first, not assumed: `--cv-danger-strong` on `--cv-danger-soft`
1336
+ is 3.34:1 light / 3.62:1 dark, well under the 4.5:1 this 0.78rem text needs (it only ever cleared
1337
+ the SEPARATE 3:1 non-text bar this token was built for — an icon on that badge, not body text on
1338
+ it). Same lesson the person-filter fix already paid for: a badge-strength pairing that works for an
1339
+ icon is not automatically safe for text.
1340
+ */
1341
+ .cv-detail-recording-status--warn { color: var(--cv-danger-strong); }
1342
+ .cv-detail-recording-spinner { animation: cv-spin 0.8s linear infinite; flex-shrink: 0; }
1343
+
1344
+ /* ── Call detail: who handled it ──────────────────────────────────────────── */
1345
+ .cv-detail-handlers { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
1346
+ .cv-detail-handler-hop { display: inline-flex; align-items: center; gap: 0.3rem; }
1347
+ .cv-detail-handler-arrow { color: var(--cv-faint); flex-shrink: 0; }
1348
+ .cv-detail-handler-chip {
1349
+ display: inline-flex;
1350
+ align-items: center;
1351
+ gap: 0.3rem;
1352
+ padding: 0.15rem 0.5rem;
1353
+ border-radius: 999px;
1354
+ background: var(--cv-surface-2);
1355
+ color: var(--cv-text);
1356
+ font-size: 0.75rem;
1357
+ font-weight: 600;
1358
+ }
1359
+ /* The LAST hop — the one the call actually ended with — is the one worth a glance first. */
1360
+ .cv-detail-handler-chip[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }
1361
+ .cv-detail-handler-ext {
1362
+ font-weight: 400;
1363
+ font-variant-numeric: tabular-nums;
1364
+ opacity: 0.75;
1365
+ }
1366
+
1367
+ .cv-detail-rows {
1368
+ border: 1px solid var(--cv-border);
1369
+ border-radius: 14px;
1370
+ overflow: hidden;
1371
+ background: var(--cv-bg);
1372
+ }
1373
+ .cv-detail-row {
1374
+ display: flex;
1375
+ align-items: center;
1376
+ gap: 0.7rem;
1377
+ width: 100%;
1378
+ padding: 0.7rem 0.85rem;
1379
+ border: none;
1380
+ border-top: 1px solid var(--cv-border);
1381
+ background: none;
1382
+ font: inherit;
1383
+ color: inherit;
1384
+ text-align: left;
1385
+ }
1386
+ .cv-detail-row:first-child { border-top: none; }
1387
+ .cv-detail-row--action {
1388
+ cursor: pointer;
1389
+ transition: background 0.12s ease;
1390
+ }
1391
+ .cv-detail-row--action:hover { background: var(--cv-surface); }
1392
+ .cv-detail-row-icon {
1393
+ display: flex;
1394
+ align-items: center;
1395
+ justify-content: center;
1396
+ width: 32px;
1397
+ height: 32px;
1398
+ border-radius: 999px;
1399
+ background: var(--cv-surface-2);
1400
+ /*
1401
+ Without this the circle nearly vanishes into its own row: `--cv-surface-2` measures only ~1.1:1
1402
+ against the row's rest-state background and ~1.05:1 against `--cv-surface`, the color
1403
+ `.cv-detail-row--action:hover` sets the ROW to — both far below a readable boundary, and worse
1404
+ than `.cv-item-avatar`/`.cv-detail-avatar` above even manage WITH their border (1.18:1 light /
1405
+ 1.57:1 dark). Same token, same fix, same reason.
1406
+ */
1407
+ border: 1px solid var(--cv-border-strong);
1408
+ color: var(--cv-muted);
1409
+ flex-shrink: 0;
1410
+ }
1411
+ .cv-detail-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
1412
+ .cv-detail-row-eyebrow {
1413
+ font-size: 0.68rem;
1414
+ font-weight: 600;
1415
+ text-transform: uppercase;
1416
+ letter-spacing: 0.03em;
1417
+ color: var(--cv-muted);
1418
+ }
1419
+ .cv-detail-row-value {
1420
+ font-size: 0.85rem;
1421
+ font-weight: 500;
1422
+ color: var(--cv-text);
1423
+ white-space: nowrap;
1424
+ overflow: hidden;
1425
+ text-overflow: ellipsis;
1426
+ }
1427
+ .cv-detail-row-chevron { color: var(--cv-faint); flex-shrink: 0; }
1428
+
1429
+ .cv-detail-foot { flex-shrink: 0; padding: 0.85rem 1.25rem 1.1rem; }
1430
+ .cv-detail-call { width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.4rem; }
1431
+
1432
+ /* ── Settings tab (device selection, SPEC §7 Phase 2) ──────────────────────────── */
1433
+ .cv-settings-body {
1434
+ flex: 1;
1435
+ min-height: 0;
1436
+ overflow-y: auto;
1437
+ padding: 1rem;
1438
+ display: flex;
1439
+ flex-direction: column;
1440
+ /* Generous separation BETWEEN sections, deliberately larger than any gap inside one — grouping
1441
+ only reads as grouping if the space between groups is clearly bigger than the space within one
1442
+ (see impeccable's layout guidance: "tight grouping, generous separation"). */
1443
+ gap: 1.5rem;
1444
+ }
1445
+
1446
+ /*
1447
+ One labeled group (`SettingsSection`): a title, and a bordered body holding its rows. Ajustes
1448
+ started as a flat list of two rows with nothing to say they were related — fine for exactly two
1449
+ rows, unreadable the moment an unrelated third setting lands with nothing to tell it apart from
1450
+ the device rows above it.
1451
+
1452
+ The body is ONE card, not a card per row: `.cv-settings-row`/`.cv-settings-note` below carry no
1453
+ border or background of their own, only a divider between siblings — the same shape
1454
+ `.cv-detail-rows`/`.cv-detail-row` already use elsewhere in this file, and deliberately not a
1455
+ stack of individually-bordered cards (nested-card territory).
1456
+ */
1457
+ .cv-settings-section { display: flex; flex-direction: column; gap: 0.5rem; }
1458
+ .cv-settings-section-title {
1459
+ margin: 0;
1460
+ padding: 0 0.2rem;
1461
+ font-size: 0.7rem;
1462
+ font-weight: 700;
1463
+ text-transform: uppercase;
1464
+ letter-spacing: 0.04em;
1465
+ color: var(--cv-muted);
1466
+ }
1467
+ .cv-settings-section-body {
1468
+ border: 1px solid var(--cv-border);
1469
+ border-radius: 14px;
1470
+ background: var(--cv-bg);
1471
+ overflow: hidden;
1472
+ }
1473
+ .cv-settings-section-body > * { padding: 0.75rem 0.85rem; }
1474
+ .cv-settings-section-body > *:not(:first-child) { border-top: 1px solid var(--cv-border); }
1475
+
1476
+ .cv-settings-row { display: flex; align-items: center; gap: 0.7rem; }
1477
+ /* Same treatment as `.cv-detail-row-icon`, same reason — see its own comment on why the border is
1478
+ load-bearing: `--cv-surface-2` alone nearly vanishes into the section's own background. */
1479
+ .cv-settings-row-icon {
1480
+ display: flex;
1481
+ align-items: center;
1482
+ justify-content: center;
1483
+ width: 32px;
1484
+ height: 32px;
1485
+ border-radius: 999px;
1486
+ background: var(--cv-surface-2);
1487
+ border: 1px solid var(--cv-border-strong);
1488
+ color: var(--cv-muted);
1489
+ flex-shrink: 0;
1490
+ }
1491
+ .cv-settings-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.35rem; }
1492
+ /* Same pairing as `.cv-detail-row-eyebrow` (`--cv-muted` on `--cv-bg`), verified there already. */
1493
+ .cv-settings-row-label {
1494
+ font-size: 0.68rem;
1495
+ font-weight: 600;
1496
+ text-transform: uppercase;
1497
+ letter-spacing: 0.03em;
1498
+ color: var(--cv-muted);
1499
+ }
1500
+ .cv-settings-select {
1501
+ width: 100%;
1502
+ padding: 0.45rem 0.6rem;
1503
+ border-radius: 8px;
1504
+ border: 1px solid var(--cv-border-strong);
1505
+ background: var(--cv-surface);
1506
+ color: var(--cv-text);
1507
+ font: inherit;
1508
+ font-size: 0.85rem;
1509
+ transition: border-color 0.15s ease, background 0.15s ease;
1510
+ }
1511
+ .cv-settings-select:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }
1512
+ .cv-settings-empty { font-size: 0.85rem; color: var(--cv-muted); }
1513
+ /* `--cv-muted` on `--cv-bg` — 4.83:1 light / 6.97:1 dark, computed fresh for THIS pairing (the note
1514
+ used to sit on `--cv-surface` with its own card; now it sits directly on the section's `--cv-bg`,
1515
+ a different background that earns its own check rather than inheriting the old one's number). */
1516
+ .cv-settings-note {
1517
+ margin: 0;
1518
+ color: var(--cv-muted);
1519
+ font-size: 0.8rem;
1520
+ line-height: 1.5;
1521
+ }
1522
+ /* Closing note for the tab as a whole, not any one section — sits directly on `.cv-settings-body`,
1523
+ which shares the same `--cv-bg` as `.cv-settings-section-body` above, so the `--cv-muted`-on-
1524
+ `--cv-bg` pairing already verified for `.cv-settings-note` carries over unchanged. Centred and
1525
+ quieter than a row: this is a footnote about the tab, not a row's worth of content. */
1526
+ .cv-settings-more {
1527
+ margin: 0;
1528
+ padding: 0 0.5rem;
1529
+ text-align: center;
1530
+ color: var(--cv-muted);
1531
+ font-size: 0.78rem;
1532
+ line-height: 1.5;
1533
+ }
1534
+
1535
+ /* ── Centred states (mic gate) ───────────────────────────────────────────────── */
1536
+ .cv-center {
1537
+ flex: 1;
1538
+ display: flex;
1539
+ flex-direction: column;
1540
+ align-items: center;
1541
+ justify-content: center;
1542
+ gap: 0.85rem;
1543
+ padding: 1.75rem;
1544
+ text-align: center;
1545
+ }
1546
+ .cv-center p { color: var(--cv-muted); margin: 0; font-size: 0.85rem; line-height: 1.5; }
1547
+
1548
+
1549
+ /* "Cómo permitirlo" — a native <details> disclosure.
1550
+ Collapsed by default: the people who already know where the padlock is should not have to read
1551
+ past three steps to get there, and the people who don't get them without leaving the app. */
1552
+ .cv-help {
1553
+ width: 100%;
1554
+ margin-top: 0.15rem;
1555
+ text-align: left;
1556
+ }
1557
+ .cv-help summary {
1558
+ display: inline-flex;
1559
+ align-items: center;
1560
+ gap: 0.3rem;
1561
+ justify-content: center;
1562
+ width: 100%;
1563
+ padding: 0.3rem;
1564
+ border-radius: 8px;
1565
+ color: var(--cv-muted);
1566
+ font: 500 0.75rem var(--cv-font);
1567
+ cursor: pointer;
1568
+ list-style: none; /* hide the default triangle; the icon is the affordance */
1569
+ transition: color 0.15s ease, background 0.15s ease;
1570
+ }
1571
+ .cv-help summary::-webkit-details-marker { display: none; }
1572
+ .cv-help summary:hover { color: var(--cv-text); background: var(--cv-surface-2); }
1573
+ .cv-help[open] summary { color: var(--cv-text); margin-bottom: 0.35rem; }
1574
+
1575
+ .cv-help ol {
1576
+ margin: 0;
1577
+ padding: 0.6rem 0.7rem 0.6rem 1.6rem;
1578
+ background: var(--cv-surface);
1579
+ border-radius: 10px;
1580
+ color: var(--cv-muted);
1581
+ font-size: 0.73rem;
1582
+ line-height: 1.55;
1583
+ }
1584
+ .cv-help li + li { margin-top: 0.3rem; }
1585
+ .cv-help li::marker { color: var(--cv-faint); font-variant-numeric: tabular-nums; }
1586
+
1587
+ .cv-help a {
1588
+ display: inline-flex;
1589
+ align-items: center;
1590
+ gap: 0.25rem;
1591
+ margin-top: 0.45rem;
1592
+ padding-left: 0.1rem;
1593
+ color: var(--cv-accent-strong);
1594
+ font-size: 0.73rem;
1595
+ font-weight: 500;
1596
+ text-decoration: none;
1597
+ }
1598
+ .cv-help a:hover { text-decoration: underline; }
1599
+
1600
+ /* The panel is the only thing that moves, and only when opened by hand — so it animates. */
1601
+ .cv-help[open] ol {
1602
+ animation: cv-gate-in 200ms var(--cv-ease-out) both;
1603
+ }
1604
+ @media (prefers-reduced-motion: reduce) {
1605
+ .cv-help[open] ol { animation: none; }
1606
+ }
1607
+
1608
+ /* ── The permission gate ─────────────────────────────────────────────────────
1609
+ Four states, and only ONE of them deserves a designed screen.
1610
+
1611
+ `checking` resolves in ~100–400 ms. Rendering it immediately means the user
1612
+ sees text appear and vanish inside a blink — a flash reads as jank, not as
1613
+ information. So it is delayed 450 ms with `both` fill: invisible during the
1614
+ delay, and if the check finishes first (the usual case, and always the case
1615
+ when permission is already granted) it is never painted at all.
1616
+
1617
+ `prompting` is the one the user actually reads and acts on. It gets the
1618
+ hierarchy: mark, title, reason, action.
1619
+ ─────────────────────────────────────────────────────────────────────────── */
1620
+ .cv-gate {
1621
+ flex: 1;
1622
+ min-height: 0;
1623
+ display: flex;
1624
+ flex-direction: column;
1625
+ align-items: center;
1626
+ justify-content: center;
1627
+ gap: 0.6rem;
1628
+ padding: 1.5rem 1.75rem;
1629
+ text-align: center;
1630
+ }
1631
+
1632
+ /* The disc echoes the call avatar — one visual language, not two. */
1633
+ .cv-gate-mark {
1634
+ display: grid;
1635
+ place-items: center;
1636
+ width: 52px;
1637
+ height: 52px;
1638
+ border-radius: 999px;
1639
+ background: var(--cv-accent-soft);
1640
+ color: var(--cv-accent-strong);
1641
+ margin-bottom: 0.15rem;
1642
+ }
1643
+ .cv-gate[data-tone='danger'] .cv-gate-mark {
1644
+ background: var(--cv-danger-soft);
1645
+ color: var(--cv-danger);
1646
+ }
1647
+
1648
+ .cv-gate-title {
1649
+ margin: 0;
1650
+ font: 600 0.95rem var(--cv-font);
1651
+ color: var(--cv-text);
1652
+ letter-spacing: -0.01em;
1653
+ }
1654
+ .cv-gate p {
1655
+ margin: 0;
1656
+ color: var(--cv-muted);
1657
+ font-size: 0.8rem;
1658
+ line-height: 1.5;
1659
+ max-width: 24ch; /* two comfortable lines, not one long one */
1660
+ }
1661
+ .cv-gate .cv-btn { margin-top: 0.5rem; }
1662
+
1663
+ /* Entrance. Staggered 45 ms apart: the eye follows mark → title → reason →
1664
+ action, which is the order it needs to read them in anyway. */
1665
+ .cv-gate > * {
1666
+ animation: cv-gate-in 260ms var(--cv-ease-out) both;
1667
+ }
1668
+ .cv-gate > *:nth-child(2) { animation-delay: 45ms; }
1669
+ .cv-gate > *:nth-child(3) { animation-delay: 90ms; }
1670
+ .cv-gate > *:nth-child(4) { animation-delay: 135ms; }
1671
+
1672
+ @keyframes cv-gate-in {
1673
+ from { opacity: 0; transform: translateY(6px); }
1674
+ to { opacity: 1; transform: none; }
1675
+ }
1676
+
1677
+ /* The checking state: held back, then a quiet fade. No stagger, no movement —
1678
+ it is a status, not a screen, and it should barely register. */
1679
+ .cv-gate[data-state='checking'] > *,
1680
+ .cv-gate[data-state='warming'] > * {
1681
+ animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both;
1682
+ }
1683
+ @keyframes cv-gate-wait {
1684
+ from { opacity: 0; }
1685
+ to { opacity: 1; }
1686
+ }
1687
+ /* A slow breath rather than a spinner: a spinner promises a long wait, and this
1688
+ is not one. */
1689
+ .cv-gate[data-state='checking'] .cv-gate-mark,
1690
+ .cv-gate[data-state='warming'] .cv-gate-mark {
1691
+ background: var(--cv-surface-2);
1692
+ color: var(--cv-muted);
1693
+ animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both,
1694
+ cv-breathe 1.8s ease-in-out 450ms infinite;
1695
+ }
1696
+ @keyframes cv-breathe {
1697
+ 0%, 100% { opacity: 1; }
1698
+ 50% { opacity: 0.45; }
1699
+ }
1700
+
1701
+ /* Reduced motion: keep the fade and — critically — keep the DELAY, which is
1702
+ timing rather than motion and is the whole point. Drop the travel. */
1703
+ @media (prefers-reduced-motion: reduce) {
1704
+ .cv-gate > * {
1705
+ animation: cv-gate-wait 200ms var(--cv-ease-out) both;
1706
+ }
1707
+ .cv-gate[data-state='checking'] .cv-gate-mark,
1708
+ .cv-gate[data-state='warming'] .cv-gate-mark {
1709
+ animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both;
1710
+ }
1711
+ }
1712
+
1713
+ @media (prefers-reduced-motion: reduce) {
1714
+ .cv-avatar-ping,
1715
+ .cv-timer i { animation: none; }
1716
+ .cv-softphone *,
1717
+ .cv-softphone *::before { transition-duration: 0.01ms !important; }
1718
+ }
1719
+
1720
+ /* `*` and `#` stay in the grid — a keypad without them is not a keypad — but they are not part of a
1721
+ number (toE164 keeps digits only). Dimmed rather than `disabled` so the grid still reads as a
1722
+ keypad and the glyphs stay legible; they become live DTMF on the in-call screen. */
1723
+ .cv-key[data-dialable='false'] {
1724
+ cursor: default;
1725
+ }
1726
+ .cv-key[data-dialable='false'] b {
1727
+ color: var(--cv-faint);
1728
+ }
1729
+ .cv-key[data-dialable='false']:hover {
1730
+ background: none;
1731
+ }
1732
+ .cv-key[data-dialable='false']:active {
1733
+ transform: none;
1734
+ }
1735
+
1736
+ /* ── Country picker ──────────────────────────────────────────────────────────
1737
+ The prefix chip + its list. Sits where the old left spacer was, so the number
1738
+ stays optically centred against the 36px erase button on the right.
1739
+
1740
+ The popover is absolutely positioned inside the widget rather than portalled:
1741
+ there is nowhere to portal TO inside a shadow root, and the widget already owns
1742
+ its stacking context. That also means it cannot be clipped by a host's
1743
+ `overflow:hidden` — the failure SPEC §2.6 lists for the CRM's Radix version.
1744
+ ──────────────────────────────────────────────────────────────────────────── */
1745
+ /* The anchor spans the whole ROW, not just the chip.
1746
+ With the chip centred, anchoring to the chip put the popover's left edge at ~120px and its right
1747
+ edge past the widget — clipped by `.cv-softphone { overflow: hidden }`, which is the classic
1748
+ absolute-inside-overflow bug. Anchoring to the full row makes overflowing arithmetically
1749
+ impossible: `left:0; right:0` cannot be wider than the row that contains it. */
1750
+ .cv-cp {
1751
+ position: relative;
1752
+ display: flex;
1753
+ justify-content: center;
1754
+ width: 100%;
1755
+ }
1756
+ /* A PILL, with a resting surface and a border.
1757
+ This was `border: none; background: none` — invisible until hover, so it read as a label rather
1758
+ than a control. That was survivable when it sat inline beside the number; alone on its own row it
1759
+ is just floating text, and nothing says "you can change this". A control that changes state has to
1760
+ look like one at rest, not only under a cursor that may never arrive (touch has no hover at all). */
1761
+ .cv-cp-chip {
1762
+ display: inline-flex;
1763
+ align-items: center;
1764
+ gap: 0.3rem;
1765
+ height: 30px;
1766
+ padding: 0 0.5rem 0 0.4rem;
1767
+ border: 1px solid var(--cv-border-strong);
1768
+ border-radius: 999px;
1769
+ background: var(--cv-surface);
1770
+ color: var(--cv-text);
1771
+ font: 600 0.8rem var(--cv-font);
1772
+ cursor: pointer;
1773
+ transition: background 0.15s, border-color 0.15s;
1774
+ }
1775
+ .cv-cp-chip:hover { background: var(--cv-surface-2); }
1776
+ .cv-cp-chip:active { transform: scale(0.95); }
1777
+ /* Open: the chip is the thing the list belongs to, so it stays visibly engaged while the list is up. */
1778
+ .cv-cp-chip[aria-expanded='true'] {
1779
+ background: var(--cv-accent-soft);
1780
+ border-color: var(--cv-accent);
1781
+ }
1782
+ .cv-cp-dial {
1783
+ font-variant-numeric: tabular-nums;
1784
+ letter-spacing: -0.02em;
1785
+ }
1786
+ .cv-cp-caret {
1787
+ color: var(--cv-faint);
1788
+ flex-shrink: 0;
1789
+ transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1);
1790
+ }
1791
+ .cv-cp-caret[data-open='true'] { transform: rotate(180deg); }
1792
+
1793
+ /* The country mark. TWO designed states in ONE 20×14 box (a flag's own 3:2), so the chip and every
1794
+ row keep their rhythm whichever is showing — nothing reflows when the flags land. The plate is the
1795
+ LOADING state (flags are a lazy import) and the offline state, not a fallback: there is no flash of
1796
+ something broken, only a designed plate becoming a designed flag, once per session.
1797
+ `::part(country-mark)` lets a host restyle either. */
1798
+ .cv-mark {
1799
+ width: 20px;
1800
+ height: 14px;
1801
+ flex-shrink: 0;
1802
+ display: inline-flex;
1803
+ align-items: center;
1804
+ justify-content: center;
1805
+ font-size: 14px;
1806
+ line-height: 1;
1807
+ font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', var(--cv-font);
1808
+ }
1809
+ /* The SVG fills the slot exactly; 3x2 is the flag aspect, so nothing is letterboxed or cropped. */
1810
+ .cv-mark-svg {
1811
+ width: 100%;
1812
+ height: 100%;
1813
+ display: block;
1814
+ border-radius: 2px;
1815
+ object-fit: cover;
1816
+ }
1817
+ .cv-mark[data-plate='true'] {
1818
+ /* The ISO code set into the slot a flag will occupy. */
1819
+ font-family: var(--cv-font);
1820
+ font-size: 8.5px;
1821
+ font-weight: 700;
1822
+ letter-spacing: 0.04em;
1823
+ color: var(--cv-text);
1824
+ background: var(--cv-surface-2);
1825
+ border: 1px solid var(--cv-border);
1826
+ border-radius: 3px;
1827
+ /* Cap-height letters sit optically high in a 14px box; nudge them back to centre. */
1828
+ padding-top: 1px;
1829
+ }
1830
+
1831
+ .cv-cp-pop {
1832
+ position: absolute;
1833
+ top: calc(100% + 6px);
1834
+ /* Pinned to BOTH row edges, so it is bounded by the container by construction — no width guess,
1835
+ no media query, nothing to clip. `max-width` keeps it from looking stretched on a wide host. */
1836
+ left: 0;
1837
+ right: 0;
1838
+ margin-inline: auto;
1839
+ max-width: 260px;
1840
+ z-index: 20;
1841
+ display: flex;
1842
+ flex-direction: column;
1843
+ background: var(--cv-surface);
1844
+ border: 1px solid var(--cv-border);
1845
+ border-radius: var(--cv-radius);
1846
+ box-shadow: var(--cv-shadow);
1847
+ overflow: hidden;
1848
+ /* Grows from the chip it belongs to, rather than appearing on top of it. */
1849
+ transform-origin: top center;
1850
+ animation: cv-cp-in 0.14s cubic-bezier(0.2, 0, 0.2, 1);
1851
+ }
1852
+ @keyframes cv-cp-in {
1853
+ from { opacity: 0; transform: translateY(-4px) scale(0.97); }
1854
+ to { opacity: 1; transform: none; }
1855
+ }
1856
+ @media (prefers-reduced-motion: reduce) {
1857
+ .cv-cp-pop { animation: none; }
1858
+ .cv-cp-caret { transition: none; }
1859
+ }
1860
+ .cv-cp-search {
1861
+ flex-shrink: 0;
1862
+ padding: 0.5rem 0.65rem;
1863
+ border: none;
1864
+ border-bottom: 1px solid var(--cv-border);
1865
+ background: none;
1866
+ color: var(--cv-text);
1867
+ font: 400 0.8rem var(--cv-font);
1868
+ outline: none;
1869
+ }
1870
+ .cv-cp-search::placeholder { color: var(--cv-faint); }
1871
+ /* Both states named explicitly. In the CRM the widget mounts via `/ui`, i.e. in the LIGHT DOM — none
1872
+ of the shadow-DOM isolation the web-component playground proves applies there, so a host's focus
1873
+ ring (or the browser's own) can land on our input. The accent underline IS the focus indicator. */
1874
+ .cv-cp-search:focus,
1875
+ .cv-cp-search:focus-visible {
1876
+ outline: none;
1877
+ border-bottom-color: var(--cv-accent);
1878
+ border-bottom-width: 2px;
1879
+ padding-bottom: calc(0.5rem - 1px);
1880
+ }
1881
+ .cv-cp-list {
1882
+ overflow-y: auto;
1883
+ max-height: 220px;
1884
+ padding: 0.25rem;
1885
+ }
1886
+ .cv-cp-item {
1887
+ width: 100%;
1888
+ display: flex;
1889
+ align-items: center;
1890
+ gap: 0.5rem;
1891
+ padding: 0.4rem 0.45rem;
1892
+ border: none;
1893
+ border-radius: calc(var(--cv-radius) - 4px);
1894
+ background: none;
1895
+ color: var(--cv-text);
1896
+ font: 400 0.8rem var(--cv-font);
1897
+ text-align: left;
1898
+ cursor: pointer;
1899
+ }
1900
+ .cv-cp-item:hover { background: var(--cv-surface-2); }
1901
+ /* The current country is stated by the row, not only by a tick at the far right. */
1902
+ .cv-cp-item[aria-selected='true'] { background: var(--cv-accent-soft); }
1903
+ .cv-cp-item[aria-selected='true'] .cv-cp-name { font-weight: 600; }
1904
+ .cv-cp-name {
1905
+ flex: 1;
1906
+ min-width: 0;
1907
+ overflow: hidden;
1908
+ text-overflow: ellipsis;
1909
+ white-space: nowrap;
1910
+ }
1911
+ .cv-cp-item .cv-cp-dial {
1912
+ color: var(--cv-muted);
1913
+ font-weight: 400;
1914
+ }
1915
+ .cv-cp-check { color: var(--cv-accent-strong); flex-shrink: 0; }
1916
+ .cv-cp-check[data-on='false'] { opacity: 0; }
1917
+ .cv-cp-empty {
1918
+ margin: 0;
1919
+ padding: 1rem 0.5rem;
1920
+ text-align: center;
1921
+ color: var(--cv-muted);
1922
+ font-size: 0.78rem;
1923
+ }