@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.
- package/dist/3x2-2HQTJDKZ.js +6583 -0
- package/dist/3x2-2HQTJDKZ.js.map +1 -0
- package/dist/3x2-INKWPNLA.js +6585 -0
- package/dist/3x2-INKWPNLA.js.map +1 -0
- package/dist/call-BTYlT2SU.d.cts +680 -0
- package/dist/call-BTYlT2SU.d.ts +680 -0
- package/dist/chunk-2ZSOGGPO.js +169 -0
- package/dist/chunk-2ZSOGGPO.js.map +1 -0
- package/dist/chunk-3V2L6UTK.js +129 -0
- package/dist/chunk-3V2L6UTK.js.map +1 -0
- package/dist/chunk-5BGEHMPP.js +945 -0
- package/dist/chunk-5BGEHMPP.js.map +1 -0
- package/dist/chunk-5WRI5ZAA.js +29 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/chunk-KAXEMJ7E.js +11 -0
- package/dist/chunk-KAXEMJ7E.js.map +1 -0
- package/dist/chunk-MHOJHG5E.js +39 -0
- package/dist/chunk-MHOJHG5E.js.map +1 -0
- package/dist/chunk-MP7CSA24.js +299 -0
- package/dist/chunk-MP7CSA24.js.map +1 -0
- package/dist/chunk-N2N2EQQJ.js +29 -0
- package/dist/chunk-N2N2EQQJ.js.map +1 -0
- package/dist/element/index.d.ts +853 -0
- package/dist/element/index.js +9635 -0
- package/dist/element/index.js.map +1 -0
- package/dist/engine-D9nbeY08.d.cts +115 -0
- package/dist/engine-Doa6Brkm.d.ts +115 -0
- package/dist/index.cjs +2133 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +630 -0
- package/dist/index.js.map +1 -0
- package/dist/min-64GIMWWL.js +5456 -0
- package/dist/min-64GIMWWL.js.map +1 -0
- package/dist/min-ATXYR23R.js +5458 -0
- package/dist/min-ATXYR23R.js.map +1 -0
- package/dist/react/index.cjs +138 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +68 -0
- package/dist/react/index.d.ts +68 -0
- package/dist/react/index.js +4 -0
- package/dist/react/index.js.map +1 -0
- package/dist/softphone-DHmhotbf.d.ts +128 -0
- package/dist/softphone-DhuhcKCy.d.cts +128 -0
- package/dist/softphone.css +1923 -0
- package/dist/sources-N4BOEDRL.js +226 -0
- package/dist/sources-N4BOEDRL.js.map +1 -0
- package/dist/testing/index.cjs +345 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +66 -0
- package/dist/testing/index.d.ts +66 -0
- package/dist/testing/index.js +186 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/ui/index.cjs +15428 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +188 -0
- package/dist/ui/index.d.ts +188 -0
- package/dist/ui/index.js +2256 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/zadarma/index.cjs +1144 -0
- package/dist/zadarma/index.cjs.map +1 -0
- package/dist/zadarma/index.d.cts +86 -0
- package/dist/zadarma/index.d.ts +86 -0
- package/dist/zadarma/index.js +5 -0
- package/dist/zadarma/index.js.map +1 -0
- package/dist/zadarma-engine-DN2SZSQ3.js +5 -0
- package/dist/zadarma-engine-DN2SZSQ3.js.map +1 -0
- 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
|
+
}
|