@coventia/softphone 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/{call-VKq1I8yP.d.cts → call-C6YpT6uR.d.cts} +107 -1
  2. package/dist/{call-VKq1I8yP.d.ts → call-C6YpT6uR.d.ts} +107 -1
  3. package/dist/chunk-5E5OEXS2.js +14 -0
  4. package/dist/chunk-5E5OEXS2.js.map +1 -0
  5. package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
  6. package/dist/chunk-UHNLEBBO.js.map +1 -0
  7. package/dist/element/index.d.ts +124 -9
  8. package/dist/element/index.js +1837 -413
  9. package/dist/element/index.js.map +1 -1
  10. package/dist/{engine-BlQBo2Ys.d.cts → engine-CDdaSojx.d.cts} +1 -1
  11. package/dist/{engine-C0BOkIcW.d.ts → engine-DPft99Wj.d.ts} +1 -1
  12. package/dist/index.cjs +213 -13
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +16 -6
  15. package/dist/index.d.ts +16 -6
  16. package/dist/index.js +75 -10
  17. package/dist/index.js.map +1 -1
  18. package/dist/react/index.cjs +2 -0
  19. package/dist/react/index.cjs.map +1 -1
  20. package/dist/react/index.d.cts +2 -2
  21. package/dist/react/index.d.ts +2 -2
  22. package/dist/react/index.js +1 -1
  23. package/dist/{softphone-DlGV12Xp.d.cts → softphone-CY5d2WWY.d.cts} +28 -10
  24. package/dist/{softphone-Cyq3hiIM.d.ts → softphone-DXgmTyrX.d.ts} +28 -10
  25. package/dist/softphone.css +364 -35
  26. package/dist/{sources-IRKNQPWW.js → sources-ONYQW733.js} +125 -6
  27. package/dist/sources-ONYQW733.js.map +1 -0
  28. package/dist/testing/index.d.cts +2 -2
  29. package/dist/testing/index.d.ts +2 -2
  30. package/dist/ui/index.cjs +1450 -33
  31. package/dist/ui/index.cjs.map +1 -1
  32. package/dist/ui/index.d.cts +25 -3
  33. package/dist/ui/index.d.ts +25 -3
  34. package/dist/ui/index.js +1445 -36
  35. package/dist/ui/index.js.map +1 -1
  36. package/dist/zadarma/index.d.cts +2 -2
  37. package/dist/zadarma/index.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/chunk-LMJXX4YM.js.map +0 -1
  40. package/dist/sources-IRKNQPWW.js.map +0 -1
@@ -6931,7 +6931,7 @@ var require_jsx_runtime = __commonJS({
6931
6931
  });
6932
6932
 
6933
6933
  // src/element/index.ts
6934
- var import_react32 = __toESM(require_react(), 1);
6934
+ var import_react46 = __toESM(require_react(), 1);
6935
6935
  var import_client = __toESM(require_client(), 1);
6936
6936
 
6937
6937
  // src/react/context.tsx
@@ -6950,7 +6950,7 @@ function useSoftphone() {
6950
6950
  }
6951
6951
 
6952
6952
  // src/ui/SoftphoneWidget.tsx
6953
- var import_react30 = __toESM(require_react(), 1);
6953
+ var import_react44 = __toESM(require_react(), 1);
6954
6954
 
6955
6955
  // ../../node_modules/lucide-react/dist/esm/createLucideIcon.js
6956
6956
  var import_react3 = __toESM(require_react());
@@ -7043,6 +7043,14 @@ var ArrowRight = createLucideIcon("ArrowRight", [
7043
7043
  ["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
7044
7044
  ]);
7045
7045
 
7046
+ // ../../node_modules/lucide-react/dist/esm/icons/bell-ring.js
7047
+ var BellRing = createLucideIcon("BellRing", [
7048
+ ["path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9", key: "1qo2s2" }],
7049
+ ["path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0", key: "qgo35s" }],
7050
+ ["path", { d: "M4 2C2.8 3.7 2 5.7 2 8", key: "tap9e0" }],
7051
+ ["path", { d: "M22 8c0-2.3-.8-4.3-2-6", key: "5bb3ad" }]
7052
+ ]);
7053
+
7046
7054
  // ../../node_modules/lucide-react/dist/esm/icons/calendar-clock.js
7047
7055
  var CalendarClock = createLucideIcon("CalendarClock", [
7048
7056
  ["path", { d: "M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5", key: "1osxxc" }],
@@ -7157,6 +7165,13 @@ var MonitorSmartphone = createLucideIcon("MonitorSmartphone", [
7157
7165
  ["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2", key: "1egngj" }]
7158
7166
  ]);
7159
7167
 
7168
+ // ../../node_modules/lucide-react/dist/esm/icons/music.js
7169
+ var Music = createLucideIcon("Music", [
7170
+ ["path", { d: "M9 18V5l12-2v13", key: "1jmyc2" }],
7171
+ ["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
7172
+ ["circle", { cx: "18", cy: "16", r: "3", key: "1hluhg" }]
7173
+ ]);
7174
+
7160
7175
  // ../../node_modules/lucide-react/dist/esm/icons/pause.js
7161
7176
  var Pause = createLucideIcon("Pause", [
7162
7177
  ["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
@@ -7282,6 +7297,19 @@ var Users = createLucideIcon("Users", [
7282
7297
  ["path", { d: "M16 3.13a4 4 0 0 1 0 7.75", key: "1da9ce" }]
7283
7298
  ]);
7284
7299
 
7300
+ // ../../node_modules/lucide-react/dist/esm/icons/volume-2.js
7301
+ var Volume2 = createLucideIcon("Volume2", [
7302
+ [
7303
+ "path",
7304
+ {
7305
+ d: "M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z",
7306
+ key: "uqj9uw"
7307
+ }
7308
+ ],
7309
+ ["path", { d: "M16 9a5 5 0 0 1 0 6", key: "1q6k2b" }],
7310
+ ["path", { d: "M19.364 18.364a9 9 0 0 0 0-12.728", key: "ijwkga" }]
7311
+ ]);
7312
+
7285
7313
  // ../../node_modules/lucide-react/dist/esm/icons/x.js
7286
7314
  var X = createLucideIcon("X", [
7287
7315
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -7403,7 +7431,9 @@ function useDevices() {
7403
7431
  outputs: audio.outputs,
7404
7432
  currentInputId: audio.currentInputId,
7405
7433
  currentOutputId: audio.currentOutputId,
7434
+ currentRingtoneOutputId: audio.currentRingtoneOutputId,
7406
7435
  canSelectOutput: audio.canSelectOutput,
7436
+ canSelectRingtoneOutput: audio.canSelectRingtoneOutput,
7407
7437
  setInput: (deviceId) => audio.setInput(deviceId),
7408
7438
  setOutput: (deviceId) => audio.setOutput(deviceId),
7409
7439
  setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),
@@ -7416,7 +7446,7 @@ function useDevices() {
7416
7446
  }
7417
7447
 
7418
7448
  // src/ui/views/Dialpad.tsx
7419
- var import_react7 = __toESM(require_react(), 1);
7449
+ var import_react11 = __toESM(require_react(), 1);
7420
7450
 
7421
7451
  // src/ui/dialing/CountryPicker.tsx
7422
7452
  var import_react5 = __toESM(require_react(), 1);
@@ -7426,6 +7456,36 @@ var PACKED = "AC247 AD376 AE971 AF93 AG1 AI1 AL355 AM374 AO244 AR54 AS1 AT43 AU6
7426
7456
  var DIAL_CODES = new Map(
7427
7457
  PACKED.split(" ").map((token) => [token.slice(0, 2), Number(token.slice(2))])
7428
7458
  );
7459
+ var REGIONS_BY_CODE = (() => {
7460
+ const map = /* @__PURE__ */ new Map();
7461
+ for (const [region, code] of DIAL_CODES) map.set(code, [...map.get(code) ?? [], region]);
7462
+ return map;
7463
+ })();
7464
+ var MAIN_REGION = {
7465
+ 1: "US",
7466
+ 7: "RU",
7467
+ 39: "IT",
7468
+ 44: "GB",
7469
+ 47: "NO",
7470
+ 61: "AU",
7471
+ 212: "MA",
7472
+ 262: "RE",
7473
+ 290: "SH",
7474
+ 358: "FI",
7475
+ 590: "GP",
7476
+ 599: "CW"
7477
+ };
7478
+ function regionOfInternational(typed) {
7479
+ const text = String(typed ?? "").trim();
7480
+ if (!text.startsWith("+")) return null;
7481
+ const digits = text.slice(1).replace(/\D/g, "");
7482
+ for (let length = 1; length <= Math.min(3, digits.length); length++) {
7483
+ const code = Number(digits.slice(0, length));
7484
+ const regions = REGIONS_BY_CODE.get(code);
7485
+ if (regions) return MAIN_REGION[code] ?? regions[0] ?? null;
7486
+ }
7487
+ return null;
7488
+ }
7429
7489
  var DEFAULT_COUNTRY = "ES";
7430
7490
  var REGION_NAMES = (() => {
7431
7491
  try {
@@ -7480,12 +7540,16 @@ function CountryMark({ code }) {
7480
7540
  }
7481
7541
  function CountryPicker({
7482
7542
  value,
7483
- onChange
7543
+ onChange,
7544
+ fromNumber = false
7484
7545
  }) {
7485
7546
  const [open, setOpen] = (0, import_react5.useState)(false);
7486
7547
  const [query, setQuery] = (0, import_react5.useState)("");
7487
7548
  const rootRef = (0, import_react5.useRef)(null);
7488
7549
  const inputRef = (0, import_react5.useRef)(null);
7550
+ (0, import_react5.useEffect)(() => {
7551
+ if (fromNumber) setOpen(false);
7552
+ }, [fromNumber]);
7489
7553
  const results = (0, import_react5.useMemo)(() => {
7490
7554
  const q = query.trim().toLowerCase().replace(/^\+/, "");
7491
7555
  if (!q) return COUNTRIES;
@@ -7514,6 +7578,12 @@ function CountryPicker({
7514
7578
  else setQuery("");
7515
7579
  }, [open]);
7516
7580
  const dial = DIAL_CODES.get(value);
7581
+ if (fromNumber) {
7582
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "cv-cp", ref: rootRef, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "cv-cp-chip", "data-from-number": true, title: "El prefijo viene del n\xFAmero marcado", part: "country-chip", children: [
7583
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CountryMark, { code: value || "+" }),
7584
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "cv-sr-only", children: dial ? `Prefijo del n\xFAmero marcado: ${countryName(value)} (+${dial})` : "Prefijo del n\xFAmero marcado" })
7585
+ ] }) });
7586
+ }
7517
7587
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "cv-cp", ref: rootRef, children: [
7518
7588
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
7519
7589
  "button",
@@ -7660,6 +7730,59 @@ var KEYPAD_KEYS = [
7660
7730
  ["#", ""]
7661
7731
  ];
7662
7732
 
7733
+ // src/ui/audio/classic-ring.ts
7734
+ var C6 = 1046.5;
7735
+ var E6 = 1318.51;
7736
+ var HARMONICITY = 4;
7737
+ var INDEX = 1.8;
7738
+ var CLASSIC_CYCLE_SECONDS = 3;
7739
+ var CLASSIC_GAIN = 0.3841;
7740
+ function classicRingGraph(ctx, dest) {
7741
+ const output = ctx.createGain();
7742
+ output.gain.value = CLASSIC_GAIN;
7743
+ const chain = ["highpass", "highpass", "lowpass", "lowpass"].map((type) => {
7744
+ const f = ctx.createBiquadFilter();
7745
+ f.type = type;
7746
+ f.frequency.value = type === "highpass" ? 250 : 7500;
7747
+ f.Q.value = 0.707;
7748
+ return f;
7749
+ });
7750
+ for (let i = 0; i < chain.length - 1; i++) chain[i].connect(chain[i + 1]);
7751
+ chain[chain.length - 1].connect(output);
7752
+ output.connect(dest);
7753
+ return { input: chain[0], output };
7754
+ }
7755
+ function strike(ctx, input, t, freq, vel, track) {
7756
+ const carrier = ctx.createOscillator();
7757
+ carrier.frequency.value = freq;
7758
+ const modulator = ctx.createOscillator();
7759
+ modulator.frequency.value = freq * HARMONICITY;
7760
+ const depth = ctx.createGain();
7761
+ depth.gain.setValueAtTime(0, t);
7762
+ depth.gain.linearRampToValueAtTime(INDEX * freq, t + 4e-3);
7763
+ depth.gain.setTargetAtTime(0, t + 4e-3, 0.02);
7764
+ modulator.connect(depth);
7765
+ depth.connect(carrier.frequency);
7766
+ const amp = ctx.createGain();
7767
+ amp.gain.setValueAtTime(0, t);
7768
+ amp.gain.linearRampToValueAtTime(vel, t + 5e-3);
7769
+ amp.gain.setTargetAtTime(0, t + 5e-3, 0.15);
7770
+ amp.gain.setTargetAtTime(0, t + 0.045, 0.08);
7771
+ carrier.connect(amp);
7772
+ amp.connect(input);
7773
+ for (const o of [carrier, modulator]) {
7774
+ o.start(t);
7775
+ o.stop(t + 0.7);
7776
+ track(o);
7777
+ }
7778
+ }
7779
+ function scheduleClassicCycle(ctx, input, t0, track) {
7780
+ for (const ring of [0, 0.6]) {
7781
+ for (let i = 0; i < 8; i++) strike(ctx, input, t0 + ring + i * 0.05, i % 2 ? E6 : C6, 0.55 + 0.1 * (i % 2), track);
7782
+ }
7783
+ return CLASSIC_CYCLE_SECONDS;
7784
+ }
7785
+
7663
7786
  // src/ui/audio/audio.ts
7664
7787
  var TONE_HZ = 425;
7665
7788
  function createRemoteAudio() {
@@ -7702,30 +7825,159 @@ function primeAudio() {
7702
7825
  if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
7703
7826
  });
7704
7827
  }
7705
- function playRingtone() {
7828
+ function playRingtone(options = {}) {
7706
7829
  const ctx = sharedContext();
7707
7830
  if (!ctx) return { stop() {
7708
7831
  } };
7832
+ const bus = ctx.createGain();
7833
+ const route = routeToDevice(ctx, bus, options.sinkId ?? null);
7834
+ const voice = options.buffer ? fileVoice(ctx, bus, options.buffer, options.loop ?? null) : synthesisedVoice(ctx, bus);
7835
+ return {
7836
+ stop() {
7837
+ voice.stop();
7838
+ route.stop();
7839
+ }
7840
+ };
7841
+ }
7842
+ function fileVoice(ctx, bus, buffer, loop) {
7843
+ const FILE_LEVEL = 1;
7844
+ const RAMP = 0.02;
7845
+ const LEAD = 0.08;
7709
7846
  const gain = ctx.createGain();
7710
7847
  gain.gain.value = 0;
7711
- gain.connect(ctx.destination);
7712
- const osc = ctx.createOscillator();
7713
- osc.type = "sine";
7714
- osc.frequency.value = TONE_HZ;
7715
- osc.connect(gain);
7716
- osc.start();
7717
- const LEVEL = 0.8;
7718
- const RAMP = 0.02;
7719
- const burst = (at, dur) => {
7848
+ gain.connect(bus);
7849
+ const src = ctx.createBufferSource();
7850
+ src.buffer = buffer;
7851
+ src.loop = true;
7852
+ const points = loop && loop.start >= 0 && loop.end > loop.start && loop.end <= buffer.duration + 1e-3 ? loop : null;
7853
+ if (points) {
7854
+ src.loopStart = points.start;
7855
+ src.loopEnd = points.end;
7856
+ }
7857
+ src.connect(gain);
7858
+ let stopped = false;
7859
+ void Promise.resolve(ctx.resume?.()).catch(() => {
7860
+ }).finally(() => {
7861
+ if (stopped) return;
7862
+ const at = ctx.currentTime + LEAD;
7720
7863
  gain.gain.setValueAtTime(0, at);
7721
- gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
7722
- gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
7723
- gain.gain.linearRampToValueAtTime(0, at + dur);
7864
+ gain.gain.linearRampToValueAtTime(FILE_LEVEL, at + RAMP);
7865
+ src.start(at, points?.start ?? 0);
7866
+ });
7867
+ return {
7868
+ stop() {
7869
+ stopped = true;
7870
+ try {
7871
+ src.stop();
7872
+ } catch {
7873
+ }
7874
+ try {
7875
+ gain.disconnect();
7876
+ } catch {
7877
+ }
7878
+ }
7724
7879
  };
7725
- const scheduleCycle = (at) => {
7726
- burst(at, 0.4);
7727
- burst(at + 0.6, 0.4);
7728
- return 3;
7880
+ }
7881
+ function routeToDevice(ctx, bus, sinkId) {
7882
+ const toDefault = () => {
7883
+ bus.connect(ctx.destination);
7884
+ };
7885
+ const el = sinkId && typeof document !== "undefined" ? document.createElement("audio") : null;
7886
+ const dest = el && typeof ctx.createMediaStreamDestination === "function" ? ctx.createMediaStreamDestination() : null;
7887
+ const canRoute = el && dest && typeof el.setSinkId === "function";
7888
+ if (!canRoute || !sinkId) {
7889
+ toDefault();
7890
+ return { stop: () => safeDisconnect(bus) };
7891
+ }
7892
+ let stopped = false;
7893
+ el.style.display = "none";
7894
+ el.srcObject = dest.stream;
7895
+ document.body.appendChild(el);
7896
+ bus.connect(dest);
7897
+ void el.setSinkId(sinkId).then(() => el.play()).catch((error) => {
7898
+ if (stopped) return;
7899
+ console.warn("[softphone] sound could not be sent to the chosen device; using the default output", error);
7900
+ safeDisconnect(bus);
7901
+ teardown(el);
7902
+ toDefault();
7903
+ });
7904
+ return {
7905
+ stop() {
7906
+ stopped = true;
7907
+ safeDisconnect(bus);
7908
+ teardown(el);
7909
+ }
7910
+ };
7911
+ }
7912
+ function safeDisconnect(node) {
7913
+ try {
7914
+ node.disconnect();
7915
+ } catch {
7916
+ }
7917
+ }
7918
+ function teardown(el) {
7919
+ el.pause();
7920
+ el.srcObject = null;
7921
+ el.remove();
7922
+ }
7923
+ var TEST_TONE_MS = 900;
7924
+ function playTestTone(sinkId) {
7925
+ const ctx = sharedContext();
7926
+ if (!ctx) return { stop() {
7927
+ } };
7928
+ const bus = ctx.createGain();
7929
+ const route = routeToDevice(ctx, bus, sinkId);
7930
+ const LEVEL = 0.5;
7931
+ const NOTES = [
7932
+ { hz: 660, at: 0.05 },
7933
+ { hz: 880, at: 0.4 }
7934
+ ];
7935
+ const oscs = [];
7936
+ let stopped = false;
7937
+ let timer = null;
7938
+ const stop = () => {
7939
+ if (stopped) return;
7940
+ stopped = true;
7941
+ if (timer) clearTimeout(timer);
7942
+ for (const o of oscs) {
7943
+ try {
7944
+ o.stop();
7945
+ } catch {
7946
+ }
7947
+ }
7948
+ route.stop();
7949
+ };
7950
+ void Promise.resolve(ctx.resume?.()).catch(() => {
7951
+ }).finally(() => {
7952
+ if (stopped) return;
7953
+ const t0 = ctx.currentTime;
7954
+ for (const { hz, at } of NOTES) {
7955
+ const gain = ctx.createGain();
7956
+ gain.gain.value = 0;
7957
+ gain.connect(bus);
7958
+ const osc = ctx.createOscillator();
7959
+ osc.type = "sine";
7960
+ osc.frequency.value = hz;
7961
+ osc.connect(gain);
7962
+ const start = t0 + at;
7963
+ gain.gain.setValueAtTime(0, start);
7964
+ gain.gain.linearRampToValueAtTime(LEVEL, start + 0.015);
7965
+ gain.gain.setValueAtTime(LEVEL, start + 0.25);
7966
+ gain.gain.linearRampToValueAtTime(0, start + 0.32);
7967
+ osc.start(start);
7968
+ osc.stop(start + 0.34);
7969
+ oscs.push(osc);
7970
+ }
7971
+ timer = setTimeout(stop, TEST_TONE_MS);
7972
+ });
7973
+ return { stop };
7974
+ }
7975
+ function synthesisedVoice(ctx, bus) {
7976
+ const { input, output } = classicRingGraph(ctx, bus);
7977
+ const scheduled = /* @__PURE__ */ new Set();
7978
+ const track = (o) => {
7979
+ scheduled.add(o);
7980
+ o.onended = () => scheduled.delete(o);
7729
7981
  };
7730
7982
  const LOOKAHEAD = 0.5;
7731
7983
  const TICK_MS = 150;
@@ -7734,7 +7986,7 @@ function playRingtone() {
7734
7986
  let timer = null;
7735
7987
  let nextAt = 0;
7736
7988
  const pump = () => {
7737
- while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
7989
+ while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleClassicCycle(ctx, input, nextAt, track);
7738
7990
  };
7739
7991
  void Promise.resolve(ctx.resume?.()).catch(() => {
7740
7992
  }).finally(() => {
@@ -7747,12 +7999,15 @@ function playRingtone() {
7747
7999
  stop() {
7748
8000
  stopped = true;
7749
8001
  if (timer) clearInterval(timer);
7750
- try {
7751
- osc.stop();
7752
- } catch {
8002
+ for (const o of scheduled) {
8003
+ try {
8004
+ o.stop();
8005
+ } catch {
8006
+ }
7753
8007
  }
8008
+ scheduled.clear();
7754
8009
  try {
7755
- gain.disconnect();
8010
+ output.disconnect();
7756
8011
  } catch {
7757
8012
  }
7758
8013
  }
@@ -7899,27 +8154,413 @@ function playRingback() {
7899
8154
  };
7900
8155
  }
7901
8156
 
7902
- // src/ui/views/Dialpad.tsx
8157
+ // src/ui/outbound/OutboundNumberPicker.tsx
8158
+ var import_react9 = __toESM(require_react(), 1);
8159
+
8160
+ // src/ui/outbound/outbound-change.ts
8161
+ var import_react8 = __toESM(require_react(), 1);
8162
+
8163
+ // src/core/outbound-numbers.ts
8164
+ function isOutboundNumberRefused(value) {
8165
+ const refusal = value?.refusal;
8166
+ return value instanceof Error && !!refusal && (refusal.reason === "platform" || refusal.reason === "user") && typeof refusal.retryAfterSeconds === "number";
8167
+ }
8168
+
8169
+ // src/ui/outbound/outbound-number-store.ts
8170
+ var import_react7 = __toESM(require_react(), 1);
8171
+ var stores = /* @__PURE__ */ new WeakMap();
8172
+ function outboundNumberStore(softphone) {
8173
+ const source = softphone.sources?.outboundNumbers;
8174
+ if (!source) return null;
8175
+ let store = stores.get(softphone);
8176
+ if (!store) {
8177
+ store = createOutboundNumberStore(source);
8178
+ stores.set(softphone, store);
8179
+ }
8180
+ return store;
8181
+ }
8182
+ function createOutboundNumberStore(source) {
8183
+ let settings = null;
8184
+ let inFlight = null;
8185
+ const listeners = /* @__PURE__ */ new Set();
8186
+ const apply = (next) => {
8187
+ settings = next;
8188
+ for (const l of listeners) l();
8189
+ };
8190
+ return {
8191
+ getSettings: () => settings,
8192
+ refresh() {
8193
+ inFlight ??= (async () => {
8194
+ if (!settings && source.cached) {
8195
+ const early = await source.cached().catch(() => null);
8196
+ if (early && !settings) apply(early);
8197
+ }
8198
+ try {
8199
+ apply(await source.get());
8200
+ } catch (error) {
8201
+ console.warn("[softphone] outbound numbers could not be loaded; keeping the previous ones", error);
8202
+ } finally {
8203
+ inFlight = null;
8204
+ }
8205
+ })();
8206
+ return inFlight;
8207
+ },
8208
+ async setMine(id) {
8209
+ apply(await source.setMine(id));
8210
+ },
8211
+ subscribe(listener) {
8212
+ listeners.add(listener);
8213
+ return () => listeners.delete(listener);
8214
+ }
8215
+ };
8216
+ }
8217
+ var noSubscribe = () => () => {
8218
+ };
8219
+ var noSettings = () => null;
8220
+ function useOutboundNumbers(store) {
8221
+ const settings = (0, import_react7.useSyncExternalStore)(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
8222
+ const [failed, setFailed] = (0, import_react7.useState)(false);
8223
+ const load = (0, import_react7.useCallback)(() => {
8224
+ if (!store) return () => {
8225
+ };
8226
+ let live = true;
8227
+ setFailed(false);
8228
+ void store.refresh().then(() => {
8229
+ if (live) setFailed(store.getSettings() === null);
8230
+ });
8231
+ return () => {
8232
+ live = false;
8233
+ };
8234
+ }, [store]);
8235
+ (0, import_react7.useEffect)(load, [load]);
8236
+ return { settings, failed, retry: load };
8237
+ }
8238
+ function useKnownOutboundNumbers(store) {
8239
+ return (0, import_react7.useSyncExternalStore)(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
8240
+ }
8241
+ function useCountdown(until) {
8242
+ const [, setTick] = (0, import_react7.useState)(0);
8243
+ (0, import_react7.useEffect)(() => {
8244
+ if (!until) return void 0;
8245
+ const timer = setInterval(() => {
8246
+ setTick((n) => n + 1);
8247
+ if (secondsLeft(until) === 0) clearInterval(timer);
8248
+ }, 1e3);
8249
+ return () => clearInterval(timer);
8250
+ }, [until]);
8251
+ return secondsLeft(until);
8252
+ }
8253
+ function secondsLeft(until) {
8254
+ return until ? Math.max(0, Math.ceil((until - Date.now()) / 1e3)) : 0;
8255
+ }
8256
+ function numberName(n, format = formatIntl) {
8257
+ if (!n) return "N\xFAmero de la organizaci\xF3n";
8258
+ return n.label || format(`+${n.number}`);
8259
+ }
8260
+ function effectiveNumber(settings) {
8261
+ return settings?.numbers.find((n) => n.id === settings.effective);
8262
+ }
8263
+
8264
+ // src/ui/outbound/outbound-change.ts
8265
+ var KIND_TAG = {
8266
+ mobile: { tag: "M\xF3vil", hint: "No usar para atenci\xF3n al cliente ni para llamadas comerciales no solicitadas." },
8267
+ commercial: { tag: "Comercial (400)", hint: "Solo para llamadas comerciales. No recibe llamadas." }
8268
+ };
8269
+ function noticeText(situation, seconds, current) {
8270
+ switch (situation) {
8271
+ case "platform":
8272
+ return `El cambio de n\xFAmero de salida no est\xE1 disponible temporalmente. Vuelve a intentarlo en ${seconds} s. Hasta entonces, tus llamadas seguir\xE1n saliendo desde ${current}.`;
8273
+ case "user":
8274
+ return `Acabas de cambiar el n\xFAmero de salida. Podr\xE1s cambiarlo de nuevo en ${seconds} s. Hasta entonces, tus llamadas saldr\xE1n desde ${current}.`;
8275
+ case "wait":
8276
+ return `Podr\xE1s cambiar el n\xFAmero de salida en ${seconds} s.`;
8277
+ case "pending":
8278
+ return seconds > 0 ? `Tu n\xFAmero de salida todav\xEDa no se ha aplicado. Podr\xE1s volver a elegirlo en ${seconds} s.` : "Tu n\xFAmero de salida todav\xEDa no se ha aplicado. Vuelve a elegirlo para aplicarlo.";
8279
+ }
8280
+ }
8281
+ function useOutboundNumberChange(store, settings, current) {
8282
+ const [busy, setBusy] = (0, import_react8.useState)(false);
8283
+ const [notice, setNotice] = (0, import_react8.useState)(null);
8284
+ const [waitUntil, setWaitUntil] = (0, import_react8.useState)(null);
8285
+ (0, import_react8.useEffect)(() => {
8286
+ const s = settings?.changeAvailableInSeconds ?? 0;
8287
+ setWaitUntil(s > 0 ? Date.now() + s * 1e3 : null);
8288
+ }, [settings]);
8289
+ const refusedUntil = notice?.kind === "refused" ? notice.until : null;
8290
+ const blockedFor = useCountdown(refusedUntil ?? waitUntil);
8291
+ (0, import_react8.useEffect)(() => {
8292
+ if (notice?.kind === "refused" && blockedFor === 0) setNotice(null);
8293
+ }, [notice, blockedFor]);
8294
+ const locked = busy || blockedFor > 0;
8295
+ const choose = async (id) => {
8296
+ if (!store || !settings || locked) return false;
8297
+ if (id === settings.mine) return true;
8298
+ setBusy(true);
8299
+ setNotice(null);
8300
+ try {
8301
+ await store.setMine(id);
8302
+ return true;
8303
+ } catch (error) {
8304
+ setNotice(
8305
+ isOutboundNumberRefused(error) ? { kind: "refused", reason: error.refusal.reason, until: Date.now() + error.refusal.retryAfterSeconds * 1e3 } : { kind: "error", text: errorText(error) }
8306
+ );
8307
+ return false;
8308
+ } finally {
8309
+ setBusy(false);
8310
+ }
8311
+ };
8312
+ return {
8313
+ busy,
8314
+ locked,
8315
+ choose,
8316
+ /** What to say about the choice right now, if anything. */
8317
+ message: settings ? currentMessage(notice, settings.state, blockedFor, current) : null,
8318
+ /** The message is an error (the change failed), not information (wait, pending). */
8319
+ isError: notice?.kind === "error"
8320
+ };
8321
+ }
8322
+ function currentMessage(notice, state, blockedFor, current) {
8323
+ if (notice?.kind === "refused") return blockedFor > 0 ? noticeText(notice.reason, blockedFor, current) : null;
8324
+ if (notice?.kind === "error") return notice.text;
8325
+ if (state === "pending" || state === "failed") return noticeText("pending", blockedFor, current);
8326
+ return blockedFor > 0 ? noticeText("wait", blockedFor, current) : null;
8327
+ }
8328
+ function errorText(error) {
8329
+ const message = error instanceof Error ? error.message : "";
8330
+ return message && !/HTTP \d+/.test(message) ? message : "No se ha podido cambiar el n\xFAmero de salida. Int\xE9ntalo de nuevo.";
8331
+ }
8332
+
8333
+ // src/ui/outbound/OutboundNumberPicker.tsx
7903
8334
  var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
8335
+ var CALLOUT_MS = 6e3;
8336
+ function OutboundNumberPicker({ onBusyChange }) {
8337
+ const softphone = useSoftphone();
8338
+ const store = outboundNumberStore(softphone);
8339
+ const { settings } = useOutboundNumbers(store);
8340
+ const formatIntl2 = useIntlFormatter();
8341
+ const current = settings ? numberName(effectiveNumber(settings), formatIntl2) : "";
8342
+ const { busy, locked, choose: apply, message, isError } = useOutboundNumberChange(store, settings, current);
8343
+ const [open, setOpen] = (0, import_react9.useState)(false);
8344
+ const [applying, setApplying] = (0, import_react9.useState)(void 0);
8345
+ const [callout, setCallout] = (0, import_react9.useState)(false);
8346
+ const rootRef = (0, import_react9.useRef)(null);
8347
+ const chipRef = (0, import_react9.useRef)(null);
8348
+ const keys = useListKeyboard(open);
8349
+ (0, import_react9.useEffect)(() => onBusyChange?.(busy), [busy, onBusyChange]);
8350
+ (0, import_react9.useEffect)(() => {
8351
+ if (!message) setCallout(false);
8352
+ }, [message]);
8353
+ (0, import_react9.useEffect)(() => {
8354
+ if (!callout) return void 0;
8355
+ const timer = setTimeout(() => setCallout(false), CALLOUT_MS);
8356
+ return () => clearTimeout(timer);
8357
+ }, [callout]);
8358
+ const floating = open || callout;
8359
+ (0, import_react9.useEffect)(() => {
8360
+ if (!floating) return;
8361
+ const close = () => {
8362
+ setOpen(false);
8363
+ setCallout(false);
8364
+ };
8365
+ const root = rootRef.current?.getRootNode() ?? document;
8366
+ const onDown = (e) => {
8367
+ if (!rootRef.current?.contains(e.target)) close();
8368
+ };
8369
+ const onKey = (e) => {
8370
+ if (e.key !== "Escape") return;
8371
+ if (rootRef.current?.contains(e.target)) chipRef.current?.focus();
8372
+ close();
8373
+ };
8374
+ root.addEventListener("pointerdown", onDown);
8375
+ document.addEventListener("keydown", onKey);
8376
+ return () => {
8377
+ root.removeEventListener("pointerdown", onDown);
8378
+ document.removeEventListener("keydown", onKey);
8379
+ };
8380
+ }, [floating]);
8381
+ if (!store || !settings || settings.numbers.length < 2) return null;
8382
+ const orgDefault = settings.numbers.find((n) => n.id === settings.orgDefault);
8383
+ const applyingNumber = applying === null ? orgDefault : settings.numbers.find((n) => n.id === applying);
8384
+ const shown = applying === void 0 ? current : numberName(applyingNumber, formatIntl2);
8385
+ const choose = async (id) => {
8386
+ if (locked) return;
8387
+ setOpen(false);
8388
+ chipRef.current?.focus();
8389
+ if (id === settings.mine) return;
8390
+ setCallout(false);
8391
+ setApplying(id);
8392
+ const applied = await apply(id);
8393
+ setApplying(void 0);
8394
+ if (!applied) setCallout(true);
8395
+ };
8396
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "cv-on", ref: rootRef, children: [
8397
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
8398
+ "button",
8399
+ {
8400
+ ref: chipRef,
8401
+ type: "button",
8402
+ className: "cv-on-chip",
8403
+ onClick: () => {
8404
+ setCallout(false);
8405
+ setOpen((o) => !o);
8406
+ },
8407
+ "aria-haspopup": "listbox",
8408
+ "aria-expanded": open,
8409
+ "aria-label": `Llamar desde: ${shown}${busy ? " (aplicando)" : ""}. Cambiar n\xFAmero de salida`,
8410
+ "aria-busy": busy,
8411
+ part: "outbound-number-chip",
8412
+ children: [
8413
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cv-on-label", children: "Llamar desde:" }),
8414
+ busy ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoaderCircle, { size: 13, className: "cv-on-icon cv-on-spin", "aria-hidden": true }) : null,
8415
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cv-on-current", children: shown }),
8416
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronDown, { size: 13, className: "cv-cp-caret", "data-open": open, "aria-hidden": true })
8417
+ ]
8418
+ }
8419
+ ),
8420
+ open ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
8421
+ "div",
8422
+ {
8423
+ className: "cv-cp-pop cv-on-pop",
8424
+ role: "dialog",
8425
+ "aria-label": "Llamar desde",
8426
+ part: "outbound-number-list",
8427
+ onBlur: (e) => {
8428
+ if (!e.currentTarget.contains(e.relatedTarget)) setOpen(false);
8429
+ },
8430
+ children: [
8431
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "cv-on-title", children: "Llamar desde" }),
8432
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "cv-cp-list", role: "listbox", "aria-disabled": locked, ref: keys.listRef, onKeyDown: keys.onKeyDown, children: [
8433
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
8434
+ OptionRow,
8435
+ {
8436
+ selected: settings.mine === null,
8437
+ disabled: locked,
8438
+ title: "Por defecto de la organizaci\xF3n",
8439
+ sub: orgDefault ? numberName(orgDefault, formatIntl2) : "Sin n\xFAmero por defecto",
8440
+ onSelect: () => void choose(null)
8441
+ }
8442
+ ),
8443
+ settings.numbers.map((n) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
8444
+ OptionRow,
8445
+ {
8446
+ selected: settings.mine === n.id,
8447
+ disabled: locked,
8448
+ title: numberName(n, formatIntl2),
8449
+ sub: n.label ? formatIntl2(`+${n.number}`) : null,
8450
+ tag: KIND_TAG[n.kind],
8451
+ onSelect: () => void choose(n.id)
8452
+ },
8453
+ n.id
8454
+ ))
8455
+ ] }),
8456
+ message ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "cv-on-notice", role: "status", "data-kind": isError ? "error" : "info", children: message }) : null
8457
+ ]
8458
+ }
8459
+ ) : null,
8460
+ callout && !open && message ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "cv-on-callout", role: "status", "data-kind": isError ? "error" : "info", part: "outbound-number-notice", children: message }) : null
8461
+ ] });
8462
+ }
8463
+ function OptionRow({
8464
+ selected,
8465
+ disabled,
8466
+ title,
8467
+ sub,
8468
+ tag,
8469
+ onSelect
8470
+ }) {
8471
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
8472
+ "button",
8473
+ {
8474
+ type: "button",
8475
+ role: "option",
8476
+ "aria-selected": selected,
8477
+ "aria-disabled": disabled,
8478
+ className: "cv-cp-item cv-on-item",
8479
+ tabIndex: -1,
8480
+ onClick: onSelect,
8481
+ children: [
8482
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "cv-on-text", children: [
8483
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "cv-on-line", children: [
8484
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cv-cp-name", children: title }),
8485
+ tag ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cv-on-tag", children: tag.tag }) : null
8486
+ ] }),
8487
+ sub ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cv-on-sub", children: sub }) : null,
8488
+ tag ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cv-on-hint", children: tag.hint }) : null
8489
+ ] }),
8490
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Check, { size: 14, className: "cv-cp-check", "data-on": selected, "aria-hidden": true })
8491
+ ]
8492
+ }
8493
+ );
8494
+ }
8495
+ var optionsOf = (list) => list ? [...list.querySelectorAll('[role="option"]')] : [];
8496
+ var fold = (text) => text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
8497
+ var TYPEAHEAD_MS = 700;
8498
+ function useListKeyboard(open) {
8499
+ const listRef = (0, import_react9.useRef)(null);
8500
+ const typed = (0, import_react9.useRef)({ text: "", at: 0 });
8501
+ (0, import_react9.useEffect)(() => {
8502
+ if (!open) return;
8503
+ const options = optionsOf(listRef.current);
8504
+ (options.find((o) => o.getAttribute("aria-selected") === "true") ?? options[0])?.focus();
8505
+ }, [open]);
8506
+ const onKeyDown = (e) => {
8507
+ const options = optionsOf(listRef.current);
8508
+ const at = options.indexOf(e.target);
8509
+ const move = {
8510
+ ArrowDown: Math.min(at + 1, options.length - 1),
8511
+ ArrowUp: Math.max(at - 1, 0),
8512
+ Home: 0,
8513
+ End: options.length - 1
8514
+ };
8515
+ let next = move[e.key];
8516
+ if (next === void 0 && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
8517
+ const now = Date.now();
8518
+ const text = now - typed.current.at < TYPEAHEAD_MS ? typed.current.text + e.key : e.key;
8519
+ typed.current = { text, at: now };
8520
+ const wanted = fold(text);
8521
+ const match = options.findIndex((o) => fold(o.querySelector(".cv-cp-name")?.textContent ?? "").startsWith(wanted));
8522
+ next = match >= 0 ? match : void 0;
8523
+ }
8524
+ if (next === void 0) return;
8525
+ e.preventDefault();
8526
+ options[next]?.focus();
8527
+ };
8528
+ return { listRef, onKeyDown };
8529
+ }
8530
+
8531
+ // src/ui/views/Dialpad.tsx
8532
+ var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
7904
8533
  var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
8534
+ var DISPLAY_SIZES = ["1.375rem", "1.25rem", "1.125rem", "1rem"];
7905
8535
  function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
7906
8536
  const softphone = useSoftphone();
7907
8537
  const state = useSoftphoneState();
7908
- const [value, setValue] = (0, import_react7.useState)("");
7909
- const [country, setCountry] = (0, import_react7.useState)(DEFAULT_COUNTRY);
8538
+ const [value, setValue] = (0, import_react11.useState)("");
8539
+ const [country, setCountry] = (0, import_react11.useState)(DEFAULT_COUNTRY);
8540
+ const [changingNumber, setChangingNumber] = (0, import_react11.useState)(false);
7910
8541
  const format = useNumberFormatter();
7911
- const inputRef = (0, import_react7.useRef)(null);
7912
- const caretRef = (0, import_react7.useRef)(null);
8542
+ const inputRef = (0, import_react11.useRef)(null);
8543
+ const caretRef = (0, import_react11.useRef)(null);
7913
8544
  const display = format(country, value);
7914
8545
  const target = toE164(country, value);
7915
- const canCall = state === "online" && target.length > 1;
7916
- (0, import_react7.useLayoutEffect)(() => {
8546
+ const canCall = state === "online" && target.length > 1 && !changingNumber;
8547
+ const typedInternational = value.trim().startsWith("+");
8548
+ const numberRegion = regionOfInternational(value);
8549
+ (0, import_react11.useLayoutEffect)(() => {
7917
8550
  const caret = caretRef.current;
7918
8551
  if (caret === null || !inputRef.current) return;
7919
8552
  caretRef.current = null;
7920
8553
  const pos = caretAfterDigits(display, caret);
7921
8554
  inputRef.current.setSelectionRange(pos, pos);
7922
8555
  }, [display]);
8556
+ (0, import_react11.useLayoutEffect)(() => {
8557
+ const input = inputRef.current;
8558
+ if (!input) return;
8559
+ for (const size of DISPLAY_SIZES) {
8560
+ input.style.fontSize = size;
8561
+ if (input.scrollWidth <= input.clientWidth) break;
8562
+ }
8563
+ }, [display]);
7923
8564
  const append = (key) => {
7924
8565
  onIntent?.();
7925
8566
  setValue((v) => v + key);
@@ -7934,11 +8575,18 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
7934
8575
  onIntent?.();
7935
8576
  void softphone.call(target);
7936
8577
  };
7937
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "cv-view", children: [
7938
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "cv-dial-prefix", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CountryPicker, { value: country, onChange: setCountry }) }),
7939
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "cv-dial-head", children: [
7940
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cv-dial-side", "aria-hidden": true }),
7941
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
8578
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "cv-view", children: [
8579
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(OutboundNumberPicker, { onBusyChange: setChangingNumber }),
8580
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "cv-dial-head", children: [
8581
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
8582
+ CountryPicker,
8583
+ {
8584
+ value: numberRegion ?? country,
8585
+ onChange: setCountry,
8586
+ fromNumber: typedInternational
8587
+ }
8588
+ ),
8589
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
7942
8590
  "input",
7943
8591
  {
7944
8592
  ref: inputRef,
@@ -7955,22 +8603,24 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
7955
8603
  inputMode: "tel"
7956
8604
  }
7957
8605
  ),
7958
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
8606
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
7959
8607
  "button",
7960
8608
  {
7961
8609
  type: "button",
7962
8610
  className: "cv-dial-side",
7963
8611
  "data-hidden": value.length === 0,
8612
+ tabIndex: value.length === 0 ? -1 : void 0,
8613
+ "aria-hidden": value.length === 0 || void 0,
7964
8614
  onClick: backspace,
7965
8615
  "aria-label": "Borrar",
7966
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Delete, { size: 19 })
8616
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Delete, { size: 19 })
7967
8617
  }
7968
8618
  )
7969
8619
  ] }),
7970
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
7971
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "cv-keypad", children: KEYPAD_KEYS.map(([key, letters]) => {
8620
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
8621
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "cv-keypad", children: KEYPAD_KEYS.map(([key, letters]) => {
7972
8622
  const dialable = !NOT_DIALABLE.has(key);
7973
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
8623
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
7974
8624
  "button",
7975
8625
  {
7976
8626
  type: "button",
@@ -7980,14 +8630,14 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
7980
8630
  "aria-disabled": !dialable,
7981
8631
  title: dialable ? void 0 : "Solo disponible durante una llamada",
7982
8632
  children: [
7983
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("b", { children: key }),
7984
- letters ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("small", { children: letters }) : null
8633
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("b", { children: key }),
8634
+ letters ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("small", { children: letters }) : null
7985
8635
  ]
7986
8636
  },
7987
8637
  key
7988
8638
  );
7989
8639
  }) }),
7990
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "cv-dial-foot", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
8640
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "cv-dial-foot", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
7991
8641
  "button",
7992
8642
  {
7993
8643
  type: "button",
@@ -7996,14 +8646,14 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
7996
8646
  disabled: !canCall,
7997
8647
  "aria-label": "Llamar",
7998
8648
  part: "call-button",
7999
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Phone, { size: 26 })
8649
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Phone, { size: 26 })
8000
8650
  }
8001
8651
  ) })
8002
8652
  ] });
8003
8653
  }
8004
8654
 
8005
8655
  // src/ui/views/CallScreen.tsx
8006
- var import_react12 = __toESM(require_react(), 1);
8656
+ var import_react16 = __toESM(require_react(), 1);
8007
8657
 
8008
8658
  // src/ui/lib/format.ts
8009
8659
  function formatDuration(totalSeconds) {
@@ -8145,10 +8795,10 @@ var ERROR_TEXT = {
8145
8795
  };
8146
8796
 
8147
8797
  // src/ui/lib/useTicker.ts
8148
- var import_react9 = __toESM(require_react(), 1);
8798
+ var import_react13 = __toESM(require_react(), 1);
8149
8799
  function useTicker(enabled, intervalMs = 1e3) {
8150
- const [now, setNow] = (0, import_react9.useState)(() => Date.now());
8151
- (0, import_react9.useEffect)(() => {
8800
+ const [now, setNow] = (0, import_react13.useState)(() => Date.now());
8801
+ (0, import_react13.useEffect)(() => {
8152
8802
  if (!enabled) return;
8153
8803
  setNow(Date.now());
8154
8804
  const id = setInterval(() => setNow(Date.now()), intervalMs);
@@ -8158,8 +8808,8 @@ function useTicker(enabled, intervalMs = 1e3) {
8158
8808
  }
8159
8809
 
8160
8810
  // src/ui/views/DevicePicker.tsx
8161
- var import_react10 = __toESM(require_react(), 1);
8162
- var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
8811
+ var import_react14 = __toESM(require_react(), 1);
8812
+ var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
8163
8813
  function deviceLabel(device, index, kind) {
8164
8814
  if (device.label) return device.label;
8165
8815
  return kind === "input" ? `Micr\xF3fono ${index + 1}` : `Altavoz ${index + 1}`;
@@ -8174,11 +8824,11 @@ function DevicePicker({
8174
8824
  onChange,
8175
8825
  emptyText
8176
8826
  }) {
8177
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "cv-settings-row", children: [
8178
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon3, { size: 16 }) }),
8179
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "cv-settings-row-text", children: [
8180
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "cv-settings-row-label", id: `cv-settings-label-${kind}`, children: label }),
8181
- options.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
8827
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-settings-row", children: [
8828
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon3, { size: 16 }) }),
8829
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "cv-settings-row-text", children: [
8830
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-settings-row-label", id: `cv-settings-label-${kind}`, children: label }),
8831
+ options.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
8182
8832
  "select",
8183
8833
  {
8184
8834
  className: "cv-settings-select",
@@ -8186,22 +8836,22 @@ function DevicePicker({
8186
8836
  value: value ?? DEFAULT_OPTION,
8187
8837
  onChange: (e) => onChange(e.target.value === DEFAULT_OPTION ? null : e.target.value),
8188
8838
  children: [
8189
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: DEFAULT_OPTION, children: "Predeterminado del sistema" }),
8190
- options.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: d.deviceId, children: deviceLabel(d, i, kind) }, d.deviceId))
8839
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: DEFAULT_OPTION, children: "Predeterminado del sistema" }),
8840
+ options.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: d.deviceId, children: deviceLabel(d, i, kind) }, d.deviceId))
8191
8841
  ]
8192
8842
  }
8193
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "cv-settings-empty", children: emptyText })
8843
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-settings-empty", children: emptyText })
8194
8844
  ] })
8195
8845
  ] });
8196
8846
  }
8197
8847
  function DevicePickers() {
8198
8848
  const softphone = useSoftphone();
8199
8849
  const devices = useDevices();
8200
- (0, import_react10.useEffect)(() => {
8850
+ (0, import_react14.useEffect)(() => {
8201
8851
  void softphone.audio.refresh();
8202
8852
  }, [softphone]);
8203
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
8204
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
8853
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
8854
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
8205
8855
  DevicePicker,
8206
8856
  {
8207
8857
  icon: Mic,
@@ -8213,7 +8863,7 @@ function DevicePickers() {
8213
8863
  emptyText: "No se encontr\xF3 ning\xFAn micr\xF3fono"
8214
8864
  }
8215
8865
  ),
8216
- devices.canSelectOutput ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
8866
+ devices.canSelectOutput ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
8217
8867
  DevicePicker,
8218
8868
  {
8219
8869
  icon: Headphones,
@@ -8227,13 +8877,13 @@ function DevicePickers() {
8227
8877
  ) : (
8228
8878
  // Safari and most of Firefox have no `setSinkId` at all — nothing here would do anything,
8229
8879
  // and offering a picker that silently fails is worse than not offering one.
8230
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "cv-settings-note", children: "Tu navegador no permite elegir el altavoz desde aqu\xED \u2014 c\xE1mbialo en los ajustes del sistema." })
8880
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "cv-settings-note", children: "Tu navegador no permite elegir el altavoz desde aqu\xED \u2014 c\xE1mbialo en los ajustes del sistema." })
8231
8881
  )
8232
8882
  ] });
8233
8883
  }
8234
8884
 
8235
8885
  // src/ui/views/CallScreen.tsx
8236
- var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
8886
+ var import_jsx_runtime6 = __toESM(require_jsx_runtime(), 1);
8237
8887
  function Control({
8238
8888
  icon: Icon3,
8239
8889
  label,
@@ -8243,8 +8893,8 @@ function Control({
8243
8893
  disabled = false,
8244
8894
  title
8245
8895
  }) {
8246
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-ctrl-item", children: [
8247
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
8896
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-ctrl-item", children: [
8897
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8248
8898
  "button",
8249
8899
  {
8250
8900
  type: "button",
@@ -8255,10 +8905,10 @@ function Control({
8255
8905
  onClick,
8256
8906
  disabled,
8257
8907
  title,
8258
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon3, { size: 19 })
8908
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon3, { size: 19 })
8259
8909
  }
8260
8910
  ),
8261
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: label })
8911
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: label })
8262
8912
  ] });
8263
8913
  }
8264
8914
  var STATUS_LABEL = {
@@ -8296,42 +8946,42 @@ function ConsultScreen({
8296
8946
  const [rawM = "0", ss = "00"] = formatDuration(heldSeconds).split(":");
8297
8947
  const mm = rawM.padStart(2, "0");
8298
8948
  const canComplete = heldSeconds >= MIN_CONSULT_HOLD_SECONDS;
8299
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-view", children: [
8300
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-call", children: [
8301
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("button", { type: "button", className: "cv-consult-hold-banner", onClick: onReturn, children: [
8302
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Pause, { size: 13, "aria-hidden": true }),
8949
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-view", children: [
8950
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-call", children: [
8951
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", className: "cv-consult-hold-banner", onClick: onReturn, children: [
8952
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Pause, { size: 13, "aria-hidden": true }),
8303
8953
  mainName,
8304
8954
  " en espera \xB7 Volver"
8305
8955
  ] }),
8306
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-avatar-wrap", children: [
8307
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
8308
- answered ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
8956
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-avatar-wrap", children: [
8957
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
8958
+ answered ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
8309
8959
  ] }),
8310
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-call-name", children: name }),
8311
- showNumber ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-call-number", children: number }) : null,
8312
- answered ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
8313
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("b", { children: mm }),
8314
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("i", { children: ":" }),
8315
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("b", { children: ss })
8316
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-call-status", children: STATUS_LABEL[s] ?? "" })
8960
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-call-name", children: name }),
8961
+ showNumber ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-call-number", children: number }) : null,
8962
+ answered ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
8963
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("b", { children: mm }),
8964
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("i", { children: ":" }),
8965
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("b", { children: ss })
8966
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-call-status", children: STATUS_LABEL[s] ?? "" })
8317
8967
  ] }),
8318
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-controls", children: [
8319
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-transfer-actions", children: [
8320
- answered ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { className: "cv-btn cv-btn--accent", onClick: onComplete, disabled: !canComplete, children: "Completar" }) : null,
8321
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { className: "cv-btn", onClick: onReturn, children: "Cancelar" })
8968
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-controls", children: [
8969
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-transfer-actions", children: [
8970
+ answered ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "cv-btn cv-btn--accent", onClick: onComplete, disabled: !canComplete, children: "Completar" }) : null,
8971
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "cv-btn", onClick: onReturn, children: "Cancelar" })
8322
8972
  ] }),
8323
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-controls-primary", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-ctrl-item", children: [
8324
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
8973
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-controls-primary", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-ctrl-item", children: [
8974
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8325
8975
  "button",
8326
8976
  {
8327
8977
  type: "button",
8328
8978
  className: "cv-round cv-round--hangup",
8329
8979
  onClick: onHangupAll,
8330
8980
  "aria-label": "Colgar todo",
8331
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PhoneOff, { size: 26 })
8981
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PhoneOff, { size: 26 })
8332
8982
  }
8333
8983
  ),
8334
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar todo" })
8984
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar todo" })
8335
8985
  ] }) })
8336
8986
  ] })
8337
8987
  ] });
@@ -8364,10 +9014,10 @@ function TransferPicker({
8364
9014
  const others = agents.filter((a) => a.userId !== myUserId);
8365
9015
  if (others.length === 0) return null;
8366
9016
  const filtered = filterAgents(others, query);
8367
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-presence", children: [
8368
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-presence-list", role: "listbox", "aria-label": "Compa\xF1eros", children: filtered.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "cv-presence-empty", children: "Sin coincidencias" }) : filtered.map((agent) => {
9017
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-presence", children: [
9018
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-presence-list", role: "listbox", "aria-label": "Compa\xF1eros", children: filtered.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "cv-presence-empty", children: "Sin coincidencias" }) : filtered.map((agent) => {
8369
9019
  const isBusy = agent.state === "en_llamada";
8370
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
9020
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
8371
9021
  "button",
8372
9022
  {
8373
9023
  type: "button",
@@ -8376,9 +9026,9 @@ function TransferPicker({
8376
9026
  disabled: isBusy,
8377
9027
  onClick: () => onSelect(agent.extension),
8378
9028
  children: [
8379
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-presence-dot", "data-state": agent.state, "aria-hidden": true }),
8380
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-presence-name", children: agent.name ?? agent.extension }),
8381
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "cv-presence-meta", children: [
9029
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-presence-dot", "data-state": agent.state, "aria-hidden": true }),
9030
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-presence-name", children: agent.name ?? agent.extension }),
9031
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "cv-presence-meta", children: [
8382
9032
  agent.extension,
8383
9033
  " \xB7 ",
8384
9034
  PRESENCE_LABEL[agent.state]
@@ -8388,7 +9038,7 @@ function TransferPicker({
8388
9038
  agent.userId
8389
9039
  );
8390
9040
  }) }),
8391
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "cv-presence-legend", children: "Desconectado: sin conexi\xF3n en el softphone web. Puede atender desde otro dispositivo." })
9041
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "cv-presence-legend", children: "Desconectado: sin conexi\xF3n en el softphone web. Puede atender desde otro dispositivo." })
8392
9042
  ] });
8393
9043
  }
8394
9044
  function CallScreen({
@@ -8398,24 +9048,27 @@ function CallScreen({
8398
9048
  }) {
8399
9049
  const view = useCall(call);
8400
9050
  const softphone = useSoftphone();
8401
- const [panel, setPanel] = (0, import_react12.useState)(null);
9051
+ const outbound = useOutboundNumbers(call.direction === "outbound" ? outboundNumberStore(softphone) : null).settings;
9052
+ const formatIntl2 = useIntlFormatter();
9053
+ const fromName = outbound && outbound.numbers.length > 1 ? numberName(effectiveNumber(outbound), formatIntl2) : null;
9054
+ const [panel, setPanel] = (0, import_react16.useState)(null);
8402
9055
  const showTransfer = panel === "transfer";
8403
9056
  const showKeypad = panel === "keypad";
8404
9057
  const showDevices = panel === "devices";
8405
9058
  const togglePanel = (p) => setPanel((cur) => cur === p ? null : p);
8406
- const [transferTo, setTransferTo] = (0, import_react12.useState)("");
8407
- const [consultCall, setConsultCall] = (0, import_react12.useState)(null);
8408
- const [consultOutcome, setConsultOutcome] = (0, import_react12.useState)(null);
8409
- const [myUserId, setMyUserId] = (0, import_react12.useState)(null);
9059
+ const [transferTo, setTransferTo] = (0, import_react16.useState)("");
9060
+ const [consultCall, setConsultCall] = (0, import_react16.useState)(null);
9061
+ const [consultOutcome, setConsultOutcome] = (0, import_react16.useState)(null);
9062
+ const [myUserId, setMyUserId] = (0, import_react16.useState)(null);
8410
9063
  const consultView = useCall(consultCall);
8411
- const consultIntentRef = (0, import_react12.useRef)(null);
8412
- const controlsRef = (0, import_react12.useRef)(null);
9064
+ const consultIntentRef = (0, import_react16.useRef)(null);
9065
+ const controlsRef = (0, import_react16.useRef)(null);
8413
9066
  const now = useTicker(view?.state === "active");
8414
9067
  const consultNow = useTicker(consultView?.state === "active");
8415
9068
  const isConnected = view ? view.state === "active" || view.state === "held" || view.state === "reconnecting" : false;
8416
9069
  const transferPickerActive = isConnected && showTransfer && !consultCall;
8417
9070
  const presence = usePresence(transferPickerActive);
8418
- (0, import_react12.useEffect)(() => {
9071
+ (0, import_react16.useEffect)(() => {
8419
9072
  const listAgents = softphone.sources?.listAgents;
8420
9073
  if (!listAgents) return;
8421
9074
  let cancelled = false;
@@ -8425,7 +9078,7 @@ function CallScreen({
8425
9078
  cancelled = true;
8426
9079
  };
8427
9080
  }, [softphone]);
8428
- (0, import_react12.useEffect)(() => {
9081
+ (0, import_react16.useEffect)(() => {
8429
9082
  if (!consultCall) return;
8430
9083
  return consultCall.on("ended" /* Ended */, ({ reason }) => {
8431
9084
  const intent = consultIntentRef.current;
@@ -8447,7 +9100,7 @@ function CallScreen({
8447
9100
  setTransferTo(extension);
8448
9101
  void startConsult(extension);
8449
9102
  };
8450
- const [quickTransfer, setQuickTransfer] = (0, import_react12.useState)(false);
9103
+ const [quickTransfer, setQuickTransfer] = (0, import_react16.useState)(false);
8451
9104
  const selectPresenceAgentAuto = (extension) => {
8452
9105
  void call.transfer(extension);
8453
9106
  };
@@ -8462,7 +9115,7 @@ function CallScreen({
8462
9115
  consultIntentRef.current = "cancel";
8463
9116
  await call.cancelConsult();
8464
9117
  };
8465
- (0, import_react12.useEffect)(() => {
9118
+ (0, import_react16.useEffect)(() => {
8466
9119
  if (!showDevices) return;
8467
9120
  const onKey = (e) => {
8468
9121
  if (e.key === "Escape") setPanel(null);
@@ -8470,7 +9123,7 @@ function CallScreen({
8470
9123
  document.addEventListener("keydown", onKey);
8471
9124
  return () => document.removeEventListener("keydown", onKey);
8472
9125
  }, [showDevices]);
8473
- (0, import_react12.useEffect)(() => {
9126
+ (0, import_react16.useEffect)(() => {
8474
9127
  if (!showKeypad) return;
8475
9128
  const onKey = (e) => {
8476
9129
  if (e.key === "Escape") setPanel(null);
@@ -8487,7 +9140,7 @@ function CallScreen({
8487
9140
  root.removeEventListener("pointerdown", onDown);
8488
9141
  };
8489
9142
  }, [showKeypad, isConnected]);
8490
- (0, import_react12.useEffect)(() => {
9143
+ (0, import_react16.useEffect)(() => {
8491
9144
  if (view && view.state !== "active") setPanel((cur) => cur === "keypad" ? null : cur);
8492
9145
  }, [view?.state]);
8493
9146
  if (!view || view.state === "ended") return null;
@@ -8498,7 +9151,7 @@ function CallScreen({
8498
9151
  const number = view.remote.number;
8499
9152
  const showNumber = number !== name;
8500
9153
  if (consultCall && consultView) {
8501
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9154
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8502
9155
  ConsultScreen,
8503
9156
  {
8504
9157
  mainName: name,
@@ -8514,20 +9167,20 @@ function CallScreen({
8514
9167
  const IncomingActionIcon = incomingAction?.icon ?? ExternalLink;
8515
9168
  const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
8516
9169
  const mm = rawM.padStart(2, "0");
8517
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-view", children: [
8518
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-call", children: showDevices ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-call-devices-inline", children: [
8519
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-call-devices-header", children: [
8520
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel(null), children: [
8521
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArrowLeft, { size: 13, "aria-hidden": true }),
9170
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-view", children: [
9171
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-call", children: showDevices ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-call-devices-inline", children: [
9172
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-call-devices-header", children: [
9173
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel(null), children: [
9174
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ArrowLeft, { size: 13, "aria-hidden": true }),
8522
9175
  "Volver"
8523
9176
  ] }),
8524
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-call-devices-title", children: "Dispositivos" })
9177
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-call-devices-title", children: "Dispositivos" })
8525
9178
  ] }),
8526
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "cv-call-devices-context", children: [
9179
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "cv-call-devices-context", children: [
8527
9180
  showNumber ? `${number} \xB7 ` : "",
8528
9181
  s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
8529
9182
  ] }),
8530
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DevicePickers, {}) })
9183
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DevicePickers, {}) })
8531
9184
  ] }) : transferPickerActive ? (
8532
9185
  /*
8533
9186
  Found live (2026-09-29): the agent list a moment ago picked between showing ONE agent row
@@ -8544,17 +9197,17 @@ function CallScreen({
8544
9197
  calling doesn't disappear" compact context line (below) instead of the full block gives
8545
9198
  that ~150px back to the one thing this screen is actually for right now: the list.
8546
9199
  */
8547
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-transfer", children: [
8548
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-transfer-context", children: [
8549
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-call-devices-title", children: name }),
8550
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "cv-call-devices-context", children: [
9200
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-transfer", children: [
9201
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-transfer-context", children: [
9202
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-call-devices-title", children: name }),
9203
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "cv-call-devices-context", children: [
8551
9204
  showNumber ? `${number} \xB7 ` : "",
8552
9205
  s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
8553
9206
  ] })
8554
9207
  ] }),
8555
- consultOutcome ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "cv-dial-outcome", role: "status", "data-shown": "true", children: consultFailText(consultOutcome) }) : null,
8556
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-transfer-row", children: [
8557
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9208
+ consultOutcome ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "cv-dial-outcome", role: "status", "data-shown": "true", children: consultFailText(consultOutcome) }) : null,
9209
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-transfer-row", children: [
9210
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8558
9211
  "input",
8559
9212
  {
8560
9213
  className: "cv-search",
@@ -8565,7 +9218,7 @@ function CallScreen({
8565
9218
  autoFocus: true
8566
9219
  }
8567
9220
  ),
8568
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9221
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8569
9222
  "button",
8570
9223
  {
8571
9224
  className: "cv-btn cv-btn--accent",
@@ -8575,10 +9228,10 @@ function CallScreen({
8575
9228
  }
8576
9229
  )
8577
9230
  ] }),
8578
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-segmented", role: "tablist", "aria-label": "Modo de transferencia", children: [
9231
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-segmented", role: "tablist", "aria-label": "Modo de transferencia", children: [
8579
9232
  { key: "consult", label: "Hablar primero" },
8580
9233
  { key: "auto", label: "Transferir directo" }
8581
- ].map(({ key, label }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9234
+ ].map(({ key, label }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8582
9235
  "button",
8583
9236
  {
8584
9237
  type: "button",
@@ -8590,7 +9243,7 @@ function CallScreen({
8590
9243
  },
8591
9244
  key
8592
9245
  )) }),
8593
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9246
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8594
9247
  TransferPicker,
8595
9248
  {
8596
9249
  agents: presence,
@@ -8600,54 +9253,61 @@ function CallScreen({
8600
9253
  }
8601
9254
  )
8602
9255
  ] })
8603
- ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
8604
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-avatar-wrap", children: [
8605
- isRinging ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-avatar-ping", "aria-hidden": true }) : null,
8606
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
8607
- s === "active" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
9256
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
9257
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-avatar-wrap", children: [
9258
+ isRinging ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-avatar-ping", "aria-hidden": true }) : null,
9259
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
9260
+ s === "active" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
8608
9261
  ] }),
8609
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-call-name", children: name }),
8610
- showNumber ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-call-number", children: number }) : null,
8611
- s === "active" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
8612
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("b", { children: mm }),
8613
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("i", { children: ":" }),
8614
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("b", { children: ss })
8615
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-call-status", children: s === "held" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-badge-hold", children: STATUS_LABEL[s] }) : STATUS_LABEL[s] ?? "" }),
8616
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-call-chips", children: [
8617
- incomingAction ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
9262
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-call-name", children: name }),
9263
+ showNumber ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-call-number", children: number }) : null,
9264
+ fromName ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-call-from", children: [
9265
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PhoneOutgoing, { size: 11, "aria-hidden": true }),
9266
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
9267
+ "Desde ",
9268
+ fromName
9269
+ ] })
9270
+ ] }) : null,
9271
+ s === "active" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
9272
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("b", { children: mm }),
9273
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("i", { children: ":" }),
9274
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("b", { children: ss })
9275
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-call-status", children: s === "held" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-badge-hold", children: STATUS_LABEL[s] }) : STATUS_LABEL[s] ?? "" }),
9276
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-call-chips", children: [
9277
+ incomingAction ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
8618
9278
  "button",
8619
9279
  {
8620
9280
  type: "button",
8621
9281
  className: "cv-call-devices-chip",
8622
9282
  onClick: () => incomingAction.onClick(view.remote.contact),
8623
9283
  children: [
8624
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(IncomingActionIcon, { size: 13, "aria-hidden": true }),
9284
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(IncomingActionIcon, { size: 13, "aria-hidden": true }),
8625
9285
  incomingAction.label
8626
9286
  ]
8627
9287
  }
8628
9288
  ) : null,
8629
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel("devices"), children: [
8630
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Headphones, { size: 13, "aria-hidden": true }),
9289
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel("devices"), children: [
9290
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Headphones, { size: 13, "aria-hidden": true }),
8631
9291
  "Dispositivos"
8632
9292
  ] })
8633
9293
  ] })
8634
9294
  ] }) }),
8635
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-controls-primary", children: [
8636
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-ctrl-item", children: [
8637
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9295
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-controls-primary", children: [
9296
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-ctrl-item", children: [
9297
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8638
9298
  "button",
8639
9299
  {
8640
9300
  type: "button",
8641
9301
  className: "cv-round cv-round--hangup",
8642
9302
  onClick: () => void call.reject(),
8643
9303
  "aria-label": "Rechazar",
8644
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PhoneOff, { size: 26 })
9304
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PhoneOff, { size: 26 })
8645
9305
  }
8646
9306
  ),
8647
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Rechazar" })
9307
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Rechazar" })
8648
9308
  ] }),
8649
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-ctrl-item", children: [
8650
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9309
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-ctrl-item", children: [
9310
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8651
9311
  "button",
8652
9312
  {
8653
9313
  type: "button",
@@ -8655,14 +9315,14 @@ function CallScreen({
8655
9315
  onClick: () => void call.answer(),
8656
9316
  "aria-label": "Contestar",
8657
9317
  part: "answer-button",
8658
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Phone, { size: 26 })
9318
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Phone, { size: 26 })
8659
9319
  }
8660
9320
  ),
8661
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Contestar" })
9321
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Contestar" })
8662
9322
  ] })
8663
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
8664
- inCall ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-controls-row", children: [
8665
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9323
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
9324
+ inCall ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-controls-row", children: [
9325
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8666
9326
  Control,
8667
9327
  {
8668
9328
  icon: view.isMuted ? MicOff : Mic,
@@ -8672,7 +9332,7 @@ function CallScreen({
8672
9332
  onClick: () => call.mute(!view.isMuted)
8673
9333
  }
8674
9334
  ),
8675
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9335
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8676
9336
  Control,
8677
9337
  {
8678
9338
  icon: view.isOnHold ? Play : Pause,
@@ -8682,7 +9342,7 @@ function CallScreen({
8682
9342
  onClick: () => view.isOnHold ? void call.unhold() : void call.hold()
8683
9343
  }
8684
9344
  ),
8685
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9345
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8686
9346
  Control,
8687
9347
  {
8688
9348
  icon: ArrowLeftRight,
@@ -8692,7 +9352,7 @@ function CallScreen({
8692
9352
  onClick: () => togglePanel("transfer")
8693
9353
  }
8694
9354
  ),
8695
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9355
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8696
9356
  Control,
8697
9357
  {
8698
9358
  icon: Grid3x3,
@@ -8705,8 +9365,8 @@ function CallScreen({
8705
9365
  }
8706
9366
  )
8707
9367
  ] }) : null,
8708
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-controls-primary", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "cv-ctrl-item", children: [
8709
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
9368
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-controls-primary", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "cv-ctrl-item", children: [
9369
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8710
9370
  "button",
8711
9371
  {
8712
9372
  type: "button",
@@ -8714,12 +9374,12 @@ function CallScreen({
8714
9374
  onClick: () => void call.hangup(),
8715
9375
  "aria-label": "Colgar",
8716
9376
  part: "hangup-button",
8717
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PhoneOff, { size: 26 })
9377
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PhoneOff, { size: 26 })
8718
9378
  }
8719
9379
  ),
8720
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
9380
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
8721
9381
  ] }) }),
8722
- inCall && showKeypad ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "cv-keypad-popover cv-keypad-popover--above", role: "dialog", "aria-label": "Teclado", children: KEYPAD_KEYS.map(([key, letters]) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
9382
+ inCall && showKeypad ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-keypad-popover cv-keypad-popover--above", role: "dialog", "aria-label": "Teclado", children: KEYPAD_KEYS.map(([key, letters]) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
8723
9383
  "button",
8724
9384
  {
8725
9385
  type: "button",
@@ -8729,8 +9389,8 @@ function CallScreen({
8729
9389
  if (keyTones) playKeyTone(key);
8730
9390
  },
8731
9391
  children: [
8732
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("b", { children: key }),
8733
- letters ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("small", { children: letters }) : null
9392
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("b", { children: key }),
9393
+ letters ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("small", { children: letters }) : null
8734
9394
  ]
8735
9395
  },
8736
9396
  key
@@ -8740,29 +9400,29 @@ function CallScreen({
8740
9400
  }
8741
9401
 
8742
9402
  // src/ui/views/ContactsTab.tsx
8743
- var import_react16 = __toESM(require_react(), 1);
9403
+ var import_react20 = __toESM(require_react(), 1);
8744
9404
 
8745
9405
  // src/ui/views/DetailRow.tsx
8746
- var import_jsx_runtime6 = __toESM(require_jsx_runtime(), 1);
9406
+ var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
8747
9407
  function DetailRow({
8748
9408
  icon: Icon3,
8749
9409
  eyebrow,
8750
9410
  text,
8751
9411
  onClick
8752
9412
  }) {
8753
- const inner = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
8754
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-detail-row-icon", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Icon3, { size: 16 }) }),
8755
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "cv-detail-row-text", children: [
8756
- eyebrow ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-detail-row-eyebrow", children: eyebrow }) : null,
8757
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "cv-detail-row-value", children: text })
9413
+ const inner = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
9414
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "cv-detail-row-icon", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon3, { size: 16 }) }),
9415
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "cv-detail-row-text", children: [
9416
+ eyebrow ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "cv-detail-row-eyebrow", children: eyebrow }) : null,
9417
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "cv-detail-row-value", children: text })
8758
9418
  ] }),
8759
- onClick ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronRight, { size: 16, className: "cv-detail-row-chevron", "aria-hidden": true }) : null
9419
+ onClick ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronRight, { size: 16, className: "cv-detail-row-chevron", "aria-hidden": true }) : null
8760
9420
  ] });
8761
- return onClick ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "cv-detail-row cv-detail-row--action", onClick, children: inner }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "cv-detail-row", children: inner });
9421
+ return onClick ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "cv-detail-row cv-detail-row--action", onClick, children: inner }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "cv-detail-row", children: inner });
8762
9422
  }
8763
9423
 
8764
9424
  // src/ui/views/ContactDetail.tsx
8765
- var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
9425
+ var import_jsx_runtime8 = __toESM(require_jsx_runtime(), 1);
8766
9426
  function ContactDetail({
8767
9427
  contact,
8768
9428
  onBack,
@@ -8772,19 +9432,19 @@ function ContactDetail({
8772
9432
  const formatIntl2 = useIntlFormatter();
8773
9433
  const formatted = formatIntl2(contact.phone);
8774
9434
  const name = contact.name ?? formatted;
8775
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "cv-detail", role: "region", "aria-label": name, children: [
8776
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "cv-detail-head", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
8777
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ArrowLeft, { size: 16, "aria-hidden": true }),
9435
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-detail", role: "region", "aria-label": name, children: [
9436
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "cv-detail-head", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
9437
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ArrowLeft, { size: 16, "aria-hidden": true }),
8778
9438
  "Contactos"
8779
9439
  ] }) }),
8780
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "cv-detail-body", children: [
8781
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "cv-detail-identity", children: [
8782
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "cv-detail-avatar", "aria-hidden": true, children: initials(contact.name) }),
8783
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h2", { className: "cv-detail-name", children: name })
9440
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-detail-body", children: [
9441
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-detail-identity", children: [
9442
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "cv-detail-avatar", "aria-hidden": true, children: initials(contact.name) }),
9443
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { className: "cv-detail-name", children: name })
8784
9444
  ] }),
8785
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "cv-detail-rows", children: [
8786
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DetailRow, { icon: Phone, eyebrow: "Tel\xE9fono", text: formatted }),
8787
- contactAction ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
9445
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-detail-rows", children: [
9446
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DetailRow, { icon: Phone, eyebrow: "Tel\xE9fono", text: formatted }),
9447
+ contactAction ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
8788
9448
  DetailRow,
8789
9449
  {
8790
9450
  icon: contactAction.icon ?? ExternalLink,
@@ -8794,14 +9454,14 @@ function ContactDetail({
8794
9454
  ) : null
8795
9455
  ] })
8796
9456
  ] }),
8797
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "cv-detail-foot", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
9457
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "cv-detail-foot", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
8798
9458
  "button",
8799
9459
  {
8800
9460
  type: "button",
8801
9461
  className: "cv-btn cv-btn--accent cv-detail-call",
8802
9462
  onClick: () => void softphone.call(contact.phone),
8803
9463
  children: [
8804
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Phone, { size: 16, "aria-hidden": true }),
9464
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Phone, { size: 16, "aria-hidden": true }),
8805
9465
  "Llamar"
8806
9466
  ]
8807
9467
  }
@@ -8810,19 +9470,19 @@ function ContactDetail({
8810
9470
  }
8811
9471
 
8812
9472
  // src/ui/lib/useInfiniteList.ts
8813
- var import_react15 = __toESM(require_react(), 1);
9473
+ var import_react19 = __toESM(require_react(), 1);
8814
9474
  function useInfiniteList(loadPage) {
8815
- const [items, setItems] = (0, import_react15.useState)([]);
8816
- const [loading2, setLoading] = (0, import_react15.useState)(true);
8817
- const [loadingMore, setLoadingMore] = (0, import_react15.useState)(false);
8818
- const [hasMore, setHasMore] = (0, import_react15.useState)(true);
8819
- const [error, setError] = (0, import_react15.useState)(false);
8820
- const seqRef = (0, import_react15.useRef)(0);
8821
- const itemsRef = (0, import_react15.useRef)(items);
9475
+ const [items, setItems] = (0, import_react19.useState)([]);
9476
+ const [loading2, setLoading] = (0, import_react19.useState)(true);
9477
+ const [loadingMore, setLoadingMore] = (0, import_react19.useState)(false);
9478
+ const [hasMore, setHasMore] = (0, import_react19.useState)(true);
9479
+ const [error, setError] = (0, import_react19.useState)(false);
9480
+ const seqRef = (0, import_react19.useRef)(0);
9481
+ const itemsRef = (0, import_react19.useRef)(items);
8822
9482
  itemsRef.current = items;
8823
- const loadingMoreRef = (0, import_react15.useRef)(false);
8824
- const hasMoreRef = (0, import_react15.useRef)(true);
8825
- (0, import_react15.useEffect)(() => {
9483
+ const loadingMoreRef = (0, import_react19.useRef)(false);
9484
+ const hasMoreRef = (0, import_react19.useRef)(true);
9485
+ (0, import_react19.useEffect)(() => {
8826
9486
  const seq = ++seqRef.current;
8827
9487
  loadingMoreRef.current = false;
8828
9488
  hasMoreRef.current = true;
@@ -8845,7 +9505,7 @@ function useInfiniteList(loadPage) {
8845
9505
  if (seq === seqRef.current) setLoading(false);
8846
9506
  });
8847
9507
  }, [loadPage]);
8848
- const loadMore = (0, import_react15.useCallback)(() => {
9508
+ const loadMore = (0, import_react19.useCallback)(() => {
8849
9509
  if (loadingMoreRef.current || !hasMoreRef.current) return;
8850
9510
  const seq = seqRef.current;
8851
9511
  loadingMoreRef.current = true;
@@ -8861,11 +9521,11 @@ function useInfiniteList(loadPage) {
8861
9521
  if (seq === seqRef.current) setLoadingMore(false);
8862
9522
  });
8863
9523
  }, [loadPage]);
8864
- const [containerEl, setContainerEl] = (0, import_react15.useState)(null);
8865
- const [sentinelEl, setSentinelEl] = (0, import_react15.useState)(null);
8866
- const loadMoreRef = (0, import_react15.useRef)(loadMore);
9524
+ const [containerEl, setContainerEl] = (0, import_react19.useState)(null);
9525
+ const [sentinelEl, setSentinelEl] = (0, import_react19.useState)(null);
9526
+ const loadMoreRef = (0, import_react19.useRef)(loadMore);
8867
9527
  loadMoreRef.current = loadMore;
8868
- (0, import_react15.useEffect)(() => {
9528
+ (0, import_react19.useEffect)(() => {
8869
9529
  if (!containerEl || !sentinelEl) return;
8870
9530
  const observer = new IntersectionObserver(
8871
9531
  ([entry]) => {
@@ -8888,21 +9548,21 @@ function useInfiniteList(loadPage) {
8888
9548
  }
8889
9549
 
8890
9550
  // src/ui/views/ContactsTab.tsx
8891
- var import_jsx_runtime8 = __toESM(require_jsx_runtime(), 1);
9551
+ var import_jsx_runtime9 = __toESM(require_jsx_runtime(), 1);
8892
9552
  var PAGE = 50;
8893
9553
  function ContactsTab({
8894
9554
  source,
8895
9555
  contactAction
8896
9556
  }) {
8897
9557
  const softphone = useSoftphone();
8898
- const [search, setSearch] = (0, import_react16.useState)("");
8899
- const [debouncedSearch, setDebouncedSearch] = (0, import_react16.useState)("");
8900
- const [selected, setSelected] = (0, import_react16.useState)(null);
8901
- (0, import_react16.useEffect)(() => {
9558
+ const [search, setSearch] = (0, import_react20.useState)("");
9559
+ const [debouncedSearch, setDebouncedSearch] = (0, import_react20.useState)("");
9560
+ const [selected, setSelected] = (0, import_react20.useState)(null);
9561
+ (0, import_react20.useEffect)(() => {
8902
9562
  const t = setTimeout(() => setDebouncedSearch(search), 200);
8903
9563
  return () => clearTimeout(t);
8904
9564
  }, [search]);
8905
- const loadPage = (0, import_react16.useCallback)(
9565
+ const loadPage = (0, import_react20.useCallback)(
8906
9566
  (skip) => source({ search: debouncedSearch, skip, limit: PAGE }).then((page) => ({
8907
9567
  items: page.items,
8908
9568
  hasMore: skip + page.items.length < page.total
@@ -8918,10 +9578,10 @@ function ContactsTab({
8918
9578
  sentinelRef
8919
9579
  } = useInfiniteList(loadPage);
8920
9580
  const loading2 = loadingPage || search !== debouncedSearch;
8921
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-view", children: [
8922
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "cv-list-head", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-search-field", children: [
8923
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Search, { className: "cv-search-icon", size: 15, "aria-hidden": true }),
8924
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
9581
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "cv-view", children: [
9582
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "cv-list-head", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "cv-search-field", children: [
9583
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Search, { className: "cv-search-icon", size: 15, "aria-hidden": true }),
9584
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
8925
9585
  "input",
8926
9586
  {
8927
9587
  className: "cv-search cv-search--icon",
@@ -8931,41 +9591,41 @@ function ContactsTab({
8931
9591
  "aria-label": "Buscar contactos"
8932
9592
  }
8933
9593
  ),
8934
- search ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
9594
+ search ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
8935
9595
  "button",
8936
9596
  {
8937
9597
  type: "button",
8938
9598
  className: "cv-search-clear",
8939
9599
  onClick: () => setSearch(""),
8940
9600
  "aria-label": "Limpiar b\xFAsqueda",
8941
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(X, { size: 14 })
9601
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(X, { size: 14 })
8942
9602
  }
8943
9603
  ) : null
8944
9604
  ] }) }),
8945
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "cv-list", ref: containerRef, children: loading2 ? (
9605
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "cv-list", ref: containerRef, children: loading2 ? (
8946
9606
  // The spinner alone was silent to a screen reader — nothing else on this state carries
8947
9607
  // the word "loading" for it to announce. `role="status"` + real text fixes both the
8948
9608
  // accessibility gap and, incidentally, gives the icon something to be decorative NEXT TO
8949
9609
  // rather than being the sole carrier of the information itself.
8950
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-empty", role: "status", children: [
8951
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
8952
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "cv-empty-title", children: "Cargando\u2026" })
9610
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "cv-empty", role: "status", children: [
9611
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
9612
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "cv-empty-title", children: "Cargando\u2026" })
8953
9613
  ] })
8954
- ) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-empty", children: [
8955
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "cv-empty-icon", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Users, { size: 20 }) }),
8956
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "cv-empty-title", children: search ? "Sin resultados" : "No tienes contactos" }),
8957
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "cv-empty-sub", children: search ? `Nada coincide con \xAB${search}\xBB.` : "Tus contactos con tel\xE9fono aparecer\xE1n aqu\xED." })
8958
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
9614
+ ) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "cv-empty", children: [
9615
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "cv-empty-icon", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Users, { size: 20 }) }),
9616
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "cv-empty-title", children: search ? "Sin resultados" : "No tienes contactos" }),
9617
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "cv-empty-sub", children: search ? `Nada coincide con \xAB${search}\xBB.` : "Tus contactos con tel\xE9fono aparecer\xE1n aqu\xED." })
9618
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
8959
9619
  items.map((c) => (
8960
9620
  // A row, not a button: the name area and the call action are separate controls, so
8961
9621
  // neither is nested inside the other (nested interactives are invalid + inaccessible).
8962
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "cv-item", children: [
8963
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "cv-item-avatar", "aria-hidden": true, children: initials(c.name) === "#" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(User, { size: 17 }) : initials(c.name) }),
8964
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("button", { type: "button", className: "cv-item-main", onClick: () => setSelected(c), children: [
8965
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "cv-item-name", children: c.name ?? c.phone }),
8966
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "cv-item-sub", children: c.phone })
9622
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "cv-item", children: [
9623
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "cv-item-avatar", "aria-hidden": true, children: initials(c.name) === "#" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(User, { size: 17 }) : initials(c.name) }),
9624
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("button", { type: "button", className: "cv-item-main", onClick: () => setSelected(c), children: [
9625
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "cv-item-name", children: c.name ?? c.phone }),
9626
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "cv-item-sub", children: c.phone })
8967
9627
  ] }),
8968
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
9628
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
8969
9629
  "button",
8970
9630
  {
8971
9631
  type: "button",
@@ -8973,15 +9633,15 @@ function ContactsTab({
8973
9633
  "data-tone": "accent",
8974
9634
  "aria-label": `Llamar a ${c.name ?? c.phone}`,
8975
9635
  onClick: () => void softphone.call(c.phone),
8976
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Phone, { size: 16 })
9636
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Phone, { size: 16 })
8977
9637
  }
8978
9638
  )
8979
9639
  ] }, c.id)
8980
9640
  )),
8981
- hasMore ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { ref: sentinelRef, className: "cv-list-sentinel", "aria-hidden": true }) : null,
8982
- loadingMore ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "cv-list-loading-more", role: "status", "aria-label": "Cargando m\xE1s contactos", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LoaderCircle, { className: "cv-list-loading-more-spinner", size: 14, "aria-hidden": true }) }) : null
9641
+ hasMore ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ref: sentinelRef, className: "cv-list-sentinel", "aria-hidden": true }) : null,
9642
+ loadingMore ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "cv-list-loading-more", role: "status", "aria-label": "Cargando m\xE1s contactos", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LoaderCircle, { className: "cv-list-loading-more-spinner", size: 14, "aria-hidden": true }) }) : null
8983
9643
  ] }) }),
8984
- selected ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
9644
+ selected ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
8985
9645
  ContactDetail,
8986
9646
  {
8987
9647
  contact: selected,
@@ -8993,13 +9653,13 @@ function ContactsTab({
8993
9653
  }
8994
9654
 
8995
9655
  // src/ui/views/HistoryTab.tsx
8996
- var import_react20 = __toESM(require_react(), 1);
9656
+ var import_react24 = __toESM(require_react(), 1);
8997
9657
 
8998
9658
  // src/ui/views/HistoryDetail.tsx
8999
- var import_react18 = __toESM(require_react(), 1);
9659
+ var import_react22 = __toESM(require_react(), 1);
9000
9660
 
9001
9661
  // src/ui/views/history-format.tsx
9002
- var import_jsx_runtime9 = __toESM(require_jsx_runtime(), 1);
9662
+ var import_jsx_runtime10 = __toESM(require_jsx_runtime(), 1);
9003
9663
  var OUTCOME_LABELS = {
9004
9664
  "no-answer": "No contestada",
9005
9665
  busy: "Ocupada",
@@ -9020,8 +9680,8 @@ function fullDateLabel(at) {
9020
9680
  });
9021
9681
  }
9022
9682
  function Icon2({ entry }) {
9023
- if (entry.missed) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PhoneMissed, { size: 16 });
9024
- return entry.direction === "inbound" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PhoneIncoming, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PhoneOutgoing, { size: 16 });
9683
+ if (entry.missed) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PhoneMissed, { size: 16 });
9684
+ return entry.direction === "inbound" ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PhoneIncoming, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PhoneOutgoing, { size: 16 });
9025
9685
  }
9026
9686
  function statusIcon(entry) {
9027
9687
  if (entry.missed) return PhoneMissed;
@@ -9033,16 +9693,16 @@ function statusTone(entry) {
9033
9693
  }
9034
9694
 
9035
9695
  // src/ui/views/HistoryDetail.tsx
9036
- var import_jsx_runtime10 = __toESM(require_jsx_runtime(), 1);
9696
+ var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
9037
9697
  function HandlerChain({ handlers }) {
9038
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "cv-detail-handlers", children: handlers.map((h, i) => {
9698
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "cv-detail-handlers", children: handlers.map((h, i) => {
9039
9699
  const isLast = i === handlers.length - 1;
9040
9700
  const label = h.name ?? `Ext. ${h.extension}`;
9041
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "cv-detail-handler-hop", children: [
9042
- i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ArrowRight, { size: 12, className: "cv-detail-handler-arrow", "aria-hidden": true }) : null,
9043
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "cv-detail-handler-chip", "data-tone": isLast ? "accent" : void 0, children: [
9701
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "cv-detail-handler-hop", children: [
9702
+ i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ArrowRight, { size: 12, className: "cv-detail-handler-arrow", "aria-hidden": true }) : null,
9703
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "cv-detail-handler-chip", "data-tone": isLast ? "accent" : void 0, children: [
9044
9704
  label,
9045
- h.name ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "cv-detail-handler-ext", children: [
9705
+ h.name ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "cv-detail-handler-ext", children: [
9046
9706
  "\xB7 ",
9047
9707
  h.extension
9048
9708
  ] }) : null
@@ -9050,15 +9710,26 @@ function HandlerChain({ handlers }) {
9050
9710
  ] }, `${h.extension}-${i}`);
9051
9711
  }) });
9052
9712
  }
9713
+ function outboundFrom(entry, known, formatIntl2) {
9714
+ if (entry.direction !== "outbound" || !entry.from) return null;
9715
+ const number = formatIntl2(`+${entry.from}`);
9716
+ const label = known?.numbers.find((n) => n.number === entry.from)?.label;
9717
+ if (!label) return number;
9718
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
9719
+ label,
9720
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "cv-detail-row-sub", children: number })
9721
+ ] });
9722
+ }
9053
9723
  function HistoryDetail({
9054
9724
  entry,
9055
9725
  onBack
9056
9726
  }) {
9057
9727
  const softphone = useSoftphone();
9058
9728
  const formatIntl2 = useIntlFormatter();
9059
- const [detail, setDetail] = (0, import_react18.useState)(null);
9060
- const [loadingDetail, setLoadingDetail] = (0, import_react18.useState)(false);
9061
- (0, import_react18.useEffect)(() => {
9729
+ const knownNumbers = useKnownOutboundNumbers(outboundNumberStore(softphone));
9730
+ const [detail, setDetail] = (0, import_react22.useState)(null);
9731
+ const [loadingDetail, setLoadingDetail] = (0, import_react22.useState)(false);
9732
+ (0, import_react22.useEffect)(() => {
9062
9733
  const load = entry.loadDetail;
9063
9734
  if (!load) return;
9064
9735
  let cancelled = false;
@@ -9071,53 +9742,55 @@ function HistoryDetail({
9071
9742
  }, [entry]);
9072
9743
  const formatted = formatIntl2(entry.number);
9073
9744
  const name = entry.name ?? formatted;
9745
+ const from = outboundFrom(entry, knownNumbers, formatIntl2);
9074
9746
  const handledBy = detail?.handledBy ?? entry.handledBy;
9075
9747
  const recordingUrls = detail?.recordingUrls ?? [];
9076
9748
  const loadingRecording = entry.recorded && loadingDetail && recordingUrls.length === 0;
9077
9749
  const statusText = entry.missed ? entry.outcome ? OUTCOME_LABELS[entry.outcome] : "Perdida" : "Contestada";
9078
9750
  const handlerLabel = handledBy.length > 1 ? "Ruta de la llamada" : entry.direction === "outbound" ? "Realizada por" : "Atendida por";
9079
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "cv-detail", role: "region", "aria-label": name, children: [
9080
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "cv-detail-head", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
9081
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ArrowLeft, { size: 16, "aria-hidden": true }),
9751
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-detail", role: "region", "aria-label": name, children: [
9752
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-detail-head", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
9753
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ArrowLeft, { size: 16, "aria-hidden": true }),
9082
9754
  "Historial"
9083
9755
  ] }) }),
9084
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "cv-detail-body", children: [
9085
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "cv-detail-identity", children: [
9086
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "cv-detail-avatar", "data-tone": statusTone(entry), "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Icon2, { entry }) }),
9087
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { className: "cv-detail-name", "data-tone": entry.missed ? "danger" : void 0, children: name }),
9088
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { className: "cv-detail-meta", children: [
9756
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-detail-body", children: [
9757
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-detail-identity", children: [
9758
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "cv-detail-avatar", "data-tone": statusTone(entry), "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Icon2, { entry }) }),
9759
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { className: "cv-detail-name", "data-tone": entry.missed ? "danger" : void 0, children: name }),
9760
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "cv-detail-meta", children: [
9089
9761
  statusText,
9090
9762
  " \xB7 ",
9091
9763
  timeLabel(entry.at)
9092
9764
  ] })
9093
9765
  ] }),
9094
- entry.recorded ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "cv-detail-recording", children: loadingRecording ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "cv-detail-recording-status", children: [
9095
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(LoaderCircle, { size: 14, className: "cv-detail-recording-spinner", "aria-hidden": true }),
9766
+ entry.recorded ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-detail-recording", children: loadingRecording ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-detail-recording-status", children: [
9767
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(LoaderCircle, { size: 14, className: "cv-detail-recording-spinner", "aria-hidden": true }),
9096
9768
  "Cargando grabaci\xF3n\u2026"
9097
- ] }) : recordingUrls.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
9098
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "cv-detail-recording-label", children: recordingUrls.length === 1 ? "Grabaci\xF3n" : "Grabaciones" }),
9099
- recordingUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("audio", { controls: true, src: url, className: "cv-detail-audio", preload: "metadata" }, url))
9100
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "cv-detail-recording-status cv-detail-recording-status--warn", children: "No se pudo cargar la grabaci\xF3n ahora. Int\xE9ntalo de nuevo m\xE1s tarde." }) }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { className: "cv-detail-recording-status", children: [
9101
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Mic, { size: 14, "aria-hidden": true }),
9769
+ ] }) : recordingUrls.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
9770
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "cv-detail-recording-label", children: recordingUrls.length === 1 ? "Grabaci\xF3n" : "Grabaciones" }),
9771
+ recordingUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("audio", { controls: true, src: url, className: "cv-detail-audio", preload: "metadata" }, url))
9772
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "cv-detail-recording-status cv-detail-recording-status--warn", children: "No se pudo cargar la grabaci\xF3n ahora. Int\xE9ntalo de nuevo m\xE1s tarde." }) }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "cv-detail-recording-status", children: [
9773
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Mic, { size: 14, "aria-hidden": true }),
9102
9774
  "Esta llamada no se grab\xF3."
9103
9775
  ] }),
9104
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "cv-detail-rows", children: [
9105
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DetailRow, { icon: statusIcon(entry), eyebrow: "Estado", text: statusText }),
9106
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DetailRow, { icon: CalendarClock, eyebrow: "Fecha y hora", text: fullDateLabel(entry.at) }),
9107
- entry.durationSec > 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DetailRow, { icon: Timer, eyebrow: "Duraci\xF3n", text: formatDuration(entry.durationSec) }) : null,
9108
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DetailRow, { icon: Phone, eyebrow: "N\xFAmero", text: formatted }),
9109
- handledBy.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DetailRow, { icon: User, eyebrow: handlerLabel, text: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(HandlerChain, { handlers: handledBy }) }) : null,
9110
- entry.direction === "inbound" && entry.did ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null
9776
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-detail-rows", children: [
9777
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DetailRow, { icon: statusIcon(entry), eyebrow: "Estado", text: statusText }),
9778
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DetailRow, { icon: CalendarClock, eyebrow: "Fecha y hora", text: fullDateLabel(entry.at) }),
9779
+ entry.durationSec > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DetailRow, { icon: Timer, eyebrow: "Duraci\xF3n", text: formatDuration(entry.durationSec) }) : null,
9780
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DetailRow, { icon: Phone, eyebrow: "N\xFAmero", text: formatted }),
9781
+ handledBy.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DetailRow, { icon: User, eyebrow: handlerLabel, text: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(HandlerChain, { handlers: handledBy }) }) : null,
9782
+ entry.direction === "inbound" && entry.did ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null,
9783
+ from ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DetailRow, { icon: PhoneOutgoing, eyebrow: "Llamada desde", text: from }) : null
9111
9784
  ] })
9112
9785
  ] }),
9113
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "cv-detail-foot", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
9786
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-detail-foot", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
9114
9787
  "button",
9115
9788
  {
9116
9789
  type: "button",
9117
9790
  className: "cv-btn cv-btn--accent cv-detail-call",
9118
9791
  onClick: () => void softphone.call(entry.number),
9119
9792
  children: [
9120
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Phone, { size: 16, "aria-hidden": true }),
9793
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Phone, { size: 16, "aria-hidden": true }),
9121
9794
  "Llamar"
9122
9795
  ]
9123
9796
  }
@@ -9126,7 +9799,7 @@ function HistoryDetail({
9126
9799
  }
9127
9800
 
9128
9801
  // src/ui/views/HistoryTab.tsx
9129
- var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
9802
+ var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
9130
9803
  var PAGE2 = 30;
9131
9804
  function startOfDay(at) {
9132
9805
  return new Date(at.getFullYear(), at.getMonth(), at.getDate()).getTime();
@@ -9152,9 +9825,9 @@ function PersonFilter({
9152
9825
  directory,
9153
9826
  onChange
9154
9827
  }) {
9155
- const [open, setOpen] = (0, import_react20.useState)(false);
9156
- const rootRef = (0, import_react20.useRef)(null);
9157
- (0, import_react20.useEffect)(() => {
9828
+ const [open, setOpen] = (0, import_react24.useState)(false);
9829
+ const rootRef = (0, import_react24.useRef)(null);
9830
+ (0, import_react24.useEffect)(() => {
9158
9831
  if (!open) return;
9159
9832
  const root = rootRef.current?.getRootNode() ?? document;
9160
9833
  const onDown = (e) => {
@@ -9176,8 +9849,8 @@ function PersonFilter({
9176
9849
  ...directory.people.filter((p) => !directory.me || p.userId !== directory.me.userId).map((p) => ({ v: p.userId, label: p.name ?? "Sin nombre" }))
9177
9850
  ];
9178
9851
  const label = options.find((o) => o.v === value)?.label ?? "Todas las personas";
9179
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-person-filter", ref: rootRef, children: [
9180
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
9852
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "cv-person-filter", ref: rootRef, children: [
9853
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
9181
9854
  "button",
9182
9855
  {
9183
9856
  type: "button",
@@ -9186,13 +9859,13 @@ function PersonFilter({
9186
9859
  "aria-haspopup": "listbox",
9187
9860
  "aria-expanded": open,
9188
9861
  children: [
9189
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Users, { size: 13, "aria-hidden": true }),
9190
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "cv-person-filter-label", children: label }),
9191
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ChevronDown, { size: 13, className: "cv-person-filter-caret", "data-open": open, "aria-hidden": true })
9862
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Users, { size: 13, "aria-hidden": true }),
9863
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "cv-person-filter-label", children: label }),
9864
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ChevronDown, { size: 13, className: "cv-person-filter-caret", "data-open": open, "aria-hidden": true })
9192
9865
  ]
9193
9866
  }
9194
9867
  ),
9195
- open ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-person-filter-pop", role: "listbox", children: options.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
9868
+ open ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "cv-person-filter-pop", role: "listbox", children: options.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
9196
9869
  "button",
9197
9870
  {
9198
9871
  type: "button",
@@ -9204,8 +9877,8 @@ function PersonFilter({
9204
9877
  setOpen(false);
9205
9878
  },
9206
9879
  children: [
9207
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "cv-person-filter-name", children: opt.label }),
9208
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Check, { size: 14, className: "cv-person-filter-check", "data-on": opt.v === value, "aria-hidden": true })
9880
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "cv-person-filter-name", children: opt.label }),
9881
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Check, { size: 14, className: "cv-person-filter-check", "data-on": opt.v === value, "aria-hidden": true })
9209
9882
  ]
9210
9883
  },
9211
9884
  opt.v || "all"
@@ -9216,11 +9889,11 @@ function HistoryTab({ source }) {
9216
9889
  const softphone = useSoftphone();
9217
9890
  const formatIntl2 = useIntlFormatter();
9218
9891
  const now = Date.now();
9219
- const [status, setStatus] = (0, import_react20.useState)("all");
9220
- const [agentId, setAgentId] = (0, import_react20.useState)("");
9221
- const [directory, setDirectory] = (0, import_react20.useState)(null);
9222
- const [selected, setSelected] = (0, import_react20.useState)(null);
9223
- (0, import_react20.useEffect)(() => {
9892
+ const [status, setStatus] = (0, import_react24.useState)("all");
9893
+ const [agentId, setAgentId] = (0, import_react24.useState)("");
9894
+ const [directory, setDirectory] = (0, import_react24.useState)(null);
9895
+ const [selected, setSelected] = (0, import_react24.useState)(null);
9896
+ (0, import_react24.useEffect)(() => {
9224
9897
  const listAgents = softphone.sources?.listAgents;
9225
9898
  if (!listAgents) return;
9226
9899
  let cancelled = false;
@@ -9230,7 +9903,7 @@ function HistoryTab({ source }) {
9230
9903
  cancelled = true;
9231
9904
  };
9232
9905
  }, [softphone]);
9233
- const loadPage = (0, import_react20.useCallback)(
9906
+ const loadPage = (0, import_react24.useCallback)(
9234
9907
  (skip) => source({
9235
9908
  skip,
9236
9909
  limit: PAGE2,
@@ -9241,9 +9914,9 @@ function HistoryTab({ source }) {
9241
9914
  );
9242
9915
  const { items: entries, loading: loading2, loadingMore, hasMore, containerRef, sentinelRef } = useInfiniteList(loadPage);
9243
9916
  const groups = groupByDay(entries, now);
9244
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-view", children: [
9245
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-list-head", children: [
9246
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-segmented", role: "tablist", "aria-label": "Filtrar por estado", children: ["all", "missed"].map((key) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
9917
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "cv-view", children: [
9918
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "cv-list-head", children: [
9919
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "cv-segmented", role: "tablist", "aria-label": "Filtrar por estado", children: ["all", "missed"].map((key) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
9247
9920
  "button",
9248
9921
  {
9249
9922
  type: "button",
@@ -9255,36 +9928,36 @@ function HistoryTab({ source }) {
9255
9928
  },
9256
9929
  key
9257
9930
  )) }),
9258
- directory ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PersonFilter, { value: agentId, directory, onChange: setAgentId }) : null
9931
+ directory ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PersonFilter, { value: agentId, directory, onChange: setAgentId }) : null
9259
9932
  ] }),
9260
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-list", ref: containerRef, children: [
9261
- loading2 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-empty", children: "Cargando\u2026" }),
9262
- !loading2 && entries.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-empty", children: status === "missed" ? "Sin llamadas perdidas" : "Sin llamadas recientes" }),
9263
- !loading2 && groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
9264
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-history-day", children: group.label }),
9265
- group.rows.map((e) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "cv-item", children: [
9266
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "cv-item-avatar", "data-tone": statusTone(e), "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Icon2, { entry: e }) }),
9267
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
9933
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "cv-list", ref: containerRef, children: [
9934
+ loading2 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "cv-empty", children: "Cargando\u2026" }),
9935
+ !loading2 && entries.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "cv-empty", children: status === "missed" ? "Sin llamadas perdidas" : "Sin llamadas recientes" }),
9936
+ !loading2 && groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
9937
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "cv-history-day", children: group.label }),
9938
+ group.rows.map((e) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "cv-item", children: [
9939
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "cv-item-avatar", "data-tone": statusTone(e), "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon2, { entry: e }) }),
9940
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
9268
9941
  "button",
9269
9942
  {
9270
9943
  type: "button",
9271
9944
  className: "cv-item-main",
9272
9945
  onClick: () => setSelected(e),
9273
9946
  children: [
9274
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "cv-item-name", "data-tone": e.missed ? "danger" : void 0, children: e.name ?? formatIntl2(e.number) }),
9275
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "cv-item-sub", children: [
9276
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "tabular-nums", children: timeLabel(e.at) }),
9277
- e.missed ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "cv-item-sub-outcome", children: [
9947
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "cv-item-name", "data-tone": e.missed ? "danger" : void 0, children: e.name ?? formatIntl2(e.number) }),
9948
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "cv-item-sub", children: [
9949
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "tabular-nums", children: timeLabel(e.at) }),
9950
+ e.missed ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "cv-item-sub-outcome", children: [
9278
9951
  " ",
9279
9952
  "\xB7 ",
9280
9953
  e.outcome ? OUTCOME_LABELS[e.outcome] : "Perdida"
9281
9954
  ] }) : e.durationSec > 0 && ` \xB7 ${formatDuration(e.durationSec)}`,
9282
- e.recorded ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Mic, { size: 11, className: "cv-item-sub-rec", "aria-label": "Grabada" }) : null
9955
+ e.recorded ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Mic, { size: 11, className: "cv-item-sub-rec", "aria-label": "Grabada" }) : null
9283
9956
  ] })
9284
9957
  ]
9285
9958
  }
9286
9959
  ),
9287
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
9960
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
9288
9961
  "button",
9289
9962
  {
9290
9963
  type: "button",
@@ -9292,41 +9965,771 @@ function HistoryTab({ source }) {
9292
9965
  "data-tone": "accent",
9293
9966
  "aria-label": `Llamar a ${e.name ?? e.number}`,
9294
9967
  onClick: () => void softphone.call(e.number),
9295
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Phone, { size: 16 })
9968
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Phone, { size: 16 })
9296
9969
  }
9297
9970
  )
9298
9971
  ] }, e.id))
9299
9972
  ] }, group.label)),
9300
- !loading2 && hasMore ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: sentinelRef, className: "cv-list-sentinel", "aria-hidden": true }) : null,
9301
- loadingMore ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "cv-list-loading-more", role: "status", "aria-label": "Cargando m\xE1s llamadas", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(LoaderCircle, { className: "cv-list-loading-more-spinner", size: 14, "aria-hidden": true }) }) : null
9973
+ !loading2 && hasMore ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { ref: sentinelRef, className: "cv-list-sentinel", "aria-hidden": true }) : null,
9974
+ loadingMore ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "cv-list-loading-more", role: "status", "aria-label": "Cargando m\xE1s llamadas", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(LoaderCircle, { className: "cv-list-loading-more-spinner", size: 14, "aria-hidden": true }) }) : null
9302
9975
  ] }),
9303
- selected ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
9976
+ selected ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
9304
9977
  ] });
9305
9978
  }
9306
9979
 
9980
+ // src/ui/views/SettingsTab.tsx
9981
+ var import_react34 = __toESM(require_react(), 1);
9982
+
9983
+ // src/ui/audio/ringtone-store.ts
9984
+ var stores2 = /* @__PURE__ */ new WeakMap();
9985
+ function ringtoneStore(softphone) {
9986
+ const source = softphone.sources?.ringtones;
9987
+ if (!source) return null;
9988
+ let store = stores2.get(softphone);
9989
+ if (!store) {
9990
+ store = createRingtoneStore(source);
9991
+ stores2.set(softphone, store);
9992
+ }
9993
+ return store;
9994
+ }
9995
+ function createRingtoneStore(source, decodeImpl = decodeRingtone) {
9996
+ let settings = null;
9997
+ let audioWanted = false;
9998
+ let loaded = null;
9999
+ let loading2 = null;
10000
+ let inFlight = null;
10001
+ const listeners = /* @__PURE__ */ new Set();
10002
+ const notify = () => {
10003
+ for (const l of listeners) l();
10004
+ };
10005
+ const effectiveUrl = () => {
10006
+ if (!settings) return null;
10007
+ return settings.catalogue.find((r) => r.id === settings.effective)?.url ?? null;
10008
+ };
10009
+ const loadEffective = async () => {
10010
+ const url = effectiveUrl();
10011
+ if (!audioWanted || !url || loaded?.url === url || loading2 === url) return;
10012
+ loading2 = url;
10013
+ try {
10014
+ const buffer = await decodeImpl(url);
10015
+ if (audioWanted && effectiveUrl() === url) {
10016
+ loaded = { url, buffer };
10017
+ notify();
10018
+ }
10019
+ } catch (error) {
10020
+ console.warn(`[softphone] ringtone could not be loaded, using the built-in tone: ${url}`, error);
10021
+ } finally {
10022
+ if (loading2 === url) loading2 = null;
10023
+ }
10024
+ };
10025
+ const apply = (next) => {
10026
+ settings = next;
10027
+ if (loaded && loaded.url !== effectiveUrl()) loaded = null;
10028
+ notify();
10029
+ };
10030
+ return {
10031
+ getSettings: () => settings,
10032
+ readyBuffer: () => loaded && loaded.url === effectiveUrl() ? loaded.buffer : null,
10033
+ readyLoop: () => {
10034
+ if (!loaded || loaded.url !== effectiveUrl() || !settings) return null;
10035
+ return settings.catalogue.find((r) => r.id === settings.effective)?.loop ?? null;
10036
+ },
10037
+ refresh() {
10038
+ inFlight ??= (async () => {
10039
+ if (!settings && source.cached) {
10040
+ const early = await source.cached().catch(() => null);
10041
+ if (early && !settings) {
10042
+ apply(early);
10043
+ void loadEffective();
10044
+ }
10045
+ }
10046
+ try {
10047
+ apply(await source.get());
10048
+ } catch (error) {
10049
+ console.warn("[softphone] ringtone settings could not be loaded; keeping the previous ones", error);
10050
+ } finally {
10051
+ inFlight = null;
10052
+ }
10053
+ await loadEffective();
10054
+ })();
10055
+ return inFlight;
10056
+ },
10057
+ async setMine(id) {
10058
+ apply(await source.setMine(id));
10059
+ void loadEffective();
10060
+ },
10061
+ wantAudio(on) {
10062
+ if (audioWanted === on) return;
10063
+ audioWanted = on;
10064
+ if (!on) {
10065
+ loaded = null;
10066
+ notify();
10067
+ } else {
10068
+ void loadEffective();
10069
+ }
10070
+ },
10071
+ decode: (url) => decodeImpl(url),
10072
+ subscribe(listener) {
10073
+ listeners.add(listener);
10074
+ return () => listeners.delete(listener);
10075
+ }
10076
+ };
10077
+ }
10078
+ async function decodeRingtone(url) {
10079
+ const Offline = typeof OfflineAudioContext !== "undefined" ? OfflineAudioContext : globalThis.webkitOfflineAudioContext;
10080
+ if (!Offline) throw new Error("this browser cannot decode audio files");
10081
+ const res = await fetch(url, { credentials: "omit" });
10082
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
10083
+ const bytes = await res.arrayBuffer();
10084
+ return new Offline(1, 1, 48e3).decodeAudioData(bytes);
10085
+ }
10086
+
9307
10087
  // src/ui/views/SettingsSection.tsx
9308
- var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
10088
+ var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
9309
10089
  function SettingsSection({
9310
10090
  title,
9311
10091
  children
9312
10092
  }) {
9313
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { className: "cv-settings-section", children: [
9314
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h3", { className: "cv-settings-section-title", children: title }),
9315
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "cv-settings-section-body", children })
10093
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("section", { className: "cv-settings-section", children: [
10094
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h3", { className: "cv-settings-section-title", children: title }),
10095
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "cv-settings-section-body", children })
9316
10096
  ] });
9317
10097
  }
9318
10098
 
10099
+ // src/ui/views/settings/SettingsNavRow.tsx
10100
+ var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
10101
+ function SettingsNavRow({
10102
+ icon: Icon3,
10103
+ title,
10104
+ value,
10105
+ onOpen,
10106
+ rowRef
10107
+ }) {
10108
+ const inner = /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
10109
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Icon3, { size: 16 }) }),
10110
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "cv-settings-nav-text", children: [
10111
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "cv-settings-nav-title", children: title }),
10112
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "cv-settings-nav-value", children: value })
10113
+ ] }),
10114
+ onOpen ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChevronRight, { size: 16, className: "cv-settings-nav-chevron", "aria-hidden": true }) : null
10115
+ ] });
10116
+ return onOpen ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: "cv-settings-nav", onClick: onOpen, ref: rowRef, children: inner }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "cv-settings-nav cv-settings-nav--static", children: inner });
10117
+ }
10118
+
10119
+ // src/ui/views/settings/DevicePage.tsx
10120
+ var import_react28 = __toESM(require_react(), 1);
10121
+
10122
+ // src/ui/views/settings/ChoiceList.tsx
10123
+ var import_react26 = __toESM(require_react(), 1);
10124
+ var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
10125
+ function ChoiceList({
10126
+ label,
10127
+ choices,
10128
+ value,
10129
+ onSelect,
10130
+ onReselect,
10131
+ disabled = false
10132
+ }) {
10133
+ const name = (0, import_react26.useId)();
10134
+ const pointer = (0, import_react26.useRef)(false);
10135
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, "aria-disabled": disabled || void 0, children: choices.map((c) => {
10136
+ const checked = c.value === value;
10137
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
10138
+ "label",
10139
+ {
10140
+ className: "cv-choice",
10141
+ "data-checked": checked || void 0,
10142
+ "data-disabled": disabled || void 0,
10143
+ onPointerDown: () => {
10144
+ pointer.current = true;
10145
+ },
10146
+ children: [
10147
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
10148
+ "input",
10149
+ {
10150
+ type: "radio",
10151
+ className: "cv-choice-input",
10152
+ name,
10153
+ value: c.value,
10154
+ checked,
10155
+ onChange: () => {
10156
+ const via = pointer.current ? "pointer" : "keyboard";
10157
+ pointer.current = false;
10158
+ if (!disabled) onSelect(c.value, via);
10159
+ },
10160
+ onClick: () => {
10161
+ if (c.value === value) {
10162
+ pointer.current = false;
10163
+ if (!disabled) onReselect?.(c.value);
10164
+ }
10165
+ }
10166
+ }
10167
+ ),
10168
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "cv-choice-text", children: [
10169
+ c.tag ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "cv-choice-line", children: [
10170
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "cv-choice-label", children: c.label }),
10171
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "cv-choice-tag", children: c.tag })
10172
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "cv-choice-label", children: c.label }),
10173
+ c.sub ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "cv-choice-sub", children: c.sub }) : null,
10174
+ c.hint ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "cv-choice-hint", children: c.hint }) : null
10175
+ ] }),
10176
+ c.trailing,
10177
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Check, { size: 16, className: "cv-choice-check", "aria-hidden": true })
10178
+ ]
10179
+ },
10180
+ c.value
10181
+ );
10182
+ }) });
10183
+ }
10184
+
10185
+ // src/ui/views/settings/SettingsPage.tsx
10186
+ var import_react27 = __toESM(require_react(), 1);
10187
+ var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
10188
+ function SettingsPage({
10189
+ title,
10190
+ intro,
10191
+ onBack,
10192
+ children
10193
+ }) {
10194
+ const titleId = (0, import_react27.useId)();
10195
+ const heading = (0, import_react27.useRef)(null);
10196
+ (0, import_react27.useEffect)(() => heading.current?.focus(), []);
10197
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
10198
+ "div",
10199
+ {
10200
+ className: "cv-detail cv-settings-page",
10201
+ role: "region",
10202
+ "aria-labelledby": titleId,
10203
+ onKeyDown: (e) => {
10204
+ if (e.key === "Escape") {
10205
+ e.stopPropagation();
10206
+ onBack();
10207
+ }
10208
+ },
10209
+ children: [
10210
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "cv-detail-head", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
10211
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ArrowLeft, { size: 16, "aria-hidden": true }),
10212
+ "Ajustes"
10213
+ ] }) }),
10214
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "cv-settings-page-body", children: [
10215
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h2", { className: "cv-settings-page-title", id: titleId, tabIndex: -1, ref: heading, children: title }),
10216
+ intro ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "cv-settings-page-intro", children: intro }) : null,
10217
+ children
10218
+ ] })
10219
+ ]
10220
+ }
10221
+ );
10222
+ }
10223
+
10224
+ // src/ui/views/settings/DevicePage.tsx
10225
+ var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
10226
+ var SYSTEM_DEFAULT = "";
10227
+ var PAGES = {
10228
+ input: {
10229
+ title: "Micr\xF3fono",
10230
+ intro: "El micr\xF3fono que usa el tel\xE9fono en las llamadas.",
10231
+ empty: "No se encontr\xF3 ning\xFAn micr\xF3fono."
10232
+ },
10233
+ output: {
10234
+ title: "Altavoces",
10235
+ intro: "D\xF3nde oyes a la otra persona durante una llamada.",
10236
+ empty: "No se encontr\xF3 ning\xFAn altavoz."
10237
+ },
10238
+ ringtone: {
10239
+ title: "Salida del tono",
10240
+ intro: "D\xF3nde suena el tono cuando entra una llamada. La llamada se oye por \xABAltavoces\xBB.",
10241
+ empty: "No se encontr\xF3 ning\xFAn altavoz."
10242
+ }
10243
+ };
10244
+ function deviceSummary(kind, list, id) {
10245
+ const i = id ? list.findIndex((d) => d.deviceId === id) : -1;
10246
+ return i >= 0 ? deviceLabel(list[i], i, kind) : "Predeterminado del sistema";
10247
+ }
10248
+ function DevicePage({ kind, onBack }) {
10249
+ const devices = useDevices();
10250
+ const page = PAGES[kind];
10251
+ const list = kind === "input" ? devices.inputs : devices.outputs;
10252
+ const current = kind === "input" ? devices.currentInputId : kind === "output" ? devices.currentOutputId : devices.currentRingtoneOutputId;
10253
+ const apply = (id) => kind === "input" ? devices.setInput(id) : kind === "output" ? devices.setOutput(id) : devices.setRingtoneOutput(id);
10254
+ const choices = [
10255
+ { value: SYSTEM_DEFAULT, label: "Predeterminado del sistema" },
10256
+ ...list.map((d, i) => ({ value: d.deviceId, label: deviceLabel(d, i, kind) }))
10257
+ ];
10258
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(SettingsPage, { title: page.title, intro: page.intro, onBack, children: [
10259
+ list.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
10260
+ ChoiceList,
10261
+ {
10262
+ label: page.title,
10263
+ choices,
10264
+ value: current ?? SYSTEM_DEFAULT,
10265
+ onSelect: (v) => void apply(v === SYSTEM_DEFAULT ? null : v)
10266
+ }
10267
+ ) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "cv-settings-page-empty", children: page.empty }),
10268
+ kind === "output" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SpeakerTest, { sinkId: devices.currentOutputId }) : null
10269
+ ] });
10270
+ }
10271
+ function SpeakerTest({ sinkId }) {
10272
+ const [testing, setTesting] = (0, import_react28.useState)(false);
10273
+ const tone = (0, import_react28.useRef)(null);
10274
+ const timer = (0, import_react28.useRef)(null);
10275
+ (0, import_react28.useEffect)(
10276
+ () => () => {
10277
+ tone.current?.stop();
10278
+ if (timer.current) clearTimeout(timer.current);
10279
+ },
10280
+ []
10281
+ );
10282
+ const test = () => {
10283
+ primeAudio();
10284
+ tone.current?.stop();
10285
+ tone.current = playTestTone(sinkId);
10286
+ setTesting(true);
10287
+ if (timer.current) clearTimeout(timer.current);
10288
+ timer.current = setTimeout(() => setTesting(false), TEST_TONE_MS);
10289
+ };
10290
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", className: "cv-btn cv-settings-test", onClick: test, disabled: testing, children: [
10291
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Volume2, { size: 16, "aria-hidden": true }),
10292
+ testing ? "Sonando\u2026" : "Probar altavoz"
10293
+ ] });
10294
+ }
10295
+
10296
+ // src/ui/views/settings/RingtonePage.tsx
10297
+ var import_react31 = __toESM(require_react(), 1);
10298
+
10299
+ // src/ui/views/settings/ringtone-state.ts
10300
+ var import_react30 = __toESM(require_react(), 1);
10301
+
10302
+ // src/ui/audio/preview.ts
10303
+ var PREVIEW_MAX_MS = 1e4;
10304
+ var BUILT_IN_PREVIEW_MS = 6e3;
10305
+ function previewDurationMs(buffer, loop) {
10306
+ if (!buffer) return BUILT_IN_PREVIEW_MS;
10307
+ const seconds = loop ? loop.end - loop.start : buffer.duration;
10308
+ return Math.min(seconds * 1e3, PREVIEW_MAX_MS);
10309
+ }
10310
+
10311
+ // src/ui/views/settings/ringtone-state.ts
10312
+ var noSubscribe2 = () => () => {
10313
+ };
10314
+ var noSettings2 = () => null;
10315
+ function useRingtoneSettings(store) {
10316
+ const settings = (0, import_react30.useSyncExternalStore)(store?.subscribe ?? noSubscribe2, store?.getSettings ?? noSettings2);
10317
+ const [failed, setFailed] = (0, import_react30.useState)(false);
10318
+ const load = (0, import_react30.useCallback)(() => {
10319
+ if (!store) return () => {
10320
+ };
10321
+ let live = true;
10322
+ setFailed(false);
10323
+ void store.refresh().then(() => {
10324
+ if (live) setFailed(store.getSettings() === null);
10325
+ });
10326
+ return () => {
10327
+ live = false;
10328
+ };
10329
+ }, [store]);
10330
+ (0, import_react30.useEffect)(load, [load]);
10331
+ return { settings, failed, retry: load };
10332
+ }
10333
+ function ringtoneSummary(settings, failed) {
10334
+ if (!settings) return failed ? "No disponible" : "Cargando\u2026";
10335
+ const byId = (id) => id ? settings.catalogue.find((r) => r.id === id) : void 0;
10336
+ const mine = byId(settings.mine);
10337
+ if (mine) return mine.name;
10338
+ const org = byId(settings.orgDefault);
10339
+ if (org) return `${org.name} \xB7 organizaci\xF3n`;
10340
+ return settings.catalogue.find((r) => r.url === null)?.name ?? "Predeterminado";
10341
+ }
10342
+ function useRingtonePreview(store, sinkId) {
10343
+ const [playing, setPlaying] = (0, import_react30.useState)(null);
10344
+ const [error, setError] = (0, import_react30.useState)(null);
10345
+ const current = (0, import_react30.useRef)(null);
10346
+ const generation = (0, import_react30.useRef)(0);
10347
+ const stop = (0, import_react30.useCallback)(() => {
10348
+ generation.current += 1;
10349
+ const c = current.current;
10350
+ current.current = null;
10351
+ if (c?.timer) clearTimeout(c.timer);
10352
+ c?.tone?.stop();
10353
+ setPlaying(null);
10354
+ }, []);
10355
+ (0, import_react30.useEffect)(() => stop, [stop]);
10356
+ const start = (settings, choice) => {
10357
+ stop();
10358
+ primeAudio();
10359
+ setError(null);
10360
+ const gen = generation.current;
10361
+ const orgDefault = settings.catalogue.some((r) => r.id === settings.orgDefault) ? settings.orgDefault : null;
10362
+ const id = choice || orgDefault;
10363
+ const option = settings.catalogue.find((r) => r.id === id) ?? settings.catalogue.find((r) => r.url === null);
10364
+ setPlaying(choice);
10365
+ current.current = { tone: null, timer: null };
10366
+ const play = (buffer) => {
10367
+ if (gen !== generation.current) return;
10368
+ const loop = buffer ? option?.loop ?? null : null;
10369
+ const tone = playRingtone({ buffer, loop, sinkId });
10370
+ current.current = { tone, timer: setTimeout(stop, previewDurationMs(buffer, loop)) };
10371
+ };
10372
+ if (!option?.url) {
10373
+ play(null);
10374
+ return;
10375
+ }
10376
+ store.decode(option.url).then(play, () => {
10377
+ if (gen !== generation.current) return;
10378
+ stop();
10379
+ setError("No se pudo reproducir el tono.");
10380
+ });
10381
+ };
10382
+ return { playing, error, start, stop };
10383
+ }
10384
+
10385
+ // src/ui/views/settings/RingtonePage.tsx
10386
+ var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
10387
+ var ORG_DEFAULT = "";
10388
+ var KEYBOARD_SETTLE_MS = 300;
10389
+ function RingtonePage({
10390
+ store,
10391
+ settings,
10392
+ failed,
10393
+ onRetry,
10394
+ onBack
10395
+ }) {
10396
+ const softphone = useSoftphone();
10397
+ const devices = useDevices();
10398
+ const preview = useRingtonePreview(store, devices.canSelectRingtoneOutput ? devices.currentRingtoneOutputId : null);
10399
+ const [pending, setPending] = (0, import_react31.useState)(void 0);
10400
+ const [error, setError] = (0, import_react31.useState)(null);
10401
+ const save = useSerialSave(store, {
10402
+ onSettled: () => setPending(void 0),
10403
+ onFailed: () => {
10404
+ setPending(void 0);
10405
+ setError("No se pudo guardar el tono. Int\xE9ntalo de nuevo.");
10406
+ }
10407
+ });
10408
+ const latest = (0, import_react31.useRef)(settings);
10409
+ latest.current = settings;
10410
+ const settle = (0, import_react31.useRef)(null);
10411
+ const { stop: stopPreview } = preview;
10412
+ (0, import_react31.useEffect)(() => softphone.on("incomingCall" /* IncomingCall */, stopPreview), [softphone, stopPreview]);
10413
+ (0, import_react31.useEffect)(
10414
+ () => () => {
10415
+ if (settle.current) {
10416
+ clearTimeout(settle.current.timer);
10417
+ save(settle.current.value || null);
10418
+ }
10419
+ },
10420
+ [save]
10421
+ );
10422
+ const commit = (value) => {
10423
+ save(value || null);
10424
+ if (latest.current) preview.start(latest.current, value);
10425
+ };
10426
+ const onSelect = (value, via) => {
10427
+ setError(null);
10428
+ setPending(value);
10429
+ if (settle.current) clearTimeout(settle.current.timer);
10430
+ settle.current = null;
10431
+ if (via === "pointer") {
10432
+ commit(value);
10433
+ return;
10434
+ }
10435
+ preview.stop();
10436
+ settle.current = {
10437
+ value,
10438
+ timer: setTimeout(() => {
10439
+ settle.current = null;
10440
+ commit(value);
10441
+ }, KEYBOARD_SETTLE_MS)
10442
+ };
10443
+ };
10444
+ const onReselect = (value) => {
10445
+ if (!settings) return;
10446
+ if (preview.playing === value) preview.stop();
10447
+ else preview.start(settings, value);
10448
+ };
10449
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
10450
+ SettingsPage,
10451
+ {
10452
+ title: "Tono de llamada",
10453
+ intro: "Lo que suena cuando entra una llamada. Se guarda en tu cuenta, as\xED que es el mismo en todos tus dispositivos.",
10454
+ onBack,
10455
+ children: settings ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
10456
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
10457
+ ChoiceList,
10458
+ {
10459
+ label: "Tono de llamada",
10460
+ choices: choicesFor(settings, preview.playing),
10461
+ value: pending !== void 0 ? pending : currentValue(settings),
10462
+ onSelect,
10463
+ onReselect
10464
+ }
10465
+ ),
10466
+ error ?? preview.error ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "cv-settings-error", role: "alert", children: error ?? preview.error }) : null
10467
+ ] }) : failed ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "cv-empty", children: [
10468
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "cv-empty-title", children: "No se pudieron cargar los tonos." }),
10469
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "cv-btn cv-settings-retry", onClick: onRetry, children: "Reintentar" })
10470
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "cv-empty", role: "status", children: [
10471
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
10472
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "cv-empty-title", children: "Cargando\u2026" })
10473
+ ] })
10474
+ }
10475
+ );
10476
+ }
10477
+ function currentValue(settings) {
10478
+ return settings.mine && settings.catalogue.some((r) => r.id === settings.mine) ? settings.mine : ORG_DEFAULT;
10479
+ }
10480
+ function choicesFor(settings, playing) {
10481
+ const org = settings.orgDefault ? settings.catalogue.find((r) => r.id === settings.orgDefault) : void 0;
10482
+ const builtIn = settings.catalogue.find((r) => r.url === null);
10483
+ const mark = (value) => playing === value ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "cv-choice-playing", children: [
10484
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Volume2, { size: 15, "aria-hidden": true }),
10485
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "cv-sr-only", children: "Sonando" })
10486
+ ] }) : void 0;
10487
+ const orgChoice = {
10488
+ value: ORG_DEFAULT,
10489
+ label: org ? "Predeterminado de la organizaci\xF3n" : "Predeterminado",
10490
+ sub: (org ?? builtIn)?.name ?? "",
10491
+ trailing: mark(ORG_DEFAULT)
10492
+ };
10493
+ return [orgChoice, ...settings.catalogue.map((r) => ({ value: r.id, label: r.name, trailing: mark(r.id) }))];
10494
+ }
10495
+ function useSerialSave(store, handlers) {
10496
+ const wanted = (0, import_react31.useRef)(null);
10497
+ const busy = (0, import_react31.useRef)(false);
10498
+ const on = (0, import_react31.useRef)(handlers);
10499
+ on.current = handlers;
10500
+ const [save] = (0, import_react31.useState)(() => (id) => {
10501
+ wanted.current = { id };
10502
+ if (busy.current) return;
10503
+ busy.current = true;
10504
+ void (async () => {
10505
+ while (wanted.current) {
10506
+ const { id: next } = wanted.current;
10507
+ wanted.current = null;
10508
+ try {
10509
+ await store.setMine(next);
10510
+ if (!wanted.current) on.current.onSettled();
10511
+ } catch {
10512
+ if (!wanted.current) on.current.onFailed();
10513
+ }
10514
+ }
10515
+ busy.current = false;
10516
+ })();
10517
+ });
10518
+ return save;
10519
+ }
10520
+
10521
+ // src/ui/views/settings/OutboundNumberPage.tsx
10522
+ var import_react33 = __toESM(require_react(), 1);
10523
+ var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
10524
+ var ORG_DEFAULT2 = "";
10525
+ var KEYBOARD_SETTLE_MS2 = 800;
10526
+ function OutboundNumberPage({
10527
+ store,
10528
+ settings,
10529
+ onBack
10530
+ }) {
10531
+ const formatIntl2 = useIntlFormatter();
10532
+ const current = numberName(effectiveNumber(settings), formatIntl2);
10533
+ const change = useOutboundNumberChange(store, settings, current);
10534
+ const [pending, setPending] = (0, import_react33.useState)(void 0);
10535
+ const settle = (0, import_react33.useRef)(null);
10536
+ const choose = (0, import_react33.useRef)(change.choose);
10537
+ choose.current = change.choose;
10538
+ (0, import_react33.useEffect)(
10539
+ () => () => {
10540
+ if (settle.current) clearTimeout(settle.current);
10541
+ },
10542
+ []
10543
+ );
10544
+ const notice = (0, import_react33.useRef)(null);
10545
+ const apply = async (value) => {
10546
+ setPending(value);
10547
+ const applied = await choose.current(value || null);
10548
+ setPending(void 0);
10549
+ if (!applied) requestAnimationFrame(() => notice.current?.scrollIntoView?.({ block: "nearest" }));
10550
+ };
10551
+ const onSelect = (value, via) => {
10552
+ if (settle.current) clearTimeout(settle.current);
10553
+ settle.current = null;
10554
+ if (via === "pointer") {
10555
+ void apply(value);
10556
+ return;
10557
+ }
10558
+ setPending(value);
10559
+ settle.current = setTimeout(() => {
10560
+ settle.current = null;
10561
+ void apply(value);
10562
+ }, KEYBOARD_SETTLE_MS2);
10563
+ };
10564
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
10565
+ SettingsPage,
10566
+ {
10567
+ title: "N\xFAmero de salida",
10568
+ intro: "El n\xFAmero que ve la persona a la que llamas. Es el mismo en todos tus dispositivos y tambi\xE9n puedes cambiarlo desde el teclado.",
10569
+ onBack,
10570
+ children: [
10571
+ change.message ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "cv-settings-notice", role: "status", "data-kind": change.isError ? "error" : "info", ref: notice, children: change.message }) : null,
10572
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
10573
+ ChoiceList,
10574
+ {
10575
+ label: "N\xFAmero de salida",
10576
+ choices: choicesFor2(settings, formatIntl2, change.busy ? pending : void 0),
10577
+ value: pending ?? currentValue2(settings),
10578
+ onSelect,
10579
+ disabled: change.locked
10580
+ }
10581
+ )
10582
+ ]
10583
+ }
10584
+ );
10585
+ }
10586
+ function currentValue2(settings) {
10587
+ return settings.mine && settings.numbers.some((n) => n.id === settings.mine) ? settings.mine : ORG_DEFAULT2;
10588
+ }
10589
+ function choicesFor2(settings, formatIntl2, applying) {
10590
+ const orgDefault = settings.numbers.find((n) => n.id === settings.orgDefault);
10591
+ const spinner = (value) => applying === value ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "cv-choice-applying", children: [
10592
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(LoaderCircle, { size: 15, className: "cv-choice-applying-icon", "aria-hidden": true }),
10593
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "cv-sr-only", children: "Aplicando" })
10594
+ ] }) : void 0;
10595
+ return [
10596
+ {
10597
+ value: ORG_DEFAULT2,
10598
+ label: "Por defecto de la organizaci\xF3n",
10599
+ sub: orgDefault ? numberName(orgDefault, formatIntl2) : "Sin n\xFAmero por defecto",
10600
+ trailing: spinner(ORG_DEFAULT2)
10601
+ },
10602
+ ...settings.numbers.map((n) => {
10603
+ const kind = KIND_TAG[n.kind];
10604
+ return {
10605
+ value: n.id,
10606
+ label: numberName(n, formatIntl2),
10607
+ ...n.label ? { sub: formatIntl2(`+${n.number}`) } : {},
10608
+ ...kind ? { tag: kind.tag, hint: kind.hint } : {},
10609
+ trailing: spinner(n.id)
10610
+ };
10611
+ })
10612
+ ];
10613
+ }
10614
+ function outboundSummary(settings, formatIntl2) {
10615
+ const effective = effectiveNumber(settings);
10616
+ const name = numberName(effective, formatIntl2);
10617
+ return settings.mine || !effective ? name : `${name} \xB7 organizaci\xF3n`;
10618
+ }
10619
+
9319
10620
  // src/ui/views/SettingsTab.tsx
9320
- var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
10621
+ var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
9321
10622
  function SettingsTab() {
9322
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "cv-view cv-settings", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "cv-settings-body", children: [
9323
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SettingsSection, { title: "Dispositivos", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(DevicePickers, {}) }),
9324
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "cv-settings-more", children: "M\xE1s ajustes se ir\xE1n a\xF1adiendo pr\xF3ximamente." })
9325
- ] }) });
10623
+ const softphone = useSoftphone();
10624
+ const devices = useDevices();
10625
+ const store = ringtoneStore(softphone);
10626
+ const ringtone = useRingtoneSettings(store);
10627
+ const outStore = outboundNumberStore(softphone);
10628
+ const outbound = useOutboundNumbers(outStore).settings;
10629
+ const formatIntl2 = useIntlFormatter();
10630
+ const [page, setPage] = (0, import_react34.useState)(null);
10631
+ const list = (0, import_react34.useRef)(null);
10632
+ const rows = (0, import_react34.useRef)({});
10633
+ const returnTo = (0, import_react34.useRef)(null);
10634
+ (0, import_react34.useEffect)(() => {
10635
+ void softphone.audio.refresh();
10636
+ }, [softphone]);
10637
+ (0, import_react34.useEffect)(() => {
10638
+ list.current?.toggleAttribute("inert", page !== null);
10639
+ if (page === null && returnTo.current) {
10640
+ rows.current[returnTo.current]?.focus();
10641
+ returnTo.current = null;
10642
+ }
10643
+ }, [page]);
10644
+ const open = (p) => () => {
10645
+ returnTo.current = p;
10646
+ setPage(p);
10647
+ };
10648
+ const back = () => setPage(null);
10649
+ const rowRef = (p) => (el) => {
10650
+ rows.current[p] = el;
10651
+ };
10652
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "cv-view cv-settings", children: [
10653
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "cv-settings-body", ref: list, children: [
10654
+ store ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SettingsSection, { title: "Llamadas entrantes", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
10655
+ SettingsNavRow,
10656
+ {
10657
+ icon: Music,
10658
+ title: "Tono de llamada",
10659
+ value: ringtoneSummary(ringtone.settings, ringtone.failed),
10660
+ onOpen: open("tone"),
10661
+ rowRef: rowRef("tone")
10662
+ }
10663
+ ) }) : null,
10664
+ outStore && outbound && outbound.numbers.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SettingsSection, { title: "Llamadas salientes", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
10665
+ SettingsNavRow,
10666
+ {
10667
+ icon: PhoneOutgoing,
10668
+ title: "N\xFAmero de salida",
10669
+ value: outboundSummary(outbound, formatIntl2),
10670
+ onOpen: open("outbound"),
10671
+ rowRef: rowRef("outbound")
10672
+ }
10673
+ ) }) : null,
10674
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(SettingsSection, { title: "Dispositivos", children: [
10675
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
10676
+ SettingsNavRow,
10677
+ {
10678
+ icon: Mic,
10679
+ title: "Micr\xF3fono",
10680
+ value: deviceSummary("input", devices.inputs, devices.currentInputId),
10681
+ onOpen: open("input"),
10682
+ rowRef: rowRef("input")
10683
+ }
10684
+ ),
10685
+ devices.canSelectOutput ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
10686
+ SettingsNavRow,
10687
+ {
10688
+ icon: Headphones,
10689
+ title: "Altavoces",
10690
+ value: deviceSummary("output", devices.outputs, devices.currentOutputId),
10691
+ onOpen: open("output"),
10692
+ rowRef: rowRef("output")
10693
+ }
10694
+ ) : (
10695
+ // Safari and most of Firefox have no `setSinkId`: offering a page whose choice silently does
10696
+ // nothing is worse than saying where the choice really lives.
10697
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
10698
+ SettingsNavRow,
10699
+ {
10700
+ icon: Headphones,
10701
+ title: "Altavoces",
10702
+ value: "Tu navegador no permite elegirlos desde aqu\xED \u2014 c\xE1mbialos en los ajustes del sistema."
10703
+ }
10704
+ )
10705
+ ),
10706
+ devices.canSelectRingtoneOutput ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
10707
+ SettingsNavRow,
10708
+ {
10709
+ icon: BellRing,
10710
+ title: "Salida del tono",
10711
+ value: deviceSummary("ringtone", devices.outputs, devices.currentRingtoneOutputId),
10712
+ onOpen: open("ringtone"),
10713
+ rowRef: rowRef("ringtone")
10714
+ }
10715
+ ) : null
10716
+ ] })
10717
+ ] }),
10718
+ page === "tone" && store ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
10719
+ RingtonePage,
10720
+ {
10721
+ store,
10722
+ settings: ringtone.settings,
10723
+ failed: ringtone.failed,
10724
+ onRetry: ringtone.retry,
10725
+ onBack: back
10726
+ }
10727
+ ) : page === "outbound" && outStore && outbound ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(OutboundNumberPage, { store: outStore, settings: outbound, onBack: back }) : page !== null && page !== "tone" && page !== "outbound" ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DevicePage, { kind: page, onBack: back }) : null
10728
+ ] });
9326
10729
  }
9327
10730
 
9328
10731
  // src/ui/permission/useMicPermission.ts
9329
- var import_react22 = __toESM(require_react(), 1);
10732
+ var import_react36 = __toESM(require_react(), 1);
9330
10733
 
9331
10734
  // src/core/env.ts
9332
10735
  function isBrowser() {
@@ -9338,8 +10741,8 @@ function isWebRtcSupported() {
9338
10741
 
9339
10742
  // src/ui/permission/useMicPermission.ts
9340
10743
  function useMicPermission() {
9341
- const [state, setState] = (0, import_react22.useState)("checking");
9342
- const warm = (0, import_react22.useCallback)(async () => {
10744
+ const [state, setState] = (0, import_react36.useState)("checking");
10745
+ const warm = (0, import_react36.useCallback)(async () => {
9343
10746
  try {
9344
10747
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
9345
10748
  for (const track of stream.getTracks()) track.stop();
@@ -9348,7 +10751,7 @@ function useMicPermission() {
9348
10751
  setState("denied");
9349
10752
  }
9350
10753
  }, []);
9351
- (0, import_react22.useEffect)(() => {
10754
+ (0, import_react36.useEffect)(() => {
9352
10755
  if (!isWebRtcSupported()) {
9353
10756
  setState("unsupported");
9354
10757
  return;
@@ -9439,60 +10842,60 @@ function unblockGuide() {
9439
10842
  }
9440
10843
 
9441
10844
  // src/ui/permission/MicPermissionGate.tsx
9442
- var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
10845
+ var import_jsx_runtime21 = __toESM(require_jsx_runtime(), 1);
9443
10846
  function MicPrompt({
9444
10847
  state,
9445
10848
  onRequest
9446
10849
  }) {
9447
10850
  if (state === "unsupported") {
9448
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "cv-gate", "data-state": state, "data-tone": "danger", children: [
9449
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MicOff, { size: 22 }) }),
9450
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "cv-gate-title", children: "Llamadas no disponibles" }),
9451
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox." })
10851
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "cv-gate", "data-state": state, "data-tone": "danger", children: [
10852
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(MicOff, { size: 22 }) }),
10853
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { className: "cv-gate-title", children: "Llamadas no disponibles" }),
10854
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox." })
9452
10855
  ] });
9453
10856
  }
9454
10857
  if (state === "denied") {
9455
10858
  const guide = unblockGuide();
9456
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "cv-gate", "data-state": state, "data-tone": "danger", children: [
9457
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MicOff, { size: 22 }) }),
9458
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "cv-gate-title", children: "Micr\xF3fono bloqueado" }),
9459
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Perm\xEDtelo en tu navegador para poder llamar." }),
9460
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("details", { className: "cv-help", children: [
9461
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("summary", { children: [
9462
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CircleHelp, { size: 13, "aria-hidden": true }),
10859
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "cv-gate", "data-state": state, "data-tone": "danger", children: [
10860
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(MicOff, { size: 22 }) }),
10861
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { className: "cv-gate-title", children: "Micr\xF3fono bloqueado" }),
10862
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: "Perm\xEDtelo en tu navegador para poder llamar." }),
10863
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("details", { className: "cv-help", children: [
10864
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("summary", { children: [
10865
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CircleHelp, { size: 13, "aria-hidden": true }),
9463
10866
  "C\xF3mo permitirlo"
9464
10867
  ] }),
9465
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("ol", { children: guide.steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("li", { children: step }, step)) }),
9466
- guide.url ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("a", { href: guide.url, target: "_blank", rel: "noreferrer noopener", children: [
10868
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("ol", { children: guide.steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("li", { children: step }, step)) }),
10869
+ guide.url ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("a", { href: guide.url, target: "_blank", rel: "noreferrer noopener", children: [
9467
10870
  "Gu\xEDa del navegador",
9468
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ExternalLink, { size: 11, "aria-hidden": true })
10871
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ExternalLink, { size: 11, "aria-hidden": true })
9469
10872
  ] }) : null
9470
10873
  ] })
9471
10874
  ] });
9472
10875
  }
9473
10876
  if (state === "warming") {
9474
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "cv-gate", "data-state": state, children: [
9475
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Mic, { size: 22 }) }),
9476
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
9477
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Acepta el aviso que muestra el navegador para continuar." })
10877
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "cv-gate", "data-state": state, children: [
10878
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Mic, { size: 22 }) }),
10879
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
10880
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: "Acepta el aviso que muestra el navegador para continuar." })
9478
10881
  ] });
9479
10882
  }
9480
10883
  if (state === "checking") {
9481
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "cv-gate", "data-state": state, children: [
9482
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Mic, { size: 22 }) }),
9483
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Preparando el micr\xF3fono\u2026" })
10884
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "cv-gate", "data-state": state, children: [
10885
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Mic, { size: 22 }) }),
10886
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: "Preparando el micr\xF3fono\u2026" })
9484
10887
  ] });
9485
10888
  }
9486
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "cv-gate", "data-state": state, children: [
9487
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Mic, { size: 22 }) }),
9488
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
9489
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Lo necesitamos para que puedas hablar durante las llamadas." }),
9490
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { className: "cv-btn cv-btn--accent", onClick: onRequest, children: "Permitir micr\xF3fono" })
10889
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "cv-gate", "data-state": state, children: [
10890
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Mic, { size: 22 }) }),
10891
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
10892
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: "Lo necesitamos para que puedas hablar durante las llamadas." }),
10893
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("button", { className: "cv-btn cv-btn--accent", onClick: onRequest, children: "Permitir micr\xF3fono" })
9491
10894
  ] });
9492
10895
  }
9493
10896
 
9494
10897
  // src/ui/permission/ErrorGate.tsx
9495
- var import_jsx_runtime15 = (
10898
+ var import_jsx_runtime22 = (
9496
10899
  /*
9497
10900
  `part` on all four, because this is the most intrusive thing the SDK draws — it replaces the
9498
10901
  whole dialpad — and inside `<coventia-softphone>` a shadow root makes it otherwise unreachable
@@ -9508,36 +10911,36 @@ function ErrorGate({
9508
10911
  onRetry
9509
10912
  }) {
9510
10913
  const text = ERROR_TEXT[error.code];
9511
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "cv-gate", "data-tone": "danger", "data-code": error.code, part: "error-gate", children: [
9512
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(TriangleAlert, { size: 22 }) }),
9513
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "cv-gate-title", part: "error-title", children: text.title }),
9514
- text.body === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { part: "error-body", children: text.body }),
9515
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { className: "cv-btn cv-btn--accent", onClick: onRetry, part: "error-retry", children: "Reintentar" })
10914
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "cv-gate", "data-tone": "danger", "data-code": error.code, part: "error-gate", children: [
10915
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TriangleAlert, { size: 22 }) }),
10916
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("h3", { className: "cv-gate-title", part: "error-title", children: text.title }),
10917
+ text.body === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { part: "error-body", children: text.body }),
10918
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("button", { className: "cv-btn cv-btn--accent", onClick: onRetry, part: "error-retry", children: "Reintentar" })
9516
10919
  ] });
9517
10920
  }
9518
10921
 
9519
10922
  // src/ui/permission/LeaderGate.tsx
9520
- var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
10923
+ var import_jsx_runtime23 = __toESM(require_jsx_runtime(), 1);
9521
10924
  function LeaderGate() {
9522
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "cv-gate", part: "leader-gate", children: [
9523
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(MonitorSmartphone, { size: 22 }) }),
9524
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "cv-gate-title", part: "leader-title", children: "Tel\xE9fono activo en otra pesta\xF1a" }),
9525
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { part: "leader-body", children: "Solo una pesta\xF1a puede usar el tel\xE9fono a la vez. Cierra la otra para activar esta." })
10925
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "cv-gate", part: "leader-gate", children: [
10926
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "cv-gate-mark", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(MonitorSmartphone, { size: 22 }) }),
10927
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h3", { className: "cv-gate-title", part: "leader-title", children: "Tel\xE9fono activo en otra pesta\xF1a" }),
10928
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { part: "leader-body", children: "Solo una pesta\xF1a puede usar el tel\xE9fono a la vez. Cierra la otra para activar esta." })
9526
10929
  ] });
9527
10930
  }
9528
10931
 
9529
10932
  // src/ui/lib/useCallOutcome.ts
9530
- var import_react23 = __toESM(require_react(), 1);
10933
+ var import_react37 = __toESM(require_react(), 1);
9531
10934
  var BACKSTOP_MS = 6e3;
9532
10935
  function useCallOutcome() {
9533
10936
  const softphone = useSoftphone();
9534
- const [code, setCode] = (0, import_react23.useState)(null);
9535
- const timer = (0, import_react23.useRef)(void 0);
9536
- const clear = (0, import_react23.useCallback)(() => {
10937
+ const [code, setCode] = (0, import_react37.useState)(null);
10938
+ const timer = (0, import_react37.useRef)(void 0);
10939
+ const clear = (0, import_react37.useCallback)(() => {
9537
10940
  clearTimeout(timer.current);
9538
10941
  setCode(null);
9539
10942
  }, []);
9540
- (0, import_react23.useEffect)(() => {
10943
+ (0, import_react37.useEffect)(() => {
9541
10944
  const off = softphone.on("error" /* Error */, (error) => {
9542
10945
  if (error.fatal) return;
9543
10946
  clearTimeout(timer.current);
@@ -9549,7 +10952,7 @@ function useCallOutcome() {
9549
10952
  clearTimeout(timer.current);
9550
10953
  };
9551
10954
  }, [softphone]);
9552
- (0, import_react23.useEffect)(() => softphone.on("incomingCall" /* IncomingCall */, () => clear()), [softphone, clear]);
10955
+ (0, import_react37.useEffect)(() => softphone.on("incomingCall" /* IncomingCall */, () => clear()), [softphone, clear]);
9553
10956
  return {
9554
10957
  outcome: code === null ? null : { code, text: ERROR_TEXT[code].title },
9555
10958
  clear
@@ -9557,12 +10960,12 @@ function useCallOutcome() {
9557
10960
  }
9558
10961
 
9559
10962
  // src/ui/audio/useCallAudio.ts
9560
- var import_react25 = __toESM(require_react(), 1);
10963
+ var import_react39 = __toESM(require_react(), 1);
9561
10964
  function useCallAudio(enabled) {
9562
10965
  const softphone = useSoftphone();
9563
- const remote = (0, import_react25.useRef)(null);
9564
- const tone = (0, import_react25.useRef)(null);
9565
- (0, import_react25.useEffect)(() => {
10966
+ const remote = (0, import_react39.useRef)(null);
10967
+ const tone = (0, import_react39.useRef)(null);
10968
+ (0, import_react39.useEffect)(() => {
9566
10969
  if (!enabled || typeof document === "undefined") return;
9567
10970
  const prime = () => primeAudio();
9568
10971
  const gestures = ["pointerdown", "keydown"];
@@ -9574,14 +10977,29 @@ function useCallAudio(enabled) {
9574
10977
  const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
9575
10978
  void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
9576
10979
  });
10980
+ const ringtones = ringtoneStore(softphone);
10981
+ ringtones?.wantAudio(true);
10982
+ const refreshRingtone = () => {
10983
+ if (softphone.isLeader) void ringtones?.refresh();
10984
+ };
10985
+ refreshRingtone();
10986
+ const offLeader = softphone.on("leaderChanged" /* LeaderChanged */, ({ isLeader }) => {
10987
+ if (isLeader) refreshRingtone();
10988
+ });
9577
10989
  const stopTone = () => {
9578
10990
  tone.current?.stop();
9579
10991
  tone.current = null;
9580
10992
  };
9581
10993
  const setTone = (next) => {
9582
10994
  stopTone();
9583
- if (next === "ringtone") tone.current = playRingtone();
9584
- else if (next === "ringback") tone.current = playRingback();
10995
+ if (next === "ringtone") {
10996
+ const audio = softphone.audio;
10997
+ tone.current = playRingtone({
10998
+ buffer: ringtones?.readyBuffer() ?? null,
10999
+ loop: ringtones?.readyLoop() ?? null,
11000
+ sinkId: audio.canSelectRingtoneOutput ? audio.currentRingtoneOutputId : null
11001
+ });
11002
+ } else if (next === "ringback") tone.current = playRingback();
9585
11003
  };
9586
11004
  const perCall = [];
9587
11005
  const followedCalls = /* @__PURE__ */ new Set();
@@ -9612,6 +11030,7 @@ function useCallAudio(enabled) {
9612
11030
  stopTone();
9613
11031
  remote.current?.detach();
9614
11032
  if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
11033
+ refreshRingtone();
9615
11034
  })
9616
11035
  );
9617
11036
  };
@@ -9629,6 +11048,8 @@ function useCallAudio(enabled) {
9629
11048
  offIncoming();
9630
11049
  offActive();
9631
11050
  offDeviceChange();
11051
+ offLeader();
11052
+ ringtones?.wantAudio(false);
9632
11053
  for (const off of perCall) off();
9633
11054
  stopTone();
9634
11055
  remote.current?.destroy();
@@ -9638,7 +11059,7 @@ function useCallAudio(enabled) {
9638
11059
  }
9639
11060
 
9640
11061
  // src/ui/notices/TransferNotices.tsx
9641
- var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
11062
+ var import_jsx_runtime24 = __toESM(require_jsx_runtime(), 1);
9642
11063
  function TransferNotices({
9643
11064
  notices,
9644
11065
  dismiss
@@ -9650,7 +11071,7 @@ function TransferNotices({
9650
11071
  if (notice.caller) void softphone.call(notice.caller).catch(() => {
9651
11072
  });
9652
11073
  };
9653
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
11074
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
9654
11075
  "div",
9655
11076
  {
9656
11077
  className: "cv-transfer-notices",
@@ -9658,20 +11079,20 @@ function TransferNotices({
9658
11079
  part: "transfer-notices",
9659
11080
  "data-has-notices": notices.length > 0,
9660
11081
  children: [
9661
- notices.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "cv-transfer-notices-count", children: [
11082
+ notices.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("p", { className: "cv-transfer-notices-count", children: [
9662
11083
  notices.length,
9663
11084
  " traspasos sin conectar"
9664
11085
  ] }) : null,
9665
- notices.map((notice) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "cv-transfer-notice", part: "transfer-notice", children: [
9666
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "cv-transfer-notice-mark", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PhoneMissed, { size: 16 }) }),
9667
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "cv-transfer-notice-body", children: [
9668
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "cv-transfer-notice-header", children: [
9669
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "cv-transfer-notice-title", children: [
11086
+ notices.map((notice) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "cv-transfer-notice", part: "transfer-notice", children: [
11087
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "cv-transfer-notice-mark", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(PhoneMissed, { size: 16 }) }),
11088
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "cv-transfer-notice-body", children: [
11089
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "cv-transfer-notice-header", children: [
11090
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("p", { className: "cv-transfer-notice-title", children: [
9670
11091
  "El traspaso a ",
9671
11092
  notice.target,
9672
11093
  " no se conect\xF3"
9673
11094
  ] }),
9674
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
11095
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
9675
11096
  "button",
9676
11097
  {
9677
11098
  type: "button",
@@ -9679,17 +11100,17 @@ function TransferNotices({
9679
11100
  onClick: () => dismiss(notice.id),
9680
11101
  "aria-label": "Descartar aviso",
9681
11102
  part: "transfer-notice-dismiss",
9682
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(X, { size: 14 })
11103
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(X, { size: 14 })
9683
11104
  }
9684
11105
  )
9685
11106
  ] }),
9686
- notice.caller ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "cv-transfer-notice-subtitle", children: [
11107
+ notice.caller ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("p", { className: "cv-transfer-notice-subtitle", children: [
9687
11108
  notice.caller,
9688
11109
  " se ha desconectado."
9689
11110
  ] }) : null,
9690
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "cv-transfer-notice-footer", children: [
9691
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "cv-transfer-notice-age", children: formatAge(elapsedSeconds(notice.at, now)) }),
9692
- notice.caller ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
11111
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "cv-transfer-notice-footer", children: [
11112
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "cv-transfer-notice-age", children: formatAge(elapsedSeconds(notice.at, now)) }),
11113
+ notice.caller ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
9693
11114
  "button",
9694
11115
  {
9695
11116
  type: "button",
@@ -9697,7 +11118,7 @@ function TransferNotices({
9697
11118
  onClick: () => callBack(notice),
9698
11119
  part: "transfer-notice-callback",
9699
11120
  children: [
9700
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Phone, { size: 14, "aria-hidden": true }),
11121
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Phone, { size: 14, "aria-hidden": true }),
9701
11122
  "Llamar de vuelta"
9702
11123
  ]
9703
11124
  }
@@ -9711,15 +11132,15 @@ function TransferNotices({
9711
11132
  }
9712
11133
 
9713
11134
  // src/ui/notices/useTransferNotices.ts
9714
- var import_react28 = __toESM(require_react(), 1);
11135
+ var import_react42 = __toESM(require_react(), 1);
9715
11136
  function useTransferNotices() {
9716
11137
  const softphone = useSoftphone();
9717
- const [notices, setNotices] = (0, import_react28.useState)([]);
9718
- const nextId = (0, import_react28.useRef)(0);
9719
- const dismiss = (0, import_react28.useCallback)((id) => {
11138
+ const [notices, setNotices] = (0, import_react42.useState)([]);
11139
+ const nextId = (0, import_react42.useRef)(0);
11140
+ const dismiss = (0, import_react42.useCallback)((id) => {
9720
11141
  setNotices((prev) => prev.filter((n) => n.id !== id));
9721
11142
  }, []);
9722
- (0, import_react28.useEffect)(
11143
+ (0, import_react42.useEffect)(
9723
11144
  () => softphone.on("transferOutcome" /* TransferOutcome */, (payload) => {
9724
11145
  if (payload.status === "not-connected") {
9725
11146
  const id = nextId.current++;
@@ -9732,7 +11153,7 @@ function useTransferNotices() {
9732
11153
  }
9733
11154
 
9734
11155
  // src/ui/SoftphoneWidget.tsx
9735
- var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
11156
+ var import_jsx_runtime25 = __toESM(require_jsx_runtime(), 1);
9736
11157
  function SoftphoneWidget(props) {
9737
11158
  const {
9738
11159
  contacts,
@@ -9748,7 +11169,7 @@ function SoftphoneWidget(props) {
9748
11169
  const fallback = softphone.sources;
9749
11170
  const contactsSource = contacts ?? fallback?.contacts;
9750
11171
  const historySource = history ?? fallback?.history;
9751
- const [tab, setTab] = (0, import_react30.useState)("dialer");
11172
+ const [tab, setTab] = (0, import_react44.useState)("dialer");
9752
11173
  const call = useActiveCall();
9753
11174
  const isLeader = useIsLeader();
9754
11175
  const softphoneState = useSoftphoneState();
@@ -9777,15 +11198,15 @@ function SoftphoneWidget(props) {
9777
11198
  // optional parts of a phone, so this is never conditionally hidden.
9778
11199
  { id: "settings", icon: Settings, label: "Ajustes", show: true }
9779
11200
  ].filter((t) => t.show);
9780
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
11201
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
9781
11202
  "div",
9782
11203
  {
9783
11204
  className: className ? `cv-softphone ${className}` : "cv-softphone",
9784
11205
  ...theme === "system" ? {} : { "data-cv-theme": theme },
9785
11206
  children: [
9786
- screenIsFree ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
9787
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(LeaderGate, {}) : blocked ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CallScreen, { call, keyTones: sounds, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SettingsTab, {}) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Dialpad, { outcome, onIntent: clearOutcome, keyTones: sounds }) }),
9788
- screenIsFree ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
11207
+ screenIsFree ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
11208
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(LeaderGate, {}) : blocked ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(CallScreen, { call, keyTones: sounds, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(SettingsTab, {}) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Dialpad, { outcome, onIntent: clearOutcome, keyTones: sounds }) }),
11209
+ screenIsFree ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
9789
11210
  "button",
9790
11211
  {
9791
11212
  role: "tab",
@@ -9793,8 +11214,8 @@ function SoftphoneWidget(props) {
9793
11214
  className: "cv-tab",
9794
11215
  onClick: () => setTab(id),
9795
11216
  children: [
9796
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Icon3, { size: 20 }),
9797
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: label })
11217
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Icon3, { size: 20 }),
11218
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { children: label })
9798
11219
  ]
9799
11220
  },
9800
11221
  id
@@ -9805,7 +11226,7 @@ function SoftphoneWidget(props) {
9805
11226
  }
9806
11227
 
9807
11228
  // src/styles/softphone.css
9808
- var softphone_default = "/**\r\n * @coventia/softphone/styles.css\r\n *\r\n * The theming contract (SPEC \xA72.6) + all component styles. A host restyles the softphone by\r\n * overriding the `--cv-*` custom properties \u2014 which are INHERITED, so they cross the shadow boundary\r\n * of `<coventia-softphone>` by design (the host's own CSS *selectors* do not, which is what stops a\r\n * stray `button { width:100% }` from destroying the dialpad).\r\n *\r\n * Defaults match the Coventia product: light surface, emerald accent, soft rounded key cards.\r\n *\r\n * Reseller: coventia-softphone { --cv-accent:#7c3aed; --cv-bg:#0b0b0f; }\r\n * CRM (shadcn bridge, ~15 lines, dark mode included):\r\n * .cv-softphone { --cv-bg:hsl(var(--background)); --cv-text:hsl(var(--foreground)); \u2026 }\r\n *\r\n * TYPOGRAPHY NOTE: `--cv-font` is a system stack on purpose. An embedded widget should inherit its\r\n * host's type, not force a webfont into someone else's app; hosts override the token if they want.\r\n *\r\n * Strings are Spanish-only (SPEC \xA72.4). No i18n.\r\n */\r\n\r\n/*\r\n * The custom element's own box.\r\n *\r\n * A custom element is `display: inline` by default and has NO size \u2014 and the widget inside now fills\r\n * its host rather than asserting 340\xD7520, so without this the element would render to ZERO. The\r\n * drop-in promise is \"one tag and it works\", which means the TAG must carry a box.\r\n *\r\n * These are defaults, not decisions: they sit on `:host`, so any host stylesheet outranks them \u2014\r\n *\r\n * coventia-softphone { width: 100%; height: 100%; } \u2190 a sidebar\r\n * coventia-softphone { height: 600px; } \u2190 taller\r\n *\r\n * React consumers never see this: they import `/ui` and their own wrapper states the box (the CRM's\r\n * PhoneDialer does exactly that).\r\n */\r\n:host {\r\n display: block;\r\n width: 340px;\r\n height: 520px;\r\n}\r\n\r\n/* React's container inside the shadow root \u2014 element path only; inert everywhere else.\r\n *\r\n * It must pass the host's height straight through. `.cv-softphone` below is `height: 100%`, and a\r\n * percentage height resolves against the PARENT: leave this div at its default `auto` and that 100%\r\n * silently becomes `auto` too, so the widget collapses to its content height and the host's `height`\r\n * is ignored. What you see is a short card stranded in a taller box \u2014 which reads as a broken widget,\r\n * not as a broken wrapper. */\r\n.cv-mount {\r\n height: 100%;\r\n}\r\n\r\n:host,\r\n.cv-softphone {\r\n /* Surface */\r\n --cv-bg: #ffffff;\r\n --cv-surface: #f9fafb;\r\n --cv-surface-2: #f3f4f6;\r\n --cv-border: #f3f4f6;\r\n --cv-border-strong: #e5e7eb;\r\n\r\n /* Text \u2014 three tiers, split by CONTRAST DUTY, not by taste:\r\n `text` informational primary 17.7:1\r\n `muted` informational secondary 4.8:1 (AA \u2014 phone numbers, timestamps, tab labels)\r\n `faint` DECORATIVE only 2.5:1 (keypad letters, placeholder \u2014 never information)\r\n The legacy used gray-400 for all secondary text, which fails AA at 2.54:1. Splitting the tier\r\n keeps the look while making the informational text legible. */\r\n --cv-text: #111827;\r\n --cv-muted: #6b7280;\r\n --cv-faint: #9ca3af;\r\n\r\n /* Intent \u2014 emerald. Split by CONTRAST DUTY, because one \"brand green\" cannot do all three jobs:\r\n `accent` FILL that carries a white glyph \u2192 emerald-600, white 3.77:1 \u2713\r\n (emerald-500 is the prettier brand green but white on it is 2.54:1 \u2014 the\r\n call button's phone icon would fail WCAG non-text contrast. Measured, not guessed.)\r\n `accent-strong` emerald AS TEXT on a surface \u2192 emerald-700, 5.48:1 \u2713 for the 10px tab label\r\n `accent-light` DECORATIVE only (the ring halo) \u2014 never carries text, so it stays vivid */\r\n --cv-accent: #059669;\r\n --cv-accent-strong: #047857;\r\n --cv-accent-light: #10b981;\r\n --cv-accent-soft: #a7f3d0;\r\n --cv-accent-contrast: #ffffff;\r\n --cv-danger: #ef4444;\r\n /* `--cv-danger` ITSELF fails 3:1 non-text contrast against `--cv-danger-soft` (2.6:1, measured) \u2014\r\n the same AS-TEXT/on-a-tinted-surface split `accent` already has, `danger` was just missing until\r\n the Historial tab's status badges needed it. Darker on light (mirrors `accent-strong`), LIGHTER on\r\n dark (red-400) \u2014 same inversion `accent-strong` documents above, same reason: emphasis on a dark\r\n surface means more light, not less.\r\n red-700 (#b91c1c) was the first pick here and overshot both minimums (6.47:1 text, 4.47:1 badge)\r\n enough to read as a dark, brownish red rather than red \u2014 red-600 clears both with real margin\r\n still (4.83:1 / 3.34:1, measured) and looks like red. */\r\n --cv-danger-strong: #dc2626;\r\n --cv-danger-soft: #fecaca;\r\n /* A third status hue, sky \u2014 for the Historial row status chip specifically: an outgoing call that\r\n WAS answered gets its own colour rather than sharing `accent`, matching the legacy CRM's own\r\n three-way split (emerald/sky/rose) that this file's earlier restrained accent-only-for-\"fine\"\r\n version had collapsed away. Same strong/soft split, same inversion, as `accent`/`danger` above \u2014\r\n 4.47:1 light / 4.42:1 dark against `-soft`, measured. */\r\n --cv-info: #0284c7;\r\n --cv-info-strong: #0369a1;\r\n --cv-info-soft: #bae6fd;\r\n --cv-hold: #6366f1;\r\n\r\n /* Shape & type */\r\n --cv-radius: 16px;\r\n /* Strong ease-out. The built-in CSS easings are too weak to read as intentional. */\r\n --cv-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\r\n --cv-radius-key: 16px;\r\n --cv-font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\r\n --cv-font-size: 14px;\r\n --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 1px 3px rgb(0 0 0 / 0.06);\r\n\r\n color: var(--cv-text);\r\n font-family: var(--cv-font);\r\n font-size: var(--cv-font-size);\r\n}\r\n\r\n/**\r\n * Dark theme \u2014 matched to the Coventia product's own dark tokens, not invented.\r\n *\r\n * The CRM defines `--background: 240 10% 10%` (#17171c) with surfaces at 15.9% (#27272a) and borders\r\n * at 25.9% (#404044) \u2014 the zinc/240-hue family. An earlier draft here used a near-black\r\n * (#0f0f13, ~half the luminance of the product's background) with 2%-luminance borders that were\r\n * effectively invisible. That is the \"black tonality\" that read wrong: right hue, wrong lightness.\r\n *\r\n * Note the accent INVERTS: on light, `accent-strong` is darker than `accent` (emerald-700); on dark\r\n * it must be LIGHTER (emerald-400), because emphasis on a dark surface means more light, not less.\r\n * Same token, opposite direction \u2014 miss this and the timer/active tab sink into the background.\r\n *\r\n * \u2500\u2500 HOW THE THEME IS CHOSEN \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n * By default it follows the OS (`prefers-color-scheme`). But a host must be able to PIN it: the\r\n * Coventia CRM toggles dark with a class (next-themes), not the OS setting \u2014 so a user with a dark\r\n * OS but the CRM in light mode would otherwise get a dark widget inside a light app.\r\n *\r\n * <SoftphoneWidget theme=\"light\" /> \u2192 data-cv-theme=\"light\" \u2014 always light\r\n * <SoftphoneWidget theme=\"dark\" /> \u2192 data-cv-theme=\"dark\" \u2014 always dark\r\n * <SoftphoneWidget /> \u2192 follows the OS\r\n *\r\n * To follow the HOST's theme rather than the OS, pass the host's current theme through, e.g.\r\n * next-themes: `const { resolvedTheme } = useTheme(); <SoftphoneWidget theme={resolvedTheme} />`.\r\n *\r\n * The dark block is duplicated below (OS-driven vs. pinned) on purpose: plain CSS has no way to\r\n * share a declaration block across a media query and a selector, and the honest duplication beats a\r\n * clever custom-property toggle nobody can debug.\r\n */\r\n@media (prefers-color-scheme: dark) {\r\n :host(:not([data-cv-theme='light'])),\r\n .cv-softphone:not([data-cv-theme='light']) {\r\n --cv-bg: #17171c; /* CRM --background 240 10% 10% */\r\n --cv-surface: #202027; /* between bg and secondary */\r\n --cv-surface-2: #27272a; /* CRM --secondary/--muted 240 3.7% 15.9% */\r\n --cv-border: #2e2e33;\r\n --cv-border-strong: #404044; /* CRM --border 240 3.7% 25.9% */\r\n --cv-text: #fafafa; /* CRM --foreground 0 0% 98% */\r\n --cv-muted: #a1a1aa; /* CRM --muted-foreground 240 5% 64.9% \u2192 6.97:1 AA */\r\n --cv-faint: #71717a; /* decorative only */\r\n --cv-accent-strong: #34d399; /* emerald-400 \u2014 9.29:1 on #17171c */\r\n --cv-accent-light: #34d399; /* the halo reads brighter on dark */\r\n --cv-accent-soft: #065f46; /* glows must darken on dark, not glare */\r\n --cv-danger-strong: #f87171; /* red-400 \u2014 LIGHTER than --cv-danger, same inversion as accent-strong */\r\n --cv-danger-soft: #7f1d1d;\r\n --cv-info-strong: #38bdf8; /* sky-400 \u2014 same inversion, same reason */\r\n --cv-info-soft: #0c4a6e;\r\n --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.4);\r\n }\r\n}\r\n\r\n/* Pinned dark \u2014 wins regardless of the OS setting. */\r\n:host([data-cv-theme='dark']),\r\n.cv-softphone[data-cv-theme='dark'] {\r\n --cv-bg: #17171c;\r\n --cv-surface: #202027;\r\n --cv-surface-2: #27272a;\r\n --cv-border: #2e2e33;\r\n --cv-border-strong: #404044;\r\n --cv-text: #fafafa;\r\n --cv-muted: #a1a1aa;\r\n --cv-faint: #71717a;\r\n --cv-accent-strong: #34d399;\r\n --cv-accent-light: #34d399;\r\n --cv-accent-soft: #065f46;\r\n --cv-danger-strong: #f87171;\r\n --cv-danger-soft: #7f1d1d;\r\n --cv-info-strong: #38bdf8;\r\n --cv-info-soft: #0c4a6e;\r\n --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.4);\r\n}\r\n\r\n.cv-softphone,\r\n.cv-softphone *,\r\n.cv-softphone *::before,\r\n.cv-softphone *::after {\r\n box-sizing: border-box;\r\n}\r\n\r\n/* \u2500\u2500 Shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-softphone {\r\n display: flex;\r\n flex-direction: column;\r\n\r\n /* FILL the box the host gives us; do not assert one.\r\n This was `width: 340px; height: 520px`, which is a component telling its host how big its\r\n layout is. The CRM's panel is 310 wide, so the third keypad column was simply cut off \u2014 and no\r\n amount of theming could have fixed it, because the widget was not asking. A component in\r\n someone else's layout accepts a size. */\r\n width: 100%;\r\n height: 100%;\r\n\r\n /* \u2026but a dialpad is not a spreadsheet. Given 900px it should not become a 900px dialpad with\r\n 300px keys; it should be itself, centred. Cap and centre rather than stretch. */\r\n max-width: var(--cv-max-width, 420px);\r\n margin-inline: auto;\r\n\r\n /* Adapt to THIS box, not to the viewport. The widget can be 310px wide inside a 1920px screen \u2014\r\n a media query would ask the wrong question and get a confident wrong answer. Container queries\r\n are the only ones that know the truth, and a shadow root is the ideal place for them. */\r\n container-type: inline-size;\r\n container-name: cv;\r\n\r\n background: var(--cv-bg);\r\n overflow: hidden;\r\n /* The containing block for `.cv-transfer-notices` \u2014 it overlays this whole shell, top edge to top\r\n edge, not just `.cv-body`, so it reads as one seamless panel rather than a strip that starts a few\r\n pixels short of the shell's own rounded corner. */\r\n position: relative;\r\n\r\n /* The frame is the HOST's call. The CRM's panel already draws `border rounded-xl shadow-2xl`, so\r\n drawing our own put a card inside a card \u2014 nested chrome, which is always wrong. Defaulting to\r\n a frame still suits a bare drop-in on a blank page; a host that has its own sets these to\r\n `none` / `0` and the double edge disappears. */\r\n border: var(--cv-shell-border, 1px solid var(--cv-border-strong));\r\n border-radius: var(--cv-shell-radius, var(--cv-radius));\r\n box-shadow: var(--cv-shell-shadow, 0 4px 24px rgb(0 0 0 / 0.06));\r\n}\r\n\r\n/* Narrow hosts \u2014 a sidebar, a phone-width column. Reclaim the horizontal padding first: at 260px\r\n the gutters cost more than they give, and a cramped keypad is worse than a tight one. */\r\n@container cv (max-width: 300px) {\r\n .cv-keypad { padding: 0 0.6rem; gap: 6px; }\r\n .cv-dial-prefix { padding: 0.7rem 0.6rem 0; }\r\n .cv-dial-head { padding: 0.3rem 0.6rem 0.6rem; }\r\n .cv-display { font-size: 1.5rem; }\r\n}\r\n\r\n/* Short hosts. The flex chain already lets the keypad absorb what is left, but below this the keys\r\n stop being tappable \u2014 so buy the height back from the chrome instead of from the keys. */\r\n@container cv (max-height: 440px) {\r\n .cv-dial-prefix { padding-top: 0.5rem; }\r\n .cv-dial-head { padding-top: 0.2rem; padding-bottom: 0.4rem; }\r\n .cv-dial-foot { padding-bottom: 0.5rem; }\r\n .cv-controls { gap: 0.4rem; padding-bottom: 0.6rem; }\r\n}\r\n\r\n/* The flex chain that makes the layout work: body flexes, views fill it, only the\r\n keypad/list grows. Without `min-height:0` a grid child refuses to shrink and overflows. */\r\n.cv-body {\r\n flex: 1;\r\n min-height: 0;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n.cv-view {\r\n flex: 1;\r\n min-height: 0;\r\n display: flex;\r\n flex-direction: column;\r\n /* Containing block for `.cv-detail` below \u2014 an absolute overlay must cover exactly THIS tab's\r\n content, not escape to whatever the next positioned ancestor up the tree happens to be. */\r\n position: relative;\r\n}\r\n\r\n/* \u2500\u2500 Tab bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-tabs {\r\n display: flex;\r\n border-top: 1px solid var(--cv-border);\r\n background: var(--cv-bg);\r\n flex-shrink: 0;\r\n}\r\n.cv-tab {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n gap: 3px;\r\n padding: 9px 0 8px;\r\n background: none;\r\n border: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n font: inherit;\r\n transition: color 0.15s ease, background 0.15s ease;\r\n}\r\n.cv-tab:hover:not(:disabled) { background: var(--cv-surface); }\r\n.cv-tab[aria-selected='true'] { color: var(--cv-accent-strong); }\r\n.cv-tab:disabled { opacity: 0.35; cursor: not-allowed; }\r\n.cv-tab span { font-size: 10px; font-weight: 600; line-height: 1; letter-spacing: 0.01em; }\r\n\r\n/* \u2500\u2500 Transfer outcome notices (Phase A \u2014 transfer-outcome-relay.md \xA75.5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n/*\r\n A persistent, dismissable banner OVER `.cv-body`, never a toast and never auto-dismissed \u2014 it carries\r\n an action, and WCAG 2.2.1 requires a time limit to be removable, adjustable or extended.\r\n\r\n Overlaps rather than reserving its own height, the same reasoning `.cv-dial-outcome`'s own comment\r\n already gives for why the widget never lets a line of text resize the keypad under a moving finger \u2014\r\n here that principle cuts the other way: appearing costs nothing but a repaint, not a reflow, so the\r\n widget's overall height stays identical whether a notice is showing or not, exactly like `.cv-detail`\r\n overlaying `.cv-body`'s own list rather than pushing it down. It sits over whatever the current tab\r\n happens to be showing (most often the dialer's country picker and number field \u2014 an acceptable loss:\r\n the agent is not expected to start a fresh manual dial before dealing with a transfer that just failed\r\n to connect), which is also why it outranks `.cv-detail` in `z-index`.\r\n\r\n `[data-has-notices='false']` collapses to zero footprint: the element stays mounted (so its\r\n `role=\"status\"` region exists before the FIRST notice ever arrives \u2014 an aria-live region inserted at\r\n the same moment as its own content is not reliably announced), but an idle widget, the common case,\r\n covers nothing and costs no paint either.\r\n*/\r\n.cv-transfer-notices {\r\n position: absolute;\r\n inset: 0 0 auto 0;\r\n z-index: 3; /* above `.cv-detail`'s 2 \u2014 this is the higher-priority thing on screen */\r\n}\r\n.cv-transfer-notices[data-has-notices='true'] {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.5rem;\r\n padding: 0.6rem 0.75rem;\r\n background: var(--cv-bg);\r\n box-shadow: var(--cv-shadow);\r\n animation: cv-transfer-notices-in 200ms var(--cv-ease-out) both;\r\n}\r\n@keyframes cv-transfer-notices-in {\r\n from { opacity: 0; transform: translateY(-6px); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-transfer-notices[data-has-notices='true'] { animation: none; }\r\n}\r\n.cv-transfer-notices-count {\r\n margin: 0;\r\n font: 600 0.72rem var(--cv-font);\r\n color: var(--cv-muted);\r\n text-transform: uppercase;\r\n letter-spacing: 0.04em;\r\n}\r\n.cv-transfer-notice {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 0.6rem;\r\n padding: 0.65rem 0.7rem;\r\n border-radius: var(--cv-radius-key);\r\n border: 1px solid var(--cv-danger-soft);\r\n background: var(--cv-surface);\r\n}\r\n/* Same shape as `.cv-gate-mark`, smaller \u2014 one visual language for \"here is a status\", not two. */\r\n.cv-transfer-notice-mark {\r\n flex-shrink: 0;\r\n display: grid;\r\n place-items: center;\r\n width: 30px;\r\n height: 30px;\r\n border-radius: 999px;\r\n background: var(--cv-danger-soft);\r\n color: var(--cv-danger);\r\n}\r\n.cv-transfer-notice-body { flex: 1; min-width: 0; }\r\n/*\r\n Title shares its row with only the dismiss button (24px) \u2014 not the whole card's width, and not the\r\n \"Llamar de vuelta\" button, which gets its own row below (`.cv-transfer-notice-footer`). The original\r\n layout put mark + title + subtitle + a full button all in one flex row, leaving the title as little as\r\n ~80px to wrap into at the real ~340px embed width \u2014 nearly every word landed on its own line.\r\n `align-items: flex-start` keeps the dismiss pinned to the first line even when the title wraps to two.\r\n*/\r\n.cv-transfer-notice-header { display: flex; align-items: flex-start; gap: 0.4rem; }\r\n.cv-transfer-notice-title {\r\n flex: 1;\r\n min-width: 0;\r\n margin: 0;\r\n font: 600 0.82rem var(--cv-font);\r\n color: var(--cv-text);\r\n line-height: 1.35;\r\n}\r\n.cv-transfer-notice-subtitle {\r\n margin: 0.15rem 0 0;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n line-height: 1.35;\r\n}\r\n/* Age and the callback button share a row too, same reasoning as the header \u2014 a short fixed-width\r\n timestamp beside a content-sized button, neither one fighting the title for space above. */\r\n.cv-transfer-notice-footer {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 0.5rem;\r\n margin-top: 0.35rem;\r\n}\r\n.cv-transfer-notice-age { margin: 0; font-size: 0.7rem; color: var(--cv-faint); flex-shrink: 0; }\r\n/*\r\n `.cv-btn.cv-transfer-notice-callback` \u2014 TWO classes, not one. Equal-specificity selectors resolve by\r\n source order, and this rule used to lose its `padding`/`font-size` to the shared `.cv-btn` rule below\r\n simply because that one appears later in the file; the button rendered at full default CTA size\r\n instead of this smaller, secondary one, which is what actually forced the title into that ~80px\r\n column above. The extra class is what makes this override win regardless of source order.\r\n*/\r\n.cv-btn.cv-transfer-notice-callback {\r\n flex-shrink: 0;\r\n padding: 0.35rem 0.6rem;\r\n font-size: 0.75rem;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n white-space: nowrap;\r\n}\r\n/* Same round icon-only shape as `.cv-search-clear` \u2014 one dismiss affordance, not a new one. */\r\n.cv-transfer-notice-dismiss {\r\n flex-shrink: 0;\r\n width: 24px;\r\n height: 24px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n border-radius: 999px;\r\n background: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n transition: color 0.12s ease, background 0.12s ease, transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-transfer-notice-dismiss:hover { color: var(--cv-text); background: var(--cv-surface-2); }\r\n.cv-transfer-notice-dismiss:active { transform: scale(0.88); }\r\n\r\n/* \u2500\u2500 Buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-btn {\r\n font: 600 0.875rem var(--cv-font);\r\n cursor: pointer;\r\n padding: 0.5rem 0.9rem;\r\n border-radius: 10px;\r\n border: 1px solid var(--cv-border-strong);\r\n background: var(--cv-bg);\r\n color: var(--cv-text);\r\n transition: background 0.15s ease, transform 0.06s ease;\r\n}\r\n.cv-btn:hover:not(:disabled) { background: var(--cv-surface); }\r\n.cv-btn:active:not(:disabled) { transform: scale(0.97); }\r\n.cv-btn:disabled { opacity: 0.45; cursor: not-allowed; }\r\n.cv-btn--accent {\r\n background: var(--cv-accent);\r\n border-color: var(--cv-accent);\r\n color: var(--cv-accent-contrast);\r\n}\r\n.cv-btn--accent:hover:not(:disabled) { background: var(--cv-accent-strong); }\r\n\r\n/* Round action buttons (answer / hangup / call) */\r\n.cv-round {\r\n width: 64px;\r\n height: 64px;\r\n border-radius: 999px;\r\n border: none;\r\n cursor: pointer;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: #fff;\r\n transition: background 0.15s ease, transform 0.08s ease, box-shadow 0.15s ease;\r\n}\r\n.cv-round:active:not(:disabled) { transform: scale(0.94); }\r\n.cv-round--answer {\r\n background: var(--cv-accent);\r\n box-shadow: 0 8px 20px -4px var(--cv-accent-soft);\r\n}\r\n.cv-round--answer:hover:not(:disabled) { background: var(--cv-accent-strong); }\r\n.cv-round--answer:disabled {\r\n background: var(--cv-surface-2);\r\n color: var(--cv-faint);\r\n box-shadow: none;\r\n cursor: not-allowed;\r\n}\r\n.cv-round--hangup {\r\n background: var(--cv-danger);\r\n box-shadow: 0 8px 20px -4px var(--cv-danger-soft);\r\n}\r\n.cv-round--hangup:hover:not(:disabled) { filter: brightness(0.94); }\r\n\r\n/* \u2500\u2500 Dialpad \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n/* The prefix chip, on its own row above the number.\r\n Sharing a line with the number starves it: chip + erase button eat the middle, and a formatted\r\n international number needs every pixel. The number is the content; the prefix is a setting. */\r\n.cv-dial-prefix {\r\n flex-shrink: 0;\r\n display: flex;\r\n justify-content: center;\r\n padding: 1rem 1rem 0;\r\n}\r\n.cv-dial-head {\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n padding: 0.35rem 1rem 0.75rem;\r\n gap: 0.25rem;\r\n}\r\n/*\r\n * The outcome of the last attempt, under the number.\r\n *\r\n * ALWAYS in the layout, visible or not. The keypad is `flex: 1` and absorbs whatever height is left,\r\n * so a line that appears and disappears would resize every key and shift the whole grid under the\r\n * user's finger \u2014 mid-dial, which is exactly when this text arrives. Reserving the row costs one line\r\n * of height and buys a keypad that never moves.\r\n */\r\n.cv-dial-outcome {\r\n flex-shrink: 0;\r\n margin: 0;\r\n min-height: 1.15rem;\r\n padding: 0 1rem;\r\n text-align: center;\r\n font-size: 0.78rem;\r\n line-height: 1.15rem;\r\n color: var(--cv-muted);\r\n /*\r\n One line, always. `error-text.test.ts` caps every outcome at 48 characters so nothing should ever\r\n reach this \u2014 but a cap depends on whoever writes the next string remembering it, and the failure\r\n it prevents is not cosmetic: the keypad below is `flex: 1`, so a second line resizes all twelve\r\n keys and shifts the grid under a finger that is already moving. Truncating is visibly wrong and\r\n harmless; reflowing the dialpad mid-dial is invisibly wrong and makes the user misdial.\r\n */\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n /* Muted, not danger: the phone still works. A red line here would read as a broken phone. */\r\n opacity: 0;\r\n transition: opacity 150ms ease-out;\r\n}\r\n.cv-dial-outcome[data-shown='true'] {\r\n opacity: 1;\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-dial-outcome {\r\n transition: none;\r\n }\r\n}\r\n\r\n.cv-display {\r\n flex: 1;\r\n min-width: 0;\r\n font: 600 1.75rem var(--cv-font);\r\n font-variant-numeric: tabular-nums;\r\n letter-spacing: -0.02em;\r\n text-align: center;\r\n color: var(--cv-text);\r\n border: none;\r\n background: none;\r\n padding: 0;\r\n caret-color: var(--cv-accent);\r\n}\r\n.cv-display::placeholder { color: var(--cv-faint); font-weight: 400; }\r\n.cv-display:focus { outline: none; }\r\n\r\n/* The spacer/erase pair keeps the number optically centred. */\r\n.cv-dial-side {\r\n width: 36px;\r\n height: 36px;\r\n flex-shrink: 0;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n background: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n border-radius: 999px;\r\n transition: color 0.15s ease, opacity 0.15s ease, transform 0.06s ease;\r\n}\r\n.cv-dial-side:hover { color: var(--cv-text); }\r\n.cv-dial-side:active { transform: scale(0.9); }\r\n.cv-dial-side[data-hidden='true'] { opacity: 0; pointer-events: none; }\r\n\r\n/* THE layout fix: the keypad flexes to fill, and its rows share the space equally.\r\n Keys must NOT use aspect-ratio \u2014 that overflows the shell and shoves the call button\r\n under the tab bar. */\r\n.cv-keypad {\r\n flex: 1;\r\n min-height: 0;\r\n display: grid;\r\n grid-template-columns: repeat(3, 1fr);\r\n grid-template-rows: repeat(4, 1fr);\r\n gap: 8px;\r\n padding: 0 1rem;\r\n}\r\n.cv-key {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 1px;\r\n border-radius: var(--cv-radius-key);\r\n border: 1px solid var(--cv-border);\r\n background: var(--cv-bg);\r\n box-shadow: var(--cv-shadow);\r\n color: var(--cv-text);\r\n cursor: pointer;\r\n user-select: none;\r\n transition: background 0.12s ease, transform 0.06s ease;\r\n}\r\n.cv-key:hover { background: var(--cv-surface); }\r\n.cv-key:active { transform: scale(0.95); background: var(--cv-surface-2); }\r\n.cv-key b { font: 500 1.25rem/1 var(--cv-font); }\r\n.cv-key small {\r\n font-size: 9px;\r\n font-weight: 600;\r\n letter-spacing: 0.14em;\r\n /* Decorative: the letters carry no information the digit doesn't. `faint`, not `muted`. */\r\n color: var(--cv-faint);\r\n line-height: 1;\r\n}\r\n\r\n.cv-dial-foot {\r\n flex-shrink: 0;\r\n display: flex;\r\n justify-content: center;\r\n padding: 0.75rem 0 1rem;\r\n}\r\n\r\n/* \u2500\u2500 Active call \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-call {\r\n flex: 1;\r\n min-height: 0;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.15rem;\r\n text-align: center;\r\n padding: 1rem;\r\n /*\r\n A REAL bug, found live (plan T7): the transfer picker's agent list can make this box's content\r\n taller than the space `flex: 1` actually allocates it \u2014 the avatar/name/timer/status ABOVE the\r\n picker are still shown while transferring, unlike `.cv-call-devices-inline`'s own \"swap the\r\n content\" fix for the pre-connect devices screen. Without this, the overflow painted straight past\r\n `.cv-call`'s own shrunk box and onto `.cv-controls` below it \u2014 not scroll, actual overlap, controls\r\n rendered ON TOP of list rows. `overflow-y: auto` keeps any excess inside THIS box, scrollable,\r\n exactly like `.cv-presence-list`'s own internal scroll already does for the list itself.\r\n */\r\n overflow-y: auto;\r\n}\r\n\r\n.cv-avatar-wrap { position: relative; margin-bottom: 0.85rem; }\r\n.cv-avatar {\r\n position: relative;\r\n width: 68px;\r\n height: 68px;\r\n border-radius: 999px;\r\n /* accent \u2192 accent-strong (emerald 600\u2192700): white initials clear 3.77:1 at EVERY stop. Starting\r\n the ramp at emerald-500 would look punchier but drop the light end to 2.54:1. */\r\n background: linear-gradient(135deg, var(--cv-accent) 0%, var(--cv-accent-strong) 100%);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: #fff;\r\n font: 700 1.3rem var(--cv-font);\r\n letter-spacing: -0.01em;\r\n box-shadow: 0 8px 20px -6px var(--cv-accent-soft);\r\n}\r\n/* A soft halo while ringing \u2014 the one moment worth animating. */\r\n.cv-avatar-ping {\r\n position: absolute;\r\n inset: 0;\r\n border-radius: 999px;\r\n /* Decorative \u2014 nothing sits on it, so it uses the vivid emerald. */\r\n background: var(--cv-accent-light);\r\n opacity: 0.16;\r\n animation: cv-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite;\r\n pointer-events: none;\r\n}\r\n@keyframes cv-ping {\r\n 0% { transform: scale(1); opacity: 0.22; }\r\n 80%, 100% { transform: scale(1.7); opacity: 0; }\r\n}\r\n.cv-avatar-dot {\r\n position: absolute;\r\n bottom: 1px;\r\n right: 1px;\r\n width: 14px;\r\n height: 14px;\r\n border-radius: 999px;\r\n background: var(--cv-accent);\r\n border: 2px solid var(--cv-bg);\r\n}\r\n\r\n.cv-call-name { font-size: 1rem; font-weight: 600; line-height: 1.2; }\r\n.cv-call-number { font-size: 0.75rem; color: var(--cv-muted); margin-top: 2px; }\r\n\r\n.cv-timer {\r\n display: flex;\r\n align-items: baseline;\r\n justify-content: center;\r\n gap: 1px;\r\n margin-top: 0.75rem;\r\n color: var(--cv-accent-strong);\r\n}\r\n.cv-timer b {\r\n font: 700 2.25rem/1 var(--cv-font);\r\n font-variant-numeric: tabular-nums;\r\n letter-spacing: -0.03em;\r\n}\r\n.cv-timer i {\r\n font: 300 1.75rem/1 var(--cv-font);\r\n font-style: normal;\r\n color: var(--cv-accent);\r\n animation: cv-blink 1.6s ease-in-out infinite;\r\n margin: 0 1px;\r\n}\r\n@keyframes cv-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }\r\n\r\n.cv-call-status {\r\n margin-top: 0.6rem;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n letter-spacing: 0.04em;\r\n}\r\n.cv-badge-hold { color: var(--cv-hold); font-weight: 600; }\r\n\r\n/* \u2500\u2500 Device check (ringing / dialing / in-call) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n Before a call connects there is only ONE other control on screen (Colgar, or\r\n Rechazar/Contestar) \u2014 putting Dispositivos in that same neighbourhood, even\r\n spaced out, keeps it a mis-tap away from hanging up or answering. The entry\r\n chip lives beside the call status instead, removing it from that neighbourhood\r\n rather than just adding distance within it \u2014 same resting-pill SHAPE as `.cv-cp`\r\n (CountryPicker), reused rather than invented fresh.\r\n\r\n Opening it does NOT pop a panel into that same tight space, though \u2014 this\r\n SDK's real production embed (a 310\xD7520 host panel with its own header eating\r\n further into that) leaves too little free room around a trigger sitting in\r\n vertically-centred content for a floating popover to open into without either\r\n overlapping Colgar/Rechazar/Contestar or clamping itself down to something\r\n cramped and scrolly for just two rows. `.cv-call-devices-inline` REPLACES the\r\n avatar/name/status block in `.cv-call` instead (see that JSX's own comment) \u2014\r\n an inline swap, not an overlay, so it gets the full box, not a leftover sliver.\r\n\r\n Once connected it is the SAME chip and the SAME swap. Dispositivos used to be a fifth control in\r\n the call row (with a floating popover), which is what forced that row down to 40px buttons and\r\n 9px labels in a 310px host. A set-once setting does not need a slot among the four things you do\r\n mid-call (mute, hold, transfer, keypad), and the control row stays live under the swap.\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-call-devices-chip {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.35rem;\r\n height: 26px;\r\n padding: 0 0.65rem;\r\n border: 1px solid var(--cv-border-strong);\r\n border-radius: 999px;\r\n background: var(--cv-bg);\r\n color: var(--cv-muted);\r\n font: 600 0.7rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\r\n}\r\n.cv-call-devices-chip:hover { background: var(--cv-surface); }\r\n.cv-call-devices-chip:active { transform: scale(0.95); }\r\n\r\n/*\r\n The entry-point row \u2014 ONE chip today (Dispositivos), up to two once a host configures\r\n `incomingCallAction` (that one first: who is calling outranks a phone-utility setting). `width:\r\n 100%` + `flex-wrap` on purpose: a host's action label is arbitrary text this component cannot\r\n assume is short, so two chips sit side by side when they fit and wrap onto their own centred line\r\n when they do not, rather than overflowing `.cv-call`'s fixed width.\r\n*/\r\n.cv-call-chips {\r\n width: 100%;\r\n display: flex;\r\n flex-wrap: wrap;\r\n justify-content: center;\r\n gap: 0.5rem;\r\n margin-top: 0.65rem;\r\n}\r\n\r\n.cv-call-devices-inline {\r\n width: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.9rem;\r\n /* `.cv-call` centres and centre-aligns its normal (avatar) content \u2014 rows read left-aligned\r\n everywhere else in the widget (Ajustes, the in-call popover), so this opts back out of both. */\r\n text-align: left;\r\n animation: cv-call-devices-in 0.18s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n@keyframes cv-call-devices-in {\r\n from { opacity: 0; transform: translateY(6px); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-call-devices-inline { animation: none; }\r\n}\r\n.cv-call-devices-header { display: flex; align-items: center; gap: 0.5rem; }\r\n.cv-call-devices-title { font-size: 0.95rem; font-weight: 600; color: var(--cv-text); }\r\n/* `--cv-muted` on `--cv-bg` \u2014 the same already-verified pairing `.cv-settings-note` uses\r\n (4.83:1 light / 6.97:1 dark), not a new one. */\r\n.cv-call-devices-context {\r\n margin: 0;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n}\r\n\r\n/* \u2500\u2500 Call controls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n TWO TIERS. Hang up is the one action that must never be hunted for, so it sits\r\n alone, centred, and largest \u2014 the anchor. The secondaries flank above it. They\r\n used to share a single row, where hang up was just the last button and carried\r\n the same weight as \"transfer\"; mid-call, that is the wrong hierarchy.\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-controls {\r\n flex-shrink: 0;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n gap: 0.9rem;\r\n padding: 0.35rem 1rem 1.1rem;\r\n /* The containing block for `.cv-keypad-popover` \u2014 see that rule's own comment for why it is\r\n anchored here rather than to the trigger button. */\r\n position: relative;\r\n}\r\n/* FOUR controls, sized so they never need to shrink to fit. This row held five (Dispositivos too) and\r\n the only way to fit five in the CRM's 310px host was a 40px button with a 9px label and ~6px gaps \u2014\r\n below the 44/48px a thumb needs, and below legible for a word read mid-call. The default 48px size\r\n below was never actually seen: it only applied above a 360px host, which no real embed is.\r\n Dispositivos is the `.cv-call-chips` chip up by the call info instead (the same entry point the\r\n ringing and dialing screens already use). 4 \xD7 52 + 3 \xD7 20 = 268px, inside 310 - 2 \xD7 16 padding. */\r\n.cv-controls-row {\r\n display: flex;\r\n justify-content: center;\r\n gap: 1.25rem;\r\n}\r\n.cv-controls-primary {\r\n display: flex;\r\n justify-content: center;\r\n gap: 2.75rem;\r\n}\r\n/* Button + its word. The column keeps the label from widening the hit target. */\r\n.cv-ctrl-item {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n gap: 0.3rem;\r\n}\r\n/* 10px, muted: present when you look for it, silent when you don't. An icon-only\r\n control is a quiz, and mid-call is the worst time to sit one. */\r\n.cv-ctrl-label {\r\n font-size: 0.625rem;\r\n line-height: 1;\r\n color: var(--cv-muted);\r\n white-space: nowrap;\r\n user-select: none;\r\n}\r\n/* The four secondaries' words get a notch more than Colgar / Rechazar / Contestar's 10px \u2014 they are\r\n the ones read mid-call, and the five-up row used to shrink them to 9px (8px in a sidebar). */\r\n.cv-controls-row .cv-ctrl-label { font-size: 0.6875rem; }\r\n.cv-ctrl {\r\n width: 52px;\r\n height: 52px;\r\n border-radius: 999px;\r\n border: 1px solid var(--cv-border-strong);\r\n background: var(--cv-bg);\r\n color: var(--cv-text);\r\n cursor: pointer;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n transition: background 0.15s ease, color 0.15s ease, transform 0.06s ease;\r\n}\r\n.cv-ctrl:hover { background: var(--cv-surface); }\r\n.cv-ctrl:active { transform: scale(0.94); }\r\n.cv-ctrl:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n pointer-events: none;\r\n}\r\n\r\n/* The ON states say what they MEAN, in the widget's own tokens: muted is danger,\r\n held is hold, transfer-open is accent. Colour is the state, not decoration. */\r\n.cv-ctrl[data-tone='danger'] {\r\n background: var(--cv-danger);\r\n border-color: var(--cv-danger);\r\n color: #fff;\r\n}\r\n.cv-ctrl[data-tone='hold'] {\r\n background: var(--cv-hold);\r\n border-color: var(--cv-hold);\r\n color: #fff;\r\n}\r\n.cv-ctrl[data-tone='accent'] {\r\n background: var(--cv-accent-soft);\r\n border-color: transparent;\r\n color: var(--cv-accent-strong);\r\n}\r\n.cv-ctrl[data-tone]:hover { filter: brightness(0.95); }\r\n\r\n/* Muted is the one state worth animating: it is invisible, it is easy to forget,\r\n and the cost of forgetting is talking to nobody. Same halo the avatar rings\r\n with \u2014 one language, not two. */\r\n.cv-ctrl[data-tone='danger'] {\r\n position: relative;\r\n}\r\n.cv-ctrl[data-tone='danger']::after {\r\n content: '';\r\n position: absolute;\r\n inset: 0;\r\n border-radius: 999px;\r\n background: var(--cv-danger);\r\n animation: cv-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite;\r\n pointer-events: none;\r\n z-index: -1;\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-ctrl[data-tone='danger']::after { animation: none; opacity: 0; }\r\n}\r\n.cv-ctrl[aria-pressed='true'] {\r\n background: var(--cv-text);\r\n color: var(--cv-bg);\r\n border-color: var(--cv-text);\r\n}\r\n\r\n/*\r\n Sidebar-narrow (this file's own \u2264300px tier, up in \"Narrow hosts\"). The four 52px controls with\r\n 20px gaps need 268px, which a 260px host cannot give \u2014 so this tier steps down to 44px with 12px\r\n gaps (4 \xD7 44 + 3 \xD7 12 = 212px) and reclaims `.cv-controls`'s own horizontal padding, the first\r\n lever that block's own comment names. 44px is still the Apple HIG minimum tap target; the five-up\r\n row this replaced went to 36px here.\r\n\r\n Placed AFTER `.cv-controls`/`.cv-controls-row` above rather than up with this file's other\r\n `@container` blocks near the top: `gap` and `padding-left`/`-right` here have the same specificity as\r\n those base rules' own shorthand, so \u2014 unlike a compound selector such as `.cv-controls-row .cv-ctrl`,\r\n which wins on specificity regardless of position \u2014 source order is what decides, and a narrower-width\r\n override declared BEFORE the base rule it means to narrow loses to it. Confirmed live: moved up into\r\n the \"Narrow hosts\" section, the `.cv-ctrl` width and label font-size reductions took effect (compound\r\n selectors) but `gap` and `.cv-controls`'s padding silently did not (plain selectors, later base rule won).\r\n*/\r\n@container cv (max-width: 300px) {\r\n .cv-controls { padding-left: 0.5rem; padding-right: 0.5rem; }\r\n .cv-controls-row { gap: 0.75rem; }\r\n .cv-controls-row .cv-ctrl { width: 44px; height: 44px; }\r\n .cv-controls-row .cv-ctrl-label { font-size: 0.625rem; }\r\n}\r\n\r\n/*\r\n The attended-transfer target-entry panel (`docs/specs/transfer-availability.md` \xA75): an optional\r\n outcome line, then a row of whatever's actionable (input+button today). Once a consult exists,\r\n `ConsultScreen` takes over the whole screen instead \u2014 this panel is ONLY ever the pre-consult /\r\n retry-after-failure state now; it used to also cover the consulting/answered phases, which is why\r\n its rule is a column container built for more than one child even though there's only ever one row\r\n here today. (History: was two near-duplicate rules \u2014 `.cv-transfer` as a bare ROW,\r\n `.cv-transfer-consult` as a column \u2014 until the row version turned out to be a real,\r\n reproduced-in-browser bug: the optional `.cv-dial-outcome` message rendered as a THIRD item in the\r\n same horizontal row as the input and button, squeezing the input to its content-minimum. Merged\r\n rather than patched.)\r\n*/\r\n.cv-transfer {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.3rem;\r\n width: 100%;\r\n padding: 0.2rem 0.25rem 0;\r\n /* `.cv-call` centres and centre-aligns its normal (avatar) content \u2014 same reasoning\r\n `.cv-call-devices-inline` already opts out of for the OTHER inline swap in this box (its own\r\n comment): rows read left-aligned everywhere else in the widget, and a centred context line\r\n sitting directly above a left-aligned input/toggle/list read as two layouts stacked on top of\r\n each other, found live (2026-09-29). */\r\n text-align: left;\r\n /* Found live (2026-09-29): `.cv-call`'s avatar/name/timer block used to sit ABOVE this,\r\n unconditionally, so this panel only ever got whatever was left over \u2014 see the JSX call site's own\r\n comment. Now that this IS the whole of `.cv-call`'s content while a transfer is in progress\r\n (a swap, not an addition), it should CLAIM `.cv-call`'s available height, not just sit in it \u2014\r\n `flex: 1` does that; `min-height: 0` is what lets a flex child actually shrink to fit instead of\r\n forcing its parent to grow past it (the same reason `.cv-call`/`.cv-view`/`.cv-body` all set it,\r\n see their own comments). This is what lets `.cv-presence-list` below flex-grow into real room\r\n instead of needing its own hand-tuned `max-height` guess. */\r\n flex: 1;\r\n min-height: 0;\r\n /* Same entrance `.cv-call-devices-inline` already uses for the analogous inline swap (its own\r\n comment) \u2014 reused, not a second animation invented for this widget's second inline-swap view. */\r\n animation: cv-call-devices-in 0.18s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-transfer { animation: none; }\r\n}\r\n/* The compact \"who am I still on the phone with\" line while the picker is open \u2014 reuses\r\n `.cv-call-devices-inline`'s own two classes verbatim (`.cv-call-devices-title` for the name,\r\n `.cv-call-devices-context` for the number/timer line) rather than inventing a second \"quiet header\"\r\n style for the same widget; only the tiny stacking gap between them is new.\r\n\r\n Found live (2026-09-29): `.cv-transfer`'s own `gap` alone gave every section \u2014 context, input row,\r\n mode toggle, results list \u2014 the exact same spacing, so nothing read as its own group; the eye had\r\n no cue for where \"who I'm on the phone with\" ends and \"pick a transfer target\" begins. This extra\r\n `margin-bottom`, on top of the base `gap`, is that cue \u2014 the toggle stays close to the list it\r\n controls (unchanged), only the boundary above the interactive controls opened up. */\r\n.cv-transfer-context {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n flex-shrink: 0;\r\n margin-bottom: 0.35rem;\r\n}\r\n/* Plan T3: the mode toggle reuses `.cv-segmented` (HistoryTab's own filter control) as-is, but its\r\n `margin-bottom` (needed THERE, where the surrounding layout has no gap of its own) is redundant\r\n here \u2014 `.cv-transfer`'s own `gap` above already spaces every child, so the two would stack. Scoped\r\n to this context only; HistoryTab's own copy is untouched. */\r\n.cv-transfer .cv-segmented { margin-bottom: 0; flex-shrink: 0; }\r\n.cv-transfer .cv-presence-legend { flex-shrink: 0; }\r\n/* Same treatment as `.cv-call-status` (the avatar's own \"Llamando\u2026\"/\"En espera\" line) \u2014 one muted\r\n status-line vocabulary for the whole call screen, not a second one invented for this panel. */\r\n.cv-transfer-status {\r\n margin: 0;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n letter-spacing: 0.04em;\r\n}\r\n/* The target-entry/retry row: an input plus one button. */\r\n.cv-transfer-row {\r\n display: flex;\r\n gap: 0.5rem;\r\n}\r\n/*\r\n Flex items default to `min-width: auto`, which floors them at their CONTENT size rather than letting\r\n them shrink or grow to fill the row \u2014 `.cv-search`'s own `width: 100%` never actually took effect\r\n here, because `.cv-btn`'s text (\"Llamar\") set an unshrinkable floor and the input, having no\r\n `flex-grow`, never reclaimed the space left over. `flex: 1` + `min-width: 0` is what actually makes\r\n an input-plus-button row behave like one; `flex: none` keeps the button at its natural size.\r\n*/\r\n.cv-transfer-row .cv-search {\r\n flex: 1;\r\n min-width: 0;\r\n}\r\n/* `max-width` + ellipsis: the label now includes whatever was typed (\"Llamar a {typed}\", spec \xA74.5)\r\n and `truncateForButton` already bounds that in JS \u2014 this is the CSS backstop, so a value that\r\n slips past it (a very wide character set, a future copy change) still can't push `.cv-search` down\r\n to an unusable sliver the way the pre-truncation button did before this was noticed. */\r\n.cv-transfer-row .cv-btn {\r\n flex: none;\r\n max-width: 60%;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n/* The consult-answered row: one or two buttons, no input. */\r\n.cv-transfer-actions {\r\n display: flex;\r\n gap: 0.5rem;\r\n}\r\n.cv-transfer-actions .cv-btn {\r\n flex: 1;\r\n}\r\n/* The ringing phase has only \"Cancelar\" \u2014 a secondary, low-stakes action. Letting the row's own\r\n `flex: 1` stretch a LONE button edge to edge reads as oversized for what it is, and nothing else in\r\n this widget sizes a single button to the full row; every other `.cv-btn` sizes to its content. */\r\n.cv-transfer-actions .cv-btn:only-child {\r\n flex: none;\r\n}\r\n\r\n/*\r\n The agent picker (spec \xA74.5, plan T7) \u2014 an inline, always-visible filtered list under the\r\n input+button row, not a click-to-open popover like `.cv-person-filter-pop`: the whole point is that\r\n typing already filters it, so hiding it behind a second interaction would undo that.\r\n\r\n Redesigned live (2026-09-29): T7 (72px) and T3 (30px) both hand-tuned a `max-height` in PIXELS\r\n against whatever else happened to be on screen at the time, and both were broken by the very next\r\n addition above the list (T3's own toggle broke T7's number the same way this would keep breaking on\r\n the next change) \u2014 down to \"barely proves the list is scrollable\" at 30px, which is what the user\r\n was actually seeing: \"users cannot almost see any of the listed users.\" The real fix wasn't a bigger\r\n magic number, it was `.cv-transfer`'s own move to `flex: 1` (see its comment) freeing real height\r\n from the block that used to sit above this whole panel \u2014 `.cv-presence` and this list now flex-grow\r\n into whatever's ACTUALLY left after the context line, input row, toggle and legend take their own\r\n natural size, on whatever real height the host panel gives `.cv-call` (this SDK's fixed ~310\xD7520\r\n embed today, but also whatever a different host gives it tomorrow \u2014 no more re-tuning a constant\r\n every time something above the list changes). `.cv-call`'s own `overflow-y: auto` (its own comment)\r\n is still the last-resort net if content ever genuinely doesn't fit; this just means that net should\r\n now rarely, if ever, have to catch anything for a normal agent list.\r\n*/\r\n.cv-presence {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.35rem;\r\n flex: 1;\r\n min-height: 0;\r\n}\r\n.cv-presence-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n flex: 1;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 0.25rem;\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius-key);\r\n background: var(--cv-surface);\r\n}\r\n.cv-presence-empty {\r\n margin: 0;\r\n padding: 0.5rem;\r\n text-align: center;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n}\r\n.cv-presence-item {\r\n width: 100%;\r\n display: flex;\r\n align-items: center;\r\n gap: 0.5rem;\r\n padding: 0.4rem 0.5rem;\r\n border: none;\r\n border-radius: calc(var(--cv-radius) - 4px);\r\n background: none;\r\n color: var(--cv-text);\r\n font: 400 0.8rem var(--cv-font);\r\n text-align: left;\r\n cursor: pointer;\r\n}\r\n.cv-presence-item:hover:not(:disabled) { background: var(--cv-surface-2); }\r\n/* En llamada rows are a hard block, not just a hint (CallScreen.tsx's own comment on the row has why)\r\n \u2014 same `:disabled` treatment `.cv-btn`/`.cv-tab` already use elsewhere in this file, not a new pattern. */\r\n.cv-presence-item:disabled { opacity: 0.45; cursor: not-allowed; }\r\n.cv-presence-dot {\r\n flex-shrink: 0;\r\n width: 8px;\r\n height: 8px;\r\n border-radius: 999px;\r\n}\r\n/* Reuses existing intent tokens rather than inventing new ones \u2014 `--cv-accent` already means\r\n \"good, reachable\" (the answer button), `--cv-hold` already means \"occupied elsewhere\" (the hold\r\n toggle), and `--cv-faint` is this file's own DECORATIVE-only tier (see the `--cv-*` legend near the\r\n top) \u2014 exactly what \"no signal for this person\" is, no signal being the whole point of that tier. */\r\n.cv-presence-dot[data-state='disponible'] { background: var(--cv-accent); }\r\n.cv-presence-dot[data-state='en_llamada'] { background: var(--cv-hold); }\r\n.cv-presence-dot[data-state='desconectado'] { background: var(--cv-faint); }\r\n.cv-presence-name {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n.cv-presence-meta {\r\n flex-shrink: 0;\r\n font-size: 0.72rem;\r\n color: var(--cv-muted);\r\n}\r\n/* Spec \xA74.6: Desconectado is a HINT, never \"can't transfer\" \u2014 this is what tells the agent so, right\r\n under the list it describes. `--cv-muted`, NOT `--cv-faint`: this text is informational (the reader\r\n needs it to not misread the state), and `--cv-faint` is reserved for decorative-only content that\r\n fails AA on purpose \u2014 using it here would repeat the exact tier mistake this file's own `--cv-*`\r\n comment block warns against. */\r\n.cv-presence-legend {\r\n margin: 0;\r\n font-size: 0.7rem;\r\n color: var(--cv-muted);\r\n line-height: 1.35;\r\n}\r\n\r\n/*\r\n `ConsultScreen`'s \"someone is still waiting\" banner (`docs/specs/transfer-availability.md` \xA75, the\r\n cross-talk fix) \u2014 full-width and tappable, not a passive status line, because the entire reason this\r\n screen exists is to make \"go back to the caller\" impossible to miss. Neutral surface + the same\r\n `--cv-hold` colour `.cv-badge-hold` already uses for the same fact elsewhere, not a new soft-fill\r\n token: a text/icon tint on a plain surface is already proven legible here, and inventing an\r\n unmeasured background-tint pairing risks a real contrast regression this file takes seriously\r\n elsewhere (see the `-soft`/`-strong` split's own comments).\r\n*/\r\n.cv-consult-hold-banner {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.4rem;\r\n width: 100%;\r\n padding: 0.5rem 0.75rem;\r\n border-radius: 999px;\r\n border: 1px solid var(--cv-border);\r\n background: var(--cv-surface);\r\n color: var(--cv-hold);\r\n font: 600 0.78rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s ease, border-color 0.15s ease;\r\n}\r\n.cv-consult-hold-banner:hover {\r\n background: var(--cv-surface-2);\r\n border-color: var(--cv-hold);\r\n}\r\n.cv-consult-hold-banner:active {\r\n transform: scale(0.98);\r\n}\r\n\r\n@keyframes cv-pop-in-up {\r\n from { opacity: 0; transform: translateY(4px) scale(0.97); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n\r\n/*\r\n The in-call DTMF keypad \u2014 the one floating popover left on the call screen. (Dispositivos was the\r\n other; it is inline content now, `.cv-call-devices-inline` above, in every call state.)\r\n\r\n Opens UPWARD (`bottom: 100%`): the trigger sits at the bottom of the call screen, and there is no\r\n room to open downward. Anchored to `.cv-controls`'s own horizontal padding \u2014 a container whose width\r\n is bounded by ITS parent, never the narrow trigger itself \u2014 the same fix CountryPicker's own popover\r\n needed (see its comment): anchoring to a narrow, off-centre trigger let the popover's edge reach\r\n `.cv-softphone`'s `overflow: hidden` and get clipped.\r\n*/\r\n.cv-keypad-popover {\r\n position: absolute;\r\n z-index: 20;\r\n display: grid;\r\n grid-template-columns: repeat(3, 1fr);\r\n gap: 8px;\r\n padding: 0.85rem 1rem;\r\n background: var(--cv-surface);\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius);\r\n box-shadow: var(--cv-shadow);\r\n}\r\n.cv-keypad-popover--above {\r\n bottom: 100%;\r\n left: 1rem;\r\n right: 1rem;\r\n margin-bottom: 0.6rem;\r\n transform-origin: bottom center;\r\n animation: cv-pop-in-up 0.14s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n/* `.cv-key` already carries the full key look (border, hover, active-scale, digit/letter type) from\r\n the pre-call dialer \u2014 only the size needs setting here, since this grid has no flex ancestor to\r\n stretch the rows the way `.cv-keypad` gets to.\r\n\r\n A fixed HEIGHT, not an `aspect-ratio`. This popover opens upward from `.cv-controls`, so it has\r\n (host height - controls block - margin) to work with, and a ratio ties its height to the host's\r\n WIDTH, which says nothing about how much height there is. Found live: the CRM's real body is 310\xD7473\r\n (a 520px panel minus its 45px header), and a 1.3 ratio made this popover 285px \u2014 with the 184px\r\n controls block that is 479px, so its top ~7px were clipped by `.cv-softphone`'s `overflow: hidden`.\r\n 48px keys give a 243px popover at ANY width, fit that host with room to spare, and stay at the\r\n 44-48px minimum tap target (the old ratio gave 45px keys at a 260px host, 58px at 310). */\r\n.cv-keypad-popover .cv-key {\r\n height: 48px;\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-keypad-popover--above { animation: none; }\r\n}\r\n\r\n/* \u2500\u2500 Lists (contacts / history) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-list-head { flex-shrink: 0; padding: 0.85rem 1rem 0.5rem; }\r\n.cv-search {\r\n width: 100%;\r\n padding: 0.5rem 0.75rem;\r\n border-radius: 10px;\r\n border: 1px solid var(--cv-border-strong);\r\n background: var(--cv-surface);\r\n color: var(--cv-text);\r\n font: inherit;\r\n transition: border-color 0.15s ease, background 0.15s ease;\r\n /* Found live (2026-09-29): in the transfer row specifically, `.cv-btn`'s own `max-width: 60%`\r\n (needed for the dynamic \"Llamar a {typed}\" label) can leave the input narrower than its\r\n placeholder text \u2014 an `<input>` has no native ellipsis, so it hard-clipped mid-word\r\n (\"...o n\xFAm\") instead of trailing off cleanly. A real, if rare, risk for ANY `.cv-search` usage\r\n wherever it's squeezed, not just this one row \u2014 the fix belongs on the base rule. */\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n.cv-search::placeholder { color: var(--cv-muted); }\r\n.cv-search:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }\r\n\r\n/*\r\n A wrapper around `.cv-search`, not a change to it \u2014 that class is also the transfer-target input\r\n in CallScreen, which wants neither an icon nor a clear button. `.cv-search--icon` is an ADDITIVE\r\n modifier on the input itself (padding only), so the base class is byte-for-byte what it was for\r\n every other consumer.\r\n*/\r\n.cv-search-field { position: relative; display: flex; align-items: center; }\r\n.cv-search--icon { padding-left: 2.15rem; padding-right: 1.85rem; }\r\n.cv-search-icon {\r\n position: absolute;\r\n left: 0.75rem;\r\n color: var(--cv-muted);\r\n pointer-events: none;\r\n}\r\n.cv-search-clear {\r\n position: absolute;\r\n right: 0.5rem;\r\n width: 22px;\r\n height: 22px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n border-radius: 999px;\r\n background: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n transition: color 0.12s ease, transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-search-clear:hover { color: var(--cv-text); }\r\n.cv-search-clear:active { transform: scale(0.88); }\r\n\r\n/* \u2500\u2500 Historial filters: status (Todas/Perdidas) + person \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-segmented {\r\n display: flex;\r\n gap: 2px;\r\n padding: 2px;\r\n border-radius: 10px;\r\n background: var(--cv-surface-2);\r\n margin-bottom: 0.4rem;\r\n}\r\n.cv-segmented-btn {\r\n flex: 1;\r\n padding: 0.35rem 0;\r\n border: none;\r\n border-radius: 8px;\r\n background: none;\r\n color: var(--cv-muted);\r\n font: 600 0.75rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;\r\n}\r\n/* The active segment is stated by its own fill + shadow, not colour alone \u2014 the same reasoning\r\n `.cv-item-action[data-tone='accent']`'s own comment gives for resting-state colour: a state that\r\n only differs by hue must still read on a screen that renders neither. */\r\n.cv-segmented-btn[aria-selected='true'] {\r\n background: var(--cv-bg);\r\n color: var(--cv-text);\r\n box-shadow: var(--cv-shadow);\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-segmented-btn { transition: none; }\r\n}\r\n\r\n/* The person filter \u2014 same combobox SHAPE as `.cv-cp-*` (CountryPicker): a chip opening a positioned\r\n popover list with a checkmark on the active row. Not the same CLASSES: the two pickers have nothing\r\n to do with each other and sharing a namespace would make a future change to one silently reach the\r\n other. */\r\n.cv-person-filter { position: relative; width: 100%; }\r\n.cv-person-filter-chip {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.4rem;\r\n width: 100%;\r\n padding: 0.4rem 0.65rem;\r\n border: 1px solid var(--cv-border-strong);\r\n border-radius: 10px;\r\n background: var(--cv-surface);\r\n color: var(--cv-muted);\r\n font: 500 0.78rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s ease, border-color 0.15s ease;\r\n}\r\n.cv-person-filter-chip:hover { background: var(--cv-surface-2); }\r\n/*\r\n Border only, no fill \u2014 the popover opening and the caret rotating already say \"this is open\";\r\n painting the whole width of a wide text pill solid green on top of that was the same over-strong\r\n green the selected row had (see its own comment below). A coloured border is the same restrained\r\n signal `.cv-search:focus` already uses for \"active\" elsewhere in this file.\r\n*/\r\n.cv-person-filter-chip[aria-expanded='true'] {\r\n border-color: var(--cv-accent);\r\n}\r\n.cv-person-filter-label {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n text-align: left;\r\n color: var(--cv-text);\r\n}\r\n.cv-person-filter-caret {\r\n flex-shrink: 0;\r\n transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n.cv-person-filter-caret[data-open='true'] { transform: rotate(180deg); }\r\n.cv-person-filter-pop {\r\n position: absolute;\r\n top: calc(100% + 6px);\r\n left: 0;\r\n right: 0;\r\n z-index: 20;\r\n max-height: 220px;\r\n overflow-y: auto;\r\n padding: 0.25rem;\r\n background: var(--cv-surface);\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius);\r\n box-shadow: var(--cv-shadow);\r\n transform-origin: top center;\r\n animation: cv-cp-in 0.14s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-person-filter-pop { animation: none; }\r\n .cv-person-filter-caret { transition: none; }\r\n}\r\n.cv-person-filter-item {\r\n width: 100%;\r\n display: flex;\r\n align-items: center;\r\n gap: 0.5rem;\r\n padding: 0.45rem 0.5rem;\r\n border: none;\r\n border-radius: calc(var(--cv-radius) - 4px);\r\n background: none;\r\n color: var(--cv-text);\r\n font: 400 0.8rem var(--cv-font);\r\n text-align: left;\r\n cursor: pointer;\r\n}\r\n.cv-person-filter-item:hover { background: var(--cv-surface-2); }\r\n/*\r\n NOT a solid `--cv-accent-soft` fill \u2014 that read as a wall of saturated green across a wide row,\r\n much louder than the legacy CRM's own treatment for this exact list (`bg-emerald-50` \u2014 a barely-\r\n there wash \u2014 plus coloured text, never a strong fill). Coloured, bold text + the checkmark carry\r\n \"selected\" instead; `--cv-accent-strong` is already verified elsewhere in this file as AA against\r\n `--cv-surface`, which is what this popover sits on.\r\n*/\r\n.cv-person-filter-item[aria-selected='true'] .cv-person-filter-name {\r\n font-weight: 600;\r\n color: var(--cv-accent-strong);\r\n}\r\n.cv-person-filter-name {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n.cv-person-filter-check { color: var(--cv-accent-strong); flex-shrink: 0; }\r\n.cv-person-filter-check[data-on='false'] { opacity: 0; }\r\n\r\n.cv-list {\r\n flex: 1;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 0 0.5rem 0.5rem;\r\n}\r\n/*\r\n Historial's day group header. Sticky within `.cv-list` (its nearest scrolling ancestor \u2014 a plain\r\n wrapper div between the two does not break that), OPAQUE `--cv-bg` rather than a translucent\r\n `/85 + backdrop-blur` treatment: it still needs to fully occlude rows scrolling underneath it, and\r\n a flat match to the list's own background does that with no new CSS feature this file doesn't\r\n already use elsewhere.\r\n*/\r\n.cv-history-day {\r\n position: sticky;\r\n top: 0;\r\n z-index: 1;\r\n padding: 0.5rem 0.6rem 0.35rem;\r\n background: var(--cv-bg);\r\n color: var(--cv-muted);\r\n font-size: 0.68rem;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.04em;\r\n}\r\n.cv-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.7rem;\r\n padding: 0.5rem;\r\n border-radius: 12px;\r\n /*\r\n NOT `cursor: pointer` \u2014 this div has no click handler of its own; only its children\r\n (`.cv-item-main`, `.cv-item-action`) do, and each already sets its own pointer cursor. Putting\r\n it here too used to make the WHOLE row's box promise a click \u2014 its own padding, and the flex\r\n `gap` between avatar/name/button \u2014 none of which does anything when clicked. Most noticeable\r\n right where one row's padding meets the next, which reads as \"there's something clickable\r\n between these two contacts\" when there is not.\r\n */\r\n transition: background 0.12s ease;\r\n text-align: left;\r\n width: 100%;\r\n border: none;\r\n background: none;\r\n font: inherit;\r\n color: inherit;\r\n}\r\n.cv-item:hover { background: var(--cv-surface); }\r\n.cv-item-avatar {\r\n width: 40px;\r\n height: 40px;\r\n border-radius: 999px;\r\n /*\r\n RADIAL, not diagonal \u2014 deliberately. The initials sit centered, so a radial gradient puts them\r\n at its lightest point (pure `--cv-surface`) no matter what, while the edges still darken toward\r\n `--cv-surface-2` for the same cheap depth a diagonal gradient gave. A diagonal version measured\r\n 4.39:1 for `--cv-muted` text at its far corner \u2014 under the 4.5:1 text needs \u2014 because centered\r\n text's exact position against a DIAGONAL gradient depends on glyph width, not just circle size.\r\n Centering the gradient on the centered text removes that dependency entirely, structurally,\r\n rather than nudging a token and hoping the margin holds. Both stops stay tokens, so this is\r\n still theme-correct for free \u2014 a hardcoded gray gradient would not adapt to dark mode.\r\n */\r\n background: radial-gradient(circle, var(--cv-surface), var(--cv-surface-2));\r\n /*\r\n Without this, the circle's own boundary can vanish: `.cv-item:hover` sets the ROW background to\r\n `--cv-surface` \u2014 the exact color the gradient above STARTS at (1.00 contrast ratio, i.e.\r\n identical) in both themes. `--cv-border-strong` is the same token `.cv-search`'s border already\r\n uses against this same surface a few rules up \u2014 a pairing already proven visible, not a new one.\r\n */\r\n border: 1px solid var(--cv-border-strong);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--cv-muted);\r\n font-size: 0.78rem;\r\n font-weight: 700;\r\n flex-shrink: 0;\r\n}\r\n/*\r\n The Historial row's status chip \u2014 WHY a call needs colour at all: direction is already the icon's\r\n shape (an arrow in/out), but \"did this connect\" is not shape-able, and a flat grey circle made every\r\n row look identical at a glance. Small badge, not a row-wide fill (that mistake already got made once\r\n on the person filter \u2014 see its own comment): a 40px circle reads as a status glyph, the same way a\r\n colored icon-badge does anywhere else; a wide fill reads as decoration competing with the text next\r\n to it. `--danger-strong`/`--accent-strong` (not the plain base tokens) because the base tokens\r\n measure BELOW 3:1 non-text contrast against their own `-soft` fill \u2014 `--cv-danger` on\r\n `--cv-danger-soft` is 2.6:1; `--cv-danger-strong` is 4.47:1 light / 3.62:1 dark, measured.\r\n*/\r\n.cv-item-avatar[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }\r\n.cv-item-avatar[data-tone='info'] { background: var(--cv-info-soft); color: var(--cv-info-strong); }\r\n.cv-item-avatar[data-tone='danger'] { background: var(--cv-danger-soft); color: var(--cv-danger-strong); }\r\n/* A button, reset to look like plain content \u2014 the row is a container, not a control. */\r\n.cv-item-main {\r\n flex: 1;\r\n min-width: 0;\r\n display: block;\r\n border: none;\r\n background: none;\r\n padding: 0;\r\n font: inherit;\r\n color: inherit;\r\n text-align: left;\r\n cursor: pointer;\r\n transition: transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-item-main:disabled { cursor: default; }\r\n.cv-item-main:not(:disabled):active { transform: scale(0.985); }\r\n.cv-item-name {\r\n display: block;\r\n font-weight: 600;\r\n font-size: 0.875rem;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n/*\r\n 14px bold does not clear WCAG's large-text bar (18.66px bold) \u2014 this text needs the full 4.5:1, not\r\n the 3:1 large-text minimum. `--cv-danger-strong`: 6.47:1 light / 6.46:1 dark against `--cv-bg`,\r\n measured; the plain `--cv-danger` this used to be was only 3.76:1 light \u2014 a real (if marginal) fail.\r\n*/\r\n.cv-item-name[data-tone='danger'] { color: var(--cv-danger-strong); }\r\n.cv-item-sub {\r\n display: block;\r\n font-size: 0.72rem;\r\n color: var(--cv-muted);\r\n margin-top: 1px;\r\n font-variant-numeric: tabular-nums;\r\n}\r\n.cv-item-sub-outcome { color: var(--cv-danger-strong); }\r\n/*\r\n Decorative reinforcement, not the sole carrier \u2014 the row's own play button is what says \"this call\r\n has a recording\" (it only renders when one exists); this is a small hint IN the metadata line that\r\n the button is worth pressing, so `--cv-faint` (2.5:1, documented above as decorative-only) is the\r\n right tier here, same reasoning `.cv-empty-icon`/`.cv-gate-mark` already use for icons next to real\r\n text rather than standing alone.\r\n\r\n `display: inline-flex` is load-bearing, not decoration \u2014 every OTHER inline icon in this file gets\r\n an explicit `display` for the same reason: `/react` + `/ui` render straight into the host's light\r\n DOM (no shadow root), so a host reset like Tailwind Preflight's `svg { display: block }` reaches\r\n this element directly. Without an explicit override here, that reset knocks the icon onto its own\r\n line instead of trailing the duration text it is meant to sit beside.\r\n*/\r\n.cv-item-sub-rec {\r\n display: inline-flex;\r\n color: var(--cv-faint);\r\n margin-left: 0.3rem;\r\n vertical-align: -1px;\r\n}\r\n.cv-item-action {\r\n width: 34px;\r\n height: 34px;\r\n border-radius: 999px;\r\n border: none;\r\n background: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n flex-shrink: 0;\r\n transition: background 0.12s ease, color 0.12s ease, transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-item-action:hover { background: var(--cv-accent); color: #fff; }\r\n.cv-item-action:active { transform: scale(0.9); }\r\n/*\r\n The call action specifically (ContactsTab) \u2014 NOT History's \"play recording\", which shares this\r\n same class and must stay neutral; a play button has no reason to be accent-colored. Scoped with\r\n `data-tone`, the same modifier convention `.cv-ctrl[data-tone]` already uses elsewhere in this file.\r\n\r\n Coloured at REST, not just on hover: a hover-only reveal never fires on a touchscreen, so the one\r\n affordance that most needs to read as \"you can tap this\" would be invisible on exactly the devices\r\n a phone widget is likeliest to run on. `accent-strong` is already verified elsewhere in this file\r\n as AA on a surface; `accent-soft` is the same hover tint `.cv-ctrl[data-tone='accent']` uses, just\r\n transparent at rest so fifty rows of solid fill never turns the list into a wall of green.\r\n*/\r\n.cv-item-action[data-tone='accent'] { color: var(--cv-accent-strong); }\r\n.cv-item-action[data-tone='accent']:hover { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-item-main, .cv-item-action, .cv-search-clear { transition: none; }\r\n}\r\n\r\n.cv-empty {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n height: 100%;\r\n color: var(--cv-muted);\r\n text-align: center;\r\n padding: 2.5rem 1.25rem;\r\n font-size: 0.82rem;\r\n}\r\n.cv-empty-icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 48px;\r\n height: 48px;\r\n border-radius: 999px;\r\n background: var(--cv-surface);\r\n color: var(--cv-faint);\r\n margin-bottom: 0.75rem;\r\n}\r\n.cv-empty-title { margin: 0; font-size: 0.85rem; font-weight: 600; color: var(--cv-muted); }\r\n/*\r\n `--cv-muted`, NOT `--cv-faint` \u2014 this file documents faint as \"decorative only... never\r\n information\" (2.5:1, below AA), and this line IS information: it is the reason the list is\r\n empty, sometimes echoing the user's own search back to them. `--cv-faint` here measured 2.54:1\r\n light / 3.70:1 dark against a 4.5:1 bar \u2014 a real failure, not a rounding difference.\r\n*/\r\n.cv-empty-sub { margin: 0.2rem 0 0; font-size: 0.75rem; color: var(--cv-muted); }\r\n.cv-empty-spinner {\r\n margin-bottom: 0.75rem;\r\n animation: cv-spin 0.8s linear infinite;\r\n /* A loading spinner's rotation IS the information, with no static equivalent \u2014 unlike this\r\n file's other motion, it stays exempt from reduced-motion rather than freezing into a shape\r\n that would just look stuck. Colour comes from `.cv-empty`'s own `color: var(--cv-muted)` \u2014\r\n the spinner no longer sets its own, now that it sits next to real text instead of standing\r\n alone: the icon is decorative reinforcement, same as `.cv-empty-icon`, not the sole carrier. */\r\n}\r\n@keyframes cv-spin {\r\n to { transform: rotate(360deg); }\r\n}\r\n\r\n/* The scroll-triggered \"load more\" marker \u2014 see `useInfiniteList`. Never visible itself; a small\r\n height so it has a real box for the `IntersectionObserver` to track. */\r\n.cv-list-sentinel {\r\n height: 1px;\r\n}\r\n.cv-list-loading-more {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 0.75rem 0 0.25rem;\r\n color: var(--cv-muted);\r\n}\r\n/* Exempt from reduced-motion for the same reason `.cv-empty-spinner` is \u2014 see its own comment. */\r\n.cv-list-loading-more-spinner {\r\n animation: cv-spin 0.8s linear infinite;\r\n}\r\n\r\n/* \u2500\u2500 Contact detail (overlays the list) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-detail {\r\n position: absolute;\r\n inset: 0;\r\n /* Above the list's own sticky day-group header (`.cv-history-day`, z-index: 1) \u2014 otherwise\r\n that header's z-index wins the stacking fight wherever its unscrolled flow position happens\r\n to land on top of this overlay, and it paints through despite both having an opaque bg. */\r\n z-index: 2;\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--cv-bg);\r\n animation: cv-detail-in 220ms var(--cv-ease-out) both;\r\n}\r\n@keyframes cv-detail-in {\r\n from { opacity: 0; transform: translateX(10px); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-detail { animation: none; }\r\n}\r\n\r\n.cv-detail-head { flex-shrink: 0; padding: 0.6rem 0.5rem 0; }\r\n.cv-detail-back {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n padding: 0.4rem 0.5rem;\r\n border: none;\r\n border-radius: 8px;\r\n background: none;\r\n color: var(--cv-muted);\r\n font: 600 0.82rem var(--cv-font);\r\n cursor: pointer;\r\n transition: color 0.12s ease, transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-detail-back:hover { color: var(--cv-text); }\r\n.cv-detail-back:active { transform: scale(0.97); }\r\n\r\n.cv-detail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 1.25rem; }\r\n.cv-detail-identity {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n text-align: center;\r\n padding: 0.75rem 0 1.5rem;\r\n}\r\n.cv-detail-avatar {\r\n width: 72px;\r\n height: 72px;\r\n border-radius: 999px;\r\n /* Same radial + border treatment as `.cv-item-avatar` (see its own comment), just larger. Neutral\r\n by default \u2014 a contact has no \"status\" to colour. `HistoryDetail` overrides both with `[data-tone]`\r\n below: a call DOES have one, the same one the list row already shows, so the hero avatar staying\r\n grey there would be information the list already gave up on repeating here. */\r\n background: radial-gradient(circle, var(--cv-surface), var(--cv-surface-2));\r\n border: 1px solid var(--cv-border-strong);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--cv-muted);\r\n font-size: 1.35rem;\r\n font-weight: 700;\r\n margin-bottom: 0.85rem;\r\n}\r\n/* Same token pairs already verified for `.cv-item-avatar[data-tone]` \u2014 size doesn't change a contrast\r\n ratio, so no new numbers to measure here. */\r\n.cv-detail-avatar[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); border-color: transparent; }\r\n.cv-detail-avatar[data-tone='info'] { background: var(--cv-info-soft); color: var(--cv-info-strong); border-color: transparent; }\r\n.cv-detail-avatar[data-tone='danger'] { background: var(--cv-danger-soft); color: var(--cv-danger-strong); border-color: transparent; }\r\n.cv-detail-name {\r\n margin: 0;\r\n font-size: 1.1rem;\r\n font-weight: 700;\r\n color: var(--cv-text);\r\n letter-spacing: -0.01em;\r\n}\r\n.cv-detail-name[data-tone='danger'] { color: var(--cv-danger-strong); }\r\n.cv-detail-meta {\r\n margin: 0.2rem 0 0;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n}\r\n\r\n/* \u2500\u2500 Call detail: recording \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-detail-recording { margin-bottom: 1rem; }\r\n.cv-detail-recording-label {\r\n margin: 0 0 0.4rem;\r\n padding: 0 0.15rem;\r\n font-size: 0.68rem;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.03em;\r\n color: var(--cv-muted);\r\n}\r\n.cv-detail-audio { width: 100%; display: block; }\r\n.cv-detail-audio + .cv-detail-audio { margin-top: 0.5rem; }\r\n.cv-detail-recording-status {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.5rem;\r\n margin: 0 0 1rem;\r\n padding: 0.65rem 0.75rem;\r\n border-radius: 12px;\r\n background: var(--cv-surface);\r\n color: var(--cv-muted);\r\n font-size: 0.78rem;\r\n}\r\n/*\r\n Text colour only, same neutral `--cv-surface` background the other states use \u2014 NOT a\r\n `--cv-danger-soft` fill. Measured first, not assumed: `--cv-danger-strong` on `--cv-danger-soft`\r\n is 3.34:1 light / 3.62:1 dark, well under the 4.5:1 this 0.78rem text needs (it only ever cleared\r\n the SEPARATE 3:1 non-text bar this token was built for \u2014 an icon on that badge, not body text on\r\n it). Same lesson the person-filter fix already paid for: a badge-strength pairing that works for an\r\n icon is not automatically safe for text.\r\n*/\r\n.cv-detail-recording-status--warn { color: var(--cv-danger-strong); }\r\n.cv-detail-recording-spinner { animation: cv-spin 0.8s linear infinite; flex-shrink: 0; }\r\n\r\n/* \u2500\u2500 Call detail: who handled it \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-detail-handlers { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }\r\n.cv-detail-handler-hop { display: inline-flex; align-items: center; gap: 0.3rem; }\r\n.cv-detail-handler-arrow { color: var(--cv-faint); flex-shrink: 0; }\r\n.cv-detail-handler-chip {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n padding: 0.15rem 0.5rem;\r\n border-radius: 999px;\r\n background: var(--cv-surface-2);\r\n color: var(--cv-text);\r\n font-size: 0.75rem;\r\n font-weight: 600;\r\n}\r\n/* The LAST hop \u2014 the one the call actually ended with \u2014 is the one worth a glance first. */\r\n.cv-detail-handler-chip[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }\r\n.cv-detail-handler-ext {\r\n font-weight: 400;\r\n font-variant-numeric: tabular-nums;\r\n opacity: 0.75;\r\n}\r\n\r\n.cv-detail-rows {\r\n border: 1px solid var(--cv-border);\r\n border-radius: 14px;\r\n overflow: hidden;\r\n background: var(--cv-bg);\r\n}\r\n.cv-detail-row {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.7rem;\r\n width: 100%;\r\n padding: 0.7rem 0.85rem;\r\n border: none;\r\n border-top: 1px solid var(--cv-border);\r\n background: none;\r\n font: inherit;\r\n color: inherit;\r\n text-align: left;\r\n}\r\n.cv-detail-row:first-child { border-top: none; }\r\n.cv-detail-row--action {\r\n cursor: pointer;\r\n transition: background 0.12s ease;\r\n}\r\n.cv-detail-row--action:hover { background: var(--cv-surface); }\r\n.cv-detail-row-icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 999px;\r\n background: var(--cv-surface-2);\r\n /*\r\n Without this the circle nearly vanishes into its own row: `--cv-surface-2` measures only ~1.1:1\r\n against the row's rest-state background and ~1.05:1 against `--cv-surface`, the color\r\n `.cv-detail-row--action:hover` sets the ROW to \u2014 both far below a readable boundary, and worse\r\n than `.cv-item-avatar`/`.cv-detail-avatar` above even manage WITH their border (1.18:1 light /\r\n 1.57:1 dark). Same token, same fix, same reason.\r\n */\r\n border: 1px solid var(--cv-border-strong);\r\n color: var(--cv-muted);\r\n flex-shrink: 0;\r\n}\r\n.cv-detail-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }\r\n.cv-detail-row-eyebrow {\r\n font-size: 0.68rem;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.03em;\r\n color: var(--cv-muted);\r\n}\r\n.cv-detail-row-value {\r\n font-size: 0.85rem;\r\n font-weight: 500;\r\n color: var(--cv-text);\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n.cv-detail-row-chevron { color: var(--cv-faint); flex-shrink: 0; }\r\n\r\n.cv-detail-foot { flex-shrink: 0; padding: 0.85rem 1.25rem 1.1rem; }\r\n.cv-detail-call { width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.4rem; }\r\n\r\n/* \u2500\u2500 Settings tab (device selection, SPEC \xA77 Phase 2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-settings-body {\r\n flex: 1;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 1rem;\r\n display: flex;\r\n flex-direction: column;\r\n /* Generous separation BETWEEN sections, deliberately larger than any gap inside one \u2014 grouping\r\n only reads as grouping if the space between groups is clearly bigger than the space within one\r\n (see impeccable's layout guidance: \"tight grouping, generous separation\"). */\r\n gap: 1.5rem;\r\n}\r\n\r\n/*\r\n One labeled group (`SettingsSection`): a title, and a bordered body holding its rows. Ajustes\r\n started as a flat list of two rows with nothing to say they were related \u2014 fine for exactly two\r\n rows, unreadable the moment an unrelated third setting lands with nothing to tell it apart from\r\n the device rows above it.\r\n\r\n The body is ONE card, not a card per row: `.cv-settings-row`/`.cv-settings-note` below carry no\r\n border or background of their own, only a divider between siblings \u2014 the same shape\r\n `.cv-detail-rows`/`.cv-detail-row` already use elsewhere in this file, and deliberately not a\r\n stack of individually-bordered cards (nested-card territory).\r\n*/\r\n.cv-settings-section { display: flex; flex-direction: column; gap: 0.5rem; }\r\n.cv-settings-section-title {\r\n margin: 0;\r\n padding: 0 0.2rem;\r\n font-size: 0.7rem;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.04em;\r\n color: var(--cv-muted);\r\n}\r\n.cv-settings-section-body {\r\n border: 1px solid var(--cv-border);\r\n border-radius: 14px;\r\n background: var(--cv-bg);\r\n overflow: hidden;\r\n}\r\n.cv-settings-section-body > * { padding: 0.75rem 0.85rem; }\r\n.cv-settings-section-body > *:not(:first-child) { border-top: 1px solid var(--cv-border); }\r\n\r\n.cv-settings-row { display: flex; align-items: center; gap: 0.7rem; }\r\n/* Same treatment as `.cv-detail-row-icon`, same reason \u2014 see its own comment on why the border is\r\n load-bearing: `--cv-surface-2` alone nearly vanishes into the section's own background. */\r\n.cv-settings-row-icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 999px;\r\n background: var(--cv-surface-2);\r\n border: 1px solid var(--cv-border-strong);\r\n color: var(--cv-muted);\r\n flex-shrink: 0;\r\n}\r\n.cv-settings-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.35rem; }\r\n/* Same pairing as `.cv-detail-row-eyebrow` (`--cv-muted` on `--cv-bg`), verified there already. */\r\n.cv-settings-row-label {\r\n font-size: 0.68rem;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.03em;\r\n color: var(--cv-muted);\r\n}\r\n.cv-settings-select {\r\n width: 100%;\r\n padding: 0.45rem 0.6rem;\r\n border-radius: 8px;\r\n border: 1px solid var(--cv-border-strong);\r\n background: var(--cv-surface);\r\n color: var(--cv-text);\r\n font: inherit;\r\n font-size: 0.85rem;\r\n transition: border-color 0.15s ease, background 0.15s ease;\r\n}\r\n.cv-settings-select:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }\r\n.cv-settings-empty { font-size: 0.85rem; color: var(--cv-muted); }\r\n/* `--cv-muted` on `--cv-bg` \u2014 4.83:1 light / 6.97:1 dark, computed fresh for THIS pairing (the note\r\n used to sit on `--cv-surface` with its own card; now it sits directly on the section's `--cv-bg`,\r\n a different background that earns its own check rather than inheriting the old one's number). */\r\n.cv-settings-note {\r\n margin: 0;\r\n color: var(--cv-muted);\r\n font-size: 0.8rem;\r\n line-height: 1.5;\r\n}\r\n/* Closing note for the tab as a whole, not any one section \u2014 sits directly on `.cv-settings-body`,\r\n which shares the same `--cv-bg` as `.cv-settings-section-body` above, so the `--cv-muted`-on-\r\n `--cv-bg` pairing already verified for `.cv-settings-note` carries over unchanged. Centred and\r\n quieter than a row: this is a footnote about the tab, not a row's worth of content. */\r\n.cv-settings-more {\r\n margin: 0;\r\n padding: 0 0.5rem;\r\n text-align: center;\r\n color: var(--cv-muted);\r\n font-size: 0.78rem;\r\n line-height: 1.5;\r\n}\r\n\r\n/* \u2500\u2500 Centred states (mic gate) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-center {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.85rem;\r\n padding: 1.75rem;\r\n text-align: center;\r\n}\r\n.cv-center p { color: var(--cv-muted); margin: 0; font-size: 0.85rem; line-height: 1.5; }\r\n\r\n\r\n/* \"C\xF3mo permitirlo\" \u2014 a native <details> disclosure.\r\n Collapsed by default: the people who already know where the padlock is should not have to read\r\n past three steps to get there, and the people who don't get them without leaving the app. */\r\n.cv-help {\r\n width: 100%;\r\n margin-top: 0.15rem;\r\n text-align: left;\r\n}\r\n.cv-help summary {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n justify-content: center;\r\n width: 100%;\r\n padding: 0.3rem;\r\n border-radius: 8px;\r\n color: var(--cv-muted);\r\n font: 500 0.75rem var(--cv-font);\r\n cursor: pointer;\r\n list-style: none; /* hide the default triangle; the icon is the affordance */\r\n transition: color 0.15s ease, background 0.15s ease;\r\n}\r\n.cv-help summary::-webkit-details-marker { display: none; }\r\n.cv-help summary:hover { color: var(--cv-text); background: var(--cv-surface-2); }\r\n.cv-help[open] summary { color: var(--cv-text); margin-bottom: 0.35rem; }\r\n\r\n.cv-help ol {\r\n margin: 0;\r\n padding: 0.6rem 0.7rem 0.6rem 1.6rem;\r\n background: var(--cv-surface);\r\n border-radius: 10px;\r\n color: var(--cv-muted);\r\n font-size: 0.73rem;\r\n line-height: 1.55;\r\n}\r\n.cv-help li + li { margin-top: 0.3rem; }\r\n.cv-help li::marker { color: var(--cv-faint); font-variant-numeric: tabular-nums; }\r\n\r\n.cv-help a {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.25rem;\r\n margin-top: 0.45rem;\r\n padding-left: 0.1rem;\r\n color: var(--cv-accent-strong);\r\n font-size: 0.73rem;\r\n font-weight: 500;\r\n text-decoration: none;\r\n}\r\n.cv-help a:hover { text-decoration: underline; }\r\n\r\n/* The panel is the only thing that moves, and only when opened by hand \u2014 so it animates. */\r\n.cv-help[open] ol {\r\n animation: cv-gate-in 200ms var(--cv-ease-out) both;\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-help[open] ol { animation: none; }\r\n}\r\n\r\n/* \u2500\u2500 The permission gate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n Four states, and only ONE of them deserves a designed screen.\r\n\r\n `checking` resolves in ~100\u2013400 ms. Rendering it immediately means the user\r\n sees text appear and vanish inside a blink \u2014 a flash reads as jank, not as\r\n information. So it is delayed 450 ms with `both` fill: invisible during the\r\n delay, and if the check finishes first (the usual case, and always the case\r\n when permission is already granted) it is never painted at all.\r\n\r\n `prompting` is the one the user actually reads and acts on. It gets the\r\n hierarchy: mark, title, reason, action.\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-gate {\r\n flex: 1;\r\n min-height: 0;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.6rem;\r\n padding: 1.5rem 1.75rem;\r\n text-align: center;\r\n}\r\n\r\n/* The disc echoes the call avatar \u2014 one visual language, not two. */\r\n.cv-gate-mark {\r\n display: grid;\r\n place-items: center;\r\n width: 52px;\r\n height: 52px;\r\n border-radius: 999px;\r\n background: var(--cv-accent-soft);\r\n color: var(--cv-accent-strong);\r\n margin-bottom: 0.15rem;\r\n}\r\n.cv-gate[data-tone='danger'] .cv-gate-mark {\r\n background: var(--cv-danger-soft);\r\n color: var(--cv-danger);\r\n}\r\n\r\n.cv-gate-title {\r\n margin: 0;\r\n font: 600 0.95rem var(--cv-font);\r\n color: var(--cv-text);\r\n letter-spacing: -0.01em;\r\n}\r\n.cv-gate p {\r\n margin: 0;\r\n color: var(--cv-muted);\r\n font-size: 0.8rem;\r\n line-height: 1.5;\r\n max-width: 24ch; /* two comfortable lines, not one long one */\r\n}\r\n.cv-gate .cv-btn { margin-top: 0.5rem; }\r\n\r\n/* Entrance. Staggered 45 ms apart: the eye follows mark \u2192 title \u2192 reason \u2192\r\n action, which is the order it needs to read them in anyway. */\r\n.cv-gate > * {\r\n animation: cv-gate-in 260ms var(--cv-ease-out) both;\r\n}\r\n.cv-gate > *:nth-child(2) { animation-delay: 45ms; }\r\n.cv-gate > *:nth-child(3) { animation-delay: 90ms; }\r\n.cv-gate > *:nth-child(4) { animation-delay: 135ms; }\r\n\r\n@keyframes cv-gate-in {\r\n from { opacity: 0; transform: translateY(6px); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n\r\n/* The checking state: held back, then a quiet fade. No stagger, no movement \u2014\r\n it is a status, not a screen, and it should barely register. */\r\n.cv-gate[data-state='checking'] > *,\r\n.cv-gate[data-state='warming'] > * {\r\n animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both;\r\n}\r\n@keyframes cv-gate-wait {\r\n from { opacity: 0; }\r\n to { opacity: 1; }\r\n}\r\n/* A slow breath rather than a spinner: a spinner promises a long wait, and this\r\n is not one. */\r\n.cv-gate[data-state='checking'] .cv-gate-mark,\r\n.cv-gate[data-state='warming'] .cv-gate-mark {\r\n background: var(--cv-surface-2);\r\n color: var(--cv-muted);\r\n animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both,\r\n cv-breathe 1.8s ease-in-out 450ms infinite;\r\n}\r\n@keyframes cv-breathe {\r\n 0%, 100% { opacity: 1; }\r\n 50% { opacity: 0.45; }\r\n}\r\n\r\n/* Reduced motion: keep the fade and \u2014 critically \u2014 keep the DELAY, which is\r\n timing rather than motion and is the whole point. Drop the travel. */\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-gate > * {\r\n animation: cv-gate-wait 200ms var(--cv-ease-out) both;\r\n }\r\n .cv-gate[data-state='checking'] .cv-gate-mark,\r\n .cv-gate[data-state='warming'] .cv-gate-mark {\r\n animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both;\r\n }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-avatar-ping,\r\n .cv-timer i { animation: none; }\r\n .cv-softphone *,\r\n .cv-softphone *::before { transition-duration: 0.01ms !important; }\r\n}\r\n\r\n/* `*` and `#` stay in the grid \u2014 a keypad without them is not a keypad \u2014 but they are not part of a\r\n number (toE164 keeps digits only). Dimmed rather than `disabled` so the grid still reads as a\r\n keypad and the glyphs stay legible; they become live DTMF on the in-call screen. */\r\n.cv-key[data-dialable='false'] {\r\n cursor: default;\r\n}\r\n.cv-key[data-dialable='false'] b {\r\n color: var(--cv-faint);\r\n}\r\n.cv-key[data-dialable='false']:hover {\r\n background: none;\r\n}\r\n.cv-key[data-dialable='false']:active {\r\n transform: none;\r\n}\r\n\r\n/* \u2500\u2500 Country picker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n The prefix chip + its list. Sits where the old left spacer was, so the number\r\n stays optically centred against the 36px erase button on the right.\r\n\r\n The popover is absolutely positioned inside the widget rather than portalled:\r\n there is nowhere to portal TO inside a shadow root, and the widget already owns\r\n its stacking context. That also means it cannot be clipped by a host's\r\n `overflow:hidden` \u2014 the failure SPEC \xA72.6 lists for the CRM's Radix version.\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n/* The anchor spans the whole ROW, not just the chip.\r\n With the chip centred, anchoring to the chip put the popover's left edge at ~120px and its right\r\n edge past the widget \u2014 clipped by `.cv-softphone { overflow: hidden }`, which is the classic\r\n absolute-inside-overflow bug. Anchoring to the full row makes overflowing arithmetically\r\n impossible: `left:0; right:0` cannot be wider than the row that contains it. */\r\n.cv-cp {\r\n position: relative;\r\n display: flex;\r\n justify-content: center;\r\n width: 100%;\r\n}\r\n/* A PILL, with a resting surface and a border.\r\n This was `border: none; background: none` \u2014 invisible until hover, so it read as a label rather\r\n than a control. That was survivable when it sat inline beside the number; alone on its own row it\r\n is just floating text, and nothing says \"you can change this\". A control that changes state has to\r\n look like one at rest, not only under a cursor that may never arrive (touch has no hover at all). */\r\n.cv-cp-chip {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n height: 30px;\r\n padding: 0 0.5rem 0 0.4rem;\r\n border: 1px solid var(--cv-border-strong);\r\n border-radius: 999px;\r\n background: var(--cv-surface);\r\n color: var(--cv-text);\r\n font: 600 0.8rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s, border-color 0.15s;\r\n}\r\n.cv-cp-chip:hover { background: var(--cv-surface-2); }\r\n.cv-cp-chip:active { transform: scale(0.95); }\r\n/* Open: the chip is the thing the list belongs to, so it stays visibly engaged while the list is up. */\r\n.cv-cp-chip[aria-expanded='true'] {\r\n background: var(--cv-accent-soft);\r\n border-color: var(--cv-accent);\r\n}\r\n.cv-cp-dial {\r\n font-variant-numeric: tabular-nums;\r\n letter-spacing: -0.02em;\r\n}\r\n.cv-cp-caret {\r\n color: var(--cv-faint);\r\n flex-shrink: 0;\r\n transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n.cv-cp-caret[data-open='true'] { transform: rotate(180deg); }\r\n\r\n/* The country mark. TWO designed states in ONE 20\xD714 box (a flag's own 3:2), so the chip and every\r\n row keep their rhythm whichever is showing \u2014 nothing reflows when the flags land. The plate is the\r\n LOADING state (flags are a lazy import) and the offline state, not a fallback: there is no flash of\r\n something broken, only a designed plate becoming a designed flag, once per session.\r\n `::part(country-mark)` lets a host restyle either. */\r\n.cv-mark {\r\n width: 20px;\r\n height: 14px;\r\n flex-shrink: 0;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 14px;\r\n line-height: 1;\r\n font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', var(--cv-font);\r\n}\r\n/* The SVG fills the slot exactly; 3x2 is the flag aspect, so nothing is letterboxed or cropped. */\r\n.cv-mark-svg {\r\n width: 100%;\r\n height: 100%;\r\n display: block;\r\n border-radius: 2px;\r\n object-fit: cover;\r\n}\r\n.cv-mark[data-plate='true'] {\r\n /* The ISO code set into the slot a flag will occupy. */\r\n font-family: var(--cv-font);\r\n font-size: 8.5px;\r\n font-weight: 700;\r\n letter-spacing: 0.04em;\r\n color: var(--cv-text);\r\n background: var(--cv-surface-2);\r\n border: 1px solid var(--cv-border);\r\n border-radius: 3px;\r\n /* Cap-height letters sit optically high in a 14px box; nudge them back to centre. */\r\n padding-top: 1px;\r\n}\r\n\r\n.cv-cp-pop {\r\n position: absolute;\r\n top: calc(100% + 6px);\r\n /* Pinned to BOTH row edges, so it is bounded by the container by construction \u2014 no width guess,\r\n no media query, nothing to clip. `max-width` keeps it from looking stretched on a wide host. */\r\n left: 0;\r\n right: 0;\r\n margin-inline: auto;\r\n max-width: 260px;\r\n z-index: 20;\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--cv-surface);\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius);\r\n box-shadow: var(--cv-shadow);\r\n overflow: hidden;\r\n /* Grows from the chip it belongs to, rather than appearing on top of it. */\r\n transform-origin: top center;\r\n animation: cv-cp-in 0.14s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n@keyframes cv-cp-in {\r\n from { opacity: 0; transform: translateY(-4px) scale(0.97); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-cp-pop { animation: none; }\r\n .cv-cp-caret { transition: none; }\r\n}\r\n.cv-cp-search {\r\n flex-shrink: 0;\r\n padding: 0.5rem 0.65rem;\r\n border: none;\r\n border-bottom: 1px solid var(--cv-border);\r\n background: none;\r\n color: var(--cv-text);\r\n font: 400 0.8rem var(--cv-font);\r\n outline: none;\r\n}\r\n.cv-cp-search::placeholder { color: var(--cv-faint); }\r\n/* Both states named explicitly. In the CRM the widget mounts via `/ui`, i.e. in the LIGHT DOM \u2014 none\r\n of the shadow-DOM isolation the web-component playground proves applies there, so a host's focus\r\n ring (or the browser's own) can land on our input. The accent underline IS the focus indicator. */\r\n.cv-cp-search:focus,\r\n.cv-cp-search:focus-visible {\r\n outline: none;\r\n border-bottom-color: var(--cv-accent);\r\n border-bottom-width: 2px;\r\n padding-bottom: calc(0.5rem - 1px);\r\n}\r\n.cv-cp-list {\r\n overflow-y: auto;\r\n max-height: 220px;\r\n padding: 0.25rem;\r\n}\r\n.cv-cp-item {\r\n width: 100%;\r\n display: flex;\r\n align-items: center;\r\n gap: 0.5rem;\r\n padding: 0.4rem 0.45rem;\r\n border: none;\r\n border-radius: calc(var(--cv-radius) - 4px);\r\n background: none;\r\n color: var(--cv-text);\r\n font: 400 0.8rem var(--cv-font);\r\n text-align: left;\r\n cursor: pointer;\r\n}\r\n.cv-cp-item:hover { background: var(--cv-surface-2); }\r\n/* The current country is stated by the row, not only by a tick at the far right. */\r\n.cv-cp-item[aria-selected='true'] { background: var(--cv-accent-soft); }\r\n.cv-cp-item[aria-selected='true'] .cv-cp-name { font-weight: 600; }\r\n.cv-cp-name {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n.cv-cp-item .cv-cp-dial {\r\n color: var(--cv-muted);\r\n font-weight: 400;\r\n}\r\n.cv-cp-check { color: var(--cv-accent-strong); flex-shrink: 0; }\r\n.cv-cp-check[data-on='false'] { opacity: 0; }\r\n.cv-cp-empty {\r\n margin: 0;\r\n padding: 1rem 0.5rem;\r\n text-align: center;\r\n color: var(--cv-muted);\r\n font-size: 0.78rem;\r\n}\r\n";
11229
+ var softphone_default = "/**\r\n * @coventia/softphone/styles.css\r\n *\r\n * The theming contract (SPEC \xA72.6) + all component styles. A host restyles the softphone by\r\n * overriding the `--cv-*` custom properties \u2014 which are INHERITED, so they cross the shadow boundary\r\n * of `<coventia-softphone>` by design (the host's own CSS *selectors* do not, which is what stops a\r\n * stray `button { width:100% }` from destroying the dialpad).\r\n *\r\n * Defaults match the Coventia product: light surface, emerald accent, soft rounded key cards.\r\n *\r\n * Reseller: coventia-softphone { --cv-accent:#7c3aed; --cv-bg:#0b0b0f; }\r\n * CRM (shadcn bridge, ~15 lines, dark mode included):\r\n * .cv-softphone { --cv-bg:hsl(var(--background)); --cv-text:hsl(var(--foreground)); \u2026 }\r\n *\r\n * TYPOGRAPHY NOTE: `--cv-font` is a system stack on purpose. An embedded widget should inherit its\r\n * host's type, not force a webfont into someone else's app; hosts override the token if they want.\r\n *\r\n * Strings are Spanish-only (SPEC \xA72.4). No i18n.\r\n */\r\n\r\n/*\r\n * The custom element's own box.\r\n *\r\n * A custom element is `display: inline` by default and has NO size \u2014 and the widget inside now fills\r\n * its host rather than asserting 340\xD7520, so without this the element would render to ZERO. The\r\n * drop-in promise is \"one tag and it works\", which means the TAG must carry a box.\r\n *\r\n * These are defaults, not decisions: they sit on `:host`, so any host stylesheet outranks them \u2014\r\n *\r\n * coventia-softphone { width: 100%; height: 100%; } \u2190 a sidebar\r\n * coventia-softphone { height: 600px; } \u2190 taller\r\n *\r\n * React consumers never see this: they import `/ui` and their own wrapper states the box (the CRM's\r\n * PhoneDialer does exactly that).\r\n */\r\n:host {\r\n display: block;\r\n width: 340px;\r\n height: 520px;\r\n}\r\n\r\n/* React's container inside the shadow root \u2014 element path only; inert everywhere else.\r\n *\r\n * It must pass the host's height straight through. `.cv-softphone` below is `height: 100%`, and a\r\n * percentage height resolves against the PARENT: leave this div at its default `auto` and that 100%\r\n * silently becomes `auto` too, so the widget collapses to its content height and the host's `height`\r\n * is ignored. What you see is a short card stranded in a taller box \u2014 which reads as a broken widget,\r\n * not as a broken wrapper. */\r\n.cv-mount {\r\n height: 100%;\r\n}\r\n\r\n:host,\r\n.cv-softphone {\r\n /* Surface */\r\n --cv-bg: #ffffff;\r\n --cv-surface: #f9fafb;\r\n --cv-surface-2: #f3f4f6;\r\n --cv-border: #f3f4f6;\r\n --cv-border-strong: #e5e7eb;\r\n\r\n /* Text \u2014 three tiers, split by CONTRAST DUTY, not by taste:\r\n `text` informational primary 17.7:1\r\n `muted` informational secondary 4.8:1 (AA \u2014 phone numbers, timestamps, tab labels)\r\n `faint` DECORATIVE only 2.5:1 (keypad letters, placeholder \u2014 never information)\r\n The legacy used gray-400 for all secondary text, which fails AA at 2.54:1. Splitting the tier\r\n keeps the look while making the informational text legible. */\r\n --cv-text: #111827;\r\n --cv-muted: #6b7280;\r\n --cv-faint: #9ca3af;\r\n\r\n /* Intent \u2014 emerald. Split by CONTRAST DUTY, because one \"brand green\" cannot do all three jobs:\r\n `accent` FILL that carries a white glyph \u2192 emerald-600, white 3.77:1 \u2713\r\n (emerald-500 is the prettier brand green but white on it is 2.54:1 \u2014 the\r\n call button's phone icon would fail WCAG non-text contrast. Measured, not guessed.)\r\n `accent-strong` emerald AS TEXT on a surface \u2192 emerald-700, 5.48:1 \u2713 for the 10px tab label\r\n `accent-light` DECORATIVE only (the ring halo) \u2014 never carries text, so it stays vivid */\r\n --cv-accent: #059669;\r\n --cv-accent-strong: #047857;\r\n --cv-accent-light: #10b981;\r\n --cv-accent-soft: #a7f3d0;\r\n --cv-accent-contrast: #ffffff;\r\n --cv-danger: #ef4444;\r\n /* `--cv-danger` ITSELF fails 3:1 non-text contrast against `--cv-danger-soft` (2.6:1, measured) \u2014\r\n the same AS-TEXT/on-a-tinted-surface split `accent` already has, `danger` was just missing until\r\n the Historial tab's status badges needed it. Darker on light (mirrors `accent-strong`), LIGHTER on\r\n dark (red-400) \u2014 same inversion `accent-strong` documents above, same reason: emphasis on a dark\r\n surface means more light, not less.\r\n red-700 (#b91c1c) was the first pick here and overshot both minimums (6.47:1 text, 4.47:1 badge)\r\n enough to read as a dark, brownish red rather than red \u2014 red-600 clears both with real margin\r\n still (4.83:1 / 3.34:1, measured) and looks like red. */\r\n --cv-danger-strong: #dc2626;\r\n --cv-danger-soft: #fecaca;\r\n /* A third status hue, sky \u2014 for the Historial row status chip specifically: an outgoing call that\r\n WAS answered gets its own colour rather than sharing `accent`, matching the legacy CRM's own\r\n three-way split (emerald/sky/rose) that this file's earlier restrained accent-only-for-\"fine\"\r\n version had collapsed away. Same strong/soft split, same inversion, as `accent`/`danger` above \u2014\r\n 4.47:1 light / 4.42:1 dark against `-soft`, measured. */\r\n --cv-info: #0284c7;\r\n --cv-info-strong: #0369a1;\r\n --cv-info-soft: #bae6fd;\r\n --cv-hold: #6366f1;\r\n\r\n /* Shape & type */\r\n --cv-radius: 16px;\r\n /* Strong ease-out. The built-in CSS easings are too weak to read as intentional. */\r\n --cv-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\r\n --cv-radius-key: 16px;\r\n --cv-font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\r\n --cv-font-size: 14px;\r\n --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 1px 3px rgb(0 0 0 / 0.06);\r\n\r\n color: var(--cv-text);\r\n font-family: var(--cv-font);\r\n font-size: var(--cv-font-size);\r\n}\r\n\r\n/**\r\n * Dark theme \u2014 matched to the Coventia product's own dark tokens, not invented.\r\n *\r\n * The CRM defines `--background: 240 10% 10%` (#17171c) with surfaces at 15.9% (#27272a) and borders\r\n * at 25.9% (#404044) \u2014 the zinc/240-hue family. An earlier draft here used a near-black\r\n * (#0f0f13, ~half the luminance of the product's background) with 2%-luminance borders that were\r\n * effectively invisible. That is the \"black tonality\" that read wrong: right hue, wrong lightness.\r\n *\r\n * Note the accent INVERTS: on light, `accent-strong` is darker than `accent` (emerald-700); on dark\r\n * it must be LIGHTER (emerald-400), because emphasis on a dark surface means more light, not less.\r\n * Same token, opposite direction \u2014 miss this and the timer/active tab sink into the background.\r\n *\r\n * \u2500\u2500 HOW THE THEME IS CHOSEN \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n * By default it follows the OS (`prefers-color-scheme`). But a host must be able to PIN it: the\r\n * Coventia CRM toggles dark with a class (next-themes), not the OS setting \u2014 so a user with a dark\r\n * OS but the CRM in light mode would otherwise get a dark widget inside a light app.\r\n *\r\n * <SoftphoneWidget theme=\"light\" /> \u2192 data-cv-theme=\"light\" \u2014 always light\r\n * <SoftphoneWidget theme=\"dark\" /> \u2192 data-cv-theme=\"dark\" \u2014 always dark\r\n * <SoftphoneWidget /> \u2192 follows the OS\r\n *\r\n * To follow the HOST's theme rather than the OS, pass the host's current theme through, e.g.\r\n * next-themes: `const { resolvedTheme } = useTheme(); <SoftphoneWidget theme={resolvedTheme} />`.\r\n *\r\n * The dark block is duplicated below (OS-driven vs. pinned) on purpose: plain CSS has no way to\r\n * share a declaration block across a media query and a selector, and the honest duplication beats a\r\n * clever custom-property toggle nobody can debug.\r\n */\r\n@media (prefers-color-scheme: dark) {\r\n :host(:not([data-cv-theme='light'])),\r\n .cv-softphone:not([data-cv-theme='light']) {\r\n --cv-bg: #17171c; /* CRM --background 240 10% 10% */\r\n --cv-surface: #202027; /* between bg and secondary */\r\n --cv-surface-2: #27272a; /* CRM --secondary/--muted 240 3.7% 15.9% */\r\n --cv-border: #2e2e33;\r\n --cv-border-strong: #404044; /* CRM --border 240 3.7% 25.9% */\r\n --cv-text: #fafafa; /* CRM --foreground 0 0% 98% */\r\n --cv-muted: #a1a1aa; /* CRM --muted-foreground 240 5% 64.9% \u2192 6.97:1 AA */\r\n --cv-faint: #71717a; /* decorative only */\r\n --cv-accent-strong: #34d399; /* emerald-400 \u2014 9.29:1 on #17171c */\r\n --cv-accent-light: #34d399; /* the halo reads brighter on dark */\r\n --cv-accent-soft: #065f46; /* glows must darken on dark, not glare */\r\n --cv-danger-strong: #f87171; /* red-400 \u2014 LIGHTER than --cv-danger, same inversion as accent-strong */\r\n --cv-danger-soft: #7f1d1d;\r\n --cv-info-strong: #38bdf8; /* sky-400 \u2014 same inversion, same reason */\r\n --cv-info-soft: #0c4a6e;\r\n --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.4);\r\n }\r\n}\r\n\r\n/* Pinned dark \u2014 wins regardless of the OS setting. */\r\n:host([data-cv-theme='dark']),\r\n.cv-softphone[data-cv-theme='dark'] {\r\n --cv-bg: #17171c;\r\n --cv-surface: #202027;\r\n --cv-surface-2: #27272a;\r\n --cv-border: #2e2e33;\r\n --cv-border-strong: #404044;\r\n --cv-text: #fafafa;\r\n --cv-muted: #a1a1aa;\r\n --cv-faint: #71717a;\r\n --cv-accent-strong: #34d399;\r\n --cv-accent-light: #34d399;\r\n --cv-accent-soft: #065f46;\r\n --cv-danger-strong: #f87171;\r\n --cv-danger-soft: #7f1d1d;\r\n --cv-info-strong: #38bdf8;\r\n --cv-info-soft: #0c4a6e;\r\n --cv-shadow: 0 1px 2px rgb(0 0 0 / 0.4);\r\n}\r\n\r\n.cv-softphone,\r\n.cv-softphone *,\r\n.cv-softphone *::before,\r\n.cv-softphone *::after {\r\n box-sizing: border-box;\r\n}\r\n\r\n/* \u2500\u2500 Shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-softphone {\r\n display: flex;\r\n flex-direction: column;\r\n\r\n /* FILL the box the host gives us; do not assert one.\r\n This was `width: 340px; height: 520px`, which is a component telling its host how big its\r\n layout is. The CRM's panel is 310 wide, so the third keypad column was simply cut off \u2014 and no\r\n amount of theming could have fixed it, because the widget was not asking. A component in\r\n someone else's layout accepts a size. */\r\n width: 100%;\r\n height: 100%;\r\n\r\n /* \u2026but a dialpad is not a spreadsheet. Given 900px it should not become a 900px dialpad with\r\n 300px keys; it should be itself, centred. Cap and centre rather than stretch. */\r\n max-width: var(--cv-max-width, 420px);\r\n margin-inline: auto;\r\n\r\n /* Adapt to THIS box, not to the viewport. The widget can be 310px wide inside a 1920px screen \u2014\r\n a media query would ask the wrong question and get a confident wrong answer. Container queries\r\n are the only ones that know the truth, and a shadow root is the ideal place for them. */\r\n container-type: inline-size;\r\n container-name: cv;\r\n\r\n background: var(--cv-bg);\r\n overflow: hidden;\r\n /* The containing block for `.cv-transfer-notices` \u2014 it overlays this whole shell, top edge to top\r\n edge, not just `.cv-body`, so it reads as one seamless panel rather than a strip that starts a few\r\n pixels short of the shell's own rounded corner. */\r\n position: relative;\r\n\r\n /* The frame is the HOST's call. The CRM's panel already draws `border rounded-xl shadow-2xl`, so\r\n drawing our own put a card inside a card \u2014 nested chrome, which is always wrong. Defaulting to\r\n a frame still suits a bare drop-in on a blank page; a host that has its own sets these to\r\n `none` / `0` and the double edge disappears. */\r\n border: var(--cv-shell-border, 1px solid var(--cv-border-strong));\r\n border-radius: var(--cv-shell-radius, var(--cv-radius));\r\n box-shadow: var(--cv-shell-shadow, 0 4px 24px rgb(0 0 0 / 0.06));\r\n}\r\n\r\n/* Narrow hosts \u2014 a sidebar, a phone-width column. Reclaim the horizontal padding first: at 260px\r\n the gutters cost more than they give, and a cramped keypad is worse than a tight one. */\r\n@container cv (max-width: 300px) {\r\n .cv-keypad { padding: 0 0.6rem; gap: 6px; }\r\n .cv-dial-head { margin: 0 0.6rem 0.6rem; }\r\n}\r\n\r\n/* Short hosts. The flex chain already lets the keypad absorb what is left, but below this the keys\r\n stop being tappable \u2014 so buy the height back from the chrome instead of from the keys. */\r\n@container cv (max-height: 440px) {\r\n .cv-dial-head { margin-bottom: 0.4rem; }\r\n .cv-dial-foot { padding-bottom: 0.5rem; }\r\n .cv-controls { gap: 0.4rem; padding-bottom: 0.6rem; }\r\n}\r\n\r\n/* The flex chain that makes the layout work: body flexes, views fill it, only the\r\n keypad/list grows. Without `min-height:0` a grid child refuses to shrink and overflows. */\r\n.cv-body {\r\n flex: 1;\r\n min-height: 0;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n.cv-view {\r\n flex: 1;\r\n min-height: 0;\r\n display: flex;\r\n flex-direction: column;\r\n /* Containing block for `.cv-detail` below \u2014 an absolute overlay must cover exactly THIS tab's\r\n content, not escape to whatever the next positioned ancestor up the tree happens to be. */\r\n position: relative;\r\n}\r\n\r\n/* \u2500\u2500 Tab bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-tabs {\r\n display: flex;\r\n border-top: 1px solid var(--cv-border);\r\n background: var(--cv-bg);\r\n flex-shrink: 0;\r\n}\r\n.cv-tab {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n gap: 3px;\r\n padding: 9px 0 8px;\r\n background: none;\r\n border: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n font: inherit;\r\n transition: color 0.15s ease, background 0.15s ease;\r\n}\r\n.cv-tab:hover:not(:disabled) { background: var(--cv-surface); }\r\n.cv-tab[aria-selected='true'] { color: var(--cv-accent-strong); }\r\n.cv-tab:disabled { opacity: 0.35; cursor: not-allowed; }\r\n.cv-tab span { font-size: 10px; font-weight: 600; line-height: 1; letter-spacing: 0.01em; }\r\n\r\n/* \u2500\u2500 Transfer outcome notices (Phase A \u2014 transfer-outcome-relay.md \xA75.5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n/*\r\n A persistent, dismissable banner OVER `.cv-body`, never a toast and never auto-dismissed \u2014 it carries\r\n an action, and WCAG 2.2.1 requires a time limit to be removable, adjustable or extended.\r\n\r\n Overlaps rather than reserving its own height, the same reasoning `.cv-dial-outcome`'s own comment\r\n already gives for why the widget never lets a line of text resize the keypad under a moving finger \u2014\r\n here that principle cuts the other way: appearing costs nothing but a repaint, not a reflow, so the\r\n widget's overall height stays identical whether a notice is showing or not, exactly like `.cv-detail`\r\n overlaying `.cv-body`'s own list rather than pushing it down. It sits over whatever the current tab\r\n happens to be showing (most often the dialer's country picker and number field \u2014 an acceptable loss:\r\n the agent is not expected to start a fresh manual dial before dealing with a transfer that just failed\r\n to connect), which is also why it outranks `.cv-detail` in `z-index`.\r\n\r\n `[data-has-notices='false']` collapses to zero footprint: the element stays mounted (so its\r\n `role=\"status\"` region exists before the FIRST notice ever arrives \u2014 an aria-live region inserted at\r\n the same moment as its own content is not reliably announced), but an idle widget, the common case,\r\n covers nothing and costs no paint either.\r\n*/\r\n.cv-transfer-notices {\r\n position: absolute;\r\n inset: 0 0 auto 0;\r\n z-index: 3; /* above `.cv-detail`'s 2 \u2014 this is the higher-priority thing on screen */\r\n}\r\n.cv-transfer-notices[data-has-notices='true'] {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.5rem;\r\n padding: 0.6rem 0.75rem;\r\n background: var(--cv-bg);\r\n box-shadow: var(--cv-shadow);\r\n animation: cv-transfer-notices-in 200ms var(--cv-ease-out) both;\r\n}\r\n@keyframes cv-transfer-notices-in {\r\n from { opacity: 0; transform: translateY(-6px); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-transfer-notices[data-has-notices='true'] { animation: none; }\r\n}\r\n.cv-transfer-notices-count {\r\n margin: 0;\r\n font: 600 0.72rem var(--cv-font);\r\n color: var(--cv-muted);\r\n text-transform: uppercase;\r\n letter-spacing: 0.04em;\r\n}\r\n.cv-transfer-notice {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 0.6rem;\r\n padding: 0.65rem 0.7rem;\r\n border-radius: var(--cv-radius-key);\r\n border: 1px solid var(--cv-danger-soft);\r\n background: var(--cv-surface);\r\n}\r\n/* Same shape as `.cv-gate-mark`, smaller \u2014 one visual language for \"here is a status\", not two. */\r\n.cv-transfer-notice-mark {\r\n flex-shrink: 0;\r\n display: grid;\r\n place-items: center;\r\n width: 30px;\r\n height: 30px;\r\n border-radius: 999px;\r\n background: var(--cv-danger-soft);\r\n color: var(--cv-danger);\r\n}\r\n.cv-transfer-notice-body { flex: 1; min-width: 0; }\r\n/*\r\n Title shares its row with only the dismiss button (24px) \u2014 not the whole card's width, and not the\r\n \"Llamar de vuelta\" button, which gets its own row below (`.cv-transfer-notice-footer`). The original\r\n layout put mark + title + subtitle + a full button all in one flex row, leaving the title as little as\r\n ~80px to wrap into at the real ~340px embed width \u2014 nearly every word landed on its own line.\r\n `align-items: flex-start` keeps the dismiss pinned to the first line even when the title wraps to two.\r\n*/\r\n.cv-transfer-notice-header { display: flex; align-items: flex-start; gap: 0.4rem; }\r\n.cv-transfer-notice-title {\r\n flex: 1;\r\n min-width: 0;\r\n margin: 0;\r\n font: 600 0.82rem var(--cv-font);\r\n color: var(--cv-text);\r\n line-height: 1.35;\r\n}\r\n.cv-transfer-notice-subtitle {\r\n margin: 0.15rem 0 0;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n line-height: 1.35;\r\n}\r\n/* Age and the callback button share a row too, same reasoning as the header \u2014 a short fixed-width\r\n timestamp beside a content-sized button, neither one fighting the title for space above. */\r\n.cv-transfer-notice-footer {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 0.5rem;\r\n margin-top: 0.35rem;\r\n}\r\n.cv-transfer-notice-age { margin: 0; font-size: 0.7rem; color: var(--cv-faint); flex-shrink: 0; }\r\n/*\r\n `.cv-btn.cv-transfer-notice-callback` \u2014 TWO classes, not one. Equal-specificity selectors resolve by\r\n source order, and this rule used to lose its `padding`/`font-size` to the shared `.cv-btn` rule below\r\n simply because that one appears later in the file; the button rendered at full default CTA size\r\n instead of this smaller, secondary one, which is what actually forced the title into that ~80px\r\n column above. The extra class is what makes this override win regardless of source order.\r\n*/\r\n.cv-btn.cv-transfer-notice-callback {\r\n flex-shrink: 0;\r\n padding: 0.35rem 0.6rem;\r\n font-size: 0.75rem;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n white-space: nowrap;\r\n}\r\n/* Same round icon-only shape as `.cv-search-clear` \u2014 one dismiss affordance, not a new one. */\r\n.cv-transfer-notice-dismiss {\r\n flex-shrink: 0;\r\n width: 24px;\r\n height: 24px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n border-radius: 999px;\r\n background: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n transition: color 0.12s ease, background 0.12s ease, transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-transfer-notice-dismiss:hover { color: var(--cv-text); background: var(--cv-surface-2); }\r\n.cv-transfer-notice-dismiss:active { transform: scale(0.88); }\r\n\r\n/* \u2500\u2500 Buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-btn {\r\n font: 600 0.875rem var(--cv-font);\r\n cursor: pointer;\r\n padding: 0.5rem 0.9rem;\r\n border-radius: 10px;\r\n border: 1px solid var(--cv-border-strong);\r\n background: var(--cv-bg);\r\n color: var(--cv-text);\r\n transition: background 0.15s ease, transform 0.06s ease;\r\n}\r\n.cv-btn:hover:not(:disabled) { background: var(--cv-surface); }\r\n.cv-btn:active:not(:disabled) { transform: scale(0.97); }\r\n.cv-btn:disabled { opacity: 0.45; cursor: not-allowed; }\r\n.cv-btn--accent {\r\n background: var(--cv-accent);\r\n border-color: var(--cv-accent);\r\n color: var(--cv-accent-contrast);\r\n}\r\n.cv-btn--accent:hover:not(:disabled) { background: var(--cv-accent-strong); }\r\n\r\n/* Round action buttons (answer / hangup / call) */\r\n.cv-round {\r\n width: 64px;\r\n height: 64px;\r\n border-radius: 999px;\r\n border: none;\r\n cursor: pointer;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: #fff;\r\n transition: background 0.15s ease, transform 0.08s ease, box-shadow 0.15s ease;\r\n}\r\n.cv-round:active:not(:disabled) { transform: scale(0.94); }\r\n.cv-round--answer {\r\n background: var(--cv-accent);\r\n box-shadow: 0 8px 20px -4px var(--cv-accent-soft);\r\n}\r\n.cv-round--answer:hover:not(:disabled) { background: var(--cv-accent-strong); }\r\n.cv-round--answer:disabled {\r\n background: var(--cv-surface-2);\r\n color: var(--cv-faint);\r\n box-shadow: none;\r\n cursor: not-allowed;\r\n}\r\n.cv-round--hangup {\r\n background: var(--cv-danger);\r\n box-shadow: 0 8px 20px -4px var(--cv-danger-soft);\r\n}\r\n.cv-round--hangup:hover:not(:disabled) { filter: brightness(0.94); }\r\n\r\n/* \u2500\u2500 Dialpad \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n/* The number: ONE bordered field, the prefix inside it on the left and erase on the right \u2014 the pattern of the\r\n providers' dialers (Talkdesk \"[\u{1F1FA}\u{1F1F8} +1 \u25BE | Type or paste a number]\", Zendesk, Aircall; outbound-number-per-agent.md\r\n \xA75.5, 2026-10-26). The prefix is part of the number being dialled, so it sits inside the same box. The number\r\n shrinks to fit (`DISPLAY_SIZES` in Dialpad.tsx). The field is the positioned ancestor of the country list, so the\r\n list spans its width (`left: 0; right: 0`). */\r\n.cv-dial-head {\r\n flex-shrink: 0;\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n height: 46px;\r\n margin: 0 1rem 0.75rem;\r\n padding: 0 0.2rem 0 0;\r\n border: 1px solid var(--cv-border-strong);\r\n border-radius: 10px;\r\n background: var(--cv-bg);\r\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\r\n}\r\n/* With no \"Llamar desde\" line above it (an org with one number), the field opens the view. */\r\n.cv-view > .cv-dial-head:first-child { margin-top: 1rem; }\r\n/* Focus shows on the field, as on any input (WCAG 2.4.7): the accent border and a soft ring. */\r\n.cv-dial-head:focus-within {\r\n border-color: var(--cv-accent);\r\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--cv-accent) 18%, transparent);\r\n}\r\n.cv-dial-head > .cv-cp {\r\n position: static;\r\n width: auto;\r\n flex-shrink: 0;\r\n align-self: stretch;\r\n}\r\n/* The prefix as the field's leading segment: the same chip, flush with the field, divided from the number. */\r\n.cv-dial-head .cv-cp-chip {\r\n height: 100%;\r\n padding: 0 0.45rem 0 0.65rem;\r\n border: 0;\r\n border-right: 1px solid var(--cv-border);\r\n border-radius: 9px 0 0 9px;\r\n background: var(--cv-surface);\r\n}\r\n.cv-dial-head .cv-cp-chip:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: -2px; }\r\n/* Following a number typed in full (\"+44 \u2026\"): read-only \u2014 the flag alone, no caret, no hover. */\r\n.cv-cp-chip[data-from-number] {\r\n cursor: default;\r\n padding: 0 0.6rem;\r\n}\r\n/*\r\n * The outcome of the last attempt, under the number.\r\n *\r\n * ALWAYS in the layout, visible or not. The keypad is `flex: 1` and absorbs whatever height is left,\r\n * so a line that appears and disappears would resize every key and shift the whole grid under the\r\n * user's finger \u2014 mid-dial, which is exactly when this text arrives. Reserving the row costs one line\r\n * of height and buys a keypad that never moves.\r\n */\r\n.cv-dial-outcome {\r\n flex-shrink: 0;\r\n margin: 0;\r\n min-height: 1.15rem;\r\n padding: 0 1rem;\r\n text-align: center;\r\n font-size: 0.78rem;\r\n line-height: 1.15rem;\r\n color: var(--cv-muted);\r\n /*\r\n One line, always. `error-text.test.ts` caps every outcome at 48 characters so nothing should ever\r\n reach this \u2014 but a cap depends on whoever writes the next string remembering it, and the failure\r\n it prevents is not cosmetic: the keypad below is `flex: 1`, so a second line resizes all twelve\r\n keys and shifts the grid under a finger that is already moving. Truncating is visibly wrong and\r\n harmless; reflowing the dialpad mid-dial is invisibly wrong and makes the user misdial.\r\n */\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n /* Muted, not danger: the phone still works. A red line here would read as a broken phone. */\r\n opacity: 0;\r\n transition: opacity 150ms ease-out;\r\n}\r\n.cv-dial-outcome[data-shown='true'] {\r\n opacity: 1;\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-dial-outcome {\r\n transition: none;\r\n }\r\n}\r\n\r\n/* Left-aligned after the prefix segment, as a phone field reads (\"+34 | 600 11 12 22\"). 1.375rem is the start of\r\n `DISPLAY_SIZES`, which steps it down until a long number fits the field. */\r\n.cv-display {\r\n flex: 1;\r\n min-width: 0;\r\n height: 100%;\r\n font: 500 1.375rem var(--cv-font);\r\n font-variant-numeric: tabular-nums;\r\n letter-spacing: -0.01em;\r\n text-align: left;\r\n color: var(--cv-text);\r\n border: none;\r\n background: none;\r\n padding: 0 0.6rem;\r\n caret-color: var(--cv-accent);\r\n}\r\n/* `--cv-muted`, not `--cv-faint`: a placeholder is read like any text (4.5:1). */\r\n.cv-display::placeholder { color: var(--cv-muted); font-weight: 400; }\r\n/* The field around it shows focus (`.cv-dial-head:focus-within`). */\r\n.cv-display:focus { outline: none; }\r\n\r\n/* The spacer/erase pair keeps the number optically centred. */\r\n.cv-dial-side {\r\n width: 36px;\r\n height: 36px;\r\n flex-shrink: 0;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n background: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n border-radius: 999px;\r\n transition: color 0.15s ease, opacity 0.15s ease, transform 0.06s ease;\r\n}\r\n.cv-dial-side:hover { color: var(--cv-text); }\r\n.cv-dial-side:active { transform: scale(0.9); }\r\n.cv-dial-side[data-hidden='true'] { opacity: 0; pointer-events: none; }\r\n\r\n/* THE layout fix: the keypad flexes to fill, and its rows share the space equally.\r\n Keys must NOT use aspect-ratio \u2014 that overflows the shell and shoves the call button\r\n under the tab bar. */\r\n.cv-keypad {\r\n flex: 1;\r\n min-height: 0;\r\n display: grid;\r\n grid-template-columns: repeat(3, 1fr);\r\n grid-template-rows: repeat(4, 1fr);\r\n gap: 8px;\r\n padding: 0 1rem;\r\n}\r\n.cv-key {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 1px;\r\n border-radius: var(--cv-radius-key);\r\n border: 1px solid var(--cv-border);\r\n background: var(--cv-bg);\r\n box-shadow: var(--cv-shadow);\r\n color: var(--cv-text);\r\n cursor: pointer;\r\n user-select: none;\r\n transition: background 0.12s ease, transform 0.06s ease;\r\n}\r\n.cv-key:hover { background: var(--cv-surface); }\r\n.cv-key:active { transform: scale(0.95); background: var(--cv-surface-2); }\r\n.cv-key b { font: 500 1.25rem/1 var(--cv-font); }\r\n.cv-key small {\r\n font-size: 9px;\r\n font-weight: 600;\r\n letter-spacing: 0.14em;\r\n /* `muted` (was `faint`, 2.5:1): the letters are read when dialling a number written in letters (900-FLORES),\r\n so they are text, held to 4.5:1 like any other (design critique, 2026-10-26). */\r\n color: var(--cv-muted);\r\n line-height: 1;\r\n}\r\n\r\n.cv-dial-foot {\r\n flex-shrink: 0;\r\n display: flex;\r\n justify-content: center;\r\n padding: 0.75rem 0 1rem;\r\n}\r\n\r\n/* \u2500\u2500 Active call \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-call {\r\n flex: 1;\r\n min-height: 0;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.15rem;\r\n text-align: center;\r\n padding: 1rem;\r\n /*\r\n A REAL bug, found live (plan T7): the transfer picker's agent list can make this box's content\r\n taller than the space `flex: 1` actually allocates it \u2014 the avatar/name/timer/status ABOVE the\r\n picker are still shown while transferring, unlike `.cv-call-devices-inline`'s own \"swap the\r\n content\" fix for the pre-connect devices screen. Without this, the overflow painted straight past\r\n `.cv-call`'s own shrunk box and onto `.cv-controls` below it \u2014 not scroll, actual overlap, controls\r\n rendered ON TOP of list rows. `overflow-y: auto` keeps any excess inside THIS box, scrollable,\r\n exactly like `.cv-presence-list`'s own internal scroll already does for the list itself.\r\n */\r\n overflow-y: auto;\r\n}\r\n\r\n.cv-avatar-wrap { position: relative; margin-bottom: 0.85rem; }\r\n.cv-avatar {\r\n position: relative;\r\n width: 68px;\r\n height: 68px;\r\n border-radius: 999px;\r\n /* accent \u2192 accent-strong (emerald 600\u2192700): white initials clear 3.77:1 at EVERY stop. Starting\r\n the ramp at emerald-500 would look punchier but drop the light end to 2.54:1. */\r\n background: linear-gradient(135deg, var(--cv-accent) 0%, var(--cv-accent-strong) 100%);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: #fff;\r\n font: 700 1.3rem var(--cv-font);\r\n letter-spacing: -0.01em;\r\n box-shadow: 0 8px 20px -6px var(--cv-accent-soft);\r\n}\r\n/* A soft halo while ringing \u2014 the one moment worth animating. */\r\n.cv-avatar-ping {\r\n position: absolute;\r\n inset: 0;\r\n border-radius: 999px;\r\n /* Decorative \u2014 nothing sits on it, so it uses the vivid emerald. */\r\n background: var(--cv-accent-light);\r\n opacity: 0.16;\r\n animation: cv-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite;\r\n pointer-events: none;\r\n}\r\n@keyframes cv-ping {\r\n 0% { transform: scale(1); opacity: 0.22; }\r\n 80%, 100% { transform: scale(1.7); opacity: 0; }\r\n}\r\n.cv-avatar-dot {\r\n position: absolute;\r\n bottom: 1px;\r\n right: 1px;\r\n width: 14px;\r\n height: 14px;\r\n border-radius: 999px;\r\n background: var(--cv-accent);\r\n border: 2px solid var(--cv-bg);\r\n}\r\n\r\n.cv-call-name { font-size: 1rem; font-weight: 600; line-height: 1.2; }\r\n.cv-call-number { font-size: 0.75rem; color: var(--cv-muted); margin-top: 2px; }\r\n/* Which of the org's numbers this outbound call shows. Smaller than the callee: context, not identity \u2014 but muted,\r\n not faint: --cv-faint is under 4.5:1 at this size in both themes, and the agent may need to read it mid-call. */\r\n.cv-call-from {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.25rem;\r\n max-width: 100%;\r\n margin-top: 4px;\r\n font-size: 0.72rem;\r\n color: var(--cv-muted);\r\n}\r\n.cv-call-from span {\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n.cv-timer {\r\n display: flex;\r\n align-items: baseline;\r\n justify-content: center;\r\n gap: 1px;\r\n margin-top: 0.75rem;\r\n color: var(--cv-accent-strong);\r\n}\r\n.cv-timer b {\r\n font: 700 2.25rem/1 var(--cv-font);\r\n font-variant-numeric: tabular-nums;\r\n letter-spacing: -0.03em;\r\n}\r\n.cv-timer i {\r\n font: 300 1.75rem/1 var(--cv-font);\r\n font-style: normal;\r\n color: var(--cv-accent);\r\n animation: cv-blink 1.6s ease-in-out infinite;\r\n margin: 0 1px;\r\n}\r\n@keyframes cv-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }\r\n\r\n.cv-call-status {\r\n margin-top: 0.6rem;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n letter-spacing: 0.04em;\r\n}\r\n.cv-badge-hold { color: var(--cv-hold); font-weight: 600; }\r\n\r\n/* \u2500\u2500 Device check (ringing / dialing / in-call) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n Before a call connects there is only ONE other control on screen (Colgar, or\r\n Rechazar/Contestar) \u2014 putting Dispositivos in that same neighbourhood, even\r\n spaced out, keeps it a mis-tap away from hanging up or answering. The entry\r\n chip lives beside the call status instead, removing it from that neighbourhood\r\n rather than just adding distance within it \u2014 same resting-pill SHAPE as `.cv-cp`\r\n (CountryPicker), reused rather than invented fresh.\r\n\r\n Opening it does NOT pop a panel into that same tight space, though \u2014 this\r\n SDK's real production embed (a 310\xD7520 host panel with its own header eating\r\n further into that) leaves too little free room around a trigger sitting in\r\n vertically-centred content for a floating popover to open into without either\r\n overlapping Colgar/Rechazar/Contestar or clamping itself down to something\r\n cramped and scrolly for just two rows. `.cv-call-devices-inline` REPLACES the\r\n avatar/name/status block in `.cv-call` instead (see that JSX's own comment) \u2014\r\n an inline swap, not an overlay, so it gets the full box, not a leftover sliver.\r\n\r\n Once connected it is the SAME chip and the SAME swap. Dispositivos used to be a fifth control in\r\n the call row (with a floating popover), which is what forced that row down to 40px buttons and\r\n 9px labels in a 310px host. A set-once setting does not need a slot among the four things you do\r\n mid-call (mute, hold, transfer, keypad), and the control row stays live under the swap.\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-call-devices-chip {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.35rem;\r\n height: 26px;\r\n padding: 0 0.65rem;\r\n border: 1px solid var(--cv-border-strong);\r\n border-radius: 999px;\r\n background: var(--cv-bg);\r\n color: var(--cv-muted);\r\n font: 600 0.7rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\r\n}\r\n.cv-call-devices-chip:hover { background: var(--cv-surface); }\r\n.cv-call-devices-chip:active { transform: scale(0.95); }\r\n\r\n/*\r\n The entry-point row \u2014 ONE chip today (Dispositivos), up to two once a host configures\r\n `incomingCallAction` (that one first: who is calling outranks a phone-utility setting). `width:\r\n 100%` + `flex-wrap` on purpose: a host's action label is arbitrary text this component cannot\r\n assume is short, so two chips sit side by side when they fit and wrap onto their own centred line\r\n when they do not, rather than overflowing `.cv-call`'s fixed width.\r\n*/\r\n.cv-call-chips {\r\n width: 100%;\r\n display: flex;\r\n flex-wrap: wrap;\r\n justify-content: center;\r\n gap: 0.5rem;\r\n margin-top: 0.65rem;\r\n}\r\n\r\n.cv-call-devices-inline {\r\n width: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.9rem;\r\n /* `.cv-call` centres and centre-aligns its normal (avatar) content \u2014 rows read left-aligned\r\n everywhere else in the widget (Ajustes, the in-call popover), so this opts back out of both. */\r\n text-align: left;\r\n animation: cv-call-devices-in 0.18s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n@keyframes cv-call-devices-in {\r\n from { opacity: 0; transform: translateY(6px); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-call-devices-inline { animation: none; }\r\n}\r\n.cv-call-devices-header { display: flex; align-items: center; gap: 0.5rem; }\r\n.cv-call-devices-title { font-size: 0.95rem; font-weight: 600; color: var(--cv-text); }\r\n/* `--cv-muted` on `--cv-bg` \u2014 the same already-verified pairing `.cv-settings-note` uses\r\n (4.83:1 light / 6.97:1 dark), not a new one. */\r\n.cv-call-devices-context {\r\n margin: 0;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n}\r\n\r\n/* \u2500\u2500 Call controls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n TWO TIERS. Hang up is the one action that must never be hunted for, so it sits\r\n alone, centred, and largest \u2014 the anchor. The secondaries flank above it. They\r\n used to share a single row, where hang up was just the last button and carried\r\n the same weight as \"transfer\"; mid-call, that is the wrong hierarchy.\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-controls {\r\n flex-shrink: 0;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n gap: 0.9rem;\r\n padding: 0.35rem 1rem 1.1rem;\r\n /* The containing block for `.cv-keypad-popover` \u2014 see that rule's own comment for why it is\r\n anchored here rather than to the trigger button. */\r\n position: relative;\r\n}\r\n/* FOUR controls, sized so they never need to shrink to fit. This row held five (Dispositivos too) and\r\n the only way to fit five in the CRM's 310px host was a 40px button with a 9px label and ~6px gaps \u2014\r\n below the 44/48px a thumb needs, and below legible for a word read mid-call. The default 48px size\r\n below was never actually seen: it only applied above a 360px host, which no real embed is.\r\n Dispositivos is the `.cv-call-chips` chip up by the call info instead (the same entry point the\r\n ringing and dialing screens already use). 4 \xD7 52 + 3 \xD7 20 = 268px, inside 310 - 2 \xD7 16 padding. */\r\n.cv-controls-row {\r\n display: flex;\r\n justify-content: center;\r\n gap: 1.25rem;\r\n}\r\n.cv-controls-primary {\r\n display: flex;\r\n justify-content: center;\r\n gap: 2.75rem;\r\n}\r\n/* Button + its word. The column keeps the label from widening the hit target. */\r\n.cv-ctrl-item {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n gap: 0.3rem;\r\n}\r\n/* 10px, muted: present when you look for it, silent when you don't. An icon-only\r\n control is a quiz, and mid-call is the worst time to sit one. */\r\n.cv-ctrl-label {\r\n font-size: 0.625rem;\r\n line-height: 1;\r\n color: var(--cv-muted);\r\n white-space: nowrap;\r\n user-select: none;\r\n}\r\n/* The four secondaries' words get a notch more than Colgar / Rechazar / Contestar's 10px \u2014 they are\r\n the ones read mid-call, and the five-up row used to shrink them to 9px (8px in a sidebar). */\r\n.cv-controls-row .cv-ctrl-label { font-size: 0.6875rem; }\r\n.cv-ctrl {\r\n width: 52px;\r\n height: 52px;\r\n border-radius: 999px;\r\n border: 1px solid var(--cv-border-strong);\r\n background: var(--cv-bg);\r\n color: var(--cv-text);\r\n cursor: pointer;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n transition: background 0.15s ease, color 0.15s ease, transform 0.06s ease;\r\n}\r\n.cv-ctrl:hover { background: var(--cv-surface); }\r\n.cv-ctrl:active { transform: scale(0.94); }\r\n.cv-ctrl:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n pointer-events: none;\r\n}\r\n\r\n/* The ON states say what they MEAN, in the widget's own tokens: muted is danger,\r\n held is hold, transfer-open is accent. Colour is the state, not decoration. */\r\n.cv-ctrl[data-tone='danger'] {\r\n background: var(--cv-danger);\r\n border-color: var(--cv-danger);\r\n color: #fff;\r\n}\r\n.cv-ctrl[data-tone='hold'] {\r\n background: var(--cv-hold);\r\n border-color: var(--cv-hold);\r\n color: #fff;\r\n}\r\n.cv-ctrl[data-tone='accent'] {\r\n background: var(--cv-accent-soft);\r\n border-color: transparent;\r\n color: var(--cv-accent-strong);\r\n}\r\n.cv-ctrl[data-tone]:hover { filter: brightness(0.95); }\r\n\r\n/* Muted is the one state worth animating: it is invisible, it is easy to forget,\r\n and the cost of forgetting is talking to nobody. Same halo the avatar rings\r\n with \u2014 one language, not two. */\r\n.cv-ctrl[data-tone='danger'] {\r\n position: relative;\r\n}\r\n.cv-ctrl[data-tone='danger']::after {\r\n content: '';\r\n position: absolute;\r\n inset: 0;\r\n border-radius: 999px;\r\n background: var(--cv-danger);\r\n animation: cv-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite;\r\n pointer-events: none;\r\n z-index: -1;\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-ctrl[data-tone='danger']::after { animation: none; opacity: 0; }\r\n}\r\n.cv-ctrl[aria-pressed='true'] {\r\n background: var(--cv-text);\r\n color: var(--cv-bg);\r\n border-color: var(--cv-text);\r\n}\r\n\r\n/*\r\n Sidebar-narrow (this file's own \u2264300px tier, up in \"Narrow hosts\"). The four 52px controls with\r\n 20px gaps need 268px, which a 260px host cannot give \u2014 so this tier steps down to 44px with 12px\r\n gaps (4 \xD7 44 + 3 \xD7 12 = 212px) and reclaims `.cv-controls`'s own horizontal padding, the first\r\n lever that block's own comment names. 44px is still the Apple HIG minimum tap target; the five-up\r\n row this replaced went to 36px here.\r\n\r\n Placed AFTER `.cv-controls`/`.cv-controls-row` above rather than up with this file's other\r\n `@container` blocks near the top: `gap` and `padding-left`/`-right` here have the same specificity as\r\n those base rules' own shorthand, so \u2014 unlike a compound selector such as `.cv-controls-row .cv-ctrl`,\r\n which wins on specificity regardless of position \u2014 source order is what decides, and a narrower-width\r\n override declared BEFORE the base rule it means to narrow loses to it. Confirmed live: moved up into\r\n the \"Narrow hosts\" section, the `.cv-ctrl` width and label font-size reductions took effect (compound\r\n selectors) but `gap` and `.cv-controls`'s padding silently did not (plain selectors, later base rule won).\r\n*/\r\n@container cv (max-width: 300px) {\r\n .cv-controls { padding-left: 0.5rem; padding-right: 0.5rem; }\r\n .cv-controls-row { gap: 0.75rem; }\r\n .cv-controls-row .cv-ctrl { width: 44px; height: 44px; }\r\n .cv-controls-row .cv-ctrl-label { font-size: 0.625rem; }\r\n}\r\n\r\n/*\r\n The attended-transfer target-entry panel (`docs/specs/transfer-availability.md` \xA75): an optional\r\n outcome line, then a row of whatever's actionable (input+button today). Once a consult exists,\r\n `ConsultScreen` takes over the whole screen instead \u2014 this panel is ONLY ever the pre-consult /\r\n retry-after-failure state now; it used to also cover the consulting/answered phases, which is why\r\n its rule is a column container built for more than one child even though there's only ever one row\r\n here today. (History: was two near-duplicate rules \u2014 `.cv-transfer` as a bare ROW,\r\n `.cv-transfer-consult` as a column \u2014 until the row version turned out to be a real,\r\n reproduced-in-browser bug: the optional `.cv-dial-outcome` message rendered as a THIRD item in the\r\n same horizontal row as the input and button, squeezing the input to its content-minimum. Merged\r\n rather than patched.)\r\n*/\r\n.cv-transfer {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.3rem;\r\n width: 100%;\r\n padding: 0.2rem 0.25rem 0;\r\n /* `.cv-call` centres and centre-aligns its normal (avatar) content \u2014 same reasoning\r\n `.cv-call-devices-inline` already opts out of for the OTHER inline swap in this box (its own\r\n comment): rows read left-aligned everywhere else in the widget, and a centred context line\r\n sitting directly above a left-aligned input/toggle/list read as two layouts stacked on top of\r\n each other, found live (2026-09-29). */\r\n text-align: left;\r\n /* Found live (2026-09-29): `.cv-call`'s avatar/name/timer block used to sit ABOVE this,\r\n unconditionally, so this panel only ever got whatever was left over \u2014 see the JSX call site's own\r\n comment. Now that this IS the whole of `.cv-call`'s content while a transfer is in progress\r\n (a swap, not an addition), it should CLAIM `.cv-call`'s available height, not just sit in it \u2014\r\n `flex: 1` does that; `min-height: 0` is what lets a flex child actually shrink to fit instead of\r\n forcing its parent to grow past it (the same reason `.cv-call`/`.cv-view`/`.cv-body` all set it,\r\n see their own comments). This is what lets `.cv-presence-list` below flex-grow into real room\r\n instead of needing its own hand-tuned `max-height` guess. */\r\n flex: 1;\r\n min-height: 0;\r\n /* Same entrance `.cv-call-devices-inline` already uses for the analogous inline swap (its own\r\n comment) \u2014 reused, not a second animation invented for this widget's second inline-swap view. */\r\n animation: cv-call-devices-in 0.18s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-transfer { animation: none; }\r\n}\r\n/* The compact \"who am I still on the phone with\" line while the picker is open \u2014 reuses\r\n `.cv-call-devices-inline`'s own two classes verbatim (`.cv-call-devices-title` for the name,\r\n `.cv-call-devices-context` for the number/timer line) rather than inventing a second \"quiet header\"\r\n style for the same widget; only the tiny stacking gap between them is new.\r\n\r\n Found live (2026-09-29): `.cv-transfer`'s own `gap` alone gave every section \u2014 context, input row,\r\n mode toggle, results list \u2014 the exact same spacing, so nothing read as its own group; the eye had\r\n no cue for where \"who I'm on the phone with\" ends and \"pick a transfer target\" begins. This extra\r\n `margin-bottom`, on top of the base `gap`, is that cue \u2014 the toggle stays close to the list it\r\n controls (unchanged), only the boundary above the interactive controls opened up. */\r\n.cv-transfer-context {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n flex-shrink: 0;\r\n margin-bottom: 0.35rem;\r\n}\r\n/* Plan T3: the mode toggle reuses `.cv-segmented` (HistoryTab's own filter control) as-is, but its\r\n `margin-bottom` (needed THERE, where the surrounding layout has no gap of its own) is redundant\r\n here \u2014 `.cv-transfer`'s own `gap` above already spaces every child, so the two would stack. Scoped\r\n to this context only; HistoryTab's own copy is untouched. */\r\n.cv-transfer .cv-segmented { margin-bottom: 0; flex-shrink: 0; }\r\n.cv-transfer .cv-presence-legend { flex-shrink: 0; }\r\n/* Same treatment as `.cv-call-status` (the avatar's own \"Llamando\u2026\"/\"En espera\" line) \u2014 one muted\r\n status-line vocabulary for the whole call screen, not a second one invented for this panel. */\r\n.cv-transfer-status {\r\n margin: 0;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n letter-spacing: 0.04em;\r\n}\r\n/* The target-entry/retry row: an input plus one button. */\r\n.cv-transfer-row {\r\n display: flex;\r\n gap: 0.5rem;\r\n}\r\n/*\r\n Flex items default to `min-width: auto`, which floors them at their CONTENT size rather than letting\r\n them shrink or grow to fill the row \u2014 `.cv-search`'s own `width: 100%` never actually took effect\r\n here, because `.cv-btn`'s text (\"Llamar\") set an unshrinkable floor and the input, having no\r\n `flex-grow`, never reclaimed the space left over. `flex: 1` + `min-width: 0` is what actually makes\r\n an input-plus-button row behave like one; `flex: none` keeps the button at its natural size.\r\n*/\r\n.cv-transfer-row .cv-search {\r\n flex: 1;\r\n min-width: 0;\r\n}\r\n/* `max-width` + ellipsis: the label now includes whatever was typed (\"Llamar a {typed}\", spec \xA74.5)\r\n and `truncateForButton` already bounds that in JS \u2014 this is the CSS backstop, so a value that\r\n slips past it (a very wide character set, a future copy change) still can't push `.cv-search` down\r\n to an unusable sliver the way the pre-truncation button did before this was noticed. */\r\n.cv-transfer-row .cv-btn {\r\n flex: none;\r\n max-width: 60%;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n/* The consult-answered row: one or two buttons, no input. */\r\n.cv-transfer-actions {\r\n display: flex;\r\n gap: 0.5rem;\r\n}\r\n.cv-transfer-actions .cv-btn {\r\n flex: 1;\r\n}\r\n/* The ringing phase has only \"Cancelar\" \u2014 a secondary, low-stakes action. Letting the row's own\r\n `flex: 1` stretch a LONE button edge to edge reads as oversized for what it is, and nothing else in\r\n this widget sizes a single button to the full row; every other `.cv-btn` sizes to its content. */\r\n.cv-transfer-actions .cv-btn:only-child {\r\n flex: none;\r\n}\r\n\r\n/*\r\n The agent picker (spec \xA74.5, plan T7) \u2014 an inline, always-visible filtered list under the\r\n input+button row, not a click-to-open popover like `.cv-person-filter-pop`: the whole point is that\r\n typing already filters it, so hiding it behind a second interaction would undo that.\r\n\r\n Redesigned live (2026-09-29): T7 (72px) and T3 (30px) both hand-tuned a `max-height` in PIXELS\r\n against whatever else happened to be on screen at the time, and both were broken by the very next\r\n addition above the list (T3's own toggle broke T7's number the same way this would keep breaking on\r\n the next change) \u2014 down to \"barely proves the list is scrollable\" at 30px, which is what the user\r\n was actually seeing: \"users cannot almost see any of the listed users.\" The real fix wasn't a bigger\r\n magic number, it was `.cv-transfer`'s own move to `flex: 1` (see its comment) freeing real height\r\n from the block that used to sit above this whole panel \u2014 `.cv-presence` and this list now flex-grow\r\n into whatever's ACTUALLY left after the context line, input row, toggle and legend take their own\r\n natural size, on whatever real height the host panel gives `.cv-call` (this SDK's fixed ~310\xD7520\r\n embed today, but also whatever a different host gives it tomorrow \u2014 no more re-tuning a constant\r\n every time something above the list changes). `.cv-call`'s own `overflow-y: auto` (its own comment)\r\n is still the last-resort net if content ever genuinely doesn't fit; this just means that net should\r\n now rarely, if ever, have to catch anything for a normal agent list.\r\n*/\r\n.cv-presence {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.35rem;\r\n flex: 1;\r\n min-height: 0;\r\n}\r\n.cv-presence-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n flex: 1;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 0.25rem;\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius-key);\r\n background: var(--cv-surface);\r\n}\r\n.cv-presence-empty {\r\n margin: 0;\r\n padding: 0.5rem;\r\n text-align: center;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n}\r\n.cv-presence-item {\r\n width: 100%;\r\n display: flex;\r\n align-items: center;\r\n gap: 0.5rem;\r\n padding: 0.4rem 0.5rem;\r\n border: none;\r\n border-radius: calc(var(--cv-radius) - 4px);\r\n background: none;\r\n color: var(--cv-text);\r\n font: 400 0.8rem var(--cv-font);\r\n text-align: left;\r\n cursor: pointer;\r\n}\r\n.cv-presence-item:hover:not(:disabled) { background: var(--cv-surface-2); }\r\n/* En llamada rows are a hard block, not just a hint (CallScreen.tsx's own comment on the row has why)\r\n \u2014 same `:disabled` treatment `.cv-btn`/`.cv-tab` already use elsewhere in this file, not a new pattern. */\r\n.cv-presence-item:disabled { opacity: 0.45; cursor: not-allowed; }\r\n.cv-presence-dot {\r\n flex-shrink: 0;\r\n width: 8px;\r\n height: 8px;\r\n border-radius: 999px;\r\n}\r\n/* Reuses existing intent tokens rather than inventing new ones \u2014 `--cv-accent` already means\r\n \"good, reachable\" (the answer button), `--cv-hold` already means \"occupied elsewhere\" (the hold\r\n toggle), and `--cv-faint` is this file's own DECORATIVE-only tier (see the `--cv-*` legend near the\r\n top) \u2014 exactly what \"no signal for this person\" is, no signal being the whole point of that tier. */\r\n.cv-presence-dot[data-state='disponible'] { background: var(--cv-accent); }\r\n.cv-presence-dot[data-state='en_llamada'] { background: var(--cv-hold); }\r\n.cv-presence-dot[data-state='desconectado'] { background: var(--cv-faint); }\r\n.cv-presence-name {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n.cv-presence-meta {\r\n flex-shrink: 0;\r\n font-size: 0.72rem;\r\n color: var(--cv-muted);\r\n}\r\n/* Spec \xA74.6: Desconectado is a HINT, never \"can't transfer\" \u2014 this is what tells the agent so, right\r\n under the list it describes. `--cv-muted`, NOT `--cv-faint`: this text is informational (the reader\r\n needs it to not misread the state), and `--cv-faint` is reserved for decorative-only content that\r\n fails AA on purpose \u2014 using it here would repeat the exact tier mistake this file's own `--cv-*`\r\n comment block warns against. */\r\n.cv-presence-legend {\r\n margin: 0;\r\n font-size: 0.7rem;\r\n color: var(--cv-muted);\r\n line-height: 1.35;\r\n}\r\n\r\n/*\r\n `ConsultScreen`'s \"someone is still waiting\" banner (`docs/specs/transfer-availability.md` \xA75, the\r\n cross-talk fix) \u2014 full-width and tappable, not a passive status line, because the entire reason this\r\n screen exists is to make \"go back to the caller\" impossible to miss. Neutral surface + the same\r\n `--cv-hold` colour `.cv-badge-hold` already uses for the same fact elsewhere, not a new soft-fill\r\n token: a text/icon tint on a plain surface is already proven legible here, and inventing an\r\n unmeasured background-tint pairing risks a real contrast regression this file takes seriously\r\n elsewhere (see the `-soft`/`-strong` split's own comments).\r\n*/\r\n.cv-consult-hold-banner {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.4rem;\r\n width: 100%;\r\n padding: 0.5rem 0.75rem;\r\n border-radius: 999px;\r\n border: 1px solid var(--cv-border);\r\n background: var(--cv-surface);\r\n color: var(--cv-hold);\r\n font: 600 0.78rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s ease, border-color 0.15s ease;\r\n}\r\n.cv-consult-hold-banner:hover {\r\n background: var(--cv-surface-2);\r\n border-color: var(--cv-hold);\r\n}\r\n.cv-consult-hold-banner:active {\r\n transform: scale(0.98);\r\n}\r\n\r\n@keyframes cv-pop-in-up {\r\n from { opacity: 0; transform: translateY(4px) scale(0.97); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n\r\n/*\r\n The in-call DTMF keypad \u2014 the one floating popover left on the call screen. (Dispositivos was the\r\n other; it is inline content now, `.cv-call-devices-inline` above, in every call state.)\r\n\r\n Opens UPWARD (`bottom: 100%`): the trigger sits at the bottom of the call screen, and there is no\r\n room to open downward. Anchored to `.cv-controls`'s own horizontal padding \u2014 a container whose width\r\n is bounded by ITS parent, never the narrow trigger itself \u2014 the same fix CountryPicker's own popover\r\n needed (see its comment): anchoring to a narrow, off-centre trigger let the popover's edge reach\r\n `.cv-softphone`'s `overflow: hidden` and get clipped.\r\n*/\r\n.cv-keypad-popover {\r\n position: absolute;\r\n z-index: 20;\r\n display: grid;\r\n grid-template-columns: repeat(3, 1fr);\r\n gap: 8px;\r\n padding: 0.85rem 1rem;\r\n background: var(--cv-surface);\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius);\r\n box-shadow: var(--cv-shadow);\r\n}\r\n.cv-keypad-popover--above {\r\n bottom: 100%;\r\n left: 1rem;\r\n right: 1rem;\r\n margin-bottom: 0.6rem;\r\n transform-origin: bottom center;\r\n animation: cv-pop-in-up 0.14s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n/* `.cv-key` already carries the full key look (border, hover, active-scale, digit/letter type) from\r\n the pre-call dialer \u2014 only the size needs setting here, since this grid has no flex ancestor to\r\n stretch the rows the way `.cv-keypad` gets to.\r\n\r\n A fixed HEIGHT, not an `aspect-ratio`. This popover opens upward from `.cv-controls`, so it has\r\n (host height - controls block - margin) to work with, and a ratio ties its height to the host's\r\n WIDTH, which says nothing about how much height there is. Found live: the CRM's real body is 310\xD7473\r\n (a 520px panel minus its 45px header), and a 1.3 ratio made this popover 285px \u2014 with the 184px\r\n controls block that is 479px, so its top ~7px were clipped by `.cv-softphone`'s `overflow: hidden`.\r\n 48px keys give a 243px popover at ANY width, fit that host with room to spare, and stay at the\r\n 44-48px minimum tap target (the old ratio gave 45px keys at a 260px host, 58px at 310). */\r\n.cv-keypad-popover .cv-key {\r\n height: 48px;\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-keypad-popover--above { animation: none; }\r\n}\r\n\r\n/* \u2500\u2500 Lists (contacts / history) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-list-head { flex-shrink: 0; padding: 0.85rem 1rem 0.5rem; }\r\n.cv-search {\r\n width: 100%;\r\n padding: 0.5rem 0.75rem;\r\n border-radius: 10px;\r\n border: 1px solid var(--cv-border-strong);\r\n background: var(--cv-surface);\r\n color: var(--cv-text);\r\n font: inherit;\r\n transition: border-color 0.15s ease, background 0.15s ease;\r\n /* Found live (2026-09-29): in the transfer row specifically, `.cv-btn`'s own `max-width: 60%`\r\n (needed for the dynamic \"Llamar a {typed}\" label) can leave the input narrower than its\r\n placeholder text \u2014 an `<input>` has no native ellipsis, so it hard-clipped mid-word\r\n (\"...o n\xFAm\") instead of trailing off cleanly. A real, if rare, risk for ANY `.cv-search` usage\r\n wherever it's squeezed, not just this one row \u2014 the fix belongs on the base rule. */\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n.cv-search::placeholder { color: var(--cv-muted); }\r\n.cv-search:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }\r\n\r\n/*\r\n A wrapper around `.cv-search`, not a change to it \u2014 that class is also the transfer-target input\r\n in CallScreen, which wants neither an icon nor a clear button. `.cv-search--icon` is an ADDITIVE\r\n modifier on the input itself (padding only), so the base class is byte-for-byte what it was for\r\n every other consumer.\r\n*/\r\n.cv-search-field { position: relative; display: flex; align-items: center; }\r\n.cv-search--icon { padding-left: 2.15rem; padding-right: 1.85rem; }\r\n.cv-search-icon {\r\n position: absolute;\r\n left: 0.75rem;\r\n color: var(--cv-muted);\r\n pointer-events: none;\r\n}\r\n.cv-search-clear {\r\n position: absolute;\r\n right: 0.5rem;\r\n width: 22px;\r\n height: 22px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n border-radius: 999px;\r\n background: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n transition: color 0.12s ease, transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-search-clear:hover { color: var(--cv-text); }\r\n.cv-search-clear:active { transform: scale(0.88); }\r\n\r\n/* \u2500\u2500 Historial filters: status (Todas/Perdidas) + person \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-segmented {\r\n display: flex;\r\n gap: 2px;\r\n padding: 2px;\r\n border-radius: 10px;\r\n background: var(--cv-surface-2);\r\n margin-bottom: 0.4rem;\r\n}\r\n.cv-segmented-btn {\r\n flex: 1;\r\n padding: 0.35rem 0;\r\n border: none;\r\n border-radius: 8px;\r\n background: none;\r\n color: var(--cv-muted);\r\n font: 600 0.75rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;\r\n}\r\n/* The active segment is stated by its own fill + shadow, not colour alone \u2014 the same reasoning\r\n `.cv-item-action[data-tone='accent']`'s own comment gives for resting-state colour: a state that\r\n only differs by hue must still read on a screen that renders neither. */\r\n.cv-segmented-btn[aria-selected='true'] {\r\n background: var(--cv-bg);\r\n color: var(--cv-text);\r\n box-shadow: var(--cv-shadow);\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-segmented-btn { transition: none; }\r\n}\r\n\r\n/* The person filter \u2014 same combobox SHAPE as `.cv-cp-*` (CountryPicker): a chip opening a positioned\r\n popover list with a checkmark on the active row. Not the same CLASSES: the two pickers have nothing\r\n to do with each other and sharing a namespace would make a future change to one silently reach the\r\n other. */\r\n.cv-person-filter { position: relative; width: 100%; }\r\n.cv-person-filter-chip {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.4rem;\r\n width: 100%;\r\n padding: 0.4rem 0.65rem;\r\n border: 1px solid var(--cv-border-strong);\r\n border-radius: 10px;\r\n background: var(--cv-surface);\r\n color: var(--cv-muted);\r\n font: 500 0.78rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s ease, border-color 0.15s ease;\r\n}\r\n.cv-person-filter-chip:hover { background: var(--cv-surface-2); }\r\n/*\r\n Border only, no fill \u2014 the popover opening and the caret rotating already say \"this is open\";\r\n painting the whole width of a wide text pill solid green on top of that was the same over-strong\r\n green the selected row had (see its own comment below). A coloured border is the same restrained\r\n signal `.cv-search:focus` already uses for \"active\" elsewhere in this file.\r\n*/\r\n.cv-person-filter-chip[aria-expanded='true'] {\r\n border-color: var(--cv-accent);\r\n}\r\n.cv-person-filter-label {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n text-align: left;\r\n color: var(--cv-text);\r\n}\r\n.cv-person-filter-caret {\r\n flex-shrink: 0;\r\n transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n.cv-person-filter-caret[data-open='true'] { transform: rotate(180deg); }\r\n.cv-person-filter-pop {\r\n position: absolute;\r\n top: calc(100% + 6px);\r\n left: 0;\r\n right: 0;\r\n z-index: 20;\r\n max-height: 220px;\r\n overflow-y: auto;\r\n padding: 0.25rem;\r\n background: var(--cv-surface);\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius);\r\n box-shadow: var(--cv-shadow);\r\n transform-origin: top center;\r\n animation: cv-cp-in 0.14s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-person-filter-pop { animation: none; }\r\n .cv-person-filter-caret { transition: none; }\r\n}\r\n.cv-person-filter-item {\r\n width: 100%;\r\n display: flex;\r\n align-items: center;\r\n gap: 0.5rem;\r\n padding: 0.45rem 0.5rem;\r\n border: none;\r\n border-radius: calc(var(--cv-radius) - 4px);\r\n background: none;\r\n color: var(--cv-text);\r\n font: 400 0.8rem var(--cv-font);\r\n text-align: left;\r\n cursor: pointer;\r\n}\r\n.cv-person-filter-item:hover { background: var(--cv-surface-2); }\r\n/*\r\n NOT a solid `--cv-accent-soft` fill \u2014 that read as a wall of saturated green across a wide row,\r\n much louder than the legacy CRM's own treatment for this exact list (`bg-emerald-50` \u2014 a barely-\r\n there wash \u2014 plus coloured text, never a strong fill). Coloured, bold text + the checkmark carry\r\n \"selected\" instead; `--cv-accent-strong` is already verified elsewhere in this file as AA against\r\n `--cv-surface`, which is what this popover sits on.\r\n*/\r\n.cv-person-filter-item[aria-selected='true'] .cv-person-filter-name {\r\n font-weight: 600;\r\n color: var(--cv-accent-strong);\r\n}\r\n.cv-person-filter-name {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n.cv-person-filter-check { color: var(--cv-accent-strong); flex-shrink: 0; }\r\n.cv-person-filter-check[data-on='false'] { opacity: 0; }\r\n\r\n.cv-list {\r\n flex: 1;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 0 0.5rem 0.5rem;\r\n}\r\n/*\r\n Historial's day group header. Sticky within `.cv-list` (its nearest scrolling ancestor \u2014 a plain\r\n wrapper div between the two does not break that), OPAQUE `--cv-bg` rather than a translucent\r\n `/85 + backdrop-blur` treatment: it still needs to fully occlude rows scrolling underneath it, and\r\n a flat match to the list's own background does that with no new CSS feature this file doesn't\r\n already use elsewhere.\r\n*/\r\n.cv-history-day {\r\n position: sticky;\r\n top: 0;\r\n z-index: 1;\r\n padding: 0.5rem 0.6rem 0.35rem;\r\n background: var(--cv-bg);\r\n color: var(--cv-muted);\r\n font-size: 0.68rem;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.04em;\r\n}\r\n.cv-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.7rem;\r\n padding: 0.5rem;\r\n border-radius: 12px;\r\n /*\r\n NOT `cursor: pointer` \u2014 this div has no click handler of its own; only its children\r\n (`.cv-item-main`, `.cv-item-action`) do, and each already sets its own pointer cursor. Putting\r\n it here too used to make the WHOLE row's box promise a click \u2014 its own padding, and the flex\r\n `gap` between avatar/name/button \u2014 none of which does anything when clicked. Most noticeable\r\n right where one row's padding meets the next, which reads as \"there's something clickable\r\n between these two contacts\" when there is not.\r\n */\r\n transition: background 0.12s ease;\r\n text-align: left;\r\n width: 100%;\r\n border: none;\r\n background: none;\r\n font: inherit;\r\n color: inherit;\r\n}\r\n.cv-item:hover { background: var(--cv-surface); }\r\n.cv-item-avatar {\r\n width: 40px;\r\n height: 40px;\r\n border-radius: 999px;\r\n /*\r\n RADIAL, not diagonal \u2014 deliberately. The initials sit centered, so a radial gradient puts them\r\n at its lightest point (pure `--cv-surface`) no matter what, while the edges still darken toward\r\n `--cv-surface-2` for the same cheap depth a diagonal gradient gave. A diagonal version measured\r\n 4.39:1 for `--cv-muted` text at its far corner \u2014 under the 4.5:1 text needs \u2014 because centered\r\n text's exact position against a DIAGONAL gradient depends on glyph width, not just circle size.\r\n Centering the gradient on the centered text removes that dependency entirely, structurally,\r\n rather than nudging a token and hoping the margin holds. Both stops stay tokens, so this is\r\n still theme-correct for free \u2014 a hardcoded gray gradient would not adapt to dark mode.\r\n */\r\n background: radial-gradient(circle, var(--cv-surface), var(--cv-surface-2));\r\n /*\r\n Without this, the circle's own boundary can vanish: `.cv-item:hover` sets the ROW background to\r\n `--cv-surface` \u2014 the exact color the gradient above STARTS at (1.00 contrast ratio, i.e.\r\n identical) in both themes. `--cv-border-strong` is the same token `.cv-search`'s border already\r\n uses against this same surface a few rules up \u2014 a pairing already proven visible, not a new one.\r\n */\r\n border: 1px solid var(--cv-border-strong);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--cv-muted);\r\n font-size: 0.78rem;\r\n font-weight: 700;\r\n flex-shrink: 0;\r\n}\r\n/*\r\n The Historial row's status chip \u2014 WHY a call needs colour at all: direction is already the icon's\r\n shape (an arrow in/out), but \"did this connect\" is not shape-able, and a flat grey circle made every\r\n row look identical at a glance. Small badge, not a row-wide fill (that mistake already got made once\r\n on the person filter \u2014 see its own comment): a 40px circle reads as a status glyph, the same way a\r\n colored icon-badge does anywhere else; a wide fill reads as decoration competing with the text next\r\n to it. `--danger-strong`/`--accent-strong` (not the plain base tokens) because the base tokens\r\n measure BELOW 3:1 non-text contrast against their own `-soft` fill \u2014 `--cv-danger` on\r\n `--cv-danger-soft` is 2.6:1; `--cv-danger-strong` is 4.47:1 light / 3.62:1 dark, measured.\r\n*/\r\n.cv-item-avatar[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }\r\n.cv-item-avatar[data-tone='info'] { background: var(--cv-info-soft); color: var(--cv-info-strong); }\r\n.cv-item-avatar[data-tone='danger'] { background: var(--cv-danger-soft); color: var(--cv-danger-strong); }\r\n/* A button, reset to look like plain content \u2014 the row is a container, not a control. */\r\n.cv-item-main {\r\n flex: 1;\r\n min-width: 0;\r\n display: block;\r\n border: none;\r\n background: none;\r\n padding: 0;\r\n font: inherit;\r\n color: inherit;\r\n text-align: left;\r\n cursor: pointer;\r\n transition: transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-item-main:disabled { cursor: default; }\r\n.cv-item-main:not(:disabled):active { transform: scale(0.985); }\r\n.cv-item-name {\r\n display: block;\r\n font-weight: 600;\r\n font-size: 0.875rem;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n/*\r\n 14px bold does not clear WCAG's large-text bar (18.66px bold) \u2014 this text needs the full 4.5:1, not\r\n the 3:1 large-text minimum. `--cv-danger-strong`: 6.47:1 light / 6.46:1 dark against `--cv-bg`,\r\n measured; the plain `--cv-danger` this used to be was only 3.76:1 light \u2014 a real (if marginal) fail.\r\n*/\r\n.cv-item-name[data-tone='danger'] { color: var(--cv-danger-strong); }\r\n.cv-item-sub {\r\n display: block;\r\n font-size: 0.72rem;\r\n color: var(--cv-muted);\r\n margin-top: 1px;\r\n font-variant-numeric: tabular-nums;\r\n}\r\n.cv-item-sub-outcome { color: var(--cv-danger-strong); }\r\n/*\r\n Decorative reinforcement, not the sole carrier \u2014 the row's own play button is what says \"this call\r\n has a recording\" (it only renders when one exists); this is a small hint IN the metadata line that\r\n the button is worth pressing, so `--cv-faint` (2.5:1, documented above as decorative-only) is the\r\n right tier here, same reasoning `.cv-empty-icon`/`.cv-gate-mark` already use for icons next to real\r\n text rather than standing alone.\r\n\r\n `display: inline-flex` is load-bearing, not decoration \u2014 every OTHER inline icon in this file gets\r\n an explicit `display` for the same reason: `/react` + `/ui` render straight into the host's light\r\n DOM (no shadow root), so a host reset like Tailwind Preflight's `svg { display: block }` reaches\r\n this element directly. Without an explicit override here, that reset knocks the icon onto its own\r\n line instead of trailing the duration text it is meant to sit beside.\r\n*/\r\n.cv-item-sub-rec {\r\n display: inline-flex;\r\n color: var(--cv-faint);\r\n margin-left: 0.3rem;\r\n vertical-align: -1px;\r\n}\r\n.cv-item-action {\r\n width: 34px;\r\n height: 34px;\r\n border-radius: 999px;\r\n border: none;\r\n background: none;\r\n color: var(--cv-muted);\r\n cursor: pointer;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n flex-shrink: 0;\r\n transition: background 0.12s ease, color 0.12s ease, transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-item-action:hover { background: var(--cv-accent); color: #fff; }\r\n.cv-item-action:active { transform: scale(0.9); }\r\n/*\r\n The call action specifically (ContactsTab) \u2014 NOT History's \"play recording\", which shares this\r\n same class and must stay neutral; a play button has no reason to be accent-colored. Scoped with\r\n `data-tone`, the same modifier convention `.cv-ctrl[data-tone]` already uses elsewhere in this file.\r\n\r\n Coloured at REST, not just on hover: a hover-only reveal never fires on a touchscreen, so the one\r\n affordance that most needs to read as \"you can tap this\" would be invisible on exactly the devices\r\n a phone widget is likeliest to run on. `accent-strong` is already verified elsewhere in this file\r\n as AA on a surface; `accent-soft` is the same hover tint `.cv-ctrl[data-tone='accent']` uses, just\r\n transparent at rest so fifty rows of solid fill never turns the list into a wall of green.\r\n*/\r\n.cv-item-action[data-tone='accent'] { color: var(--cv-accent-strong); }\r\n.cv-item-action[data-tone='accent']:hover { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-item-main, .cv-item-action, .cv-search-clear { transition: none; }\r\n}\r\n\r\n.cv-empty {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n height: 100%;\r\n color: var(--cv-muted);\r\n text-align: center;\r\n padding: 2.5rem 1.25rem;\r\n font-size: 0.82rem;\r\n}\r\n.cv-empty-icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 48px;\r\n height: 48px;\r\n border-radius: 999px;\r\n background: var(--cv-surface);\r\n color: var(--cv-faint);\r\n margin-bottom: 0.75rem;\r\n}\r\n.cv-empty-title { margin: 0; font-size: 0.85rem; font-weight: 600; color: var(--cv-muted); }\r\n/*\r\n `--cv-muted`, NOT `--cv-faint` \u2014 this file documents faint as \"decorative only... never\r\n information\" (2.5:1, below AA), and this line IS information: it is the reason the list is\r\n empty, sometimes echoing the user's own search back to them. `--cv-faint` here measured 2.54:1\r\n light / 3.70:1 dark against a 4.5:1 bar \u2014 a real failure, not a rounding difference.\r\n*/\r\n.cv-empty-sub { margin: 0.2rem 0 0; font-size: 0.75rem; color: var(--cv-muted); }\r\n.cv-empty-spinner {\r\n margin-bottom: 0.75rem;\r\n animation: cv-spin 0.8s linear infinite;\r\n /* A loading spinner's rotation IS the information, with no static equivalent \u2014 unlike this\r\n file's other motion, it stays exempt from reduced-motion rather than freezing into a shape\r\n that would just look stuck. Colour comes from `.cv-empty`'s own `color: var(--cv-muted)` \u2014\r\n the spinner no longer sets its own, now that it sits next to real text instead of standing\r\n alone: the icon is decorative reinforcement, same as `.cv-empty-icon`, not the sole carrier. */\r\n}\r\n@keyframes cv-spin {\r\n to { transform: rotate(360deg); }\r\n}\r\n\r\n/* The scroll-triggered \"load more\" marker \u2014 see `useInfiniteList`. Never visible itself; a small\r\n height so it has a real box for the `IntersectionObserver` to track. */\r\n.cv-list-sentinel {\r\n height: 1px;\r\n}\r\n.cv-list-loading-more {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 0.75rem 0 0.25rem;\r\n color: var(--cv-muted);\r\n}\r\n/* Exempt from reduced-motion for the same reason `.cv-empty-spinner` is \u2014 see its own comment. */\r\n.cv-list-loading-more-spinner {\r\n animation: cv-spin 0.8s linear infinite;\r\n}\r\n\r\n/* \u2500\u2500 Contact detail (overlays the list) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-detail {\r\n position: absolute;\r\n inset: 0;\r\n /* Above the list's own sticky day-group header (`.cv-history-day`, z-index: 1) \u2014 otherwise\r\n that header's z-index wins the stacking fight wherever its unscrolled flow position happens\r\n to land on top of this overlay, and it paints through despite both having an opaque bg. */\r\n z-index: 2;\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--cv-bg);\r\n animation: cv-detail-in 220ms var(--cv-ease-out) both;\r\n}\r\n@keyframes cv-detail-in {\r\n from { opacity: 0; transform: translateX(10px); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-detail { animation: none; }\r\n}\r\n\r\n.cv-detail-head { flex-shrink: 0; padding: 0.6rem 0.5rem 0; }\r\n.cv-detail-back {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n padding: 0.4rem 0.5rem;\r\n border: none;\r\n border-radius: 8px;\r\n background: none;\r\n color: var(--cv-muted);\r\n font: 600 0.82rem var(--cv-font);\r\n cursor: pointer;\r\n transition: color 0.12s ease, transform 0.1s var(--cv-ease-out);\r\n}\r\n.cv-detail-back:hover { color: var(--cv-text); }\r\n.cv-detail-back:active { transform: scale(0.97); }\r\n\r\n.cv-detail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 1.25rem; }\r\n.cv-detail-identity {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n text-align: center;\r\n padding: 0.75rem 0 1.5rem;\r\n}\r\n.cv-detail-avatar {\r\n width: 72px;\r\n height: 72px;\r\n border-radius: 999px;\r\n /* Same radial + border treatment as `.cv-item-avatar` (see its own comment), just larger. Neutral\r\n by default \u2014 a contact has no \"status\" to colour. `HistoryDetail` overrides both with `[data-tone]`\r\n below: a call DOES have one, the same one the list row already shows, so the hero avatar staying\r\n grey there would be information the list already gave up on repeating here. */\r\n background: radial-gradient(circle, var(--cv-surface), var(--cv-surface-2));\r\n border: 1px solid var(--cv-border-strong);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--cv-muted);\r\n font-size: 1.35rem;\r\n font-weight: 700;\r\n margin-bottom: 0.85rem;\r\n}\r\n/* Same token pairs already verified for `.cv-item-avatar[data-tone]` \u2014 size doesn't change a contrast\r\n ratio, so no new numbers to measure here. */\r\n.cv-detail-avatar[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); border-color: transparent; }\r\n.cv-detail-avatar[data-tone='info'] { background: var(--cv-info-soft); color: var(--cv-info-strong); border-color: transparent; }\r\n.cv-detail-avatar[data-tone='danger'] { background: var(--cv-danger-soft); color: var(--cv-danger-strong); border-color: transparent; }\r\n.cv-detail-name {\r\n margin: 0;\r\n font-size: 1.1rem;\r\n font-weight: 700;\r\n color: var(--cv-text);\r\n letter-spacing: -0.01em;\r\n}\r\n.cv-detail-name[data-tone='danger'] { color: var(--cv-danger-strong); }\r\n.cv-detail-meta {\r\n margin: 0.2rem 0 0;\r\n font-size: 0.78rem;\r\n color: var(--cv-muted);\r\n}\r\n\r\n/* \u2500\u2500 Call detail: recording \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-detail-recording { margin-bottom: 1rem; }\r\n.cv-detail-recording-label {\r\n margin: 0 0 0.4rem;\r\n padding: 0 0.15rem;\r\n font-size: 0.68rem;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.03em;\r\n color: var(--cv-muted);\r\n}\r\n.cv-detail-audio { width: 100%; display: block; }\r\n.cv-detail-audio + .cv-detail-audio { margin-top: 0.5rem; }\r\n.cv-detail-recording-status {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.5rem;\r\n margin: 0 0 1rem;\r\n padding: 0.65rem 0.75rem;\r\n border-radius: 12px;\r\n background: var(--cv-surface);\r\n color: var(--cv-muted);\r\n font-size: 0.78rem;\r\n}\r\n/*\r\n Text colour only, same neutral `--cv-surface` background the other states use \u2014 NOT a\r\n `--cv-danger-soft` fill. Measured first, not assumed: `--cv-danger-strong` on `--cv-danger-soft`\r\n is 3.34:1 light / 3.62:1 dark, well under the 4.5:1 this 0.78rem text needs (it only ever cleared\r\n the SEPARATE 3:1 non-text bar this token was built for \u2014 an icon on that badge, not body text on\r\n it). Same lesson the person-filter fix already paid for: a badge-strength pairing that works for an\r\n icon is not automatically safe for text.\r\n*/\r\n.cv-detail-recording-status--warn { color: var(--cv-danger-strong); }\r\n.cv-detail-recording-spinner { animation: cv-spin 0.8s linear infinite; flex-shrink: 0; }\r\n\r\n/* \u2500\u2500 Call detail: who handled it \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-detail-handlers { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }\r\n.cv-detail-handler-hop { display: inline-flex; align-items: center; gap: 0.3rem; }\r\n.cv-detail-handler-arrow { color: var(--cv-faint); flex-shrink: 0; }\r\n.cv-detail-handler-chip {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n padding: 0.15rem 0.5rem;\r\n border-radius: 999px;\r\n background: var(--cv-surface-2);\r\n color: var(--cv-text);\r\n font-size: 0.75rem;\r\n font-weight: 600;\r\n}\r\n/* The LAST hop \u2014 the one the call actually ended with \u2014 is the one worth a glance first. */\r\n.cv-detail-handler-chip[data-tone='accent'] { background: var(--cv-accent-soft); color: var(--cv-accent-strong); }\r\n.cv-detail-handler-ext {\r\n font-weight: 400;\r\n font-variant-numeric: tabular-nums;\r\n opacity: 0.75;\r\n}\r\n\r\n.cv-detail-rows {\r\n border: 1px solid var(--cv-border);\r\n border-radius: 14px;\r\n overflow: hidden;\r\n background: var(--cv-bg);\r\n}\r\n.cv-detail-row {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.7rem;\r\n width: 100%;\r\n padding: 0.7rem 0.85rem;\r\n border: none;\r\n border-top: 1px solid var(--cv-border);\r\n background: none;\r\n font: inherit;\r\n color: inherit;\r\n text-align: left;\r\n}\r\n.cv-detail-row:first-child { border-top: none; }\r\n.cv-detail-row--action {\r\n cursor: pointer;\r\n transition: background 0.12s ease;\r\n}\r\n.cv-detail-row--action:hover { background: var(--cv-surface); }\r\n.cv-detail-row-icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 999px;\r\n background: var(--cv-surface-2);\r\n /*\r\n Without this the circle nearly vanishes into its own row: `--cv-surface-2` measures only ~1.1:1\r\n against the row's rest-state background and ~1.05:1 against `--cv-surface`, the color\r\n `.cv-detail-row--action:hover` sets the ROW to \u2014 both far below a readable boundary, and worse\r\n than `.cv-item-avatar`/`.cv-detail-avatar` above even manage WITH their border (1.18:1 light /\r\n 1.57:1 dark). Same token, same fix, same reason.\r\n */\r\n border: 1px solid var(--cv-border-strong);\r\n color: var(--cv-muted);\r\n flex-shrink: 0;\r\n}\r\n.cv-detail-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }\r\n.cv-detail-row-eyebrow {\r\n font-size: 0.68rem;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.03em;\r\n color: var(--cv-muted);\r\n}\r\n.cv-detail-row-value {\r\n font-size: 0.85rem;\r\n font-weight: 500;\r\n color: var(--cv-text);\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n.cv-detail-row-chevron { color: var(--cv-faint); flex-shrink: 0; }\r\n/* A second line under a row's value, for two facts that would each be cut off sharing one line (\"Llamada desde\":\r\n the name the org gave the number, then the number). */\r\n.cv-detail-row-sub {\r\n display: block;\r\n font-size: 0.75rem;\r\n font-weight: 400;\r\n color: var(--cv-muted);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n\r\n.cv-detail-foot { flex-shrink: 0; padding: 0.85rem 1.25rem 1.1rem; }\r\n.cv-detail-call { width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.4rem; }\r\n\r\n/* \u2500\u2500 Settings tab (device selection, SPEC \xA77 Phase 2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-settings-body {\r\n flex: 1;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 1rem;\r\n display: flex;\r\n flex-direction: column;\r\n /* Generous separation BETWEEN sections, deliberately larger than any gap inside one \u2014 grouping\r\n only reads as grouping if the space between groups is clearly bigger than the space within one\r\n (see impeccable's layout guidance: \"tight grouping, generous separation\"). */\r\n gap: 1.5rem;\r\n}\r\n\r\n/*\r\n One labeled group (`SettingsSection`): a title, and a bordered body holding its rows. Ajustes\r\n started as a flat list of two rows with nothing to say they were related \u2014 fine for exactly two\r\n rows, unreadable the moment an unrelated third setting lands with nothing to tell it apart from\r\n the device rows above it.\r\n\r\n The body is ONE card, not a card per row: `.cv-settings-row`/`.cv-settings-note` below carry no\r\n border or background of their own, only a divider between siblings \u2014 the same shape\r\n `.cv-detail-rows`/`.cv-detail-row` already use elsewhere in this file, and deliberately not a\r\n stack of individually-bordered cards (nested-card territory).\r\n*/\r\n.cv-settings-section { display: flex; flex-direction: column; gap: 0.5rem; }\r\n.cv-settings-section-title {\r\n margin: 0;\r\n padding: 0 0.2rem;\r\n font-size: 0.7rem;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.04em;\r\n color: var(--cv-muted);\r\n}\r\n.cv-settings-section-body {\r\n border: 1px solid var(--cv-border);\r\n border-radius: 14px;\r\n background: var(--cv-bg);\r\n overflow: hidden;\r\n}\r\n.cv-settings-section-body > * { padding: 0.75rem 0.85rem; }\r\n.cv-settings-section-body > *:not(:first-child) { border-top: 1px solid var(--cv-border); }\r\n\r\n.cv-settings-row { display: flex; align-items: center; gap: 0.7rem; }\r\n/* Same treatment as `.cv-detail-row-icon`, same reason \u2014 see its own comment on why the border is\r\n load-bearing: `--cv-surface-2` alone nearly vanishes into the section's own background. */\r\n.cv-settings-row-icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 999px;\r\n background: var(--cv-surface-2);\r\n border: 1px solid var(--cv-border-strong);\r\n color: var(--cv-muted);\r\n flex-shrink: 0;\r\n}\r\n.cv-settings-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.35rem; }\r\n/* Same pairing as `.cv-detail-row-eyebrow` (`--cv-muted` on `--cv-bg`), verified there already. */\r\n.cv-settings-row-label {\r\n font-size: 0.68rem;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.03em;\r\n color: var(--cv-muted);\r\n}\r\n.cv-settings-select {\r\n width: 100%;\r\n padding: 0.45rem 0.6rem;\r\n border-radius: 8px;\r\n border: 1px solid var(--cv-border-strong);\r\n background: var(--cv-surface);\r\n color: var(--cv-text);\r\n font: inherit;\r\n font-size: 0.85rem;\r\n transition: border-color 0.15s ease, background 0.15s ease;\r\n}\r\n.cv-settings-select:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }\r\n.cv-settings-empty { font-size: 0.85rem; color: var(--cv-muted); }\r\n\r\n/* `--cv-danger-strong` is the AS-TEXT danger token (see its definition): red-600 on the light `--cv-bg`,\r\n red-400 on the dark one. */\r\n.cv-settings-error { margin: 0.6rem 0.2rem 0; font-size: 0.78rem; line-height: 1.4; color: var(--cv-danger-strong); }\r\n/* Why the choice cannot change right now, and until when (N\xFAmero de salida). Calm, like the dialpad's notice: the\r\n phone keeps working. Above the list, so it is in sight of the row just tapped. */\r\n.cv-settings-notice {\r\n margin: 0 0 0.75rem;\r\n padding: 0.6rem 0.75rem;\r\n border-radius: 10px;\r\n background: var(--cv-surface-2);\r\n color: var(--cv-text);\r\n font-size: 0.78rem;\r\n line-height: 1.4;\r\n}\r\n.cv-settings-notice[data-kind='error'] { color: var(--cv-danger-strong); }\r\n\r\n/*\r\n Ajustes as a list (custom-ringtone.md \xA74.6, revised 2026-10-14): each row names a setting, shows its CURRENT\r\n value under the name, and opens a page. Same icon circle and dividers as the device rows above (they live in\r\n the same `.cv-settings-section-body`, which supplies the padding and the dividers), same chevron as\r\n `.cv-detail-row--action` \u2014 one row vocabulary across the widget.\r\n*/\r\n.cv-settings-nav {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.7rem;\r\n width: 100%;\r\n border: none;\r\n background: none;\r\n font: inherit;\r\n color: inherit;\r\n text-align: left;\r\n}\r\nbutton.cv-settings-nav { cursor: pointer; transition: background 0.12s ease; }\r\nbutton.cv-settings-nav:hover { background: var(--cv-surface); }\r\nbutton.cv-settings-nav:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: -2px; }\r\n.cv-settings-nav-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }\r\n.cv-settings-nav-title { font-size: 0.88rem; font-weight: 600; color: var(--cv-text); }\r\n/* One line, cut with an ellipsis \u2014 the full device name is on its page. `--cv-muted` on `--cv-bg` is the\r\n pairing already verified for `.cv-settings-note`. The static row (a note, no page) may wrap instead. */\r\n.cv-settings-nav-value {\r\n font-size: 0.8rem;\r\n color: var(--cv-muted);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n.cv-settings-nav--static .cv-settings-nav-value { white-space: normal; line-height: 1.45; }\r\n.cv-settings-nav-chevron { flex-shrink: 0; color: var(--cv-muted); }\r\n\r\n/* A settings page: the `.cv-detail` overlay (entrance, back button, reduced motion all inherited) with a\r\n scrolling body of its own. */\r\n.cv-settings-page-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0.25rem 1rem 1rem; }\r\n.cv-settings-page-title {\r\n margin: 0.15rem 0.2rem 0.35rem;\r\n font-size: 1.05rem;\r\n font-weight: 700;\r\n letter-spacing: -0.01em;\r\n color: var(--cv-text);\r\n}\r\n/* Focus lands here on open (for keyboard and screen-reader users); a ring on a heading would only look\r\n like a stray box to everyone else. */\r\n.cv-settings-page-title:focus { outline: none; }\r\n.cv-settings-page-intro { margin: 0 0.2rem 0.85rem; font-size: 0.8rem; line-height: 1.45; color: var(--cv-muted); }\r\n.cv-settings-page-empty { margin: 0; padding: 0 0.2rem; font-size: 0.85rem; color: var(--cv-muted); }\r\n\r\n/* The checkmark list: one card like `.cv-settings-section-body`, a row per choice. */\r\n.cv-choices {\r\n border: 1px solid var(--cv-border);\r\n border-radius: 14px;\r\n background: var(--cv-bg);\r\n overflow: hidden;\r\n}\r\n.cv-choice {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n gap: 0.6rem;\r\n min-height: 44px;\r\n padding: 0.6rem 0.85rem;\r\n cursor: pointer;\r\n transition: background 0.12s ease;\r\n}\r\n.cv-choice + .cv-choice { border-top: 1px solid var(--cv-border); }\r\n.cv-choice:hover { background: var(--cv-surface); }\r\n/* The real control: a native radio, kept in the accessibility tree and keyboard order, drawn by the row. */\r\n.cv-choice-input {\r\n position: absolute;\r\n opacity: 0;\r\n width: 1px;\r\n height: 1px;\r\n margin: 0;\r\n pointer-events: none;\r\n}\r\n.cv-choice:has(.cv-choice-input:focus-visible) { outline: 2px solid var(--cv-accent); outline-offset: -2px; }\r\n.cv-choice-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }\r\n.cv-choice-label { font-size: 0.88rem; color: var(--cv-text); overflow-wrap: anywhere; }\r\n.cv-choice-sub { font-size: 0.78rem; color: var(--cv-muted); }\r\n.cv-choice[data-checked] .cv-choice-label { font-weight: 600; }\r\n.cv-choice-check { flex-shrink: 0; color: var(--cv-accent-strong); visibility: hidden; }\r\n.cv-choice[data-checked] .cv-choice-check { visibility: visible; }\r\n.cv-choice-playing { display: inline-flex; flex-shrink: 0; color: var(--cv-accent-strong); }\r\n/* A tagged choice (N\xFAmero de salida): the label and its tag on one line, like the dialpad's list. */\r\n.cv-choice-line { display: flex; align-items: center; gap: 0.35rem; min-width: 0; }\r\n/* Locked while a change is in flight or must wait: the chosen row stays clear, the others step back. */\r\n.cv-choice[data-disabled] { cursor: default; }\r\n.cv-choice[data-disabled]:not([data-checked]) { opacity: 0.55; }\r\n.cv-choice[data-disabled]:hover { background: none; }\r\n.cv-choice-applying { display: inline-flex; flex-shrink: 0; color: var(--cv-muted); }\r\n/* Exempt from reduced-motion like `.cv-empty-spinner`: the rotation is the information. */\r\n.cv-choice-applying-icon { animation: cv-spin 0.8s linear infinite; }\r\n\r\n.cv-settings-test,\r\n.cv-settings-retry {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.4rem;\r\n}\r\n.cv-settings-test { margin-top: 0.85rem; }\r\n.cv-settings-retry { margin-top: 0.6rem; }\r\n\r\n/* Visually hidden, still read aloud. */\r\n.cv-sr-only {\r\n position: absolute;\r\n width: 1px;\r\n height: 1px;\r\n overflow: hidden;\r\n clip-path: inset(50%);\r\n white-space: nowrap;\r\n}\r\n/* `--cv-muted` on `--cv-bg` \u2014 4.83:1 light / 6.97:1 dark, computed fresh for THIS pairing (the note\r\n used to sit on `--cv-surface` with its own card; now it sits directly on the section's `--cv-bg`,\r\n a different background that earns its own check rather than inheriting the old one's number). */\r\n.cv-settings-note {\r\n margin: 0;\r\n color: var(--cv-muted);\r\n font-size: 0.8rem;\r\n line-height: 1.5;\r\n}\r\n\r\n/* \u2500\u2500 Centred states (mic gate) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-center {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.85rem;\r\n padding: 1.75rem;\r\n text-align: center;\r\n}\r\n.cv-center p { color: var(--cv-muted); margin: 0; font-size: 0.85rem; line-height: 1.5; }\r\n\r\n\r\n/* \"C\xF3mo permitirlo\" \u2014 a native <details> disclosure.\r\n Collapsed by default: the people who already know where the padlock is should not have to read\r\n past three steps to get there, and the people who don't get them without leaving the app. */\r\n.cv-help {\r\n width: 100%;\r\n margin-top: 0.15rem;\r\n text-align: left;\r\n}\r\n.cv-help summary {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n justify-content: center;\r\n width: 100%;\r\n padding: 0.3rem;\r\n border-radius: 8px;\r\n color: var(--cv-muted);\r\n font: 500 0.75rem var(--cv-font);\r\n cursor: pointer;\r\n list-style: none; /* hide the default triangle; the icon is the affordance */\r\n transition: color 0.15s ease, background 0.15s ease;\r\n}\r\n.cv-help summary::-webkit-details-marker { display: none; }\r\n.cv-help summary:hover { color: var(--cv-text); background: var(--cv-surface-2); }\r\n.cv-help[open] summary { color: var(--cv-text); margin-bottom: 0.35rem; }\r\n\r\n.cv-help ol {\r\n margin: 0;\r\n padding: 0.6rem 0.7rem 0.6rem 1.6rem;\r\n background: var(--cv-surface);\r\n border-radius: 10px;\r\n color: var(--cv-muted);\r\n font-size: 0.73rem;\r\n line-height: 1.55;\r\n}\r\n.cv-help li + li { margin-top: 0.3rem; }\r\n.cv-help li::marker { color: var(--cv-faint); font-variant-numeric: tabular-nums; }\r\n\r\n.cv-help a {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.25rem;\r\n margin-top: 0.45rem;\r\n padding-left: 0.1rem;\r\n color: var(--cv-accent-strong);\r\n font-size: 0.73rem;\r\n font-weight: 500;\r\n text-decoration: none;\r\n}\r\n.cv-help a:hover { text-decoration: underline; }\r\n\r\n/* The panel is the only thing that moves, and only when opened by hand \u2014 so it animates. */\r\n.cv-help[open] ol {\r\n animation: cv-gate-in 200ms var(--cv-ease-out) both;\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-help[open] ol { animation: none; }\r\n}\r\n\r\n/* \u2500\u2500 The permission gate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n Four states, and only ONE of them deserves a designed screen.\r\n\r\n `checking` resolves in ~100\u2013400 ms. Rendering it immediately means the user\r\n sees text appear and vanish inside a blink \u2014 a flash reads as jank, not as\r\n information. So it is delayed 450 ms with `both` fill: invisible during the\r\n delay, and if the check finishes first (the usual case, and always the case\r\n when permission is already granted) it is never painted at all.\r\n\r\n `prompting` is the one the user actually reads and acts on. It gets the\r\n hierarchy: mark, title, reason, action.\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n.cv-gate {\r\n flex: 1;\r\n min-height: 0;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.6rem;\r\n padding: 1.5rem 1.75rem;\r\n text-align: center;\r\n}\r\n\r\n/* The disc echoes the call avatar \u2014 one visual language, not two. */\r\n.cv-gate-mark {\r\n display: grid;\r\n place-items: center;\r\n width: 52px;\r\n height: 52px;\r\n border-radius: 999px;\r\n background: var(--cv-accent-soft);\r\n color: var(--cv-accent-strong);\r\n margin-bottom: 0.15rem;\r\n}\r\n.cv-gate[data-tone='danger'] .cv-gate-mark {\r\n background: var(--cv-danger-soft);\r\n color: var(--cv-danger);\r\n}\r\n\r\n.cv-gate-title {\r\n margin: 0;\r\n font: 600 0.95rem var(--cv-font);\r\n color: var(--cv-text);\r\n letter-spacing: -0.01em;\r\n}\r\n.cv-gate p {\r\n margin: 0;\r\n color: var(--cv-muted);\r\n font-size: 0.8rem;\r\n line-height: 1.5;\r\n max-width: 24ch; /* two comfortable lines, not one long one */\r\n}\r\n.cv-gate .cv-btn { margin-top: 0.5rem; }\r\n\r\n/* Entrance. Staggered 45 ms apart: the eye follows mark \u2192 title \u2192 reason \u2192\r\n action, which is the order it needs to read them in anyway. */\r\n.cv-gate > * {\r\n animation: cv-gate-in 260ms var(--cv-ease-out) both;\r\n}\r\n.cv-gate > *:nth-child(2) { animation-delay: 45ms; }\r\n.cv-gate > *:nth-child(3) { animation-delay: 90ms; }\r\n.cv-gate > *:nth-child(4) { animation-delay: 135ms; }\r\n\r\n@keyframes cv-gate-in {\r\n from { opacity: 0; transform: translateY(6px); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n\r\n/* The checking state: held back, then a quiet fade. No stagger, no movement \u2014\r\n it is a status, not a screen, and it should barely register. */\r\n.cv-gate[data-state='checking'] > *,\r\n.cv-gate[data-state='warming'] > * {\r\n animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both;\r\n}\r\n@keyframes cv-gate-wait {\r\n from { opacity: 0; }\r\n to { opacity: 1; }\r\n}\r\n/* A slow breath rather than a spinner: a spinner promises a long wait, and this\r\n is not one. */\r\n.cv-gate[data-state='checking'] .cv-gate-mark,\r\n.cv-gate[data-state='warming'] .cv-gate-mark {\r\n background: var(--cv-surface-2);\r\n color: var(--cv-muted);\r\n animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both,\r\n cv-breathe 1.8s ease-in-out 450ms infinite;\r\n}\r\n@keyframes cv-breathe {\r\n 0%, 100% { opacity: 1; }\r\n 50% { opacity: 0.45; }\r\n}\r\n\r\n/* Reduced motion: keep the fade and \u2014 critically \u2014 keep the DELAY, which is\r\n timing rather than motion and is the whole point. Drop the travel. */\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-gate > * {\r\n animation: cv-gate-wait 200ms var(--cv-ease-out) both;\r\n }\r\n .cv-gate[data-state='checking'] .cv-gate-mark,\r\n .cv-gate[data-state='warming'] .cv-gate-mark {\r\n animation: cv-gate-wait 200ms var(--cv-ease-out) 450ms both;\r\n }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-avatar-ping,\r\n .cv-timer i { animation: none; }\r\n .cv-softphone *,\r\n .cv-softphone *::before { transition-duration: 0.01ms !important; }\r\n}\r\n\r\n/* `*` and `#` stay in the grid \u2014 a keypad without them is not a keypad \u2014 but they are not part of a\r\n number (toE164 keeps digits only). Dimmed rather than `disabled` so the grid still reads as a\r\n keypad and the glyphs stay legible; they become live DTMF on the in-call screen. */\r\n.cv-key[data-dialable='false'] {\r\n cursor: default;\r\n}\r\n.cv-key[data-dialable='false'] b {\r\n color: var(--cv-faint);\r\n}\r\n.cv-key[data-dialable='false']:hover {\r\n background: none;\r\n}\r\n.cv-key[data-dialable='false']:active {\r\n transform: none;\r\n}\r\n\r\n/* \u2500\u2500 Country picker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n The prefix chip + its list. Sits where the old left spacer was, so the number\r\n stays optically centred against the 36px erase button on the right.\r\n\r\n The popover is absolutely positioned inside the widget rather than portalled:\r\n there is nowhere to portal TO inside a shadow root, and the widget already owns\r\n its stacking context. That also means it cannot be clipped by a host's\r\n `overflow:hidden` \u2014 the failure SPEC \xA72.6 lists for the CRM's Radix version.\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n/* The anchor spans the whole ROW, not just the chip.\r\n With the chip centred, anchoring to the chip put the popover's left edge at ~120px and its right\r\n edge past the widget \u2014 clipped by `.cv-softphone { overflow: hidden }`, which is the classic\r\n absolute-inside-overflow bug. Anchoring to the full row makes overflowing arithmetically\r\n impossible: `left:0; right:0` cannot be wider than the row that contains it. */\r\n.cv-cp {\r\n position: relative;\r\n display: flex;\r\n justify-content: center;\r\n width: 100%;\r\n}\r\n/* A PILL, with a resting surface and a border.\r\n This was `border: none; background: none` \u2014 invisible until hover, so it read as a label rather\r\n than a control. That was survivable when it sat inline beside the number; alone on its own row it\r\n is just floating text, and nothing says \"you can change this\". A control that changes state has to\r\n look like one at rest, not only under a cursor that may never arrive (touch has no hover at all). */\r\n.cv-cp-chip {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n height: 30px;\r\n padding: 0 0.5rem 0 0.4rem;\r\n border: 1px solid var(--cv-border-strong);\r\n border-radius: 999px;\r\n background: var(--cv-surface);\r\n color: var(--cv-text);\r\n font: 600 0.8rem var(--cv-font);\r\n cursor: pointer;\r\n transition: background 0.15s, border-color 0.15s;\r\n}\r\n.cv-cp-chip:hover { background: var(--cv-surface-2); }\r\n.cv-cp-chip:active { transform: scale(0.95); }\r\n/* Open: the chip is the thing the list belongs to, so it stays visibly engaged while the list is up. */\r\n.cv-cp-chip[aria-expanded='true'] {\r\n background: var(--cv-accent-soft);\r\n border-color: var(--cv-accent);\r\n}\r\n.cv-cp-dial {\r\n font-variant-numeric: tabular-nums;\r\n letter-spacing: -0.02em;\r\n}\r\n.cv-cp-caret {\r\n color: var(--cv-faint);\r\n flex-shrink: 0;\r\n transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n.cv-cp-caret[data-open='true'] { transform: rotate(180deg); }\r\n\r\n/* The country mark. TWO designed states in ONE 20\xD714 box (a flag's own 3:2), so the chip and every\r\n row keep their rhythm whichever is showing \u2014 nothing reflows when the flags land. The plate is the\r\n LOADING state (flags are a lazy import) and the offline state, not a fallback: there is no flash of\r\n something broken, only a designed plate becoming a designed flag, once per session.\r\n `::part(country-mark)` lets a host restyle either. */\r\n.cv-mark {\r\n width: 20px;\r\n height: 14px;\r\n flex-shrink: 0;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 14px;\r\n line-height: 1;\r\n font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', var(--cv-font);\r\n}\r\n/* The SVG fills the slot exactly; 3x2 is the flag aspect, so nothing is letterboxed or cropped. */\r\n.cv-mark-svg {\r\n width: 100%;\r\n height: 100%;\r\n display: block;\r\n border-radius: 2px;\r\n object-fit: cover;\r\n}\r\n.cv-mark[data-plate='true'] {\r\n /* The ISO code set into the slot a flag will occupy. */\r\n font-family: var(--cv-font);\r\n font-size: 8.5px;\r\n font-weight: 700;\r\n letter-spacing: 0.04em;\r\n color: var(--cv-text);\r\n background: var(--cv-surface-2);\r\n border: 1px solid var(--cv-border);\r\n border-radius: 3px;\r\n /* Cap-height letters sit optically high in a 14px box; nudge them back to centre. */\r\n padding-top: 1px;\r\n}\r\n\r\n.cv-cp-pop {\r\n position: absolute;\r\n top: calc(100% + 6px);\r\n /* Pinned to BOTH row edges, so it is bounded by the container by construction \u2014 no width guess,\r\n no media query, nothing to clip. `max-width` keeps it from looking stretched on a wide host. */\r\n left: 0;\r\n right: 0;\r\n margin-inline: auto;\r\n max-width: 260px;\r\n z-index: 20;\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--cv-surface);\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius);\r\n box-shadow: var(--cv-shadow);\r\n overflow: hidden;\r\n /* Grows from the chip it belongs to, rather than appearing on top of it. */\r\n transform-origin: top center;\r\n animation: cv-cp-in 0.14s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n@keyframes cv-cp-in {\r\n from { opacity: 0; transform: translateY(-4px) scale(0.97); }\r\n to { opacity: 1; transform: none; }\r\n}\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-cp-pop { animation: none; }\r\n .cv-cp-caret { transition: none; }\r\n}\r\n.cv-cp-search {\r\n flex-shrink: 0;\r\n padding: 0.5rem 0.65rem;\r\n border: none;\r\n border-bottom: 1px solid var(--cv-border);\r\n background: none;\r\n color: var(--cv-text);\r\n font: 400 0.8rem var(--cv-font);\r\n outline: none;\r\n}\r\n.cv-cp-search::placeholder { color: var(--cv-faint); }\r\n/* Both states named explicitly. In the CRM the widget mounts via `/ui`, i.e. in the LIGHT DOM \u2014 none\r\n of the shadow-DOM isolation the web-component playground proves applies there, so a host's focus\r\n ring (or the browser's own) can land on our input. The accent underline IS the focus indicator. */\r\n.cv-cp-search:focus,\r\n.cv-cp-search:focus-visible {\r\n outline: none;\r\n border-bottom-color: var(--cv-accent);\r\n border-bottom-width: 2px;\r\n padding-bottom: calc(0.5rem - 1px);\r\n}\r\n.cv-cp-list {\r\n overflow-y: auto;\r\n max-height: 220px;\r\n padding: 0.25rem;\r\n}\r\n.cv-cp-item {\r\n width: 100%;\r\n display: flex;\r\n align-items: center;\r\n gap: 0.5rem;\r\n padding: 0.4rem 0.45rem;\r\n border: none;\r\n border-radius: calc(var(--cv-radius) - 4px);\r\n background: none;\r\n color: var(--cv-text);\r\n font: 400 0.8rem var(--cv-font);\r\n text-align: left;\r\n cursor: pointer;\r\n}\r\n.cv-cp-item:hover { background: var(--cv-surface-2); }\r\n/* The current country is stated by the row, not only by a tick at the far right. */\r\n.cv-cp-item[aria-selected='true'] { background: var(--cv-accent-soft); }\r\n.cv-cp-item[aria-selected='true'] .cv-cp-name { font-weight: 600; }\r\n.cv-cp-name {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n.cv-cp-item .cv-cp-dial {\r\n color: var(--cv-muted);\r\n font-weight: 400;\r\n}\r\n.cv-cp-check { color: var(--cv-accent-strong); flex-shrink: 0; }\r\n.cv-cp-check[data-on='false'] { opacity: 0; }\r\n.cv-cp-empty {\r\n margin: 0;\r\n padding: 1rem 0.5rem;\r\n text-align: center;\r\n color: var(--cv-muted);\r\n font-size: 0.78rem;\r\n}\r\n\r\n/* \u2500\u2500 \"Llamar desde\" (outbound-number-per-agent.md \xA75.5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\r\n/* \"Llamar desde: Oficina \u25BE\" \u2014 a light line right above the number field, as the providers' dialers label their\r\n caller ID (\"Caller ID: \u2026 \u25BE\", \"Outbound on: \u2026 \u25BE\", \"From: \u2026 \u25BE\"; outbound-number-per-agent.md \xA75.5, 2026-10-26). Not a\r\n strip with a divider: under the host's own header that read as a second title bar. The line is the positioned\r\n ancestor, so its list and callout drop under it. */\r\n.cv-on {\r\n position: relative;\r\n flex-shrink: 0;\r\n display: flex;\r\n padding: 0.9rem 1rem 0.4rem;\r\n}\r\n.cv-on-chip {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 0.3rem;\r\n max-width: 100%;\r\n min-width: 0;\r\n height: 26px;\r\n margin-left: -0.3rem;\r\n padding: 0 0.3rem;\r\n border: 0;\r\n border-radius: 6px;\r\n background: none;\r\n color: var(--cv-text);\r\n font: 0.8rem var(--cv-font);\r\n text-align: left;\r\n cursor: pointer;\r\n transition: background 0.15s;\r\n}\r\n.cv-on-chip:hover { background: var(--cv-surface-2); }\r\n.cv-on-chip:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: 0; }\r\n.cv-on-label { flex-shrink: 0; color: var(--cv-muted); }\r\n.cv-on-chip .cv-on-current { font-weight: 600; }\r\n.cv-on-icon { color: var(--cv-accent-strong); flex-shrink: 0; }\r\n.cv-on-spin { animation: cv-spin 0.9s linear infinite; }\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-on-spin { animation: none; }\r\n}\r\n.cv-on-current {\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n/* Drops under the strip, over the number and the keypad, inset to the strip's edges. Capped so that in the CRM's\r\n 473px panel it never reaches the tab bar: the LIST gives way (it scrolls), never the title or a refusal message. */\r\n.cv-on-pop {\r\n top: calc(100% + 4px);\r\n left: 0.5rem;\r\n right: 0.5rem;\r\n max-width: none;\r\n max-height: 330px;\r\n}\r\n.cv-on-pop .cv-cp-list {\r\n flex: 1 1 auto;\r\n min-height: 0;\r\n max-height: none;\r\n}\r\n.cv-on-title {\r\n flex-shrink: 0;\r\n margin: 0;\r\n padding: 0.5rem 0.75rem 0.4rem;\r\n border-bottom: 1px solid var(--cv-border);\r\n font: 600 0.7rem var(--cv-font);\r\n letter-spacing: 0.06em;\r\n text-transform: uppercase;\r\n color: var(--cv-muted);\r\n}\r\n.cv-on-item { align-items: flex-start; }\r\n/* Locked while a change is applying or the limit says wait: still legible (the choice is visible), not usable. */\r\n.cv-on-item[aria-disabled='true'] { cursor: default; }\r\n.cv-on-item[aria-disabled='true']:not([aria-selected='true']) { opacity: 0.55; }\r\n.cv-on-item[aria-disabled='true']:not([aria-selected='true']):hover { background: none; }\r\n.cv-on-text {\r\n flex: 1;\r\n min-width: 0;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 1px;\r\n}\r\n.cv-on-line {\r\n display: flex;\r\n align-items: center;\r\n gap: 0.35rem;\r\n min-width: 0;\r\n}\r\n.cv-on-sub {\r\n color: var(--cv-muted);\r\n font-size: 0.74rem;\r\n font-variant-numeric: tabular-nums;\r\n}\r\n/* A quiet, neutral pill: it tells the agent what the number is for, it is not a warning. Shared with Ajustes. */\r\n.cv-on-tag,\r\n.cv-choice-tag {\r\n flex-shrink: 0;\r\n padding: 0 0.35rem;\r\n border: 1px solid var(--cv-border-strong);\r\n border-radius: 999px;\r\n color: var(--cv-muted);\r\n font-size: 0.66rem;\r\n font-weight: 600;\r\n line-height: 1.4;\r\n}\r\n/* The usage rule IS information (when not to call from this number): --cv-muted, not --cv-faint, which is under\r\n 4.5:1 at this size in both themes (see `.cv-empty-sub`). */\r\n.cv-on-hint,\r\n.cv-choice-hint {\r\n color: var(--cv-muted);\r\n font-size: 0.7rem;\r\n line-height: 1.3;\r\n}\r\n.cv-on-item .cv-cp-check { margin-top: 2px; }\r\n/* Why a change cannot happen right now, and until when. Calm, not alarming: the phone keeps working. */\r\n.cv-on-notice {\r\n flex-shrink: 0;\r\n margin: 0;\r\n padding: 0.55rem 0.75rem;\r\n border-top: 1px solid var(--cv-border);\r\n background: var(--cv-surface-2);\r\n color: var(--cv-text);\r\n font-size: 0.74rem;\r\n line-height: 1.4;\r\n}\r\n.cv-on-notice[data-kind='error'] { color: var(--cv-danger-strong); }\r\n/* A change that did not go through, after the (optimistic) list already closed: the same words, where the list\r\n was (under the strip). Same place and surface as the list; dismissed by a tap elsewhere, when the wait is over,\r\n or after a few seconds. */\r\n.cv-on-callout {\r\n position: absolute;\r\n top: calc(100% + 4px);\r\n left: 0.5rem;\r\n right: 0.5rem;\r\n margin: 0;\r\n z-index: 20;\r\n padding: 0.6rem 0.75rem;\r\n background: var(--cv-surface);\r\n border: 1px solid var(--cv-border);\r\n border-radius: var(--cv-radius);\r\n box-shadow: var(--cv-shadow);\r\n color: var(--cv-text);\r\n font-size: 0.74rem;\r\n line-height: 1.4;\r\n transform-origin: top center;\r\n animation: cv-cp-in 0.14s cubic-bezier(0.2, 0, 0.2, 1);\r\n}\r\n.cv-on-callout[data-kind='error'] { color: var(--cv-danger-strong); }\r\n@media (prefers-reduced-motion: reduce) {\r\n .cv-on-callout { animation: none; }\r\n}\r\n";
9809
11230
 
9810
11231
  // src/element/index.ts
9811
11232
  var EVENTS = {
@@ -9982,9 +11403,9 @@ var CoventiaSoftphoneElement = class extends HTMLElement {
9982
11403
  ...this.#config.incomingCallAction ? { incomingCallAction: this.#config.incomingCallAction } : {}
9983
11404
  };
9984
11405
  this.#root.render(
9985
- (0, import_react32.createElement)(
11406
+ (0, import_react46.createElement)(
9986
11407
  SoftphoneProvider,
9987
- { softphone: this.#softphone, children: (0, import_react32.createElement)(SoftphoneWidget, widgetProps) }
11408
+ { softphone: this.#softphone, children: (0, import_react46.createElement)(SoftphoneWidget, widgetProps) }
9988
11409
  )
9989
11410
  );
9990
11411
  }
@@ -10037,6 +11458,7 @@ lucide-react/dist/esm/createLucideIcon.js:
10037
11458
  lucide-react/dist/esm/icons/arrow-left-right.js:
10038
11459
  lucide-react/dist/esm/icons/arrow-left.js:
10039
11460
  lucide-react/dist/esm/icons/arrow-right.js:
11461
+ lucide-react/dist/esm/icons/bell-ring.js:
10040
11462
  lucide-react/dist/esm/icons/calendar-clock.js:
10041
11463
  lucide-react/dist/esm/icons/check.js:
10042
11464
  lucide-react/dist/esm/icons/chevron-down.js:
@@ -10052,6 +11474,7 @@ lucide-react/dist/esm/icons/loader-circle.js:
10052
11474
  lucide-react/dist/esm/icons/mic-off.js:
10053
11475
  lucide-react/dist/esm/icons/mic.js:
10054
11476
  lucide-react/dist/esm/icons/monitor-smartphone.js:
11477
+ lucide-react/dist/esm/icons/music.js:
10055
11478
  lucide-react/dist/esm/icons/pause.js:
10056
11479
  lucide-react/dist/esm/icons/phone-incoming.js:
10057
11480
  lucide-react/dist/esm/icons/phone-missed.js:
@@ -10065,6 +11488,7 @@ lucide-react/dist/esm/icons/timer.js:
10065
11488
  lucide-react/dist/esm/icons/triangle-alert.js:
10066
11489
  lucide-react/dist/esm/icons/user.js:
10067
11490
  lucide-react/dist/esm/icons/users.js:
11491
+ lucide-react/dist/esm/icons/volume-2.js:
10068
11492
  lucide-react/dist/esm/icons/x.js:
10069
11493
  lucide-react/dist/esm/lucide-react.js:
10070
11494
  (**