voicecc 1.0.7

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 (59) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/README.md +48 -0
  3. package/bin/voicecc.js +39 -0
  4. package/dashboard/dist/assets/index-BXemFrMp.css +1 -0
  5. package/dashboard/dist/assets/index-dAYfRls7.js +11 -0
  6. package/dashboard/dist/audio-processor.js +126 -0
  7. package/dashboard/dist/index.html +13 -0
  8. package/dashboard/routes/auth.ts +119 -0
  9. package/dashboard/routes/browser-call.ts +87 -0
  10. package/dashboard/routes/claude-md.ts +50 -0
  11. package/dashboard/routes/conversations.ts +203 -0
  12. package/dashboard/routes/integrations.ts +154 -0
  13. package/dashboard/routes/mcp-servers.ts +198 -0
  14. package/dashboard/routes/settings.ts +64 -0
  15. package/dashboard/routes/tunnel.ts +66 -0
  16. package/dashboard/routes/twilio.ts +120 -0
  17. package/dashboard/routes/voice.ts +48 -0
  18. package/dashboard/routes/webrtc.ts +85 -0
  19. package/dashboard/server.ts +130 -0
  20. package/dashboard/tsconfig.json +13 -0
  21. package/init/CLAUDE.md +18 -0
  22. package/package.json +59 -0
  23. package/run.ts +68 -0
  24. package/scripts/postinstall.js +228 -0
  25. package/services/browser-call-manager.ts +106 -0
  26. package/services/device-pairing.ts +176 -0
  27. package/services/env.ts +88 -0
  28. package/services/tunnel.ts +204 -0
  29. package/services/twilio-manager.ts +126 -0
  30. package/sidecar/assets/startup.pcm +0 -0
  31. package/sidecar/audio-adapter.ts +60 -0
  32. package/sidecar/audio-capture.ts +220 -0
  33. package/sidecar/browser-audio-playback.test.ts +149 -0
  34. package/sidecar/browser-audio.ts +147 -0
  35. package/sidecar/browser-server.ts +331 -0
  36. package/sidecar/chime.test.ts +69 -0
  37. package/sidecar/chime.ts +54 -0
  38. package/sidecar/claude-session.ts +295 -0
  39. package/sidecar/endpointing.ts +163 -0
  40. package/sidecar/index.ts +83 -0
  41. package/sidecar/local-audio.ts +126 -0
  42. package/sidecar/mic-vpio +0 -0
  43. package/sidecar/mic-vpio.swift +484 -0
  44. package/sidecar/mock-tts-server-tagged.mjs +132 -0
  45. package/sidecar/narration.ts +204 -0
  46. package/sidecar/scripts/generate-startup-audio.py +79 -0
  47. package/sidecar/session-lock.ts +123 -0
  48. package/sidecar/sherpa-onnx-node.d.ts +4 -0
  49. package/sidecar/stt.ts +199 -0
  50. package/sidecar/tts-server.py +193 -0
  51. package/sidecar/tts.ts +481 -0
  52. package/sidecar/twilio-audio.ts +338 -0
  53. package/sidecar/twilio-server.ts +436 -0
  54. package/sidecar/types.ts +210 -0
  55. package/sidecar/vad.ts +101 -0
  56. package/sidecar/voice-loop-bugs.test.ts +522 -0
  57. package/sidecar/voice-session.ts +523 -0
  58. package/skills/voice/SKILL.md +26 -0
  59. package/tsconfig.json +22 -0
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "claude-code-voice",
3
+ "description": "Voice mode for hands-free interaction with Claude Code",
4
+ "version": "1.0.0",
5
+ "skills": ["skills/voice/SKILL.md"]
6
+ }
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Voice CC
2
+
3
+ A Claude Code plugin for hands-free voice interaction with local speech-to-text, text-to-speech, and voice activity detection.
4
+
5
+ ## Installation
6
+
7
+ ### Prerequisites
8
+
9
+ - macOS with Apple Silicon (M1/M2/M3/M4)
10
+ - Node.js 18+
11
+ - Python 3.10+
12
+ - Homebrew
13
+
14
+ ### Install
15
+
16
+ ```bash
17
+ # 1. Install system dependencies
18
+ xcode-select --install
19
+ brew install espeak-ng cloudflared
20
+
21
+ # 2. Install Voice CC
22
+ npm install -g voicecc
23
+
24
+ # 3. Start the dashboard
25
+ voicecc
26
+ ```
27
+
28
+ The postinstall script handles sox, the Whisper model, Python venv, and TTS dependencies automatically.
29
+
30
+ ## How It Works
31
+
32
+ The voice loop runs locally with zero external API calls except to Claude:
33
+
34
+ 1. **Mic capture**: VPIO (macOS Voice Processing IO) records 16kHz mono PCM with echo cancellation
35
+ 2. **Voice activity detection**: Silero VAD v5 detects speech segments
36
+ 3. **Speech-to-text**: sherpa-onnx (Whisper ONNX model) transcribes audio locally
37
+ 4. **Endpointing**: VAD silence-based turn detection
38
+ 5. **Claude inference**: Transcript sent to Claude Code Agent SDK session with streaming response
39
+ 6. **Narration**: Claude's response stripped of markdown and split into sentences
40
+ 7. **Text-to-speech**: Kokoro-82M via mlx-audio on Apple Silicon GPU (~8x realtime)
41
+ 8. **Speaker playback**: Audio output through VPIO at 24kHz with echo cancellation
42
+
43
+ ## Troubleshooting
44
+
45
+ - **"sox not found"**: Install sox with `brew install sox`
46
+ - **"espeak not installed"**: Install espeak-ng with `brew install espeak-ng`
47
+ - **tts-server.py not ready**: Ensure the Python venv is set up correctly
48
+ - **Mic permission denied**: Grant microphone permissions to your terminal or IDE
package/bin/voicecc.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI entry point for the voicecc command.
5
+ *
6
+ * Resolves the package install directory and spawns `tsx run.ts`
7
+ * with inherited stdio so the dashboard server runs in the foreground.
8
+ *
9
+ * Responsibilities:
10
+ * - Resolve the package root from this script's location
11
+ * - Spawn tsx with run.ts in the correct working directory
12
+ * - Forward signals (SIGINT, SIGTERM) so Ctrl+C stops the server cleanly
13
+ */
14
+
15
+ import { spawn } from "node:child_process";
16
+ import { dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ // ============================================================================
20
+ // CONSTANTS
21
+ // ============================================================================
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const PKG_ROOT = join(__dirname, "..");
25
+ const TSX_BIN = join(PKG_ROOT, "node_modules", ".bin", "tsx");
26
+
27
+ // ============================================================================
28
+ // MAIN ENTRYPOINT
29
+ // ============================================================================
30
+
31
+ const child = spawn(TSX_BIN, ["run.ts"], {
32
+ cwd: PKG_ROOT,
33
+ stdio: "inherit",
34
+ });
35
+
36
+ process.on("SIGINT", () => child.kill("SIGINT"));
37
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
38
+
39
+ child.on("exit", (code) => process.exit(code ?? 1));
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:ital,wght@0,100..700;1,100..700&display=swap";:root{--bg-main: #ffffff;--bg-sidebar: #f7f7f8;--bg-surface: #ffffff;--bg-hover: #f1f3f5;--bg-active: #e9ecef;--border-color: #eaeaea;--text-primary: #111827;--text-secondary: #6b7280;--accent-color: #2ea043;--btn-primary-bg: #111111;--btn-primary-text: #ffffff;--btn-primary-hover: #333333;--btn-secondary-bg: #ffffff;--btn-secondary-text: #111827;--btn-secondary-border: #eaeaea;--btn-secondary-hover: #f9fafb}@media(prefers-color-scheme:dark){:root{--bg-main: #0a0a0a;--bg-sidebar: #111111;--bg-surface: #171717;--bg-hover: #262626;--bg-active: #333333;--border-color: #333333;--text-primary: #f3f4f6;--text-secondary: #9ca3af;--accent-color: #3fb950;--btn-primary-bg: #ececec;--btn-primary-text: #111111;--btn-primary-hover: #ffffff;--btn-secondary-bg: #111111;--btn-secondary-text: #f3f4f6;--btn-secondary-border: #333333;--btn-secondary-hover: #1f1f1f}}body.dark{--bg-main: #0a0a0a;--bg-sidebar: #111111;--bg-surface: #171717;--bg-hover: #262626;--bg-active: #333333;--border-color: #333333;--text-primary: #f3f4f6;--text-secondary: #9ca3af;--accent-color: #3fb950;--btn-primary-bg: #ececec;--btn-primary-text: #111111;--btn-primary-hover: #ffffff;--btn-secondary-bg: #111111;--btn-secondary-text: #f3f4f6;--btn-secondary-border: #333333;--btn-secondary-hover: #1f1f1f}body.light{--bg-main: #ffffff;--bg-sidebar: #f7f7f8;--bg-surface: #ffffff;--bg-hover: #f1f3f5;--bg-active: #e9ecef;--border-color: #eaeaea;--text-primary: #111827;--text-secondary: #6b7280;--accent-color: #2ea043;--btn-primary-bg: #111111;--btn-primary-text: #ffffff;--btn-primary-hover: #333333;--btn-secondary-bg: #ffffff;--btn-secondary-text: #111827;--btn-secondary-border: #eaeaea;--btn-secondary-hover: #f9fafb}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Inter,Segoe UI,Roboto,sans-serif;background:var(--bg-main);color:var(--text-primary);height:100vh;overflow:hidden}#root{height:100vh}.sidebar{width:240px;background:var(--bg-sidebar);border-right:1px solid var(--border-color);display:flex;flex-direction:column;flex-shrink:0;height:100vh}.sidebar-nav{flex:1;padding:16px 12px;display:flex;flex-direction:column;overflow:hidden}.sidebar-item{display:flex;align-items:center;gap:8px;margin-bottom:2px;padding:8px 12px;font-size:13px;font-weight:500;color:var(--text-secondary);background:transparent;border:none;border-radius:0;text-align:left;cursor:pointer;font-family:inherit;transition:all .15s ease;flex-shrink:0}.sidebar-item:hover{color:var(--text-primary);background:var(--bg-hover)}.sidebar-item.active{color:var(--accent-color);background:#2ea0431a;font-weight:600}.sidebar-footer{padding:12px 0}.sidebar-section-label{padding:16px 24px 8px;font-size:11px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px}.sidebar-conversation{display:block;margin-bottom:2px;padding:8px 12px;font-size:13px;color:var(--text-secondary);background:transparent;border:none;border-radius:0;text-align:left;cursor:pointer;font-family:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}.sidebar-conversation:hover{color:var(--text-primary);background:var(--bg-hover)}.sidebar-conversation.active{color:var(--text-primary);background:var(--bg-active);font-weight:500}.btn-start-voice{display:block;width:calc(100% - 24px);margin:0 12px 8px;padding:8px 12px;font-size:13px;font-weight:500;font-family:inherit;color:var(--btn-primary-text);background:var(--btn-primary-bg);border:none;border-radius:0;cursor:pointer;text-align:center;transition:all .2s}.btn-start-voice:hover{background:var(--btn-primary-hover)}.btn-start-voice:disabled{opacity:.5;cursor:not-allowed}.btn-browser-call{display:block;width:calc(100% - 24px);margin:0 12px 16px;padding:8px 12px;font-size:13px;font-weight:500;font-family:inherit;color:var(--btn-secondary-text);background:var(--btn-secondary-bg);border:1px solid var(--btn-secondary-border);border-radius:0;cursor:pointer;text-align:center;transition:all .2s}.btn-browser-call:hover{background:var(--btn-secondary-hover)}.btn-browser-call:disabled{opacity:.5;cursor:not-allowed}.browser-call-wrap{display:block;width:100%}.browser-call-tooltip{display:none;position:absolute;left:50%;transform:translate(-50%);bottom:calc(100% + 6px);padding:4px 10px;font-size:12px;color:var(--text-primary);background:var(--bg-surface);border:1px solid var(--border-color);white-space:nowrap;pointer-events:none;z-index:10}.browser-call-wrap:hover .browser-call-tooltip{display:block}.main{flex:1;display:flex;flex-direction:column;overflow:hidden}.page{display:none;flex:1;flex-direction:column;overflow:hidden}.page.active{display:flex}.page-header{background:var(--bg-main);padding:24px 32px 16px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}.page-header h1{font-size:24px;font-weight:600;color:var(--text-primary);letter-spacing:-.5px}.toolbar{display:flex;align-items:center;gap:10px}.toolbar button{background:var(--btn-primary-bg);color:#fff;border:none;padding:5px 14px;border-radius:0;font-size:12px;cursor:pointer}.toolbar button:hover{background:var(--btn-primary-hover)}.toolbar button:disabled{background:var(--text-secondary);cursor:default}#status{font-size:12px;color:var(--text-secondary)}.warning{background:#3e2f00;border-bottom:1px solid #665500;padding:8px 16px;font-size:12px;color:#cca700;display:none;flex-shrink:0}.warning.visible{display:block}.warning code{background:#2a2000;padding:1px 4px;border-radius:0}#editor{flex:1;width:100%;background:var(--bg-surface);color:var(--text-primary);border:none;padding:16px 20px;font-family:SF Mono,Fira Code,Cascadia Code,monospace;font-size:13px;line-height:1.6;resize:none;outline:none;tab-size:2}.settings-panel{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;padding:24px;margin-bottom:24px;box-shadow:0 1px 3px #0000000d}.settings-panel h2{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:12px}.settings-row{display:flex;align-items:center;margin-bottom:8px;gap:10px}.settings-row label{font-size:12px;color:var(--text-secondary);width:200px;flex-shrink:0}.settings-row input{flex:1;max-width:340px;background:var(--bg-main);border:1px solid var(--border-color);color:var(--text-primary);padding:8px 12px;font-size:13px;border-radius:0;outline:none;transition:border-color .2s}.settings-row input:focus{border-color:var(--btn-primary-bg)}.settings-actions{margin-top:12px;display:flex;align-items:center;gap:10px}.settings-actions button{background:var(--btn-primary-bg);color:var(--btn-primary-text);border:none;padding:8px 16px;border-radius:0;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s}.settings-actions button:hover{background:var(--btn-primary-hover)}.settings-actions button:disabled{background:var(--text-secondary);cursor:default}.settings-status{font-size:12px;color:var(--text-secondary)}.integrations-panel{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;padding:24px;margin-bottom:24px;box-shadow:0 1px 3px #0000000d}.integrations-panel h2{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:12px}.btn-integration{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;width:120px;height:120px;background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;color:var(--text-primary);font-size:12px;cursor:pointer;font-family:inherit;gap:6px}.btn-integration:hover{background:#37373d;border-color:var(--text-secondary)}.integration-dot{width:8px;height:8px;border-radius:50%;background:#666}.integration-dot.running{background:#2ea043}.mcp-panel{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;padding:24px;margin-bottom:24px;box-shadow:0 1px 3px #0000000d}.mcp-panel h2{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:12px}.mcp-server-list{display:flex;flex-direction:column;gap:6px}.mcp-server-row{display:flex;align-items:center;gap:10px;padding:6px 10px;background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;font-size:12px}.mcp-dot{width:8px;height:8px;border-radius:50%;background:#666;flex-shrink:0}.mcp-dot.connected{background:#2ea043}.mcp-dot.failed{background:#d73a49}.mcp-dot.needs-auth{background:#d29922}.mcp-server-name{color:var(--text-primary);font-weight:600;min-width:100px}.mcp-server-url{color:var(--text-secondary);font-family:SF Mono,Fira Code,monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.mcp-server-badge{font-size:10px;padding:1px 6px;border-radius:0;background:#333;color:var(--text-secondary);border:1px solid var(--border-color);flex-shrink:0}.mcp-server-status{font-size:11px;flex-shrink:0}.mcp-server-status.connected{color:#2ea043}.mcp-server-status.failed{color:#d73a49}button.mcp-server-status.needs-auth{color:#d29922;cursor:pointer;background:none;border:1px solid #d29922;border-radius:0;padding:1px 6px;font-size:11px}button.mcp-server-status.needs-auth:hover{background:#d2992226}.mcp-loading,.mcp-error{font-size:12px;color:var(--text-secondary);padding:4px 0}.mcp-error{color:#d73a49}.mcp-delete-wrap{position:relative;flex-shrink:0}.mcp-delete-btn{background:none;border:none;color:var(--text-secondary);font-size:14px;cursor:pointer;padding:0 4px;line-height:1}.mcp-delete-btn:hover{color:#d73a49}.mcp-delete-btn.disabled{cursor:not-allowed;opacity:.4}.mcp-delete-btn.disabled:hover{color:var(--text-secondary)}.mcp-delete-tooltip{display:none;position:absolute;bottom:calc(100% + 6px);right:0;background:var(--bg-surface);border:1px solid var(--border-color);color:var(--text-secondary);font-size:11px;padding:4px 8px;white-space:nowrap;z-index:10;pointer-events:none}.mcp-delete-wrap:hover .mcp-delete-tooltip{display:block}.mcp-add-btn{background:var(--btn-primary-bg);color:var(--btn-primary-text);border:none;padding:6px 14px;font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap;flex-shrink:0}.mcp-add-btn:hover{opacity:.85}.mcp-add-scope-row{display:flex;align-items:center;gap:6px;margin-bottom:16px}.mcp-add-scope-btn{background:var(--bg-surface);border:1px solid var(--border-color);color:var(--text-secondary);padding:3px 10px;font-size:11px;cursor:pointer}.mcp-add-scope-btn.active{background:var(--btn-primary-bg);color:var(--btn-primary-text);border-color:transparent}.mcp-add-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.mcp-add-card{background:var(--bg-surface);border:1px solid var(--border-color);padding:14px;display:flex;flex-direction:column;gap:6px}.mcp-add-card.installed{opacity:.55}.mcp-add-card-header{display:flex;align-items:center;justify-content:space-between}.mcp-add-card-name{font-size:13px;font-weight:600;color:var(--text-primary);text-transform:capitalize}.mcp-add-card-badge{font-size:10px;padding:1px 6px;background:#2ea043;color:#fff;font-weight:500}.mcp-add-card-badge.needs-auth{background:#d29922}.mcp-add-card-desc{font-size:11px;color:var(--text-secondary);line-height:1.4;margin:0}.mcp-add-card-footer{display:flex;align-items:center;justify-content:space-between;margin-top:auto;gap:8px}.mcp-add-card-url{font-size:10px;color:var(--text-secondary);font-family:SF Mono,Fira Code,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.mcp-add-card-btn{background:var(--btn-primary-bg);color:var(--btn-primary-text);border:none;padding:3px 12px;font-size:11px;font-weight:500;cursor:pointer;flex-shrink:0}.mcp-add-card-btn:hover{opacity:.85}.mcp-add-card-btn.auth{background:none;border:1px solid #d29922;color:#d29922}.mcp-add-card-btn.auth:hover{background:#d2992226;opacity:1}.mcp-add-card-btn:disabled{opacity:.5;cursor:not-allowed}.mcp-add-card-btn.delete{background:none;border:1px solid #d73a49;color:#d73a49}.mcp-add-card-btn.delete:hover{background:#d73a4926;opacity:1}.mcp-add-card-btn.delete.disabled{opacity:.4;cursor:not-allowed}.mcp-add-card-btn.delete.disabled:hover{background:none}.toast{position:fixed;bottom:20px;right:20px;background:#6e3630;border:1px solid #d73a49;color:#f0f0f0;font-size:12px;padding:10px 14px;display:flex;align-items:center;gap:10px;z-index:200;max-width:400px}.toast-close{background:none;border:none;color:#f0f0f0;font-size:14px;cursor:pointer;padding:0 2px;line-height:1;opacity:.7}.toast-close:hover{opacity:1}.modal-overlay{display:none;position:fixed;inset:0;background:#0009;z-index:100;justify-content:center;align-items:center}.modal-overlay.visible{display:flex}.modal{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;width:560px;max-height:80vh;overflow-y:auto;padding:20px 24px}.modal h2{font-size:15px;font-weight:600;color:var(--text-primary);margin-bottom:16px}.modal-close{float:right;background:none;border:none;color:var(--text-secondary);font-size:18px;cursor:pointer;padding:0 4px;line-height:1}.modal-close:hover{color:var(--text-primary)}.setup-step{margin-bottom:16px}.setup-step-number{display:inline-block;width:20px;height:20px;background:var(--btn-primary-bg);color:var(--btn-primary-text);font-size:11px;font-weight:600;text-align:center;line-height:20px;border-radius:50%;margin-right:8px;flex-shrink:0}.setup-step-title{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:4px}.setup-step-desc{font-size:12px;color:var(--text-secondary);line-height:1.5;margin-left:28px}.setup-step-desc code{background:var(--bg-surface);padding:1px 5px;border-radius:0;font-family:SF Mono,Fira Code,monospace;font-size:11px;color:var(--accent-color)}.setup-step-desc a{color:var(--accent-color);text-decoration:none}.setup-step-desc a:hover{text-decoration:underline}.setup-paste-row{display:flex;gap:8px;margin:8px 0 0 28px;align-items:center}.setup-paste-row input{flex:1;background:var(--bg-surface);border:1px solid var(--border-color);color:var(--text-primary);padding:5px 8px;font-size:12px;font-family:SF Mono,Fira Code,monospace;border-radius:0;outline:none}.setup-paste-row input:focus{border-color:var(--btn-primary-bg)}.setup-paste-row button{background:var(--btn-primary-bg);color:var(--btn-primary-text);border:none;padding:5px 10px;border-radius:0;font-size:11px;cursor:pointer;white-space:nowrap}.setup-paste-row button:hover{background:var(--btn-primary-hover)}.setup-paste-row button.applied{background:#2ea043}.setup-divider{border:none;border-top:1px solid var(--border-color);margin:16px 0}.conversation-messages{flex:1;overflow-y:auto;padding:16px 20px}.msg{margin-bottom:24px;max-width:100%}.msg-header{display:flex;align-items:center;gap:8px;margin-bottom:6px}.msg-role{font-size:13px;font-weight:600;color:var(--text-primary)}.msg-time{font-size:11px;color:var(--text-secondary)}.msg-content{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;padding:12px 16px;font-size:13px;line-height:1.6;color:var(--text-primary);white-space:pre-wrap}.msg-user .msg-content{background:var(--bg-hover)}.conversation-empty{color:var(--text-secondary);font-size:13px;padding:20px}.qr-container{display:flex;justify-content:center;margin:16px 0}.qr-container img,.qr-container svg,.qr-container table{border-radius:0}.call-url{font-size:12px;color:var(--text-secondary);word-break:break-all;margin:8px 0 4px}.call-url a{color:#3794ff;text-decoration:none}.call-url a:hover{text-decoration:underline}.pairing-code{font-size:36px;font-weight:700;letter-spacing:12px;color:var(--text-primary);font-family:SF Mono,Fira Code,monospace;margin:20px 0 8px;text-align:center}.pairing-countdown{font-size:12px;color:var(--text-secondary);text-align:center;margin-bottom:16px}.btn-regenerate{background:#333;border:1px solid var(--border-color);color:var(--text-primary);padding:6px 16px;font-size:12px;cursor:pointer;border-radius:0;font-family:inherit}.btn-regenerate:hover{background:var(--border-color)}.call-container{width:100%;max-width:400px;padding:32px 24px;text-align:center;margin:0 auto;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center}.call-container h1{font-size:20px;font-weight:600;color:var(--text-primary);margin-bottom:8px}.subtitle{font-size:13px;color:var(--text-secondary);margin-bottom:32px}.call-state{text-align:center}.pin-inputs{display:flex;gap:8px;justify-content:center;margin-bottom:20px}.pin-inputs input{width:44px;height:56px;text-align:center;font-size:24px;font-weight:600;font-family:inherit;background:var(--bg-surface);border:2px solid var(--border-color);border-radius:0;color:var(--text-primary);outline:none}.pin-inputs input:focus{border-color:var(--btn-primary-bg)}.btn{display:inline-block;padding:12px 32px;border:none;border-radius:0;font-size:14px;font-weight:600;font-family:inherit;cursor:pointer;transition:opacity .15s}.btn:hover{opacity:.85}.btn:disabled{opacity:.4;cursor:not-allowed}.btn-pair{background:var(--btn-primary-bg);color:#fff}.btn-call{background:#2ea043;color:#fff;font-size:16px;padding:14px 48px}.btn-hangup{background:#d73a49;color:#fff;font-size:16px;padding:14px 48px}.error-msg{color:#f85149;font-size:13px;margin-top:12px;min-height:18px}.status-msg{color:var(--text-secondary);font-size:13px;margin-top:12px}.pulse-ring{display:inline-block;width:80px;height:80px;border-radius:50%;background:#2ea0434d;margin-bottom:24px;position:relative}.pulse-ring:before{content:"";position:absolute;inset:20px;border-radius:50%;background:#2ea043}.pulse-ring:after{content:"";position:absolute;inset:0;border-radius:50%;border:2px solid #2ea043;animation:pulse 2s ease-out infinite}@keyframes pulse{0%{transform:scale(1);opacity:.6}to{transform:scale(1.6);opacity:0}}.call-label{font-size:15px;color:#2ea043;margin-bottom:24px}