diff --git a/static/app.js b/static/app.js index ab0bcde..3c53cea 100644 --- a/static/app.js +++ b/static/app.js @@ -517,7 +517,7 @@ async function clientAutoTrimBounds(fid) { function applyTheme(t) { document.documentElement.dataset.theme = t; - $('theme-btn').textContent = t === 'dark' ? '☀️' : '🌙'; + $('theme-btn').innerHTML = t === 'dark' ? '' : ''; $('theme-btn').title = t === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'; localStorage.setItem('vcf-theme', t); } @@ -6244,7 +6244,7 @@ loadSettings().then(() => { const key = (s.elevenlabs_api_key || '').trim(); const inp = $('el-api-key'), st = $('el-key-status'); if (inp) inp.placeholder = key ? '••••••••••••••• (key saved)' : 'ElevenLabs API key (free — unlocks full library)…'; - if (st) { st.textContent = key ? '✓ Key active' : 'No key — max 3 results'; st.className = 'el-key-status ' + (key ? 'el-key-ok' : 'el-key-none'); } + if (st) { st.innerHTML = key ? ' Key active' : 'No key — max 3 results'; st.className = 'el-key-status ' + (key ? 'el-key-ok' : 'el-key-none'); } elFetch(); }).catch(() => elFetch()); } @@ -6256,7 +6256,7 @@ loadSettings().then(() => { fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ elevenlabs_api_key: val }) }) .then(() => { const st = $('el-key-status'); - if (st) { st.textContent = '✓ Key saved'; st.className = 'el-key-status el-key-ok'; } + if (st) { st.innerHTML = ' Key saved'; st.className = 'el-key-status el-key-ok'; } if (inp) { inp.value = ''; inp.placeholder = '••••••••••••••• (key saved)'; } _elPage = 0; elFetch(); }); @@ -6306,7 +6306,7 @@ loadSettings().then(() => { const res = await fetch('/api/elevenlabs/voices?' + elParams()); const data = await res.json(); if (data.detail) { - if (grid) grid.innerHTML = `
⚠ ${_elEsc(data.detail?.message || JSON.stringify(data.detail))}
`; + if (grid) grid.innerHTML = `
${_elEsc(data.detail?.message || JSON.stringify(data.detail))}
`; if (sb) sb.hidden = true; return; } @@ -6334,7 +6334,7 @@ loadSettings().then(() => { if (pi) pi.textContent = `Page ${_elPage + 1}${_elTotal > 24 ? ' of ~' + Math.ceil(_elTotal / 24) : ''}`; } } catch (e) { - if (grid) grid.innerHTML = `
⚠ ${_elEsc(e.message)}
`; + if (grid) grid.innerHTML = `
${_elEsc(e.message)}
`; } } @@ -6357,8 +6357,8 @@ loadSettings().then(() => { uc ? `${_elEsc(uc)}` : '', (v.accent && v.accent !== 'standard') ? `${_elEsc(v.accent)}` : '', v.free_users_allowed ? 'Free' : '', - v.featured ? '' : '', - cl ? `↓ ${cl}` : '', + v.featured ? '' : '', + cl ? ` ${cl}` : '', ].join(''); return `
${letter}
@@ -6456,16 +6456,20 @@ function renderLocalContainers(containers) { 'magpie-tts': '🐦', 'parakeet-rnnt-nim': '🦜', }; + const _t = (v) => ``; + const _l = (v) => ``; + const _q = (v) => ``; + const _m = (v) => ``; const DC_METRICS = { - 'faster-qwen3-tts-voiceclone': [['','~0.3× GPU'],['⏰','1–3 s'],['⭐','Premium clone'],['💾','6–8 GB VRAM']], - 'faster-qwen3-tts-voicedesign': [['','~0.4× GPU'],['⏰','1–3 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']], - 'faster-qwen3-tts-customvoice': [['','~0.3× GPU'],['⏰','1–3 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']], - 'faster-qwen3-tts-streaming': [['','~0.1× GPU'],['⏰','0.5–1 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']], - 'magpie-tts': [['','~0.05× GPU'],['⏰','0.3–0.8 s'],['⭐','High'], ['💾','4–6 GB VRAM']], - 'parakeet-asr': [['','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-TDT'],['💾','2 GB VRAM']], - 'parakeet-rnnt-nim': [['','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-1B'], ['💾','2 GB VRAM']], + 'faster-qwen3-tts-voiceclone': [[_t(),'~0.3× GPU'],[_l(),'1–3 s'],[_q(),'Premium clone'],[_m(),'6–8 GB VRAM']], + 'faster-qwen3-tts-voicedesign': [[_t(),'~0.4× GPU'],[_l(),'1–3 s'],[_q(),'Premium'], [_m(),'6–8 GB VRAM']], + 'faster-qwen3-tts-customvoice': [[_t(),'~0.3× GPU'],[_l(),'1–3 s'],[_q(),'Premium'], [_m(),'6–8 GB VRAM']], + 'faster-qwen3-tts-streaming': [[_t(),'~0.1× GPU'],[_l(),'0.5–1 s'],[_q(),'Premium'], [_m(),'6–8 GB VRAM']], + 'magpie-tts': [[_t(),'~0.05× GPU'],[_l(),'0.3–0.8 s'],[_q(),'High'], [_m(),'4–6 GB VRAM']], + 'parakeet-asr': [[_t(),'~200× RT GPU'],[_l(),'<0.3 s'],[_q(),'Parakeet-TDT'],[_m(),'2 GB VRAM']], + 'parakeet-rnnt-nim': [[_t(),'~200× RT GPU'],[_l(),'<0.3 s'],[_q(),'Parakeet-1B'], [_m(),'2 GB VRAM']], }; - const roleIcon = { tts: '', stt: '️', 'stt+tts': '🔄', llm: '' }; + const roleIcon = { tts: '', stt: '', 'stt+tts': '', llm: '' }; grid.innerHTML = containers.map(c => { const st = c.status || 'not_found'; @@ -6605,7 +6609,7 @@ $('dc-refresh-btn')?.addEventListener('click', loadLocalContainers); const btn = card.querySelector('.llm-local-ping'); if (!btn) return; if (connected) { - btn.textContent = '✓ Disconnect'; btn.dataset.action = 'disconnect'; + btn.innerHTML = ' Disconnect'; btn.dataset.action = 'disconnect'; btn.className = 'llm-local-ping ok'; } else { btn.textContent = 'Connect'; btn.dataset.action = 'connect'; @@ -6866,7 +6870,7 @@ function renderHistory() {
- +
`; }).join(''); diff --git a/static/sections/s-settings.html b/static/sections/s-settings.html index 7152fcf..4f2527d 100644 --- a/static/sections/s-settings.html +++ b/static/sections/s-settings.html @@ -8,144 +8,131 @@
-
+ + +

Settings

-

Configure the service URLs you actually use first. Advanced payloads, folders, and keys are tucked away below.

+

Configure the service URLs you use. Advanced payloads, folders, and keys are tucked away below.

-
Local stack
+ Local stack
-
- Quick setup - 1. Check core TTS URLs. - 2. Point STT at Whisper, Parakeet, or the NVIDIA router. - 3. Save settings. + +
+ +

These are the endpoints you change most often. NVIDIA TTS and STT are grouped separately.

+ +
+
+ TTS — Text to Speech + Qwen3 engines: clone, design, custom, streaming · Kokoro FastAPI +
+
+
+ + + Uploaded/cloned WAV voices. Expected: POST /v1/audio/speech. +
+
+ + + Prompt-designed voices and vd_... virtual voices. +
+
+ + + Style over configured speakers such as Ryan, Vivian, Serena. +
+
+ + + Progressive low-latency WAV playback. +
+
+ + + OpenAI-compatible TTS. Built-in voices: af_bella, bf_emma, am_adam… ~300 MB RAM. +
+
+
+ +
+
+ NVIDIA speech stack + router, Magpie TTS, Parakeet ASR, clone NIM +
+
+
+ + + OpenAI-compatible base URL for the NVIDIA speech router. +
+
+ + + Direct Magpie endpoint. Fixed speakers, not WAV cloning. +
+
+ + + Direct Parakeet endpoint for transcription. +
+
+ + + Magpie Zeroshot clone endpoint with audio_prompt. +
+
+ + + Magpie Flow clone endpoint with audio_prompt and transcript. +
+
+
+ +
+
+ STT — Speech to Text + Whisper, faster-whisper, whisper.cpp, Groq, Parakeet, NVIDIA router +
+
+
+ + + Default recognition endpoint. Expected: POST /v1/audio/transcriptions. +
+ + + + +
+
+
+ + + GPU-accelerated Whisper via CTranslate2. ~70× RT · 1.5 GB VRAM. +
+
+ + + Lightweight C++ Whisper server. ~8–15× RT CPU · ~1 GB RAM. +
+
+ +
+ + +
+ Used for Groq Whisper STT and LLM. Free: 2 000 req/day. Fastest cloud transcription. +
+
+
-
-
-
-

Core connections

-

These are the endpoints you change most often. Qwen3 TTS, NVIDIA TTS, and STT are grouped separately.

-
-
-
-
-
- TTS Text to Speech - Qwen3 engines: clone, design, custom, streaming · Kokoro FastAPI -
-
-
- - - Uploaded/cloned WAV voices. Expected: POST /v1/audio/speech. -
-
- - - Prompt-designed voices and vd_... virtual voices. -
-
- - - Style over configured speakers such as Ryan, Vivian, Serena. -
-
- - - Progressive low-latency WAV playback. -
-
- - - OpenAI-compatible TTS. Built-in voices: af_bella, bf_emma, am_adam… ~300 MB RAM · ~0.1× CPU RTF. -
-
-
- -
-
- NVIDIA speech stack - router, Magpie TTS, Parakeet ASR, clone NIM -
-
-
- - - OpenAI-compatible base URL for the NVIDIA speech router. -
-
- - - Direct Magpie endpoint. Fixed speakers, not WAV cloning. -
-
- - - Direct Parakeet endpoint for transcription. -
-
- - - Magpie Zeroshot clone endpoint with audio_prompt. -
-
- - - Magpie Flow clone endpoint with audio_prompt and transcript. -
-
-
- -
-
- STT Speech to Text - Whisper, faster-whisper, whisper.cpp, Groq, Parakeet, NVIDIA router -
-
-
- - - Default recognition endpoint. Expected: POST /v1/audio/transcriptions. -
- - - - -
-
-
- - - GPU-accelerated Whisper via CTranslate2. ~70× RT · 1.5 GB VRAM · POST /v1/audio/transcriptions. -
-
- - - Lightweight C++ Whisper server. ~8–15× RT CPU · ~1 GB RAM · POST /v1/audio/transcriptions. -
-
- -
- - -
- Used for Groq Whisper STT (whisper-large-v3-turbo) and Groq LLM. Free: 2 000 req/day. Fastest cloud transcription. -
-
-
-
-
- -
-
-
-

Playback behavior

-

Small behavior switches for previews and OpenAI-compatible TTS calls.

-
-
+ +
+
@@ -167,16 +154,19 @@ Controls payload shape for the normal TTS API URL.
-
+
-
+ +
- Advanced request payloads + + Advanced request payloads JSON extras sent to each backend + -
+
- + Extra fields for the 8020 WAV voice clone/base model.
@@ -198,12 +188,12 @@
- Usually empty. Magpie accepts fixed speaker voices such as sofia, aria, jason, leo, and john. + Usually empty. Magpie accepts fixed speakers: sofia, aria, jason, leo, john.
- Optional multipart fields. The app supplies text, language, and audio_prompt. + Optional multipart fields. The app supplies text, language, audio_prompt.
@@ -213,17 +203,20 @@
- Extra JSON fields for the Kokoro backend. Usually empty — voice is selected from the voice dropdown. + Extra JSON fields for Kokoro. Usually empty — voice is selected from the dropdown.
-
+ +
- Voice folders + + Voice folders container paths and Portainer volume mounts + -
+
@@ -237,28 +230,31 @@
-
+ +
- API keys + + API keys usually empty for local containers + -
+
- +
- +
- +
@@ -267,22 +263,25 @@
+
-
-

Backup & restore

-

Export all voices and settings as a ZIP for backup or migration. Import to restore. API keys are excluded from exports.

-
-
- Export voices - - + +
+ +

Export all voices and settings as a ZIP for backup or migration. Import to restore. API keys are excluded from exports.

+
+ Export voices + + +
+
diff --git a/static/sections/s-studio.html b/static/sections/s-studio.html index 29dddb2..e51d2d4 100644 --- a/static/sections/s-studio.html +++ b/static/sections/s-studio.html @@ -1,5 +1,5 @@
- 🌐 +

Get Voices Online

Browse public voice clip sources, preview direct audio files, and import voices from the web.

@@ -61,7 +61,7 @@
- + diff --git a/static/sections/s-tryout.html b/static/sections/s-tryout.html index 79f0d24..5502e6c 100644 --- a/static/sections/s-tryout.html +++ b/static/sections/s-tryout.html @@ -77,7 +77,7 @@ - +
diff --git a/static/style.css b/static/style.css index 7749ead..a730510 100644 --- a/static/style.css +++ b/static/style.css @@ -318,62 +318,75 @@ audio { width: 100%; } @media (max-width: 900px) { .preview-match-panel { grid-template-columns: 1fr; } } /* ── Settings ───────────────────────────────────────────────────────────── */ -#settings-box { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: none; box-sizing: border-box; } -#settings-box h2 { font-size: 17px; font-weight: 700; } -.settings-section { border: 1px solid var(--border); border-radius: 6px; background: rgba(37,99,235,.03); padding: 14px; display: flex; flex-direction: column; gap: 12px; } -.settings-section-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; } -.settings-section-head h3 { font-size: 13px; color: var(--text); margin: 0; text-transform: uppercase; letter-spacing: .07em; } -.settings-section-head p { font-size: 13px; color: var(--subtext); line-height: 1.45; margin: 4px 0 0; max-width: 980px; } -.settings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; } -.settings-grid.three { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } -.settings-actions { padding-top: 12px; margin-top: 2px; border-top: 1px solid var(--border); } -.setup-intro { background: rgba(37,99,235,.06); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; color: var(--subtext); font-size: 13px; line-height: 1.55; } -.setup-intro strong { color: var(--text); } -.s-group { font-size: 14px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .09em; padding-top: 6px; border-top: 1px solid var(--border); } -.s-field { display: flex; flex-direction: column; gap: 5px; } -.s-field label { font-size: 13px; color: var(--subtext); font-weight: 500; } -.s-field input, .s-field select, .s-field textarea { background: var(--panel); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius); padding: 9px 14px; font-size: 14px; } -.s-field textarea { min-height: 76px; resize: vertical; font-family: monospace; } -.s-hint { font-size: 12px; color: var(--subtext); } -.s-key-row { display: flex; gap: 6px; } -.s-key-row input { flex: 1; } -.s-eye-btn { background: var(--panel); border: 1px solid var(--border); color: var(--subtext); border-radius: var(--radius); padding: 0 13px; font-size: 15px; cursor: pointer; flex-shrink: 0; } -.s-eye-btn:hover { color: var(--text); } -.settings-title-row { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; } -.settings-title-row h2 { margin:0; } -.settings-title-row .note { margin-top:4px; max-width:760px; } -.settings-status-pill { border:1px solid var(--border); background:rgba(22,163,74,.10); color:var(--green); border-radius:999px; padding:5px 10px; font-size:12px; font-weight:700; white-space:nowrap; } -.settings-checklist { display:flex; flex-wrap:wrap; align-items:center; gap:8px 14px; padding:10px 12px; } -.settings-checklist strong { margin-right:2px; } -.settings-checklist span { display:inline-flex; align-items:center; min-height:24px; padding:2px 9px; border:1px solid var(--border); border-radius:999px; background:var(--panel); color:var(--subtext); } -.settings-section { padding:12px; gap:10px; } -.settings-section-primary { background:rgba(37,99,235,.04); } -.settings-section-head h3 { font-size:12px; } -.settings-cluster-grid { display:grid; grid-template-columns: repeat(2, minmax(320px,1fr)); gap:12px; } -.settings-cluster { border:1px solid var(--border); border-radius:7px; background:var(--surface); padding:12px; display:flex; flex-direction:column; gap:10px; min-width:0; } -.settings-cluster-head { display:flex; justify-content:space-between; gap:10px; align-items:baseline; border-bottom:1px solid var(--border); padding-bottom:8px; } -.settings-cluster-head strong { color:var(--text); font-size:13px; text-transform:uppercase; letter-spacing:.07em; } -.settings-cluster-head span { color:var(--subtext); font-size:12px; text-align:right; } -.settings-grid.compact { grid-template-columns: repeat(2, minmax(220px,1fr)); gap:10px; } -.settings-behavior-grid { grid-template-columns: minmax(220px,.7fr) minmax(220px,.7fr) minmax(340px,1.1fr); align-items:start; } -.settings-mini-actions { margin-top:2px; gap:8px; } -.settings-mini-actions button { min-height:34px; padding:7px 12px; } -.settings-details { border:1px solid var(--border); border-radius:7px; background:rgba(37,99,235,.025); overflow:hidden; } -.settings-details summary { list-style:none; cursor:pointer; padding:12px 14px; display:flex; align-items:center; justify-content:space-between; gap:12px; color:var(--text); font-weight:800; text-transform:uppercase; letter-spacing:.07em; font-size:12px; } -.settings-details summary::-webkit-details-marker { display:none; } -.settings-details summary::before { content:'+'; width:20px; height:20px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; border:1px solid var(--border); color:var(--accent); margin-right:2px; flex:0 0 auto; } -.settings-details[open] summary::before { content:'-'; } -.settings-details summary span { display:flex; align-items:center; gap:8px; margin-right:auto; } -.settings-details summary small { color:var(--subtext); font-weight:500; text-transform:none; letter-spacing:0; font-size:12px; } -.settings-details > .settings-grid { padding:0 14px 14px; } +#settings-box { display:flex; flex-direction:column; gap:0; width:100%; max-width:none; box-sizing:border-box; padding:0; overflow:hidden; } + +/* Header */ +.s-header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; padding:28px 32px 24px; border-bottom:1px solid var(--border); } +.s-header h2 { font-size:18px; font-weight:700; margin:0 0 5px; } +.s-subtitle { font-size:13px; color:var(--subtext); line-height:1.55; margin:0; max-width:580px; } +.s-stack-badge { display:inline-flex; align-items:center; gap:5px; border:1px solid color-mix(in srgb,var(--green) 40%,transparent); background:rgba(22,163,74,.07); color:var(--green); border-radius:999px; padding:5px 11px; font-size:12px; font-weight:600; white-space:nowrap; flex-shrink:0; margin-top:2px; } + +/* Sections */ +.s-section { padding:26px 32px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:22px; } +.s-section-last { border-bottom:none; } +.s-section-label { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:var(--subtext); } +.s-section-desc { font-size:13px; color:var(--subtext); margin:0; line-height:1.5; } + +/* Field groups within a section */ +.s-group { display:flex; flex-direction:column; gap:14px; padding-top:4px; } +.s-group + .s-group { border-top:1px solid var(--border); padding-top:22px; } +.s-group-head { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; } +.s-group-head strong { font-size:14px; font-weight:600; color:var(--text); } +.s-group-head span { font-size:12px; color:var(--subtext); } + +/* Field grids */ +.settings-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(280px,1fr)); gap:16px; } +.settings-grid.three { grid-template-columns:repeat(auto-fit, minmax(240px,1fr)); } +.settings-grid.compact { grid-template-columns:repeat(2, minmax(220px,1fr)); gap:14px; } +.settings-behavior-grid { grid-template-columns:minmax(220px,.6fr) minmax(220px,.6fr); gap:16px; align-items:start; } + +/* Individual field */ +.s-field { display:flex; flex-direction:column; gap:6px; } +.s-field label { font-size:13px; color:var(--text); font-weight:500; } +.s-label-note { font-weight:400; color:var(--subtext); } +.s-field input, .s-field select, .s-field textarea { background:var(--panel); border:1px solid var(--border); color:var(--text); border-radius:var(--radius); padding:9px 12px; font-size:14px; transition:border-color .15s,box-shadow .15s; width:100%; box-sizing:border-box; } +.s-field input:focus, .s-field select:focus, .s-field textarea:focus { border-color:var(--accent); outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent); } +.s-field textarea { min-height:76px; resize:vertical; font-family:monospace; } +.s-hint { font-size:12px; color:var(--subtext); line-height:1.5; } +.s-key-row { display:flex; gap:6px; } +.s-key-row input { flex:1; } +.s-eye-btn { background:var(--panel); border:1px solid var(--border); color:var(--subtext); border-radius:var(--radius); padding:0 13px; font-size:15px; cursor:pointer; flex-shrink:0; transition:color .15s; } +.s-eye-btn:hover { color:var(--text); } + +/* STT layout */ +.stt-settings-grid { grid-template-columns:minmax(320px,.65fr) minmax(240px,1fr); } +.settings-mini-actions { margin-top:4px; gap:8px; flex-wrap:wrap; } +.settings-mini-actions button { min-height:32px; padding:6px 11px; font-size:12px; } + +/* Expandable details */ +.s-details { border-bottom:1px solid var(--border); } +.s-details summary { list-style:none; cursor:pointer; padding:16px 32px; display:flex; align-items:center; gap:10px; color:var(--text); font-size:14px; font-weight:600; user-select:none; transition:background .15s; } +.s-details summary:hover { background:rgba(0,0,0,.02); } +.s-details summary::-webkit-details-marker { display:none; } +.s-details-title { flex:1; } +.s-details summary small { color:var(--subtext); font-weight:400; font-size:13px; } +.s-details-chevron { color:var(--subtext); font-size:18px; transition:transform .2s; flex-shrink:0; } +.s-details[open] .s-details-chevron { transform:rotate(180deg); } +.s-details-body { padding:4px 32px 24px; } .settings-param-grid textarea { min-height:58px; } -.settings-actions { display:flex; gap:10px; align-items:center; padding:12px 0 0; } + +/* Actions row */ +.settings-actions { display:flex; gap:10px; align-items:center; padding:20px 32px; } .settings-actions button { min-width:128px; } -.settings-cluster-stt { grid-column: 1 / -1; } -.stt-settings-grid { grid-template-columns: minmax(320px, .65fr) minmax(240px, 1fr); } -.stt-settings-grid .stt-url-field { max-width: 720px; } -@media (max-width: 1100px) { .settings-cluster-grid { grid-template-columns:1fr; } .settings-behavior-grid { grid-template-columns:1fr; } } -@media (max-width: 900px) { .stt-settings-grid { grid-template-columns: 1fr; } .stt-settings-grid .stt-url-field { max-width: none; } } + +/* Responsive */ +@media (max-width:1100px) { .settings-grid.compact { grid-template-columns:1fr; } .settings-behavior-grid { grid-template-columns:1fr; } } +@media (max-width:900px) { + .stt-settings-grid { grid-template-columns:1fr; } + .s-header, .s-section, .settings-actions { padding-left:18px; padding-right:18px; } + .s-details summary { padding-left:18px; padding-right:18px; } + .s-details-body { padding-left:18px; padding-right:18px; } +} /* ── Toast ──────────────────────────────────────────────────────────────── */ #toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 14px 20px; border-radius: var(--radius); font-size: 14px; opacity: 0; transition: opacity .3s; pointer-events: none; z-index: 200; max-width: 380px; box-shadow: var(--shadow); }